C++ Annotations Version 4.0.0

Chapter 5: Classes and memory allocation

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.

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:

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:

5.1: Classes with pointer data members

In this section we shall again use the class 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; } void 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; }

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.

5.2: The assignment operator

Variables which are structs or classes can be directly assigned in C++ in the same way that structs 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.

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.

5.2.1: Overloading the assignment operator

Obviously, the right way to assign one 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.

figure 4 is shown here.
figure 4: Private data and public interface functions of the class Person, using the `correct' assignment.

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 = strdup(other.name); address = strdup(other.address); phone = strdup(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).

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:

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.

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 = strdup(other.name); address = strdup(other.address); phone = strdup(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.

5.3: The this pointer

As we have seen, a member function of a given class is always called in the context of some object of the class. There is always an implicit `substrate' for the function to act on. C++ defines a keyword, 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 = strdup(n); } // alternative 2: explicit usage of this void Person::setname(char const *n) { delete this->name; this->name = strdup(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.

5.3.1: Preventing self-destruction with this

As we have seen, the operator = 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 = strdup(other.name); address = strdup(other.address); phone = strdup(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.

5.3.2: Associativity of operators and this

According to C++'s syntax, the associativity of the assignment operator is to the right-hand side. I.e., in statements like:

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 = strdup(other.address); name = strdup(other.name); phone = strdup(other.phone); } // return current object, compiler will make sure // that a const reference is returned return (*this); }

5.4: The copy constructor: Initialization vs. Assignment

In the following sections we shall take a closer look at another usage of the operator =. 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: // constructor, destructors 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:

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); }

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 = strdup(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.

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 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.

5.4.1: Similarities between the copy constructor and operator=()

The similarities between on one hand the copy constructor and on the other hand the overloaded assignment operator are reinvestigated in this section. We present here two primitive functions which often occur in our code, and which we think are quite useful. Note the following features of copy constructors, overloaded assignment operators, and destructors:

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 implementationsof the functions copy() and destroy():

// copy(): unconditionally copy other object's data void Person::copy(Person const &other) { name = strdup(other.name); address = strdup(other.address); phone = strdup(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().

5.5: Conclusion

Two important extensions to classes have been discussed in this chapter: the overloaded assignment operator and the copy constructor. As we have seen, classes with pointer data which address allocated memory are potential sources of semantic errors. The two introduced extensions represent the standard ways to prevent unintentional loss of allocated data.

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.