We're always interested in getting feedback. E-mail us if you like this guide, if you think that important material is omitted, if you encounter errors in the code examples or in the documentation, if you find any typos, or generally just if you feel like e-mailing. Send your email to Frank Brokken.Please state the document version you're referring to, as found in the title (in this document: 4.4.2).
In contrast to the set of functions which handle memory allocation in C
(i.e., malloc()
etc.), the operators new
and delete
are
specifically meant to be used with the features that C++ offers.
Important differences between malloc()
and new
are:
malloc()
doesn't `know' what the allocated memory
will be used for. E.g., when memory for int
s is allocated, the programmer
must supply the correct expression using a multiplication by
sizeof(int)
. In contrast, new
requires the use of a type; the
sizeof
expression is implicitly handled by the compiler.
malloc()
is to use calloc()
, which allocates memory and resets it to a given
value. In contrast, new
can call the constructor of an allocated object
where initial actions are defined. This constructor may be supplied with
arguments.
NULL
-returns. In contrast, the new
-operator provides a facility called
a new_handler (cf. section 5.3.3) which can be used instead of
the explicit checks for NULL
-returns.
The relationship between free()
and delete
is analogous: delete
makes sure that when an object is deallocated, a corresponding destructor is
called.
The automatic calling of constructors and destructors when objects are created and destroyed, has a number of consequences which we shall discuss in this chapter. Many problems encountered during C program development are caused by incorrect memory allocation or memory leaks: memory is not allocated, not freed, not initialized, boundaries are overwritten, etc.. C++ does not `magically' solve these problems, but it does provide a number of handy tools.
Unfortunately, the very frequently used str...()
functions, like
strdup()
are all malloc()
based, and should therefore preferably
not be used anymore in C++ programs. Instead, a new set of corresponding
functions, based on the operator new
, are preferred.
For the function strdup()
a comparable function char *strdupnew(char
const *str)
could be developed as follows:
char *strdupnew(char const *str) { return (strcpy(new char [strlen(str) + 1], str)); }Similar functions could be developed for comparable
malloc()
-based
str...()
and other functions.
In this chapter we discuss the following topics:
this
pointer,
Person
as example:
class Person { public: // constructors and destructor Person(); Person(char const *n, char const *a, char const *p); ~Person(); // interface functions void setname(char const *n); void setaddress(char const *a); void setphone(char const *p); char const *getname(void) const; char const *getaddress(void) const; char const *getphone(void) const; private: // data fields char *name; char *address; char *phone; };
In this class the destructor is necessary to prevent that memory,
once allocated for the fields name
, address
and phone
, becomes
unreachable when an object ceases to exist. In the following example a
Person
object is created, after which the data fields are printed. After
this the main()
function stops, which leads to the deallocation of
memory. The destructor of the class is also shown for illustration purposes.
Note that in this example an object of the class Person
is also created
and destroyed using a pointer variable; using the operators new
and
delete
.
Person::~Person() { delete name; delete address; delete phone; } int main() { Person kk("Karel", "Rietveldlaan", "050 542 6044"), *bill = new Person("Bill Clinton", "White House", "09-1-202-142-3045"); printf("%s, %s, %s\n" "%s, %s, %s\n", kk.getname(), kk.getaddress(), kk.getphone(), bill->getname(), bill->getaddress(), bill->getphone()); delete bill; return (0); }
The memory occupied by the object kk
is released automatically
when main()
terminates: the C++ compiler makes sure that the
destructor is called. Note, however, that the object pointed to by bill
is
handled differently. The variable bill
is a pointer; and a pointer
variable is, even in C++, in itself no Person
. Therefore, before
main()
terminates, the memory occupied by the object pointed to by
bill
must be explicitly released; hence the statement delete
bill
. The operator delete
will make sure that the destructor is
called, thereby releasing the three strings of the object.
struct
s or classes
can be directly assigned in
C++ in the same way that struct
s can be assigned in C. The
default action of such an assignment is a straight bytewise copy from one
compound variable to another.
Let us now consider the consequences of this default action in a program statement such as the following:
void printperson(Person const &p) { Person tmp; tmp = p; printf("Name: %s\n" "Address: %s\n" "Phone: %s\n", tmp.getname(), tmp.getaddress(), tmp.getphone()); }
We shall follow the execution of this function step by step.
printperson()
expects a reference to a
Person
as its parameter p
. So far, nothing extraordinary is
happening.
tmp
. This means that the
default constructor of Person
is called, which -if defined properly-
resets the pointer fields name
, address
and phone
of the
tmp
object to zero.
p
is copied to tmp
. By
default this means that sizeof(Person)
bytes from p
are copied
to tmp
.
Now a potentially dangerous situation has arisen. Note that the actual
values in p
are pointers, pointing to allocated memory.
Following the assignment this memory is addressed by two objects: p
and tmp
.
printperson()
terminates:
the object tmp
is destroyed. The destructor of the class Person
releases the memory pointed to by the fields name
, address
and
phone
: unfortunately, this memory is also in use by p
....
The incorrect assignment is illustrated in figure 3.
Having executed printperson()
, the object which was
referenced by p
now contain pointers to deallocated memory.
This action is undoubtedly not a desired
effect of a function like the above. The deallocated memory will likely become
occupied during subsequent allocations: the pointer members of p
have
effectively become wild pointers, as they don't point to allocated memory
anymore.
In general it can be concluded that every class containing pointer data members is a potential candidate for trouble. It is of course possible to prevent such troubles, as will be discussed in the next section.
Person
object to another, is
not to copy the contents of the object bytewise. A better way is to
make an equivalent object; one with its own allocated memory, but which
contains the same strings.
The `right' way to duplicate a Person
object is illustrated in
figure 4.
There is a number of solutions for the above wish. One solution consists of
the definition of a special function to handle assignments of objects of the
class Person
. The purpose of this function would be to create a copy of
an object, but one with its own name
, address
and phone
strings. Such a member function might be:
void Person::assign(Person const &other) { // delete our own previously used memory delete name; delete address; delete phone; // now copy the other Person's data name = strdupnew(other.name); address = strdupnew(other.address); phone = strdupnew(other.phone); }
Using this tool we could rewrite the offending function printperson()
:
void printperson(Person const &p) { Person tmp; // make tmp a copy of p, but with its own allocated // strings tmp.assign(p); printf("Name: %s\n" "Address: %s\n" "Phone: %s\n", tmp.getname(), tmp.getaddress(), tmp.getphone()); // now it doesn't matter that tmp gets destroyed.. }
In itself this solution is valid, although it is a purely symptomatic solution.
This
solution requires that the programmer uses a specific member function instead
of the operator =
. The problem, however, remains if this rule is not
strictly adhered to. Experience learns that errare humanum est: a
solution which doesn't enforce exceptions is therefore preferable.
The problem of the assignment operator is solved by means of operator
overloading: the syntactic possibility C++ offers
to redefine the actions of
an operator in a given context. Operator overloading was mentioned earlier,
when the operators <<
and >>
were redefined for the
usage with streams as cin
, cout
and cerr
(see section
3.1.2).
Overloading the assignment operator is probably the most common form of operator overloading. However, a word of warning is appropriate: the fact that C++ allows operator overloading does not mean that this feature should be used at all times. A few rules are:
Person
.
+
for a class which represents a complex
number. The meaning of a +
between two complex numbers is quite
clear and unambiguous.
Using these rules, operator overloading is minimized which helps keep source
files readable. An operator simply does what it is designed to do. Therefore,
in our vision, the operators insertion (<<
) and extraction (>>
)
operators in the context of streams are unfortunate: the stream
operations do not have anything in common with the bitwise shift operations.
6.2.1.1: The function 'operator=()'
To achieve operator overloading in the context of a class, the class is simply
expanded with a public
function stating the particular operator. A
corresponding function, the implementation of the overloaded operator,
is thereupon defined.
For example, to overload the addition operator +
, a function
operator+()
must be defined. The function name consists of two parts:
the keyword operator
, followed by the operator itself.
In our case we define a new function operator=()
to redefine the actions
of the assignment operator. A possible extension to the class Person
could therefore be:
// new declaration of the class class Person { public: ... void operator=(Person const &other); ... private: ... }; // definition of the function operator=() void Person::operator=(Person const &other) { // deallocate old data delete name; delete address; delete phone; // make duplicates of other's data name = strdupnew(other.name); address = strdupnew(other.address); phone = strdupnew(other.phone); }
The function operator=()
presented here is the first version of
the overloaded assignment operator.
We shall present better and less bug-prone versions shortly.
The actions of this member function are similar to those of the previously
proposed function assign()
, but now its name makes sure that this
function is
also activated when the assignment operator =
is used. There are actually
two ways to call this function, as illustrated below:
Person pers("Frank", "Oostumerweg 17", "403 2223"), copy; // first possibility copy = pers; // second possibility copy.operator=(pers);
It is obvious that the second possibility, in which operator=()
is
explicitly stated, is not used often. However, the code fragment does
illustrate the two ways of calling the same function.
this
, to address
this substrate (Note that `this
' is not available in the not yet
discussed static
member functions.)
The this
keyword is a pointer
variable, which always contains the address of the object in question. The
this
pointer is implicitly declared in each member function (whether
public
or private
). Therefore, it is as if in each member function of
the class Person
would contain the following declaration:
extern Person *this;
A member function like setname()
, which sets a name
field of a
Person
to a given string, could therefore be implemented in two ways:
with or without the this
pointer:
// alternative 1: implicit usage of this void Person::setname(char const *n) { delete name; name = strdupnew(n); } // alternative 2: explicit usage of this void Person::setname(char const *n) { delete this->name; this->name = strdupnew(n); }
Explicit usage of the this
pointer is not used very frequently.
However, there exist a number of situations where the this
pointer is
really needed.
=
can be redefined for the class
Person
in such a way that two objects of the class can be assigned,
leading to two copies of the same object.
As long as the two variables are different ones, the previously presented
version of the function operator=()
will behave properly: the memory of
the assigned object is released, after which it is allocated again to hold new
strings. However, when an object is assigned to itself (which is called
auto-assignment), a problem occurs: the allocated strings of the receiving
object are
first released, but this also leads to the release of the strings of the
right-hand side variable, which we call self-destruction.
An example of this situation is illustrated below:
void fubar(Person const &p) { p = p; // auto-assignment! }
In this example it is perfectly clear that something unnecessary, possibly even wrong, is happening. But auto-assignment can also occur in more hidden forms:
Person one, two, *pp; pp = &one; ... *pp = two; ... one = *pp;
The problem of the auto-assignment can be solved using the this
pointer. In the overloaded assignment operator function we simply test whether
the address of the right-hand side object is the same as the address of the
current object: if so, no action needs to be taken. The definition of the
function operator=()
then becomes:
void Person::operator=(Person const &other) { // only take action if address of current object // (this) is NOT equal to address of other // object(&other): if (this != &other) { delete name; delete address; delete phone; name = strdupnew(other.name); address = strdupnew(other.address); phone = strdupnew(other.phone); } }
This is the second version of the overloaded assignment function. One, yet better version remains to be discussed.
As a subtlety, note the usage of the address operator '&
'
in the statement
if (this != &other)
The variable this
is a pointer to the `current' object, while other
is a reference; which is an `alias' to an actual Person
object. The
address of the other object is therefore &other
, while the address of
the current object is this
.
a = b = c;
the expression b = c
is evaluated first, and the result is assigned to
a
.
The implementation of the overloaded assignment operator so far does not
permit such constructions, as an assignment using the member function returns
nothing (void
). We can therefore conclude that the previous
implementation does circumvent an allocation problem, but is
syntactically not quite right.
The syntactical problem can be illustrated as follows. When we rewrite the
expression a = b = c
to the form which explicitly
mentions the overloaded assignment member functions, we get:
a.operator=(b.operator=(c));
This variant is syntactically wrong, since the sub-expression
b.operator=(c)
yields void
; and the class Person
contains no member functions with
the prototype operator=(void)
.
This problem can also be remedied using the this
pointer. The
overloaded assignment function expects as its argument a reference to a
Person
object. It can also return a reference to such an
object. This reference can then be used as an argument for a nested
assignment.
It is customary to let the overloaded assignment return a reference to the
current object (i.e., *this
), as a const
reference: the receiver
is not supposed to alter the *this
object.
The (final)
version of the overloaded assignment operator for the class Person
thus
becomes:
// declaration in the class class Person { public: ... Person const &operator=(Person const &other) ... }; // definition of the function Person const &Person::operator=(Person const &other) { // only take action when no auto-assignment occurs if (this != &other) { // deallocate own data delete address; delete name; delete phone; // duplicate other's data address = strdupnew(other.address); name = strdupnew(other.name); phone = strdupnew(other.phone); } // return current object, compiler will make sure // that a const reference is returned return (*this); }
=
. For this, we shall use a class String
. This class is
meant to handle allocated strings, and its interface is as follows:
class String { public: // constructors, destructor String(); String(char const *s); ~String(); // overloaded assignment String const &operator=(String const &other); // interface functions void set(char const *data); char const *get(void); private: // one data field: ptr to allocated string char *str; };
Concerning this interface we remark the following:
char *str
, possibly pointing to
allocated memory. Consequently, the class needs a constructor and a
destructor.
A typical action of the constructor would be to set the str
pointer to 0. A typical action of the destructor would be to release the
allocated memory.
String const &String::operator=(String const &other) { if (this != &other) { delete str; str = strdupnew(other.str); } return (*this); }
String a("Hello World!\n");
Now let's consider the following code fragment. The statement references are discussed following the example:
String a("Hello World\n"), // see (1) b, // see (2) c = a; // see (3) int main() { b = c; // see (4) return (0); }
a
is
initialized with a string ``Hello World''. This construction of the object
a
therefore uses the constructor which expects one string argument.
It should be noted here that this form is identical to
String a = "Hello World\n";Even though this piece of code uses the operator
=
, this is no
assignment: rather, it is an initialization, and hence, it's
done at construction time by a constructor of the class String
.
String
object is created. Again a
constructor is called. As no special arguments are present,
the default constructor is used.
c
is created. A constructor
is therefore called once more.
The new object is also initialized. This time with a copy of the data of
object a
.
This form of initializations has not yet been discussed. As we can rewrite this statement in the form
String c(a);it suggests that a constructor is called, with as argument a (reference to a)
String
object. Such constructors are
quite common in C++ and are called copy constructors. More
properties of these constructors are discussed below.
The simple rule emanating from these examples is that whenever an object is created, a constructor is needed. All constructors have the following characteristics:
=
.
Therefore, we conclude that, given the above statement (3), the class
String
must be rewritten to define a copy constructor:
// class definition class String { public: ... String(String const &other); ... }; // constructor definition String::String(String const &other) { str = strdupnew(other.str); }
The actions of copy constructors are comparable to those of the overloaded assignment operators: an object is duplicated, so that it contains its own allocated data. The copy constructor function, however, is simpler in the following respect:
Besides the above mentioned quite obvious usage of the copy constructor, the copy constructor has other important tasks. All of these tasks are related to the fact that the copy constructor is always called when an object is created and initialized with another object of its class. The copy constructor is called even when this new object is a hidden or temporary variable.
This is illustrated in the following code fragment:
void func(String s) // no pointer, no reference { // but the String itself puts(s.get()); } int main() { String hi("hello world"); func(hi); return (0); }In this code fragment
hi
itself is not passed as an argument, but
instead a
temporary(stack) variable is created using the copy constructor. This
temporary variable is known within func()
as s
. Note that if
func()
would have been defined using a reference argument,
extra stack usage and a
call to the copy constructor would have been avoided.
This situation occurs when, e.g., a function returns keyboard input in a
String
format:
String getline() { char buf [100]; // buffer for kbd input gets(buf); // read buffer String ret = buf; // convert to String return(ret); // and return it }A hidden
String
object is here initialized with the return value
ret
(using the copy constructor) and is returned by the function. The
local variable ret
itself ceases to exist when getline()
terminates.
To demonstrate that copy constructors are not called in all situations,
consider the following. We could rewrite the above function getline()
to
the following form:
String getline() { char buf [100]; // buffer for kbd input gets(buf); // read buffer return (buf); // and return it }This code fragment is quite valid, even though the return value
char *
doesn't match the prototype String
. In this situation, C++
will try to convert the char *
to a String
. It can do so given a
constructor expecting a char *
argument. This means that the copy
constructor is not used in this version of getline()
. Instead, the
constructor expecting a char *
argument is used.
Contrary to the situation we encountered with the default constructor, the default copy constructor remains available once a constructor (any constructor) is defined explicitly. The copy constructor can be redefined, but it will not disappear once another constructor is defined.
The two above actions (duplication and deallocation) can be coded in two
private functions, say copy()
and destroy()
, which are used in the
overloaded assignment operator, the copy constructor, and the destructor. When
we apply this method to the class Person
, we can rewrite the code as
follows.
First, the class definition is expanded with two private
functions
copy()
and destroy()
. The purpose of these functions is to
copy the data of another object or to deallocate the
memory of the current object unconditionally.
Hence these functions implement `primitive' functionality:
// class definition, only relevant functions are shown here class Person { public: // constructors, destructor Person(Person const &other); ~Person(); // overloaded assignment Person const &operator=(Person const &other); private: // data fields char *name, *address, *phone; // the two primitives void copy(Person const &other); void destroy(void); };
Next, we present the implementations of the functions copy()
and
destroy()
:
// copy(): unconditionally copy other object's data void Person::copy(Person const &other) { name = strdupnew(other.name); address = strdupnew(other.address); phone = strdupnew(other.phone); } // destroy(): unconditionally deallocate data void Person::destroy () { delete name; delete address; delete phone; }
Finally the three public
functions in which other object's memory is
copied or in which memory is deallocated are rewritten:
// copy constructor Person::Person (Person const &other) { // unconditionally copy other's data copy(other); } // destructor Person::~Person() { // unconditionally deallocate destroy(); } // overloaded assignment Person const &Person::operator=(Person const &other) { // only take action if no auto-assignment if (this != &other) { destroy(); copy(other); } // return (reference to) current object for // chain-assignments return (*this); }
What we like about this approach is that the destructor, copy constructor and
overloaded assignment functions are completely standard: they are independent
of a particular class, and their implementations
can therefore be used in every class.
Any class dependencies are reduced to the implementations of the private
member functions copy()
and destroy()
.
The conclusion is therefore: as soon as a class is defined in which pointer data-members are used, a destructor, an overloaded assignment function and a copy constructor should be implemented.