Book
Book
with C++
Saad Mneimneh
2
Contents
3 Functions 23
3.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
3.2 Function definition . . . . . . . . . . . . . . . . . . . . . . . . . . 23
3.3 Function evaluation using substitution . . . . . . . . . . . . . . . 25
3.4 Function as a black box . . . . . . . . . . . . . . . . . . . . . . . 26
3
6.5 Sorting elements . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
9 Operator overloading 77
9.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77
9.2 Operator overloading . . . . . . . . . . . . . . . . . . . . . . . . . 78
9.3 Rules for operator overloading . . . . . . . . . . . . . . . . . . . . 80
9.4 The increment/decrement operators and introduction to references 81
9.4.1 Why does C++ forbid pointers for operators? . . . . . . . 81
9.4.2 References . . . . . . . . . . . . . . . . . . . . . . . . . . . 82
9.4.3 Overloading operators ++ and −− (almost) correctly . . 85
9.4.4 Overloading operator << . . . . . . . . . . . . . . . . . . 86
9.5 Making friends and moving operators inside the class . . . . . . . 87
10 Constness 91
10.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91
10.2 Using const (again) . . . . . . . . . . . . . . . . . . . . . . . . . . 91
10.3 Constant member functions . . . . . . . . . . . . . . . . . . . . . 92
10.4 Moral of the story . . . . . . . . . . . . . . . . . . . . . . . . . . 95
10.5 Redoing operator overloading . . . . . . . . . . . . . . . . . . . . 95
10.6 Yet a better way . . . . . . . . . . . . . . . . . . . . . . . . . . . 96
4
13 Multidimensional arrays 121
13.1 A bit of a review . . . . . . . . . . . . . . . . . . . . . . . . . . . 121
13.2 Tic-Tac-Toe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 122
13.3 Multidimensional arrays as arguments . . . . . . . . . . . . . . . 124
13.4 Initializing multidimensional arrays . . . . . . . . . . . . . . . . . 126
13.5 Multidimensional arrays of unknown size . . . . . . . . . . . . . . 126
5
6
Chapter 1
A quick overview of
computing
1.1 Introduction
It has become clear to me over the years that, whenever students see the word
“computer”, they visualize this thing:
Well, more or less... Of course, the figure that will register in your mind
depends on the setting and on the current technology (which is very dynamic
by the way). For instance, I am sure that, to you, a computer would range from
being a laptop, to possibly an advanced PDA (Personal Digital Assistant) or a
smart phone (hence I will not ask you to turn off your cell phone in class).
Nevertheless, Figure 1 is definitely a standard interpretation of the word
computer. Next thing you know is that someone is sitting behind the keyboard,
looking very eagerly into the screen, and with the only moving parts of the
body being the eyes and fingers: That’s what a general audience would think
computer science is all about. This course will hopefully help us change that
image.
7
The concept that we should focus on is computation. Let’s start with a
(rather unclear) definition:
8
transcription translation
gene
protein
RNA
DNA
another protein
01001010100
retrieve code load in memory
00101010
compile 00001010101
1111
storage device text
e.g. Hard Disk e.g. C++ code program
another program
Biology Electronics
DNA (chemical) Hard disk (magnetic)
Transcription Data retrieval
RNA (chemical data) Code (textual data)
Translation Compilation
Protein Program
9
Therefore, one can claim that the biological process is actually a computa-
tion. Moreover, this makes our body a computer!
• The program is loaded into memory by the Operating System (OS), which
is another program already running
• The operating system instructs the CPU to start reading instructions at
the given memory location where the program is loaded
• The CPU start fetching instructions and data into special registers
• The registers act like the short term memory of the CPU, they hold data
needed to perform the most recently fetched instructions
• An Arithmetic and Logic Unit (ALU) inside the CPU performs the oper-
ations
01001010100
compiler OS
00101010
CPU
00001010101
1111
text
e.g. C++ code machine
language
memory
software hardware
10
as a relation between an input and an output. Therefore, an algorithm is a well
defined sequence of computational steps that transforms some input into the
desired output. Here are some examples:
• sorting numbers: the input is a set of numbers, the output is the same set
of numbers in sorted order (from smallest to largest). The algorithm in
this case is a sorting algorithm.
• cooking: the input is a set of ingredients, the output is the dish. The
algorithm in this case is a recipe.
• addition: the input is two numbers, the output is one number which is
their sum. The algorithm in this case is an adder.
Add 12 and 8:
12
+ 8
129
+ 17
If everything works as I expect, you will actually perform two different al-
gorithms, one for each addition. For the first addition, you are likely to write
down the digit 2 followed by the digit 0. The answer is 20. For this addition,
you perform a simple lookup. The answer to 12+8 is somehow stored perma-
nently in your brain. When you see the pattern 12+8, you access that area in
your brain and retrieve the answer. For the second addition, you are likely to
11
write down the digit 6 first, followed by the digit 4, followed by the digit 1. The
answer is 146. For this addition, you perform the standard addition procedure
that ripples a carry. Therefore, you add 9 and 7, which gives 16, you write down
the 6 and you carry 1, etc... You use your short term memory (or in this case
maybe the paper) to keep track of the carry. In either case, you are doing what
a computer would do.
You may be unaware of the representation of the addition algorithm in your
brain, but it is definitely encoded into your brain cells. Similarly, a computer
needs a way to represent algorithms in a form that is compatible with the
machine. A representation of an algorithm is often called a program. You may
have seen these programs, written in C++ or Java for instance, and printed on
a piece of paper, or displayed on the screen. The whole process of writing the
program and encoding it is called programming. Programs and the algorithms
they represent are called software. The machine itself is referred to as hardware.
Therefore, the software must be compatible with the hardware for it to work.
For instance, a PC and a MAC require different software.
#include <iostream>
using std::cout;
int main() {
cout<<’’Hello World\n’’;
}
12
Chapter 2
Basic elements of
programming
2.1 Introduction
We argued last time that a program is a representation of an algorithm. There-
fore, any powerful programming language must provide elements to represents
ideas and thoughts. These elements are:
• Expressions: represent simple entities, like integers, characters, strings,
etc...
• Combination: provides a way to build compound expressions from simpler
ones
• Abstraction: provides ways by which compound expressions can be named
For instance, the following program outputs the number 486.
#include <iostream>
using std::cout;
int main() {
cout<<486;
}
To output the character ’c’, we could have written the following instead:
cout<<’c’;
There are special characters that can be outputed using the escape character
backslash ’\’. Such special characters are called escape sequences:
13
To output the string “Hello World” followed by a newline, we could have
written the following statement:
cout<<’’Hello World\n’’;
Basic expressions can be combined together to form more complex expres-
sions. By now you might have noticed that every statement in C++ must end
with a semicolon. A statement is a form of combining expressions together. For
instance “cout<<486” consists of the output stream, which is a built in entity,
the << operator, and the number 486. The semicolon provides a way to sep-
arate statements. Moreover, a bunch of statements can be combined together
using the open { and close }. Here are some examples of combining expressions
in statements using the arithmetic operators and numbers.
#include <iostream>
using std::cout;
int main() {
cout<<137+349<<’’\n’’;
cout<<1000-334<<’’\n’’;
cout<<5*99<<’’\n’’;
cout<<10/5<<’’\n’’;
cout<<2.7+10<<’’\n’’;
cout<<7%2<<’’\n’’;
}
The above example will produce the following:
486
666
495
2
12.7
1
The following table describes the different operators used above:
operator description
+ addition
− subtraction
∗ multiplication
/ division
% modulus
14
cout<<3*2*4+3+5+10-7+6;
Let’s
P100 us write a program to output the sum of the integers from 1 to 100,
i.e. i=1 i.
#include <iostream>
using std::cout;
int main() {
cout<<1+2+3+4+5+6+7+8+9+10+
11+12+13+14+15+16+17+18+19+20+
21+22+23+24+25+26+27+28+29+30+
21+22+23+24+25+26+27+28+29+30+
31+32+33+34+35+36+37+38+39+40+
41+42+43+44+45+46+47+48+49+50+
51+52+53+54+55+56+57+58+59+60+
61+62+63+64+65+66+67+68+69+70+
71+72+73+74+75+76+77+78+79+80+
81+82+83+84+85+86+87+88+89+90+
91+92+93+94+95+96+97+98+99+100;
}
This program definitely works and outputs the correct answer. We can safely
claim
P100 that this program is a representation of a correct algorithm to compute
i=1 i. However, it suffers from two drawbacks:
Let us investigate how we would perform this particular task using our brain.
Without being smart about it like Gauss 1 , we keep a running sum and we
repeatedly add the next number to it. This means that we actually perform the
following operation (...+(((1+2)+3)+4)+...+100), which is identical to how the
above program evaluates the expression (left to right). The problem with the
above program, however, is the lack of abstraction. The process in our brain
uses two place holders x and y that evolve as follows:
1
Pn
Gauss discovered that i=1
i = n(n + 1)/2 at the age of 10.
15
x←0
y←1
repeat
x←x+y
y ←y+1
until y > 100
P100
Figure 2.1: Mental process for i=1 i
The name identifies a variable of the type type whose value is given by
expression.
16
int main() {
float pi=3.14159;
int radius=10;
float circumference=2*pi*radius;
cout<<circumference<<’’\n’’;
}
Puzzle: find out what the output of this program is and explain it to yourself:
#include <iostream>
using std::cout;
int main() {
float x=1/3;
cout<<x;
}
int main() {
int m;
cout<<m;
}
17
{
block
A name is not accessible outside its scope. This locality is very important
for abstraction as we will see later when we study functions. In the following
program, the second reference to y will cause an error.
int main() {
int x=2;
{
float y=1/3;
cout<<x<<’’\n’’;
cout<<y<<’’\n’’;
}
cout<<y<<’’\n’’; //not the scope of y
}
Note that two variables cannot be declared with the same name in the same
scope (why?).
2.3 Memory
When a variable is declared, a space is allocated in the computer’s main memory
for it. We say that the variable is created in memory. In fact, that’s what the
human mind does too (think about that mental process for computing 1 + 2 +
. . . + 100. This brings us to the following three questions:
• How much space does the variable occupy in memory (in bytes)?
• Where is it in memory?
• How long will it stay there?
The answer to the first question is that it depends on the type of the vari-
ables. Below is a list of most of the C++ fundamental types, grouped in two
categories: integral types, and floating types. Each unsigned type has the same
memory requirement as its corresponding type.
18
The size in bytes for each type may depend on the machine; however, they
are listed in a non-decreasing order. Usually, bool and char take 1 byte each. On
a 32 bit machine, an int takes 32 bits, i.e. 4 bytes. Therefore, we may represent
232 different values with an int. Half of the values represent positive integers
and half of the value represent negative integers. Therefore int ∈ [−231 , 231 − 1].
On the other hand, unsinged int represents only positive integers. Since both
have the same memory requirements, unsigned int ∈ [0, 232 − 1].
Consider the following program:
#include <iostream>
using std::cout;
int main() {
int n=1000;
cout<<n*n*n*n;
}
#include <iostream>
using std::cout;
int main() {
float x=100/3.0;
cout<<x*3-99;
}
To answer the second question, one could obtain the memory address of a
variable (the starting byte). Such address is also called a reference or a pointer
(because it points to where the variable resides in memory). A pointer is simply
an integer value. C++ provides a reference operator ’&’ for that purpose. For
example:
#include <iostream>
using std::cout;
int main() {
int x=2;
cout<<’’x is ‘‘<<x<<’’\n’’; //value of x
cout<<’’x is located at ‘‘<<&x<<’’\n’’; //memory address of x
}
19
int x=2;
&x 00000000
00000000
x
00000000
00000010
#include <iostream>
using std::cout;
int main() {
int x=2;
cout<<addr;
}
Moreover, a variable can be accessed through its pointer using the C++
dereferencing operator ’*’. For example,
#include <iostream>
using std::cout;
int main() {
int x=2;
int * addr=&x;
20
int x=2;
int * addr=&x;
addr 00000000
00000000
x ≡ *addr
00000000
00000010
&addr 10111111
11111111
addr ≡ &x
10010000
11010100
To answer the third question, a variable lives in memory until the end of
the block, i.e. it lives only in its scope. For now, this simple answer will be
sufficient. Later on, and through the use of pointers, we will see how a variable
can continue to live outside the scope in which it was declared (although we
may loose the ability to access it).
21
22
Chapter 3
Functions
3.1 Introduction
So far, we have seen elements that must appear in any powerful programming
language:
square(x) = x ∗ x
This declares a function with the name square. The function square repre-
sents the operation of multiplying something by itself. The thing to multiply is
given a local name x. The scope of x is the body of the function defined by the
block between { and }.
where
23
• type: represents the type of the result returned by the function, aka the
return type, i.e. the type of the expression that appears after the return
keyword.
• name: the name given to the function and by which it can be called.
• parameters: a list of names with their types used within the body of the
function to refer to the corresponding arguments of the function.
• body: statements/operations that will eventually yield the value of the
function when each parameter is replaced by it corresponding argument.
The value is returned using the return keyword.
#include <iostream>
using std::cout;
int square(int x) {
return x*x;
}
int main() {
cout<<square(21); //21 is the argument corresponding to parameter x
}
The above program will output 441, i.e. the square of 21. The function
square is called on its argument 21. This means that the parameter of the
function corresponding to this argument, i.e. x, is assigned the value 21 in the
body of the function (the scope of x). Since the function returns x*x, it returns
21*21 which is 441. There are two important things to note:
• the number of parameters and the number of arguments must be the same
(1 in this case)
• the type of the parameter and the type of the argument must match (int
in this case)
int main() {
cout<<square(2+5);
cout<<square(square(3));
}
Note that since the argument of the function square is the result of a another
call to square in the second statement, the return type of square and the type
of its parameter must be the same (int in this case).
Once a function is defined, it can be used as a building block to define other
functions. For example, let us define the function (C++ is case sensitive)
P ythagoras(x, y) = x2 + y 2
24
#include <iostream>
using std::cout;
int main() {
cout<<Pythagoras(3,4);
//there must be 2 arguments to replace the 2 parameters
}
repeat
evaluate the arguments
substitute parameters by values of arguments in body of function
until done
For this reason, C++ is said to use the call-by-value strategy, i.e. the ar-
gument is evaluated first, then the parameter is assigned that value. In other
words, the parameter and the argument are two different variables in memory
(even if they have the same name). They have different scopes. Consider the
following function:
int f(int a) {
return Pythagoras(a+1,a*2);
}
Pythagoras(5+1,5*2)
square(6)+square(10)
25
• Substitute: x is assigned the value 6 in its scope, and another x is assigned
the value 10 in its scope, the function evaluates to:
6*6+10*10
function
Therefore, the choice of names for the parameters should not matter. The
following function must remain the same regardless of which definition for square
is used.
Otherwise, the behavior of this function will be confusing. If the first defi-
nition for square is used, then what is being computed when square(y) is called
from within Pythagoras? It is x*x or y*y? What if the second definition is
used? To avoid such confusion, and to enforce the black box concept, names
of parameters are only local to the function, and they only refer to the names
used inside the body of the function.
26
Chapter 4
4.1 Introduction
The class of functions that we can define at this point is very limited because
we have no way to make tests and to perform different operations based on
the result of a test. Moreover, we did not cover how to define a process to be
repeated several times within a function (although this is possible using recursive
functions as we will see later). We cover these two issues here.
4.2 Testing
Without being able to perform a test, we cannot define a function to compute
the absolute value of a number x, denoted mathematically by |x|.
x x>0
|x| = 0 x=0
−x x < 0
C++ provides the if keyword for performing such case analysis. Generally,
one could write the following:
if (cond) {body}
where
Note that if the body contains a single statement, the open { and close }
can be omitted. The flow chart in Figure 1 describes the operation of an if
statement.
27
cond false
true
body
We can now define a function that takes a number and returns its absolute
value:
float abs(float x) {
if (x>0)
return x;
if (x<0)
return -x;
if (x==0)
return 0;
}
28
while (true) {
int status = GetRadarInfo();
if (status = 1)
LaunchMissiles();
}
The value of an assignment is the value of the assigned variable after per-
forming the assignment. Therefore, status=1 evaluates to 1, which is considered
true when interpreted as boolean (any integer different than 0 is true). What
happens if == is replaced by = in the abs function above?
The operands for logical operators must be of type bool. While both &&
and || are both binary operators (i.e. they require two operands each), ! is a
unary operator that reverses the boolean value of its operand. The following
tables summarize the operations of these three logical operators:
operand 1 operand 2 ||
false false false
false true true
true false true
true true true
operand !
false true
true false
29
The function returns a boolean value depending on whether x ∈ [a, b] or
not. It is worth mentioning here that the first compound condition cannot be
replaced by (a <= x <= b). While this is mathematically correct, it does not
evaluate as one might expect in C++. The expression a <= x <= b is evaluated
left to right. Therefore, a <= x is evaluated first, and depending on whether
it evaluates to false or true, a <= x is replaced by 0 or 1 respectively, yielding
either the condition 0 <= b or the condition 1 <= b (which is not the pro-
grammers intention). It is also worth mentioning that C++ uses short circuit
evaluation: if x >= a evaluates to false, C++ does not continue evaluating the
expression x >= a && <= b because the result is already determined to be
false (see the operation of logical AND). This also holds if the expression x < a
evaluates to true in the second compound condition.
Example 2: Here’s a function that tests if two numbers x and y are close
enough, i.e. if the absolute value of their difference is not bigger than a certain
threshold.
In most cases, the programmer can avoid using the NOT operator by ex-
pressing the condition with an appropriate relational operator. For instance,
the above expression can be replaced by (abs(x − y) < 0.0001). Theoreti-
cally speaking, ! can always be avoided because !expression is equivalent to
expression==false. However, the latter form is more error prone (e.g. acci-
dentally replacing == with =).
Note that the parentheses around the condition abs(x − y) >= 0.0001 are
needed because the logical operator ! has a higher precedence than the rela-
tional operator >=. In general, it is a good idea to have parentheses around
every condition to avoid errors due to precedence. The following tables updates
that of Chapter 2 and shows the operators in decreasing order of precedence.
operator evaluation
() left to right
! right to left !!a is !(!a)
∗ / % left to right
+ − left to right
< <= > >= left to right
== ! = left to right
&& left to right
|| left to right
= right to left a=b=c is a=(b=c)
30
4.2.3 Another form of if and the dangling else problem
C++ provides another form of if given by the following syntax:
true
body_1
float abs(float x) {
if (x>=0)
return x;
else
return -x;
}
The use of if-else may lead to what is referred to as the dangling else problem.
Consider the following code:
if (lives>0)
if (score>1000)
addBonusLives();
else
gameOver();
Obviously, for a human being, the intention of the programmer is the fol-
lowing: if the player has some lives left, then check the score, and if it is greater
than 1000, add some bonus lives. If the player has no more lives, the game is
over. However, the above code is indistinguishable from the code below for the
C++ compiler (which causes a game over if the score is less than 1000).
if (lives>0)
if (score>1000)
addBonusLives();
else
gameOver();
31
Which one is it? Here’s the rule: C++ matches an else with the closest
unmatched if. Therefore, the above code is a programmer’s mistake. To enforce
to correct logic, we can use open { and close } as follows:
if (lives>0) {
if (score>1000)
addBonusLives();
}
else
gameOver();
if (lives>0)
if (score>1000)
addBonusLives();
else ; //empty statement
else
gameOver();
4.3 Repetition
Let us revisit the process of adding numbers 1 through 100. We saw in Chapter
2 that this process can be described mentally as follows:
x←0
y←1
repeat
x←x+y
y ←y+1
until y > 100
P100
Figure 4.3: Mental process for i=1 i
This is similar to if, except that the body of the while loop is performed
repeatedly as long as cond evaluates to true (as opposed to just once in if).
Here’s the flow chart for a while loop.
32
cond false
true
body
#include <iosteam>
using std::cout;
int main() {
int x=0;
int y=1;
while (y<101) {
x=x+y;
y=y+1;
}
cout<<x;
}
where
• body: is the body of the for loop which is performed as long as cond
evaluates to true
• update: is an update statement that may change the value of cond and is
performed after every performance of body.
33
initialization
cond false
true
body
update
using std::cout;
int main() {
int x=0;
using std::cout;
int main() {
int x=0;
int y; //y is declared but not initialized (usually bad)
34
And yet, here’s another way:
#include <iostream>
using std::cout;
int main() {
int x;
int y;
for(x=0,y=1; y<101; x=x+y,y=y+1)
; //body of for loop is empty
cout<<x;
}
where
• body: is the body of the do while loop which is performed once first, and
then as long as cond evaluates to true
Therefore, the body of the do while loop is performed at least once (before
the condition is evaluated). The do while loop has the following flow chart:
body
35
If y is a guess for the square root of x, then
y + x/y
2
is a better guess.
1 + 2/1
= 1.5
2
1.5 + 2/1.5
= 1.4167
2
1.4167 + 2/1.4167
= 1.4142
2
...
Here’s a function called sqrt that returns an estimate of the square root of
x, given an initial guess:
There are some issues that need to be discussed here, namely the possibilities
that x < 0 or guess ≤ 0. If x < 0, then the square root of x does not exist and
the guess will never converge. Similarly, if we start with guess = 0, then the
next guess will be infinity and will never converge. If we start with a negative
guess, then we converge to a negative root (not the definition of square root).
All these issues can be solved by adding the following statements before the
while loop:
36
Chapter 5
5.1 Introduction
Consider the problem of working with rational numbers of the form a/b where
both a and b are integers. For instance, we would like to add two rational
numbers and obtain the result in fractional form. If we want to define a function
for performing this addition, then the function must return two integers: the
numerator and the denominator of the result. Adding a/b and c/d requires
passing four integers as parameters to the function.; however, each function can
specify only one type for its return.
. . .
return ___?___;
}
How can we make such function return two integers? In general, how can we
define a function that returns multiple values? The immediate answer is that we
can’t! Instead, we make the function store the numerator and the denominator
of the result in two variables that are specified in advance. Let’s call these two
variables numer and denom. For this approach to work, two issues must be
considered:
• Both numer and denom must be declared in a scope that contains the
function definition; otherwise, numer and denom cannot be accessed from
within the function.
We can declare numer and denom in the global scope, i.e. not inside any
block. Such variables are called global variables. It is generally not a good idea
to have global variables because they can be accessed from anywhere in the
program and, therefore, it is hard to keep track of which parts of the program
are affecting their values. To specify that a function does not return any value,
we make it return the special type void 1 .
1 We could still make the function return something that we will ignore.
37
Here’s an example:
#include <iostream>
using std::cout;
int numer;
int denom;
int main() {
int a=2;
int b=3;
int c=4;
int d=5;
addRat(a,b,c,d);
cout<<numer<<’’/’’<<denom<<’’\n’’;
}
Besides the fact that global variables are not desired, there is a potential
problem with the above approach. Since the result is always stored in the
global variables numer and denom, we can only compute one number at a time.
Every call to the function addRat will change the values of numer and denom.
Therefore, one has to save those values in other variables before issuing another
call to the function addRat; otherwise, the last result will be overridden. A
better approach is described below.
}
Luckily, we should be able to deal with the above issues by reviewing material
from Chapter 2.
38
Since the numerator and the denominator of the result are both integers,
their memory addresses will have the type int *, i.e. a pointer to an int.
Moreover, given a pointer, we can access its content using the dereferencing
operator *. Therefore, our addRat function will be the following:
int main() {
int a=2;
int b=3;
int c=4;
int d=5;
cout<<x<<’’/’’<<y<<’’\n’’;
}
#include <iostream>
using std::cout;
39
int main() {
int a=2;
int b=3;
int c=4;
int d=5;
addRat(a,b,c,d,x,y);
cout<<x<<’’/’’<<y<<’’\n’’;
}
The variables x and y will not change after calling the function addRat. This
is because the values of x and y are simply stored into the variables numer and
denom which are local to the function. The scope of numer and denom is the
body of the function and, therefore, any change to these variables is only visible
within the body of the function. This holds even if x and y are renamed numer
and denom respectively.
Note that passing a variable by reference using a pointer is the same as
passing its address by value. For instance, the parameters numer and denom
of type int * are also local to the function, except that they are pointers.
Therefore, chaning numer or denom inside addRat does not have any effect
outside the function. Here’s another example:
int main() {
int x=1;
int * p=&x; //p points to x
f(p);
cout<<x<<’’\n’’; //x changed to 2
cout<<p<<’’\n’’; //p unchanged
}
When function f is called, ptr takes the value of p (by substitution). There-
fore, ptr local to f) and p are now two different pointers with the same value.
Modifying *ptr or *p will have the same effect on x (both point to the same
memory address). However, modifying ptr will only modify the value of ptr
itself, i.e. ptr will simply point to another address.
5.3 Arrays
Consider the following simple task: define a function that computes the average
of two integers. Of course, this should be trivial by now:
40
float average(int a, int b) {
return (a+b)/2;
}
While this is not the point of the exercise, the above function contains an
error. Since a, b, and 2 are all integers, (a+b)/2 is interpreted as integer division
and, therefore, will produce an integer, i.e. truncated result (the integer is then
converted to the return type float). To solve this problem, we may re-write the
function as follows:
Now let us define a function that computes the average of three integers:
While both functions have the same name, they can coexist in the same
scope. The compiler can distinguish between them because they have different
parameter lists. A call to average can be resolved without ambiguity by simply
counting the number of arguments supplied to the function. This is called
function overloading. In general, two functions in the same scope can have the
same name if they differ in their parameter lists:
Note that the return type is not considered for function overloading.
Now let us define a function that computes the average of four integers:
By now, it should be clear what we are getting into. Although the concept
of average is unique, it seems that we have to define a separate function for
every possible number of integers. That would be really dramatic!
We need a way to represent a collection of integers as one entity, and pass it
to the function. C++ provides a way to create such entities using arrays. An
array is declared as follows:
type name[size];
where
• type: is the type of each element in the array (arrays are homogeneous)
• name: is the name of the array
• size: is the size of the array
41
For instance, one could declare an array of eight integers by writing:
int a[8];
a[0]
a[1]
a[7]
Since the array a must be passed as one parameter to the average function,
we must ask ourselves the following question: what is the type of a? We know
that a[i] has type int for i = 0 . . . 7. But what is the type of a itself? The
answer is: int *. This means that a is a pointer to int. So what is a really?
T a[size];
How come (a+i) points to the ith element of the array if each element occu-
pies a number of bytes? The reason for this is that C++ takes care of this kind
of pointer arithmetics. Since the type of the elements is known, incrementing
the pointer by 1 increments it by the appropriate number of bytes. However,
if we force C++ to interpret the pointer as an integer using casting, we will
observe a different behavior.
int main() {
int * a;
int * b=a+1;
cout<<b-a; //1
cout<<(int)b-(int)a; //4, int takes 4 bytes
}
42
a
a[0]
a+1
a[1]
a+7
a[7]
43
Here’s a variation using a for loop:
Note the use of casting in the return statement to avoid the integer division.
Alternatively, sum could have been declared as float.
Another way of specifying an array as a parameter to the function is the
following form (which will be useful later when we consider multi-dimensional
arrays):
In any case, here’s how the average function can now be used:
int main() {
int a[8];
a[0]=2;
a[1]=8;
a[2]=7;
a[3]=1;
a[4]=3;
a[5]=5;
a[6]=6;
a[7]=4;
cout<<average(a,8);
}
int main() {
int a[8]={2,8,7,1,3,5,6,4};
cout<<average(a,8);
}
44
#include <iostream>
using std::cin;
int main() {
int n;
cin>>n;
int a[n];
. . .
}
Like cout which is the standard output stream (outputs to the screen), cin
is the standard input stream which reads information from the keyboard. The
operator >> is the one used in conjunction with input streams and the state-
ment cin>>n extract information from the stream (in this case the keyboard)
and stores it in variable n. Obviously, the intention of the programmer was to:
However, since the value of n is not known at compile time (it depends on
what the user will input), the compiler will complain. This is the case even with
the following program:
int main() {
int n=8;
int a[n];
. . .
}
The compiler has no way of telling whether n will have the value 8 at the
point of declaring the array. Obviously it will, but the compiler has no such
intelligence. For instance, consider the following wicked scenario:
void f(int * x) {
*x=*x*2;
}
int main() {
int n=8;
f(&n);
int a[n];
. . .
}
Obviously the compiler has to develop some intelligence to figure out that
the value of variable n is doubling. To avoid this necessity, the compiler simply
complains. To make the compiler happy, we need to give it some assurance.
45
int main() {
const int n=8;
int a[n];
. . .
}
int main() {
const int n; //error
. . .
}
The use of const is very tricky and elaborate in C++, but for now the above
will be enough for our purpose. Coming back to our problem, it seems that the
only way to declare our array appropriately is the following:
#include <iostream>
using std::cin;
int main() {
const int max_size=100;
int a[max_size];
int n;
cin>>n;
if (n>max_size) {
//do the hard work
}
. . .
}
A better solution is for the programmer to allocate enough memory for the
array himself. This means that the compiler will not worry about the size of
the array at compile time, but the array will be created at run time in a special
part of the memory, called the free store. The downside is that in doing so, the
46
compiler will not be able to free the allocated memory when the array goes out
of scope. It becomes the responsibility of the programmer to do so. Memory
allocated on the free store persists until it is explicitly freed. Failing to do so
will result in what is known as memory leak. While this may not be a severe
problem because memory is released when the program ends, a continuously
running program that repeatedly allocates but fails to free memory may cause
the system performance to deteriorate. C++ provides the two operators new
to allocate memory and delete to free memory.
#include <iostream>
using std::cin;
int main() {
int n;
cin>>n;
. . .
int main() {
. . .
. . .
delete a;
}
We will see why this is useful later in the course when we talk about classes
and inheritance. In the meantime, remember to always delete[ ] what you new[ ]
and delete what you new, but never mix them.
One last word about dynamic memory allocation. It might seem that using
a compiler like g++, that does not require the size of the array to be known
at compile time, can avoid the use of dynamic memory allocation. However,
we will see later that sometimes the size of the array is not known (not even
through a variable name) to the programmer at the point of declaration.
47
48
Chapter 6
6.1 Introduction
In this chapter we will try to put everything we have learned so far in a useful
application: Sorting numbers. This application illustrates the concepts of ar-
rays, dynamic memory allocation, random number generator, loops, passing by
value, and passing by reference.
using std::cout;
using std::cin;
int main() {
cout<<’’enter the size of the array:’’;
int n;
cin>>n;
int * list=new int[n];
for (int i=0; i<n; i=i+1)
list[i]=rand()%100;
. . .
delete[] list;
}
49
void init(int * a, int n) {
for (int i=0; i<n; i=i+1)
a[i]=rand()%100;
}
using std::cout;
using std::cin;
int main() {
cout<<’’enter the size of the array:’’;
int n;
cin>>n;
int * list=new int[n];
init(list,n);
. . .
delete[] list;
}
One subtle point here is whether this change in the array will be visible
outside the function. We know that a is a local name for function init and,
therefore, must have the body of the function as its scope. Does a change
in a[i] affect list[i]? The answer is yes, because list is passed by reference
using a pointer (recall that an array is simply a pointer to its first element).
To further emphasize this, we restate it in bold: A C++ array is always
passed by reference. As we discussed before, a[i] is nothing but a syntactic
sugar for *(a+i), i.e. increment the pointer by i locations in memory and then
dereference it. Therefore, a[i]= . . . ; is equivalent to *(a+i)= . . . ;. As a
result, the content of the memory is altered inside the function and the effect is
visible to the outside. On the other hand, n is passed by value, so any attempt
to change n inside function init will not be visible to the outside.
The index of the smallest element is set to zero initially. A loop is used to
check every element of the array. Every time a smaller element is discovered, the
index is updated. Finally, the index is returned. An alternative way of defining
50
the function minimum is to make it return the index of the smallest element of
a sub-array, given by its starting and ending positions.
This version of the function may seem more general. Well, conceptually it is;
for instance, minimum(a, 0, n-1) emulates minimum(a, n). However, mini-
mum(a, i, j) can be achieved using minimum(a+i, j-i+1) (why?). Therefore,
both have the same power.
The function first saves the value of a[i] in a temporary local variable called
temp. Then assigns a[i] the value stored in a[j]. Finally, assigns a[j] the value
stored in temp (the original value in a[i]).
51
Here’s an example:
2 8 7 1 3 5 6 4
1 8 7 2 3 5 6 4
1 2 7 8 3 5 6 4
1 2 3 8 7 5 6 4
1 2 3 4 7 5 6 8
What may be surprising is that our function to sort an array turned out to
be really small. This is because we relied on building blocks that we developed
earlier. In general, it is always a good idea to break the problem into smaller
ones and identify abstract concepts that can be tackled separately. For instance,
sorting definitely requires some kind of swapping because the elements in the
array must change places. Moreover, finding the smallest elements is a useful
concept on its own. Sorting can be implemented in terms of these two concepts.
52
Chapter 7
7.1 Introduction
Let us revisit the problem of working with rational numbers of the form a/b
where both a and b are integers. Previously, we handled this problem by explic-
itly keeping track of two integers for each rational number. Moreover, we had
to use passing by reference to compensate for the fact that a function cannot
return two things. For instance, a function to add two rational numbers would
have to be defined as follows:
void addRat(int a, int b, int c, int d,
int * numer_addr, int * denom_addr) {
*numer_addr=a*d+b*c;
*denom_addr=b*d;
}
Let us now imagine a better way. It would be very nice if we could handle
rational numbers more conveniently as follows:
int main() {
Rat x=Rat(2,3);
cout<<’’x is:’’<<numer(x)/denom(x);
}
In the above imaginary piece of code, we assumed the following:
• Rat is a type (stands for rational number)
• Rat(n,d) magically constructs a rational number whose numerator is the
integer n and whose denominator is the integer d
• numer(x) returns the numerator of a rational number x
• denom(x) returns the denominator of a rational number x
53
In our imaginary code, we declare a variable of type Rat whose name is
x, and initialize it with a value given by the result of Rat(2,3), which magi-
cally constructs the rational number 2/3. Then we output the value of x using
numer(x) and denom(x) which return the numerator and denominator of the
rational number x respectively.
What we have done so far is some wishful thinking. But if we assume that
this magic is given to us, then we can define the addRat function in a much
convenient way:
Or simply:
This is a function that takes two parameters of type Rat and returns a Rat.
The function uses Rat(n,d) to construct a rational number whose numerator
and denominator are given by the expressions
numer(x)*denom(y)+numer(y)*denom(x)
and
denom(x)*denom(y)
int main() {
Rat x=Rat(2,3);
Rat y=Rat(4,5);
Rat z=addRat(x,y);
cout<<’’z is:’’<<numer(z)<<’’/’’<<denom(z);
}
54
7.2 The class
The following represents the Rat class:
class Rat {
public:
int numer;
int denom;
Rat(int n, int d) { . . . }
};
In general
creates a new type with the name Name. That’s why we often refer to a
C++ class rather than a C++ type, but we can use both terms interchangeably.
Let’s dissect the above code one line at a time:
• class is a keyword in C++ and is used to name a new type, e.g. Rat in
this case. This is followed by { to start the class definition
• public is also a keyword in C++ and signifies that what follows is public
and, therefore, accessible outside the class. We will see the importance of
this later on.
• int numer; and int denom; declare two variables of type int that will
hold the values for the numerator and denominator respectively. Both
numer and denom are called member data of the class. Note that numer
and denom are declared but not initialized (initializing them would give
an error, see below).
• }; ends the class definition. Note that ’;’ at the end is required.
Rat(int n, int d) {
numer=n;
denom=d;
}
55
class Rat {
public:
int numer;
int denom;
Rat(int n, int d) {
numer=n;
denom=d;
}
};
&x
numer = 2
x
class Rat {
denom = 3
int main() { public:
Rat x=Rat(2,3); int numer;
} int denom;
Rat(int n, int d) {
numer=n;
denon=d;
}
};
The class provides a recipe for rational numbers. Every rational number
must contain two integers (called numer and denom), and that’s how it is rep-
resented in memory. Both numer and denom are part of the representation.
1 We also say that x is an object of class Rat or an instance of class Rat.
56
The constructor (called upon initialization of the object) performs the required
initialization for each member datum (numer and denom in this case).
In general, every member datum is part of the representation of the object.
Therefore, as long as the object is accessible, all member data are theoretically
accessible (they are in memory). Back to our example, both numer and denom
are accessible from within the scope of x, but only through x. In deed C++
provides a way to access the member through the object using the dot
notation. The following syntax
object.member
provides access to the member through the object. Therefore, x.numer refers to
the numerator of x, and x.denom refers to the denominator of x. C++ permits
this kind of access for a member datum only when the member datum is declared
public. Here’s an example:
int main() {
Rat x=Rat(2,3);
cout<<’’x is ‘‘<<x.numer<<’’/’’<<x.denom;
}
We are now very close to finishing our magic. We still have to implement
the functions numer(x) and denom(x). Now that we know how to access the
public member data, it is easy to define these two functions.
int numer(Rat x) {
return x.numer;
}
int denom(Rat x) {
return x.denom;
}
Here’s the complete work:
class Rat {
public:
int numer;
int denom;
Rat(int n, int d) {
numer=n;
denom=d;
}
};
int numer(Rat x) {
return x.numer;
}
int denom(Rat x) {
return x.denom;
}
57
7.4 Restricting access
Assume that we would like (for one reason or another) our rational numbers
to be reduced to their lowest terms. For instance, 6/9 can be reduced to 2/3.
Obviously, we cannot rely on the user to maintain this property. Consider the
following code:
int main() {
Rat x=Rat(1,3); //x is 1/3
Rat y=addRat(x,x); //y must be 2/3
cout<<’’y is:’’<<numer(y)<<’’/’’<<denom(y);
}
The above program will output 6/9 (why?). One possible way to solve this
problem is by changing the constructor to always initialize the numerator and
denominator to their reduced forms. To do that, it is enough to divide both
numer and denom by their greatest common divisor. The greatest common
divisor can be obtained using Euclid’s algorithm (you can search for this online
if you have not heard of it).
Rat(int n, int d) {
int z=gcd(n,d); //assuming the gcd function exists
numer=n/z;
denom=d/z;
}
While this ensures that every rational number is in its reduced form upon
construction, it does not protect against direct access to the member data.
Consider the following code:
int main() {
Rat x=Rat(1,3);
x.numer=2; //now x is 2/3
x.denom=4; //now x is 2/4 (not reduced)
}
Note that the above is possible because numer and denom are declared public
in class Rat (i.e. each is a member datum that is accessible through the object
using the dot notation). To remedy this problem, one must prevent this type of
access by moving numer and denom from the public section:
class Rat {
int numer;
int denom;
public:
Rat(int n, int d) {
numer=n;
denom=d;
}
};
58
Both numer and denom are now called private member data. As a result,
our numer(x) and denom(x) functions will now fail:
int numer(Rat x) {
return x.numer; //error, numer is not public in Rat
}
int denom(Rat x) {
return x.denom; //error, denom is not public in Rat
}
Since access has been restricted by making numer and denom private in class
Rat, the only way to access numer and denom is now from within the class. This
means our functions numer(x) and denom(x) must be moved inside the class.
They will become member functions. Before we actually perform this move,
we stop and ask ourselves the following question: what is really meant by data?
Is rational number a data? It seems that we are eventually reaching a point
where a rational number is not only composed of two integers, but also consists
of a bunch of functions!
We may think of data as defined by some collection of functions (including
constructors), together with specified conditions that these functions must fulfill
in order to be a valid representation. For instance, our rational numbers satisfy
the following condition: if x is constructed using Rat(n,d) then the result of the
division numer(x)/denom(x) is equal to n/d (note that this is true regardless
of which implementation of the constructor we use: reduced vs. non-reduced).
Therefore, implementation details are not important. What is important is the
interface with the outside world.
59
60
Chapter 8
8.1 Introduction
In the previous chapter, we learned how to restrict access to data members
by making them private. If not declared under the keyword public, member
data (and functions) in a class are private by default, but we may also use the
keyword private as a reminder.
class Rat {
private:
int numer;
int denom;
public:
Rat(int n, int d) {
numer=n/gcd(n,d);
denom=d/gcd(n,d);
}
};
This approach ensures that the internal representation of a rational number
is protected. The only way of constructing a rational number is through the
constructor Rat(n,d), which guarantees that numer and denom are reduced
to their lowest terms. After that point, the programmer has no direct access to
numer and denom using the dot notation (because they are declared private
in the class). Unfortunately, this also means that the following two functions
will fail:
int number(Rat x) {
return x.numer; //error, numer is declared private
}
int denom(Rat x) {
return x.denom; //error, denom is declared private
}
The only way to access numer and denom is now from within the class.
This means the above two functions numer(x) and denom(x) must be moved
inside the class. They become member functions.
61
8.2 Member functions
A class can contain functions other than a constructor. Unlike a constructor,
these functions may have a return type. The scope of a member function is the
entire class; therefore, a member function can be called from within another.
Moreover, if these functions are declared public, they may be accessed outside
the class but only through the object, using the dot notation (like member
data) 1 . As we would expect, a member function has full access to all member
data. Here’s our first attempt to make numer(x) and denom(x) two member
functions:
class Rat {
private:
int numer;
int denom;
public:
Rat(int n, int d) {
numer=n/gcd(n,d);
denom=d/gcd(n,d);
}
int numer(Rat x) {
return x.numer;
}
int denom(Rat x) {
return x.denom;
}
};
class Rat {
private:
int numer;
int denom;
1 This is not true for a constructor. Once the object is constructed, we cannot explicitly
62
public:
Rat(int n, int d) {
numer=n/gcd(n,d);
denom=d/gcd(n,d);
}
int getNumer(Rat x) {
return x.numer;
}
int getDenom(Rat x) {
return x.denom;
}
};
8.2.2 Shadowing
In the event that we like to keep the original names of our functions (and this is
justifiable because they are public while the member data are private), then we
would rename the member data instead. For instance, we can replace numer
and denom with n and d respectively for every occurrence of these names.
class Rat {
private:
int n;
int d;
public:
Rat(int n, int d) {
n=n/gcd(n,d);
d=d/gcd(n,d);
}
int numer(Rat x) {
return x.n;
}
int denom(Rat x) {
return x.d;
}
};
63
a variable in an outer scope. Therefore, a name refers to the declaration found
in the inner most scope that contains such declaration.
T1 x;
{
T2 x;
Figure 8.1: Shadowing: in the inner scope, x refers to the second declaration
To solve the shadowing problem, we can simply rename the parameters (we
will see another way later).
class Rat {
private:
int n;
int d;
public:
Rat(int a, int b) {
n=a/gcd(a,b);
d=b/gcd(a,b);
}
int numer(Rat x) {
return x.n;
}
int denom(Rat x) {
return x.d;
}
};
Our class is now almost ready. However, since public member functions must
be accessed through the object using the dot notation, the syntax for using our
two newly added functions will look a bit weird. Here’s an example:
int main() {
Rat x=Rat(2,3);
cout<<x.numer(x)<<’’/’’<<x.denom(x)<<’’\n’’;
}
The variable x appears twice with each function call: once to access the
function itself using the dot notation, and once as an argument to the function.
Even more weird is the semantics of using these functions.
64
int main() {
Rat x=Rat(2,3);
Rat y=Rat(4,5);
cout<<x.numer(y)<<’’/’’<<x.denom(y)<<’’\n’’;
}
In the example above, we access the function through object x using the dot
notation, but we use object y as an argument. Therefore, we will output the
numerator and denominator of y, although we are accessing the function through
x. To avoid such wired syntax and semantics, the argument to the function
should be implicit. In other words, the functions numer() and denom() should
not have any parameters. The parameter should be understood from the object
used in the dot notation.
class Rat {
private:
int n;
int d;
public:
Rat(int a, int b) {
n=a/gcd(a,b);
d=b/gcd(a,b);
}
int main() {
Rat x=Rat(2,3);
Rat y=Rat(4,5);
cout<<x.numer()<<’’/’’<<x.denom()<<’’\n’’;
cout<<y.numer()<<’’/’’<<y.denom()<<’’\n’’;
}
65
8.3 Exposing the class, self reference
Every member function (including constructors) in a class has a hidden or im-
plicit parameter added by C++ at the end of the list of parameters explicitly
defined by the programmer. That parameter is known as the self reference pa-
rameter and it has the name this. Since this is a parameter, we must ask the
what type does this have? The answer is that this is a constant pointer of
the type defined by the class. Therefore, in our example, this is declared at the
end of the parameter list as follows:
A small section on the basic use of const is included at the end of this
chapter. For now, a constant pointer means that the pointer cannot be changed,
i.e. cannot be assigned another address.
C++ uses the self reference parameter this to identify the object on which
the dot notation was used to invoke the function (or the objet being constructed
in case of a constructor). Therefore, another way of visualizing the class is as
follows (what is really happening):
//the nice look //what is really happening
class Rat {
private:
int n;
int d;
public:
Rat(int a, int b) { Rat::Rat(int a, int b, Rat * const this) {
n=a/gcd(a,b); (*this).n=a/gcd(a,b);
d=b/gcd(a,b); (*this).d=a/gcd(a,b);
} }
int main() {
Rat x=Rat(2,3);
Rat y=Rat(4,5);
cout<<x.numer()<<’’/’’<<x.denom()<<’’\n’’; cout<<Rat::numer(&x)<<’’/’’<<Rat::denom(&x)<<’’\n’’;
cout<<y.numer()<<’’/’’<<y.denom()<<’’\n’’; cout<<Rat::numer(&y)<<’’/’’<<Rat::denom(&y)<<’’\n’’;
}
Although the this pointer is hidden, we can actually use it in the body of
the function as the rest of the parameters. For instance, this provides a way
to overcome the shadowing problem without having to rename parameters (or
member data):
class Rat {
private:
int n;
int d;
66
public:
Rat(int n, int d) {
(*this).n=n/gcd(n,d);
(*this).d=d/gcd(n,d);
}
int numer() {
return n;
}
int denom() {
return d;
}
};
In this new version of the constructor, first the object is obtained by deref-
erencing the this pointer. Then the member data is accessed using the dot
notation. Therefore, while n in the body of the constructor refers to the pa-
rameter, (*this).n refers to the member datum n declared in the class.
C++ provides the operator − > to simplify the combined use of dereferenc-
ing and the dot notation.
A− >B ≡ (*A).B
class Rat {
private:
int n;
int d;
public:
Rat(int n, int d) {
this->n=n/gcd(n,d);
this->d=d/gcd(n,d);
}
int numer() {
return n;
}
int denom() {
return d;
}
};
67
int main() {
Rat x=Rat(2,1); //construct the rational number 2
}
int main() {
Rat x=2; //WOW!
}
int main() {
Rat x=Rat(2);
}
Therefore, C++ assumes the existence of a constructor that takes only one
parameter (an integer). Of course we don’t have one for our Rat class. There-
fore, we face a compile time error. One way to make the compiler happy is
by giving a default value for one of the parameters. In this case, when only
one argument is passed, the default value is assumed for the second one (this
technique applies in general to any function and not only in the context of a
class).
class Rat {
. . .
public:
Rat(int n, int d=1) {
this->n=n/gcd(n,d);
this->d=n/gcd(n,d);
}
. . .
};
int main() {
Rat x=Rat(2,1);
Another way is to actually provide another constructor for the class that
takes only one integer as a parameter. This is acceptable by the function over-
loading rules: two functions with the same name and in the same scope must
have different parameters (number of parameters and/or types of parameters).
68
class Rat {
. . .
public:
Rat(int n, int d) {
this->n=n/gcd(n,d);
this->d=n/gcd(n,d);
}
Rat(int n) {
this->n=n;
d=1;
}
. . .
};
int main() {
Rat x=Rat(2,1); //calling the first constructor
. . .
public:
Rat(int n, int d=1) {
this->n=n/gcd(n,d);
this->d=n/gcd(n,d);
}
Rat(int n) {
this->n=n;
d=1;
}
. . .
};
int main() {
Rat x=2; //is this calling Rat(2,1) i.e. first constructor
//or calling Rat(2) i.e. second constructor
69
While both constructors are distinguishable according to the function over-
loading rules, the compiler cannot resolve which constructor must be called in
the example above. Therefore, the only way to construct a rational number in
this case is by calling the first constructor with two arguments explicitly all the
time!
int main() {
Rat x;
}
int main() {
Rat x=Rat();
}
class Rat {
. . .
public:
Rat(int n, int d) {
this->n=n/gcd(n,d);
this->d=n/gcd(n,d);
}
Rat(int n) {
this->n=n;
d=1;
}
Rat() {
n=0;
d=1;
}
. . .
};
70
Therefore, it is possible to obtain a compile time error just because the
default constructor is missing. We will look at other cases later.
If a class has no constructors at all, then C++ adds a default constructor
to it that does nothing. Here’s an example:
class Rat {
private:
int n;
int d;
public:
int numer() {
return n;
}
int denom() {
return d;
}
};
class Rat {
private:
int n;
int d;
public:
Rat() { }
int numer() {
return n;
}
int denom() {
return d;
}
};
allowed because x.numer() simply returns the value of a variable and not the variable itself to
be assigned. We say that x.numer() is not an l-value (something that can appear to the left
of an assignment).
71
int main() {
Rat x=Rat(2,3);
. . .
//attempt to modify x
x.n=3;
x.d=4;
. . .
}
The only way we can modify a rational number is to assign it to another one
as in the following example:
int main() {
Rat x=Rat(2,3);
. . .
x=Rat(4,5);
. . .
}
However, this means that almost every time we have to modify a rational
number, we have to construct another one. The above code is equivalent to the
following.
int main() {
Rat x=Rat(2,3);
. . .
Rat temp=Rat(4,5);
x=temp;
. . .
}
While this may be acceptable in some cases, it is not acceptable if the object
to be constructed is very large (wasting memory). Moreover, with such a strat-
egy, we have to carefully examine the meaning of an assignment. What does
assignment of objects mean? For instance, consider the following program:
int main() {
Rat x=Rat(2,3);
Rat y;
y=x;
}
Clearly, if x and y were simply integers for instance (or any other basic type),
then the semantics of the assignment are intuitive: copy the value of x into y.
72
&x
copy
&y
This means copy the integer in location &x (the address of x) into location &y
(the address of y).
When x and y are objects of some defined class, then assignment is still a
copy operation, except that the copying is done on a member by member
basis. Therefore, in the above program, x.n is copied into y.n and x.d is copied
into y.d. This is generally the default operation of assignment.
&x n
d
copy
&y n
d
int main() {
Rat x=Rat(2,3);
Rat y=x; //this is not an assignment
}
73
The copy constructor is also involved in constructing the parameters of a
function using the arguments, and in copying the result returned by the function.
Here’s an example:
Rat dummy(Rat x) {
return x;
}
int main() {
Rat x=Rat(2,3);
dummy(x);
}
class Rat {
private:
int n;
int d;
public:
Rat(int n, int d) {
set(n,d);
}
Rat(int n) {
set(n,1);
}
Rat() {
set(0,1);
}
int numer() {
return n;
}
int denom() {
return d;
}
74
int main() {
Rat x=Rat(2,3);
. . .
x.set(4,5);
. . .
}
Note that the constructors have been rewritten in terms of the new function.
This is possible since a member function of the class can be called from within
another member function.
. . .
}
However, the use of const is more versatile and can be tricky. Here are some
basic variations.
. . .
}
This means that x cannot be assigned after it is declared. Any attempt to
assign x will cause a compile time error.
. . .
}
This means that p cannot be assigned after it is declared. Any attempt to
assign p, i.e. change the address to which is is pointing, will cause a compile
time error.
75
8.6.3 Pointer to constant object
To declare a pointer to a constant object we use the following syntax:
int main() {
const T * p; //note: no need to initialize p, it’s not constant
. . .
}
This means that the object cannot change through pointer p. Any attempt
to change the object by dereferencing p will cause a compile time error.
int main() {
int x;
const int * p;
p=&x; //ok, p is assigned the address of x
x=2; //ok, x is not constant
*p=3; //error, attempt to change x through p
}
int main() {
const T * const p=...;
. . .
}
int main() {
int x;
int y;
const int z=1;
const int * const p=&x; //ok, p must be initialized because it is constant
x=2; //ok, x is not constant
*p=3; //error, attempt to change x through p
p=&y; //error, attempt to assign p another address
z=4; //error, z is constant;
}
76
Chapter 9
Operator overloading
9.1 Introduction
Consider the problem of adding rational numbers and outputting the result. So
far, we can do this task using our Rat class our addRat function as follows:
class Rat {
. . .
};
int main() {
Rat x=Rat(2,3);
Rat y=Rat(4,5);
Rat z;
z=addRat(x,y);
cout<<z.numer()<<’’/’’<<z.denom();
}
It would be more convenient if we could carry our abstraction one step
further and add rational numbers in the same way we add integers. Moreover,
to output a rational number we need to explicitly output its numerator, a “/”
symbol, and its denominator. It would be more convenient if we do not have to
worry about this formatting.
int main() {
Rat x=Rat(2,3);
Rat y=Rat(4,5);
Rat z;
z=x+y;
cout<<z;
}
While the code above looks more natural, unfortunately operator + does
not support our newly defined types. Similarly, operator << does not know
77
how to send a rational number to an output stream. Luckily, C++ provides a
way to redefine these operators to deal with new types. This is called operator
overloading.
//unary +
Rat operator+(Rat x) {
return x;
}
//unary -
Rat operator-(Rat x) {
return Rat(-x.numer(),x.denom());
}
78
int main() {
Rat x=Rat(2,3);
Rat y=Rat(4,5);
z=x+y; //equivalent to operator+(x,y);
z=x-y; //equivalent to operator-(x,y);
z=-y+x; //equivalent to operator+(operator-(y),x);
z=x+x*y; //equivalent to operator+(x,operator*(x,y));
z=x/y*x; //equivalent to operator*(operator/(x,y),x);
Note that the precedence of the operators is preserved when operators are
overloaded and cannot be changed. So far, what we have done offers us more
than what we would expect. For instance, all the following will work:
int main() {
Rat x=Rat(2,3);
Rat z;
z=x+x;
z=x+2;
z=2+x;
z=2+2; //this does not call the overloaded operator
//the result is converted from int to Rat
//before assignment (see below)
}
int main() {
Rat x=Rat(2,3);
Rat z;
z=x+2;
}
the class. The C++ standard allows an implementation to omit creating a temporary which
is only used to initialize another object of the same type. This optimization in enabled by
default in g++ for instance and, therefore, this step is not done. Specifying the option -fno-
elide-constructors disables that optimization, and forces g++ to call the copy constructor in
all cases. Another way to avoid the extra construction in Rat x=Rat(2,3) is to replaced it
with Rat x(2,3).
79
6. the copy constructor is used upon return to construct a temporary Rat
from the result, see footnote 1. 2 .
7. the local parameter x and the local parameter y are destroyed
8. the temporary Rat in step 6 is used by the assignment operator to assign
z
9. the temporary Rat is destroyed
10. the local Rat is destroyed (placed in this order in case step 6 is not done)
11. x and z are destroyed
If we did not have the second constructor (or if the constructor was declared
explicit as we will see later on), then step 4 of the above process will fail.
Instead, C++ will be looking for a + operator defined as operator+(Rat, int).
Therefore, it is a simply type mismatch. To solve the problem, we can overload
the + operator many times, once for every possible combination of parameter
types:
Unfortunately, the last option does not compile. The reason for this is that
C++ prevents from overloading the default behavior of operators for basic types.
If the last one were to succeed, then every addition of two integers will produce
a Rat instead of an integer (most likely an undesired behavior).
used to initialize another object of the same type. This optimization in enabled by default
in g++ for instance and, therefore, this step is not done. Specifying the option -fno-elide-
constructors disables that optimization, and forces g++ to call the copy constructor in all
cases.
80
• At least one parameter must be of non-pointer type and of non-basic type
(e.g. user defined class), unless the overloading is done inside the class
(see below)
• When overloaded within a class (we will see this shortly), one parameter
is implicit, i.e. the this pointer; therefore, the only restriction in this case
is the number of parameters (the number includes the implicit one)
int main() {
Rat x=Rat(2,3);
Rat y;
y=++x;
}
In the above program, y should be the rational number 5/3 (borrowing the
semantics from the ++ operator for integers and floating numbers). The pre
increment operator ++ increments a number by 1 and returns the result of the
increment. To achieve this behavior with rational numbers, we need to overload
the pre increment operator ++ to accept a parameter of class Rat. The ++
operator is a unary operator; therefore, we have to provide the appropriate
overloading with just one parameter:
Rat operator++(Rat x) {
return Rat(x.numer()+x.denom(),x.denom());
}
While the above returns the correct thing, which is a rational number equal
to 1 + x, it does not increment the argument itself. Therefore, the statement
y=++x assigns the correct value for y, but leaves x unchanged. At this point,
the dilemma is clear: How can we change x without passing it by reference using
a pointer?
Rat operator++(Rat * x) {
x->set(x->numer()+x->denom(),x->denom());
return x;
}
81
that purpose. For example, such overloading for the pre increment operator
would have to be invoked using an expression like ++&x, which does not look
right.
For this reason, C++ provides a way to pass arguments by reference with-
out pointers. Such a scheme is simply called passing by reference. It’s a
way to provide pointer semantics with non-pointer syntax or form. A parameter
declared this way is called a reference.
9.4.2 References
At some level, one might think of a reference as an alternative name of some-
thing. Here’s a simple example that illustrates the idea:
int main() {
int i=1;
//pointer
int * p=&i; //p is a pointer to i
*p=2; //change i to 2 by dereferencing p
//reference
int& r=i; //r is a reference to i
r=2; //change i to 2 through its reference
//pointer semantics
//non-pointer form
}
int main() {
int& r=i; int * const r=&i; //constant
//pointer
r=2; //changing pointer
//is not the intention *r=2;
}
82
References as types of function parameters
A reference can be the type of a function parameter. Here’s an example:
int main() {
int i=1;
int j=1;
f(i,j);
//i is still 1
//j is now 2
. . .
}
It should not be surprising that the value for j is changing in the above
program. After all, that’s what a reference must do: it provides pointer seman-
tics with non-pointer syntax. The above program can be better understood if
we replace the reference with its internal implementation (when an argument is
passed by reference, a pointer is passed instead).
int main() {
int i=1;
int j=1;
f(i,&j);
//i is still 1
//j is now 2
}
int main() {
int x=1;
int y=2;
swap(x,y);
//now x is 2 and y is 1
}
83
And it’s equivalence:
int main() {
int x=1;
int y=2;
swap(&x,&y);
//now x is 2 and y is 1
}
int& dummy(int& x) {
return x;
}
int main() {
int x=1;
dummy(x)=2; //in this case,
//equivalent to x=2
}
int& dummy(int x) {
return x;
}
Here the local variable x is being returned, which ceases to exist after the
function returns. We end up with a reference to something that does not exist!
84
9.4.3 Overloading operators ++ and −− (almost) cor-
rectly
3
Now that we know how to achieve pointer semantics without pointers, let’s
overload the pre increment operator:
Rat operator++(Rat& x) {
x.set(x.numer()+x.denom(),x.denom());
return x;
}
What if we would like to overload the post increment operator ++? The
post increment operator is similar to the pre increment operator except that it
returns the value before the actual increment is performed. For instance, in the
following program, y is assigned the value 1 and then x becomes 2.
int main() {
int x=1;
int y=x++;
}
Therefore, we have two issues to consider. First, since both operators (pre
and post increment) are unary, we must find a way to distinguish them from
each other. Having resolved this issue, we must find a way to return the value
of the argument before increment.
To distinguish pre increment from post increment, C++ assumes that the
post increment operator takes an additional parameter of type int, which is
ignored. Therefore, we are looking at overloading the following operator:
. . .
}
To return the value of the argument before the increment, we can simply
use a temporary variable as follows:
//pre decrement
Rat operator--(Rat& x) {
x.set(x.numer()-x.denom().x.denom());
return x;
}
3 See further notes to understand why almost.
85
//post decrement
Rat operator--(Rat& x, int) {
Rat temp=x;
--x;
return temp;
}
int main() {
Rat x=Rat(2,3);
cout<<x;
}
then we must overload operator << with the first parameter being an output
stream (the type of cout) and the second parameter being a Rat.
#include <iostream>
using std::ostream;
The important question here is what should operator<< return? The answer
really depends on how the operator << is intended to be used. Generally, with
such an operator, chaining is desired. For instance, the following statement
should work.
cout<<x<<y<<z;
Unfortunately, this specific way of overloading the << operator will cause
a compiler error due to a particular implementation of the ostream class. To
understand this point, let’s examine what happens with the following statement:
cout<<x;
4 See further notes to learn a better way for overloading this operator.
86
The function operator<< is called with two arguments: cout (an ostream
object) and x (a Rat object). Focusing on the first argument, the local parame-
ter s in operator<< is constructed using the copy constructor of class ostream.
Similarly, when s is returned, this copy constructor is involved again. However,
the copy constructor of class ostream is private (not really, it’s that of its base
class but we are not concerned about this at the moment). The key point is that
C++ forbids the call to the copy constructor of class ostream. The reason for
this is efficiency to avoid excessive copying of a large object such as an output
stream.
Consequently, we must refrain from copying such an object. The only way
is, therefore, to pass it (and return it) by reference. Here’s the final version.
The following would have worked also (why?) but is not desired (why?):
Since the designer of class Rat is likely also the person providing this operator
overloading, C++ provides an escape strategy for this situation. We can declare
operator+ as a friend of class Rat. In general, a friend function of a class can
access the private members (date or functions) of the class. Here’s how this
friendship can be achieved:
class Rat {
. . .
};
87
Here’s another example to re-instate numer() and denom():
class Rat {
. . .
};
int numer(Rat x) {
return x.n; //ok, it’s a friend
}
int denom(Rat x) {
return x.d; //ok, it’s a friend
}
Note that only the designer/implementer of a class can decide which func-
tions must be friends. Therefore, the technique of friend functions cannot jeop-
ardize the class.
An alternative to using friend functions is to move operator+ inside the class,
i.e. to make it a member function (recall that member functions have access to
private member data). But every member function has an implicit parameter
(the this constant pointer). Therefore, the implicit pointer provides the first
operand for the operator 5 . Here’s an example:
class Rat {
int n;
int d;
public:
. . .
Rat operator+(Rat y) {
return Rat(n*y.d+d*y.n,d*y.d);
}
. . .
};
parameters is removed, since the constant this pointer is always one of the parameters and is
passed implicitly.
88
Now the operator can be used as before:
int main() {
Rat x=Rat(2,3);
Rat y=Rat(4,5);
Rat z;
z=x+y; //equivalent to z=x.operator+(y);
//(internally, z=Rat::operator+(y,&x);)
}
With this technique, however, we break some functionality that used to work
when the operator was defined outside the class. Consider the following:
int main() {
Rat x=Rat(2,3);
Rat z;
z=x+2; //ok, equivalent to z=x+Rat(2);
z=2+x; //error, 2.operator+(x) meaningless
}
Before, what made x+2 and 2+x work was the fact that C++ was implicitly
converting the 2 into a rational number using the appropriate constructor (the
one that takes one integer as parameter). Now, however, a function operator+
that takes two rational numbers as parameters does not exists. We only have a
member function operator+ in class Rat. This means that this function must
be called through a Rat object. The statement 2+x does not give C++ enough
information on which object to use (compiler is not intelligent enough to switch
the order of parameters, and of course it shouldn’t in general).
In this situation, to fix the inconsistent behavior of x+2 and 2+x, one
could only disable x+2 from working. This is done by declaring the constructor
Rat(int) as explicit, which tells C++ not to use it implicitly for conversion.
class Rat {
. . .
public:
. . .
. . .
};
Based on the above scenario, it may be better to overload the + operator
outside class Rat. This raises the question: which operators should be over-
loaded as member functions? Well, here’s a rule of thumb:
• unary operators (the implicit this parameter becomes the operand)
• when the first operand is always the object itself
• when we have to (some operators must be member functions, e.g. the
assignment operator which we will study later, the [] operator, ...)
89
class Rat {
int n;
int d;
public:
. . .
Rat operator++() {
n=n+d;
return *this;
}
Rat operator++(int) {
Rat temp=*this;
n=n+d;
return temp;
}
int operator[](int i) {
if (i==0)
return n;
else
return d;
};
90
Chapter 10
Constness
10.1 Introduction
So far, we have seen the use of references in two contexts:
While the first use of references deals with form, the second deals with
efficiency. The problem is that both uses are indistinguishable and, therefore,
fail to convey the intention of the programmer. For instance, consider the
following function (the body of the function is intentionally unrevealed):
void secret(Rat& x) {
. . .
};
int main() {
Rat x=Rat(2,3);
secret(x);
}
The user knows that passing by reference provides pointer semantics (well,
assuming he/she took CSCI135). Therefore, to be on the safe side, the user may
assume that the secret function is going to modify the Rat argument (he/she
cannot rely on x remaining the same after calling the function). However,
the programmer might have just used a reference to avoid calling the copy
constructor of Rat, and had no intention of changing the argument.
91
void secret(const Rat& x) {
. . .
};
In fact, const is not just a way to convey intention, it is used by the compiler
as a guarantee: any attempt to modify x within the body of the function will
generate a compiler error.
Let’s see how we can benefit from this technique. For instance, we can
rewrite our operator+ function to pass the arguments by reference for efficiency
(copy constructor is not involved in constructing the parameters). Moreover,
since operator+ is not supposed to modify the operands, we can use const in
the declaration of the parameters.
The compiler must guarantee that the body of the function operator+ does
not modify parameters x and y. Obviously not such attempt is identified. At
least for us. But how can the compiler really tell that, for instance, calling the
member functions x.numer() and x.denom() does not modify object x? To
make this issue clear, consider the following two functions:
92
class Rat {
int n;
int d;
public:
. . .
int numer()const {
return n;
}
int denom()const {
return d;
}
. . .
};
public:
int numer()const {
//cannot modify the object here
//i.e. neither n nor d can change
return n;
}
};
93
What seems to be a complicated behavior is simply achieved by a simple
typing convention: when a member function is declared const, the type of its
implicit parameter (the this pointer) is changed from a “constant pointer to an
object” to a “constant pointer to a constant object”.
class Rat {
int n;
int d;
public:
. . .
. . .
};
x.numer();
//equivalent to Rat::numer(&x)
//&x is pointer to const, and numer(const Rat * const)
//accepts pointer to const
//ok
x.set(4,5);
//equivalent to Rat::set(4,5,&x)
//&x is pointer to const, but set(int, int, Rat * const)
//accepts pointer to non-const
//error
}
If member function numer() were not declared as const, the second state-
ment of the above program would have failed to compile. Therefore, it is im-
94
portant to declared every member function that does not modify the object as
const.
• we may pass an argument by reference for efficiency (to avoid the copy con-
structor), in which case it is better to declare its corresponding parameter
as a constant reference
• every member function that does not modify the object must be declared
as const; this declares its implicit parameter as a constant reference
public:
. . .
int numer()const {
return n;
}
int denom()const {
return d;
}
Rat operator++() {
n=n+d;
return *this;
}
95
Rat operator++(int) {
Rat temp=*this;
n=n+d;
return temp;
}
int main() {
Rat x=Rat(2,3);
Rat y=Rat(4,5);
Rat z=Rat(7,6);
x+y=z;
}
class Rat {
int n;
int d;
public:
. . .
96
int numer()const {
return n;
}
int denom()const {
return d;
}
97
98
Chapter 11
11.1 Introduction
To declare a character, we simply need to specify the type char, for example:
int main() {
char c=’a’; //use single quotation for character
. . .
}
But what does it take to declare a string. So far we have been using strings
as expressions between double quotation marks like for instance “hello”. As we
will see shortly, these expressions represent constant strings, i.e. values to be
assigned to strings. But how do we declare a string as a variable that can be
assigned? The first thing we have to figure out is the type of such a variable.
In C, a string is simply an array of characters. This implies that a string has
the type char *, a pointer to the first character (element) of the string (array).
int main() {
char * s=’’hello’’;
cout<<s; //outputs the characters of ‘‘hello’’
}
There are two important issues with the above program:
• In general, there is no way to determine the size of an array simply from a
pointer to its first element. Therefore, how does cout know when to stop
outputting characters?
• According to the C++ standard, “hello” is a constant string, i.e. the type
of “hello” is const char *. Therefore, how come we can initialize a char
* variable with a string like “hello” (violation of constness rules)?
The answer to the first question is that every string expression “...” is
terminated by a special character known as the null character ’\0’. Therefore,
“hello” is actually “hello\0”. The null character is interpreted as the end of the
string. That’s why we call C strings null terminated strings.
99
char s[]=“hello”; s h
e
l
l
o
\0
The answer to the second question is that C++ grants a special dispensation
for initializations like that because the practice is so common. Nevertheless, they
should be avoided. For instance, the following program generates a run-time
error:
int main() {
char * s=’’hello’’;
s[0]=’b’; //s is pointer to non-const, ok at compile time
//but it points to a const, error at run-time
}
int main() {
char s[]=’’hello’’;
s[0]=’b’;
}
int main() {
char s[6]={’h’,’e’,’l’,’l’,’o’,’\0’};
s[0]=’b’; //ok at compile time and run-time
}
int main() {
char s[]=’’hello’’;
char t[]=s; //error, invalid initializer
char r[6];
r=s; //error, C++ forbids assignment of arrays
}
100
Therefore, the only way to assign strings is to treat them as pointers (re-
call that an array is a pointer to its first element). C++ does not forbid the
assignment of pointer of course; however, this means that assignment of strings
has pointer semantics: one pointer is assigned the value of another. Here’s an
example:
int main() {
char s[]=’’hello’’;
char * t=s; //ok
}
s t
h
e
l
l
o
\0
Figure 11.2: Strings (pointers) s and t share the same physical memory
. . .
return t;
}
int main() {
char s[6]=’’hello’’;
char t[6];
strcpy(t,s);
}
101
11.3 Strings as parameters to functions
In the event of copying string s into string t, what if the length of s is unknown?
This can happen for instance if s is passed as an argument to a function:
. . .
}
One possibility is to make t large enough (of course there is always a question
of what is enough):
. . .
}
However, the code above is not totally safe. What if the length of s is 100 or
more? Then we would need at least 101 characters in t including the terminating
null character. Soon we realize that there is no escape from determining the
length l of string s. But if s is null terminated, we can do that easily and then
copy min(l + 1, 100) characters from s into t.
. . .
}
We can accomplish the same result using the built in functions strlen and
strncpy. strlen determines the length of a string and strncpy copies a number
of characters from one string into another. Both use essentially loop based
algorithms similar to above.
. . .
}
102
char * strncpy ( char * destination, const char * origin, int num) {
//copies the first num characters of source to destination
//no null-character is implicitly appended to the end of
//destination, so destination will only be null terminated
//if the length of the string in source is less than num
. . .
return t;
}
. . .
}
Since the length of a string s can be determined, one might want to replace
the above code with the following:
. . .
}
While this approach is appealing, we have seen that many compilers will
complain because the length of string (array) t is not known at compile time.
Therefore, we can keep such an approach if we use dynamic memory allocation:
. . .
delete[] t; //if needed
}
class Person {
char name[100];
public:
Person(const char * s) {
strncpy(name,s,min(100,strlen(s)+1));
name[99]=’\0’;
}
103
void print()const {
cout<<name<<’’\n’’;
}
};
The class Person has one private member datum, the name, which is a string
of 100 characters. For the public interface, it has a constructor which takes a
string as parameter (note the const), and a function to print the name on the
screen. Therefore, this class can be used as follows:
int main() {
Person saad=Person(’’saad’’);
saad.print();
}
class Person {
friend ostream& operator<<(ostream& s, const Person& p);
char name[100];
public:
Person(const char * s) {
strncpy(name,s,min(100,strlen(s)+1));
name[99]=’\0’;
}
};
With this operator overloading added, the class Person can now be used as
follows:
int main() {
Person saad=Person(’’saad’’);
cout<<saad;
}
Regardless of the choice used for printing a Person object, let us conduct a
little “experiment”:
int main() {
Person saad=Person(’’saad’’);
Person clone1=saad;
Person clone2;
clone2=saad;
}
104
Here’s a line by line description of the above “experiment”:
• Person saad=Person(”saad”);: declares a Person object called saad
using the class constructor with the string ”saad” as argument.
• Person clone1=saad;: declares a Person object called clone1 using the
default copy constructor of the class
• Person clone2;: declares a Person object called clone2 using the class
default constructor, and gives a compile time error because class Person
has no default constructor (we add one below)
• clone2=saad;: assigns saad to clone2 using the default assignment
operator
Here’s the added default constructor to class Person that will remove the
compile time error generated by the third line described above:
class Person {
friend ostream& operator<<(ostream& s, const Person& p);
char name[100];
public:
Person(const char * s) {
strncpy(name,s,min(100,strlen(s)+1));
name[99]=’\0’;
}
Person() {
strcpy(name,’’john’’);
}
};
105
Since the member datum name is a static array, each element of name is
copied separately. But each element is a character (built in type); therefore,
copying the name is equivalent to a simple assignment on a character by char-
acter basis. As a result, we have the correct behavior that we expect from a
Person class.
But there is one small problem. One might argue that the 100 characters are
enough to fit any name. That is likely true; however, we can’t claim that this is
completely safe. Bill Gates once said “640K ought to be enough for anybody”.
Maybe a person with a more than 100 character name exists! To be completely
safe, we must declare name as a pointer and use dynamic memory allocation:
class Person {
friend ostream& operator<<(ostream& s, const Person& p);
char * name;
public:
Person(const char * s) {
name=new char[strlen(s)+1];
strcpy(name,s);
}
Person() {
name=new char[5];
strcpy(name,’’john’’);
}
};
int main() {
Person saad=Person(’’saad’’);
Person clone1=saad;
Person clone2;
clone2=saad;
}
• We must free the memory that we allocated for each Person object when
this object is no longer in use
• The member datum name is now declared as a pointer, so the default copy
constructor will now simply assign pointers (see Section 2 on assignment
and pointer semantics)
• Same point above holds for the default assignment operator
106
name name
clone1 s clone2
a
a
name d
\0
saad
We will see how to deal with the three issues mentioned above in the next
chapter.
107
108
Chapter 12
Destructor, copy
constructor, and assignment
operator
char * name;
public:
Person(const char * s) {
name=new char[strlen(s)+1];
strcpy(name,s);
}
Person() {
name=new char[5];
strcpy(name,’’john’’);
}
};
109
int main() {
Person saad=Person(’’saad’’);
Person clone1=saad; //default copy constructor
Person clone2;
clone2=saad; //default assignment operator
//memory is not freed
}
name name
clone1 s clone2 j
a o
a h
name d n
\0 \0
saad
a memory leak
(not accessible anymore)
• The destructor has the same name as the class name preceded by the ’˜’
symbol
From the second point above, we conclude that a destructor cannot be over-
loaded, i.e. we can have at most one destructor for a class (otherwise, the
multiple destructors will have identical list of parameters).
1 This is not true for objects that are dynamically allocated using the new operator.
110
class Person {
friend ostream& operator<<(ostream& s, const Person& p);
char * name;
public:
. . .
~Person() {
delete[] name;
}
};
int main() {
Person saad=Person(’’saad’’);
}
is equivalent to:
int main() {
Person saad=Person(’’saad’’);
saad.~Person(); //this is implicitly added by compiler
}
s
a
a
~Person() { d
delete[] name; \0
}
saad.~Person();
For dynamically allocated objects (declared using the new operator), the
destructor is not called until that object is explicitly freed (using the delete op-
erator). This is because a dynamically allocated object must live until explicitly
deleted.
int main() {
Person * saad=new Person(’’saad’’);
}
111
is equivalent to:
int main() {
Person * saad=new Person(’’saad’’);
}
int main() {
Person * saad=new Person(’’saad’’);
delete saad;
}
is equivalent to:
int main() {
Person * saad=new Person(’’saad’’);
saad->~Person();
delete saad; //by now there are two news and two deletes
}
saad . . .
}
name
delete s
a
a
~Person() { d
delete[] name; \0
}
saad->~Person();
delete saad;
int main() {
Person people[10]; //default constructor
}
is equivalent to:
int main() {
Person people[10]; //default constructor
for (int i=0; i<10; i++)
people[i].~Person();
}
112
int main() {
Person * people=new Person[10]; //default constructor
delete[] people;
}
is equivalent to:
int main() {
Person * people=new Person[10]; //default constructor
for (int i=0; i<10; i++)
people[i].~Person();
delete[] people;
}
class Person {
friend ostream& operator<<(ostream& s, const Person& p);
char * name;
public:
. . .
~Person() {
delete[] name;
name=0;
}
};
Finally (and somewhat related to the issue above), there is a small problem
with the way we defined the destructor for class Person. Consider the following
program again:
int main() {
Person saad=Person(’’saad’’);
Person clone1=saad; //default copy constructor
Person clone2;
clone2=saad; //default assignment operator
}
113
int main() {
Person saad=Person(’’saad’’);
Person clone1=saad; //default copy constructor
Person clone2;
clone2=saad; //default assignment operator
saad.~Person();
clone1.~Person();
clone2.~Person()
}
Since saad, clone1, and clone2 share the same string (i.e. saad.name,
clone1.name, and clone2.name are all equal, see Figure 1), the memory for
the string ”saad” if freed three times (once per each destructor call). Nulling
the pointer in this case does not help because each object has its own pointer.
This problem will disappear once we redefine the copy constructor and overload
the assignment operator to produce real clones.
int main() {
Person saad=Person(’’saad’’);
dummy(saad);
cout<<saad;
}
This code looks very “innocent”. The body of the function dummy is empty
and, therefore, nothing is done (hence the name of the function). Consequently,
one might naively think that this code is simply equivalent to the following:
int main() {
Person saad=Person(’’saad’’);
cout<<saad;
}
Unfortunately, it’s not! Let’s examine what really happens: When dummy
is called with object saad as argument, the local parameter p of the function
is constructed from saad using the copy constructor (the default one in this
case). Now p and saad share the same string, i.e. p.name==saad.name.
The parameter p goes out of scope when function dummy returns; therefore,
p.˜Person() (the newly declared destructor) is called. This means p.name is
freed, which also means that saad.name is freed. As a result, when function
dummy returns, object saad has no name. The statement cout<<saad; is
likely to produce a run-time error because operator<< will be accessing a place
in memory (i.e. pointer saad.name) that is not properly allocated anymore.
114
void dummy(Person p) {
//p is constructed using
//the default copy constructor
int main() {
Person saad=Person(’’saad’’);
dummy(saad);
//saad lost its name
cout<<saad;
}
To avoid such anomalies, the copy constructor must be properly declared and
defined to allocate separate memory for the newly constructed object, and to
copy the string pointed to by name character by character to the newly allo-
cated memory. Since a copy constructor is a constructor after all, it must have
the name of the class and no return type. But how can a constructor be iden-
tified as the copy constructor. The only way is by its list of parameters. C++
recognizes a copy constructor as a constructor that takes a constant reference
to the class (why constant? and why reference?), i.e. in our case it must be
declared as:
Person(const Person&);
In general, for a class X, the copy constructor is: X(const X&). Let’s
declare a copy constructor for our Person class:
class Person {
friend ostream& operator<<(ostream& s, const Person& p);
char * name;
public:
. . .
Person(const Person& p) {
name=new char[strlen(p.name)+1];
strcpy(name,p.name);
}
. . .
};
Note that we can access p.name inside the constructor even if name is
declared as a private member: the unit of protection is the class and not the
object.
115
12.4 Overloading the assignment operator
Referring to Figure 1, there are two problems to consider with the default as-
signment operator. First, after an assignment, objects share the sane physical
memory (memberwise copy, same as default copy constructor). Second, there
is a memory leak produced as a result of this default operation. Therefore, we
must overload the assignment operator and define it in a way to avoid the two
problems mentioned above.
The first thing to note when overloading the assignment operator is that
C++ requires that it must be a member function (hence enforcing the first
operand to be an instance of a user-defined class). The second thing to note
is that the assignment operator is a binary operator (it takes two operands).
Hence, besides the implicit this parameter (the first operand) as a consequence
of being a member function, it takes one additional parameter. Therefore, we
are looking at something like the following:
class Person {
friend ostream& operator<<(ostream& s, const Person& p);
char * name;
public:
. . .
---?--- operator=(---?---) {
. . .
}
. . .
};
Of course, the questions are now what type should the assignment operator
take and what type should it return? To answer these questions we must deter-
mine how the assignment operator should be used. For instance, the following
should work:
int main() {
Person saad=Person(’’saad’’);
Person clone; //default constructor
clone=saad; //equivent to clone.operator=(saad);
}
116
int main() {
const Person saad=Person(’’saad’’);
Person clone; //default constructor
clone=saad; //equivalent to clone.operator=(saad);
//saad is const by clone is not
}
class Person {
friend ostream& operator<<(ostream& s, const Person& p);
char * name;
public:
. . .
. . .
}
. . .
};
For the return type, one can simply choose void. In this case, however, the
following code will fail:
int main() {
Person x;
Person y;
Person z=...;
x=y=z; //equivalent to x=(y=z);
//equivalent to x.operator=(y.operator=(z));
}
class Person {
friend ostream& operator<<(ostream& s, const Person& p);
char * name;
117
public:
. . .
. . .
return *this;
}
. . .
};
One might suggest to return the second operand instead as in the following:
. . .
return p;
}
int main() {
Person x;
Person y;
Person z=...;
(x=y)=z;
}
If x=y returns a const Person&, then it cannot be assigned and the code
fails to compile. Since such code compiles if x, y, and z were built-in types
(although no one would actually write such code), it is a good idea to maintain
this behavior for user-defined classes. We conclude that
118
Finally, there is a small problem with the way we defined the assignment
operator for class Person. Consider the following program:
int main() {
Person saad=Person(’’saad’’);
saad=saad;
cout<<saad;
}
Obviously, once the memory for object saad is freed inside operator=,
the information is permanently lost! Therefore, we must first check for self-
assignment. Here’s the final version (and the correct one):
class Person {
friend ostream& operator<<(ostream& s, const Person& p);
char * name;
public:
. . .
. . .
};
• Destructor: ˜X()
• Always NULL pointers when you can (at initialization and after delete)
• If your class allocates memory dynamically but you don’t want to declare
a copy constructor or an assignment operator (object may be too compli-
cated to copy or must be unique), then declare them as private (without
defining them), C++ won’t be looking for their definitions (why?):
119
class X {
. . .
private:
X(const X&);
X& operator=(const X&);
. . .
};
120
Chapter 13
Multidimensional arrays
int main() {
int a[10];
. . .
}
We have also argued that an array is simply a pointer to its first element
and, as a consequence, the above array has type int *. Therefore, to pass an
array as an argument to a function, we simply have to specify the correct type:
The second parameter (the size) is needed because there is no way of deter-
mining the size of an array simply from a pointer to its first element. The above
function can be also defined as follows:
The syntax int a[] for a function parameter is the same as writing int * a.
While a bit misleading, the following syntax can also be used:
The reason why this is misleading is that the 10 between brackets is mean-
ingless. The compiler interprets the parameter int a[10] as int a[], i.e. it
does not enforce any type on the argument except it being a pointer to int.
Therefore, the following code compiles perfectly:
121
void f(int a[10]) {
//assume a has size 10
//do something
}
int main() {
int a[5];
. . .
f(a); //ok
}
We will see later how such type enforcement can be achieved. For now, the
compiler treats an array as a pointer to its first element (does not care about
its size). Therefore, the size of the array is usually provided as an additional
argument when passing the array as an argument to a function (unless the size
is known to the programmer in advance). Having said that, we must also em-
phasize the fact that the compiler must know the type of the individual elements
of an array. This is particularly important when dealing with multidimensional
arrays.
13.2 Tic-Tac-Toe
Suppose we want to create a tic-tac-toe game. Therefore, we need to represent a
3×3 grid of symbols (possibilities include ’ ’, ’X’, and ’O’). Such a representation
can be achieved using an array as follows:
int main() {
char grid[9]; //9 characters
. . .
}
So far, what was nice about arrays (one dimensional arrays that is) is their
ability to capture our mental image, which is a linear sequence of things. While
nine characters are all we need to store the state of a tic-tac-toe game, such a
linear array is not quite the mental picture that we usually have for tic-tac-toe.
grid O
X
O
X X
O
X O
(a) (b)
122
As Figure 1 shows, we have to come up with some convention to transform
the position of characters in the linear order to a position on the grid, and vice-
versa. For instance, grid[6] is the bottom left corner. While such a convention
is definitely possible, it may not be intuitive. Moreover, it may require some
extra work; for instance, what does a row of X’s mean in the linear order?
The problem here is that the linear array does not provide an appropriate
mapping between the representation of a grid and the notion of a grid. It would
be nice if the representation allows us to say: “the first element of the last row”.
But such a statement is meaningful only because we are viewing the grid as a
two dimensional object. Fortunately, we can declare an array to capture this
dimensionality. We simply have to declare an array of three elements, with each
element being itself an array of three characters. Such a two dimensional array
can be declared as follows:
int main() {
int grid[3][3]; //grid is an array of 3 elements
//each element is an array of 3 characters
. . .
}
The first element of the last row is now simply grid[2][0]. This is because
grid[2] is the last element of the array declared above. Since each element
is itself an array, grid[2][0] is the first element of the last row. In general,
grid[i][j] refers to the j th element of the ith row (both rows and columns start
with zero). Note that we can switch the notion of row and column in our mind
without affecting the representation (and this is not simply because we have
three rows and three columns).
The two dimensional array provides now a better mapping between the rep-
resentation of a grid and the notion of a grid. In fact, the use of the term
mapping here is essential. The representation itself did not change tremen-
dously because memory inside the machine is still linear. We cannot make the
memory representation live up to our mental image. Consider the following
“fancy” tic-tac-toe mental representation.
X
O
O
X
123
grid ≡ grid[0] O
one element (array) of grid
124
void f(char * a[3], int n) {
//do something
}
But this makes the parameter an array of pointers to characters, i.e. each
element of the array is a pointer to a character (this would be appropriate for
an array of strings). The fact that the array has a size of three is irrelevant.
It turns out that the correct way to specify the type is the following:
void f(char a[][3], int n) {
//a is an array
//the size of the first dimension is irrelevant
//the size of the second dimension is 3
//do something
}
Let’s use our tic-tac-toe grid as argument in a meaningful way:
void init(char a[][3], int n) {
for (int i=0; i<n; i++)
for (int j=0; j<3; j++)
a[i][j]=’ ’;
}
int main() {
char grid[3][3];
init(grid,3);
. . .
}
Another way is to give the type “array of three characters” a name and use
that name as a built in type. This can be achieved using the typedef keyword
(a meaningful example follows).
typedef char symbol; //symbol is simply another name for char
typedef symbol row[3]; //row is simply another name
//for ‘‘array of 3 symbols’’
int main() {
row grid[3]; //array of 3 rows
init(grid,3);
. . .
}
125
In general, all dimensions except the first must be specified when the array
is passed as argument. Here’s a example:
int main() {
int cube[2][3][4];
. . .
f(cube,2);
}
int main() {
int a[2][3][4]={{{1,2,3,4},{5,6,7,8},{9,10,11,12}},
{{13,14,15,16},{17,18,19,20},{21,22,23,24}}};
. . .
}
In the above, a is an array that contains two elements, each of the two is
itself an array that contains three elements, each of the three is itself an array
that contains four integers. Since the representation of a multidimensional array
in memory is actually linear, with elements stored in the same order listed above
(see Figure 4), the compiler does not generally require the explicit braces in the
initialization.
int main() {
int a[2][3][4]={1,2,3,4,5,6,7,8,9,10,11,12,
13,14,15,16,17,18,19,20,21,22,23,24};
. . .
}
126
a ≡ a[0] 1
2
3
4
5
6
7
8
9
10
11
12
a+1 ≡ a[1] 13 a[1] ≡ a[1][0] 13
14 14
15 15
16 16
17 a[1]+1 ≡ a[1][1] 17
18 18
19 19
20 20
21 a[1]+2 ≡ a[1][2] 21 a[1][2] ≡ &a[1][2][0] 21
22 22 a[1][2]+1 ≡ &a[1][2][1] 22
23 23 a[1][2]+2 ≡ &a[1][2][2] 23
24 24 a[1][2]+3 ≡ &a[1][2][3] 24
int main() {
int m,n; //to be determined
. . .
//allocate memory
int ** grid=new int *[m];
for (int i=0; i<m; i++)
grid[i]=new int[n];
. . .
//free memory
for (int i=0; i<m; i++)
delete[] grid[i];
delete[] grid;
}
127
Essentially, we first allocate an array of m pointers. For this we need a
pointer (array) of type int **. Then, each one of the m pointers is dynamically
allocated as an array of n integers (of type int *). Note that the m arrays of
size n are not necessarily consecutive in memory as before. The following figure
illustrates the memory representation of the m × n array thus obtained.
grid ≡ &grid[0]
grid+1 ≡ &grid[1]
grid[i]+n-1 ≡ &grid[i][n-1]
128
Chapter 14
. . .
}
. . .
}
At the surface, it seems that the above two functions differ in their parameter
list (namely the type of the parameter). However, the fact that the array is
simply a pointer to its first element makes the size given between brackets
irrelevant (see previous chapter). The compiler treats both of these functions
as (thus a violation to the overloading rules):
. . .
}
When passed as argument, we say that the array decays into a pointer. The
pointer carries no information about the size of the array and, therefore, one
should pass the size as an extra argument as follows:
. . .
}
129
But what if we want the compiler to fully enforce the type on the argument?
Say for instance that we want to define a function that takes as parameter an
array of integers of size 10, i.e. the whole thing as one entity, not just the
pointer. We will look at two ways for achieving this task: passing the array by
reference and wrapping the array inside an object.
. . .
}
int main() {
int a[10];
int b[5];
f(a); //ok, correct type
f(b); //error, wrong type
}
We can use the sizeof operator to observe the difference in the type of the
argument when the array is passed as usual or by reference:
void f(int (&a)[10]) {
//array does not decay into a pointer
cout<<sizeof(a); //we will see 40 (each int is 4 bytes)
. . .
}
. . .
}
int main() {
int a[10];
cout<<sizeof(a); //we will see 40 (each int is 4 bytes)
f(a);
g(a);
}
130
14.3 A first look at templates
While we figured out a way to enforce the correct types on array arguments, we
lost some flexibility. We now need a function for every possible array size!
.
.
.
int main() {
int a[10];
int b[5];
f(a); //ok, specialize template with 10, equivalent to f<10>(a);
f(b); //ok, specialize template with 5, equivalent to f<5>(b);
}
131
Using a template, we can go even one step further and make the function
independent of the type of elements of the array.
The above template has two parameters, a class T and an int n. The function
thus defined takes an array of size n, each element of which has type T. The
array is passed by reference as before.
int main() {
int a[10];
char b[5];
f(a); //ok, equivalent to f<int,10>(a);
f(b); //ok, equivalent to f<char,5>(b);
}
Note that for the compiler to specialize the template, all the parameters
of the template must be known at compile time. Moreover, when arrays
are declared dynamically, they are declared as pointers and, therefore, cannot
be passed as arguments to a function expecting a reference to an array.
int main() {
int n;
. . .
A more general solution for handling arrays is to wrap the array inside an
object as explained in the following section.
is to wrap the array inside an object and passing the object around while it
provides the appropriate public interface to access the array.
132
Here’s an example of a wrapper for an array of integers:
class Array {
int * a; //the real array
int s; //the size of it
public:
Array() {
s=0;
a=0; //NULL pointer
}
Array(int n) {
s=n;
if (s==0)
a=0; //NULL pointer
else
a=new int[s];
}
~Array() {
delete[] a;
}
133
int size()const {
return s;
}
int& operator[](int i) {
return a[i];
}
void f(Array& a) {
int n=a.size();
int main() {
Array a=Array(10); //array of 10 integers
//initialize
for(int i=0; i<10; i++)
a[i]=0;
. . .
f(a);
}
We can use a template to convert the above class to a general array object
of any type.
template<class T>
class Array {
T * a; //the real array
int s; //the size of it
public:
Array() {
s=0;
a=0; //NULL pointer
}
134
Array(int n) {
s=n;
if (s==0)
a=0; //NULL pointer
else
a=new T[s];
}
~Array() {
delete[] a;
}
int size()const {
return s;
}
T& operator[](int i) {
return a[i];
}
135
template<class T>
void f(Array<T>& a) {
int n=a.size();
int main() {
Array<int> a=Array<int>(10); //array of 10 integers
Array<char> b=Array<char>(5); //array of 5 characters
. . .
f(a);
f(b);
}
C++ provides a vector class as part of the standard library, which is similar
to the class defined above except that it is more detailed.
template<class T>
class vector {
. . .
public:
//constructors
vector(); //constructs an empty vector
vector(const vector& c); //copy constructor
vector(int num, const T& val=T()); //constructs a vector of num
//elements of type T each being
//a copy of val or the default
//for class T if val is not given
. . .
//operators
T& operator[](int i); //returns a reference to the ith element
const T& operator[](int i)const; //same but constant reference
//operators =, ==, !=, <, >, <=, >= are also overloaded
. . .
136
//member functions
int size()const; //returns the number of items in the vector
bool empty()const; //returns true if the vector has no elements
void push_back(const T& val); //adds an element to the end of the vector
void pop_back(); //removes the last element of the vector
. . .
};
We will illustrate how to use the vector class through the tic-tac-toe example:
int main() {
vector<char> row=vector<char>(3,’ ’);
vector<vector<char> > tictactoe=vector<vector<char> >(3, row);
}
The above example creates a vector of three characters called row and ini-
tializes its characters to the space character. Therefore, it uses the third con-
structor of the class with T being char, num being 3, and val being ’ ’. Then
it creates a vector of three vectors of characters (two dimensional) and initial-
izes each vector to row. This also used the third constructor but with T being
vector<char>, num being 3, and val being row. Note the space between
the two ’>’ in vector<vector<char> >. This is needed because othewise the
compiler will think of >> as the shift operator (like the one used with an input
stream, e.g. cin).
Another variation is to declare tictactoe as an empty vector and add row
three times to it.
int main() {
vector<char> row=vector<char>(3,’ ’);
vector<vector<char> > tictactoe;
for (int i=o; i<3; i++)
tictactoe.push_back(row);
}
Because operator[] is overloaded for vectors, vectors can be used in the same
way as arrays. Here’s an example:
int main() {
. . .
. . .
}
137
14.6 Arrays vs. vectors
One of the legitimate questions now is when to use arrays and when to use vec-
tors. A vector has the added functionality to change its size (using push back()
and manage dynamic memory. Therefore, here’s a rule of thumb:
• If the size of the array is known at compile time, and does not change at
run time, use an array
• If the size of the array is not known at compile time, but does not change
at run time, use either an array or a vector
#include <vector>
using std::vector;
template<class T>
int minimum (const vector<T>& a, int start, int end) {
int index=start;
for (int i=start; i<=end; i++)
if (a[i]<a[index]) //assumes type T overloads operator <
index=i;
return index;
}
template<class T>
void swap(vector<T>& a, int i, int j) {
T temp=a[i];
a[i]=a[j];
a[j]=temp;
}
template<class T>
void sort(vector<T>& a) {
int n=a.size();
for (int i=0;i<n;i++)
swap(a, i, minimum(a,i,n-1));
}
138
int main() {
vector<int> a=vector<int>(10);
a[0]=1;
a[1]=2;
a[2]=5;
a[3]=4;
a[4]=3;
a[5]=8;
a[6]=7;
a[7]=6;
a[8]=10;
a[9]=9;
sort(a);
}
To illustrate the power of templates, we can sort rational numbers using the
same code, provided that the class Rat has overloaded operator <.
class Rat {
. . .
public:
bool operator<(const Rat& r) {
return (n*r.d<d*r.n);
}
. . .
};
int main() {
vector<Rat> a=vector<Rat>(3); //assumes Rat has a default constructor
a[0]=Rat(2,3);
a[1]=Rat(1,2);
a[2]=Rat(1,4);
sort(a); //voila!
}
class Person {
char * name;
public:
Person() {...}
Person(const char * s) {...}
Person(const Person& p) {...}
Person& operator=(const Person& p) {...}
~Person() {...};
139
char * getName()const {
return name;
}
};
The newly added function returns a pointer to private data (the string).
This means that a code can now change the person’s name through that pointer
as shown below:
int main() {
Person saad=Person(’’saad’’);
char * s=saad.getName();
s[0]=...;
cout<<saad.getName(); //now changed
}
How can we avoid this problem of exposing private data while still being
able to obtain a person’s name? One may think of a number of solutions.
. . .
};
In this case however, the characters of name cannot be assigned, not even
in the constructor. For instance, the following code will fail to compile.
class Person {
const char * name;
public:
Person() {
name=new char[5];
strcpy(name, ’’john’’); //error, name is pointer to const
}
. . .
};
public:
. . .
140
void getName(char * s)const {
strcpy(s, name);
}
};
int main() {
Person saad=Person(’’saad’’);
char s[5];
saad.getName(s);
cout<<s;
}
class Person {
char * name;
public:
. . .
int length()const {
return strlen(name);
}
int main() {
Person saad=Person(’’saad’’);
char * s=new char[saad.length()+1]; //make sure to do this step
saad.getName(s);
cout<<s;
}
public:
. . .
141
char * getName()const {
char * s=new char[strlen(name)+1];
strcpy(s, name);
return s;
}
};
The person’s name is copied into another char * variable which is dynam-
ically allocated and returned. Such approach however will make it impossible
to predict when this newly allocated variable should be freed. It becomes the
responsibility of the class user to free the memory.
int main() {
Person saad=Person(’’saad’’);
char * s=saad.getName();
cout<<s;
. . .
public:
. . .
int main() {
Person saad=Person(’’saad’’);
char * s=saad.getName(); //error, must be const
const char * t=saad.getName(); //ok
t[0]=...; //error, t is pointer to const
}
This might seem to be the perfect solution for our problem. However, not if
someone knows how to “cast away” const:
142
int main() {
Person saad=Person(’’saad’’);
char * s=(char *)saad.getName(); //ok, cast into non-const
s[0]=...;
cout<<saad.getName(); //now changed
}
class String {
friend ostream& operator<<(ostream&, const String&);
public:
String() {
len=0;
str=0; //NULL pointer
}
143
String(const String& s) {
len=s.len;
if (len==0)
str=0; //NULL pointer
else {
str=new char[len+1];
for(int i=0; i<len+1; i++)
str[i]=s.str[i];
}
}
~String() {
delete[] str;
}
int size()const {
return len;
}
char& operator[](int i) {
return str[i];
}
C++ provides a string class as part of the standard library, which is similar
to the class defined above except that it is more detailed.
144
14.9 C++ strings
C++ provides the following class:
class string {
. . .
public:
//constructors
string(); //constructs an empty string
string(const string& s); //copy constructor
string(int len, const char& c); //len copies of c
string(const char * str); //constructs a string from str
string(const char * str, int len); //from str up to len characters
string(const char * str, int index, int len); //same starting at index
. . .
//operators
char& operator[](int i); //returns a reference to the ith character
const char& operator[](int i)const; //same but constant reference
string& operator=(const string& s);
string& operator=(const char * str);
string& operator=(char c);
. . .
//member functions
int size()const; //returns the length of the string
bool empty()const; //returns true if the string is empty
string substr(int index, int num); //returns a substring with num
//characters starting at index
. . .
};
//concatenation operators
string operator+(const string& s1, const string& s2);
string operator+(const string& s, const char * str);
string operator+(const char * str, const string& s);
string operator+(const string& s, char c);
string operator+(char c, const string& s);
145
//stream operators
ostream& operator<<(ostream& os, const string& s);
istream& operator>>(istream& is, string& s);
As an example of using C++ strings, let us now rewrite our Person class.
For one thing, we do not have to manage the dynamic memory allocation of a
char * string. This will be handled internally by the C++ string class. This
means that we no longer need to declare a destructor, a copy constructor, and
an assignment operator.
class Person {
string name;
public:
Person() {
name=’’john’’;
}
Person(const string& s) {
name=s;
}
string getName()const {
return name;
}
};
Note that getName() now returns a string object. This means the copy
constructor of class string is used to return name and, therefore, the user has
no control over this private member datum.
int main() {
Person saad=Person(’’saad’’);
string s=saad.getName();
s[0]=...; //changes s not saad.name;
cout<<saad.getName(); //unchanged
}
class Person {
string name;
public:
146
Person() {
name=’’john’’;
}
Person(const string& s) {
name=s;
}
string getName()const {
return name;
}
};
This is the first time we have a user defined class in which one of the mem-
ber data is itself an object of a user defined class. This raises an issue about
construction: To construct a Person, one must construct a string. A Person
object cannot exist without a string object as part of it. So how is that done?
Here’s the rule: Before an object is constructed, each of its member
data objects is constructed using the default constructor of the cor-
responding class 1 . For our particular example, name is contructed first using
the default constructor of class string, then the Person object is constructed
using whatever constructor was invoked. This means that name=... in the
above three constructors is actually an assignment of an empty string.
This seems a bit inefficient. The data member name is first constructed as
an empty string, then assigned using the assignment operator of class string.
This is not our idea of initialization. We would like to directly initialize name
to the desired string. Such initialization (not assignment) can be achieved by
overriding the rule stated above. In each constructor for Person, we can instruct
the compiler what constructor to use for name, hence avoiding the default
constructor and the need for the extra assignment. Here’s the syntax:
class Person {
string name;
public:
Person(): name(’’john’’) { //call constructor with const char *
}
1 Similarly, after the object is desctucted, the corresponding destructor for each of its
147
string getName()const {
return name;
}
};
148
Chapter 15
Recursive functions
int main() {
cout<<fact(6);
}
Using the substitution model described earlier (the parameter is replaced
with the value of the argument in the body of the function), we can observe the
recursive process generated by fact(6).
149
fact(6)
6*fact(5)
5*fact(4)
4*fact(3)
3*fact(2)
2*fact(1)
1
2
6
24
120
720
int fact_iter(int n) {
return fact_iter(1,1,n);
}
int main() {
cout<<fact_iter(6);
}
The idea is basically to carry forward the new values for all parameters as
arguments for the recursive function call. Let us observe the process generated
by fact iter(6) as we did in the previous section.
150
fact_iter(6)
fact_iter(1,1,6)
fact_iter(1,2,6)
fact_iter(2,3,6)
fact_iter(6,4,6)
fact_iter(24,5,6)
fact_iter(120,6,6)
fact_iter(720,7,6)
720
1
2 2 2
3 3 3 3 3
4 4 4 4 4 4 4
5 5 5 5 5 5 5 5 5
6 6 6 6 6 6 6 6 6 6 6
function (syntax). In fact, a recursive function may represent an iterative process (Section 2).
151
With the iterative process, the compiler needs to remember only the current
values of the parameters: product, counter, and n. This is due to the fact that
the recursive call is the last thing performed by the process. This type of
recursion is called tail recursion: there is nothing left to do after the recursive
call returns. Therefore, the compiler does not really need to recall anything.
In fact, the current values of product, counter, and n completely determine the
state of the computation. If we stop the computation at any point in time,
and forget all the history, we can simply resume by starting from those values.
This is not the case for the recursive process (we must remember where we
stopped). While in general compilers still use the call stack, a “smart enough”
compiler will detect tail recursion. In this case, the amount of memory needed
is a constant independent of n (only store the current values of the parameters),
or Θ(1).
Compilers with the above property are said to use a tail recursive imple-
mentation. With the tail recursive implementation, the compiler transforms the
recursive function into a standard loop iteration (thus eliminating the stack).
To transform a tail recursion into a loop, the following is done:
int fact_iter(int n) {
return fact_iter(1,1,n);
}
This should now look like a familiar implementation. It is nothing but the
iterative version of a tail recursive function. Here’s a comparison among the
alternative implementations of factorials:
So why use recursion at all? The answer to this question is that recursion is
sometimes the only natural way to think about a solution for the problem, and
gives a simple and elegant code.
152
15.4 Bring on the rabbits
In 1200 AD, the italian mathematician Leonardo De Piza, also known as Leonardo
Fibonacci (don’t confuse him with Leonardo Da Vinci who is more recent), for-
mulated a problem on rabbits. His formulation lead to the conclusion that the
number of rabbits grows according the the following sequence, which has become
to be known as the Fibonacci sequence.
1, 1, 2, 3, 5, 8, 13, 21, . . .
Therefore, the Fibonacci of an integer n is defined as:
½
F ib(n − 1) + F ib(n − 2) n > 1
F ib(n) =
1 otherwise
To illustrate the power of recursion, here’s a recursive implementation of
Fibonaccis which is a direct translation from the mathematical definition:
int fib(int n) {
if (n>1)
return fib(n-1)+fib(n-2);
else
return 1;
}
This code is simple, elegant, clear, and self explanatory. But it is a terrible
way of computing Fibonaccis. Let’s look at the recursive process generated by,
say fib(5). The fib function calls itself twice each time it is invoked, so we will
end up with a tree like “shape” for the recursive process.
fib(5)
fib(4) fib(3)
1 1 1 1 1
fib(1) fib(0)
1 1
153
It can be shown that the number of leaves in this case is always equal
to F ib(n). It can be also shown that F ib(n) is proportional to φn , where
φ = 1.61803... is the golden number. Therefore, the running time of this imple-
mentation of Fibonacci is Θ(φn ), which is exponential in n (bad). By contrast,
the height of the tree, and hence the required memory, is only Θ(n) (not that
bad).
A better way is to think about an iterative process for Fibonacci which will
lead to an implementation with Θ(n) running time and Θ(1) space. The idea
is to come up with a number of variables that will capture the state of the
computation at any point in time. If we keep track of the last two Fibonaccis
in the sequence, we can always compute the next one. Therefore, a possible
iterative process is to maintain two numbers a and b, and updates them as
follows:
a←b
b←a+b
Starting with both a = 1 and b = 1, b will be F ib(n) after repeating this
process n − 1 times.
int fib_iter(int n) {
return fib_iter(1,1,n);
}
fib_iter(5)
fib_iter(1,1,5)
fib_iter(1,2,4)
fib_iter(2,3,3)
fib_iter(3,5,2)
fib_iter(5,8,1)
8
154
int fib_iter(int a, int b, int n) {
while (n>1) {
b=a+b;
a=b-a;
n=n-1;
}
return b;
}
int fib_iter(int n) {
return fib_iter(1,1,n);
}
15.5 Exponentiation
Consider the problem of computing bn for an integer base b and a non-negative
integer exponent n. Again, using recursion, we first seek a simple direct trans-
lation from a mathematical definition to C++ code. Mathematically,
b · bn−1 n > 0
½
n
b =
1 otherwise
Here’s a direct translation:
This recursive process runs in Θ(n) time and Θ(n) space. Let us look now
for an iterative process. We maintain a running product and a counter, and we
update them according to the following rule until the counter reaches n:
product ← product · b
counter ← counter + 1
Alternatively, we can use n itself as a counter and decrement it until it
reaches zero. Starting with product= 1, and after n repetitions, product will be
equal to bn .
155
int exp_iter(int product, int b, int n) {
if (n>0)
return exp_iter(product*b,b,n-1);
else
return product;
}
This process runs in Θ(n) time and Θ(n) space. But it leads to a tail
recursive implementation that uses only Θ(1) space (see Section 3 for the trans-
formation).
A B C
156
The objective is to transfer the entire tower to one of the other pegs, say C,
using the other, say B, as a temporary peg, and obeying the following two rules
of the game:
• only one disk at a time can be moved
• a larger disk can never be moved onto a smaller one
The original legend (most likely also invented by Luca) says that n = 64 and
the disks are golden, and when all the disks are moved, the tower will collapse
and the world will end. But not to worry. It can be shown that at least 2n − 1
moves are needed. For n = 64, that’s 264 − 1 moves. If each move takes 1
second, that’s 584.9 billion years (the universe began around 13.7 billion years
ago).
The importance of this problem lies in the fact that the solution is naturally
recursive. Therefore, being able to write a recursive function is almost the only
practical way for solving this problem (of course every recursive implementation
can be made non-recursive, just simulate the call stack with arrays or vectors).
While the solution to this problem seems to be very hard at first, we may
assume that we know how to move a tower of n − 1 disks. Once we have done
that, here’s a solution for moving a tower of n disks.
• Move the first n − 1 disks from A to B using C as temporary
• Move the nth disk from A to C
• Move the first n − 1 disks from B to C using A as temporary
Therefore, to move the n disks, move the first n − 1 disks first, then move
the nth disk, then move the first n−1 disks again. This suggests a tree recursive
process in which the function calls itself twice each time it is invoked.
Let’s start with a basic function to move a disk from one beg to another.
void move(char a, char c) {
cout<<a<<’’->’’c<<’’\n’’;
}
Now onto the recursive implementation.
void hanoi(char a, char b, char c, int n) {
if (n>1) {
hanoi(a,c,b,n-1);
move(a,c);
hanoi(b,a,c,n-1);
}
else
move(a,c);
}
The tower of Hanoi is one of the best classical problems to illustrate the
power of recursion. One cannot hope for a better example of such a simple so-
lution for such a hard problem. Nevertheless, we can make some enhancements.
We can convert the tree recursive process into a linear recursive one by noting
that the second recursive call is always the last thing to be done. Therefore,
the second recursive call can be eliminated with a tail recursive implementation.
Here’s a tail recursive implementation following the guidelines listed in Section
3.
157
void hanoi(char a, char b, char c, int n) {
while (n>1) {
hanoi(a,c,b,n-1);
move(a,c);
char temp=a;
a=b;
b=temp;
n=n-1;
}
move(a,c);
}
This runs in Θ(2n ) time (2n − 1 moves are needed) and Θ(n) space (height
of the call stack).
158
Chapter 16
16.1 Introduction
In this chapter we are going to focus on “crazy” ways for doing things. Why
say crazy? Because there exist ways that are much more direct and intuitive.
So why do things the crazy way? Because it’s going to be more efficient in
terms of time. We are going to look at four crazy examples of doing things:
exponentiation, testing primes, sorting, and searching (this introduction does
not suggest that crazy always means efficient).
16.2 Exponentiation
We have seen before how to compute bn for an integer base b and a non-negative
integer exponent n. This was done in a straight forward way using the following
recursive definition:
b · bn−1
½
n>0
bn =
1 otherwise
Here’s a direct translation of the mathematical definition:
159
[bn/2 ]2 n even 6= 0
n
b = b · bn−1 n odd
1 n=0
int square(int x) {
return x*x;
}
bool prime(int n) {
for (int i=2; i<=sqrt(n); i++)
if (n%i==0)
return false;
return true;
}
√
√The “crazy” idea: Instead of checking n numbers, and hence spending
Θ( n) time, perform Fermat’s test on few numbers. Huh? Of course this crazy
idea seems to demand from us a little number theory. So here it is:
160
Of course there is still a tiny probability that we are wrong, but this idea
gives a much faster way for testing primality because we can use fast exp() to
compute an−1 . For illustration, here’s a code to perform one Fermat test.
bool fast_prime(int n) {
int a=rand()%(n-1)+1; //a in {1..n-1}
if (fast_exp(a,n-1)%n==1)
return true;
else return false;
}
The function fast prime() makes a one simple call to fast exp(a, n − 1).
Since fast exp() runs in Θ(log2 n) time, then fast prime() also runs
√ in Θ(log2 n)
time. While this is a much better running time compared to Θ( n), we have
introduced a problem. Namely, an−1 can be very large (a can be close to n) and,
therefore, we risk the problem of overflow. Since we are interested in computing
an−1 %n and not an−1 , we can solve this problem by making fast exp(a, n − 1)
compute all its intermediate results modulo n. Therefore, we change fast exp()
to take one additional parameter m and compute everything modulo m.
int square(int x) {
return x*x;
}
bool fast_prime(int n) {
int a=rand()%(n-1)+1; //a in {1,...,n-1}
if (fast_exp(a,n-1,n)==1)
return true;
else return false;
}
16.4 Sorting
Sorting an array of elements is a problem that we have seen before and solved
using the idea of repeatedly finding the smallest element and swapping it with
the first in successive parts of the array (see Note 6). The running time of
such an algorithm is proportional to the square of the number of elements to be
sorted. Therefore, we spend Θ(n2 ) time to sort n elements. The reason for this
is that we need to check all n elements to find the smallest. Having done that,
we then need to check n − 1 elements to find the second smallest, then n − 2
elements for the third smallest, and so on until the elements are sorted. The
total number of checks is 1 + 2 + . . . + (n − 1) + n = n(n+1)
2 = Θ(n2 ).
161
The “crazy” idea: Assume that the array is such that the left half and the
right half are sorted. Then sort the elements in Θ(n) time by merging the two
halves of the array: Use an auxiliary array and repeatedly copy the smallest
element of the two halves into successive positions. Then copy the auxiliary
array back into the original one. Here’s an example:
2 4 5 7 1 2 3 6
2 4 5 7 1 2 3 6
2 4 5 7 2 3 6
1 2
4 5 7 2 3 6
1 2 2
4 5 7 3 6
1 2 2 3
4 5 7 6
1 2 2 3 4
5 7 6
1 2 2 3 4 5
7 6
1 2 2 3 4 5 6
1 2 2 3 4 5 6 7
162
1 2 2 3 4 5 6 7
1 2 2 3 4 5 6 7
The running time for this merging operation is Θ(n) because every time we
make a comparison we copy one element into the auxiliary array. Therefore, we
do this at most n times. Without worrying about the detail, we will assume the
existence of a function to merge two sorted halves of the array (assume also an
array of integers):
void merge(int * a, int p, int q, int r) {
//merge a[p]...a[q] and a[q+1]...a[r]
}
This “crazy” idea assumed that each half of the array is sorted. But what
if that’s not the case? In fact, that’s why the idea is “crazy”, because now we
are going to ensure that the two halves are sorted by performing the following:
• split each in two halves
• assume the two halves are sorted
• merge the two halves
The argument is carried recursively until each half is only one element and
is, therefore, safe to be assumed sorted. This method for sorting is known as
merge sort. To some extent, merge sort refrains from actually sorting. It only
perform merges. But to ensure that the two halves are sorted and ready to be
merged, it recursively uses merge sort on each one of them.
void merge_sort(int * a, int p, int r) {
if (p<r) { //more than one element
int q=(p+r)/2;
merge_sort(a,p,q);
merge_sort(a,q+1,r);
merge(a,p,q,r);
}
}
int main() {
int a[n];
. . .
merge_sort(a,0,n-1);
. . .
};
163
So what is the running time of merge sort? As we argued before, to merge
the first two halves we need Θ(n) time. But before doing so, each half is merge
sorted. This means each half produces two other halves (quarters) to be merged
first. To merge these, we need Θ(n/2) + Θ(n/2) time. That’s Θ(n) time again.
We can observe that at every level of the recursion tree, we need at most Θ(n)
time. Therefore, we need a total time of Θ(n) times the number of levels. The
number of levels is guaranteed not to be more than log2 n because that’s how
many times we can divide n by 2 before reaching 1. We conclude that the
running time is Θ(n log2 n) (compare this to the previous Θ(n2 ) time).
8 8
4 4 8
2 2 2 2 8
1 1 1 1 1 1 1 1
Figure 16.1: Number of elements merged at each level of the recursion tree
starting with 8 elements
11 11
6 5 11
3 3 3 2 11
2 1 2 1 2 1 1 1 6
1 1 1 1 1 1
Figure 16.2: Number of elements merged at each level of the recursion tree
starting with 11 elements
16.5 Searching
Finally, consider the problem of searching an array for a particular element,
called the key. A simple way to search is to check every element against the key.
If found, we return its position in the array; otherwise, we return -1.
This algorithm runs in Θ(n) time because it will possibly check every element
in the array. For instance, if the key is not an element of the array, the loop
will be completely exhausted before we return.
164
The “crazy” idea: Assume the array is sorted and split it in two halves,
determine which half may potentially contain the key, and only search that
half. By now we have hopefully learned that such a crazy idea is not really
crazy because we can apply it recursively. But what if the array is not sorted?
Then we can sort it, say using merge sort above in Θ(n log2 n) time. But this
alone is more than the trivial Θ(n) time presented above. So it is not justifiable.
However, it we are performing multiple searches, we only need to sort the array
once. Then every search will be efficient. Therefore, sorting will pay off. So
let’s assume the array is sorted and see how we can proceed from here. Assume
the following strategy:
• check the key against the middle element
• if found, return that position
• if the key is smaller, then we only need to search the first half
• if the key is larger, then we only need to search the second half
We can recursively apply the same strategy on the successive halves. There-
fore, for every check that we perform, we reduce the size of the array by half.
Of course, we can do this at most log2 n times before we reach an array of size
1. We conclude that in doing so, we only perform log2 n checks and, therefore,
our algorithm runs in Θ(log2 n) time. This algorithm is called binary search.
We need to make our search function now take extra parameters to determine
which part of the array we need to recursively search (in a similar way to merge
sort).
int binary_search(int * a, int p, int r, int key) {
if (p<=r) { //at least one element
int q=(p+r)/2;
if (a[q]==key)
return q;
if (a[q]>key)
return search(a,p,q-1,key);
if (a[q]<key)
return search(a,q+1,r,key);
}
else
return -1;
}
int main() {
int a[n];
. . .
merge_sort(a,0,n-1);
. . .
int pos=binary_search(a,0,n-1,key);
. . .
};
165
This recursive search function is tail recursive because the recursive call
always comes last. Therefore, we can eliminate recursion using the standard tail
recursive implementation (change if to while, drop else, and replace recursive
call with assignment).
166
Chapter 17
Happy Feet
17.1 Introduction
Consider our Person class as a motivating example:
class Person {
string name;
public:
Person(): name(’’john’’) {}
Person(const string& s): name(s) {}
Person(const char * str): name(str) {}
string getName()const {
return name;
}
};
• How can we keep track of the number of persons alive at any time in the
program (static members)?
• How can we create a new class Student without having to reinvent a Person
(inheritance)?
167
int main() {
int counter=0;
Person p=...;
++counter;
. . .
}
However, a deeper understanding of the problem will reveal some real diffi-
culty. What if Person objects are allocated dynamically at run time?
int main() {
int counter=0;
int n;
cin>>n;
. . .
. . .
}
What about destruction? We need to also decrement the counter by 1 every
time a Person object is destructed.
int main() {
int counter=0;
int n;
cin>>n;
. . .
. . .
delete[] p;
counter=counter-n;
}
But what if the destruction is handled automatically by the compiler, e.g.
local non-dynamic allocation:
void f() {
Person p=...;
++counter; //which counter?!
. . .
168
And what about constructions that are automatically done by the compiler?
void f(Person p) {
//p is constructed locally
//using the copy constructor
//number of persons here changed
. . .
int main() {
counter=0;
Person p=...;
++counter;
f(p);
. . .
}
class Person {
int counter;
string name;
public:
. . .
};
But soon we realize that by making the counter one of the member data of
class Person, ever person object will have its own counter. This definitely will
not achieve the desired effect. Fortunately, we can tell the compiler that all
Person objects must share the same counter. This is done by declaring counter
as a static member.
169
class Person {
static int counter;
string name;
public:
. . .
};
class Person {
static int counter;
string name;
public:
Person(): name(’’john’’) {
++counter;
}
Person(const string& s): name(s) {
++counter;
}
Person(const char * str): name(str) {
++counter;
}
~Person() {
--counter;
}
int count()const {
return counter;
}
string getName()const {
return name;
}
};
int Person::counter=0;
170
Here’s an example of using the modified Person class:
void f(Person p) {
. . .
. . .
}
int main() {
Person saad=Person(’’saad’’);
cout<<saad.count(); //will see 1
Person john;
cout<<saad.count(); //will see 2
cout<<john.count(); //will see 2
f(john);
cout<<saad.count(); //will see 2
cout<<john.count(); //will see 2
}
class Person {
static int counter;
string name;
public:
. . . //equivalent to
171
While it would still be possible to invoke a static member function through
an object, a static member function can be invoked through the class itself (a
better way):
int main() {
Person saad=Person(’’saad’’);
Person john;
cout<<saad.count(); //will see 2
cout<<john.count(); //will see 2
cout<<Person::count(); //will see 2
}
Public and private rules apply to static member data and static member
functions as well. For instance, if counter were public in class Person, one
could have done the following:
int main() {
Person saad=Person(’’saad’’);
Person john;
cout<<saad.counter; //will see 2
cout<<john.counter; //will see 2
cout<<Person::counter; //will see 2
}
17.4 Inheritance
Consider the problem of creating a class Student. This of course can be easily
achieved by repeating what we have done for class Person, and adding the extra
information specific to a student. In doing so, however, we are reinventing the
Person class. Moreover, creating Student objects will not appropriately update
the Person count, after all, a student is a person.
Instead of creating a Student class from scratch, C++ provides a way to
build on top of an existing class. This is motivated by the fact that a student
IS A person. Therefore, we should reuse the Person class to create the Student
class. This form of reuse is known as inheritance. More specifically, the IS A
relation means public inheritance:
. . .
};
172
• Anywhere an object of type Person can be used, an object of type Student
can be used just as well
Here’s an example:
. . .
}
. . .
}
int main() {
Person p=...;
Student s=...;
play(p); //ok
play(s); //ok, a student is a person (true only with public inheritance)
study(s); //ok
study(p); //error, a person is not a student
}
int main() {
int n=...;
bool party=...;
Person * p;
. . .
if (party)
p=new Person[n];
else
p=new Student[n];
. . .
}
• The Student class can access only the public members (data or functions)
of Person (why?)
• The Student class must declare its own constructors, and each constructor
must also tell the compiler how to construct the Person part (or the default
constructor for Person is used)
• The Student class must declare its own member data and functions
173
class Student: public Person {
int id;
public:
Student() { //default constructor for Person used
id=0;
}
int getId()const {
return id;
}
};
int main() {
Student john;
Student saad=Student(’’saad’’,1);
cout<<Person::count(); //we will see 2
}
class Bird {
public:
void fly() {...}
. . .
};
174
class Penguin: public Bird {
. . .
};
int main() {
Penguin p=...;
p.fly(); //ok, but it shouldn’t
}
When we say that buirds can fly, we don’t really mean that all birds can
fly, only that, in general, birds have the ability to fly. Therefore, a more precise
model will recognize that there are several types of non-flying birds.
class Bird {
. . .
};
. . .
};
. . .
};
Similarly, it is a fact that penguins can sing, and it is a fact that Happy Feet
is a penguin (well, it’s controversial). If we express this in C++, we get:
. . .
};
. . .
};
Suddenly we have another problem: Happy Feet can sing! Obviously this is
not true according to the movie. A better implementation is the following:
175
class SingingPenguin: public Penguin {
public:
void sing() {...}
. . .
};
. . .
}
class Rectangle {
int w;
int h;
public:
Rectangle(int w, int h) {
this->w=w;
this->h=h;
}
void setWidth(int w) {
this->w=w;
}
void setHeight(int h) {
this->h=h;
}
176
int width()const {
return w;
}
int height()const {
return h;
}
};
. . .
};
. . .
};
It is really bad to adopt the following implementation instead (why?):
class Person: public string {
. . .
};
class Student {
Person p;
. . .
};
177
Consider creating a class Stack with two public member functions push()
and pop(). We can use the C++ vector class as a base class for Stack:
template<class T>
class Stack: public vector<T> {
public:
Stack() {}
Stack(const T& e): vecotr<T>(1,e) {}
T pop() {
if (!empty()) {
T e=(*this)[size()-1];
pop_back();
return e;
}
else
return T(); //return a default element
}
};
int main() {
Stack s;
. . .
template<class T>
class Stack {
vector<T> v;
public:
Stack() {}
Stack(const T& e): v(1,e) {}
178
T pop() {
if (!v.empty()) {
T e=v[v.size()-1];
v.pop_back();
return e;
}
else
return T(); //return a default element
}
};
17.7.2 Destruction
Destruction is done from Derived to Base. For instance, when destructing a
Student object, the Student is destructed first, then the Person.
. . .
};
. . .
};
179
17.7.4 Default assignment operator
Same description as above, simply replace copy constructor with assignment
operator. For instance, since we have not declared an assignment operator for
Student, the default assignment operator will copy the id, and then perform a
copy at the base level. Since we have not declared an assignment operator for
Person, the default assignment operator for Person will be used (it will copy the
name). Again, if we supply our own assignment operator for Student, we must
make sure to copy the base.
. . .
};
. . .
};
180