280 Thinking in C++ www.BruceEckel.com
int i;
char j;
float f;
void func();
};
void B::func() {}
int main() {
A a; B b;
a.i = b.i = 1;
a.j = b.j = 'c';
a.f = b.f = 3.14159;
a.func();
b.func();
} ///:~
The
private
keyword, on the other hand, means that no one can
access that member except you, the creator of the type, inside
function members of that type.
private
is a brick wall between you
and the client programmer; if someone tries to access a
private
member, they’ll get a compile-time error. In
struct B
in the example
can access any member of
B
(because
func( )
is a
member of
B
, thus automatically granting it permission), an
ordinary global function like
main( )
cannot. Of course, neither can
member functions of other structures. Only the functions that are
clearly stated in the structure declaration (the “contract”) can have
access to
private
members.
There is no required order for access specifiers, and they may
appear more than once. They affect all the members declared after
them and before the next access specifier.
protected
The last access specifier is
protected
.
protected
acts just like
private
, with one exception that we can’t really talk about right
now: “Inherited” structures (which cannot access
private
members)
Bob
!”
and expect to see the
private
and
protected
members of
Bob
.
You can declare a global function as a
friend
, and you can also
declare a member function of another structure, or even an entire
structure, as a
friend
. Here’s an example :
//: C05:Friend.cpp
// Friend allows special access
// Declaration (incomplete type specification):
struct X;
struct Y {
void f(X*);
};
struct X { // Definition
private:
int i;
public:
}
void Z::g(X* x) {
x->i += j;
}
void h() {
X x;
x.i = 100; // Direct data manipulation
}
int main() {
X x;
Z z;
z.g(&x);
} ///:~
struct Y
has a member function
f( )
that will modify an object of
type
X
. This is a bit of a conundrum because the C++ compiler
requires you to declare everything before you can refer to it, so
struct Y
must be declared before its member
Y::f(X*)
can be
declared as a friend in
X
prior to declaring
Y::f(X*)
. This is
accomplished in the declaration:
struct X;
This declaration simply tells the compiler there’s a
struct
by that
name, so it’s OK to refer to it as long as you don’t require any more
knowledge than the name.
Now, in
struct X
, the function
Y::f(X*)
can be declared as a
friend
with no problem. If you tried to declare it before the compiler had
seen the full specification for
Y
, it would have given you an error.
This is a safety feature to ensure consistency and eliminate bugs.
Notice the two other
friend
functions. The first declares an
ordinary global function
g( )
as a
declaration, otherwise
it would be seen by the compiler as a non-member. Here’s an
example:
//: C05:NestFriend.cpp
// Nested friends
#include <iostream>
5: Hiding the Implementation 285
#include <cstring> // memset()
using namespace std;
const int sz = 20;
struct Holder {
private:
int a[sz];
public:
void initialize();
struct Pointer;
friend Pointer;
struct Pointer {
private:
Holder* h;
int* p;
public:
void initialize(Holder* h);
// Move around in the array:
void next();
void previous();
void top();
void end();
// Access values:
int Holder::Pointer::read() {
return *p;
}
void Holder::Pointer::set(int i) {
*p = i;
}
int main() {
Holder h;
Holder::Pointer hp, hp2;
int i;
h.initialize();
hp.initialize(&h);
hp2.initialize(&h);
for(i = 0; i < sz; i++) {
hp.set(i);
hp.next();
}
hp.top();
hp2.end();
for(i = 0; i < sz; i++) {
cout << "hp = " << hp.read()
<< ", hp2 = " << hp2.read() << endl;
hp.next();
hp2.previous();
}
} ///:~
main( )
and use them to select different
parts of the array.
Pointer
is a structure instead of a raw C pointer,
so you can guarantee that it will always safely point inside the
Holder
.
The Standard C library function
memset( )
(in
<cstring>
) is used
for convenience in the program above. It sets all memory starting at
a particular address (the first argument) to a particular value (the
second argument) for
n
bytes past the starting address (
n
is the
third argument). Of course, you could have simply used a loop to
iterate through all the memory, but
memset( )
is available, well-
tested (so it’s less likely you’ll introduce an error), and probably
more efficient than if you coded it by hand.
Is it pure?
The class definition gives you an audit trail, so you can see from
looking at the class which functions have permission to modify the
private parts of the class. If a function is a
When you start using access specifiers, however, you’ve moved
completely into the C++ realm, and things change a bit. Within a
particular “access block” (a group of declarations delimited by
access specifiers), the variables are guaranteed to be laid out
contiguously, as in C. However, the access blocks may not appear
in the object in the order that you declare them. Although the
compiler will
usually
lay the blocks out exactly as you see them,
there is no rule about it, because a particular machine architecture
and/or operating environment may have explicit support for
private
and
protected
that might require those blocks to be placed
in special memory locations. The language specification doesn’t
want to restrict this kind of advantage.
Access specifiers are part of the structure and don’t affect the
objects created from the structure. All of the access specification
information disappears before the program is run; generally this
happens during compilation. In a running program, objects become
“regions of storage” and nothing more. If you really want to, you
can break all the rules and access the memory directly, as you can
in C. C++ is not designed to prevent you from doing unwise things.
It just provides you with a much easier, highly desirable
alternative.
In general, it’s not a good idea to depend on anything that’s
implementation-specific when you’re writing a program. When
you must have implementation-specific dependencies, encapsulate
5: Hiding the Implementation 289
programming, where a structure is describing a class of objects as
you would describe a class of fishes or a class of birds: Any object
belonging to this class will share these characteristics and
behaviors. That’s what the structure declaration has become, a
description of the way all objects of this type will look and act.
In the original OOP language, Simula-67, the keyword
class
was
used to describe a new data type. This apparently inspired
Stroustrup to choose the same keyword for C++, to emphasize that
1
As noted before, sometimes access control is referred to as encapsulation.
290 Thinking in C++ www.BruceEckel.com
this was the focal point of the whole language: the creation of new
data types that are more than just C
struct
s with functions. This
certainly seems like adequate justification for a new keyword.
However, the use of
class
in C++ comes close to being an
unnecessary keyword. It’s identical to the
struct
keyword in
absolutely every way except one:
class
defaults to
private
, whereas
void g();
};
int B::f() {
return i + j + k;
}
void B::g() {
5: Hiding the Implementation 291
i = j = k = 0;
}
int main() {
A a;
B b;
a.f(); a.g();
b.f(); b.g();
} ///:~
The
class
is the fundamental OOP concept in C++. It is one of the
keywords that will
not
be set in bold in this book – it becomes
annoying with a word repeated as often as “class.” The shift to
classes is so important that I suspect Stroustrup’s preference would
have been to throw
struct
out altogether, but the need for
class X {
void private_function();
292 Thinking in C++ www.BruceEckel.com
int internal_representation;
public:
void interface_function();
};
Some people even go to the trouble of decorating their own private
names:
class Y {
public:
void f();
private:
int mX; // "Self-decorated" name
};
Because
mX
is already hidden in the scope of
Y
, the
m
(for
“member”) is unnecessary. However, in projects with many global
variables (something you should strive to avoid, but which is
sometimes inevitable in existing projects), it is helpful to be able to
distinguish inside a member function definition which data is
global and which is a member.
Modifying Stash to use access control
private
because it is used
only by the
add( )
function and is thus part of the underlying
implementation, not the interface. This means that, sometime later,
you can change the underlying implementation to use a different
system for memory management.
Other than the name of the include file, the header above is the
only thing that’s been changed for this example. The
implementation file and test file are the same.
Modifying Stack to use access control
As a second example, here’s the
Stack
turned into a class. Now the
nested data structure is
private
, which is nice because it ensures
that the client programmer will neither have to look at it nor be
able to depend on the internal representation of the
Stack
:
//: C05:Stack2.h
// Nested structs via linked list
#ifndef STACK2_H
#define STACK2_H
class Stack {
struct Link {
void* data;
compiler must still see the declarations for all parts of an object in
order to create and manipulate it properly. You could imagine a
programming language that requires only the public interface of an
object and allows the private implementation to be hidden, but C++
performs type checking statically (at compile time) as much as
possible. This means that you’ll learn as early as possible if there’s
an error. It also means that your program is more efficient.
However, including the private implementation has two effects: the
implementation is visible even if you can’t easily access it, and it
can cause needless recompilation.
5: Hiding the Implementation 295
Hiding the implementation
Some projects cannot afford to have their implementation visible to
the client programmer. It may show strategic information in a
library header file that the company doesn’t want available to
competitors. You may be working on a system where security is an
issue – an encryption algorithm, for example – and you don’t want
to expose any clues in a header file that might help people to crack
the code. Or you may be putting your library in a “hostile”
environment, where the programmers will directly access the
private components anyway, using pointers and casting. In all
these situations, it’s valuable to have the actual structure compiled
inside an implementation file rather than exposed in a header file.
Reducing recompilation
The project manager in your programming environment will cause
a recompilation of a file if that file is touched (that is, modified)
or
if
another file it’s dependent upon – that is, an included header file –
is touched. This means that any time you make a change to a class,
//: C05:Handle.h
// Handle classes
#ifndef HANDLE_H
#define HANDLE_H
class Handle {
struct Cheshire; // Class declaration only
Cheshire* smile;
public:
void initialize();
void cleanup();
int read();
void change(int);
};
#endif // HANDLE_H ///:~
This is all the client programmer is able to see. The line
struct Cheshire;
is an
incomplete type specification
or a
class declaration
(A
class
definition
includes the body of the class.) It tells the compiler that
Cheshire
is a structure name, but it doesn’t give any details about
the
void Handle::change(int x) {
smile->i = x;
} ///:~
Cheshire
is a nested structure, so it must be defined with scope
resolution:
struct Handle::Cheshire {
In
Handle::initialize( )
, storage is allocated for a
Cheshire
structure, and in
Handle::cleanup( )
this storage is released. This
storage is used in lieu of all the data elements you’d normally put
into the
private
section of the class. When you compile
Handle.cpp
,
this structure definition is hidden away in the object file where no
one can see it. If you change the elements of
Cheshire
, the only file
that must be recompiled is
Handle.cpp
because the header file is
with the knowledge that no client programmer will be affected by
the changes because they can’t access that part of the class.
When you have the ability to change the underlying
implementation, you can not only improve your design at some
later time, but you also have the freedom to make mistakes. No
matter how carefully you plan and design, you’ll make mistakes.
Knowing that it’s relatively safe to make these mistakes means
you’ll be more experimental, you’ll learn faster, and you’ll finish
your project sooner.
The public interface to a class is what the client programmer
does
see, so that is the most important part of the class to get “right”
during analysis and design. But even that allows you some leeway
for change. If you don’t get the interface right the first time, you can
5: Hiding the Implementation 299
add
more functions, as long as you don’t remove any that client
programmers have already used in their code.
Exercises
Solutions to selected exercises can be found in the electronic document
The Thinking in C++ Annotated
Solution Guide
, available for a small fee from www.BruceEckel.com.
1. Create a class with
public
,
private
, and
. Print out the values. Now
replace
a
,
b,
and
c
with an array of
string s[3]
. Show that
your code in
main( )
breaks as a result of the change.
Now create a
class
called
Libc
, with
private
string
objects
a
,
b,
and
c
, and member functions
main( )
does
not
break as a result
of the change.
3. Create a class and a global
friend
function that
manipulates the
private
data in the class.
4. Write two classes, each of which has a member function
that takes a pointer to an object of the other class. Create
instances of both objects in
main( )
and call the
aforementioned member function in each class.
5. Create three classes. The first class contains
private
data,
and grants friendship to the entire second class and to a
member function of the third class. In
main( )
,
demonstrate that all of these work correctly.
6. Create a
Hen
class. Inside this, nest a
Nest
class. Inside
member function
showMap( )
that prints the names of
each of these data members and their addresses. If
possible, compile and run this program on more than one
compiler and/or computer and/or operating system to
see if there are layout differences in the object.
9. Copy the implementation and test files for
Stash
in
Chapter 4 so that you can compile and test
Stash.h
in this
chapter.
10. Place objects of the
Hen
class from Exercise 6 in a
Stash
.
Fetch them out and print them (if you have not already
done so, you will need to add
Hen::print( )
).
11. Copy the implementation and test files for
Stack
in
Chapter 4 so that you can compile and test
Stack2.h
in
this chapter.
int
, and one that uses a
vector<int>
. Have a preset maximum size for the stack so
you don’t have to worry about expanding the array in
the first version. Note that the
StackOfInt.h
class doesn’t
have to change with
StackImp
.
301 6: Initialization
& Cleanup
Chapter 4 made a significant improvement in library
use by taking all the scattered components of a typical
C library and encapsulating them into a structure (an
abstract data type, called a
class
from now on).
302 Thinking in C++ www.BruceEckel.com
This not only provides a single unified point of entry into a library
struct
by hand.) Cleanup is a special problem because C programmers are
comfortable with forgetting about variables once they are finished,
so any cleaning up that may be necessary for a library’s
struct
is
often missed.
6: Initialization & Cleanup 303
In C++, the concept of initialization and cleanup is essential for
easy library use and to eliminate the many subtle bugs that occur
when the client programmer forgets to perform these activities.
This chapter examines the features in C++ that help guarantee
proper initialization and cleanup.
Guaranteed initialization with the
constructor
Both the
Stash
and
Stack
classes defined previously have a
function called
initialize( )
, which hints by its name that it should
be called before using the object in any other way. Unfortunately,
this means the client programmer must ensure proper initialization.
Client programmers are prone to miss details like initialization in
their headlong rush to make your amazing library solve their
problem. In C++, initialization is too important to leave to the client
programmer. The class designer can guarantee initialization of
the same thing happens as if
a
were an
int
: storage is allocated for
the object. But when the program reaches the
sequence point
(point
of execution) where
a
is defined, the constructor is called
automatically. That is, the compiler quietly inserts the call to
X::X( )
for the object
a
at the point of definition. Like any member function,
the first (secret) argument to the constructor is the
this
pointer – the
address of the object for which it is being called. In the case of the
constructor, however,
this
is pointing to an un-initialized block of
memory, and it’s the job of the constructor to initialize this memory
properly.
Like any function, the constructor can have arguments to allow you
to specify how an object is created, give it initialization values, and
so on. Constructor arguments provide you with a way to guarantee