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. Send your email to Frank Brokken.Please state the document version you're referring to, as found in the title (in this document: 5.2.0a) and please state the paragraph you're referring to.
All mail received is seriously considered, and new (sub)releases of the Annotations will normally reflect your suggestions for improvements. Except for the incidental case I will not otherwise acknowledge the receipt of suggestions for improvements. Please don't misinterpret this for lack of appreciation.
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:
#include <string> class Person { string d_name, // name of person d_address, // address field d_phone; // telephone number unsigned d_weight; // the weight in kg. public: // interface functions void setname(string const &n); void setaddress(string const &a); void setphone(string const &p); void setweight(unsigned weight); string const &getname() const; string const &getaddress() const; string const &getphone() const; unsigned getweight() const; };
The data fields in this class are d_name, d_address, d_phone
and
d_weight
. All fields (but d_weight
) are string
objects. As the
data fields are not given a specific
access modifier, they are
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.,
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.
This chapter, hewever, will concentrate on the basic form of the class
and
of their
constructor members.
The constructor member function has by definition the same name as the
corresponding 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 makes sure that the
constructor of a class, if defined, is called when a variable of the class,
called an
object, 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
6.4) will `automatically' call the member initializers, and
therefore does perform some actions. We postpone the discussion of such
constructors to 6.4.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 locally defined (in a function), the constructor is called every time the function is called. The object's constructor is then 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 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 also holds true for the definition of the constructor function, as in:
Demo::Demo() // no return value here { // statements ... }
NOTE: Once a constructor is defined having arguments, the default constructor doesn't exist anymore, unless the default constructor is definied explicitly too.
The above note has important consequences, as the default constructor is required in cases where it must be able to construct an object either with or witout explicit initialization values. By merely defining a constructor having at least one arguments, 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 an unsigned d_weight
data
member. These data members can be manipulated by the
interface functions. The internal workings of the class are as follows:
when a name, address or phone number or weight of a Person
is defined, the
corresponding private data member is given a (new) value. An obvious setup is
as follows:
set...()
function) consists of the assignment of the new value to the
corresponding data member.
get...()
functions simply returns the corresponding data member
The 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(unsigned weight) { d_weight = weight; }
The get...()
interface functions are now defined. 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. Furthermore, notice that the
return values of the member functions of the string
data members are
string const &
values: the const
here indicates that the caller of
the memberfunction cannot alter the returned value itself. The caller of
the get...()
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 more detail in section 6.2. The
return value of the getweight()
member function, however, is a plain
unsigned
, as this can be a simple copy of the value that's stored in the
Person
's weight
member:
#include "person.h" // given earlier // interface functions get...() string const &Person::getname() const { return d_name; } string const &Person::getaddress() const { return d_address; } string const &Person::getphone() const { return d_phone; } unsigned Person::getweight() const { return d_weight; }
The class definition of the Person
class given earlier can still be
used. The set...()
and get...()
functions merely implement the member
functions declared in that class definition.
To demonstrate the use of the class Person
, an example is now given. 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 argument is declared const
.
Alternatively, the function printperson()
could 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.getname() << endl << "Address : " << p.getaddress() << endl << "Phone : " << p.getphone() << endl << "Weight : " << p.getweight() << 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 may be handy which expects three
strings: the name, address and phone number. Such a constructor is, for
example:
Person::Person(string const &name, string const &address, string const &phone, unsigned weight) { d_name = name; d_address = address; d_phone = phone; d_weight = weight; }The constructor must be declared in the class declaration as well:
class Person { public: Person(string const &name, string const &address, string const &phone, unsigned 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 value of its name,
address and phone 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(string const &name, string const &address, string const &phone, unsigned 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: these data members are by
default
initialized to empy strings themselves. However, there is also an unsigned
data member. That member is a variable of a basic type, contrary to the
string
members, which already use constructors of their own. So, unless
the value of the 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), 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(string const &name, string const &address = "--unknown--", string const &phone = "--unknown--", unsigned weight = 0); // rest of the class interface };
6.1.2.1: The order of construction
The possibility to pass arguments to constructors offers us the chance to
monitor at which moment in a program's execution an object is created
This is shown in the next listing, using a class 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> 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 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. These member functions are called
const member functions.
Using the example of the class Person
, we see that the get...()
functions were declared const
:
class Person { public: string const &getname() const; string const &getaddress() const; string const &getphone() const; };As illustrated in this fragment, the keyword
const
appears
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 const
specification must be repeated in the definition of member
functions themselves:
string const &Person::getname() 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 getname()
would result in a
compilation error.
Const
member functions exist because C++ allows const
objects to
be created, or 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 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", "f.b.brokken@rc.rug.nl", "363 3688");
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.
Person::getname()
:
string const &Person::getname() 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.getname();the following actions take place:
Person::getname()
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; 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: string const &getname() const { return d_name; } };Note that the inline code of the function
getname()
now literally
occurs inline 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 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.
inline
in
the function definition. The interface and implementation in this case are as
follows:
class Person { public: string const &getname() const; }; inline string const &Person::getname() const { return d_name; }Again, the compiler will insert the code of the function
getname()
instead of generating a call.
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 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 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 hardly any effect at all
is the following:
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 a very 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 using the keyword inline
is not really a
command for 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 it is quite
common to include a struct
or union
in other compound types.
The initialization of composed objects deserves some extra 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, unsigned 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.,
name(mn)
. 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 automatically initialized.
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 weight
from the parameter weight
. For further
illustration, we used the same identifiers here: with member initialization
there is no ambiguity and the first (left) identifier in weight(weight)
is
always 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 much 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, unsigned 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, unsigned 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. 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 { d_conf = conf; // copying to conf_member }
Configfile
objects, 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 can 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.
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 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, as a starting point 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 (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
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.
#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. But it doesn't work with composition, nor with inline member functions. 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 13) 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 { String // composition ! d_line; 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 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.
This approach allows us to introduce yet another refinement:
#ifndef ... #endif
construction introduced
in section 2.5.9. Sources using the class only need to include this
header file
(Lakos, (2001) refines this process even
further. See his book Large-Scale C++ Software Design for further
details). This header file should be made available in a well-known location,
such as a directory or subdirectory of the standard
INCLUDE
path.
#include
<string>
) are required as well. The class header file itself as well as these
additional header files are included in a separate internal header file (for
which the extension .ih
(`
internal header') is
suggested. (In the past I tended to use the extension .H
for this
internal header file. However, some file systems (guess which?) have problems
distinguishing lower- and uppercase filename characters. Therefore another
extension is probably preferable to the .H
extension).)
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 definition { 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 want 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::Extractor
, 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.
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 would want to register the number of times it was used. A
simple class allowing just, as well as an example in which a const
object
of the class Mutable
that could be:
#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 advantage of having a mutable
keyword is of 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 assumptions about the stability of
const
objects. For one's own classes the advantage might outweight the
disadvantage, but when classes are used which are developed elsewhere (like
the classes of the
Qt
Graphical User Interface Toolkit (see
http://www.trolltech.com
)), using a mutable
keyword might be more of a
disadvantage than an advantage (I stress, however that I'm not
suggesting here that the keyword mutable
is used in Qt's
sources). The least one should do when classes are developed which are
intended to be used by others is to document the use of the mutable
keyword very clearly to the potential users of the class.
Personally I think the use of the mutable
keyword is best avoided
altogether.