Standard C++ With Object-Oriented Programming BOOK CH 3
Standard C++ With Object-Oriented Programming BOOK CH 3
3.2 Namespaces 84
3.4 Recursion 89
Exercises 120
CHAPTER THREE I
Key Constructs
in the function. When many functions share an external variable, this coding
can be tedious. It is easier simply to put the necessary extern declarations
outside the functions at the beginning of a file - once and for all. In OOP, use
of global variables is discouraged.
long Xi
float y;
int z;
Here we have a function nested in file scope and a local block nested within the
function fn. The global variable float Yi is hidden by the local double Yi in
the function fn. This local Yis in turn hidden by the local variable char Yi inside
the block. As control exits the block, the variable Yof type double resurfaces.
This further illustrates the scope rules. (More is said about variables and their
declarations in Section 3.3.)
Note how the file scope operator (: :) is used to refer to the global variable y
from within a local scope.
In C++, each class has its own scope. Enclosed in class scope are names for data,
functions, typedefs, and enumconstants. Even another class can be put inside
a class scope. With the exceptions stated earlier, identifiers declared within a
class are known from the point of declaration to the end of the class. Function
and static member definitions in an implementation file can be regarded as
being at the end of the class. A class scope identifier hides an identifier with
the same name in the enclosing scope.
Unless qualified, a class member name is generally not recognized outside
its class. A class scope operator (ClassName: :) in front of an identifier explicitly
specifies the class scope within which the identifier is interpreted.
Consider the Vector2D constructor:
Because the formal parameters hide the class scope data members X and y, the
Vector2D: : notation is required. The class scope operator is also used to access
static members (Section 5.10) in classes.
Another way to qualify a name and put it in a specific class scope is to use
the object member-of notation, as in the following examples:
Similarly, a class scope identifier hides a file scope identifier with the same
name. Suppose there is also a file scope function inner ().In this case, a member
function of Vector2D must use : : inner to access the file scope function.
namespace nSJ1ame
{
make each of the four identifiers usable directly as cout, cin, and so forth. If a
namespace name is very long, a namespace alias can help:
are not definitions because they do not allocate storage. The storage for x
should be provided by a unique definition somewhere else in the program. In
a C++ program, multiple declarations of the same quantity, usually in different
files, are allowed provided that all declarations are consistent.
int counter = 0;
extern int max = 0;
int a [] = {1,2,3,4,5} ;
char name[] = "Wang";
Internal and External Linkage The C++ compiler compiles each source
code file as a separate compilation unit and generates a corresponding .0 file.
When multiple .0 files are put together into an executable program, global
names of variables, objects, functions, and classes used across multiple files
must be linked together. A global identifier in a file to be linked with like
identifiers in other files has external linkage. Otherwise, the global identifier has
internal linkage and is not linked with identifiers in other files with the same
name. For example, a global variable int population shared by two source
code files has external linkage.
Let's examine how linkage is determined. First of all, a file scope identifier
automatically has external linkage, unless specifically declared otherwise. To
make external linkage explicit, you can add the extern specifier in front of any
global identifier declaration:
External linkage allows use of the same global variables across files but brings
with it the danger of global-variable-name conflicts between those files, espe-
cially if the files are written at different times or by different programmers.
A per-file global variable can be protected by putting it in an unnamed
namespace
namespace
{ canst int TABLE_SIZE 64i
int maXi
which implies that the variables are used only in their source code file. Names
in unnamed namespaces are not accessible from other files. Alternatively, you
may declare a file scope identifier static to limit it to a single file.
Knowing how to declare global variables does not mean you should use
them. Object orientation encourages encapsulation and discourages global
data sharing.
Later in this chapter, coverage of c++ declarations continues with type&
(reference), canst, and typedef.
While object orientation focuses on classes and objects, it is still important to
define functions and procedures for them. Often, the value of objects is directly
related to the efficiency or intricacy of the algorithms they encapsulate.
Many problems are solvable by a type of algorithm that reduces the orig-
inal problem into one or several smaller problems of exactly the same nature.
The solutions of the smaller problems then combine to form the solution of the
original problem. These subproblems can be further reduced by applying the
same algorithm recursively until they become simple enough to solve. A recur-
sive algorithm can be implemented most naturally by a recursive function.
Consider computing the greatest common divisor (gcd) of two integers. The gcd
of integers a and b is defined as the largest integer that evenly divides both
a and b. The gcd is not defined if both a and b are zero. A negative a or b
can be replaced by its absolute value without affecting the gcd. Hence, we
can assume that a and b are nonnegative and not both zero. The recursive
algorithm to compute gcd(a, b) can be described by the pseudocode:
1. If b is zero, the answer is a.
2. If b is not zero, the answer is gcd(b, a mod b).
It is interesting to note that the idea for this simple but effective integer gcd
algorithm is credited to Euclid, a Greek mathematician (ca. 300 B.C.).
The recursive function for Euclid's algorithm is straightforward:
int gcd(int a, int b)
if ( b == 0 )
return ai
else
return gcd(b, a % b) i
Note that the function gcd calls itself and that the value of the arguments
for each successive call to gcd gets smaller (see Table 3.1 for an example).
Eventually, the second argument becomes zero and the recursion unwinds:
The deepest recursive call returns, then the next level call returns, and so on
until the first call to gcd returns.
When a function is called recursively, each new invocation gets its own
set of formal parameters and automatic variables, independent of the previous
set. This is consistent with how automatic variables and formal parameters are
normally treated.
Table 3.1 RECURSION OF gcd12970, 1265) = 55
Call Level
2970 1265
1265 440
440 385
385 55
55 o
For many people, recursion is a new way of thinking that brings a powerful
tool for problem solving. Given a problem, two questions can be asked:
• Do I know a way to solve the problem if it is small?
• For a larger problem, can it be broken down into smaller problems of
the same nature whose solutions combine into the solution of the
original problem?
If you answered yes to both questions, then you already have a recursive
solution.
Recursive programs are concise and easy to write once you recognize
the overall structure of a recursive program. All recursive solutions use the
following sequence of steps:
1. Termination conditions: Always begin a recursive function with tests to
catch the simple or trivial cases at the end of the recursion. A terminal
case (e.g., remainder zero for gcd) is treated directly and the function
call returns.
2. Subproblems: Then, break the given problem into smaller problems of
the same kind. Each is solved by a recursive call to the function itself
passing arguments of reduced size or complexity.
3. Recombination of answers: Finally, take the answers from the
subproblems and combine them into the solution of the original bigger
problem. The task is finished and the function now returns. The
combination may involve adding, multiplying, or other operations on
the results from the recursive calls. For problems such as the gcd, no
recombination is necessary, and this step becomes a trivial return
statement.
Sorting means arranging data items into a specified order. Items are sorted to
make retrieval easier. Imagine trying to look up (retrieve) a phone number
from an unsorted phone book! Among many competing sorting algorithms,
the quicksort algorithm remains one of the fastest.
Let's consider arranging an array of integers in increasing order with
quicksort, which applies recursion:
The function quicksort is called with the lower index i and the higher
index j of the array. If j is greater than i, the function partition is called to
select a partition element and to split the array into two parts. The returned
value of partition is the index of the partition point. The smaller arrays to
either side of pe are then sorted by calling quicksort recursively.
The function parti tion is not recursive, and a simple implementation is
easy. Let's consider an efficient partition and see how it works.
The arguments to partition are the array a and the two indices low and
high. The range of the array from allow] to a[high] inclusive is to be par-
titioned. Basically, the middle element is chosen to be the pe. By searching
simultaneously from both ends of the range toward the middle, elements be-
longing to the other side are located. Out-of-place entries are interchanged in
pairs. Finally, the searches in opposite directions end when they meet some-
where in the range, pinpointing the location for the partition element.
The partition function begins by exchanging the rightmost element with
pe. Starting from both ends, the left-to-right search locates an element greater
than pe, and the right-to-Ieft search finds an element less than pe. The two
elements located are exchanged (with the inline function). Thereafter, the
searches in opposite directions continue. Eventually, no more exchanges are
needed, and the searches meet somewhere between low and high inclusive.
This is the partition spot that contains an element greater than or equal to pe.
The pe at the rightmost position is now interchanged with the element at the
partition position. Finally, the index of the partition element is returned:
class Fraction
public:
Fraction () { } II default constructor
Fraction(int n, int d); II constructor, d != 0
Fraction operator- ()i II unary negation
Fraction operator- (Fraction& Y)i II binary difference
void display () i
bool operator==(Fraction& y)
{ return ( num == y.num &&denom
}
booloperator> (Fraction& y)i
bool isZero() { return(denom == 1 &&num
bool isOne() {return(denom == 1 &&num
bool islnt() {return denom==li }
int floor () ;
int ceiling () i
There are quite a few members in the Fraction class: the private data
members numand denom,constructors, arithmetic and relational operators,
logical tests, and so on. Only a few typical members are shown here so that the
class definition remains uncluttered and thus easy to read. In practice, a full
complement of member functions is included to support the intended use of
the objects.
A class usually encapsulates a data structure with its manipulation proce-
dures. In designing a class, an important task is to decide on the internal data
representation, which is isolated from outside view. In this way, member func-
tions keep the data representation consistent in any way that is appropriate;
outside routines are not affected. Here are some internal representation items
to consider:
IIIIIII testFraction.C
#include "Fraction.h"
int main ( )
Fraction x(l,30), u(-l,60), v(-l,60);
Fraction y;
x.display(); std::cout « std: :endl;
y = x + u + V;
y.display(); std: :cout « std: :endl;
return 0;
The fraction subtraction function uses the pointer this (line 2), which deserves
careful explanation. Recall that an object is built using its class as a blueprint.
Hence, an object is an instance of its class and contains data and function
members specified by the class. Thus, members within individual objects are
known as instance members. The host object for an instance function is the object
containing that function. In C++, an instance function is called with an extra
pointer argument, this, supplied by the compiler, which is a pointer to the
host object and is known as the self-pointer or host pointer. This host pointer is
crucial to the operation of an instance function. For example, the code (line 1)
Thus, when referring directly to another member in the same host object, an
instance function really relies on the self-pointer (this) to do the job.
The self-pointer can also be used explicitly by instance functions when
there is such a need. In fraction subtraction, *this, the host object itself, is
the answer if zero is to be subtracted (line 2). The pointer this is not a fixed
quantity; it depends on the host object in question. For host object r, it points
to r; for host s, it points to s.
Fraction is another example of using class to build new data types from
existing ones. Fraction is now an abstract data type because it is characterized
only by its external behavior. Specific implementation details are hidden and
immaterial to users of fractions. By attaching all related routines to the data,
encapsulation is achieved. A fraction object is therefore self-sufficient, and it
even knows how to display itself. By operator overloading, Fraction objects
can be treated almost as built-in types (e.g., r - s).
A function usually takes a fixed number of arguments. But there are situations
when it is convenient or even necessary to relax this rule. In C++, it is possible
to define functions with optional arguments or even an arbitrary number of
arguments.
An argument of a function becomes optional (mayor may not be supplied in a
function call) if it has been given a default value. Optional arguments must be
grouped at the end of the formal parameter list. The = val ue syntax is used to
supply a default value. For example, the class
class Time
public:
Time() {}
Time(int hr, int min, int sec = 0, char ap = 'A'); II (1)
private:
int second, minute, hour;
char a_or-p; II 'A' or 'P'
anywhere (after line 1) makes the min argument also optional. This is possible
but not advisable. Rather, always supply all the default values in one proto-
type declaration at a place where any potential caller can see how to supply
arguments. The usual place for such a prototype is in a header file, where the
meaning of the arguments as well as other possible values for the optional
arguments are clearly documented with comments.
Tofurther ensure code clarity and consistency, it is good practice to use the
same header for a function in all its declarations and prototypes. The default
values can be commented out in all places but one. Thus, the implementation
of Time: :Timeshould look like this:
Time: :Time(int hr, int min, int see /*
{ hour = hr;
minute mln;
second see;
a_orJ! ap;
If all arguments are optional, the function can be called with no arguments.
When such a function is a constructor, it is not necessary to supply another
default constructor. In fact, it is an error to supply one because a call with no
arguments becomes ambiguous.
The initial value for an optional argument can be a constant expression
or any expression involving no local variables. Specifically, global variables,
static members (Section 5.10), and enumconstants (Section 1.11)can be used.
C++ also supports writing functions that take an indefinite number of
arguments. For example, the notation
Now the same power can compute powers of int and double-how conve-
nient! Furthermore, you can add the duty of computing powers of fractions:
Fraction power(Fraction a, int n)
{ Fraction ans(l, 1, 0); II ans is 1
for (int i = 0 ; i < n ; i++)
ans = ans * a; II * of Fraction
return ans;
Note that the preceding defines power, not Fraction: :power. Thus, you are
not dealing with a member function of the class Fraction. Had you used
Fraction: :power, you would be adding a function in the scope of Fraction
and not overloading the file scope function power.Hence, overloading occurs
only if additional meanings are assigned to a function name in the same scope.
When a function definition involves default arguments, it results essentially
in several versions of an overloaded function taking different numbers of
arguments.
There is no practical limit on how many different duties can be piled on
the same function name. The added definitions can also be in different places
or files in a program.
To overload a function, the new definition must carry a signature distinct from
all existing ones (in the same scope). For example, all of the following function
prototypes have distinct signatures:
int power(int a, int n); double power(Fraction a, float n);
int power(int a, short n); double power(float a, int n);
int power(int a, unsigned n); double power(int a, float n);
double power(double a, int n); Fraction power(Fraction a, int n);
double power(float a, float n); Fraction power(Fraction a, int* n);
Remember that the return value type is not part of the function signature.
The C++ compiler produces an error message and the compilation fails if the
overloading signature conflicts with an existing signature. For example, the
following signatures conflict with one another:
double power(double a, int n); I I mutually
double power(double a, const int n); I I conflicting
double power(double a, int& n); II signatures
For any type Tpthat is not a pointer or reference, the types TP, TP&, and cons t
Tp1 cannot be distinguished when function signatures are compared. However,
the types const TP& and TP& can be distinguished because read-only and read-
write reference parameters are very different. For similar reasons, const TP*
and TP* have different signatures. For example, the signatures
do not conflict.
Since arrays are always passed as pointers in function calls, the types TP*
and TP[] (with or without array dimension) are not distinguishable as far as
the signature is concerned.
Overloaded functions only give the appearance of having different func-
tions with the same name. But in fact, each overloaded version is internally
encoded by the compiler with a name that reflects the function's signature.
Thus, in a compiled program, there are no functions with the same name.
In C++, operators can also be given extra duties. Operator overloading is
similar to function overloading but becomes more involved for certain opera-
tors (Section 8.1).
The best is an exact match where the arguments match the parameter types
exactly or with only trivial differences that the two signatures would be in
conflict as overloaded functions. Next best are, in order, promotions, standard
conversions, and user-defined conversions. See Section 3.13for details on these
conversions.
Reference parameters (described in Section 2.2) are just one form of references
in c++. A variable declared type& is a reference of the given type and must be
initialized when declared. For example,
int aj
Account sally(55123, 450.0) i
int& ra = ai II ra is reference to a
Account& rsally = sallYi II rsally is reference to sally
The reference variables ra and rsally become aliases of the actual variables a
and sally. The initializer of a reference must be an Ivalue, an expression that
can be used on the left-hand side of an assignment. Common lvalues include
variables (x), array cells (a [i]), and dereferenced pointers (*ptr). Constants or
results of expressions such as (2 * a + 3) are not lvalues. Thus, for example,
the codes
are not possible. The general syntax for declaring a reference variable is
type& refname = lvaluei (declaring a reference)
where &signifies a reference declaration and is not to be confused with the
address-of operator. It cannot be because it is used after a type name. The
initializer must be of the same or a compatible type. Several reference variables
can be declared on one line, as in
int c = 9j
ra = Ci
a
int
When a function takes a reference parameter, the formal reference pa-
rameter is considered initialized by the argument being passed each time the
function is called. And such an argument must be an lvalue.
A function's return value can also be declared a reference - in which case,
an lvalue not local to the function must be returned. Consider the function
int a = 9, b = 9;
maxi (a, b) = 16; II assigns 16 to b
maxi (a, b) -= 10; II decreases b by 10
maxi(a,b)++; II increases a by 1
The type qualifier const expresses the read-only nature of variables and objects.
If a type name is preceded by const, it becomes a constant or read-only type. If
a variable or an array element is declared read-only, the value of the variable
stays constant and cannot be changed after initialization:
const float pi = 3.14159f;
const int lower_limit 32;
const char greeting[] = "Hello There"
Similarly, the pointer declaration
prevents any assignments through the pointer variable str. In Standard C++,
a string literal is strictly read-only and of type const char*. For instance,
* str = 'A' is illegal. However, the pointer variable str itself can still be set.
Thus, ++str is perfectly all right.
The old-style code
The compiler also disallows assignment of a pointer (reference) to read-only
data to a regular pointer (reference) to protect the read-only data. For example,
char *s = stri
const Account&ac = susani
Account& act aCi II errori act not const Account&
Account actl = aCi II OK; actl is a copy
The cons t qualifier is often used in function declarations. For example, the
function header
bool stringMatch(const char str[], const char liner])
means calling the function stringMatch does not result in any modifications
to the elements in str and line. The compiler checks for any illegal attempts
to modify read-only data.
Similarly,it is important to indicate the read-only nature of reference formal
parameters with const not only to prevent accidental modification but also to
assure any caller of a function that the reference argument will not be damaged.
For example, the member function operator- (Fraction&) can be improved
with the code
The added const modifier ensures that the reference being passed will
not be modified in any way. This code states that the right operand of
Fraction: :operator- () is read-only. But what about the left operand? It is
the host object itself and can also be designated read-only with the code
The const keyword at the end indicates to the compiler that the host object
will not be modified by the function. In writing member functions for a class,
be sure to declare the host const for any function that does not modify the host
object,2directly or indirectly. For example,
The typedef declaration is used to create new names for basic as well as
user-defined types, including classes. The new type name should be either
more descriptive or more compact to use. Once a new type name has been
established, it can be used just like any other type name. Type names should
also be distinct £rom other identifiers in the same scope:
typedef int Enrollmenti II Enrollment is int
typedef unsigned short Age; II Age is unsigned short
typedef char *String; II String is char *
typedef Fraction *Fracptr; II Fraction pointer
Note that capitalized identifiers are used for new type names. This con-
vention makes it simpler to distinguish typedef names from other names. With
these new type names, clearer codes, as in
Age Xi
String a, b = "hello there", argv[5];
can be used-in this case, to declare x unsigned short; a, b, and argv[O]
through argv [4] char *.
Having seen some examples, we are ready for the general syntax oj
typedef. To establish a new type name Abc, just declare Abc as though it were
a variable of the desired type and then precede the entire variable declaration
with the modifier typedef. Hence,
defines the type name StringArray and allows the main function header to be
written as
It must be pointed out that typedef does not actually create a new data type;
rather, it simply gives a new name to an existing type. The class declaration,
on the other hand, is used to define new types.
Besides aesthetics, the use of typedef simplifies complicated declarations
and provides readability and documentation for a program. Clearly, an Age
variable is more specific than an arbitrary int variable, and StringArray is
more to the point than what it replaces. Later, when we deal with complex
declarations or types defined in classes, typedef will come in handy. You'll
also see that typedefs defined inside a class can supply useful information to
clients of the class.
and newreturns a pointer to the newly allocated space appropriate to hold data
of the indicated type. The pointer returned is of type typeName*. For example,
This expression returns a type* pointer that points to the same address as ptr.
It is helpful in reusing a pool of space allocated and managed by a program
and in the explicit placement of data in memory. Be sure to include the header
<new>for this usage.
Dynamically allocated storage is freed with the operator delete when the
space is no longer needed. The delete operation must be explicitly invoked in
a program in the form
where ptr must be a pointer returned earlier by new.Freed space goes back
to the pool of available space for dynamic allocation. Be careful not to free
space that has not been dynamically allocated. Otherwise, data and even func-
tions in your program can be destroyed in unpredictable ways. However, C++
guarantees that deleting a pointer with the value zero is not a problem. This
means that a pointer to dynamic storage that is initialized to NULL can always
be deleted. After delete, the value of ptr is invalid (most likely NULL), and ptr
should not be dereferenced again.
Dynamically allocated arrays are freed using
Note that the private data member cb points to the first cell of a character
array dynamically allocated by the cirbuf constructor. A default buffer size of
16 is used.
The explici t keyword in front of Cirbuf (line A) needs some explain-
ing. Because any constructor taking exactly one argument also does double
duty as an implicit type conversion operation (Section 8.9),we add the keyword
explici t to remove the type conversion semantics so Cirbuf is only a construc-
tor and not an integer-to-Cirbuf conversion operation. Be sure to declare any
constructor taking one argument explicit if you don't want the associated
implicit conversion semantics.
As discussed earlier, the index head points to the first character to be
consumed in cb, and the index tail locates the slot for the next incoming
character. The number of characters remaining in the buffer is length. The
buffer is empty if length is zero. It is full if length becomes equal to size.
These details are important only to you, the designer of the Cirbuf class.
Any program that uses a circular buffer object is isolated from these details
and uses a Cirbuf object only through its public interface:
Note also that the isEmpty (isFull) test should be used before a consume
(produce) operation. These operations together with the constructor and de-
structor are implemented in the file Cirbuf .C:
IIIIII! Cirbuf.C
#include <iostream>
# include "Cirbuf.h"
using std::cout; using std: :cerr;
using std::endl;
Cirbuf: :-Cirbuf()
{ delete [) cb;
}
In the Cirbuf constructor, the operator new is used to dynamically allocate the
character buffer of the desired capacity. Thus, the code
Cirbuf a_buf(64);
Cirbuf *d_buf = new Cirbuf(128);
establishes a_buf and *d_buf as Cirbuf objects with the indicated capacities.
When a Cirbuf object is destroyed, the buffer space should be freed. This is
programmed into the destructor -Cirbuf (Section 5.7), which is automatically
invoked before a Cirbuf object is deallocated. During program execution, there
are two occasions when a variable is destroyed:
delete (d_buf) ;
c = cb [head] ;
length--;
incr (head) ; II increment with wraparound
return c; II return character
Let's put the circular buffer to use. Our example program counts the number
of words, separated by SPACE, TAB, and/ or NEWLINE characters, in the standard
input. The WordCountclass utilizes a Cirbuf object to store characters.
class WordCount
{ public:
WordCount()
{ buf = new Cirbuf(128);
wcnt = 0;
word = false;
}
-WordCount() { delete buf;
bool readin () ; II obtain input from cin
void count(); II count number of words
int getCount() { return wcnt; }
private:
int wcnt; II word count
bool word; II partial word indicator
Cirbuf* buf; II input buffer
The WordCount constructor dynamically allocates a Cirbuf of size 128, and the
destructor frees this space. A producer member function readin obtains input
characters and deposits them in the circular buffer until it is full. A consumer
function count then takes characters out of the buffer and counts the number
of words until the buffer is empty.
Note how the partial-word indicator word is used to count one whole word
across multiple invocations of count and to avoid counting words of length
zero. Also note that WordCountis our first example in which a class creates and
uses a member that is an instance of another class.
The main program testWordCount establishes a WordCountobject counter
and calls counter. readin () and counter. count () repeatedly until input is
exhausted. It then makes one final call to counter. count () before reporting
the final result:
IIIIII testWordCount.C IIIIII
#include <iostream>
#include "WordCount.h"
using std: :cout; using std::endl;
int main()
WordCountcounter;
for (i i)
if ( counter.readin()
else
counter.count(); break; }
cout « "total" « counter.getCount()
« "words"« endl;
Now the program is ready for some sample files. If your computer system has
an independent word-count program (e.g., the UNIX we command), it can be
used to verify the output of the c++ program.
c++ is a strongly typed language that requires all quantities be declared a type
before being used in a program. The compiler uses the type information to
check for possible argument-passing errors and to generate efficiently running
codes. Both primitive and user-defined types may need to be converted to a
related type before an operation can be performed. The conversion is some-
times done implicitly, or automatically. At other times, it is done explicitly, or
by program request.
Consider arithmetic operations. An arithmetic operator acting on operands
of the same type produces a result of the same type. But if the operands
are of different types, they must be converted to a common type before the
operation is performed. For example, an integer must be converted to floating-
point before an arithmetic operation with another floating-point number. Such
conversions are made automatically according to a set,of rules.
Since a char is just a small integer, characters can be used freely
in arithmetic expressions involving integers or other characters (as in
Cirbuf: :consume).If a char is converted to an int and then back to a char, no
information is lost.
In general, implicit conversions are performed for integral and floating-
point arithmetic operands, function arguments, function return values, and
initializers. For example, a function expecting an int argument can be passed
any arithmetic type that converts to an int, and the conversion is done by
the compiler automatically. Implicit type conversion also takes place when the
two sides of an assignment have different types; the right-hand-side value is
converted to the type of the left-hand side. For instance, if a floating-point
number (right-hand side) is assigned to an integer variable (left-hand side),
the fractional part is truncated. Therefore, the function
int round(float f)
int g = fi II truncate fractional part
float fracpart = f - gi
return ( (fracpart < 0.5) ? g : g+l )i
For binary arithmetic operations with operands of two different types, the
operand of a lower type will be automatically converted to the operand of
a higher type. (The precise rules can be found in Appendix H.) If there are
no unsigned operands, the rules given in Table 3.2, applied sequentially, will
suffice for most situations.
Note that when integral promotion is applied, a char, short, enumtype, or
an int bit field (Appendix D) is converted to an int if int can represent all
c++ uses the same semantics for function argument passing as variable initial-
ization; certain argument conversions are performed automatically including
standard conversions and user-defined conversions.
However, argument conversions take place only if the function call has been
matched with a function prototype supplying the necessary type information.
The situation becomes more complicated when the function is overloaded
(Section 3.7). It is best to avoid relying on implicit type conversion to pick a
version out of a set of overloaded functions and to use explicit type-casting for
an exact match (Section 3.7).
A programmer can also request type conversion explicitly to force data of one
type to become another. Standard c++ introduces the type-cast notation
to request that the expression expr be converted to the given type. There are
four type-cast operators for different kinds of conversions:
sta tic_cas t is used to make conversion between related types, between related
pointer types, and from a pointer type to void*. It is also used to make
compiler-supplied conversions explicit (to avoid warning messages). For
example,
double d = 3.1415j
int i = static_cast< int >(d)j
enum Days { MON=l,TUE, ... }i
Days w = static_cast< Days >(i) i II int to enum
float* ptr2 = static_cast< float* >(ptrl)i II double* to float*
Using static_cast, a short version of round can be written as:
int round(float f)
return static_cast< int >(f + 0.5);
1. Class member names have class scope and are generally not recognized outside
the class without qualification. Can you think of any class members that are
recognized without being qualified?
2. Write a gcd that is nonrecursive and that can take any int arguments, not just
nonnegative ones.
3. Write a function lcmthat takes two integer arguments and returns the least com-
mon multiple of all the arguments (e.g., lcm (-15 12) is 60). Modify operator-
f
8. Add a member to CirBuf so that the capacity of a circular buffer object can
be increased dynamically. The member prototype should be int grow(int n),
which causes the capacity to increase by n characters. Any unconsumed char-
acters in the buffer remain. A false is returned when grow() fails.
9. Examine closely the function parti tion used in our quicksort (Section 3.4).
Can you show that, after the whi1e loop, the element a [i] is not less than pe?
Is it an improvement to the parti tion code to modify the exchange call to
exchange (a, i++, j--)?
10. Given an array of distinct integers, write an efficient program to find the median,
the element of the array whose value is in the middle. Namely, roughly half of
the elements are over and half are under the median. (Hint: Modify parti tion.)
11. List the explicit conversion notations and explain their meaning using your
own words.
12. Write a function sumthat produces the total of an indefinite number of argu-
ments uniformly of type int, float, or double. Make sumalways return double
for now.
13. Consider the following code for the function swap.Is there any syntax problem
here? Does the function achieve its intended purpose? Why?
void swap(int& a, int& b)
{ int& tmp = b;
b = a;
14. In a function header, does it make sense to declare as const any formal param-
eter other than a pointer or a reference? Why? Is it possible to use the same
identifier both as a variable and as a function name? What about an enumtag?
A typedef name?
15. Write a program that creates an array frac_arr with new,initializes each ele-
ment frac3rr [i] with the Fraction object i~l' displays the array, and then
deletes it.
16. A stack is a first-in/last-out buffer. If the numbers 1.0, 2.0, 3.0 are entered
into the buffer in that order, then they are taken out of the stack in the sequence
3.0, 2.0, 1. O.The operation push enters an item on the top of a stack, and
the operation pop removes an item from the top of the stack. These are the
only two operations that are allowed to modify a stack. Following the circular
buffer example in Section 3.12, implement a class Stack for type double.
17. Exercise RP-l: Use the Stack object in Exercise 16 to implement a program rp
to evaluate a reverse Polish expression. For example,
rp 3 .1 -4.2 / 5.3 6.4 - 7.5 * +
displays the value of 3.1 / -4.2 + (5.3 - 6.4) * 7.5. (Hint: Use the library
function strtod.)
19. (a) If you have a function sumat file scope and a function sumin a class, is the
function sumconsidered to be overloaded? Why? (b) If the file scope sumhas
a different signature than the class scope sum,can you call the file scope sum
from a class member function without using the scope operator : :?
20. A function compareis a natural candidate for overloading. Write versions that
compare integers, floating-point numbers, strings, characters, and fractions.
21. In C++, the return type of a function is not considered part of the signature
of a function. Does this prevent you from defining functions that, in effect,
make the return value part of the signature? If not, explain the mechanism you
would use.