Don't hesitate to send in feedback: send an e-mail if you like the C++ Annotations; if you think that important material was omitted; if you find errors or typos in the text or the code examples; or if you just feel like e-mailing. Send your e-mail to Frank B. Brokken.Please state the document version you're referring to, as found in the title (in this document: 7.2.2) and please state chapter and paragraph name or number you're referring to.
All received mail is processed conscientiously, and received suggestions for improvements will usually have been processed by the time a new version of the Annotations is released. Except for the incidental case I will normally not acknowledge the receipt of suggestions for improvements. Please don't interpret this as me not appreciating your efforts.
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 7.2.2) which can be used instead
of explicitly checking for 0 return values.
free()
and delete
:
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. Also, since the class
string
is available, there is less need for these functions in C++
than in C. In cases where operations on char *
are preferred or
necessary, comparable functions based on new
could be developed. E.g.,
for the function strdup()
a comparable function
char *strdupnew(char const *str)
could be developed as
follows:
char *strdupnew(char const *str) { return str ? strcpy(new char [strlen(str) + 1], str) : 0; }In this chapter the following topics will be covered:
this
pointer,
new
and
delete
.
The most basic example of the use of these operators is given below. An
int
pointer variable is used to point to memory which is allocated by the
operator new
. This memory is later released by the operator delete
.
int *ip; ip = new int; delete ip;Notes:
new
and delete
are operators and therefore do not
require parentheses, as required for functions like malloc()
and
free()
;
new
returns a pointer to the kind of memory that's
asked for by its argument (e.g., a pointer to an int
in the above
example);
new
uses a type as its operand, which has the important
benefit that the correct amount of memory, given the type of the object to be
allocated, becomes automatically available;
new
is a
type safe operator as it always
returns a pointer to the type that was given as its operand, which pointer
must match the type of the variable receiving the pointervalue;
new
may fail, this is normally no concern to the
programmer. In particular, the program does not have to test the success
of the memory allocation, as is required when using malloc()
and
friends. Section 7.2.2 delves into this aspect of new
;
delete
returns void
;
new
a matching delete
should eventually be
executed, lest a
memory leak occurs;
delete
can safely operate on a
0-pointer (in which case
nothing happens);
delete
should only be used to return memory allocated
by new
. It should not be used to return memory allocated by
malloc()
and friends.
The operator new
can be used to
allocate primitive types and to
allocate objects. When a non-class type is allocated (a primitive type
or a struct
type without a constructor), the allocated memory is not
guaranteed to be initialized to 0. Alternatively, an
initialization
expression may be provided:
int *v1 = new int; // not guaranteed to be initialized to 0 int *v1 = new int(); // initialized to 0 int *v2 = new int(3); // initialized to 3 int *v3 = new int(3 * *v2); // initialized to 9When class-type objects are allocated, the constructor must be mentioned, and the allocated memory will be initialized according to the constructor that is used. For example, to allocate a
string
object the following statement
can be used:
string *s = new string();Here, the default constructor was used, and
s
will point to the newly
allocated, but empty, string
. If overloaded forms of the constructor are
available, these can be used as well. E.g.,
string *s = new string("hello world");which results in
s
pointing to a string
containing the text
hello world
.
new[]
is used to
allocate arrays. The generic
notation
new[]
is an abbreviation used in the Annotations. Actually, the number of
elements to be allocated is specified as an expression between the square
brackets, which are prefixed by the type of the values or class of the
objects that must be allocated:
int *intarr = new int[20]; // allocates 20 intsNote well that
operator new
is a different operator than operator
new[]
. In section 9.9 redefining operator new[]
is
covered.
Arrays allocated by operator new[]
are called
dynamic arrays.
They are constructed during the execution of a program, and their lifetime may
exceed the lifetime of the function in which they were created. Dynamically
allocated arrays may last for as long as the program runs.
When new[]
is used to allocate an array of primitive values or an
array of objects, new[]
must be
specified with a type and an (unsigned) expression between square
brackets. The type and expression together are used by the compiler to
determine the required size of the block of memory to make available. With the
array allocation, all elements are stored consecutively in memory. The
array index notation can be used to access the individual elements:
intarr[0]
will be the very first int
value, immediately followed by
intarr[1]
, and so on until the last element: intarr[19]
. With
non-class types (primitive types, struct
types without constructors,
pointer types) the returned allocated block of memory is not guaranteed to
be initialized to 0.
To
allocate arrays of objects, the new[]
-bracket notation is used
as well. For example, to allocate an array of 20 string
objects the
following construction is used:
string *strarr = new string[20]; // allocates 20 stringsNote here that, since objects are allocated, constructors are automatically used. So, whereas
new int[20]
results in a block of 20
uninitialized int
values, new string[20]
results in a block of 20
initialized string
objects. With arrays of objects the
default constructor is used for the initialization. Unfortunately it
is not possible to use a constructor having arguments when arrays of objects
are allocated. However, it is possible to overload
operator new[]
and
provide it with arguments which may be used for a non-default initialization
of arrays of objects. Overloading operator new[]
is discussed in section
9.9.
Similar to C, and without resorting to the operator new[]
, arrays
of variable size can also be constructed as
local arrays within
functions. Such arrays are not dynamic arrays, but
local arrays, and
their lifetime is restricted to the lifetime of the block in which they were
defined.
Once allocated, all arrays are
fixed size arrays. There is no simple way to enlarge or
shrink arrays: there is no
renew operator
. In section
7.1.3 an example is given showing how to
enlarge an array.
delete[]
. Operator delete[]
expects a pointer to a block of
memory, previously allocated using operator
new[]
.
When an object is deleted, its destructor (see section 7.2) is called automatically, comparable to the calling of the object's constructor when the object was created. It is the task of the destructor, as discussed in depth later in this chapter, to do all kinds of cleanup operations that are required for the proper destruction of the object.
The operator delete[]
(empty square brackets) expects as its argument
a pointer to an
array of objects. This operator will now first call the
destructors of the individual objects, and will then delete the allocated
block of memory. So, the proper way to delete an array of Objects
is:
Object *op = new Object[10]; delete[] op;Note that
delete[]
only has an additional effect if the block of
memory to be deallocated consists of objects. With pointers or values of
primitive types normally no special action is performed. Following
int *it = new int[10]
the statement delete[] it
the memory
occupied by all ten int
values is returned to the common pool. Nothing
special happens.
Note especially that an
array of pointers to objects
is not handled as an
array of objects
by delete[]
: the array of pointers to objects doesn't
contain objects, so the objects are not properly destroyed by delete[]
,
whereas an array of objects contains objects, which are properly destroyed by
delete[]
. In section 7.2 several examples of the use of
delete
versus delete[]
will be given.
The operator delete
is a different operator than operator
delete[]
. In section 9.9 redefining delete[]
is
discussed. The
rule of thumb is: if
new[]
was used, also use
delete[]
.
renew operator
. In this section an example is given showing how to
enlarge an array. Enlarging arrays is only possible with
dynamic arrays.
Local and global arrays cannot be enlarged. When an array must be enlarged,
the following procedure can be used:
string
objects:
#include <string> using namespace std; string *enlarge(string *old, unsigned oldsize, unsigned newsize) { string *tmp = new string[newsize]; // allocate larger array for (unsigned idx = 0; idx < oldsize; ++idx) tmp[idx] = old[idx]; // copy old to tmp delete[] old; // using [] due to objects return tmp; // return new array } int main() { string *arr = new string[4]; // initially: array of 4 strings arr = enlarge(arr, 4, 6); // enlarge arr to 6 elements. }
operator delete
call for every call
to operator new
, there is a noticeable exception to that rule. It is
called the
placement new
operator.
In this variant of operator new
the operator accepts a block of memory and
initializes it using a constructor of choice. The block of memory should of
course be large enough to contain the object, but apart from that no other
requirements exist.
The placement new
operator uses the following syntax (using Type
to
indicate the data type that is used):
Type *new(void *memory) Type(arguments);Here,
memory
is block of memory of at least sizeof(Type)
bytes
large (usually memory
will point to an array of characters), and
Type(arguments)
is any constructor of the class Type
.
The placement new
operator comes in handy when the memory to contain
one or more objects is already available or when its size is known
beforehand. The memory could have been statically or dynamically allocated;
when allocated dynamically the appropriate destructor must
eventually be called to destroy the objects and the block of memory. When
allocated statically the memory on which the placement new
operator will
operate will eventually be returned automatically.
The memory that is made available to the placement new
operator will
normally be memory containing primitive types.
The question of how to call the destructors of objects initialized using
the placement new
operator is an interesting one:
delete
can of course not be called for the statically allocated memory.
delete
is called for the pointer returned by the
placement new
operator the object's destructor isn't called either. So,
the string
destructor is not called in the following situation:
char *buffer = new char[sizeof(string)]; delete new(buffer) string("hello world");
new
operator: the object's destructors are
not called.
delete
on the pointer returned by the placement new
operator doesn't help: it's still the same memory address as the original
block of characters.
So, how then can the destructors of objects initialized by the
placement new
operator be called? The answer may be surprising: it
must be called
explicitly. Here is an
example:
#include <iostream> using namespace std; class Object { public: Object(); ~Object(); }; inline Object::Object() { cout << "Constructor\n"; }; inline Object::~Object() { cout << "Destructor\n"; }; int main() { char buffer[2 * sizeof(Object)]; Object *obj = new(buffer) Object; // placement new, 1st object new(buffer + sizeof(Object)) Object; // placement new, 2nd object // delete obj; // DON'T DO THIS obj[0].~Object(); // destroy 1st object obj[1].~Object(); // destroy 2nd object } // Displays: // Constructor // Constructor // Destructor // Destructor
It's clearly dangerous to use the placement new
operator: object
destruction is not guaranteed, and since destruction must be performed
manually there's no guarantee that object destruction takes place in the
reverse order of object construction.
If in the above example buffer
would have been allocated dynamically,
a final statement
delete [] buffer;should be added to
main
. It would merely return buffer
's allocated
memory to the common pool, without calling any object destructor
whatsoever. Of course, if the buffer is allocated dynamically the need to
call explicitly the objects destructor and use the delete
operator for
the dynamically allocated block is somewhat overdone. In that case a procedure
like the following can be used to return the memory to the common pool:
Object *obj = new (new char[](sizeof(Object))) Object; delete obj;But then again: the same is realized with a simple
Object *obj = new Object; delete obj;But the placement new operator nicely illustrates what actually happens when an object is allocated dynamically. Using the placement new operator we're `playing memory allocator' ourselves, see also sections 9.7, 9.8 and 9.9.
However, when a program is interrupted using an
exit()
call, the
destructors are called only for global objects existing at that time.
Destructors of objects defined locally within functions are not called
when a program is forcefully terminated using exit()
.
The definition of a destructor must obey the following rules:
Person
is thus declared as follows:
class Person { public: Person(); // constructor ~Person(); // destructor };The position of the constructor(s) and destructor in the class definition is dictated by convention: first the constructors are declared, then the destructor, and only then other members are declared.
The main task of a destructor is to make sure that memory allocated by the
object (e.g., by its constructor) is properly deleted when the object goes out
of scope. Consider the following definition of the class Person
:
class Person { char *d_name; char *d_address; char *d_phone; public: Person(); Person(char const *name, char const *address, char const *phone); ~Person(); char const *name() const; char const *address() const; char const *phone() const; }; inline Person::Person() {} /* person.ih contains: #include "person.h" char const *strdupnew(char const *org); */The task of the constructor is to initialize the data fields of the object. E.g, the constructor is defined as follows:
#include "person.ih" Person::Person(char const *name, char const *address, char const *phone) : d_name(strdupnew(name)), d_address(strdupnew(address)), d_phone(strdupnew(phone)) {}In this class the destructor is necessary to prevent that memory, allocated for the fields
d_name
, d_address
and d_phone
, becomes
unreachable when an object ceases to exist, thus producing a
memory leak. The destructor of an object is called automatically
delete[]
operator (see section 7.1.2).
Person
's
destructor would be to delete the memory to which its three data
members point. The implementation of the destructor would therefore be:
#include "person.ih" Person::~Person() { delete d_name; delete d_address; delete d_phone; }In the following example a
Person
object is created, and its data
fields are printed. After this the showPerson()
function stops, resulting
in the deletion of memory. Note that in this example a second
object of the class Person
is created and destroyed dynamically by
respectively, the operators new
and delete
.
#include "person.h" #include <iostream> void showPerson() { Person karel("Karel", "Marskramerstraat", "038 420 1971"); Person *frank = new Person("Frank", "Oostumerweg", "050 403 2223"); cout << karel.name() << ", " << karel.address() << ", " << karel.phone() << endl << frank->name() << ", " << frank->address() << ", " << frank->phone() << endl; delete frank; }The memory occupied by the object
karel
is deleted automatically when
showPerson()
terminates: the C++ compiler makes sure that the
destructor is called. Note, however, that the object pointed to by frank
is handled differently. The variable frank
is a pointer, and a pointer
variable is itself no Person
. Therefore, before main()
terminates, the
memory occupied by the object pointed to by frank
should be explicitly
deleted; hence the statement delete
frank
. The operator delete
will make sure that the destructor is called, thereby deleting the three
char *
strings of the object.
new
and delete
are used when an object of a given
class is allocated. As we have seen, one of the advantages of
the operators new
and delete
over functions like
malloc()
and
free()
is that new
and delete
call the corresponding
constructors and destructors. This is illustrated in the next example:
Person *pp = new Person(); // ptr to Person object delete pp; // now destroyedThe allocation of a new
Person
object pointed to by pp
is a
two-step process. First, the memory for the object itself is
allocated. Second, the constructor is called, initializing the object. In
the above example the constructor is the argument-free version; it is however
also possible to use a constructor having arguments:
frank = new Person("Frank", "Oostumerweg", "050 403 2223"); delete frank;Note that, analogously to the construction of an object, the destruction is also a two-step process: first, the destructor of the class is called to delete the memory allocated and used by the object; then the memory which is used by the object itself is freed.
Dynamically allocated arrays of objects can also be manipulated by
new
and delete
. In this case the size of the array is given between
the []
when the array is created:
Person *personarray = new Person [10];The compiler will generate code to call the default constructor for each object which is created. As we have seen in section 7.1.2, the
delete[]
operator must be used here to destroy such an array in the
proper way:
delete[] personarray;The presence of the
[]
ensures that the destructor is called for each
object in the array.
What happens if delete
rather than delete[]
is used? Consider the
following situation, in which the destructor ~Person()
is modified so that
it will tell us that it's called. In a main()
function an array of two
Person
objects is allocated by new
, to be deleted by delete
[]
. Next, the same actions are repeated, albeit that the delete
operator
is called without []
:
#include <iostream> #include "person.h" using namespace std; Person::~Person() { cout << "Person destructor called" << endl; } int main() { Person *a = new Person[2]; cout << "Destruction with []'s" << endl; delete[] a; a = new Person[2]; cout << "Destruction without []'s" << endl; delete a; return 0; } /* Generated output: Destruction with []'s Person destructor called Person destructor called Destruction without []'s Person destructor called */Looking at the generated output, we see that the destructors of the individual
Person
objects are called if the delete[]
syntax is
followed, while only the first object's destructor is called if the []
is
omitted.
If no destructor is defined, it is not called. This may seem to be a trivial statement, but it has serious implications: objects which allocate memory will result in a memory leak when no destructor is defined. Consider the following program:
#include <iostream> #include "person.h" using namespace std; Person::~Person() { cout << "Person destructor called" << endl; } int main() { Person **a = new Person* [2]; a[0] = new Person[2]; a[1] = new Person[2]; delete[] a; return 0; }This program produces no output at all. Why is this? The variable
a
is
defined as a
pointer to a pointer. For this situation, however, there
is no defined destructor. Consequently,
the []
is
ignored.
Now, as the []
is ignored, only the array a
itself
is deleted, because here `delete[] a
' deletes the memory pointed to
by a
. That's all there is to it.
Of course, we don't want this, but require the Person
objects pointed
to by the elements of a
to be deleted too. In this case we have two
options:
a
array, deleting them in
turn. This will call the destructor for a pointer to Person
objects, which
will destroy all elements if the []
operator is used, as in:
#include <iostream> #include "person.h" Person::~Person() { cout << "Person destructor called" << endl; } int main() { Person **a = new Person* [2]; a[0] = new Person[2]; a[1] = new Person[2]; for (int index = 0; index < 2; index++) delete[] a[index]; delete[] a; } /* Generated output: Person destructor called Person destructor called Person destructor called Person destructor called */
Person
objects,
and allocate a pointer to this class, rather than a pointer to a pointer to
Person
objects. The topic of containing classes in classes,
composition, was discussed in section 6.4. Here is an
example showing the deletion of pointers to memory using such a wrapper class:
#include <iostream> using namespace std; class Informer { public: ~Informer(); }; inline Informer::~Informer() { cout << "destructor called\n"; } class Wrapper { Informer *d_i; public: Wrapper(); ~Wrapper(); }; inline Wrapper::Wrapper() : d_i(new Informer()) {} inline Wrapper::~Wrapper() { delete d_i; } int main() { delete[] new Informer *[4]; // memory leak: no destructor called cout << "===========\n"; delete[] new Wrapper[4]; // ok: 4 x destructor called } /* Generated output: =========== destructor called destructor called destructor called destructor called */
new
. This default behavior may be
modified in various ways. One way to modify this default behavior is to
redefine the function handling failing memory allocation. However, any
user-defined function must comply with the following prerequisites:
The redefined error function might, e.g., print a message and terminate
the program. The user-written error function becomes part of the allocation
system through the function
set_new_handler()
.
The implementation of an error function is illustrated below ( This implementation applies to the Gnu C/C++ requirements. Actually using the program given in the next example is not advised, as it will slow down the computer enormously due to the resulting use of the operating system's swap area.):
#include <iostream> #include <string> using namespace std; void outOfMemory() { cout << "Memory exhausted. Program terminates." << endl; exit(1); } int main() { long allocated = 0; set_new_handler(outOfMemory); // install error function while (true) // eat up all memory { memset(new int [100000], 0, 100000 * sizeof(int)); allocated += 100000 * sizeof(int); cout << "Allocated " << allocated << " bytes\n"; } }After installing the error function it is automatically invoked when memory allocation fails, and the program exits. Note that memory allocation may fail in indirectly called code as well, e.g., when constructing or using streams or when strings are duplicated by low-level functions.
So far for the theory. On some systems the ` out of memory' condition may actually never be reached, as the operating system may interfere before the run-time sypport system gets a chance to stop the program (see also this link).
Note that it may not be assumed that the standard C functions which
allocate memory, such as
strdup()
,
malloc()
,
realloc()
etc. will
trigger the new
handler when memory allocation fails. This means that once
a new
handler is installed, such functions should not automatically be
used in an unprotected way in a C++ program. An example using
new
to duplicate a string, was given in a rewrite of the function
strdup()
(see section 7).
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 for non-class type data members is a straight
byte-by-byte copy from one data member to another. Now consider the
consequences of this default action in a function such as the following:
void printperson(Person const &p) { Person tmp; tmp = p; cout << "Name: " << tmp.name() << endl << "Address: " << tmp.address() << endl << "Phone: " << tmp.phone() << endl; }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 5.
printperson()
, the object which was referenced by
p
now contains
pointers to deleted memory.
This situation is undoubtedly not a desired effect of a function like the
above. The deleted 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
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 6.
There are several ways to duplicate a Person
object. One way would be
to define a special member function to handle assignments of objects of the
class Person
. The purpose of this member 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 d_name; delete d_address; delete d_phone; // now copy the other Person's data d_name = strdupnew(other.d_name); d_address = strdupnew(other.d_address); d_phone = strdupnew(other.d_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 memory tmp.assign(p); cout << "Name: " << tmp.name() << endl << "Address: " << tmp.address() << endl << "Phone: " << tmp.phone() << endl; // now it doesn't matter that tmp gets destroyed.. }By itself this solution is valid, although it is a purely symptomatic solution. This solution requires the programmer to use a specific member function instead of the operator
=
. The basic problem, however, remains if
this rule is not strictly adhered to. Since it is very hard to `strictly
adhere to a rule' it is higly preferable to have a solution that is solving
the problem for us (rather than putting the burden of avoiding the problem on
our shoulders).
The problem of the assignment operator is solved using 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 to be used with
streams (like 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.
For example, to overload the assignment operator =
, a function
operator=()
must be defined. Note that the function name consists of two
parts: the keyword
operator
, followed by the operator itself. When we
augment a class interface with a member function operator=()
, then that
operator is redefined for the class, which prevents the default operator
from being used. Previously (in section 7.3.1) the function
assign()
was offered to solve the memory-problems resulting from using the
default assignment operator. However, instead of using an ordinary member
function it is much more common in C++ to define a dedicated operator
for these special cases. So, the earlier assign()
member may be redefined
as follows (note that the member operator=()
presented below is a first,
rather unsophisticated, version of the overloaded assignment operator. It
will be improved shortly):
class Person { public: // extension of the class Person // earlier members are assumed. void operator=(Person const &other); };and its implementation could be
void Person::operator=(Person const &other) { delete d_name; // delete old data delete d_address; delete d_phone; d_name = strdupnew(other.d_name); // duplicate other's data d_address = strdupnew(other.d_address); d_phone = strdupnew(other.d_phone); }The actions of this member function are similar to those of the previously proposed function
assign()
, but now its name ensures that this
function is also activated when the assignment operator =
is used. There
are actually two ways to
call overloaded operators:
Person pers("Frank", "Oostumerweg", "403 2223"); Person copy; copy = pers; // first possibility copy.operator=(pers); // second possibilityActually, the second possibility, explicitly calling
operator=()
, is
not used very often. However, the code fragment does illustrate two
ways to call the same overloaded operator member 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, protected
, or private
). Therefore, it
is as if each member function of the class Person
contains the
following declaration:
extern Person *const this;A member function like
name()
, which returns the name
field of
a Person
, could therefore be implemented in two ways: with or without the
this
pointer:
char const *Person::name() // implicit usage of `this' { return d_name; } char const *Person::name() // explicit usage of `this' { return this->d_name; }The
this
pointer is not frequently used explicitly.
However, situations do exist where the this
pointer is actually required
(cf. chapter 15).
=
can be redefined for the class
Person
in such a way that two objects of the class can be assigned,
resulting in 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 deleted, resulting in the deletion of the
memory of the right-hand side variable, which we call
self-destruction.
An example of this situation is illustrated here:
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; Person two; Person *pp = &one; *pp = two; one = *pp;The problem of 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=()
thus becomes:
void Person::operator=(Person const &other) { // only take action if address of the current object // (this) is NOT equal to the address of the other object if (this != &other) { delete d_name; delete d_address; delete d_phone; d_name = strdupnew(other.d_name); d_address = strdupnew(other.d_address); d_phone = strdupnew(other.d_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
.
So far, the implementation of the overloaded assignment operator does not
permit such constructions, as an assignment using the member function returns
nothing (void
). We can therefore conclude that the previous implementation
does solve an allocation problem, but concatenated assignments are still not
allowed.
The 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
. However, the class Person
contains no
member functions with the prototype operator=(void)
.
This problem too can 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 in a
concatenated assignment.
It is customary to let the overloaded assignment return a
reference to the current object (i.e., *this
). The (final) version
of the overloaded assignment operator for the class Person
thus becomes:
Person &Person::operator=(Person const &other) { if (this != &other) { delete d_address; delete d_name; delete d_phone; d_address = strdupnew(other.d_address); d_name = strdupnew(other.d_name); d_phone = strdupnew(other.d_phone); } // return current object. The compiler will make sure // that a reference is returned return *this; }
=
. Consider, once again, the class Person
. The class has the
following characteristics:
A typical action of the constructor would be to set the pointer members to 0. A typical action of the destructor would be to delete the allocated memory.
Person
object.
Person
object.
Person karel("Karel", "Marskramerstraat", "038 420 1971"); // see (1) Person karel2; // see (2) Person karel3 = karel; // see (3) int main() { karel2 = karel3; // see (4) return 0; }
karel
is initialized with appropriate texts. This construction of
karel
therefore uses the constructor expecting three char
const *
arguments.
Assume a Person
constructor is available having only one char const
*
parameter, e.g.,
Person::Person(char const *n);It should be noted that the initialization `
Person frank("Frank")
' is
identical to
Person frank = "Frank";Even though this piece of code uses the operator
=
, it is no
assignment: rather, it is an initialization, and hence, it's done at
construction time by a constructor of the class Person
.
Person
object is created. Again a
constructor is called. As no special arguments are present, the
default constructor is used.
karel3
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 karel
.
This form of initializations has not yet been discussed. As we can rewrite this statement in the form
Person karel3(karel);it is suggested that a constructor is called, having a reference to a
Person
object as its argument. Such constructors are quite common in
C++ and are called
copy constructors.
The simple rule emanating from these examples is that whenever an object is created, a constructor is needed. All constructors have the following characteristics:
Person
must be augmented with a
copy constructor:
class Person { public: Person(Person const &other); };The implementation of the
Person
copy constructor is:
Person::Person(Person const &other) { d_name = strdupnew(other.d_name); d_address = strdupnew(other.d_address); d_phone = strdupnew(other.d_phone); }The actions of copy constructors are comparable to those of the overloaded assignment operators: an object is duplicated, so that it will contain its own allocated data. The copy constructor, however, is simpler in the following respects:
void nameOf(Person p) // no pointer, no reference { // but the Person itself cout << p.name() << endl; } int main() { Person frank("Frank"); nameOf(frank); return 0; }In this code fragment
frank
itself is not passed as an argument, but
instead a temporary (stack) variable is created using the copy
constructor. This temporary variable is known inside nameOf()
as
p
. Note that if nameOf()
would have had a reference parameter, extra
stack usage and a call to the copy constructor would have been avoided.
Person person() { string name; string address; string phone; cout << "Enter name, address, phone of a person: "; cin >> name >> address >> phone; Person p1(name.c_str(), address.c_str(), phone.c_str()); cout << "Enter name, address, phone of another person: "; cin >> name >> address >> phone; Person p2(name.c_str(), address.c_str(), phone.c_str()); cout << "Enter `one' if you want to return the first person: "; string command; cin >> command; return command == "one" ? p1 : p2; // returns a copy of p1 or p2. }Here a hidden object of the class
Person
is initialized using the
copy constructor, as the value returned by the function. The local variable
p
itself ceases to exist when person()
terminates.
person()
to
the following form:
Person person() { string name; string address; string phone; cin >> name >> address >> phone; return Person(name.c_str(), address.c_str(), phone.c_str()); }This code fragment is perfectly valid, and illustrates the use of an
anonymous object
. Anonymous objects are
const objects: their data
members may not change. The use of an anonymous object in the above example
illustrates the fact that
object return values should be considered
constant objects, even though the keyword const
is not explicitly
mentioned in the return type of the function (as in Person const
person()
).
As an other example, once again assuming the availability of a
Person(char const *name)
constructor, consider:
Person namedPerson() { string name; cin >> name; return name.c_str(); }Here, even though the return value
name.c_str()
doesn't match the
return type Person
, there is a constructor available to construct a
Person
from a char const *
. Since such a constructor is available, the
(anonymous) return value can be constructed by promoting a
char const *
type to a Person
type using an
appropriate constructor.
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 if not, then the default copy constructor will still be available when another constructor is defined.
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 implement this approach
as follows:
private
functions copy()
and destroy()
. The purpose of these functions is to
copy the data of another object or to delete the memory of the current
object unconditionally. Hence these functions implement `primitive'
functionality:
// class definition, only relevant functions are shown here class Person { char *d_name; char *d_address; char *d_phone; public: Person(Person const &other); ~Person(); Person &operator=(Person const &other); private: void copy(Person const &other); // new members void destroy(void); };
copy()
and destroy()
are constructed:
void Person::copy(Person const &other) { d_name = strdupnew(other.d_name); // unconditional copying d_address = strdupnew(other.d_address); d_phone = strdupnew(other.d_phone); } void Person::destroy() { delete d_name; // unconditional deletion delete d_address; delete d_phone; }
public
functions in which other object's memory
is copied or in which memory is deleted are rewritten:
Person::Person (Person const &other) // copy constructor { copy(other); } Person::~Person() // destructor { destroy(); } // overloaded assignment Person const &Person::operator=(Person const &other) { if (this != &other) { destroy(); copy(other); } return *this; }
copy()
and destroy()
.
Note, that the
copy()
member function is responsible for the copying
of the other object's data fields to the current object. We've shown the
situation in which a class only has pointer data members. In most
situations
classes have non-pointer data
members as well. These members must be copied in the copy constructor as
well. This can simply be implemented by the copy constructor's body except
for the initialization of
reference data members, which must be
initialized using the
member initializer method, introduced in section
6.4.2. However, in this case the
overloaded assignment operator
can't be fully implemented either, as reference members cannot
be given another value once initialized. An object having reference data
members is inseparately attached to its referenced object(s) once it has been
constructed.
private
section of the class interface. This will effectively prevent the
user from the class to use these members. By moving the assignment operator to
the private section, objects of the class cannot be assigned to each other
anymore. Here the compiler will detect the use of a private member outside
of its class and will flag a compilation error.
The simple conclusion is therefore: classes whose objects allocate memory which is used by these objects themselves, should implement a destructor, an overloaded assignment operator and a copy constructor as well.