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. Mail to
Frank Brokken
or use an
e-mail form.
Please state the concerned document version, found in
the title. If you're interested in a printable
PostScript copy, use the
form, or
better yet,
pick up your own copy by ftp
from ftp.icce.rug.nl/pub/http.
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.
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.
In this chapter we discuss the following topics:
this
pointer,
Person
as example:
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
.
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:
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.
Using this tool we could rewrite the offending function printperson()
:
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).
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.
5.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.
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:
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:
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:
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:
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:
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:
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
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
.
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:
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:
=
. For this, we shall use a class String
. This class is
meant to handle allocated strings, and its interface is as follows:
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.
Now let's consider the following code fragment. The statement references are discussed following the example:
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
=
, 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
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:
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:
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
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:
char *
doesn't match the prototype String
. In this situation, C++ will try
to convert the char *
to a String
. It can do that
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.
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:
Next, we present the implementationsof the functions copy()
and
destroy()
:
Finally the three public
functions in which other object's memory is
copied or in which memory is deallocated are rewritten:
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, an overloaded assignment function and a copy constructor should be implemented.