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.
In this chapter classes are the topic of discussion. Two special member
functions, the constructor and the destructor, are introduced.
In steps we will construct a class Person, which could be used in a
database application to store a name, an address and a phone number of a
person.
Let's start off by introducing the declaration of a class Person
right away. The class declaration is normally contained in the header file
of the class, e.g., person.h. The class declaration is generally not called
a declaration, though. Rather, the common name for class declarations is
class interface, to be distinguished from the definitions of the function
members, called the class implementation. Thus, the interface of the
class Person is given next:
class Person
{
public: // interface functions
void setname(char const *n);
void setaddress(char const *a);
void setphone(char const *p);
char const *getname(void);
char const *getaddress(void);
char const *getphone(void);
private: // data fields
char *name; // name of person
char *address; // address field
char *phone; // telephone number
};
The data fields in this class are name, address and phone. The
fields are char *s which point to allocated memory. The data are
private, which means that they can only be accessed by the functions of
the class Person.
The data are manipulated by interface functions which take care of all
communication with code outside of the class. Either to set the data fields
to a given value (e.g., setname()) or to inspect the data (e.g.,
getname()).
Note once again how similar the class is to the struct. The
fundamental difference being that by default classes have private members,
whereas structs have public members. Since the convention calls for the
public members of a class to appear first, the keyword private is needed
to switch back from public members to the (default) private situation.
The basic forms and functions of these two categories are discussed next.
void. E.g., for the class Person the constructor is
Person::Person(). The C++ run-time system makes sure that the
constructor of a class, if defined, is called when an object of the class is
created. It is of course possible to define a class which has no constructor
at all; in that case the run-time system either calls no function or it calls
a dummy constructor (i.e., a constructor which performs no actions) when a
corresponding object is created. The actual generated code of course depends
on the compiler (A compiler-supplied constructor in a class which
contains composed objects (see section 4.5) will `automatically'
call the member initializers, and therefore does perform some actions. We
postpone the discussion of such constructors to 4.5.1.).
Objects may be defined at a local (function) level, or at a global level (in
which its status is comparable to a global variable.
When an object is a local (non-static) variable of a function, the constructor
is called every time the function is called at the point where the
variable is defined (a subtlety here is that a variable may be defined
implicitly as, e.g., a temporary variable in an expression).
When an object is a static variable, the constructor is
called when the function in which the static variable is defined is called for
the first time.
When an object is a global variable the constructor is
called when the program starts. Note that in even this case the constructor is
called even before the function main() is started. This feature is
illustrated in the following listing:
#include <iostream>
// a class Test with a constructor function
class Test
{
public: // 'public' function:
Test(); // the constructor
};
Test::Test() // here is the
{ // definition
cout << "constructor of class Test called\n";
}
// and here is the test program:
Test
g; // global object
void func()
{
Test // local object
l; // in function func()
cout << "here's function func()" << endl;
}
int main()
{
Test // local object
x; // in function main()
cout << "main() function" << endl;
func();
return (0);
}
The listing shows how a class Test is defined which consists of only one
function: the constructor. The constructor performs only one action; a message
is printed. The program contains three objects of the class Test: one
global object, one local object in main() and one local object in
func().
Concerning the definition of a constructor we have the following remarks:
class Test
{
public:
/* no return value here */ Test();
};
and also holds true for the definition of the constructor function, as in:
/* no return value here */ Test::Test()
{
// statements ...
}
The constructor of the three objects of the class Test in the above
listing are called in the following order:
g.
main() is started. The object x is
created as a local variable of this function and hence the constructor is
called again. After this we expect to see the text main()
function.
func() is activated from main(). In
this function the local object l is created and hence the constructor
is called. After this, the message here's function func()
appears.
As expected, the program yields therefore the following output (the text in
parentheses is added for illustration purposes):
constructor of class Test called (global object g)
constructor of class Test called (object x in main())
main() function
constructor of class Test called (object l in func())
here's function func()
However, when a program is interrupted using an exit() call, the
destructors are called only for global objects which exist at that time.
Destructors of objects defined locally within functions are not called
when a program is forcefully terminated using exit().
When defining a destructor for a given class the following rules apply:
The destructor for the class Test from the previous section could be
declared as follows:
class Test
{
public:
Test(); // constructor
~Test(); // destructor
// any other members
};
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 any other members follow.
Person.
As illustrated at the beginning of this chapter, the class Person
contains three private pointers, all char *s. These data members are
manipulated by the interface functions. The internal workings of the class are
as follows: when a name, address or phone number of a Person is defined,
memory is allocated to store these data. An obvious setup is described below:
set...() functions) consists of two steps. First, previously
allocated memory is released. Next, the string which is supplied as an
argument to the set...() function is duplicated in memory.
get...()
functions simply returns the corresponding pointer: either a 0-pointer,
indicating that the data is not defined, or a pointer to
allocated memory holding the data.
The set...() functions are illustrated below. Strings are duplicated in
this example by an imaginary function xstrdup(), which would duplicate a
string or terminate the program when the memory pool is exhausted ( As
a word to the initiated reader it is noted here that many other ways to handle
the memory allocation are possible here: As discussed in section
5, new could be used, together with set_new_handler(), or
exceptions could be used to catch any failing memory allocation. However,
since we haven't covered that subject yet, and since these annotations start
from C, we used the tried and true method of a `protected allocation
function' xstrdup() here for didactical reasons.).
// interface functions set...()
void Person::setname(char const *n)
{
free(name);
name = xstrdup(n);
}
void Person::setaddress(char const *a)
{
free(address);
address = xstrdup(a);
}
void Person::setphone(char const *p)
{
free(phone);
phone = xstrdup(p);
}
Note that the statements free(...) in the above listing are executed
unconditionally. This never leads to incorrect actions: when a name, address
or phone number is defined, the corresponding pointers point to previously
allocated memory which should be freed. When the data are not (yet) defined,
then the corresponding pointer is a 0-pointer; and free(0) performs no
action (Actually, free(0) should perform no action. However, later on
we'll introduce the operators new and delete. With the delete
operator delete 0 is formally ignored.).
Furthermore it should be noted that this code example uses the
standard C function free() which should be familiar to most
C programmers. The delete statement, which has more
`C++ flavor', will be discussed later.
The interface functions get...() are defined now. Note the
occurence of the keyword const following the parameter lists of the
functions: the member functions are const member functions, indicating
that they will not modify their object when they're called.
The matter of const member functions is postponed to section
4.2, where it will be discussed in greater detail.
// interface functions get...()
char const *Person::getname() const
{
return (name);
}
char const *Person::getaddress() const
{
return (address);
}
char const *Person::getphone() const
{
return (phone);
}
The destructor, constructor and the class definition are given below.
// class definition
class Person
{
public:
Person(); // constructor
~Person(); // destructor
// functions to set fields
void setname(char const *n);
void setaddress(char const *a);
void setphone(char const *p);
// functions to inspect fields
char const *getname() const;
char const *getaddress() const;
char const *getphone() const;
private:
char *name; // name of person
char *address; // address field
char *phone; // telephone number
};
// constructor
Person::Person()
{
name = 0;
address = 0;
phone = 0;
}
// destructor
Person::~Person()
{
free(name);
free(address);
free(phone);
}
To demonstrate the usage of the class Person, a code example follows
next. An object is initialized and passed to a function printperson(),
which prints the contained data. Note also the usage of the reference operator
& in the argument list of the function printperson(). This way
only a reference to a Person object is passed, rather than a whole object.
The fact that printperson() does not modify its argument is evident from
the fact that the argument is declared const. Also note that the example
doesn't show where the destructor is called; this action occurs implicitly
when the below function main() terminates and hence when its local
variable p ceases to exist.
It should also be noted that the function printperson() could be defined
as a public member function of the class Person.
#include <iostream>
void printperson(Person const &p)
{
cout << "Name : " << p.getname() << endl
<< "Address : " << p.getaddress() << endl
<< "Phone : " << p.getphone() << endl;
}
int main()
{
Person
p;
p.setname("Linus Torvalds");
p.setaddress("E-mail: Torvalds@cs.helsinki.fi");
p.setphone(" - not sure - ");
printperson(p);
return (0);
}
When printperson() receives a fully defined Person object (i.e.,
containing a name, address and phone number), the data are correctly printed.
However, when a Person object is only partially filled, e.g. with only a
name, printperson() passes 0-pointers to cout. This unesthetic
feature can be remedied with a little more code:
void printperson(Person const &p)
{
if (p.getname())
cout << "Name : " << p.getname() << "\n";
if (p.getaddress())
cout << "Address : " << p.getaddress() << "\n";
if (p.getphone())
cout << "Phone : " << p.getphone() << "\n";
}
Alternatively, the constructor Person::Person() might initialize the
members to `printable defaults', like " ** undefined ** ".
Person the constructor
has no arguments. C++ allows constructors to be defined
with argument lists. The arguments are supplied when an object is created.
For the class Person a constructor may be handy which expects three
strings: the name, address and phone number. Such a constructor is shown
below:
Person::Person(char const *n, char const *a, char const *p)
{
name = xstrdup(n);
address = xstrdup(a);
phone = xstrdup(p);
}
The constructor must be included in the class declaration, as illustrated
here:
class Person
{
public:
Person::Person(char const *n,
char const *a, char const *p);
.
.
.
};
Since C++ allows function overloading, such a declaration of a constructor
can co-exist with a constructor without arguments. The class Person would
thus have two constructors.
The usage of a constructor with arguments is illustrated in the following code
fragment. The object a is initialized at its definition:
int main()
{
Person
a("Karel", "Rietveldlaan 37", "542 6044"),
b;
return (0);
}
In this example, the Person objects a and b are created when
main() is started. For the object a the constructor with arguments
is selected by the compiler. For the object b the default constructor
(without arguments) is used.
4.1.4.1: The order of construction
The possibility to pass arguments to constructors offers us the chance to
monitor at which exact moment in a program's execution an object is created or
destroyed. This is shown in the next listing, using a class Test:
class Test
{
public:
// constructors:
Test(); // argument-free
Test(char const *name); // with a name argument
// destructor:
~Test();
private:
// data:
char *n; // name field
};
Test::Test()
{
n = xstrdup("without name");
printf("Test object without name created\n");
}
Test::Test(char const *name)
{
n = xstrdup(name);
cout << "Test object " << name << " created" << endl;
}
Test::~Test()
{
cout << "Test object " << n << " destroyed" << endl;
free(n);
}
By defining objects of the class Test with specific names, the
construction and destruction of these objects can be monitored:
Test
globaltest("global");
void func()
{
Test
functest("func");
}
int main()
{
Test
maintest("main");
func();
return (0);
}
This test program thus leads to the following (and expected) output:
Test object global created
Test object main created
Test object func created
Test object func destroyed
Test object main destroyed
Test object global destroyed
const is often seen in the declarations of member functions
following the argument list. This keyword is used to indicate that a member
function does not alter the data fields of its object, but only inspects them.
Using the example of the class Person, the get...() functions should
be declared const:
class Person
{
public:
.
.
// functions to inspect fields
char const *getname(void) const;
char const *getaddress(void) const;
char const *getphone(void) const;
private:
.
.
};
As is illustrated in this fragment, the keyword const occurs
following the argument list of functions. Note that in this situation
the rule of thumb given in
section 3.1.3 applies once again: whichever appears before the
keyword const, may not be altered and doesn't alter (its own) data.
The same specification must be repeated in the definition of member functions
themselves:
char const *Person::getname() const
{
return (name);
}
A member function which is declared and defined as const may not alter
any data fields of its class. In other words, a statement like
name = 0;
in the above const function getname() would result in a compilation
error.
The const member functions exist because C++ allows
const objects to be created, or references to const objects to be
passed on to functions. For such objects only member functions which do
not modify it, i.e., the const member functions, may be called. The only
exception to this rule are the constructors and destructor: these are called
`automatically'. The possibility of calling constructors or destructors
is comparable to the definition of a variable
int const max = 10. In situations like these, no assignment but
rather an initialization takes place at creation-time.
Analogously, the constructor can
initialize its object when the variable is created,
but subsequent assignments cannot take place.
The following example shows how a const object of the class
Person can be defined. When the object is created the data fields are
initialized by the constructor:
Person const
me("Karel", "karel@icce.rug.nl", "542 6044");
Following this definition it would be illegal to try to redefine the name,
address or phone number for the object me: a statement as
me.setname("Lerak");
would not be accepted by the compiler. Once more, look at the position of the
const keyword in the variable definition: const, following Person
and preceding me associates to the left: the Person object in general
must remain unaltered. Hence, if multiple objects were defined here, both
would be constant Person objects, as in:
Person const // all constant Person objects
kk("Karel", "karel@icce.rug.nl", "542 6044"),
fbb("Frank", "frank@icce.rug.nl", "403 2223");
Member functions which do not modify
their object should be defined as const member functions.
This subsequently allows the use of these functions with const
objects or with const references.
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;
// any other statements
delete ip;
Note that new and delete are operators and therefore do not
require parentheses, which are required for functions like malloc()
and free(). The operator delete returns void, the operator 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 is used to allocate an array, the size of
the variable is placed between square brackets following the type:
int
*intarr;
intarr = new int [20]; // allocates 20 ints
The syntactical rule for the operator new is that this operator must be
followed by a type, optionally followed by a number in square brackets. The
type and number specification lead to an expression which is used by the
compiler to deduce its size; in C an expression like sizeof(int[20])
might be used.
An array is deallocated by using the operator delete:
delete [] intarr;
In this statement the array operators [] indicate that an array is
being deallocated. The rule of thumb here is: whenever new is
followed by [], delete should be followed by it too.
What happens if delete rather than delete [] is used? Consider the
following situation: a class X is defined having a destructor telling us
that it's called. In a main() function an array of two X 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>
class X
{
public:
~X();
};
X::~X()
{
cout << "X destructor called" << endl;
}
int main()
{
X
*a;
a = new X[2];
cout << "Destruction with []'s" << endl;
delete [] a;
a = new X[2];
cout << "Destruction without []'s" << endl;
delete a;
return (0);
}
Here's the generated output:
Destruction with []'s
X destructor called
X destructor called
Destruction without [] 's
X destructor called
So, as we can see, the destructor of the individual X objects are called
if the delete [] syntax is followed, and not if the [] is omitted.
If no destructor is defined, it is not called. Consider the following fragment:
#include <iostream>
class X
{
public:
~X();
};
X::~X()
{
cout << "X destructor called" << endl;
}
int main()
{
X
**a;
a = new X* [2];
a[0] = new X [2];
a[1] = new X [2];
delete [] a;
return (0);
}
This program produces no messages 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 as we do not have something as a 'class pointer
to X objects'. Consequently, the [] is ignored.
Now, because of the []
being ignored, not all elements of the array a points to are considered
when a is deleted. The two pointer elements of a are deleted,
though, because delete a (note that the [] is not written here) frees
the memory pointed to by a. That's all there is to it.
What if we don't want this, but require the X objects pointed to by the
elements of a to be deleted as well? In this case we have two options:
a array, deleting them
in turn. This will call the destructor for a pointer to X objects,
which will destroy all elements if the [] operator is used, as in:
#include <iostream>
class X
{
public:
~X();
};
X::~X()
{
cout << "X destructor called" << endl;
}
int main()
{
X
**a;
a = new X* [2];
a[0] = new X [2];
a[1] = new X [2];
for (int index = 0; index < 2; index++)
delete [] a[index];
delete a;
return (0);
}
X objects, and allocate
a pointer to this super-class, rather than a pointer to a pointer
to X objects. The topic of containing classes in classes,
composition, is discussed in section 4.5.
new and delete are also used when an object of a given
class is allocated. As we have seen in the previous section,
the advantage of the operators new and delete over functions like
malloc() and free() lies in the fact that new and delete
call the corresponding constructors or destructor. This is illustrated in the
next example:
Person
*pp; // ptr to Person object
pp = new Person; // now constructed
...
delete pp; // now destroyed
The 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 which initializes the object. In the above example the
constructor is the argument-free version; it is however also possible to
choose an explicit constructor:
pp = new Person("Frank", "Oostumerweg 17", "050 403 2223");
...
delete pp;
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
deallocate the memory 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 with new
and delete. In this case the size of the array is given between the
[] when the array is created:
Person
*personarray;
personarray = new Person [10];
The compiler will generate code to call the default constructor for each
object which is created. As we have seen, the array operator []
must be used with the delete operator 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. Note again
that delete personarray would only release the
memory of the array itself.
new, so that the pointer which is assigned by new is
set to zero. The error function can be redefined, but it must comply with a
few prerequisites, which are, unfortunately, compiler-dependent.
E.g., for the
Microsoft C/C++ compiler version 7, the prerequisites are:
size_t value which
indicates how many bytes should have been allocated (The type
size_t is usually identical to unsigned.).
int, which is the value passed by
new to the assigned pointer.
The Gnu C/C++ compiler gcc, which is present on many Unix
platforms, requires that the error handler:
void return type).
Then again, Microsoft's Visual C++ interprets the returnvalue of the
the function as follows:
In short: there's no standard here, so make sure that you lookup the
particular characteristics of the set_new_handler function for your
compiler. Whatever you do, in any case make sure you use this function: it
saves you a lot of checks (and problems with a failing allocation that you just
happened to forget to protect with a check...).
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(), defined in the header file new.h. With
some compilers, the
installing function is called _set_new_handler() (note the leading
underscore).
The implementation of an error function is illustrated below. This
implementation applies to the Gnu C/C++ requirements (
The actual try-out of the program is not encouraged, as it will slow down
the computer enormously due to the resulting occupation of
Unix's swap area):
#include <new.h>
#include <iostream>
void out_of_memory()
{
cout << "Memory exhausted. Program terminates." << endl;
exit(1);
}
int main()
{
int
*ip;
long
total_allocated = 0;
// install error function
set_new_handler(out_of_memory);
// eat up all memory
puts("Ok, allocating..");
while (1)
{
ip = new int [10000];
total_allocated += 10000 * sizeof(int);
printf("Now got a total of %ld bytes\n",
total_allocated);
}
return (0);
}
The advantage of an allocation error function lies in the fact that
once installed, new can be used without wondering whether the allocation
succeeded or not: upon failure the error function is automatically invoked and
the program exits. It is good practice to install a new handler in each
C++ program, even when the actual code of the program does not allocate
memory. Memory allocation can also fail in not directly visible code, e.g.,
when streams are used or when strings are duplicated by low-level functions.
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. As an example of the use of new for duplicating
a string, a rewrite of the function strdup() using the operator new is
given in section 5. It is strongly suggested to revert to this
approach, rather than to using functions like xstrdup(), when the
allocation of memory is required.
Person::getname():
char const *Person::getname() const
{
return (name);
}
This function is used to retrieve the name field of an object of the class
Person. In a code fragment, like:
Person
frank("Frank", "Oostumerweg 17", "403 2223");
puts(frank.getname());
the following actions take place:
Person::getname() is called.
name of the
object frank.
puts().
puts() finally is called and prints a string.
Especially the first part of these actions leads to some time loss, since an
extra function call is necessary to retrieve the value of the name field.
Sometimes a faster process may be desirable, in which the name field
becomes immediately available; thus avoiding the call to getname(). This
can be realized by using inline functions, which can be defined in two
ways.
inline functions, the code of a
function is defined in a class declaration itself. For the class
Person this would lead to the following implementation of getname():
class Person
{
public:
...
char const *getname(void) const
{
return (name);
}
...
};
Note that the code of the function getname() now literally occurs in the
interface of the class Person. The keyword const occurs after the
function declaration, and before the code block.
Thus, inline functions appearing in the class interface show their full
(and standard) definition within the class interface itself.
The effect of this is the following. When getname() is called in a
program statement, the compiler generates the code of the function
when the function is used in the source-text, rather than a call to the
function, appearing only once in the compiled program.
This construction, where the function code itself is inserted rather than a
call to the function, is called an inline function. Note that the use of
inline function results in duplication of the code of the function for each
invokation of the inline function. This is probably ok if the function is a
small one, and needs to be executed fast. It's not so desirable if the code of
the function is extensive.
inline in the function definition. The interface
and implementation in this case are as follows:
class Person
{
public:
...
char const *getname(void) const;
...
};
inline char const *Person::getname() const
{
return (name);
}
Again, the compiler will insert the code of the function getname() instead
of generating a call.
However, the inline function must still appear in the same file as the
class interface, and cannot be compiled to be stored in, e.g., a library.
The reason for this is that the compiler rather than the linker must
be able to insert the code of the function in a source text offered for
compilation. Code stored in a library is inaccessible to the compiler.
Consequently, inline functions are always defined together with the class
interface.
inline functions be used, and when not? There is a number of
simple rules of thumb which may be followed:
inline functions should not be used.
Voilà, that's simple, isn't it?
inline functions can be considered once a fully
developed and tested program runs too slowly and shows `bottlenecks' in
certain functions. A profiler, which runs a program and determines where
most of the time is spent, is necessary for such optimization.
inline functions can be used when member functions consist of
one very simple statement (such as the return statement in the function
Person::getname()).
inline, its implementation is inserted
in the code wherever the function is used. As a consequence, when the
implementation of the inline function changes, all sources using the
inline function must be recompiled. In practice that means that all functions
must be recompiled that include (either directly or indirectly) the header
file of the class in which the inline function is defined.
inline function when the
time which is spent during a function call is long compared to the code in
the function. An example where an inline function has no effect at
all is the following:
void Person::printname() const
{
cout << name << endl;
}
This function, which is, for the sake of the argument, presumed to be a
member of the class Person, contains only one statement.
However, the statement
takes a relatively long time to execute. In general, functions which
perform input and output take lots of time. The effect of the conversion
of this function printname() to inline would therefore lead to
a very insignificant gain in execution time.
All inline functions have one disadvantage: the actual code is inserted by
the compiler and must therefore be known compile-time. Therefore, as mentioned
earlier, an
inline function can never be located in a run-time library. Practically
this means that an inline function is placed near the interface of a
class, usually in the same header file. The result is a header file which not
only shows the declaration of a class, but also part of its
implementation, thus blurring the distinction between interface and
implementation.
Finally, note that using the keyword inline is not really an order for
the compiler. Rather, it is a suggestion the compiler may either choose to
follow or to ignore.
For example, the class Person could hold information about the name,
address and phone number, but additionally a class Date could be used to
keep the information about the birth date:
class Person
{
public:
// constructor and destructor
Person();
Person(char const *nm, char const *adr,
char const *ph);
~Person();
// interface functions
void setname(char const *n);
void setaddress(char const *a);
void setphone(char const *p);
void setbirthday(int yr, int mnth, int d);
char const *getname() const;
char const *getaddress() const;
char const *getphone() const;
int getbirthyear() const;
int getbirthmonth() const;
int getbirthday() const;
private:
// data fields
char *name, *address, *phone;
Date birthday;
};
We shall not further elaborate on the class Date: this class could, e.g.,
consist of three int data fields to store a year, month and day. These
data fields would be set and inspected using interface functions
setyear(), getyear() etc..
The interface functions of the class Person would then use Date's
interface functions to manipulate the birth date. As an example the function
getbirthyear() of the class Person is given below:
int Person::getbirthyear() const
{
return (birthday.getyear());
}
Composition is not extraordinary or C++ specific: in C it is quite
common to include structs or unions in other compound types.
Note that the composed objects can be reached through their member functions:
the normal field selector operators are used for this.
However, the initialization of the composed objects deserves some extra
attention: the topics of the coming sections.
Often it is desirable to initialize a composed object from the constructor of
the composing class. This is illustrated below for the composed class
Date in a Person. In this fragment it assumed that a constructor for
a Person should be defined expecting six arguments: the name, address and
phone number plus the year, month and day of the birth date. It is furthermore
assumed that the composed class Date has a constructor with three
int arguments for the year, month and day:
Person::Person(char const *nm, char const *adr,
char const *ph,
int d, int m, int y)
:
birthday(d, m, y)
{
name = xstrdup(nm);
address = xstrdup(adr);
phone = xstrdup(ph);
}
Note that following the argument list of the constructor
Person::Person(), the constructor of the data field Date is
specifically called, supplied with three arguments. This constructor is
explicitly called for the composed object birthday. This occurs even
before the code block of Person::Person() is executed. This means
that when a Person object is constructed and when six arguments are
supplied to the constructor, the birthday field of the object is
initialized even before Person's own data fields are set to their values.
In this situation, the constructor of the composed data member is also
referred to as member initializer.
When several composed data members of a class exist, all member
initializers can be called using a `constructor list': this list consists
of the constructors of all composed objects, separated by commas.
When member initializers are not used, the compiler automatically
supplies a call to the default constructor (i.e., the constructor without
arguments). In this case a default constructor must have been
defined in the composed class.
Member initializers should be used as much as possible: not using member
initializers can result in inefficient code, and can be downright necessary.
As an example showing the inefficiency of not using a member initializer,
consider the following code fragment where the birthday field is not
initialized by the Date constructor, but instead the setday(),
setmonth() and setyear() functions are called:
Person::Person(char const *nm, char const *adr,
char const *ph,
int d, int m, int y)
{
name = xstrdup(nm);
address = xstrdup(adr);
phone = xstrdup(ph);
birthday.setday(d);
birthday.setmonth(m);
birthday.setyear(y);
}
This code is inefficient because:
birthday is called (this
action is implicit),
Date.
This method is not only inefficient, but even more: it may not work
when the composed
object is declared as a const object.
A data field like birthday is a good
candidate for being const, since a person's birthday usually doesn't
change.
This means that when the definition of a Person is changed so that the
data member birthday is declared as a const object,
the implementation of the
constructor Person::Person() with six arguments must use member
initializers. Calling the birthday.set...() would be illegal, since these
are no const functions.
Concluding, the rule of thumb is the following: when composition of
objects is used, the member initializer method is preferred to explicit
initialization of the composed object. This not only results in more efficient
code, but it also allows the composed object to be declared as a const
object.
const objects or not), there is another situation where member
initializers must be used. Consider the following situation.
A program uses an object of the class Configfile, defined in main()
to access the information in a configuration file. The configuration file
contains parameters of the program which may be set by changing the values in
the configuration file, rather than by supplying command line arguments.
Assume that another object that is used in the function main() is an
object of the class Process, doing `all the work'. What possibilities do
we have to tell the object of the class Process that an object of the
class Configfile exists?
Configfile object may be passed to the Process object
at construction time. Passing an object in a blunt way (i.e., by value)
might not be a very good idea, since the object must be copied into the
Configfile parameter, and then a data member of the Process
class can be used to make the Configfile object accessible
throughout the Process class. This might involve yet another
object-copying task, as in the following situation:
Process::Process(Configfile conf) // a copy from the caller
{
conf_member = conf; // copying to conf_member
...
}
Configfile objects, as in:
Process::Process(Configfile *conf) // a pointer to an external object
{
conf_ptr = conf; // the conf_ptr is a Configfile *
...
}
This construction as such is ok, but forces us to use the -> field
selector operator, rather than the . operator, which is (disputably)
awkward: conceptually one tends to think of the Configfile object as
an object, and not as a pointer to an object. In C this would
probably have been the preferred method, but in C++ we can do
better.
Configfile
parameter could be defined as a reference parameter to the Process
constructor. Next, we can define a Config reference data member in the
class Process. Using the reference variable effectively uses a
pointer, disguised as a variable.
However, the following construction will
not result in the correct initialization of the
Configfile &conf_ref reference data member:
Process::Process(Configfile &conf)
{
conf_ref = conf; // wrong: no assignment
}
The statement conf_ref = conf fails, because the compiler won't
see this as an initialization, but considers this an assignment of
one Configfile object (i.e., conf), to another (conf_ref).
It does so, because that's the normal interpretation: an assignment to
a reference variable is actually an assignment to the variable the
reference variable refers to. But to what variable does conf_ref
refer? To no variable, since we haven't initialized conf_ref.
After all, the whole purpose of the statement conf_ref = conf was
to initialize conf_ref....
So, how do we proceed when conf_ref must be initialized? In this
situation we once again use the member-initializer syntax. The following
example shows the correct way to initialize conf_ref:
Process::Process(Configfile &conf)
:
conf_ref(conf) // initializing reference member
{
...
}
Note that this syntax can be used in all cases where reference data
members are used. If int_ref would be an int reference data member,
a construction like
Process::Process(int &ir)
:
int_ref(ir)
{
...
}
would have been called for.
private data or function
members are normally only accessible by the code which is part of the
corresponding class. However, situations may arise in which it is desirable to
allow the explicit access to private members of one class to one or
more other classless functions or member functions of classes.
E.g., consider the following code example (all functions are inline
for purposes of brevity):
class A // class A: just stores an
{ // int value via the constructor
public: // and can retrieve it via
A(int v) // getval
{ value = v; }
int getval()
{ return (value); }
private:
int value;
};
void decrement(A &a) // function decrement: tries
{ // to alter A's private data
a.value--;
}
class B // class B: tries to touch
{ // A's private parts
public:
void touch(A &a)
{ a.value++; }
};
This code will not compile, since the classless
function decrement() and the function touch() of the class
B attempt to access a private datamember of A.
We can explicitly allow decrement() to access A's data, and
we can explicitly allow the class B to access these data. To
accomplish this, the offending classless function decrement() and the
class B are declared to be friends of A:
class A
{
public:
friend class B; // B's my buddy, I trust him
friend void decrement(A // decrement() is also a good pal
&what);
...
};
Concerning friendship between classes, we remark the following:
B is declared as a friend of A, this does not give
A the right to access B's private members.
A changes, all its
friends must be recompiled (and possibly modified) as well.
Having thus issued some warnings against the use of friends, we'll leave our
discussion of friends for the time being.
However, in section 13 we'll continue the discussion,
having covered, by that time, the topic of operator overloading.
When classes are used, there are more requirements for the organization of
header files. In this section these requirements are covered.
First, the source files. With the exception of the occasional classless
function, source files should contain the code of memberfunctions of classes.
With source files there are basically two approaches:
The first alternative has the advantage of economy for the compiler: it only
needs to read the header files that are necessary for a particular source
file. It has the disadvantage that the program developer must include multiple
header files again and again in sourcefiles: it both takes time to type in the
include-directives and to think about the header files which are needed in
a particular source file.
The second alternative has the advantage of economy for the program developer:
the header file of the class accumulates header files, so it tends to become
more and more generally useful. It has the disadvantage that the compiler will
often have to read header files which aren't actually used by the function
defined in the source file.
With computers running faster and faster we think the second alternative is to
be preferred over the first alternative. So, we suggest that
source files of a particular class MyClass are organized according to
the following example:
#include <myclass.h>
int MyClass::aMemberFunction()
{
...
}
There is only one include-directive. Note that the directive refers to
a header file in a directory mentioned in the INCLUDE-file environment
variable. Local header files (using #include "myclass.h") could be used
too, but that tends to complicate the organization of the class header file
itself somewhat. If name-collisions with existing header files might occur it
pays off to have a subdirectory of one of the directories mentioned in the
INCLUDE environment variable (comparable to, e.g., the sys
subdirectory). If class MyClass is developed as part of some larger
project, create a subdirectory (or subdirectory link) of one of the
INCLUDE directories, to contain all header files of all classes that are
developed as part of the project. The include-directives will then be
similar to #include <myproject/myclass.h>, and name collisions with other
header files are avoided.
The organization of the header-file itself requires some attention. Consider
the following example, in which two classes File and String are
used. The File class has a member gets(String &destination), which
reads a line from a file, and stores the line in the String object passed
to the gets() member function as reference, while the class String has
a member function getLine(File &file), which reads one line from the
File object which is passed to the getLine() member function as a
reference. The (partial) header file for the class String is then:
#ifndef _String_h_
#define _String_h_
#include <project/file.h> // to know about a File
class String
{
public:
void getLine(File &file);
...
};
#endif
However, a similar setup is required for the class File:
#ifndef _File_h_
#define _File_h_
#include <project/string.h> // to know about a String
class File
{
public:
void gets(String &string);
...
};
#endif
Now we have created a problem. The compiler, trying to compile
File::gets() proceeds as follows:
project/string.h is opened to be read
_String_h_ is defined
project/file.h is opened to be read
_File_h_ is defined
project/string.h is opened to be read
_String_h_ has been defined, so project/string.h is skipped
class File is parsed.
String object
class String hasn't been parsed yet, a String is
an undefined type, and the compiler quits with an error.
The solution for this problem is to use a forward class reference before
the class definition, and to include the corresponding class header file after
the class definition. So we get:
#ifndef _String_h_
#define _String_h_
class File; // forward reference
class String
{
public:
void getLine(File &file);
...
};
#include <project/file.h> // to know about a File
#endif
However, a similar setup is required for the class File:
#ifndef _File_h_
#define _File_h_
class String; // forward reference
class File
{
public:
void gets(String &string);
...
};
#include <project/string.h> // to know about a String
#endif
This works well in all situations where either references or pointers to
another class are involved. But it doesn't work with composition. Assume the
class File has a composed data member of the class String. In that
case, the class definition of the class File must include the header
file of the class String before the class definition itself, because
otherwise the compiler can't tell how big a File object will be, as it
doesn't know the size of a String object once the definition of the
File class is completed.
In cases where classes contain composed objects (or are derived from other
classes, see chapter 14) the header files of the classes of the
composed objects must have been read before the class definition itself.
In such a case the class File might be defined as follows:
#ifndef _File_h_
#define _File_h_
#include <project/string.h> // to know about a String
class File
{
public:
void gets(String &string);
...
private:
String // composition !
line;
};
#endif
Note that the class String can't have a File object as a composed
member: such a situation would result again in an undefined class while
compiling the sources of these classes.
All other required header files are either related to classes that are used
only within the source files themselves (without being part of the current
class definition), or they are related to classless functions (like
memcpy()). All headers that are not required by the compiler to parse the
current class definition can be mentioned below the class definition.
To summarize, a class header file should be organized as follows:
ifndef and endif directives.
#ifndef _File_h_
#define _File_h_
#include <fstream> // for composed 'instream'
class String; // forward reference
class File // class definition
{
public:
void gets(String &string);
...
private:
ifstream
instream;
};
// for the class String
#include <project/string.h>
// for remaining software
#include <memory.h>
#include <sys/stat.h>
#endif
A class can be nested in every part of the surrounding class: in the
public, protected or private section. Such a nested class can be
considered a member of the surrounding class. The normal access and visibility
rules in classes apply to nested classes. If a class is nested in the
public section of a class, it is visible outside the surrounding class. If
it is nested in the protected section it is visible in subclasses, derived
from the surrounding class (see chapter 14), if it is nested in
the private section, it is only visible for the members of the surrounding
class.
The surrounding class has no privileges with respect to the nested class. So,
the nested class still has full control over the accessibility of its members
by the surrounding class.
For example, consider the following class definition:
class Surround
{
public:
class FirstWithin
{
public:
FirstWithin();
int getVar() const
{
return (variable);
}
private:
int
variable;
};
private:
class SecondWithin
{
public:
SecondWithin();
int getVar() const
{
return (variable);
}
private:
int
variable;
};
// other private members of Surround
};
In this definition access to the members is defined as follows:
FirstWithin is visible both outside and inside
Surround. The class FirstWithin has therefore global scope.
FirstWithin() and the memberfunction getVar()
of the class FirstWithin are also globally visible.
int variable datamember is only visible for the members
of the class FirstWithin. Neither the members of Surround nor the members
of SecondWithin can access the variable of the class FirstWithin directly.
SecondWithin is visible only inside
Surround. The public members of the class SecondWithin can also be used by
the members of the class FirstWithin, as nested classes can be considered
members of their surrounding class.
SecondWithin() and the memberfunction getVar()
of the class SecondWithin can also only be reached by the members of
Surround (and by the members of its nested classes).
int variable datamember of the class SecondWithin is only
visible for the members of the class SecondWithin. Neither the members of
Surround nor the members of FirstWithin can access the variable of the
class SecondWithin directly.
friend classes (see section 4.8.3).
The nested classes can be considered members of the surrounding class, but
the members of nested classes are not members of the surrounding
class. So, a member of the class Surround may not access
FirstWithin::getVar() directly. This is understandable considering the
fact that a Surround object is not also a FirstWithin or
SecondWithin object. The nested classes are only available as
typenames. They do not imply containment as objects by the surrounding
class. If a member of the surrounding class should use a (non-static) member
of a nested class then a pointer to a nested class object or a nested class
datamember must be defined in the surrounding class, which can thereupon be
used by the members of the surrounding class to access members of the nested
class.
For example, in the following class definition there is a surrounding class
Outer and a nested class Inner. The class Outer contains a
memberfunction caller() which uses the inner object that is composed
in Outer to call the infunction() memberfunction of Inner:
class Outer
{
public:
void caller()
{
inner.infunction();
}
private:
class Inner
{
public:
void infunction();
};
Inner
inner;
};
Also note that the function Inner::infunction() can be called as part
of the inline definition of Outer::caller(), even though the definition of
the class Inner is yet to be seen by the compiler.
Inline functions can be defined as if they were functions that were defined
outside of the class definition: if the function Outer::caller() would
have been defined outside of the class Outer, the full class definition
(including the definition of the class Inner would have been available to
the compiler. In that situation the function is perfectly compilable. Inline
functions can be compiled accordingly and there is, e.g., no need to define a
special private section in Outer in which the class Inner is defined
before defining the inline function caller().
FirstWithin in the example of the previous
section. The constructor FirstWithin() is defined in the class
FirstWithin, which is, in turn, defined within the class
Surround. Consequently, the class scopes of the two classes must be used
to define the constructor. E.g.,
Surround::FirstWithin::FirstWithin()
{
variable = 0;
}
Static (data) members can be defined accordingly. If the class FirstWithin
would have a static unsigned datamember epoch, it could be initialized
as follows:
Surround::FirstWithin::epoch = 1970;
Furthermore, both class scopes are needed to refer to public static
members in code outside of the surrounding class:
void showEpoch()
{
cout << Surround::FirstWithin::epoch = 1970;
}
Of course, inside the members of the class Surround only the FirstWithin:: scope
needs to be mentioned, and inside the members of the class FirstWithin there
is no need to refer explicitly to the scope.
What about the members of the class SecondWithin? The classes FirstWithin
and SecondWithin are both nested within Surround, and can be considered
members of the surrounding class. Since members of a class may directy refer
to each other, members of the class SecondWithin can refer to (public) members
of the class FirstWithin. Consequently, members of the class SecondWithin could
refer to the epoch member of FirstWithin as
FirstWithin::epoch
For example, the following class Outer contains two nested classes
Inner1 and Inner2. The class Inner1 contains a pointer to
Inner2 objects, and Inner2 contains a pointer to Inner1
objects. Such cross references require forward declarations:
class Outer
{
...
private:
class Inner2; // forward declaration
class Inner1
{
...
private:
Inner2
*pi2; // points to Inner2 objects
};
class Inner2
{
...
private:
Inner1
*pi1; // points to Inner1 objects
};
...
};
friend keyword must be used. Consider the following
situation, in which a class Surround has two nested classes FirstWithin
and SecondWithin, while each class has a static data member int variable:
class Surround
{
public:
class FirstWithin
{
public:
int getValue();
private:
static int
variable;
};
int getValue();
private:
class SecondWithin
{
public:
int getValue();
private:
static int
variable;
};
static int
variable;
};
If the class Surround should be able to access the private members of
FirstWithin and SecondWithin, these latter two classes must declare
Surround to be their friend. The function Surround::getValue() can
thereupon access the private members of the nested classes. For example (note
the friend declarations in the two nested classes):
class Surround
{
public:
class FirstWithin
{
friend class Surround;
public:
int getValue();
private:
static int
variable;
};
int getValue()
{
FirstWithin::variable = SecondWithin::variable;
return (variable);
}
private:
class SecondWithin
{
friend class Surround;
public:
int getValue();
private:
static int
variable;
};
static int
variable;
};
Now, in order to allow the nested classes to access the private members of
the surrounding class, the class Surround must declare the nested classes
as friends. The friend keyword may only be used when the class that is to
become a friend is already known as a class by the compiler, so either a
forward declaration of the nested classes is required, which is followed
by the friend declaration, or the friend declaration follows the definition of
the nested classes. The forward declaration followed by the friend declaration
looks like this:
class Surround
{
class FirstWithin;
class SecondWithin;
friend class FirstWithin;
friend class SecondWithin;
public:
class FirstWithin
... (etc)
Alternatively, the friend declaration may follow the definition of the
classes. Note that a class can be declared a friend following its definition,
while the inline code in the definition already uses the fact that it will be
declared a friend of the outer class. Also note that the inline code of
the nested class uses members of the surrounding class which have not yet been
seen by the compiler. Finally note that the variable variable that is
defined in the class Surround is accessed in the nested classes as
Surround::variable:
class Surround
{
public:
class FirstWithin
{
friend class Surround;
public:
int getValue()
{
Surround::variable = 4;
return (variable);
}
private:
static int
variable;
};
friend class FirstWithin;
int getValue()
{
FirstWithin::variable = SecondWithin::variable;
return (variable);
}
private:
class SecondWithin
{
friend class Surround;
public:
int getValue()
{
Surround::variable = 40;
return (variable);
}
private:
static int
variable;
};
friend class SecondWithin;
static int
variable;
};
Finally, we want to allow the nested classes to access each other's
private members. Again this requires some friend declarations. In order to
allow FirstWithin to access SecondWithin's private members nothing but a friend
declaration in SecondWithin is required. However, to allow SecondWithin to
access the private members of FirstWithin the friend class SecondWithin
declaration cannot be plainly given in the class FirstWithin, as the
definition of SecondWithin has not yet been given. A forward declaration of
SecondWithin is required, and this forward declaration must be given in the
class Surround, rather than in the class FirstWithin. Clearly, the forward
declaration class SecondWithin in the class FirstWithin itself makes no sense,
as this would refer to an external (global) class FirstWithin. But the attempt
to provide the forward declaration of the nested class SecondWithin inside
FirstWithin as class Surround::SecondWithin also fails miserably, with the
compiler issuing a message like
`Surround' does not have a nested type named `SecondWithin' SecondWithin in
the class Surround, before the class FirstWithin is defined. Using this
procedure, the friend declaration of SecondWithin is accepted inside the
definition of FirstWithin. The following class definition allows full access
of the private members of all classes by all other classes:
class Surround
{
class SecondWithin;
public:
class FirstWithin
{
friend class Surround;
friend class SecondWithin;
public:
int getValue()
{
Surround::variable = SecondWithin::variable;
return (variable);
}
private:
static int
variable;
};
friend class FirstWithin;
int getValue()
{
FirstWithin::variable = SecondWithin::variable;
return (variable);
}
private:
class SecondWithin
{
friend class Surround;
friend class FirstWithin;
public:
int getValue()
{
Surround::variable = FirstWithin::variable;
return (variable);
}
private:
static int
variable;
};
friend class SecondWithin;
static int
variable;
};
DataStructure may be traversed in a forward or backward direction. Such a
class can define an enumerator Traversal having the values forward and
backward. Furthermore, a memberfunction setTraversal() can be defined
requiring either of the two enumeration values. The class can be defined as
follows:
class DataStructure
{
public:
enum Traversal
{
forward,
backward
};
setTraversal(Traversal mode);
...
private:
Traversal
mode;
...
};
Within the class DataStructure the values of the
Traversal enumeration can be used directly. For example:
void DataStructure::setTraversal(Traversal modeArg)
{
mode = modeArg;
switch (mode)
{
forward:
....
break;
backward:
....
break;
}
}
Ouside of the class DataStructure the name of the enumeration type is
not used to refer to the values of the enumeration. Here the classname is
enough. Only if a variable of the enumeration type is required the name of the
enumeration type is needed, as illustrated by the following piece of code:
void fun()
{
DataStructure::Traversal // enum typename required
localMode = DataStructure::forward; // enum typename not required
DataStructure
ds;
// enum typename not required
ds.setTraversal(DataStructure::backward);
}
Again, if DataStructure would define a nested class Nested in
which the enumeration Traversal would have been defined, the two class
scopes would have been required. In that case the former example would have to
be coded as follows:
void fun()
{
DataStructure::Nested::Traversal
localMode = DataStructure::Nested::forward;
DataStructure
ds;
ds.setTraversal(DataStructure::Nested::backward);
}