Thinking in C plus plu (P9) - Pdf 16

380 Thinking in C++ www.BruceEckel.com

int main() {
cout << "sizeof(Bunch) = " << sizeof(Bunch)
<< ", sizeof(i[1000]) = "
<< sizeof(int[1000]) << endl;
} ///:~

The use of
enum
here is guaranteed to occupy no storage in the
object, and the enumerators are all evaluated at compile time. You
can also explicitly establish the values of the enumerators:
enum { one = 1, two = 2, three };

With integral
enum
types, the compiler will continue counting
from the last value, so the enumerator
three
will get the value 3.
In the
StringStack.cpp
example above, the line:
static const int size = 100;

would be instead:
enum { size = 100 };

Although you’ll often see the
enum

Here,
b
is a
const
object of type
blob
. Its constructor is called with
an argument of two. For the compiler to enforce
const
ness, it must
ensure that no data members of the object are changed during the
object’s lifetime. It can easily ensure that no public data is modified,
but how is it to know which member functions will change the data
and which ones are “safe” for a
const
object?
If you declare a member function
const
, you tell the compiler the
function can be called for a
const
object. A member function that is
not specifically declared
const
is treated as one that will modify
data members in an object, and the compiler will not allow you to
call it for a
const
object.
It doesn’t stop there, however. Just


means the return value is
const
, so that doesn’t produce the desired
results. Instead, you must place the
const
specifier
after
the
argument list. For example,
//: C08:ConstMember.cpp
class X {
int i;
public:
X(int ii);
int f() const;
};

X::X(int ii) : i(ii) {}
int X::f() const { return i; }
382 Thinking in C++ www.BruceEckel.com

int main() {
X x1(10);
const X x2(20);
x1.f();
x2.f();
} ///:~

Note that the

const
objects.
Here’s an example that contrasts a
const
and non-
const
member
function:
//: C08:Quoter.cpp
// Random quote selection
#include <iostream>
#include <cstdlib> // Random number generator
#include <ctime> // To seed random generator
using namespace std;

class Quoter {
int lastquote;
public:
Quoter();
int lastQuote() const;
const char* quote();
};

Quoter::Quoter(){
lastquote = -1;
srand(time(0)); // Seed random number generator
8: Constants 383
}

int Quoter::lastQuote() const {

Neither constructors nor destructors can be
const
member
functions because they virtually always perform some modification
on the object during initialization and cleanup. The
quote( )

member function also cannot be
const
because it modifies the data
member
lastquote
(see the
return
statement). However,
lastQuote( )
makes no modifications, and so it can be
const
and can
be safely called for the
const
object
cq
.
384 Thinking in C++ www.BruceEckel.com
mutable: bitwise vs. logical const
What if you want to create a
const
member function, but you’d still
like to change some of the data in the object? This is sometimes

const
member function.

The first approach is the historical one and is called
casting away
constness
. It is performed in a rather odd fashion. You take
this
(the
keyword that produces the address of the current object) and cast it
to a pointer to an object of the current type. It would seem that
this

is
already
such a pointer. However, inside a
const
member function
it’s actually a
const
pointer, so by casting it to an ordinary pointer,
you remove the
const
ness for that operation. Here’s an example:
//: C08:Castaway.cpp
// "Casting away" constness

class Y {
int i;
public:

keyword in the class declaration to specify that a
particular data member may be changed inside a
const
object:
//: C08:Mutable.cpp
// The "mutable" keyword

class Z {
int i;
mutable int j;
public:
Z();
void f() const;
};

Z::Z() : i(0), j(0) {}

void Z::f() const {
//! i++; // Error const member function
j++; // OK: mutable
}

int main() {
const Z zz;
zz.f(); // Actually changes it!
} ///:~

This way, the user of the class can see from the declaration which
members are likely to be modified in a
const

or
struct
must have no user-defined constructors or
destructor.
2. There can be no base classes (covered in Chapter 14) or
member objects with user-defined constructors or
destructors.
The effect of a write operation on any part of a
const
object of a
ROMable type is undefined. Although a suitably formed object
may be placed in ROM, no objects are ever
required
to be placed in
ROM.
volatile
The syntax of
volatile
is identical to that for
const
, but
volatile

means “This data may change outside the knowledge of the
compiler.” Somehow, the environment is changing the data
(possibly through multitasking, multithreading or interrupts), and
volatile
tells the compiler not to make any assumptions about that
data, especially during optimization.
If the compiler says, “I read this data into a register earlier, and I

unsigned char buf[bufsize];
int index;
public:
Comm();
void isr() volatile;
char read(int index) const;
};

Comm::Comm() : index(0), byte(0), flag(0) {}

// Only a demo; won't actually work
// as an interrupt service routine:
void Comm::isr() volatile {
flag = 0;
buf[index++] = byte;
// Wrap to beginning of buffer:
if(index >= bufsize) index = 0;
}

char Comm::read(int index) const {
if(index < 0 || index >= bufsize)
return 0;
return buf[index];
}

int main() {
388 Thinking in C++ www.BruceEckel.com
volatile Comm Port;
Port.isr(); // OK
//! Port.read(0); // Error, read() not volatile

, so discussions of the two
are often treated together. The two are referred to in combination as
the
c-v qualifier
.
Summary
The
const
keyword gives you the ability to define objects, function
arguments, return values and member functions as constants, and
to eliminate the preprocessor for value substitution without losing
any preprocessor benefits. All this provides a significant additional
form of type checking and safety in your programming. The use of
so-called
const correctness
(the use of
const
anywhere you possibly
can) can be a lifesaver for projects.
Although you can ignore
const
and continue to use old C coding
practices, it’s there to help you. Chapters 11 and on begin using
references heavily, and there you’ll see even more about how
critical it is to use
const
with function arguments.
Exercises
Solutions to selected exercises can be found in the electronic document
The Thinking in C++ Annotated

const
definition in a header file, include that
header file in two
.cpp
files, then compile those files and
link them. You should not get any errors. Now try the
same experiment with C.
5. Create a
const
whose value is determined at runtime by
reading the time when the program starts (you’ll have to
use the
<ctime>
standard header). Later in the program,
try to read a second value of the time into your
const
and
see what happens.
6. Create a
const
array of
char
, then try to change one of the
char
s.
7. Create an
extern const
declaration in one file, and put a
main( )
in that file that prints the value of the

const
object. Show that you can
only read the value that the pointer points to, but you
can’t change the pointer or what it points to.
11. Remove the comment on the error-generating line of
code in
PointerAssignment.cpp
to see the error that your
compiler generates.
12. Create a character array literal with a pointer that points
to the beginning of the array. Now use the pointer to
modify elements in the array. Does your compiler report
this as an error? Should it? If it doesn’t, why do you think
that is?
13. Create a function that takes an argument by value as a
const
; then try to change that argument in the function
body.
14. Create a function that takes a
float
by value. Inside the
function, bind a
const float&
to the argument, and only
use the reference from then on to ensure that the
argument is not changed.
15. Modify
ConstReturnValues.cpp
removing comments on
the error-causing lines one at a time, to see what error


and has a constructor that initializes the
string
, and a

print( )
function. Modify
StringStack.cpp
so that the
container holds
MyString
objects, and
main( )
so it prints
them.
21. Create a class containing a
const
member that you
initialize in the constructor initializer list and an
untagged enumeration that you use to determine an
array size.
22. In
ConstMember.cpp
, remove the
const
specifier on the
member function definition, but leave it on the
declaration, to see what kind of compiler error message
you get.
23. Create a class with both

by making
quote( )
a
const
member
function and
lastquote

mutable
.
27. Create a class with a
volatile
data member. Create both
volatile
and non-
volatile
member functions that modify
the
volatile
data member, and see what the compiler
says. Create both
volatile
and non-
volatile
objects of
your class and try calling both the
volatile
and non-
volatile
member functions to see what is successful and

9: Inline Functions
One of the important features C++ inherits from C is
efficiency. If the efficiency of C++ were dramatically
less than C, there would be a significant contingent of
programmers who couldn’t justify its use.
394 Thinking in C++ www.BruceEckel.com
In C, one of the ways to preserve efficiency is through the use of
macros
, which allow you to make what looks like a function call
without the normal function call overhead. The macro is
implemented with the preprocessor instead of the compiler proper,
and the preprocessor replaces all macro calls directly with the
macro code, so there’s no cost involved from pushing arguments,
making an assembly-language CALL, returning arguments, and
performing an assembly-language RETURN. All the work is
performed by the preprocessor, so you have the convenience and
readability of a function call but it doesn’t cost you anything.
There are two problems with the use of preprocessor macros in
C++. The first is also true with C: a macro looks like a function call,
but doesn’t always act like one. This can bury difficult-to-find bugs.
The second problem is specific to C++: the preprocessor has no
permission to access class member data. This means preprocessor
macros cannot be used as class member functions.
To retain the efficiency of the preprocessor macro, but to add the

can actually
call
the macro with the gap
F (1)

and it will still expand properly to
(1 + 1)

The example above is fairly trivial and the problem will make itself
evident right away. The real difficulties occur when using
expressions as arguments in macro calls.
There are two problems. The first is that expressions may expand
inside the macro so that their evaluation precedence is different
from what you expect. For example,
#define FLOOR(x,b) x>=b?0:1

Now, if expressions are used for the arguments
if(FLOOR(a&0x0f,0x07)) //

the macro will expand to
if(a&0x0f>=0x07?0:1)

The precedence of
&
is lower than that of
>=
, so the macro
evaluation will surprise you. Once you discover the problem, you
can solve it by putting parentheses around everything in the macro
396 Thinking in C++ www.BruceEckel.com


As long as you use an “ordinary” argument, the macro works very
much like a real function. But as soon as you relax and start
believing it
is
a real function, the problems start. Thus:
//: C09:MacroSideEffects.cpp
#include " /require.h"
#include <fstream>
using namespace std;

#define BAND(x) (((x)>5 && (x)<10) ? (x) : 0)

9: Inline Functions 397
int main() {
ofstream out("macro.out");
assure(out, "macro.out");
for(int i = 4; i < 11; i++) {
int a = i;
out << "a = " << a << endl << '\t';
out << "BAND(++a)=" << BAND(++a) << endl;
out << "\t a = " << a << endl;
}
} ///:~

Notice the use of all upper-case characters in the name of the
macro. This is a helpful practice because it tells the reader this is a
macro and not a function, so if there are problems, it acts as a little
reminder.
Here’s the output produced by the program, which is not at all

becomes five, which is what you would expect from a
normal function call in the same situation. However, when the
number is within the band, both conditionals are tested, which
results in two increments. The result is produced by evaluating the
argument again, which results in a third increment. Once the
number gets out of the band, both conditionals are still tested so
you get two increments. The side effects are different, depending
on the argument.
This is clearly not the kind of behavior you want from a macro that
looks like a function call. In this case, the obvious solution is to
make it a true function, which of course adds the extra overhead
and may reduce efficiency if you call that function a lot.
Unfortunately, the problem may not always be so obvious, and you
can unknowingly get a library that contains functions and macros
mixed together, so a problem like this can hide some very difficult-
to-find bugs. For example, the
putc( )
macro in
cstdio
may evaluate
its second argument twice. This is specified in Standard C. Also,
careless implementations of
toupper( )
as a macro may evaluate the
argument more than once, which will give you unexpected results
with
toupper(*p++)
.
1


Inline functions
In solving the C++ problem of a macro with access to
private
class
members,
all
the problems associated with preprocessor macros
were eliminated. This was done by bringing the concept of macros
under the control of the compiler where they belong. C++
implements the macro as
inline function
, which is a true function in
every sense. Any behavior you expect from an ordinary function,
you get from an inline function. The only difference is that an inline
function is expanded in place, like a preprocessor macro, so the
overhead of the function call is eliminated. Thus, you should
(almost) never use macros, only inline functions.
Any function defined within a class body is automatically inline,
but you can also make a non-class function inline by preceding it
with the
inline
keyword. However, for it to have any effect, you
must include the function body with the declaration, otherwise the
compiler will treat it as an ordinary function declaration. Thus,
inline int plusOne(int x);

has no effect at all other than declaring the function (which may or
may not get an inline definition sometime later). The successful
approach provides the function body:
inline int plusOne(int x) { return ++x; }

inline
keyword. However, this is not
necessary inside a class definition. Any function you define inside a
class definition is automatically an inline. For example:
//: C09:Inline.cpp
// Inlines inside classes
#include <iostream>
#include <string>
using namespace std;

class Point {
9: Inline Functions 401
int i, j, k;
public:
Point(): i(0), j(0), k(0) {}
Point(int ii, int jj, int kk)
: i(ii), j(jj), k(kk) {}
void print(const string& msg = "") const {
if(msg.size() != 0) cout << msg << endl;
cout << "i = " << i << ", "
<< "j = " << j << ", "
<< "k = " << k << endl;
}
};

int main() {
Point p, q(1,2,3);
p.print("value of p");
q.print("value of q");
} ///:~

// Inline access functions

class Access {
int i;
public:
int read() const { return i; }
void set(int ii) { i = ii; }
};

int main() {
Access A;
A.set(100);
int x = A.read();
} ///:~

Here, the class user never has direct contact with the state variables
inside the class, and they can be kept
private
, under the control of
the class designer. All the access to the
private
data members can
be controlled through the member function interface. In addition,
access is remarkably efficient. Consider the
read( )
, for example.
Without inlines, the code generated for the call to
read( )
would
typically include pushing

is part of the public interface, you can’t change it. Or
you may want to perform some additional calculation as part of
reading or setting
i
, which you can’t do if it’s
public
.

If, on the
other hand, you’ve always used member functions to read and
change the state information of an object, you can modify the
underlying representation of the object to your heart’s content.
In addition, the use of member functions to control data members
allows you to add code to the member function to detect when that
data is being changed, which can be very useful during debugging.
If a data member is
public
, anyone can change it anytime without
you knowing about it.
Accessors and mutators
Some people further divide the concept of access functions into
accessors
(to read state information from an object) and
mutators
(to
change the state of an object). In addition, function overloading
may be used to provide the same function name for both the
accessor and mutator; how you call the function determines
whether you’re reading or modifying state information. Thus,
//: C09:Rectangle.cpp

identifiers as data members, so you might be tempted to
distinguish the data members with a leading underscore. However,
identifiers with leading underscores are reserved so you should not
use them.
You may choose instead to use “get” and “set” to indicate accessors
and mutators:
//: C09:Rectangle2.cpp
// Accessors & mutators with "get" and "set"

class Rectangle {
int width, height;
public:
Rectangle(int w = 0, int h = 0)
: width(w), height(h) {}
int getWidth() const { return width; }
void setWidth(int w) { width = w; }
int getHeight() const { return height; }
void setHeight(int h) { height = h; }
};

int main() {
Rectangle r(19, 47);
// Change width & height:
r.setHeight(2 * r.getWidth());
r.setWidth(2 * r.getHeight());
} ///:~

Of course, accessors and mutators don’t have to be simple pipelines
to an internal variable. Sometimes they can perform more
sophisticated calculations. The following example uses the


Nhờ tải bản gốc

Tài liệu, ebook tham khảo khác

Music ♫

Copyright: Tài liệu đại học © DMCA.com Protection Status