Don't hesitate to send in feedback: send an e-mail if you like the C++ Annotations; if you think that important material was omitted; if you find errors or typos in the text or the code examples; or if you just feel like e-mailing. Send your e-mail to Frank B. Brokken.Please state the document version you're referring to, as found in the title (in this document: 7.3.0) and please state chapter and paragraph name or number you're referring to.
All received mail is processed conscientiously, and received suggestions for improvements will usually have been processed by the time a new version of the Annotations is released. Except for the incidental case I will normally not acknowledge the receipt of suggestions for improvements. Please don't interpret this as me not appreciating your efforts.
In this chapter classes are formally introduced. Two special member functions, the constructor and the destructor, are presented.
In steps we will construct a class Person
, which could be used in a
database application to store a person's name, address and phone number.
Let's start by creating 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
. A class declaration is generally not called
a declaration, though. Rather, these days the common name for class
declarations is
class interface, to be distinguished from the definitions
of the function members, called the
class implementation. The term `interface' nicely avoids the
confusion `declaration' might present, since `declaration' is traditionally
used with `variable declaration' and `function declaration'.
Thus, the interface of the class Person
is:
#include <string> class Person { std::string d_name; // name of person std::string d_address; // address field std::string d_phone; // telephone number size_t d_weight; // the weight in kg. public: // interface functions void setName(std::string const &n); void setAddress(std::string const &a); void setPhone(std::string const &p); void setWeight(size_t weight); std::string const &name() const; std::string const &address() const; std::string const &phone() const; size_t weight() const; };Note, however, that this terminology is frequently loosely applied. Sometimes, class definition is used to indicate the class interface. While the class definition (so, the interface) contains the declarations of its members, the actual implementation of these members is also referred to as the definition of these members. As long as the concept of the class interface and the class implementation is well distinguished, it should be clear from the context what is meant by a `definition'.
The data fields in this class are d_name, d_address, d_phone
and
d_weight
. All fields except d_weight
are string
objects. As the
data fields are not given a specific
access modifier, they are
private
, which means that they can only be accessed by the functions
of the class Person
. Alternatively, the label `private:
' might have
been used at the beginning of a private section of the class definition.
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.,
name()
). Functions merely returning values stored inside the object, not
allowing the caller to modify these internally stored values, are called
accessor functions.
Note once again how similar the class
is to the
struct
. The only
formal difference between a class
and
a struct
is the fact that by default classes have private members,
whereas structs have
public members. In practice structs are used in the
way they are used in C: to aggregate data, which are all freely
accessible, whereas classes usually hide their data from access by the outside
world, and feature
member functions defining the actions class-objects
may perform.
A few remarks concerning style. Following Lakos (Lakos, J., 2001) Large-Scale C++ Software Design (Addison-Wesley). I suggest the following setup of class interfaces:
d_
, followed by a name suggesting the
meaning of the variable (In chapter 10 we'll also
encounter data members starting with s_
).
set
. E.g., setName()
.
get
-prefix is encountered, e.g.,
getName()
. However, following the conventions used in the
Qt
Graphical User Interface Toolkit (see
http://www.trolltech.com
), the get
-prefix is dropped. So, rather
than defining the member getAddress()
, the function will simply be defined
as address()
.
private
is needed to switch back from public
members to the (default) private situation which thus nicely separates the
members that may be used `by the general public' from the class's own support
members.
In this chapter the emphasis will be on the basic form of the class
and
on its
constructors.
The constructor has by definition the same name as its class. The constructor
does not specify a return value, not even void
. E.g., for the class
Person
the constructor is Person::Person()
. The C++ run-time
system ensures that the constructor of a class, if defined, is called when a
variable of the class, called an
object, is defined (`created'). It is of
course possible to define a class with no constructor at all. In that case the
program will call a default constructor when a corresponding object is
created. What actually happens in that case depends on the way the class has
been defined. The actions of the default constructors are covered in section
6.4.1.
Objects may be defined locally or globally. However, in C++ most objects are defined locally. Globally defined objects are hardly ever required.
When an object is defined locally (in a function), the constructor is called every time the function is called. The object's constructor is then activated at the point where the object 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 defined as a static object (i.e., it is static variable) in a function, the constructor is called when the function in which the static variable is defined is called for the first time.
When an object is defined as a
global object the constructor is called when
the program starts. Note that in this case the constructor is called even
before the function main()
is started. This feature is illustrated in the
following program:
#include <iostream> using namespace std; class Demo { public: Demo(); }; Demo::Demo() { cout << "Demo constructor called\n"; } Demo d; int main() {} /* Generated output: Demo constructor called */The above listing shows how a class
Demo
is defined which consists
of just one function: the constructor. The constructor performs but one
action: a message is printed. The program contains one global object of the
class Demo
, and main()
has an empty body. Nonetheless, the program
produces some output.
Some important characteristics of constructors are:
class Demo { public: Demo(); // no return value here };and it holds true for the definition of the constructor function, as in:
Demo::Demo() // no return value here { // statements ... }
This has important consequences, as the default constructor is required in cases where it must be able to construct an object either with or without explicit initialization values. By merely defining a constructor having at least one argument, the implicitly available default constructor disappears from view. As noted, to make it available again in this situation, it must be defined explicitly too.
Person
contains
three private
string
data members and a size_t d_weight
data
member. These data members can be manipulated by the
interface functions.
Classes (should) operate as follows:
set...()
function) consists of the assignment of the new value to the
corresponding data member. This assignment is fully controlled by the
class-designer. Consequently, the object itself is `responsible' for its own
data-integrity.
set...()
functions could be constructed as follows:
#include "person.h" // given earlier // interface functions set...() void Person::setName(string const &name) { d_name = name; } void Person::setAddress(string const &address) { d_address = address; } void Person::setPhone(string const &phone) { d_phone = phone; } void Person::setWeight(size_t weight) { d_weight = weight; }
Next the
accessor functions are defined. Note the occurence of the
keyword const
following the parameter lists of these functions: these
member functions are called
const member functions, indicating that
they will not modify their object's data when they're called. Furthermore,
notice that the return types of the member functions returning the values of
the string
data members are string const &
types: the const
here
indicates that the caller of the member function cannot alter the
returned value itself. The caller of the accessor member function could
copy the returned value to a variable of its own, though, and that
variable's value may then of course be modified ad lib. Const member
functions are discussed in greater detail in section 6.2. The
return value of the weight()
member function, however, is a plain
size_t
, as this can be a simple copy of the value that's stored in the
Person
's weight
member:
#include "person.h" // given earlier // accessor functions ...() string const &Person::name() const { return d_name; } string const &Person::address() const { return d_address; } string const &Person::phone() const { return d_phone; } size_t Person::weight() const { return d_weight; }
The class definition of the Person
class given earlier can still be
used. The set...()
and accessor functions merely implement the member
functions declared in that class definition.
The following example shows the use of the class Person
. An object is
initialized and passed to a function printperson()
, which prints the
person's data. Note also the usage of the
reference operator &
in the
argument list of the function printperson()
. This way only a reference to
an existing 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 parameter is declared const
.
Alternatively, the function printperson()
might have been defined
as a public
member function of the class Person
, rather than a plain,
objectless function.
#include <iostream> #include "person.h" // given earlier void printperson(Person const &p) { cout << "Name : " << p.name() << endl << "Address : " << p.address() << endl << "Phone : " << p.phone() << endl << "Weight : " << p.weight() << endl; } int main() { Person p; p.setName("Linus Torvalds"); p.setAddress("E-mail: Torvalds@cs.helsinki.fi"); p.setPhone(" - not sure - "); p.setWeight(75); // kg. printperson(p); return 0; } /* Produced output: Name : Linus Torvalds Address : E-mail: Torvalds@cs.helsinki.fi Phone : - not sure - Weight : 75 */
Person
the constructor has no
arguments. C++ allows constructors to be defined with or without argument
lists. The arguments are supplied when an object is created.
For the class Person
a constructor expecting three strings and an size_t
may be handy: these arguments then represent, respectively, the person's name,
address, phone number and weight. Such a constructor is:
Person::Person(string const &name, string const &address, string const &phone, size_t weight) { d_name = name; d_address = address; d_phone = phone; d_weight = weight; }The constructor must also be declared in the class interface:
class Person { public: Person(std::string const &name, std::string const &address, std::string const &phone, size_t weight); // rest of the class interface };However, now that this constructor has been declared, the default constructor must be declared explicitly too, if we still want to be able to construct a plain
Person
object without any specific initial
values for its data members.
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, and the relevant part of the
class interface becomes:
class Person { public: Person(); Person(std::string const &name, std::string const &address, std::string const &phone, size_t weight); // rest of the class interface };In this case, the
Person()
constructor doesn't have to do much, as it
doesn't have to initialize the string
data members of the Person
object: as these data members themselves are objects, they are already
initialized to empty strings by
default. However,
there is also a size_t
data member. That member is a variable of a
basic type and basic type variabes are not initialized automatically. So,
unless the value of the d_weight
data member is explicitly initialized, it
will be
Person
objects,
Person
objects
Person::Person() { d_weight = 0; }The use of a constructor with and without arguments (i.e., the default constructor) is illustrated in the following code fragment. The object
a
is initialized at its definition using the constructor with arguments, with
the b
object the default constructor is used:
int main() { Person a("Karel", "Rietveldlaan 37", "542 6044", 70); Person b; return 0; }In this example, the
Person
objects a
and b
are created when
main()
is started: they are local objects, living for as long as the
main()
function is active.
If Person
objects must be contructed using other arguments, other
constructors are required as well. It is also possible to define default
parameter values. These
default parameter values must be given in the class
interface, e.g.,
class Person { public: Person(); Person(std::string const &name, std::string const &address = "--unknown--", std::string const &phone = "--unknown--", size_t weight = 0); // rest of the class interface };
Often, the constructors are implemented highly similarly. This results
from the fact that often the constructor's parameters are defined for
convenience: a constructor not requiring a phone
number but requiring a
weight
cannot be defined using default arguments, since only the last but
one parameter in the constructor defining all four parameters is not
required. This cannot be solved using default argument values, but only by
defining another constructor, not requiring phone
to be specified.
Although some languages (e.g., Java) allow constructors to call other constructors from their bodies, this is conceptually weird. It's weird because it makes a kludge out of the constructor concept. A constructor is meant to construct an object, not to construct itself while it hasn't been constructed yet.
In C++ the way to proceed is as follows: All constructors must
initialize their reference and const
data members, or the compiler will
(rightfully) complain. This is one of the fundamental reasons why you can't
call a constructor during a construction. For the remaining (non-const and
non-reference members), we have two options:
init(maybe having params)
called by the constructors. Each
constructor furthermore initializes any reference data members its class may
have.
Test
. The program listing below shows a class
Test
, a global Test
object, and two local Test
objects: in a
function func()
and in the main()
function. The order of construction
is as expected: first global, then main's first local object, then
func()
's local object, and then, finally, main()
's second local
object:
#include <iostream> #include <string> using namespace std; class Test { public: Test(string const &name); // constructor with an argument }; Test::Test(string const &name) { cout << "Test object " << name << " created" << endl; } Test globaltest("global"); void func() { Test functest("func"); } int main() { Test first("main first"); func(); Test second("main second"); return 0; } /* Generated output: Test object global created Test object main first created Test object func created Test object main second created */
const
is often used behind the parameter list of
member functions. This keyword indicates that a member
function does not alter the data members of its object, but will only inspect
them. These member functions are called
const member functions.
Using the example of the class Person
, we see that the accessor
functions were declared const
:
class Person { public: std::string const &name() const; std::string const &address() const; std::string const &phone() const; };This fragment illustrates that the keyword
const
appears
behind the functions' argument lists. Note that in this situation the
rule of thumb given in section 3.1.3 applies as well: whichever
appears before the keyword const
, may not be altered and doesn't alter
(its own) data.
The const
specification must be repeated in the definitions of member
functions:
string const &Person::name() const { return d_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
d_name = 0;in the above
const
function name()
would result in a
compilation error.
Const
member functions exist because C++ allows const
objects to
be created, or (used more often) references to const
objects to be passed
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 const
variable is created, but
subsequent assignments cannot take place.
The following example shows the definition of a const
object of the class
Person
. 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", "f.b.brokken@rug.nl", "363 9281");
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. As a
rule of thumb it is stated here that member functions should always be
given the const
attribute, unless they actually modify the object's data.
Earlier, in section 2.5.11 the concept of function
overloading was introduced. There it noted that member functions may
be overloaded merely by their const
attribute. In those cases, the
compiler will use the member function matching most closely the
const-qualification of the object:
const
object, only const
member
functions can be used.
const
object, non-const
member
functions will be used, unless only a const
member function is
available. In that case, the const
member function will be used.
const
member functions is
given in the following example:
#include <iostream> using namespace std; class X { public: X(); void member(); void member() const; }; X::X() {} void X::member() { cout << "non const member\n"; } void X::member() const { cout << "const member\n"; } int main() { X const constObject; X nonConstObject; constObject.member(); nonConstObject.member(); } /* Generated output: const member non const member */Overloading member functions by their
const
attribute commonly occurs
in the context of operator overloading. See chapter
9, in particular section 9.1 for
details.
For example, the class Print
may offer a facility to print a string,
prefixing it with a configurable prefix, and affixing a configurable affix to
it. Such a class could be given the following prototype:
class Print { public: printout(std::string const &prefix, std::string const &text, std::string const &affix) const; };An interface like this would allow us to do things like:
Print print; for (int idx = 0; idx < argc; ++idx) print.printout("arg: ", argv[idx], "\n");This would work well, but can greatly be improved if we could pass
printout
's invariant arguments to Print
's constructors: this way we
would not only simplify printout
's prototype (only one argument would need
to be passed rather than three, allowing us to make faster calls to
printout
) but we could also capture the above code in a function expecting
a Print
object:
void printText(Print const &print, int argc, char *argv[]) { for (int idx = 0; idx < argc; ++idx) print.printout(argv[idx]); }Now we have a fairly generic piece of code, at least as far as
Print
is concerned. If we would provide Print
's interface with the following
constructors we would be able to configure our output stream as well:
Print(char const *prefix, char const *affix); Print(ostream &out, char const *prefix, char const *affix);Now
printText
could be used as follows:
Print p1("arg: ", "\n"); // prints to cout Print p2(cerr, "err: --", "--\n"); // prints to cerr printText(p1, argc, argv); // prints to cout printText(p2, argc, argv); // prints to cerrHowever, when looking closely at this example, it should be clear that both
p1
and p2
are only used inside the printText
function. Furthermore, as we can see from printText
's prototype,
printText
won't modify the internal data of the Print
object it is
using.
In situations like these it is not necessary to define objects before they are used. Instead anonymous objects should be used. Using anonymous objects is indicated when:
const
reference to an object;
Anonymous objects are defined by calling a constructor without providing a name for the constructed object. In the above example anonymous objects can be used as follows:
printText(Print("arg: ", "\n"), argc, argv); // prints to cout printText(Print(cerr, "err: --", "--\n"), argc, argv);// prints to cerrIn this situation the
Print
objects are constructed and immediately
passed as first arguments to the printText
functions, where they are
accessible as the function's print
parameter. While the printText
function is executing they can be used, but once the function has completed,
the Print
objects are no longer accessible.
Anonymous objects cease to exist when the function for which they were created has terminated. In this respect they differ from ordinary local variables whose lifetimes end by the time the function block in which they were defined is closed.
const
references to objects. These objects are created just
before such a function is called, and are destroyed once the function has
terminated. This use of anonymous objects to initialize function parameters is
often seen, but C++'s grammar allows us to use anonymous objects in other
situations as well. Consider the following snippet of code:
int main() { // initial statements Print("hello", "world"); // later statements }In this example the anonymous
Print
object is constructed, and is
immediately destroyed after its construction. So, following the `initial
statements' our Print
object is constructed, then it is destroyed again,
followed by the execution of the `later statements'. This is remarkable as it
shows that the standard lifetime rules do not apply to anonymous
objects.
Their
lifetime is limited to the statement, rather than to the end of the
block in which they are defined.
Of course one might wonder why a plain anonymous object could ever be considered useful. One might think of at least one situation, though. Assume we want to put markers in our code producing some output when the program's execution reaches a certain point. An object's constructor could be implemented so as to provide that marker-functionality, thus allowing us to put markers in our code by defining anonymous, rather than named objects.
However, C++'s grammar contains another remarkable characteristic. Consider the next example:
int main(int argc, char *argv[]) { Print p("", ""); // 1 printText(Print(p), argc, argv); // 2 }In this example a non-anonymous object
p
is constructed in statement
1, which object is then used in statement 2 to initialize an anonymous
object which, in turn, is then used to initialize printText
's const
reference parameter. This use of an existing object to initialize another
object is common practice, and is based on the existence of a so-called
copy constructor. A copy constructor creates an object (as it is a
constructor) using an existing object's characteristics to initialize the new
object's data. Copy constructors are discussed in depth in chapter
7, but presently merely the concept of a copy constructor is used.
In the last example a copy constructor was used to initialize an anonymous
object, which was then used to initialize a parameter of a function. However,
when we try to apply the same trick (i.e., using an existing object to
initialize an anonymous object) to a plain statement, the compiler generates
an error: the object p
can't be redefined (in statement 3, below):
int main(int argc, char *argv[]) { Print p("", ""); // 1 printText(Print(p), argc, argv); // 2 Print(p); // 3 error! }So using an existing object to initialize an anonymous object that is used as function argument is OK, but an existing object can't be used to initialize an anonymous object in a plain statement?
The answer to this apparent contradiction is actually found in the compiler's error message itself. At statement 3 the compiler states something like:
error: redeclaration of 'Print p'which solves the problem, noting that within a compound statement objects and variables may be defined as well. Inside a compound statement, a type name followed by a
variable name
is the grammatical form of a
variable definition. Parentheses can be used to break priorities, but if
there are no priorities to break, they have no effect, and are simply ignored
by the compiler. In statement 3 the parentheses allowed us to get rid of the
blank that's required between a type name and the variable name, but to the
compiler we wrote
Print (p);which is, since the parentheses are superfluous, equal to
Print p;thus producing
p
's redeclaration.
As a further example: when we define a variable using a basic type (e.g.,
double
) using superfluous parentheses the compiler will quietly remove
these parentheses for us:
double ((((a)))); // weird, but OK.
To summarize our findings about anonymous variables:
const
reference
parameters.
Person::name()
:
std::string const &Person::name() const { return d_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"); cout << frank.name();the following actions take place:
Person::name()
is called.
name
of the object frank
as a
reference.
cout
.
Especially the first part of these actions results in some time loss, since an
extra function call is necessary to retrieve the value of the name
field.
Sometimes a faster procedure may be desirable, in which the name
field
becomes immediately available, without ever actually calling a function
name()
. This can be implemented using
inline
functions. An inline
function is a request to the compiler to insert the function's code at the
location of the function's call. This speeds up execution by avoiding a
function call, which typically comes with some (stack handling and parameter
passing) overhead. Note that inline
is a request to the compiler: the
compiler may decide to ignore it, and will probably ignore it when the
function's body contains much code. Good programming discipline suggests to be
aware of this, and to avoid inline
unless the function's body is fairly
small. More on this in section 6.3.2.
Person
this results in the following implementation of
name()
:
class Person { public: std::string const &name() const { return d_name; } };Note that the inline code of the function
name()
now literally
occurs inline in the interface of the class Person
. The keyword const
occurs after the function declaration, and before the code block.
Although members can be defined inside the class interface itself, it should be considered bad practice because of the following considerations:
name()
member of the Person
class is therefore preferably defined as follows:
class Person { public: std::string const &name() const; }; inline std::string const &Person::name() const { return d_name; }This version of the
Person
class clearly shows that:
inline
keyword and including the
appropriate class-header file. E.g.,
#include "person.h" std::string const &Person::name() const { return d_name; }
This construction, where the function code itself is inserted rather than a call to the function, is called an inline function. Note that using inline functions may result in multiple occurrences of the code of those functions in a program: one copy for each invocation 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. The compiler knows this too, and considers the use of inline functions a request rather than a command: if the compiler considers the function too long, it will not grant the request, but will, instead, treat the function as a normal function. As a rule of thumb: members should only be defined inline if they are small (containing a single, small statement) and if it is highly unlikely that their definition will ever change.
inline
functions be used, and when
not? There are some
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 to perform for such optimizations.
inline
functions can be used when member functions consist of one
very simple statement (such as the return statement in the function
Person::name()
).
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
spent during a function call is long compared to the code in the
function. An example of an inline
function which will hardly have any
effect on the program's speed is:
void Person::printname() const { cout << d_name << endl; }This function, which is, for the sake of the example, presented as 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 an
insignificant gain in execution time.
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 once again that the keyword inline
is not really a
command to the compiler. Rather, it is a request the compiler may
or may not grant.
For example, the class Person
holds information about the name,
address and phone number. This information is stored in string
data
members, which are themselves objects: composition.
Composition is not extraordinary or C++ specific: in C
a struct
or union
field is commonly used in other compound types.
The initialization of composed objects deserves some special attention: the topics of the coming sections.
Often it is desirable to initialize a composed object from a specific
constructor of the composing class. This is illustrated below for the
class Person
. In this fragment it assumed that a constructor
for a Person
should be defined expecting four arguments: the name, address
and phone number plus the person's weight:
Person::Person(char const *name, char const *address, char const *phone, size_t weight) : d_name(name), d_address(address), d_phone(phone), d_weight(weight) {}Following the argument list of the constructor
Person::Person()
, the
constructors of the string
data members are explicitly called, e.g.,
d_name(name)
. The initialization takes place before the code block of
Person::Person()
(now empty) is executed. This construction, where member
initialization takes place before the code block itself is executed is called
member initialization. Member initialization can be made explicit in the
member initializer list, that may appear after the parameter list, between
a colon (announcing the start of the member initializer list) and the opening
curly brace of the code block of the constructor.
Member initialization always occurs when objects are composed in classes: if no constructors are mentioned in the member initializer list the default constructors of the objects are called. Note that this only holds true for objects. Data members of primitive data types are not initialized automatically.
Member initialization can, however, also be used for primitive data members,
like int
and double
. The above example shows the initialization of the
data member d_weight
from the parameter weight
. Note that with member
initializers the data member could even have the same name as the constructor
parameter (although this is deprecated): with member initialization there is
no ambiguity and the first (left) identifier in, e.g., weight(weight)
is
interpreted as the data member to be initialized, whereas the identifier
between parentheses is interpreted as the parameter.
When a class has multiple composed data members, all members can be initialized using a `member initializer list': this list consists of the constructors of all composed objects, separated by commas. The order in which the objects are initialized is defined by the order in which the members are defined in the class interface. If the order of the initialization in the constructor differs from the order in the class interface, the compiler complains, and reorders the initialization so as to match the order of the class interface.
Member initializers should be used as often as possible: it can be downright
necessary to use them, and not using member initializers can result in
inefficient code: with objects always at least the default constructor is
called. So, in the following example, first the string
members are
initialized to empty strings, whereafter these values are immediately
redefined to their intended values. Of course, the immediate initialization to
the intended values would have been more efficent.
Person::Person(char const *name, char const *address, char const *phone, size_t weight) { d_name = name; d_address = address; d_phone = phone; d_weight = weight; }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 too much.
This means that when the definition of a Person
is altered so as to
contain a string const birthday
member, the implementation of the
constructor Person::Person()
in which also the birthday must be
initialized, a member initializer must be used for birthday
. Direct
assignment of the birthday would be illegal, since birthday
is a const
data member. The next example illustrates the const
data member
initialization:
Person::Person(char const *name, char const *address, char const *phone, char const *birthday, size_t weight) : d_name(name), d_address(address), d_phone(phone), d_birthday(birthday), // assume: string const d_birthday d_weight(weight) {}Concluding, the rule of thumb is the following: when composition of objects is used, the member initializer method is preferred to explicit initialization of composed objects. This not only results in more efficient code, but it also allows composed objects to be declared as
const
objects.
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. Bluntly passing an object (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 { d_conf = conf; // copying to conf_member }
Configfile
objects are used, as in:
Process::Process(Configfile *conf) // pointer to external object { d_conf = conf; // d_conf 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.
Configfile &d_conf
reference data member:
Process::Process(Configfile &conf) { d_conf = conf; // wrong: no assignment }The statement
d_conf = 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 (d_conf
). 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 d_conf
refer? To no variable, since
we haven't initialized d_conf
. After all, the whole purpose of the
statement d_conf = conf
was to initialize d_conf
....
So, how do we proceed when d_conf
must be initialized? In this
situation we once again use the member initializer syntax. The following
example shows the correct way to initialize d_conf
:
Process::Process(Configfile &conf) : d_conf(conf) // initializing reference member {}Note that this syntax must be used in all cases where reference data members are used. If
d_ir
would be an int
reference data member, a
construction like
Process::Process(int &ir) : d_ir(ir) {}would have been called for.
Local classes can be very useful in advanced applications involving inheritance or templates (cf. section 13.8). At this point in the Annotations they have limited use, although it is possible to describe their main features now. Refer to the example shown at the end of this section for code-examples illustrating the following featurees of local classes:
enum
may be anonymous, exposing only the
enum
values.
Local
cannot directly access main
's argc
parameter.
#include <iostream> #include <string> using namespace std; int main(int argc, char *argv[]) { static size_t staticValue = 0; class Local { int d_argc; // non-static data members OK public: enum // enums OK { value = 5 }; Local(int argc) // constructors and member functions OK : // in-class implementation required d_argc(argc) { // global data: accessible cout << "Local constructor\n"; // static function variables: accessible staticValue += 5; } static void hello() // static member functions: OK { cout << "hello world\n"; } }; Local::hello(); // call Local static member Local loc(argc); // define object of a local class. return 0; }
const
member functions and const
objects were introduced.
C++, however, allows the construction of objects which are, in a sense,
neither const
objects, nor non-const
objects. Data members which
are defined using the keyword
mutable
, can be modified by const
member
functions.
An example of a situation where mutable
might come in handy is where a
const
object needs to register the number of times it was used. The
following example illustrates this situation:
#include <string> #include <iostream> #include <memory> class Mutable { std::string d_name; mutable int d_count; // uses mutable keyword public: Mutable(std::string const &name) : d_name(name), d_count(0) {} void called() const { std::cout << "Calling " << d_name << " (attempt " << ++d_count << ")\n"; } }; int main() { Mutable const x("Constant mutable object"); for (int idx = 0; idx < 4; idx++) x.called(); // modify data of const object } /* Generated output: Calling Constant mutable object (attempt 1) Calling Constant mutable object (attempt 2) Calling Constant mutable object (attempt 3) Calling Constant mutable object (attempt 4) */
The keyword mutable
may also be useful in classes implementing, e.g.,
reference counting. Consider a class implementing reference counting for
textstrings. The object doing the reference counting might be a const
object, but the class may define a copy constructor. Since const
objects
can't be modified, how would the copy constructor be able to increment the
reference count? Here the mutable
keyword may profitably be used, as it
can be incremented and decremented, even though its object is a const
object.
The advantage of having a mutable
keyword is that, in the end, the
programmer decides which data members can be modified and which data members
can't. But that might as well be a disadvantage: having the keyword
mutable
around prevents us from making rigid assumptions about the
stability of const
objects. Depending on the context, that may or may not
be a problem. In practice, mutable
tends to be useful only for internal
bookkeeping purposes: accessors returning values of mutable data members might
return puzzling results to clients using these accessors with const
objects. In those situations, the nature of the returned value should clearly
be documented. As a
rule of thumb: do not use mutable
unless there is a
very clear reason to violate this rule.
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 member functions of classes. With source files there are basically two approaches:
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 frequently has to read header files which aren't actually used by the function defined in the source file.
With computers running faster and faster (and compilers getting smarter and
smarter) I think the second alternative is to be preferred over the first
alternative. So, as a starting point source files of a particular class
MyClass
could be 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 (e.g., /usr/local/include/myheaders/
).
If a class MyClass
is developed there, create a subdirectory (or
subdirectory link) myheaders
of one of the standard 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 <myheaders/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.
Assume the File
class has a member gets(String &destination)
, while
the class String
has a member function getLine(File &file)
. 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); }; #endifHowever, 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); }; #endifNow we have created a problem. The compiler, trying to compile the source file of the function
File::gets()
proceeds as follows:
project/file.h
is opened to be read;
File_h_
is defined
project/string.h
is opened to be read
String_h_
is defined
project/file.h
is (again) opened to be read
File_h_
is already defined, so the remainder of
project/file.h
is skipped.
String
is now parsed.
File
object is
encountered.
class File
hasn't been parsed yet, a File
is still
an undefined type, and the compiler quits with an error.
#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 #endifA 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 #endifThis works well in all situations where either references or pointers to another classes are involved and with (non-inline) member functions having class-type return values or parameters.
Note that this setup doesn't work with
composition, nor with in-class
inline member functions. Assume the class File
has a composed data
member of the class String
. In that case, the class interface of the class
File
must include the header file of the class String
before the
class interface 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 interface of the File
class is completed.
In cases where classes contain composed objects (or are derived from other
classes, see chapter 13) the header files of the classes of the
composed objects must have been read before the class interface 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 { String d_line; // composition ! public: void gets(String &string); }; #endifNote 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 remaining header files (appearing below the class interface itself) are required only because they are used by the class's source files.
This approach allows us to introduce yet another refinement:
#ifndef ... #endif
construction introduced
in section 2.5.9.
INCLUDE
path.
#include <string>
) as
well. The class header file itself as well as these additional header files
should be included in a separate internal header file (for which the extension
.ih
(`
internal header') is suggested).
The .ih
file should be defined in the same directory as the source
files of the class, and has the following characteristics:
#ifndef
.. #endif
shield, as the header file is never included by other header files.
.h
header file defining the class interface
is included.
.h
header file are included.
/usr/local/include/myheaders/file.h
:
#ifndef File_h_ #define File_h_ #include <fstream> // for composed 'ifstream' class Buffer; // forward reference class File // class interface { ifstream d_instream; public: void gets(Buffer &buffer); }; #endif
#include <myheaders/file.h> // make the class File known #include <buffer.h> // make Buffer known to File #include <string> // used by members of the class #include <sys/stat.h> // File.
using
directives should not be used in these header
files if they are to be used as general header files declaring classes or
other entities from a
library. When the using
directive is used in a
header file then users of such a header file are forced to accept and use the
declarations in all code that includes the particular header file.
For example, if in a namespace special
an object Inserter cout
is
declared, then special::cout
is of course a different object than
std::cout
. Now, if a class Flaw
is constructed, in which the
constructor expects a reference to a special::Inserter
, then the class
should be constructed as follows:
class special::Inserter; class Flaw { public: Flaw(special::Inserter &ins); };Now the person designing the class
Flaw
may be in a
lazy mood, and
might get bored by continuously having to prefix special::
before every
entity from that namespace. So, the following construction is used:
using namespace special; class Inserter; class Flaw { public: Flaw(Inserter &ins); };This works fine, up to the point where somebody wants to include
flaw.h
in other source files: because of the using
directive, this
latter person is now by implication also using namespace special
, which
could produce unwanted or unexpected effects:
#include <flaw.h> #include <iostream> using std::cout; int main() { cout << "starting" << endl; // doesn't compile }The compiler is confronted with two interpretations for
cout
: first,
because of the using
directive in the flaw.h
header file, it considers
cout
a special::Inserter
, then, because of the using
directive in
the user program, it considers cout
a std::ostream
. As compilers do,
when confronted with an ambiguity, an error is reported.
As a
rule of thumb, header files intented to be generally used should
not contain using
declarations. This rule does not hold true
for header files which are included only by the sources of a class: here the
programmer is free to apply as many using
declarations as desired, as
these directives never reach other sources.