Additional code snippets and screen dumps are shown in this appendix. For
best understanding of the material presented, read the rest of the text and
follow the hyperlinks that lead here. Note that the SimpleString
in these examples is the corrected version provided
in this appendix. The corrected version does not cause dangling
references.
SimpleStringThe corrected SimpleString class is used in subsequent examples
in this appendix. See article three in the series for a detailed discussion of
how SimpleString was fixed.
The simplestring.h file.
//*** SIMPLESTRING DECLARATION ***
#ifndef EXAMPLE_SIMPLE_STRING
#define EXAMPLE_SIMPLE_STRING
class SimpleString {
public:
explicit SimpleString(char* data = ""); //Use 'explicit' keyword to disable
//automatic type conversions --
//generally a good idea.
//Copy constructor and assignment operator.
SimpleString(const SimpleString& original);
SimpleString& operator=(const SimpleString& right_hand_side);
virtual ~SimpleString(); //Virtual destructor, in case someone inherits
//from this class.
virtual const char* to_cstr() const; //Get a read-only C string.
//Many other methods are needed to create a complete string class.
//This example implements only a tiny subset of these, in order
//to keep the discussion focused.
//N.B. no 'inline' methods -- add inlining later, if needed for
//optimization.
private:
char* data_p_; //Distinguish private class members: a trailing underscore
//in the name is one common method.
};
#endif
//*** END: SIMPLESTRING DECLARATION ***
The simplestring.cpp file.
//*** SIMPLESTRING IMPLEMENTATION ***
#include <cstring>
#include "simplestring.h"
using namespace std;
//Constructor
SimpleString::SimpleString(char* data_p) :
data_p_(new char[strlen(data_p)+1]) {
strcpy(data_p_,data_p);
}
//Copy constructor.
SimpleString::SimpleString(const SimpleString& original) :
data_p_(new char[strlen(original.data_p_)+1]) {
strcpy(data_p_,original.data_p_);
}
//Assignment operator.
SimpleString&
SimpleString::operator=(const SimpleString& right_hand_side) {
//It is possible for the caller to request assignment to self
//(i.e. "a = a;"). Do nothing in this case, or a serious
//error will result.
if (this != &right_hand_side) {
//Allocate a new buffer first. If this fails (i.e. throws
//an exception), everything is still consistent.
char* data_p = new char[strlen(right_hand_side.data_p_)+1];
//Now, delete the old buffer, and start using the new one.
delete [] data_p_; //(1)
data_p_ = data_p; //(2)
//Copy the data over from the right hand side. We checked
//before that this is not self assignment, so we are safe.
//Otherwise, we would have already destroyed this data
//in statements (1) and (2)!
strcpy(data_p_,right_hand_side.data_p_);
}
//This allows assignments to be chained (i.e. "a = b = c = d;").
return *this;
}
//Destructor
SimpleString::~SimpleString() {
//N.B. Use of 'delete []' corresponds to previous use of 'new []'.
// Using just 'delete' here would be a disaster.
delete [] data_p_;
}
//Returns a read-only C string representation.
const char* SimpleString::to_cstr() const {
return data_p_;
}
//*** END: SIMPLESTRING IMPLEMENTATION ***
//---
class Base {
public:
Base();
Base(const Base& original);
virtual ~Base(); //N.B. virtual, so derived classes can override.
void scare_me(); //Prints out a very scary message.
void virtual example();
private:
SimpleString scary_msg_;
SimpleString* base_data_p_;
//See the Training Wheels Class
//for an explanation of this declaration.
Base& operator=(const Base& right_hand_side);
};
Base::Base() :
scary_msg_("Now formatting your hard drive ... just kidding!") {
base_data_p_ = new SimpleString("Base Data");
}
//A proper copy constructor.
Base::Base(const Base& original) :
scary_msg_("Now formatting your hard drive ... just kidding!") {
base_data_p_ = new SimpleString(*original.base_data_p_);
}
Base::~Base() {
cout << "Deleting Base Data" << endl;
delete base_data_p_;
}
void Base::scare_me() {
cout << scary_msg_.to_cstr() << endl;
}
void Base::example() {
cout << base_data_p_->to_cstr() << endl;
}
//---
//---
class Derived : public Base {
public:
Derived();
Derived(const Derived& original);
virtual ~Derived();
void virtual example();
private:
SimpleString* derived_data_p_;
//See the Training Wheels Class
//for an explanation of this declaration.
Derived& operator=(const Derived& right_hand_side);
};
Derived::Derived() {
derived_data_p_ = new SimpleString("Derived Data");
}
//A proper copy constructor for a derived class.
Derived::Derived(const Derived& original) : Base(original) {
derived_data_p_ = new SimpleString(*original.derived_data_p_);
}
Derived::~Derived() {
cout << "Deleting Derived Data, ";
delete derived_data_p_;
}
void Derived::example() {
cout << derived_data_p_->to_cstr() << ", ";
Base::example(); //Specify the Base version of the virtual method.
}
//---
Calling 'the_slicer' on a Base
Base Data
Deleting Base Data
Calling 'the_slicer' on a Derived
Base Data
Deleting Base Data
You can see the derived object being sliced in this output — the function being called takes its argument by value. The local copies of the argument are destroyed when the function exits, which accounts for the destructor calls.
//---
class NVDBase {
public:
NVDBase();
~NVDBase(); //N.B. Non-virtual.
private:
SimpleString* base_data_p_;
//See the Training Wheels Class
//for an explanation of these declarations.
NVDBase(const NVDBase& original);
NVDBase& operator=(const NVDBase& right_hand_side);
};
NVDBase::NVDBase() {
base_data_p_ = new SimpleString("Base Data");
}
NVDBase::~NVDBase() {
cout << "Deleting Base Data" << endl << endl;
delete base_data_p_;
}
class NVDDerived : public NVDBase {
public:
NVDDerived();
~NVDDerived();
private:
SimpleString* derived_data_;
//See the Training Wheels Class
//for an explanation of these declarations.
NVDDerived(const NVDDerived& original);
NVDDerived& operator=(const NVDDerived& right_hand_side);
};
NVDDerived::NVDDerived() {
derived_data_ = new SimpleString("Derived Data");
}
NVDDerived::~NVDDerived() {
cout << "Deleting Derived Data" << endl;
delete derived_data_;
}
//---
Deleting Base Data
Deleting Derived Data
Deleting Base Data
Deleting Base Data
Base Data
Base Data
Base Data
Derived Data, Base Data
Segmentation fault
When an array of Base is used, everything works. Try this with
an array of Derived, however, and there is a serious problem after
the first object.
//---
class NotDerived {
public:
NotDerived();
virtual ~NotDerived();
void soothe_me();
private:
SimpleString lullaby_;
//See the Training Wheels Class
//for an explanation of these declarations.
NotDerived(const NotDerived& original);
NotDerived& operator=(const NotDerived& right_hand_side);
};
NotDerived::NotDerived() : lullaby_("Rock-a-bye-baby, etc., etc.") {}
NotDerived::~NotDerived() {}
void NotDerived::soothe_me() {
cout << lullaby_.to_cstr() << endl;
}
//---
Value of 'wild_p' is now: 0xbfffd830
Now formatting your hard drive ... just kidding!
Value of 'wild_p' is now: 0x0
Segmentation fault
one two
Segmentation fault
Only the initial printout of the two strings (before the function call) succeeds; any attempt to use the bitwise-copied objects causes a crash.
I am a local object!
Segmentation fault
Deleting Derived Data, Deleting Base Data
This output shows what happens if someone tries to create a CtorThrow object. When the exception leaves the constructor, the object is not yet fully constructed, so the destructor is not invoked. Hence, the object pointed to by first_p_ —
which has already been dynamically allocated — is leaked. The
member_obj_, however, is a fully constructed member object; so it
is properly destructed (you can see that both the base and derived parts of
member_obj_ are handled correctly).
//---
class DtorThrow {
public:
DtorThrow();
~DtorThrow(); //N.B. non-virtual,
//not meant for subclassing.
private:
//See the Training Wheels Class
//for an explanation of these declarations.
DtorThrow(const DtorThrow&);
DtorThrow& operator=(const DtorThrow&);
};
DtorThrow::DtorThrow() {}
DtorThrow::~DtorThrow() {
//Could also call a function/method that throws.
throw SimpleString("Exception!");
}
//---
Only one exception ...
Deleting Derived Data, Deleting Base Data
Deleting Base Data
Caught an exception
Exception during another exception ...
Aborted
Deleting Derived Data, Deleting Base Data (1)
Derived Data, Base Data (2)
Deleting Derived Data, Deleting Base Data (3)
Base Data (4)
Deleting Base Data (5)
Throwing a Derived; throwing always causes a copy, and the
original is later deleted.
The catch calls Derived's overloaded virtual
example method — correct.
The improper rethrow results in an additional copy of type
Base; the Derived object is later deleted.
The outer try block's catch gets the sliced
object. Only the Base is left, so its example method
is called.
The Base object is cleaned up.
Deleting Derived Data, Deleting Base Data (1)
caught a Base
Base Data (2)
Deleting Base Data (3)
Deleting Derived Data, Deleting Base Data (4)
Throwing a Derived; throwing always causes a copy, and the
original is later deleted.
The Base catch clause is invoked, because it
is ahead of the Derived catch clause. The exception object is
also sliced, due to catch-by-value.
The Base class object (likewise resulting from the
catch-by-value) is destroyed.
The Derived object (which was actually thrown, and is still
around) is destroyed.