Download as pdf or txt
Download as pdf or txt
You are on page 1of 179

Computer Programming-2

Level 3

2021-2022
‫رؤيــة الكليــة‬

‫كلية رائدة تتميز بتخريج تكنولوجي صحي ذو كفاءة عالية قادر‬


‫على املنافسة يف سوق العمل حملياً وإقليمياً لالرتقاء مبستوى‬
‫اخلدمات الصحية‪.‬‬
‫رسالــة الكليـة‬

‫ختريج تكنولوجي صحي‪ ،‬ملتزم ابألخالقيات املهنية متقن‬


‫للمهارات التقنية املتخصصة الىت تؤهله للمساعدة ىف تقدمي رعاية‬
‫صحية متميزة وأمنة مبنية على االدله والرباهني ختدم خمتلف‬
‫املؤسسات الصحية‪ ،‬وأن يكون اخلريج قادر على املشاركة يف‬
‫البحث العلمى وخدمه اجملتمع ومهيأ للتعليم املستمر والعمل‬
‫اجلماعى متمتعا مبهارات االدارة من خالل تقدمي برامج متنوعة‬
‫تلتزم مبعايري اجلودة التعليمية‪.‬‬
Table of Contents

Table of Contents

Chapter 1: Basic Concepts of C++ ........................................... 1


1.1 Introduction...................................................... 1
1.2 Program Structure ............................................. 2
1.3 Variable Declaration .......................................... 7
1.4 Input/output Statements ................................... 7
1.5 Flow Control ..................................................... 9
1.6 Iterative Statements (Loops) ........................... 12
1.7 Other Statements ............................................ 15
1.8 Summary ........................................................ 16

Chapter 2: Functions............................................................. 17
2.1 What is a Function ........................................ 17
2.2 Function Declaration ..................................... 17
2.3 Function Calling ............................................ 19
2.4 Scope of Variables ......................................... 25
2.5 Functions with no Return Types ..................... 26
2.6 Passing Arguments to Functions...................... 29
2.7 Parameters Default Values ............................. 32
2.8 Overloaded Functions .................................... 34
2.9 Prototyping Functions .................................... 36
2.10 Recursion ..................................................... 40

i
Table of Contents

Chapter 3: Structures and Arrays ......................................... 43


3.1 Structures ..................................................... 43
3.1.1 Declaring Structures ............................... 44
3.1.2 Assignments with Structures ................... 45
3.1.3 Structures as Function Arguments ........... 48
3.1.4 Complex Structures ............................... 50
3.2 Arrays ........................................................... 52
3.2.1 Array Declaration ................................... 53
3.2.2 Array Initialization .................................. 54
3.2.3 Accessing Array Elements ....................... 55
3.2.4 Multi-dimensional Arrays ........................ 58
3.2.5 Arrays as Function Arguments ................ 61
3.2.6 Arrays of Structures ............................... 63
3.2.7 Arrays of Characters ............................... 65
3.3 Summary ...................................................... 70

Chapter 4: Pointers and Dynamic Memory ........................... 71


4.1 Introduction .................................................. 71
4.2 What are Pointers? ........................................ 72
4.3 Declaring Pointers .......................................... 73
4.4 Operations with Pointers ................................. 75
4.5 Pointers to Structures .................................... 92
4.6 Pointers to Arrays .......................................... 94
4.7 Passing Structures and Arrays ........................ 97
4.8 Dynamic Memory .......................................... 101
4.9 Summary ..................................................... 110

ii
Table of Contents

CHAPTER 5: lINKED LISTS .......................................... 111


5.1 What is a Linked List ..................................... 111
5.2 Defining a Node and Start Pointer ................. 113
5.3 Creating a Node ........................................... 114
5.4 Accessing a Node ......................................... 115
5.5 Adding a Node to a List ................................. 116
5.6 Display the List of Nodes .............................. 118
5.7 Deleting a Node from the List ....................... 120
5.8 Traversing the List ....................................... 124

CHAPTER 6: STACKS and QUEUES .............................. 131


6.1 Stack Data Structures ..................................... 131
6.1.1 Definition .. ............................................ 132
6.1.2 Primitive Operations ............................... 136
6.1.3 Representing Stacks ................................ 139
6.1.4 Representing Stack Operations ................ 144
6.1.4.1 Implementing empty Operation .................... 144
6.1.4.2 Implementing pop Operation........................ 149
6.1.4.3 Implementing Push Operation ...................... 151
6.1.4.4 Implementing stacktop Operation ................. 154
6.2 Queues Data Structures ................................ 156
6.2.1 Definition ............................................... 156
6.2.2 Primitive Operations ............................... 157
6.2.3 Queues Representation ........................... 159
6.2.2 Implementation Queue Operations ........... 165
6.3 Drawbacks of using sequential storage........... 170

REFERENCES ................................................................. 171

iii
Table of Contents

iv
Chapter 1: Basic Concepts of C++. _____________________________________

Chapter 1

Basic Concepts of C++

In this chapter, the primary interest is to introduce the basics


of the C++ programming language. It first presents the C++
program structure, variables, data types, constants and
operators. It then presents some of the C++ program
statements like input/output statements, assignment
statement, flow control, iterative statements and other
statements.

1.1 Introduction

A data structure is a way of storing and organizing data in


a computer so that it can be used efficiently, while an
algorithm is a sequence of finite instructions or operations
often used for calculation and data processing. Organizing
___________________________________________________________
1
Chapter 1: Basic Concepts of C++. _____________________________________

the data for processing is an essential step in the


development of a computer program. For many applications,
the choice of the proper data structure is the only major
decision involved in the implementation. Once the choice has
been made, the necessary algorithms are simple. For the
same data, some data structures require more or less
memory space than others. For the same operations on the
data, some data structures leads to more or less efficient
algorithms than others. Hence, the choice of algorithms and
data structures are closely intertwined, and we continually
seek ways to save time by making the choice properly.

This chapter presents the structures, arrays, and pointers.


These classical data structures differ from each other in how
their memory is allocated, how they are indexed, and how
particular values can be looked up within them. They form
the basis for all the data structures considered in this course.
We also discuss the operations to be performed on data
structure. We consider various primitive operations to
develop sophisticated algorithms for difficult problems.

1.2 C++ Program Structure

The general structure of the C++ program consists of two


Parts: Header Part and Main Block.

___________________________________________________________
2
Chapter 1: Basic Concepts of C++. _____________________________________

Header Part:
 Directives (Ex: #include <filename>)
 Prototypes (related to functions if they coded after they called)
 Global Declaration (Global variables to all functions )

Block:
 The block is a function.
 Any C++ program may have many functions but at least
one function called main() in the form:

void main()
{
Variables Declaration;
STATEMENTS;
return;
}

The following is our first program:

/* my first program in C++

#include <iostream>
using namespace std;
int main ()
{
cout << "My First Program"; // print My First Program
return 0;
}

My First Program

___________________________________________________________
3
Chapter 1: Basic Concepts of C++. _____________________________________

The previous program is very simple but it contains the


fundamental components of every C++ program. Let us look
at the program code, line by line:

 #include <iostream>

This is a directive line tells the preprocessor to include a


standard header file called iostream in the current
program code. This is because it contains the declarations
of the basic input-output library in C++ and its
functionality is going to be used later in the program.
Generally, lines beginning with a hash sign (#) are
directives for the preprocessor.

 using namespace std;

All the elements of the standard C++ library are declared


within what is called a namespace std. So, to access its
functionality, it must be declared with this expression.
Note that, this line depends on the C++ compiler and it is
frequent in C++ programs that use the standard library.

 int main ()

This line corresponds to the beginning of the main


function. It is the point by which all C++ programs start
their execution, independently of its location within the
source code. It does not matter whether there are other

___________________________________________________________
4
Chapter 1: Basic Concepts of C++. _____________________________________

functions with other names defined before of after it. The


instructions contained within this function will be the first
ones to be executed in any program. For that reason, it is
essential that all C++ programs have a main function.

The word main is followed by a pair of parentheses ()


because it is a function. Optionally, these parentheses
may enclose a list of parameters within them. After these
parentheses, the body of the main function is enclosed in
braces ({}). What is contained within these braces is
what the function does when it is executed.

 cout << "First Program";

This line is a C++ statement. It performs the only action


that generates a visible effect in the above program. It
inserts a sequence of characters (First Program) into
the standard output (screen). Generally, cout represents
the standard output, and it is declared in the header file
iostream within the std namespace, so that's why we
needed to include that specific file in the program header.

Notice that, a statement (simple or compound) is one that


can actually produce some effect. Indeed, a semicolon (;)
must be included at the end of all statements in C++
programs as it marks the end of the statement. One of
the most common syntax errors is to forget to include a
semicolon after a statement.
___________________________________________________________
5
Chapter 1: Basic Concepts of C++. _____________________________________

 return 0;

The return statement causes the main function to finish.


Return may be followed by a return code (0). A return
code 0 is generally interpreted as the program worked as
expected without any errors during its execution. This is
the most usual way to end a C++ program.

 // and /*……*/

These are comment characters used to insert notes or


descriptions within the program. Generally, C++ supports
two ways to insert comments:

// line comment
/* block comment */

The first one, known as line comment, discards


everything from where the pair of slash signs (//) is
found up to the end of the same line. The second one,
known as block comment, discards everything between
the characters /* and the first appearance of the */, with
the possibility of including more than one line. The two
types of comment are illustrated in the above program.

___________________________________________________________
6
Chapter 1: Basic Concepts of C++. _____________________________________

1.3 Variables Declaration

The general syntax to declare a variable is to write the


desired data type followed by valid identifier:

Data_type Variable_name;

A variable is a name consisting of a sequence of one or


more letters, digits or underline characters (_), and begins
with a letter, indeed, cannot match any keyword of the C++.

The variable is used to reserve a portion of memory to store


a determined value. The size of the reserved memory portion
depends on the data type.

For example:

int a;

float number;

1.4 Input/Output Statements

The standard C++ library includes the header file


iostream.h, where the standard input and output stream
objects are declared.

___________________________________________________________
7
Chapter 1: Basic Concepts of C++. _____________________________________

1.4.1 Standard Output (cout)

The standard output is the screen, and the C++ stream


object defined cout to access it. The cout is used in
conjunction with the insertion operator, <<, which is written
as (two "less than" signs).

cout << "Output sentence"; // prints Output sentence


cout << 120; // prints number 120
cout << x; // prints the content of x on screen

1.4.2 Standard Input (cin)

The standard input device is the keyboard. Handling the


standard input is done by applying the extraction operator
(>>) on the cin stream followed by the variable that will
store the data. For example:

int age;
cin >> age;

The second statement waits for an input from the keyboard


in order to store it in location in memory denoted by the
variable age.

___________________________________________________________
8
Chapter 1: Basic Concepts of C++. _____________________________________

1.5 Flow Control

Generally, there are four kinds of decision making


statements: if statement, if….else statement, elseif
statement and switch statement.

1.5.1 If statement

The general syntax of the if statement is simply written as:

(A) for one statement (B) for n statements


if (condition) if (condition)
statement; {
statement 1;
……
statement n;
}

The if keyword is used to execute a statement or block of


statements only if a condition is True. Braces { } are used to
specify a block of statements to be executed in case that the
condition is true. For example, the following code prints the
message “x is 100” only if the value stored in the variable x
equals to 100:

if (x == 100) if (x == 100)
cout << "x is 100"; {
cout << "x is ";
cout << x;
}

___________________________________________________________
9
Chapter 1: Basic Concepts of C++. _____________________________________

1.5.2 If … else… statement

The general syntax of if … else statement is:

(A) for one statement (B) for n statements

if (condition) if (condition)
statement 1; {
else statement 1;
statement 2; ……
statement n;
}
else
{
statement n+1;
……
statement m;
}

For example:

if (x == 100)
cout << "x is 100";
else
cout << "x is not 100";

1.5.3 Nested If statement

The if…else can be concatenated with additional if for the


intention of verifying multiple conditions or selections. For
example:

___________________________________________________________
10
Chapter 1: Basic Concepts of C++. _____________________________________

if (condition_1)
if (condition_2)
statement_1;
else
statement_1;

1.5.4 Switch…case… Statement

The general syntax is:

switch (expression)
{
case 1:
group of statements 1;
break;

case 2:
group of statements 2;
break;
……….
default:
default group of statements;
}

For example:

switch (x)
{
case 1: cout << "x is 1";
break;
case 2: cout << "x is 2";
break;
case 3: cout << "x is 3";
break;

___________________________________________________________
11
Chapter 1: Basic Concepts of C++. _____________________________________

default:
cout << "x is not 1, 2 nor 3";
}

1.6 Iterative Statements (loops)

Iterative statements or loops are used to repeat a statement


or a block of statements a predefined number of times or
while a condition is true. In C++, there are three types of
loops: for loop, while loop, and do…while loop.

1.6.1 For loop

The general layout or syntax of the ‘for loop’ is that:

for (initialization; repeat_condition; updating)

statement or block of statements;

Here is an example of countdown using for loop:

#include <iostream>
using namespace std;
int main ()
{
int n;
for (n=10; n>0; n--)
{
cout << n << ", ";
}
cout << "FIRE!";
___________________________________________________________
12
Chapter 1: Basic Concepts of C++. _____________________________________

return 0;
}
10, 9, 8, 7, 6, 5, 4, 3, 2, 1, FIRE!

1.6.2 while loop

The basic structure of the while loop is as follows:

while (condition)

statement or block of statements;

For example, we are going to make a program to count down


using a while-loop:

#include <iostream.h>

int main ()
{
int n;
cout << "Enter the starting number>";
cin >> n;

while (n>0)
{
cout << n << ", ";
--n;
}
cout << "FIRE!";
return 0;
}

Enter the starting number > 8


8, 7, 6, 5, 4, 3, 2, 1, FIRE!

___________________________________________________________
13
Chapter 1: Basic Concepts of C++. _____________________________________

1.6.3 do…while loop

The syntax is:

do
{
statement or block of statements;
}
while (condition);

For example, the following example program echoes any


number you enter until you enter 0.

#include <iostream>
using namespace std;

int main ()
{
unsigned long n;
do
{
cout <<"Enter number (0 to end):";
cin >> n;
cout << "You entered:" << n <<"\n";
}
while (n != 0);
return 0;
}

Enter number (0 to end): 12345


You entered: 12345

Enter number (0 to end): 160277


You entered: 160277

Enter number (0 to end): 0


You entered: 0

___________________________________________________________
14
Chapter 1: Basic Concepts of C++. _____________________________________

1.7 Other Statements

1.7.1 break statement

Using break we can leave a loop even if the condition for its
end is not fulfilled. It can be used to end an infinite loop, or
to force it to end before its natural end. For example, the
following program stops the count down before its natural
end:

// break loop example

#include <iostream>
using namespace std;

int main ()
{
int n;
for (n=10; n>0; n--)
{
cout << n << ", ";
if (n==3)
{
cout << "countdown aborted!";
break;
}
}
return 0;
}
10, 9, 8, 7, 6, 5, 4, 3, countdown aborted!

___________________________________________________________
15
Chapter 1: Basic Concepts of C++. _____________________________________

1.7.2 continue statement

The continue statement causes the program to skip the rest


of the loop in the current iteration as if the end of the
statement block had been reached, causing it to jump to the
start of the following iteration. For example, the following
program skips the number 5 in the countdown:

// continue loop example

#include <iostream>
using namespace std;

int main ()
{
for (int n=10; n>0; n--)
{
if (n==5) continue;
cout << n << ", ";
}
cout << "FIRE!";
return 0;
}

10, 9, 8, 7, 6, 4, 3, 2, 1, FIRE!

1.8 Summary

In this chapter, we have looked at the basics of the C++


programming language.
___________________________________________________________
16
Chapter 2

Functions

Now, you should have learned about variables, loops, and


conditional statements, it is time to learn about functions. Using
functions, the programs can be structured in a more modular way;
accessing all the potential that structured programming in C++
can offer us.

2.1 What is a Functions

A function is a group of statements that is executed when it is


called from some point of the program.

2.2 Function Declaration

Functions can be declared similar to variables, but they enclose


their arguments in parenthesis (even if there are no arguments,
the parenthesis must be specified). The general syntax is that:
Chapter 2. Functions. ____________________________________________

type Function-name (argument1, argument2, ...)


{
statement 1;
statement 1;
………..
return value;
}

where:

 Type: is the type of data returned by the function (optional).


 Function_name: is an identifier representing the function
name by which it will be possible to call the function.
 Arguments: are a list of parameters or identifiers (as many
as needed). Each argument consists of a data type followed
by an identifier, like in a variable declaration (e.g., int x) and
which acts within the function as a regular local variable like
any other variable. These identifiers allow passing
parameters to the function when it is called. The different
parameters are separated by commas.

For example:
int Fac(int num); /* Declaration of Fac with one argument*/

int bar(); /* Declaration of bar with no arguments*/

void sum(int x, int y); /* Declaration of sum with two arguments*/

 Statement: is the function's body. It can be a single


statement or a block of statements surrounded by curly
brackets {}.

___________________________________________________________
18
Chapter 2. Functions. ____________________________________________

To actually define a function, just add its body as follows:

int sum(int num)


{
int i, ret;
ret = 0;
for (i = 0; i < num; i ++)
ret = ret + i;
return (ret); /* return function's value */
} /* sum */

2.3 Function Calling

A function code can be called within a program just by its name


followed by the necessary parameters enclosing in parenthesis.
The following table shows the first function example:

// function example

#include <iostream>
using namespace std;

int addition (int a, int b)


{
int r;
r=a+b;
return (r);
}

int main ()
{
int z;

___________________________________________________________
19
Chapter 2. Functions. ____________________________________________

z = addition (5,3);
cout << "The result is " << z;
return 0;
}

The result is 8

In order to examine this code, first of all remember something


said at the beginning of this book: a C++ program always begins
its execution by the main function.

We can see how the main function begins by declaring the


variable z of type int. Right after that, we see a call to a function
called addition. Paying attention we will be able to see the
similarity between the structure of the call to the function and the
declaration of the function itself. Some code lines:

The parameters and arguments have a clear correspondence.


Within the main function we call to the function addition and
passing two values: 5 and 3, that correspond to the arguments
int a and int b that declared for function addition.

At the point where the function is called from the function main,
the execution control is lost by main and passed to the function
addition. The values passed in the call (5 and 3) are copied to
the local variables int a and int b within the function.

___________________________________________________________
20
Chapter 2. Functions. ____________________________________________

In the above example, the function addition declares another


local variable (int r), and the expression r=a+b assigns the result
of a plus b to r. Because the actual parameters passed to a and b
are 5 and 3 respectively, the result is 8.

The following line of code:

return (r);

finalizes the execution of the function addition, and returns the


execution control back to the function that called it in the first
place (in this case, main). At this moment the program follows its
regular execution from the same point at which it was interrupted
by the call to addition. As the return statement in the function
addition specified a value, the function returns the content of
variable r (return (r)), which at that moment had a value of 8.
This value becomes the value of evaluating the function call.

The value returned by a function when it is evaluated is the value


given to the function call itself. So, the variable z will be set to the
value returned by addition (5, 3), that is 8. In other words, you
can imagine that the call to a function (addition (5, 3)) is literally
replaced by the value it returns (8).

Finally, the following line of code:


___________________________________________________________
21
Chapter 2. Functions. ____________________________________________

cout << "The result is " << z;

causes the printing of the result on the screen.

Another example about functions is shown in the following table:

// function example

#include <iostream>
using namespace std;

int subtraction (int a, int b)


{
int r;
r=a-b;
return (r);
}

int main ()
{
int x=5, y=3, z;
z = subtraction (7,2);
cout << "The first result is" << z << '\n';
cout << "The second result is "
<< subtraction (7,2) << '\n';
cout << "The third result is "
<< subtraction (x,y) << '\n';
z= 4 + subtraction (x,y);
cout << "The fourth result is" <<z<< '\n';
return 0;
}

The first result is 5


The second result is 5
The third result is 2
The fourth result is 6

___________________________________________________________
22
Chapter 2. Functions. ____________________________________________

In this example, we have created a function called subtraction.


The only thing that this function does is to subtract both passed
parameters and to return the result. If we examine the function
main we will see that we have made several calls to the function
subtraction. We have used some different calling methods so as
to see other ways or moments when a function can be called.

In order to understand well these calls, you must consider once


again that a call to a function could be replaced by the value that
the function call itself is going to return. For example, the first
case is the same pattern that we have used in previous example:

z = subtraction (7,2);
cout << "The first result is " << z;

If we replace the function call by the value that it returns (i.e., 5),
we would have:

z = 5;
cout << "The first result is " << z;

Also, the following call

cout <<"The second result is"<<subtraction (7,2);

has the same result as the previous call, but in this case we made
the call to subtraction directly as an insertion parameter for the

___________________________________________________________
23
Chapter 2. Functions. ____________________________________________

cout. Simply consider that the result is the same as if we had


written:

cout << "The second result is " << 5;

since 5 is the value returned by subtraction (7,2).

In the case of the following call:

cout <<"The third result is" <<subtraction (x,y);

The only new thing that we introduced is that the parameters of


the function subtraction are variables instead of constants. This
is perfectly valid. In this case the values passed to the function
subtraction are the values of x and y, that are 5 and 3
respectively, giving 2 as result.

The fourth case is more of the same. Simply note that instead of:

z = 4 + subtraction (x,y);

we could have written:

z = subtraction (x,y) + 4;

with exactly the same result. I have switched places so you can
see that the semicolon sign (;) goes at the end of the whole
statement. It does not necessarily have to go right after the

___________________________________________________________
24
Chapter 2. Functions. ____________________________________________

function call. The explanation might be once again that you


imagine that a function can be replaced by its returned value:

z = 4 + 2;
z = 2 + 4;

2.4 Scope of Variables

The scope of variables declared within a function or any other


inner block is only identified to their own function or their own
block and cannot be used outside of them. For example, in the
previous example it would have been impossible to use the
variables a, b or r directly in function main since they were local
variables to the function subtraction. Also, it would have been
impossible to use the variable z directly within the function
subtraction, since this was a local variable to the function main.
Therefore, the scope of the local variables is limited to the same
block level in which they are declared.

Also, we have the possibility to declare global variables. These are


visible from any point of the code, inside and outside all functions
of the same program code. In order to declare global variables
you simply have to declare the variable outside any function or
block; that means, directly in the body of the program header.

___________________________________________________________
25
Chapter 2. Functions. ____________________________________________

2.5 Functions with no Types

If you remember the syntax of a function declaration:

type Function-name (argument1, argument2, ...)


{
statement 1;
statement 1;
………..
return value;
}

___________________________________________________________
26
Chapter 2. Functions. ____________________________________________

you will see that the function declaration begins with a type, that
is the type of the data that will be returned by the function with
the return statement. But what if we want to return no value?

Imagine that we want to make a function just to show a message


on the screen. We do not need it to return any value. In this case,
we should use the void type specifier for the function. This is a
special specifier that indicates absence of returned data type.

// void function example

#include <iostream>
using namespace std;

void printmessage ()
{
cout << "I'm a function!";
}

int main ()
{
printmessage ();
return 0;
}

I'm a function!

The void can also be used in the function's parameter list to


explicitly specify that we want the function to take no actual
parameters when it is called. For example, function
printmessage could have been declared as follows:

___________________________________________________________
27
Chapter 2. Functions. ____________________________________________

void printmessage (void)


{
cout << "I'm a function!";
}

Although it is optionally to specify the void in the parameter list,


in C++, a parameter list can simply be left blank if we want a
function with no parameters.

What you must always aware is that the format for calling a
function includes specifying its name and enclosing its parameters
between parentheses. The non-existence of parameters does not
exempt us from the obligation to write the parentheses. For that
reason the call to printmessage is:

printmessage ();

The parentheses clearly indicate that this is a call to a function


and not a name of a variable or some other C++ statement. The
following call would have been incorrect:

printmessage;

___________________________________________________________
28
Chapter 2. Functions. ____________________________________________

2.6 Passing Arguments to Functions

Until now, in all the functions we have seen, the arguments


passed to the functions have been passed by value. This means
that when calling a function with parameters, what we have
passed to the function were copies of their values but never the
variables themselves. For example, suppose that we called the
first function addition using the following code:

int x=5, y=3, z;


z = addition (x, y);

What we did in this case was that calling the function addition
and passing the values of x and y, i.e. 5 and 3 respectively, but
not the variables x and y themselves.

This way, when the function addition is called, the value of its
local variables a and b become 5 and 3 respectively. But, any
modification to either a or b within the function addition will not
have any effect in the values of x and y outside it. This is because
variables x and y were not themselves passed to the function, but
only copies of their values at the moment the function was called.

___________________________________________________________
29
Chapter 2. Functions. ____________________________________________

There might be some cases where you need to manipulate the


value of an external variable from the inside of a function. For that
purpose we can use arguments passed by reference, as in the
function duplicate of the following example:

// passing parameters by reference

#include <iostream>
using namespace std;

void duplicate (int& a, int& b, int& c)


{
a*=2;
b*=2;
c*=2;
}

int main ()
{
int x=1, y=3, z=7;
duplicate (x, y, z);
cout << "x=" << x << ", y=" << y << ", z=" << z;
return 0;
}

x=2, y=6, z=14

The first thing that should be noted here is that, in the declaration
of the function duplicate, the type of each parameter was
followed by an ampersand sign (&). This ampersand is what
specifies that their corresponding arguments are to be passed by
reference instead of by value as usual.

When a variable is passed by reference, we are not passing a copy


of its value, but we are somehow passing the variable itself to the
___________________________________________________________
30
Chapter 2. Functions. ____________________________________________

function and any modification to the local variables will have an


effect in their counterpart variables passed as arguments in the
call to the function. For example:

Here, we associate a, b and c to the arguments passed by the


function call (i.e., x, y and z) and any change on a, b and c within
the function itself will affect the value of x, y, and z outside the
function. Any change on a will affect x, b will affect y, and c will
affect z. This illustrates why the program's output, that shows the
values stored in x, y and z after the call to duplicate, shows the
values of all the three variables of main are doubled.

If the function declaration:

void duplicate (int& a, int& b, int& c)

is changed as:

void duplicate (int a, int b, int c)

i.e., without the ampersand signs (&), we would have not passed
the variables by reference, but a copy of their values instead, and
therefore, the output on screen of the program would have been
the values of x, y and z without having been modified.

___________________________________________________________
31
Chapter 2. Functions. ____________________________________________

Passing by reference is also an effective way to allow a function to


return more than one value. For example, here is a function that
returns the previous and next numbers of the first parameter
passed.

// more than one returning value

#include <iostream>
using namespace std;

void prevnext (int x, int& prev, int& next)


{
prev = x-1;
next = x+1;
}

int main ()
{
int x=100, y, z;
prevnext (x, y, z);
cout <<"Previous=" << y <<", Next=" <<z;
return 0;
}

Previous=99, Next=101

2.7 Parameters Default Values

When declaring a function we can specify a default value for each


parameter. This value will be used if the corresponding argument
is left blank when calling the function. To do that, we simply have
to use the assignation operator to assign a value to the argument

___________________________________________________________
32
Chapter 2. Functions. ____________________________________________

in the function declaration. If a value is not passed to that


parameter when the function is called, the default value is used,
but if a value is specified, the default value is ignored and the
passed value is used instead. For example:

// default values in functions

#include <iostream>
using namespace std;

int divide (int a, int b=2)


{
int r;
r=a/b;
return (r);
}

int main ()
{
cout << divide (12);
cout << endl;
cout << divide (20,4);
return 0;
}
6
5

As we can see in the body of the program, there are two calls to
function divide. In the first one:

divide (12)

We have only specified one argument, but the function divide


allows two Arguments. So the function divide has assumed that
the second parameter is 2 since that is what we have specified to
___________________________________________________________
33
Chapter 2. Functions. ____________________________________________

happen if this parameter was not passed (notice the function


declaration, which finishes with int b=2, not just int b). Therefore
the result of this function call is 6 (12/2).

In the second call:

divide (20,4)

There are two parameters, so the default value for b (int b=2) is
ignored and b takes the value passed as argument, that is 4,
making the result returned equal to 5 (20/4).

2.8 Overloaded Functions

In C++, two different functions can have the same name if their
parameter types or number are different. This means that you can
give the same name to more than one function if they have either
a different number of parameters or different types in their
parameters.

In the following example program, we have defined two functions


with the same name, operate, but one of them accepts two
parameters of type int while the other one accepts them of type
float. The compiler knows which one to call in each case by
examining the passed arguments types when the function is
called. If it is called with two int as its arguments it calls to the
___________________________________________________________
34
Chapter 2. Functions. ____________________________________________

function that has two int parameters in its prototype and if it is


called with two float it will call to the one which has two float
parameters in its prototype.

// overloaded function

#include <iostream>
using namespace std;

int operate (int a, int b)


{
return (a*b);
}

float operate (float a, float b)


{
return (a/b);
}

int main ()
{
int x=5, y=2;
float n=5.0,m=2.0;
cout << operate (x,y);
cout << "\n";
cout << operate (n,m);
cout << "\n";
return 0;
}
10
2.5

In the above example, the two arguments passed by the first call
are of type int, therefore, the function with the first prototype is
called. This function returns the result of multiplying both
parameters. While the second call passes two arguments of type
float, so the function with the second prototype is called. This
___________________________________________________________
35
Chapter 2. Functions. ____________________________________________

one has a different behavior: it divides one parameter by the


other. So the behavior of a call to operate depends on the type of
the arguments passed because the function has been overloaded.

For simplicity we have included the same code within both


functions, but this is not compulsory. You can make two functions
with the same name but with completely different behaviors.
Notice that, a function cannot be overloaded only by its return
type. At least one of its parameters must have a different type.

2.9 Prototyping Functions

Until now, we have defined all of the functions before the first
appearance of calls to them in the source code. These calls were
generally in the function main which we have always left at the
end of the source code. But if you try to repeat some of the
examples of functions described so far, but placing the function
main before any of the other functions that were called from
within it, you will most likely obtain compiling errors. The reason is
that to be able to call a function, it must be declared in some
earlier point of the code, like we have done in all the above
examples.

But there is an alternative way to avoid writing the whole code of


a function before it can be called in main or in some other
function. This can be achieved by just declaring a prototype of the
___________________________________________________________
36
Chapter 2. Functions. ____________________________________________

function before it is used, instead of the entire definition. This


declaration is shorter than the entire definition, but enough
significant for the compiler to determine its return type and the
types of its parameters.

The general syntax for declaring a function prototype is that:

type function_name (argument_type1, argument_type2, ..);

It is identical to a function definition, except that:

 The prototype does not include the body of the function itself
(i.e., the function statements that are enclosed in braces { },
as in the normal definitions).
 The prototype declaration ends with a semicolon (;).
 The parameter enumeration does not need to include the
identifiers, but only the data type. The inclusion of a name
for each parameter as in the function definition is optional in
the prototype declaration. For example, we can declare a
function called protofunction with two int parameters with
any of the following declarations:

int protofunction (int first, int second);


int protofunction (int, int);

Anyway, including a name for each variable makes the prototype


more legible.

// declaring functions prototypes


#include <iostream>
___________________________________________________________
37
Chapter 2. Functions. ____________________________________________

using namespace std;

void odd (int a);


void even (int a);

int main ()
{
int i;
do
{
cout << "Type a number:(0 to exit)";
cin >> i;
odd (i);
} while (i!=0);
return 0;
}

void odd (int a)


{
if ((a%2)!=0)
cout << "Number is odd.\n";
else
even (a);
}

void even (int a)


{
if ((a%2)==0)
cout << "Number is even.\n";
else
odd (a);
}

Type a number (0 to exit): 9


Number is odd.

Type a number (0 to exit): 6


Number is even.

Type a number (0 to exit): 1030


Number is even.

___________________________________________________________
38
Chapter 2. Functions. ____________________________________________

Type a number (0 to exit): 0


Number is even.

This example is not an efficiency example. I am sure that at this


point you can already make a program with the same result, but
using only half of the above code lines that have been used in this
example. Anyway this example illustrates how prototyping works.
Moreover, in this concrete example the prototyping of at least one
of the two functions is necessary in order to compile the code
without errors.

The first things that we see are the declaration of functions odd
and even:

void odd (int a);


void even (int a);

This allows the functions to be used before they are defined, for
example, in main, which now is located where some people find it
to be a more logical place for the start of a program: the
beginning of the source code.

Anyway, the reason why this program needs at least one of the
two functions to be declared before it is defined is that in odd
there is a call to even and in even there is a call to odd. If none of
the two functions had been previously declared, a compilation
error would happen, since either odd would not be visible from
even (because it has still not been declared), or even would not
be visible from odd (for the same reason).
___________________________________________________________
39
Chapter 2. Functions. ____________________________________________

Having the prototype of all functions together in the same place


within the source code is found practical by some programmers,
and this can be easily achieved by declaring all functions
prototypes at the beginning of a program.

2.10 Recursion

Recursion is a property that functions have to be called by


themselves. It is useful for many tasks, like sorting or calculating
the factorial of numbers. For example, to obtain the factorial of a
number (n!), the mathematical formula would be:

n! = n * (n-1) * (n-2) * (n-3) ... * 1

more concretely, 5! (factorial of 5) would be:

5! = 5 * 4 * 3 * 2 * 1 = 120

and a recursive function to calculate this in C++ could be:

// factorial calculator

#include <iostream>
using namespace std;

long factorial (long a)


{
if (a > 1)
return (a * factorial (a-1));
else

___________________________________________________________
40
Chapter 2. Functions. ____________________________________________

return (1);
}

int main ()
{
long number;
cout << "Please type a number: ";
cin >> number;
cout << number << "! = " << factorial (number);
return 0;
}

Please type a number: 9


9! = 362880

Notice how in the function factorial we included a call to itself, but


only if the argument passed was greater than 1, since otherwise
the function would perform an infinite recursive loop in which
once it arrived to 0 it would continue multiplying by all the
negative numbers (probably provoking a stack overflow error on
runtime error).

This function has a limitation because of the data type we used in


its design (long) for more simplicity. The results given will not be
valid for values much greater than 10! or 15!, depending on the
system you compile it.

___________________________________________________________
41
Chapter 3

Structures and Arrays

This chapter briefly introduces the basics of structures and arrays.


It first shows how a list of variables of different data types may be
grouped as a structure/record and how a list of variables of the
same data type may be grouped as an array. Then, it briefly
introduces the multi dimensional arrays. Finally, it shows how the
C++ manipulates strings, which are simply arrays of characters
always including a special sentinel character.

3.1 Structures

A structure is a container for a few different data, possibly with


different types that are associated with one another. In other
words, a structure is a way of grouping and storing many data
elements of different types under the same name. These data
elements, known as members, can have different types and
lengths. Such a data structure is useful for building databases.
Chapter 3. Structures and Arrays. ____________________________________

3.1.1 Declaring Structures

A structure is a group of data elements grouped together under


one name. The general syntax to actually create a single data
structure for using inside a program is that:

struct struct_name
{
member_type1 member_name1;
member_type2 member_name2;
member_type3 member_name3;
… …
… ...
};

Where, struct is a reserved word defining a structure data type


and struct_name is the name of the entire data type of
structure, it can any of valid identifier. Within braces { } there is a
list of data members defining different fields inside the
structure/record, each one is specified with a type and a valid
identifier as its name. Finally, the declaration is ended with a
semicolon (;). For example,

struct Example
{
int x;
char y;
};

The above example creates a structure data type (record),


denoted by Example, having two fields: the first field, denoted by
___________________________________________________________
44
Chapter 3. Structures and Arrays. ____________________________________

x, has a type integer while the second field, denoted by y, has a


type character. The variables (x and y) that held within the
structure are defined between braces and declared by different
data types.

3.1.2 Assignment with Structures

The first thing we have to know is that a data structure creates a


new type. In other words, once a data structure is declared, a
new type with the identifier specified as structure_name is
created and can be used in the rest of the program as if it was
any other type (i.e., the standard data types: int, char,…etc). So,
to use the defined structure in a program, a variable of this data
type must be declared. For example:

struct product
{
int model;
int weight;
float price;
} ;

product apple;
product banana, melon;

We have first declared a structure data type called product with


three members: model, weight and price, each of a different
fundamental data type. We have then used the name of the

___________________________________________________________
45
Chapter 3. Structures and Arrays. ____________________________________

structure type (product) to declare three objects of that type:


apple, banana and melon as we would have done with any
fundamental data type. This is valid definition because, once
declared, product has become a new valid data type name like
the fundamental ones int, char or short. Hence, from that point
on, we are able to declare objects (variables) of this compound
new type, like we have done with apple, banana and melon.

Note that, right at the end of the struct declaration, and before
the ending semicolon, we can use an optional field to directly
declare objects of the structure type. For example, we can also
declare the structure objects apple, banana and melon directly
at the moment we define the structure data type, as follows:

struct product
{
int model;
int weight;
float price;
} apple, banana, melon;

It is important to clearly differentiate between what is the


structure type name, and what is an object (variable) that has this
structure type. We can instantiate many objects (i.e. variables, like
apple, banana and melon) from a single structure type (product).

Once objects are declared by determined structure type (like,


apple, banana and melon), we can operate directly with their
members.

___________________________________________________________
46
Chapter 3. Structures and Arrays. ____________________________________

To access a field/member inside an object of structure type, use a


dot (.) inserted between the object name and the member name.
For example, we could operate with any of the object elements as
if they were standard variables of their respective types:

apple.model = 1044;
apple.weight = 100;
apple.price = 50;

cin >> banana.model;


cin >> banana.weight;
cin >> banana.price;

cout << melon.weight;


cout << melon.price;

Each one of these has the data type corresponding to the member
they refer to: apple.model, banana.model, melon.model
apple.weight, banana.weight and melon.weight are of type int,
while apple.price, banana.price and melon.price are of type float.

Generally, the dot (.) operator is used to access different variables


inside a record. Simple example program:

// simple example about structures


#include <iostream>
using namespace std;

struct database
{
int id_number;
int age;
float salary;
};
___________________________________________________________
47
Chapter 3. Structures and Arrays. ____________________________________

int main()
{
database employee; //employee is a structure
employee.id_number=10;
employee.age=28;
employee.salary=500.75;
cout << employee.id_number << " "
<< employee.age << " "
<< employee.salary<<endl;
return 0;
}

The struct database creates a structure data type naming


database and having three fields defined by three different
variables: id_number, age and salary. This database type can
be used like other standard data types, i.e., int, char, float. The
statement database employee defines the variable employee as
a record with this database type. To access content of a field, call
the field name with the 'employee.' in front of it, as described in
the above program.

3.1.3 Structures as function Arguments

A structure can be used as an arguments of a function and can be


used in the same way as fundamental types. Let's see a real
example:

___________________________________________________________
48
Chapter 3. Structures and Arrays. ____________________________________

// example about argument structures

#include <iostream>
#include <string>
#include <sstream>
using namespace std;

struct movies_t
{
string title;
int year;
} mine, yours;

void printmovie (movies_t movie);

int main ()
{
string mystr;

mine.title = "2001 A Space Odyssey";


mine.year = 1968;

cout << "Enter title: ";


getline (cin, yours.title);
cout << "Enter year: ";
getline (cin, mystr);
stringstream(mystr) >> yours.year;

cout << "My favorite movie is:\n ";


printmovie (mine);
cout << "And yours is:\n ";
printmovie (yours);
return 0;
}

void printmovie (movies_t movie)


{
cout << movie.title;
cout << " (" << movie.year << ")\n";
}

___________________________________________________________
49
Chapter 3. Structures and Arrays. ____________________________________

Enter title: Alien


Enter year: 1979

My favorite movie is:


2001 A Space Odyssey (1968)
And yours is:
Alien (1979)

The example first shows how we can use the members of an


object as regular variables. For example, the member yours.year
is a valid variable of type int, and mine.title is a valid variable of
type string.

The objects mine and yours can also be treated as valid variables
of type movies_t, for example we have passed them to the
function printmovie as we would have done with regular
variables. Therefore, one of the most important advantages of
data structures is that we can either refer to their members
individually or to the entire structure as a block with only one
identifier.

3.1.4 Complex Structures

It is possible to declare multiple levels of structures inside each


other, called complex structure or structure of structure, as
follows:

___________________________________________________________
50
Chapter 3. Structures and Arrays. ____________________________________

// example about complex struture

#include <iostream>
using namespace std;

sturct Tel
{
int Fix;
int mobile;
};

struct Friend
{
char name[10];
int birth;
Tel phone;
};

int main()
{
Friend Friends;
Friends.name = {A, h, m, e, d};
Friends.birth = 1990;
Friends.Phone.Fix = 2222222;
Friends.Phone.Mobile = 0102020200;
// ..complete the rest of the program
return 0;
}

Here we have used a structure of a structure, with two full stops.


Note that the structure Tel must be defined before the structure
Friend so that it can be used. If we ask for the name of the
variable itself like Friends without referring to any of its elements,
we are referring to a pointer to the structure itself.

___________________________________________________________
51
Chapter 3. Structures and Arrays. ____________________________________

3.2 Arrays

C++ provides another structured data type called arrays. The


array permits us to set aside a group of memory locations to store
many values of the same data type under the same name. We can
then manipulate the array as a single entity, but that at the same
time gives us direct access to any individual component inside the
array. In other words, an array is a series of elements of the same
type placed in contiguous memory locations that can be
individually referenced by adding an index to a unique identifier.
This means that, for example, we can store 5 values of type int in
an array without having to declare 5 different variables, each one
with a different identifier. Instead of that, using an array, we can
store 5 different values of the same type int with a unique
identifier. Arrays can be used in many ways, for example, a list of
students and their grades can be held in an array.

For example, an array containing 5 integer values of type int


called myarray could be represented as:

Where, each blank panel represents an element of the array that,


in this case, is integer values of type int. These elements are
numbered from 0 to 4 since the first index in arrays is always 0,
independently of its length.
___________________________________________________________
52
Chapter 3. Structures and Arrays. ____________________________________

3.2.1 Array Declaration

Like a regular variable, an array must be declared before it is used


in a program. A typical declaration for an array in C++ is:

type array_name [elements];

Where, type is a valid data type (like int, float...), array_name is


a valid identifier and the elements field (which is always enclosed
in square brackets []), specifies how many elements the array has
to contain. Therefore, in order to declare an array called myarray
as the one shown in the above diagram it is as simple as:

int myarray [5];

In this case, there are 5 variables of type int with identifiers:


myarray[0], myarray[1], myarray[2], myarray[3] and
myarray[4]. Each of these is referred to as an element or
component of the array. In this example, the numbers 0, ...,4 are
the indexes or subscripts of the elements. An important feature of
these 5 variables is that they are allocated in consecutive memory
locations in the computer.

Note that, the elements field within brackets [] which represents


the number of the array elements must hold a constant value,
since arrays are blocks of non-dynamic (static) memory whose
size must be determined before execution. In order to create
___________________________________________________________
53
Chapter 3. Structures and Arrays. ____________________________________

arrays with a variable length, dynamic memory is needed, which is


explained later in pointers.

Note also that, the array elements can be defined by either the
standard data type (int, char or float) or any other data-type
including structures, arrays and classes.

3.2.2 Array Initialization

When declaring a regular array within a function, its elements will


not be initialized to any value by default, so their content will be
undetermined until we store some values in them. Generally,
when we declare an array, we have the possibility to assign initial
values to each one of its elements by enclosing the values in
braces { }. For example:

int myarray [5] = { 16, 2, 77, 40, 12071 };

This declaration would have created an array like this:

The amount of values between braces { } must not be larger than


the number of elements that we declare for the array between
square brackets [ ]. In the above example, we have declared that

___________________________________________________________
54
Chapter 3. Structures and Arrays. ____________________________________

the array has 5 elements and in the list of initial values within
braces { } we have specified 5 values, one for each element.

When an initialization of values is provided for an array, C++


allows the possibility of leaving the square brackets empty [ ]. In
this case, the compiler will assume a size for the Array that
matches the number of values included between braces { }. For
example, the declaration

int hours [] = { 16, 2, 77, 40, 12071 };

creates an array called hours containing 5 elements of type int,


since we have provided 5 initialization values.

3.2.3 Accessing Array Elements

At any point of a program in which an array is visible, we can


access any of the array elements individually as if it was a normal
variable, thus being able to both read and modify its value. The
general format to access an element is as simple as:

Array_name[element_index]

Following the previous examples in which myarray had 5


elements and each of those elements was of type int, the name
which we can use to refer to each element is that: myarray[0],
myarray[1], myarray[2], myarray[3] and myarray[4].
___________________________________________________________
55
Chapter 3. Structures and Arrays. ____________________________________

For example, to store the value 75 in the third element of


myarray, we could write the following statement:

myarray[2] = 75;

Another example, to pass the value of the third element of


myarray to a variable called X, we could write:

X = myarray[2];

Therefore, the expression myarray[2] is for all purposes like a


variable of type int.

Notice that, the third element of myarray is specified myarray[2],


since the first one is myarray[0], the second one is myarray[1],
and therefore, the third one is myarray[2]. By the same reason,
the last element is myarray[4]. Some other valid operations with
arrays:

myarray[0] = a;
b = myarray [a+2];
myarray[myarray[a]] = myarray[2] + 5;

Note that, if we write myarray[5], we would be accessing the sixth


element of myarray and therefore exceeding the size of the
array. In C++ it is syntactically correct to exceed the valid range
of indices for an Array. But, this can create problems, since
accessing out-of-range elements do not cause compilation errors
but can cause runtime errors.
___________________________________________________________
56
Chapter 3. Structures and Arrays. ____________________________________

At this point it is important to be able to clearly distinguish


between the two uses of brackets [ ] that have related to arrays.
They perform two different tasks: one is to specify the size of
arrays when they are declared; and the second one is to specify
indices for concrete array elements. Do not confuse these two
possible uses of brackets [ ] with arrays.

int myarray[5]; // declaration of a new Array


myarray[2] = 75; // access an element of the Array.

If you read carefully, you will see that a type specifier precedes a
variable or array declaration, while it never precedes an access.

A common way to access the entire array elements within a


program can be done by using either for or while loops.

// arrays example
#include <iostream>
using namespace std;

int myarray [] = {16, 2, 77, 40, 12071};


int n, result=0;

int main ()
{
for ( n=0 ; n<5 ; n++ )
{
result += myarray[n];
}
cout << result;
return 0;
}

12206

___________________________________________________________
57
Chapter 3. Structures and Arrays. ____________________________________

3.2.4 Multidimensional Arrays

Multidimensional arrays can be described as "arrays of arrays". For


example, a bi-dimensional array can be imagined as a bi-
dimensional table made of elements, all of them of a same
uniform data type.

The array called myarray represents a two dimensional array of 3


per 5 elements of type int. The way to declare this array is that:

int myarray [3][5];

and, for example, the way to reference the second element


vertically and fourth horizontally in an expression would be:

myarray[1][3]

(Remember that array indices always begin by zero).

___________________________________________________________
58
Chapter 3. Structures and Arrays. ____________________________________

Multidimensional arrays are not limited to two indices (i.e., two


dimensions). They can contain as many indices as needed. But be
careful! The amount of memory needed for an array rapidly
increases with each dimension. For example:

char century [100][365][24][60][60];

declares an array with a char element for each second in a


century, that is more than 3 billion chars. So this declaration
would consume more than 3 gigabytes of memory!

Multidimensional arrays are just an abstraction for programmers,


since we can obtain the same results with a simple array just by
putting a factor between its indices:

int myarray [3][5]; // is equivalent to


int myarray [15]; // (3 * 5 = 15)

With the only difference is that, with multidimensional arrays the


compiler remembers the depth of each imaginary dimension. Take
as example, the following two pieces of code, with both exactly
the same result. One uses a bi-dimensional array and the other
one uses a simple array:

multidimensional array pseudo-multidimensional array


#define WIDTH 5 #define WIDTH 5
#define HEIGHT 3 #define HEIGHT 3

int myarray [HEIGHT][WIDTH]; int myarray [HEIGHT * WIDTH];

___________________________________________________________
59
Chapter 3. Structures and Arrays. ____________________________________

int n,m; int n,m;

int main () int main ()


{ {
for (n=0;n<HEIGHT;n++) for (n=0;n<HEIGHT;n++)
for (m=0;m<WIDTH;m++) for (m=0;m<WIDTH;m++)
{ {
myarray[n][m]=(n+1)*(m+1); myarray[n*WIDTH+m]=(n+1)*(m+1);
} }
return 0; return 0;
} }

None of the two source codes above produce any output on the
screen, but both assign values to the memory block called
myarray in the following way:

We have used "defined constants" (#define) to simplify possible


future modifications of the program. For example, in case that we
decided to enlarge the array to a height of 4 instead of 3 it could
be done simply by changing the line:

#define HEIGHT 3

to:

#define HEIGHT 4

with no need to make any other modifications to the program.

___________________________________________________________
60
Chapter 3. Structures and Arrays. ____________________________________

3.2.5 Arrays as Function Arguments

At some moment we may need to pass an array to a function as a


parameter. In C++ it is not possible to pass a complete block of
memory by value as a parameter to a function, but it is allowed to
pass a reference address. In practice this has almost the same
effect and it is a much faster and more efficient operation.

In order to accept arrays as parameters the only thing that we


have to do when declaring the function is to specify in its
parameters the element type of the array, an identifier and a pair
of void brackets []. For example, the following function:

void Fun1(int arg[])


{
statements;
..
}

defines an array called arg as a parameter of type "Array of int".


In order to pass to this function an array declared as:

int myarray [40];

it would be enough to write a call statement like this:

Fun1 (myarray);

Here is a complete example:

___________________________________________________________
61
Chapter 3. Structures and Arrays. ____________________________________

// arrays as parameters

#include <iostream>
using namespace std;

void printarray (int arg[], int length)


{
for (int n=0; n<length; n++)
cout << arg[n] << " ";
cout << "\n";
}

int main ()
{
int firstarray[] = {5, 10, 15};
int secondarray[] = {2, 4, 6, 8, 10};
printarray (firstarray,3);
printarray (secondarray,5);
return 0;
}

5 10 15
2 4 6 8 10

As you can see, the first parameter (int arg[]) accepts any array
whose elements are of type int, whatever its length. For that
reason, a second parameter is included to tell the function the
length of each array that we pass to it as its first parameter. This
allows the for loop that prints out the array to know the range to
iterate in the passed array without going out of range.

It is also possible to include multidimensional arrays in a function


declaration. The format for a tri-dimensional array parameter is:

data_type array_name[][depth][depth]

___________________________________________________________
62
Chapter 3. Structures and Arrays. ____________________________________

For example, a function with a multidimensional array as


argument could be in the form:

void procedure (int myarray[][3][4])

Notice that the first brackets [] are left blank while the following
ones are not. This is because the compiler must be able to
determine the depth of each additional dimension within function.

Note also that, arrays, either simple or multidimensional, passed


as function parameters are a quite common source of errors for
programmers. We recommend the reading of the chapter about
Pointers for a better understanding on how arrays operate.

3.2.6 Array of structures

Data structures can be used to represent databases, especially if


we consider the possibility of building arrays of them:

// array of structures
#include <iostream>
#include <string>
#include <sstream>
using namespace std;

#define N_MOVIES 3

struct movies_t
{
string title;

___________________________________________________________
63
Chapter 3. Structures and Arrays. ____________________________________

int year;
} films [N_MOVIES];

void printmovie (movies_t movie);

int main ()
{
string mystr;
int n;

for (n=0; n<N_MOVIES; n++)


{
cout << "Enter title: ";
getline (cin,films[n].title);
cout << "Enter year: ";
getline (cin,mystr);
stringstream(mystr) >> films[n].year;
}

cout << "\nYou have entered these movies:\n";


for (n=0; n<N_MOVIES; n++)
printmovie (films[n]);
return 0;
}

void printmovie (movies_t movie)


{
cout << movie.title;
cout << " (" << movie.year << ")\n";
}

Enter title: Blade Runner


Enter year: 1982
Enter title: Matrix
Enter year: 1999
Enter title: Taxi Driver
Enter year: 1976

You have entered these movies:


Blade Runner (1982)
Matrix (1999)
Taxi Driver (1976)
___________________________________________________________
64
Chapter 3. Structures and Arrays. ____________________________________

3.2.7 Array of Characters

As you may already know, the C++ Standard Library implements


a powerful string class, which is very useful to handle and
manipulate strings of characters. However, because strings are in
fact sequences of characters, we can represent them also as plain
arrays of char elements. For example, the following array:

char text [20];

is an array called text that can store up to 20 elements of type


char. It can be represented as:

Therefore, in this array, we can store sequence of characters up


to 20 characters long. But we can also store shorter sequences.
For example, text could store at some point in a program either
the sequence "Hello" or the sequence "Merry christmas", since
both are shorter than 20 characters.

Therefore, since the array of characters can store shorter


sequences than its total length, a special character is used to
signal the end of the valid sequence: the null character, whose
literal constant can be written as \0 (backslash, zero).

___________________________________________________________
65
Chapter 3. Structures and Arrays. ____________________________________

The array text, that contains 20 elements of type char, can


represent/store the character sequence "Hello" and "Merry
Christmas" as:

Notice how a null character ('\0') has been included after the valid
content in order to indicate the end of the sequence. The panels
in gray color represent char elements with undetermined values.

3.2.7.1 Initializing Character Arrays

Because arrays of characters are generally similar to ordinary


arrays, they follow all their same rules. For example, if we want to
initialize an array of characters with some predetermined
sequence of characters we can do it just like any other array:

char myword[] = {'H', 'e', 'l', 'l', 'o', '\0'};

In this case we would have declared an array of 6 elements of


type char initialized with the characters that form the word
"Hello" plus a null character '\0' at the end.

___________________________________________________________
66
Chapter 3. Structures and Arrays. ____________________________________

But arrays of char elements have an additional method to initialize


their values: using string literals.

In the expressions we have used in some examples in previous


chapters, constants that represent entire strings of characters
have already showed up several times. These are specified
enclosing the text to become a string literal between double
quotes ("). For example:

"the result is: "

is a constant string literal that we have probably used already.

Double quoted strings (") are literal constants whose type is in


fact a null-terminated array of characters. So, string literals
enclosed between double quotes always have a null character
('\0') automatically appended at the end.

Therefore we can initialize the array of char elements called


myword with a null-terminated sequence of characters by either
one of these two methods:

char myword [] = {'H', 'e', 'l', 'l', 'o', '\0'};


char myword [] = "Hello";

In both cases the array of characters myword is declared with a


size of 6 elements of type char: the 5 characters that compose the
word "Hello" plus a final null character ('\0') which specifies the
___________________________________________________________
67
Chapter 3. Structures and Arrays. ____________________________________

end of the sequence and that, in the second case, when using
double quotes (") it is appended automatically.

Please notice that we are talking about initializing an array of


characters in the moment it is being declared, and not about
assigning values to them once they have already been declared.
In fact because this type of null-terminated arrays of characters
are regular arrays we have the same restrictions that we have
with any other array, so we are not able to copy blocks of data
with an assignation operation.

Assuming mytext is a char[] variable, expressions within a source


code like:

mytext = "Hello";
mytext[] = "Hello";

would not be valid, like neither would be:

mytext = { 'H', 'e', 'l', 'l', 'o', '\0' };

The reason for this may become more comprehensible once you
know a bit more about pointers, since then it will be clarified that
an array is in fact a constant pointer pointing to a block of
memory.

___________________________________________________________
68
Chapter 3. Structures and Arrays. ____________________________________

3.2.7.2 null-terminated sequences of


characters

Null-terminated sequences of characters are the natural way of


treating strings in C++, so they can be used as such in many
procedures. In fact, regular string literals have this type (char[])
and can also be used in most cases.

For example, cin and cout support null-terminated sequences as


valid containers for sequences of characters, so they can be used
directly to extract strings of characters from cin or to insert them
into cout. For example:

// null-terminated sequences of characters

#include <iostream>
using namespace std;

int main ()
{
char question[] = "Please, enter your first name: ";
char greeting[] = "Hello, ";
char yourname [80];
cout << question;
cin >> yourname;
cout << greeting << yourname << "!";
return 0;
}
Please, enter your first name: John
Hello, John!

As you can see, we have declared three arrays of char elements.


The first two were initialized with string literal constants, while the

___________________________________________________________
69
Chapter 3. Structures and Arrays. ____________________________________

third one was left un-initialized. In any case, we have to specify


the size of the array: in the first two (question and greeting) the
size was implicitly defined by the length of the literal constant they
were initialized to. While for yourname we have explicitly
specified that it has a size of 80 chars.

Finally, sequences of characters stored in char arrays can easily be


converted into string objects just by using the assignation
operator:

mystring;
char mycharseq[]="some text";
mystring = mycharseq;

3.3 Summary

In this chapter, we have looked at two types of static data


structure: structures and arrays. We have shown how a list of
variables may be represented as a structure and how they may be
represented as one dimensional or two dimensional arrays.

___________________________________________________________
70
Chapter 4

Pointers and Dynamic Memory

In the previous chapter, we have not given any method to control


the amount of memory used in a program. In this chapter, we
introduce the notion of pointers (dynamic data structure), which
gives the programmer a greater level of control over the way the
program allocates and de-allocates memory locations during its
execution.

4.1 Introduction

The memory of the computer can be imagined as a succession of


memory cells, each one represents the minimal size that computer
manage (one byte). These single-byte memory cells are numbered
in a consecutive way, so as, within any block of memory, every
cell has the same number as the previous one plus one. In this
way, each cell can be easily located in the memory because it has
a unique address and all the memory cells follow a successive
Chapter 4. Pointers and Dynamic Memory. ____________________________

pattern. For example, if we are looking for cell 1776 we know that
it is going to be right between cells 1775 and 1777, exactly one
thousand cells after 776 and one thousand cells before cell 2776.

In all of the programs we have looked at so far, a certain amount


of memory is reserved for each declared variable at compilation
time, and this memory is retained to the variable as long as the
program, or the block in which the variable is defined, is active.
These declared variables are seen as memory cells that can be
accessed using their equivalent identifiers. In this way, we did not
have to care about the physical location of our data within
memory; we simply use its identifier whenever we want to refer to
it. In terms of memory management, it is often convenient to use
pointers (dynamic data structure), which gives the programmer a
greater level of control over the way the program allocates and
de-allocates memory locations during its execution.

4.2 What are pointers?

Pointers are a very powerful feature of the C++ language that has
many uses in advanced programming. Pointers are what they
sound like...pointers; they point to locations/places in memory.
Perhaps, an analogy would be illuminating: a pointer is like a sign
pointing to a window. It is not the window itself but an indication
to where the window is. Similarly, a pointer is like a postal address
which tells us where it is, but is not the actual building of the post.
___________________________________________________________
72
Chapter 4. Pointers and Dynamic Memory. ____________________________

Pointers can be confusing, and at times, you may wonder why you
would ever want to use them. The truth is that they can make
some things in programming much easier. For example, pointers
are often used with arrays, structure and objects to move
information between functions. This is because, these objects tend
to be large, and thus making it is difficult to pass the whole array
or structure between functions (as we would normally do with
variables). The solution proposed is that; pass only the memory
address of the array or the structure. It is also possible to use
pointers to dynamically allocate memory locations. Indeed,
pointers allow setting up certain programming techniques such as
stacks, linked lists and queues. Farther ahead, we will see how
this type of variable is used and declared.

4.3 Declaring Pointers

A pointer is just a memory address of a variable, so a pointer


variable is just a variable points to a memory location in which we
can store data. Due to the ability of a pointer to directly refer to
the value that it points to, it becomes necessary to specify in its
declaration which data type a pointer is going point to. It is not
the same thing to point to a char than to point to an int or a float.
The general syntax for declaring a pointer is that:

data_type *pointer_name;

___________________________________________________________
73
Chapter 4. Pointers and Dynamic Memory. ____________________________

Where, data_type is the type of the data value that the pointer is
intended to point to. This type is not the type of pointer itself but
the type of the data the pointer points to. The asterisk symbol * is
the key of declaring a pointer. If you use it before a variable
name, it will declare the variable as a pointer. In other words,
pointer variables are declared using a "*" and has data types like
the other variables we have seen previously. For example:

int* number;
char* character;
float* greatnumber;

These are three declarations of pointers. Each one is intended to


point to a different data type: the first pointer points to an int, the
second one to a char and the last one to a float. But, in fact, all
of them are pointers and all of them will occupy the same amount
of memory (the size in memory of a pointer depends on the
platform where the code is going to run). Therefore, although
these three example variables are all of them pointers which
occupy the same size in memory, they are said have different
types: int*, char* and float* respectively, depending on the
type they point to.

Note that, the pointer data type may be a standard data type like
int and char or a user defined data type like structures and arrays.
In the following, we will first deal with pointers to basic data types
and then we deal with pointers to both structures and arrays.

___________________________________________________________
74
Chapter 4. Pointers and Dynamic Memory. ____________________________

4.4 Operations with pointers

Although the declaration of pointers is very simple, operations


with pointer can be confusing, because pointers can both give the
memory location and give the actual value stored in that same
location. Generally, there are two basic pointer operations, the
contents of address operation represented by the asterisk (*) and
the address of operation represented by the ampersand (&). Both
of the symbols ‘* and &’ are unary operators, placed in the front
of the variable. Given a particular data type, such as "int", we can
write assignment statements involving both ordinary variables and
pointer variables of this data type using the operator "*" and the
(complementary) address-of operator "&". Roughly speaking, "*"
means "the variable located at the address", and "&" means "the
address of the variable".

4.4.1 Reference operator (&)

As soon as we declare a variable, the amount of memory needed


is assigned for it at a specific location in memory (its memory
address). We generally do not actively decide the exact location of
the variable within the panel of cells that we have imagined the
memory to be - Fortunately, that is a task automatically performed
by the operating system during runtime. However, in some cases
we may be interested in knowing the address where our variable
___________________________________________________________
75
Chapter 4. Pointers and Dynamic Memory. ____________________________

is being stored during runtime in order to operate with relative


positions to it.

The address that locates a variable within memory is what we call


a reference to that variable. In order to get the memory address
of a variable, put the ampersand sign (&) in the front of the
variable name. This sign is known as reference operator, because
it returns the memory address, and can be literally translated as
"address of". For example:

ptr = &num ;

This would assign the address of variable num to ptr. This means
that, when preceding the name of a variable with the reference
operator (&) we are no longer talking about the content of the
variable itself but about its reference (i.e., its address in memory).

For now on we are going to assume that num is placed during


runtime in the memory address 1776. This number (1776) is just
and arbitrary assumption we are inventing right now in order to
help clarify some concepts in this chapter, but in reality, we
cannot know the real address of a variable before runtime.

Consider the following code fragment:

num = 25;
fred = num;
ptr = &num;

___________________________________________________________
76
Chapter 4. Pointers and Dynamic Memory. ____________________________

The values contained in each variable after the execution of this


piece of code are shown in the following diagram:

First, we have assigned the value 25 to num (a variable whose


address in memory is 1776 as we have assumed).

The second statement copied the content of variable num to fred


(which is 25). This is a standard assignation, as we have done so
many times before.

Finally, the third statement copies to ptr not the value contained
in num but a reference to it (i.e., its address which is 1776). The
reason is that, in this third assignation operation, we have
preceded the identifier num with the reference operator (&), so
we were no longer referring to the value of num but to its
reference (its address in memory). The variable that stores the
reference to another variable (like ptr in the previous example) is
what we call a pointer.

Hence, the reference operator (&) can be used to assign pointers


to variables.
___________________________________________________________
77
Chapter 4. Pointers and Dynamic Memory. ____________________________

4.4.2 Dereference operator (*)

We have just seen that a variable which stores a reference to


another variable is called a pointer. Indeed, pointers are said to
"point to" the variable. Using a pointer we can directly access the
value stored in the variable which it points to. To do this, we
simply have to precede the pointer's identifier with an asterisk (*),
which acts as dereference operator and that can be literally
translated to "value pointed by".

Therefore, considering the previous example, if we write:

var1 = *ptr;

(that we could read as: "var1 equals to value pointed by ptr")


var1 would take the value 25, since ptr is 1776, and the value
pointed by 1776 is 25.

Notice the difference of including or not including the dereference


operator:
___________________________________________________________
78
Chapter 4. Pointers and Dynamic Memory. ____________________________

Var1 = ptr; // var1 equals to ptr (1776)


var1 = *ptr; // var1 equal to value pointed by ptr( 25)

You must clearly differentiate that the ptr refers to the value
1776, while *ptr (with asterisk * preceding the identifier) refers
to the value stored at address 1776, which in this case is 25.

Here, we want to emphasize that the asterisk sign (*) that we


use when declaring a pointer only means that it is a pointer (it is
part of its type compound specifier), and should not be confused
with the dereference operator that we have seen a bit earlier, but
which is also written with an asterisk (*). They are simply two
different things represented with the same sign. For example:

// my first pointer
#include <iostream>
using namespace std;

int main ()
{
int firstvalue, secondvalue;
int * mypointer; // * declaring a pointer

mypointer = &firstvalue;
*mypointer = 10; // * deference operator
mypointer = &secondvalue;
*mypointer = 20; // * deference operator

cout << "firstvalue is" << firstvalue << endl;


cout << "secondvalue is" << secondvalue << endl;
return 0;
}

firstvalue is 10
secondvalue is 20

___________________________________________________________
79
Chapter 4. Pointers and Dynamic Memory. ____________________________

Notice that although we have never directly set a value to either


firstvalue or secondvalue, both end up with a value set
indirectly through the use of mypointer. The procedure is that:

First, we have assigned mypointer a reference to firstvalue


using the reference operator (&). Then we have assigned the
value 10 to the memory location pointed by mypointer. This in
fact modifies the value of firstvalue, this is because at this
moment mypointer is pointing to the memory location of
firstvalue. Finally, the same process is repeated with
secondvalue using same pointer, mypointer.

Here is an example a little bit more elaborated:

// more pointers
#include <iostream>
using namespace std;

int main ()
{
int firstvalue = 5, secondvalue = 15;
int * p1, * p2;
p1 = &firstvalue; // p1 = address of firstvalue
p2 = &secondvalue; // p2 = address of secondvalue
*p1 = 10; // value pointed by p1 = 10
*p2 = *p1; //value pointed by p2 = value pointed by p1
p1 = p2; // p1 = p2 (value of pointer is copied)
*p1 = 20; // value pointed by p1 = 20

cout << "firstvalue is " << firstvalue << endl;


cout << "secondvalue is " << secondvalue << endl;
return 0;
}

firstvalue is 10
secondvalue is 20

___________________________________________________________
80
Chapter 4. Pointers and Dynamic Memory. ____________________________

In the above code, we have included as a comment on each line


how the code can be read: ampersand (&) as "address of" and
asterisk (*) as "value pointed by".

In the program, there are expressions with pointers p1 and p2,


both with and without dereference operator (*). The meaning of
an expression using the dereference operator (*) is very different
from one that does not. When this operator precedes the pointer
name, the expression refers to the memory location pointed by it,
while when a pointer name appears without this operator; it refers
to the value of the pointer itself, that is, a reference.

Another thing that may call your attention is the line:

int * p1, * p2;

This declares the two pointers used in the previous example. But
notice that there is an asterisk (*) for each pointer, in order for
both to have type int* (pointer to int). If we had written:

int * p1, p2;

p1 would indeed have int* type, but p2 would have type int.
Spaces do not matter at all for this purpose. Anyway, simply
remember to put one asterisk per pointer.

___________________________________________________________
81
Chapter 4. Pointers and Dynamic Memory. ____________________________

4.4.3 Simple pointer example

To operate with pointers it is important to differentiate between


the reference and the dereference operators:

 & is the reference operator and can be read as "address of”.


 * is the dereference operator and can be read as "value pointed by".

They have complementary (or opposite) meanings. A variable


referenced with & can be dereferencing with *. We can illustrate
the uses of these operators with a simple example program:

#include <iostream.h>
int main()
{
int * ptr_a, ptr_b;
int num_c = 4, num_d = 7;

ptr_a = &num_c; /* LINE 10 */


ptr_b = ptr_a; /* LINE 11 */
cout << *ptr_a << " " << *ptr_b << "\n";

ptr_b = &num_d; /* LINE 15 */


cout << *ptr_a << " " << *ptr_b << "\n";

*ptr_a = *ptr_b; /* LINE 19 */


cout << *ptr_a << " " << *ptr_b << "\n";
cout << num_c << " " << *&*&*&num_c << "\n";
return 0;
}
The output is:
4 4
4 7
7 7
7 7

___________________________________________________________
82
Chapter 4. Pointers and Dynamic Memory. ____________________________

In the above program, the assignment statement

ptr_a = &num_c;

(in line 10) effectively gives an alternative name to the variable


num_c, which can now also be referred to as *ptr_a. This
variable can also be referred to by dereference pointer variable
ptr_b because of the statement (in line 11) ptr_b = ptr_a.
Diagrammatically, the state of the program after the assignments
at lines 10 and 11 is:

After the assignment at line 15 the above diagram changes to:

After the assignment at line 19 it becomes:

Note that "*" and "&" are in a certain sense complementary


operations; "*&*&*&num_c" is simply "num_c".

___________________________________________________________
83
Chapter 4. Pointers and Dynamic Memory. ____________________________

4.4.4 Pointer initialization

When declaring pointers we may want to explicitly specify which


variable we want them to point to:

int number;
int *ptr = &number;

The behavior of this code is equivalent to:

int number;
int *ptr;
ptr = &number;

When a pointer initialization takes place, we are always assigning


the reference value to where the pointer points (ptr), never the
value being pointed (*ptr). You must consider that at the moment
of declaring a pointer, the asterisk (*) indicates only that it is a
pointer, it is not the reference operator (although both use the
same sign: *). Remember, they are two different functions of one
sign. Thus, we must take care not to confuse the previous code
with:

int number;
int *ptr;
*ptr = &number;

that is incorrect, and anyway would not have much sense in this
case if you think about it.

___________________________________________________________
84
Chapter 4. Pointers and Dynamic Memory. ____________________________

As in the case of arrays, the compiler allows the special case that
we want to initialize the content at which the pointer points with
constants at the same moment the pointer is declared:

char * ptr = "hello";

In this case, memory space is reserved to contain "hello" and then


a pointer to the first character of this memory block is assigned to
ptr. If we imagine that "hello" is stored at the memory locations
that start at addresses 1702, we can represent the previous
declaration as:

It is important to indicate that ptr contains the value 1702, and


not 'h' nor "hello", although 1702 is the address of both of these.

The pointer ptr points to a sequence of characters and can be


used exactly as if it was an array (remember that an array is just
like a constant pointer). For example, if our mood changed and
we wanted to replace the 'o' by a '!' sign in the sequence of
characters pointed by ptr, we could do it by any of the following
two statements:

___________________________________________________________
85
Chapter 4. Pointers and Dynamic Memory. ____________________________

ptr[4] = '!';
*(ptr+4) = '!';

Remember that to write ptr[4] is equivalent to write *(ptr+4),


although the first expression is shorter and is the most usual one.
With either of those two expressions something like this would
happen:

4.4.5 void pointers

The void type of pointer is a special type of pointer. In C++, void


represents the absence of type, so void pointers are pointers that
point to a value that has no type (and thus also an undetermined
length and undetermined dereference properties).

This allows void pointers to point to any data type, from an


integer value or a float to a string of characters. But in exchange
they have a great limitation: the data pointed by them cannot be
directly dereference (which is logical, since we have no type to
dereference to), and for that reason we will always have to
change the type of the void pointer to some other pointer type
___________________________________________________________
86
Chapter 4. Pointers and Dynamic Memory. ____________________________

that points to a concrete data type before dereferencing it. This is


done by performing type-castings.

One of its uses may be to pass generic parameters to a function:

// increaser
#include <iostream>
using namespace std;

void increase (void* data)


{
switch (sizeof(data))
{
case sizeof(char): (*((char*)data))++; break;
case sizeof(int) : (*((int*)data))++; break;
}
}

int main ()
{
char a = 'x';
int b = 1602;
increase (&a);
increase (&b);
cout << a << ", " << b;
return 0;
}

y, 1603

The sizeof is an operator integrated in the C++ language that


returns either a variable or constant value with the size in bytes of
its parameter, so, for example, sizeof(char) is 1, because char
type is one byte long.

___________________________________________________________
87
Chapter 4. Pointers and Dynamic Memory. ____________________________

4.4.6 Null pointer

A null pointer is a regular pointer of any pointer type which has a


special value that indicates that it is not pointing to any valid
reference or memory address. This value is the result of type-
casting the integer value zero to any pointer type.

int * p;
p = 0; // p has a null pointer value

Do not confuse null pointers with void pointers. A null pointer is a


value that any pointer may take to represent that it is pointing to
"nowhere", while a void pointer is a special type of pointer that
can point to somewhere without a specific type. One refers to the
value stored in the pointer itself and the other to the type of data
it points to.

4.4.7 Pointer arithmetic

To conduct arithmetical operations on pointers is a little different


than to conduct them on regular integer data types. To begin
with, only addition and subtraction operations are allowed to be
conducted with them, the others make no sense in the world of
pointers. But both addition and subtraction have a different

___________________________________________________________
88
Chapter 4. Pointers and Dynamic Memory. ____________________________

behavior with pointers according to the size of the data type to


which they point.

When we saw the different fundamental data types, we saw that


some occupy more or less space than others in the memory. For
example, in the case of integer numbers, char takes 1 byte, short
takes 2 bytes and long takes 4.

Suppose that we have three pointers:

char *mychar;
short *myshort;
long *mylong;

and that we know that they point to memory locations 1000, 2000
and 3000 respectively.

So if we write:

mychar++;
myshort++;
mylong++;

mychar, as you may expect, would contain the value 1001.


Nevertheless, myshort would contain the value 2002, and mylong
would contain 3004. The reason is that when adding one to a
pointer we are making it to point to the following element of the
same type with which it has been defined, and therefore the size
in bytes of the type pointed is added to the pointer.

___________________________________________________________
89
Chapter 4. Pointers and Dynamic Memory. ____________________________

This is applicable both when adding and subtracting any number


to a pointer. It would be happen exactly the same if we write:

mychar = mychar + 1;
myshort = myshort + 1;
mylong = mylong + 1;

Both the increase (++) and decrease (--) operators have greater
operator precedence than the dereference operator (*), but both
have a special behavior when used as suffix (the expression is
evaluated with the value it had before being increased).
Therefore, the following expression may lead to confusion:

*p++

Because ++ has greater precedence than *, this expression is


equivalent to *(p++). Therefore, what it does is to increase the
value of p (so it now points to the next element), but because ++

___________________________________________________________
90
Chapter 4. Pointers and Dynamic Memory. ____________________________

is used as postfix the whole expression is evaluated as the value


pointed by the original reference (the address the pointer pointed
to before being increased).

Notice the difference with:

(*p)++

Here, the expression would have been evaluated as the value


pointed by p increased by one. The value of p (the pointer itself)
would not be modified (what is being modified is what it is being
pointed to by this pointer).

If we write:

*p++ = *q++;

Because ++ has a higher precedence than *, both p and q are


increased, but because both increase operators (++) are used as
postfix and not prefix, the value assigned to *p is *q before both
p and q are increased. And then both are increased. It would be
roughly equivalent to:

*p = *q;
++p;
++q;

Like always, I recommend you to use parentheses () in order to


avoid unexpected results and to give more legibility.

___________________________________________________________
91
Chapter 4. Pointers and Dynamic Memory. ____________________________

4.5 Pointers to structures

Like any other type, structures can be pointed by pointers. For


example:

struct point
{
float x;
float y;
};

point point_a;
point * ptr;

In this example, point_a is an object of structure data type


point, and ptr is a pointer points to objects of structure type
point. So, the following code would be valid:

ptr = &point_a;

The reference value to the object point_a (its memory address)


would be assigned to the pointer ptr. Hence, the pointer ptr
points to a structure of type point.

When using pointers to structures, we use the indirect pointer


operator (->) or to access the structure members. To see how this
works consider the following program:

___________________________________________________________
92
Chapter 4. Pointers and Dynamic Memory. ____________________________

// pointer to structure
#include <iostream.h>

struct product
{
char name[4];
double price;
double mark;
};
int main()
{
product item, *Ptr;
item.name = "ABC"; //Initializing parts of Item
item.price = 5.43;
item.mark = 1000;

ptr = &item;

cout << "What's the name of the Product?" <<endl;


cout << ptr->name << endl;
ptr->name = "AEC";
cout << "What's it again?" << endl;
cout << ptr->name << endl;
return 1;
}

Here again we initialize the pointer as we would for a basic data


type, ptr=&item. To access elements of the structure from the
pointer, we have to point indirectly using the ‘->’ operator. Thus
ptr->price gives us one of the elements, i.e., price that ptr is
pointing to. As seen above we can use the indirect pointer in
simple assignment operations as well. Note that the ptr->name
has the same meaning as (*ptr).name and ‘item.name’.

___________________________________________________________
93
Chapter 4. Pointers and Dynamic Memory. ____________________________

The following panel summarizes possible combinations of pointers


and structure members:

Expression What is evaluated Equivalent

a.b Member b of object a

a->b Member b of object pointed by pointer a (*a).b

*a.b Value pointed by member b of object a *(a.b)

4.6 Pointers to Arrays

In the previous chapter, using arrays, we saw how to declare


groups of variables by unique identifier. The concept of array is
very much bound to the one of pointer. In fact, the identifier of an
array is equivalent to the address of its first element, as a pointer
is equivalent to the address of the first element that it points to,
so in fact they are the same concept. For example, assuming the
following two declarations:

int num [20];


int * ptr;

After the execution of the following valid assignation statement:

ptr = num;

Both ptr and num would be equivalent and would have the same
properties with respect to the array, where they are points to the
___________________________________________________________
94
Chapter 4. Pointers and Dynamic Memory. ____________________________

first array element. Thus, num[0], *num, and *ptr are all now
different variable names denoting the first element. Thus, the
following two expressions are equivalent and valid either if num is
a pointer or if num is an array.

num[2] = 0; // num [offset of 5] = 0


*(num+2) = 0; // pointed by (num+5) = 0

Now also there are alternative names to the other array elements.
We can refer to them either as

*( num + 1), *( num + 2), ... … , *( num + 19)


or as
*(ptr + 1), *(ptr + 2), ... …, *(ptr + 19)

This means that, pointer arithmetic (addition and subtraction


numerical values to and from pointer variables) gives an
alternative and sometimes more succinct method of manipulating
arrays. Multiplication and division cannot be used in pointer
arithmetic, but the increment and decrement operators "++" and
"--" can be used, and one pointer can be subtracted from another
of the same type. Note that, the "ptr+2" is shorthand for "plus
enough memory to store 2 integer values".

The only difference between ptr and num is that we could


change the value of pointer ptr by another one, whereas num
will always point to the first of the 20 elements of type int with
which it was defined. Therefore, unlike ptr, which is an ordinary

___________________________________________________________
95
Chapter 4. Pointers and Dynamic Memory. ____________________________

pointer, num is an array that can be considered a constant


pointer. Therefore, the following allocation would not be valid:

num = p;

Because num is an array, so it operates as a constant pointer,


and we cannot assign values to constants.

The following example illustrates some valid expressions that


include pointers:

// more pointers

#include <iostream>
using namespace std;

int main ()
{
int num[5];
int * p;
p = num;
*p = 10;
p++;
*p = 20;
p = &num[2];
*p = 30;
p = num + 3;
*p = 40;
p = num;
*(p+4) = 50;
for (int i=0; i<5; i++)
cout << num[i] << ", ";
return 0;
}

10, 20, 30, 40, 50,

___________________________________________________________
96
Chapter 4. Pointers and Dynamic Memory. ____________________________

4.7 Passing Structures and Arrays

There are two possible methods of passing arrays and structures


to functions. These are passing by pointers and by reference. In
all of the discussions below, it is worth keeping in mind, a model
of what is happening in the memory. When we pass a basic date
type to a function, a copy of the variable is made by the function
in a new memory space. When the variable is manipulated in the
function, the copy and not the original is used. However, what we
intend to do, since it is not possible to pass every element in large
array or structure, is to pass either the pointer or the address of
the array/structure. In this case, only the original array or
structure is kept and any manipulations done will affect it (i.e., no
copy is made). This lies at the heart of manipulating arrays and
structures using pointers. The following examples will illustrate
this point.

4.7.1 Passing Arrays by Pointers

We first deal with passing arrays by pointers through the example


below:

#include <iostream.h>

void PrintArray(int* Array, const int Dim);


void ChangeArray(int* Array, const int Dim);

___________________________________________________________
97
Chapter 4. Pointers and Dynamic Memory. ____________________________

int main()
{
const int Dim = 10;
int i, List[Dim];

for(i=0; i<Dim; i++)


List[i]=i*13;
cout << "13 times table" << endl;
PrintArray(List, Dim);
ChangeArray(List, Dim);
cout << "14 times table" << endl;
PrintArray(List, Dim);
return 1;
}

void PrintArray(int* TTable, const int D)


{
int i;
for(i=0; i<D; i++)
cout << TTable[i]<< " ";
cout << endl;
}

void ChangeArray(int* TTable, const int D)


{
int i;
for(i=0; i<D; i++)
TTable[i] = TTable[i]*14;
}

This program first initializes the array, and sends a pointer to it


through to the function PrintArray(). This function displays the
array by accessing the contents of List in the main function. This
is because only a pointer was passed; no copy of the array was
made. It then makes a change to the array by changing items in
___________________________________________________________
98
Chapter 4. Pointers and Dynamic Memory. ____________________________

List directly through ChangeArray(), as TTable and List share


the same memory space. When the function returns and List is
printed, the new values appear as expected. Note that in passing
an array, the dimension of the array is lost. To keep things, it is
usually necessary to pass the dimension of the array along with
the pointer to it. The keyword const is used to ensure that no
changes are accidentally made to the dimension intended by the
program, as it restricts any assignments to that particular variable.
It is also possible to make assignments to the array elements like
in ChangeArray.

4.7.2 Passing Structures by Pointers

A similar story for structure can be seen in the following example:

#include <iostream.h>

struct Complex
{
double Real;
double Imaginary;
};

void Conjugate(Complex* PointToI);

int main()
{
Complex I;
I.Real=0;
I.Imaginary=1;

___________________________________________________________
99
Chapter 4. Pointers and Dynamic Memory. ____________________________

if (I.Imaginary>=0)
cout << I.Real << "+" << I.Imaginary
<< "i" << endl;
else
cout << I.Real << "-" << I.Imaginary
<< "i" << endl;

Conjugate(&I);

if (I.Imaginary>=0)
cout << I.Real << "+" << I.Imaginary
<< "i" << endl;
else
cout << I.Real << "-" << I.Imaginary
<< "i" << endl;

return 1;
}

void Conjugate(Complex *MinusI)


{
MinusI->Real=0;
MinusI->Imaginary=-1;
}

Note that Complex must be defined before the function


declaration as it is used as a parameter in the function. Also, we
are required to pass the address of I hence, Conjugate(&I);. Since
we have passed a pointer over, the structure within the function
must be accessed indirectly.

___________________________________________________________
100
Chapter 4. Pointers and Dynamic Memory. ____________________________

4.8 Dynamic Memory

Until now, in all the mentioned programs, we have only had as


much memory available as we have declared the variables, having
the size of all of them to be determined in the source code, before
the execution of the program. But, what if we need a variant
amount of memory that can only be determined during runtime?
For example, in the case that we need some user input to
determine the necessary amount of memory space.

The answer is to use dynamic memory, for which C++ integrates


the operators new and delete.

4.8.1 Dynamic Allocation

In order to request dynamic memory, we use the operator new


followed by a data type specifier. If a sequence of more than one
element is required, the number of these elements should mention
within brackets []. The new operator returns a pointer to the
beginning of the new block of memory allocated. Its form is:

pointer = new data_type


pointer = new data_type [n_elements]

The first expression is used to allocate memory to contain one


single element of specific data_type. The second one is used to

___________________________________________________________
101
Chapter 4. Pointers and Dynamic Memory. ____________________________

assign a block (an array) of elements of specific data_type,


where n_elements is an integer value representing the number
of these elements. For example:

int * newptr;
newptr = new int [5];

In this case, the system dynamically assigns an enough space for


five elements of type int and returns a pointer to the first element
of the sequence, which is assigned to newptr. Therefore, now,
newptr points to a valid block of memory with space for five
elements of type int.

The first element pointed by newptr can be accessed either with


the expression newptr[0] or the expression * newptr. Both are
equivalent as has been explained in the section about pointers.
The second element can be accessed either with newptr[1] or
*(newptr+1) and so on...

You could be wondering the difference between declaring a


normal array and assigning dynamic memory to a pointer, as we
have just done. The most important difference is that the size of
an array has to be a constant value, which limits its size to what
we decide at the moment of designing the program, before its
___________________________________________________________
102
Chapter 4. Pointers and Dynamic Memory. ____________________________

execution, whereas the dynamic memory allocation allows us to


assign memory during the execution of the program (runtime)
using any variable or constant value as its size.

The dynamic memory requested by our program is allocated by


the system from the memory heap. However, computer memory is
a limited resource, and it can be exhausted. Therefore, it is
important to have some mechanism to check if the request to
allocate memory was successful or not.

C++ provides two standard methods to check if the allocation was


successful: One is by handling exceptions. Using this method an
exception of type bad_alloc is thrown when the allocation fails.
Exceptions are a powerful C++ feature explained later in this
book. But for now you should know that if this exception is thrown
and it is not handled by a specific handler, the program execution
is terminated. This exception method is the default method used
by new, and is the one used in a declaration like:

newptr=new int [5]; // if it fails an exception is thrown

The other method is known as nothrow, and what happens when


it is used is that when a memory allocation fails, instead of
throwing a bad_alloc exception or terminating the program, the
pointer returned by new is a null pointer, and the program
continues its execution. This method can be specified by using a
special object called nothrow as parameter for new:

___________________________________________________________
103
Chapter 4. Pointers and Dynamic Memory. ____________________________

newptr = new (nothrow) int [5];

In this case, if the allocation of this block of memory failed, the


failure could be detected by checking if newptr took a null
pointer value:

int * newptr;
newptr = new (nothrow) int [5];
if (newptr == 0)
{
// error assigning memory. Take measures.
};

This nothrow method requires more work than the exception


method, since the value returned has to be checked after each
and every memory allocation, but we will use it in our examples
due to its simplicity. Anyway this method can become tedious for
larger projects, which nowadays mostly rely in the more efficient
exception method, which will be explained later in this book.

4.8.2 Dynamic De-allocation

Since the necessity of dynamic memory is usually limited to


specific moments within a program, once it is no longer needed it
should be freed so that the memory becomes available again for
other requests of dynamic memory. This is the purpose of the
operator delete, whose format is:

___________________________________________________________
104
Chapter 4. Pointers and Dynamic Memory. ____________________________

delete pointer;
delete [] pointer;

The first expression should be used to delete memory allocated


for a single element, and the second one for memory allocated for
arrays of elements.

The value passed as argument to delete must be either a pointer


to a memory block previously allocated with new, or a null
pointer (in the case of a null pointer, delete produces no effect).

4.8.3 Dynamic Variables and Arrays

Dynamic variables can be "created" using the reserved word


"new", and "destroyed" (freeing-up memory for other uses) using
the reserved word "delete". Below is a program which illustrates
the use of the new and the delete operations:

#include <iostream.h>
typedef int *IntPtrType;

int main()
{
IntPtrType ptr_a, ptr_b; /* LINE 7 */
ptr_a = new int; /* LINE 9 */
*ptr_a = 4;
ptr_b = ptr_a; /* LINE 11 */
cout << *ptr_a << " " << *ptr_b << "\n";
ptr_b = new int; /* LINE 15 */
*ptr_b = 7; /* LINE 16 */
cout << *ptr_a << " " << *ptr_b << "\n";

___________________________________________________________
105
Chapter 4. Pointers and Dynamic Memory. ____________________________

delete ptr_a;
ptr_a = ptr_b; /* LINE 21 */
cout << *ptr_a << " " << *ptr_b << "\n";
delete ptr_a; /* LINE 25 */
return 0;
}

The output of this program is:

4 4
4 7
7 7

The state of the program after the declarations in line 7 is:

After the assignments in lines 9, 10 and 11 this changes to:

After the assignments at lines 15 and 16 the state is:

After the assignment at line 21 it becomes:

___________________________________________________________
106
Chapter 4. Pointers and Dynamic Memory. ____________________________

Finally, after the "delete" statement in lines 25, the program state
returns to:

In the first and last diagrams above, the pointers "ptr_a" and
"ptr_b" are said to be dangling. Note that "ptr_b" is dangling at
the end of the program even though it has not been explicitly
included in a "delete" statement.

If "ptr" is a dangling pointer, use of the corresponding dereference


expression "*ptr" produces unpredictable (and sometimes
disastrous) results. Unfortunately, C++ does not provide any
inbuilt mechanisms to check for dangling pointers. However,
safeguards can be added to a program using the special symbolic
memory address "NULL". Any pointer of any data type can be set
to "NULL". For example, if we planned to extend the above
program and wanted to safeguard against inappropriate use of the
dereference pointer identifiers "*ptr_a" and "*ptr_b", we could
add code as follows:

#include <iostream>
...
delete ptr_a;
ptr_a = NULL;

___________________________________________________________
107
Chapter 4. Pointers and Dynamic Memory. ____________________________

ptr_b = NULL;
...
if (ptr_a != NULL)
{
*ptr_a = ...
...
}

The mechanisms described above to create and destroy dynamic


variables of type "int", "char", "float", etc., can also be applied to
create and destroy dynamic arrays. This can be especially useful
since arrays sometimes require large amounts of memory. A
dynamic array of 10 integers can be declared as follows:

int num_ptr;
num_ptr = new int [10];

To destroy the dynamic array, we write

delete [] num_ptr;

The brackets "[]" are important. They signal the program to


destroy all 10 variables, not just the first. To illustrate the use of
dynamic arrays, here is a program that prompts the user to input
a list of integers, and then prints them on the screen:

// dynamic memory

#include <iostream>
using namespace std;
___________________________________________________________
108
Chapter 4. Pointers and Dynamic Memory. ____________________________

int main ()
{
int i;
int * p;
cout << "How many numbers would you like to type? ";
cin >> i;
p= new (nothrow) int[i];
if (p == 0)
cout << "Error: memory could not be allocated";
else
{
for (int n=0; n<i; n++)
{
cout << "Enter number: ";
cin >> p[n];
}
cout << "You have entered: ";
for (n=0; n<i; n++)
cout << p[n] << ", ";
delete[] p;
}
return 0;
}
How many numbers would you like to type? 5
Enter number : 75
Enter number : 436
Enter number : 1067
Enter number : 8
Enter number : 32
You have entered: 75, 436, 1067, 8, 32,

Notice how the value within brackets in the new statement is not
a constant value but is a variable value i entered by the user. But
the user could have entered a value for i so big that our system
could not handle it. For example, when tried to give a value of 1
billion to the question "How many numbers", my system could not
allocate that much memory for the program and you will get the
text message that prepared for this case (Error: memory could not

___________________________________________________________
109
Chapter 4. Pointers and Dynamic Memory. ____________________________

be allocated). Remember that in the case of allocating the


memory without specifying the nothrow parameter in the new
expression, an exception would be thrown, which terminates the
program if it's not handled.

It is a good practice to always check if a dynamic memory block


was successfully allocated. Therefore, if you use the nothrow
method, you should always check the value of the pointer
returned. Otherwise, use the exception method, even if you do
not handle the exception. This way, the program will terminate at
that point without causing the unexpected results of continuing
executing a code that assumes a block of memory to have been
allocated when in fact it has not.

4.9 Summary

In this chapter we have seen how computer memory can be


dynamically allocated and de-allocated during the execution of a
program, using the new and delete operators. We have discussed
how arrays may be manipulated using pointer arithmetic, and how
arrays may be created and destroyed dynamically by a program.

___________________________________________________________
110
Chapter 5. Linked Lists. ___________________________________________________

Chapter 5

Linked Lists

This chapter briefly introduces the notion of linked list, as an


example of abstract data type which implemented by using
pointers.

5.1 What is a Linked List?

A linked list is an unordered set of data, not required to be


continuous in memory, yet grouped via links between
adjacent elements starting from a head pointer. It is
generally a way to store data with structures so that the
programmer can automatically create a new place to store
data whenever necessary. The advantage of linked lists over
arrays is that individual nodes can be added or deleted
dynamically, at the beginning, at the end, or in the middle of
the list.
___________________________________________________________
111
Chapter 5. Linked Lists. ___________________________________________________

A linked list is built from structures and pointers. It forms a


chain of "nodes" with pointers representing the links of the
chain and holding the entire thing together. Specifically, the
programmer writes a structure that contains variables holding
information about something and then has a pointer to a
structure of its type. Each of this individual structure in the
list is known as a node. Think of it like a train. The
programmer always stores the first node of the list. This
would be the engine of the train. The pointer is the
connector between cars of the train. Every time the train ads
a car, it uses the connectors to add a new car. This is like a
programmer using the keyword new to create a pointer to a
new structure.

In the diagram given below, a linked list consists of a series


of nodes, each containing some data and a pointer pointing
to the next node in the list. There is an additional separate
pointer which points to the first node, and the pointer in the
last node simply points to "NULL". In memory it is often
looking like this:

___________________________________________________________
112
Chapter 5. Linked Lists. ___________________________________________________

This linked list has three nodes in it, each with a link to the
next node in the series. Each of the big blocks is a struct that
has a pointer to another one. Remember that the pointer
only stores the memory location of something; it is not that
thing, so the arrow goes to the next one. At the end, there is
nothing for the pointer to point to, so it does not point to
anything, it should be set to "NULL" to prevent it from
accidentally pointing to a totally arbitrary and random
location in memory (which is very bad). There is also another
special pointer, here called pointer, which points to the first
link in the chain so that we can keep track of it.

5.2 Defining a node and Start Pointer

The key part of a linked list is a structure, which holds the


data for each node (whatever for the items in the list) and
most importantly, a pointer to the next node. For example:

struct node
{
char name[20]; // Name of up to 20 letters
int age; // The current age in years
float height; // In metres
node *next; // Pointer to next node
};

___________________________________________________________
113
Chapter 5. Linked Lists. ___________________________________________________

Here, the structure of a typical node has three data fields:


name, age, height. Indeed, the important part of the
structure is the line before the closing curly brackets (node
*next;). This gives a pointer to the next node in the list. This
is the only case in C++ where you are allowed to refer to a
data type (in this case node) before you have even finished
defining it!

Having defined the structure "node", we can declare pointer


variable of this new data type as in usual way:

node *start_ptr = NULL;

The declared pointer is called start_ptr. The start_ptr will


permanently point to the start of the list; it may be also
defined as root. To start with, there are no nodes in the list,
which is why start_ptr is set to NULL.

5.3 Creating a node

Firstly, we reserve a memory space for a node and assign a


temporary pointer to it. This is done using the new
statement as follows:

node *temp; // Temporary pointers


temp = new node;
___________________________________________________________
114
Chapter 5. Linked Lists. ___________________________________________________

We can refer to the new node as *temp, i.e. "the node that
temp points to". When the fields of this structure are referred
to, brackets can be put round the *temp part, as otherwise
the compiler will think we are trying to refer to the fields of
the pointer. Alternatively, we can use the new pointer
notation. That's what I shall do here.

5.4 Accessing a Node

Having declared the node, we ask the user to fill in the


details of the person, i.e. name, age, height or whatever:

cout << "Please enter the name of the person:";


cin >> temp->name;
cout << "Please enter the age of the person:";
cin >> temp->age;
cout <<"Please enter the height of the person:";
cin >> temp->height;
temp->next = NULL;

The last line sets the pointer from this node to the next to
NULL, indicating that this node, when it is inserted in the
list, will be the last node. Having set up the information, we
have to decide what to do with the pointers.

___________________________________________________________
115
Chapter 5. Linked Lists. ___________________________________________________

5.5 Adding a Node to the List

The first problem that we face is how to add a node to the


list. For simplicity, we will assume that it has to be added to
the end of the list, although it could be added anywhere in
the list.

Adding at Start:

Of course, if the list is empty to start with, there's no


problem - just set the Start pointer to point to this node (i.e.
set it to the same value as temp):

if (start_ptr == NULL)
start_ptr = temp;

Adding at end:

The node insertion is harder if there are already nodes in the


list. In this case, the secret is to declare a second pointer,
temp2, to step through the list until it finds the last node.

node *temp2; // Temporary pointers


temp2 = start_ptr;
while (temp2->next != NULL)
{

___________________________________________________________
116
Chapter 5. Linked Lists. ___________________________________________________

temp2 = temp2->next; // Move to next link


in chain
}

The loop will terminate when temp2 points to the last node
in the chain, and this happens when the pointer next in that
node to NULL. When it has found it, it sets the pointer from
that last node (temp2->next) to point to the node we have
just declared. The link joining the last two nodes can be
defined as:

temp2->next = temp;

The full code for adding a node at the end of the list is
shown below, in its own little function:

void add_node_at_end ()
{
node *temp, *temp2; // Temporary pointers

// Reserve space for new node and fill it with data

temp = new node;


cout << "Please enter the person name: ";
___________________________________________________________
117
Chapter 5. Linked Lists. ___________________________________________________

cin >> temp->name;


cout << "Please enter the person age: ";
cin >> temp->age;
cout << "Please enter the person height: ";
cin >> temp->height;
temp->next = NULL;

// Set up link to this node

if (start_ptr == NULL)
start_ptr = temp;
else // this is not NULL - list not empty!
{
temp2 = start_ptr;
while (temp2->nxt != NULL)
{
temp2 = temp2->next; // Move to next node
}
temp2->next = temp;
}
}

5.6 Displaying the list of nodes

Having added one or more nodes in the list, we need to


display the list of nodes on the screen. This is comparatively
easy to do. Here is the method:

1. Set a temporary pointer to point to the same thing as the start


pointer.
___________________________________________________________
118
Chapter 5. Linked Lists. ___________________________________________________

2. If the pointer points to NULL, display the message "End of


list" and stop.
3. Otherwise, display the details of the node pointed to by the
start pointer.
4. Make the temporary pointer point to the same thing as the
next pointer of the node it is currently indicating.
5. Jump back to step 2.

The temporary pointer moves along the list, displaying the


details of the nodes it comes across. At each stage, it can get
hold of the next node in the list by using the next pointer of
the node it is currently pointing to. Here is the C++ code that
does the job:

temp = start_ptr;
do
{
if (temp == NULL)
cout << "End of list" << endl;
else
{
// Display details for what temp points to
cout << "Name: " << temp->name << endl;
cout << "Age: " << temp->age << endl;
cout << "Height: " << temp->height;
cout << endl<< endl; // Blank line

// Move to next node (if present)


temp = temp->nxt;
}
}
while (temp != NULL);

___________________________________________________________
119
Chapter 5. Linked Lists. ___________________________________________________

Check through this code, matching it to the method listed


above. It helps if you draw a diagram on paper of a linked
list and work through the code using the diagram.

5.7 Deleting a node from the list

When it comes to deleting nodes, we have three choices:

1. Delete a node from the start of the list.


2. Delete one from the end of the list.
3. Delete one from somewhere in the middle.

For simplicity, I shall just deal with deleting one from the
start or from the end. When a node is deleted, the space that
it took up should be reclaimed. Otherwise the computer will
eventually run out of memory space. This is done with the
delete instruction:

delete temp; // Release the memory pointed to by temp

However, we can't just delete the nodes willy-nilly as it would


break the chain. We need to reassign the pointers and then
delete the node at the last moment. Here is how we go about
deleting the first node in the linked list:

temp = start_ptr; // Make the temporary pointer


// identical to the start pointer
___________________________________________________________
120
Chapter 5. Linked Lists. ___________________________________________________

Now that the first node has been safely tagged (so that we
can refer to it even when the start pointer has been
reassigned), we can move the start pointer to the next node
in the chain:

start_ptr = start_ptr->nxt; // Second node in chain.

delete temp; // out original start node

Here is the function that deletes a node from the start:

void delete_start_node()
{
node *temp;
temp = start_ptr;
start_ptr = start_ptr->next;
delete temp;
}

Deleting a node from the end of the list is harder, as the


temporary pointer must find where the end of the list is by
hopping along from the start. This is done using code that is
almost identical to that used to insert a node at the end of
the list. It is necessary to maintain two temporary pointers,
temp1 and temp2. The pointer temp1 will point to the last
node in the list and temp2 will point to the previous node.
We have to keep track of both as it is necessary to delete the
last node and immediately afterwards, to set the next pointer
of the previous node to NULL (it is now the new last node).
___________________________________________________________
121
Chapter 5. Linked Lists. ___________________________________________________

1. Look at the start pointer. If it is NULL, then the list is empty,


so print out a "No nodes to delete" message.
2. Make temp1 point to whatever the start pointer is pointing to.
3. If the next pointer of what temp1 indicates is NULL, then
we've found the last node of the list, so jump to step 7.
4. Make another pointer, temp2, point to the current node in the
list.
5. Make temp1 point to the next item in the list.
6. Go to step 3.
7. If you get this far, then the temporary pointer, temp1, should
point to the last item in the list and the other temporary
pointer, temp2, should point to the last-but-one item.
8. Delete the node pointed to by temp1.
9. Mark the next pointer of the node pointed to by temp2 as
NULL - it is the new last node.

Firstly, the start pointer doesn't point to NULL, so we don't


have to display a "Empty list, wise guy!" message. Let's get
straight on with step2 - set the pointer temp1 to the same as
the start pointer. The next pointer from this node isn't NULL,
so we haven't found the end node. Instead, we set the
pointer temp2 to the same node as temp1 and then move
temp1 to the next node in the list.

Going back to step 3, we see that temp1 still doesn't point


to the last node in the list, so we make temp2 point to what
temp1 points to and temp1 is made to point to the next node
along. Eventually, this goes on until temp1 really is pointing
to the last node in the list, with temp2 pointing to the
penultimate node.
___________________________________________________________
122
Chapter 5. Linked Lists. ___________________________________________________

Now we have reached step 8. The next thing to do is to


delete the node pointed to by temp1 and set the pointer
next of what temp2 indicates to NULL. The code for all
seems a lot shorter than the explanation!

void delete_end_node()
{
node *temp1, *temp2;
if (start_ptr == NULL)
cout << "The list is empty!" << endl;
else
{
temp1 = start_ptr;
while (temp1->next != NULL)
{
temp2 = temp1;
temp1 = temp1->nxt;
}
delete temp1;
temp2->nxt = NULL;
}
}

Now, the sharp-witted amongst you will have spotted a


problem. If the list only contains one node, the code above
will malfunction. This is because the function goes as far as
the temp1 = start_ptr statement, but never gets as far as
setting up temp2.

The code above has to be adapted so that if the first node is


also the last (has a NULL next pointer), then it is deleted and
the start_ptr pointer is assigned to NULL. In this case, there
is no need for the pointer temp2:
___________________________________________________________
123
Chapter 5. Linked Lists. ___________________________________________________

void delete_end_node()
{
node *temp1, *temp2;
if (start_ptr == NULL)
cout << "The list is empty!" << endl;
else
{
temp1 = start_ptr;
if (temp1->next == NULL) // This part is new!
{
delete temp1;
start_ptr = NULL;
}
else
{
while (temp1->next != NULL)
{
temp2 = temp1;
temp1 = temp1->next;
}
delete temp1;
temp2->next = NULL;
}
}
}

5.8 Traversing the List

One thing you may need to do is to navigate through the list,


with a pointer that moves backwards and forwards through
the list, like an index pointer in an array. This is certainly

___________________________________________________________
124
Chapter 5. Linked Lists. ___________________________________________________

necessary when you want to insert or delete a node from


somewhere inside the list, as you will need to specify the
position.

I will call the mobile pointer current. First of all, it is


declared, and set to the same value as the start_ptr pointer:

node *current;
current = start_ptr;

The statement above makes them both points to the same


thing.

Moving Forwards:

It's easy to get the current pointer to point to the next node
in the list (i.e. move from left to right along the list). If you
want to move current along one node, use the next field of
the node that it is pointing to at the moment:

current = current->next;

In fact, we had better check that it isn't pointing to the last


item in the list. If it is, then there is no next node to move to:

___________________________________________________________
125
Chapter 5. Linked Lists. ___________________________________________________

if (current->next == NULL)
cout << "You are at the end of list." << endl;
else
current = current->next;

Moving Backwards:

Moving the current pointer back one step is a little harder.


This is because we have no way of moving back a step
automatically from the current node. The only way to find the
node before the current one is to start at the beginning, work
our way through and stop when we find the node before the
one we are considering the moment.

We can tell when this happens, as the nxt pointer from that
node will point to exactly the same place in memory as the
current pointer (i.e. the current node).

First of all, we had better check to see if the current node is


also first the one. If it is, then there is no "previous" node to
point to. If not, check through all the nodes in turn until we
detect that we are just behind the current one.
___________________________________________________________
126
Chapter 5. Linked Lists. ___________________________________________________

if (current == start_ptr)
cout << "You are at the start of list" << endl;
else
{
node *previous; // Declare the pointer
previous = start_ptr;

while (previous->next != current)


{
previous = previous->next;
}

current = previous;
}

The else clause translates as follows: Declare a temporary


pointer (for use in this else clause only). Set it equal to the
start pointer. All the time that it is not pointing to the node
before the current node, move it along the line. Once the
previous node has been found, the current pointer is set to
that node - i.e. it moves back along the list.

Now that you have the facility to move back and forth, you
need to do something with it. Firstly, let's see if we can alter
the details for that particular node in the list:

cout << "Please enter the new name of the person: ";
cin >> current->name;
cout << "Please enter the new age of the person : ";
cin >> current->age;
cout << "Please enter the new height of the person : ";
cin >> current->height;

___________________________________________________________
127
Chapter 5. Linked Lists. ___________________________________________________

Deleting a node:

The next easiest thing to do is to delete a node from the list


directly after the current position. We have to use a
temporary pointer to point to the node to be deleted. Once
this node has been "anchored", the pointers to the remaining
nodes can be readjusted before the node on death row is
deleted. Here is the sequence of actions:

1. Firstly, the temporary pointer is assigned to the node after


the current one. This is the node to be deleted:

2. Now the pointer from the current node is made to leap-frog


the next node and point to the one after that:

3. The last step is to delete the node pointed to by temp.

___________________________________________________________
128
Chapter 5. Linked Lists. ___________________________________________________

Here is the code for deleting the node after the current one.
It includes a test at the start to test whether the current
node is the last one in the list:

if (current->next == NULL)
cout << "There is no node after current" << endl;
else
{
node *temp;
temp = current->next;
current->next = temp->next; // Could be NULL
delete temp;
}

Inserting a node:

Here is the code to add a node after the current one.

if (current->next == NULL)
add_node_at_end();
else
{
node *temp;
new temp;
get_details(temp);
// Make the new node point to the same
// thing as the current node
temp->next = current->next;
// Make the current node point to the new node
current->next = temp;
}

___________________________________________________________
129
Chapter 5. Linked Lists. ___________________________________________________

Its is assumed that the function add_node_at_end() is the


routine for adding the node to the end of the list that we
created before. This routine is called if the current pointer is
the last one in the list so the new one would be added on to
the end. Similarly, the routine get_details(temp) is a routine
that reads in the details for the new node similar to the one
defined just above.

___________________________________________________________
130
Chapter 6: Stacks and Queues. _________________________________________

Chapter 6

Stacks and Queues

In this chapter we shall examine both the stacks and


queues data structures and see why they play a prominent
role in the areas of programming languages. We show how
the stacks and queues can be made into a concrete and
valuable tool in problem solving. Various forms of lists and
their associated operations are examined and several
applications are presented.

6.1 Stack Data Structure

One of the most useful concepts in computer science is the


stack. In this section we shall examine this data structure
and see why it plays a prominent role in the areas of

131
Chapter 6: Stacks and Queues. _________________________________________

programming and programming languages. We shall


define the abstract concept of a stack and show how that
concept can be made into a concrete and valuable tool in
problem solving.

6.1.1 Definition

A stack is an ordered collection of items into which new


items may be inserted and from which items may be
deleted at one end, called the top of the stack. We can
picture a stack as in Figure 6.1.

Unlike that of the array, the definition of the stack provides


for the insertion and deletion of items, so that a stack is a
dynamic, constantly changing object. The question
therefore arises, how does a stack change? The definition
specifies that a single end of the stack is designated as the
stack top. New items may be put on top of the stack (in
which case the top of the stack moves upward to
correspond to the new highest element), or items which
are at the top of the stack may be removed (in which case
the top of the stack moves downward to correspond to the
new highest element). To answer the question, which may
is up? We must decide which end of the stack is

132
Chapter 6: Stacks and Queues. _________________________________________

designated as its top—that is, at which end items are


added or deleted. By drawing Figure 6.1 so that F is
physically higher on the page than all the other items in
the stack, we imply that F is the current top element of the
stack. If any new items are added to the stack, they are
placed on top of F, and if any items are deleted, F is the
first to be deleted. This is also indicated by the vertical
lines that extend past the items of the stack in the
direction of the stack top.

F
E
D
C
B
A

Figure 6.1: Stack containing stack terms.

Figure 6.2 is a motion picture of a stack as it expands and


shrinks with the passage of time. Figure 6.2a shows the
stack as it exists at the time of the snapshot of Figure 6.1.
In Figure 6.2b, item G is added to the stack. According to
the definition, there is only one place on the stack where it
can be placed on the top.

133
Chapter 6: Stacks and Queues. _________________________________________

J
I I I
H H H H H
top G G G G G G G
F F F F F F F F F K

134
E E E E E E E E E E E E
D D D D D D D D D D D D D G
C C C C C C C C C C C C C C C
B B B B B B B B B B B B B B B
A A A A A A A A A A A A A A A
(a) (b) (c) (d) (e) (f) (g) (h) (i) (j) (k) (l) (m) (n) (o)
Figur 2.2 Motion picture of a stack
Chapter 6: Stacks and Queues. _________________________________________

The top element on the stack is now G. As the motion


picture progresses through frames c, d, and e, items H, I,
and J are successively added onto the stack. Notice that
the last item inserted (in this case 7) is at the top of die
stack. Beginning with frame f, however, the stack begins
to shrink, as first J, then I, H, G, and F are successively
removed. At each point, the top element is removed, since
a deletion can be made only from the top. Item G could
not be removed from the stack before items J, I, and H
were gone. This illustrates the most important attribute of
a stack, that the last element inserted into a stack is the
first element deleted. Thus J is deleted before I because J
was inserted after I. For this reason a stack is sometimes
called a last-in, first-out (or LIFO) list.

Between frames j and k the stack has stopped shrinking


and begins to expand again as item K is added. However,
this expansion is short-lived, as the stack then shrinks to
only three items in frame n.

Note that there is no way to distinguish between frame a


and frame i by looking at the stack's state at the two
instances. In both cases the stack contains the identical
items in the same order and has the same stack top. No
record is kept on the stack of the fact that four items had

135
Chapter 6: Stacks and Queues. _________________________________________

been pushed and popped in the meantime. Similarly, there


is no way to distinguish between frames d and f, or j and i.
If a record is needed of the intermediate items having
been on the stack, that record must be kept elsewhere; it
does not exist within the stack itself.

In fact, we have actually taken an extended view of what


is really observed in a stack. The true picture of a stack is
given by a view from the top looking down, rather than
from a side looking in. Thus, in Figure 6.2, there is no
perceptible difference between frames h and o. In each
case the element at the top is G. Although the stack at
frame h and the stack at frame o are not equal, the only
way to determine this is to remove all the elements on
both stacks and compare them individually. Although we
have been looking at cross sections of stacks to make our
understanding clearer, it should be noted that this is an
added liberty, and there is no real provision for taking such
a picture.

6.1.2 Primitive Operations

The two changes which can be made to a stack are given


special names. When an item is added to a stack, it is

136
Chapter 6: Stacks and Queues. _________________________________________

pushed onto the stack, and when an item is removed, it is


popped from the stack. Given a stack s, and an item i,
performing the operation push (s, i) adds the item i to
the top of stack s.

Similarly, the operation pop(s) removes the top element


and returns it as a function value. Thus the assignment
operation i = pop(s) removes the element at the top of S
and assigns its value to i.

For example, if s is the stack of Figure 6.2, we performed


the operation push (s, G) in going from frame a to frame
b. We then performed, in turn, the following operations:

push (s, H); (frame (c))


push (s, I); (frame (d))
push (s, J); (frame (e))
pop (s); (frame (f))
pop (s); (frame (g))
pop (s); (frame (h))
pop (s); (frame (i))
pop (s); (frame (j))
push (s, K); (frame (k))
pop (s); (frame (l))
pop (s); (frame (m))
pop (s); (frame (n))
push (s, G); (frame (o)).

Because of the push operation adds elements to a stack, a


stack is sometimes called a pushdown list.

137
Chapter 6: Stacks and Queues. _________________________________________

There is no upper limit on-the number of items that may


be kept in a stack, since the definition does not specify
how many items are allowed in the collection. Pushing
another item onto a stack merely produces a larger
collection of items. However, if a stack contains a single
item and the stack is popped, the resulting stack contains
no items and is called the empty stack. Although the
push operation is applicable to any stack, the pop
operation cannot be applied to the empty stack because
such a stack has no elements to pop. Therefore, before
applying the pop operator to a stack, we must ensure that
the stack is not empty. The operation empty(s)
determines whether or not a stack s is empty. If the stack
is empty, empty(s) returns the value TRUE; otherwise it
returns the value FALSE.

Another operation that can be performed on a stack is


to determine what the top item on a stack is without
removing it. This operation is written stacktop(s) and
returns the top element of stack s. The operation
stacktop(s) is not really a new operation, since it can be
decomposed into a pop and a push.

i = stacktop(s);

is equivalent to

138
Chapter 6: Stacks and Queues. _________________________________________

i = pop(s);

push (s, i);

Like the operation pop, stacktop is not defined for an


empty stack. The result of an illegal operation attempt to
pop or access an item from an empty stack is called
underflow. Underflow can be avoided by ensuring that
empty(s) is false before attempting the operation pop(s)
or stacktop(s).

6.1.3 Representing Stacks

Before programming a problem solution that uses a stack,


we must decide how to represent a stack using the data
structures that exist in our programming language. In this
section, we are concerned with the actual implementation
of the stacks.

As we shall see, there are several ways to represent a


stack. We first consider the simplest of these, and then we
introduce the other possible representations. Each of
them, however, is merely an implementation of the
concept introduced in Section 6.1.1. Each has advantages
and disadvantages in terms of how close it comes so

139
Chapter 6: Stacks and Queues. _________________________________________

mirroring the abstract concept of a stack and how much


effort must be made by the programmer and the computer
in using it.

A stack is an ordered collection of items, and C already


contains a data type that is an ordered collection of items:
the array. Whenever a problem solution calls for the use of
a stack, therefore, it is tempting to begin a program by
declaring a variable stack as an array. However, a stack
and an array are two entirely different things. The number
of elements in an array is fixed and is assigned by the
declaration for the array. In general, the user cannot
change this number. A stack, on the other hand, is
fundamentally a dynamic object whose size is constantly
changing as items are popped and pushed.

However, although an array cannot be a stack, it can be


the home of a stack. That is, an array can be declared
large enough for the maximum size of the stack. During
the course of program execution, the stack can grow and
shrink within the space reserved for it. One end of the
array is the fixed bottom of the stack, while the top of the
stack constantly shifts as items are popped and pushed.
Thus, another field is needed that, at each point during
program execution keeps track of the current position of
the top of the stack.

140
Chapter 6: Stacks and Queues. _________________________________________

A stack in C++ may therefore be declared as a structure


containing two objects: an array to hold the elements of
the stack, and an integer to indicate the position of the
current stack top within the array. This may be done for a
stack of integers by the declarations

#define STACKSIZE 100


struct stack
{
int top;
int items[STACKSIZE];
};

Once this is done, an actual stack s may be declared by

struct stack s;

Here, we assume that the elements of the stack s


contained in the array s.items are integers and that the
stack will at no time contain more than STACKSIZE
integers. In this example STACKSIZE is set to 100 to
indicate that the stack can contain 100 elements (items[0]
through items[99]).

The identifier top must always be declared as an integer,


since its value represents the position within the array
items of the topmost stack element. Therefore, if the value
of s.top is 4, there are five elements on the stack:

141
Chapter 6: Stacks and Queues. _________________________________________

s.items[0], s.items[1], s.items[2], s.items[3], and


s.items[4]. When the stack is popped, the value of s.top is
changed to 3 to indicate that there are now only four
elements on the stack and that s.items[3] is the top
element. On the other hand, if a new object is pushed
onto the stack, the value of s.top must be increased by 1
to 5 and the new object inserted into s.items[5].

There is, of course, no reason to restrict a stack to contain


only integers; items could just as easily have been
declared asfloat items[STACKSIZE] or char
items[STACKSIZE], or whatever other type we might wish
to give to the elements of the stack. In fact, should the
need arise; a stack can contain objects of different types
by using C unions. Thus

#define STACKSIZE 100


#define INT 1
# define FLOAT 2
# define STRING 3

struct stackelement
{
int etype; /* etype equals INT, FLOAT,or STRING */
/* depending on the type of the */
/* corresponding element. */
union
{
int ival;
142
Chapter 6: Stacks and Queues. _________________________________________

float fval;
char *pval; /* pointer to a string */
} element;
};

struct stack
{
int top;
struct stackelement items[STACKSIZE];
};

defines a stack whose items may be either integers,


floating-point numbers, or strings, depending on the value
of the corresponding etype. Given a stack s declared by

struct stack s;

We could print the top element of the stack as follows:

struct stackelement se;


…………………
se = s.items[s.top];
switch (se.etype)
{
case INTGR : cout << se.ival << endl;
case FLT : cout << se.fval << endl;
case STRING : cout << se.pval<< endl;
} /*end switch */

For simplicity, in the remainder of this section we assume


that a stack is declared to have only homogeneous
elements (so that unions are not necessary).

143
Chapter 6: Stacks and Queues. _________________________________________

6.1.4 Representing Stack Operations

In this section, we are concerned with the actual


implementation of the stack operations.

6.1.4.1 Implementing Empty Operation

The empty stack contains no elements and can therefore


be indicated by top equaling -1. To initialize a stack s to
the empty state, we may initially execute s.top = -1;.

To determine, during the course of execution, whether or


not a stack is empty the condition s.top == -1 may be
tested in an if statement as follows:

if (s.top == -1)
/* stack is empty */
else
/* stack is not empty */

Alternatively, we may write a function that returns TRUE if


the stack is empty and FALSE if it is not empty, as follows:

int empty(struct stack *ps)


{
if (ps->top = -1)

144
Chapter 6: Stacks and Queues. _________________________________________

return(TRUE);
else
return(FALSE);
} /* end empty */

Once this function exists, a test for the empty stack is


implemented by the statement

if (empty (&s))
/* stack is empty */
else
/* stack is not empty */

Note the difference between the syntax of the call to


empty in the algorithm of Section 6.1 arid in the program
segment here. In the algorithm, s represented a stack and
the call to empty was expressed as

empty(s)

Since parameters in C are passed by value, the only way


to modify the argument passed to a function is to pass the
address of the argument rather than the argument itself.
Further, the original definition of C and many older C
compilers do not allow a structure to be passed as an
argument even if its value remains unchanged. Thus in
functions such as pop and push (which modify their
structure arguments), as well as empty (which does not),

145
Chapter 6: Stacks and Queues. _________________________________________

we adopt the convention that we pass the address of the


stack structure, rather than the stack itself.

You may wonder why we bother to define the function


empty when we could just as easily write if s. top = = - 1
each time that we want to test for the empty condition.
The answer is that we wish to make our programs more
comprehensible and to make the use of a stack
independent of its implementation. Once we understand
the stack concept, the phrase “empty(&s)” is more
meaningful than the phrase “s.top = = - 1.” If we should
later introduce a better implementation of a stack, so that
“s.top = = - 1” becomes meaningless, we would have to
change every reference to the field identifier s.top
throughout the entire program. On the other hand, the
phrase “empty(&s)” would still retain its meaning, since it
is an inherent attribute of the stack concept rather than of
an implementation of that concept. All that would be
required to revise a program to accommodate a new
implementation of the stack would be a possible revision
of the declaration of the structure stack in the main
program and the rewriting of the function empty. (It is
also possible that the form of the call to empty would have
to be modified so that it does not use an address.)

146
Chapter 6: Stacks and Queues. _________________________________________

Aggregating the set of implementation-dependent trouble


spots into small, easily identifiable units is an important
method of making a program more understandable and
modifiable. This concept is known as modularization, in
which individual functions are isolated into low-level
modules whose properties are easily verifiable. These low-
level modules can then be used by more complex routines,
which do not have to concern themselves with the details
of the low-level modules but only with their function. The
complex routines may themselves then be viewed as
modules by still higher-level routines that use them
independently of their internal details.

A programmer should always be concerned with the


readability of the code he or she produces. A small amount
of attention to clarity will save a large amount of time in
debugging. Large- and medium-sized programs will almost
never be correct the first time they are run. If precautions
are taken at the time that a program is written to ensure
that it is easily modifiable and comprehensible, the total
time needed to get the program to run correctly is reduced
sharply.

For example, the if statement in the empty function could


be replaced by the shorter, more efficient statement:

147
Chapter 6: Stacks and Queues. _________________________________________

return (ps->top == -1);

This statement is precisely equivalent to the longer


statement

if (ps->top == -1)
return(TRUE);
else
return(FALSE);

This is because the value of the expression ps - >top == -


1 is TRUE if and only if the condition ps - > top = = -1 is
TRUE. However, someone who reads a program will
probably be much more comfortable reading the if
statement. Often you will find that if you use “tricks” of the
language in writing programs, you will be unable to
decipher your own programs after putting them aside for a
day or two.

Although it is true that the C programmer is often


concerned with economy of code, it is also important to
consider the time that will no doubt be spent in
debugging. The nature professional, whether in C/C++ or
other language, is constantly concerned with the proper
balance between code economy and code clarity.

148
Chapter 6: Stacks and Queues. _________________________________________

6.1.4.2 Implementing pop Operation

The possibility of underflow must be considered in


implementing the pop operation, since the user may
inadvertently attempt to pop an element from an empty
stack. Of course, such an attempt is illegal and should be
avoided. However, if such an attempt should be made the
user should be informed of the underflow condition. We
therefore introduce a function pop that performs the
following three actions:

 If the stack is empty, print a warning message and


halt execution.
 Remove the top element from the stack.
 Return this element to the calling program.

We assume that the stack consists of integers, so that the


pop operation can be implemented as a function. This
would also be the case if the stack consisted of some other
type of simple variable. However, if a stack consists of a
more complex structure (for example, a structure or a
union), the pop operation would either be implemented as
returning a pointer to a data element of the proper type
(rather than the data element itself), or the operation
would be implemented with the popped value as a

149
Chapter 6: Stacks and Queues. _________________________________________

parameter (in which case the address of the parameter


would be passed rather than the parameter, so that the
pop function could modify the actual argument).

int pop(struct stack *ps)


{
if (empty(ps))
{
cout << “stack underflow”;
exit(l);
} /* end if */
else
return(ps->items[ps->top--]);
} /* end pop */

Note that ps is already a pointer to a structure of type


stack; therefore, the address operator “&” is not used in
calling empty. In all applications in C, one must always
distinguish between pointers and actual data objects.

Let us look at the pop function more closely. If the stack is


not empty, the top element of the stack is retained as the
returned value. This element is then removed from the
stack by the expression ps -> top--. Assume that when
pop is called, ps -> top equals 87; that is, there are 88
items on the stack. The value of ps -> items[87] is
returned, and the value of ps -> top is changed to 86.
Note that ps -> items[87] still retains its old value; the

150
Chapter 6: Stacks and Queues. _________________________________________

array ps -> items remains unchanged by the call to pop.


However, the stack is modified, since it now contains only
87 elements rather than 88. Recall that an array and a
stack are two different objects. The array only provides a
home for the stack. The stack itself contains only those
elements between the zeroth element of the array and the
topth element. Thus reducing the value of ps -> top by 1
effectively removes an element from the stack. This is true
despite the fact that ps -> items[87] retains its old value.

To use the pop function, the programmer can declare int x


and write

x = pop (&s);

x then contains the value popped from the stack. If the


intent of the pop operation is not to retrieve the element
on the top of the stack but only to remove it from the
stack, the value of x will not be used again in the program.

6.1.4.3 Implementing Push Operation

Let us now examine the push operation. It seems that this


operation should be quite easy to implement using the

151
Chapter 6: Stacks and Queues. _________________________________________

array representation of a stack. A first attempt at a push


procedure might be the following:

void push(struct stack *ps, int x)


{
ps->items[++(ps->top)] = x;
return;
} /* end push */

This routine makes room for the item x to be pushed onto


the stack by incrementing s.top by 1, and then inserts x
into the array s. items.

The routine directly implements the push operation


introduced in Section 6.1.1. Yet, as it stands, it is quite
incorrect. It allows a subtle error to creep in, caused by
using the array representation of the stack. Recall that a
stack is a dynamic structure that is constantly allowed to
grow and shrink and thus change its size. An array, on the
other hand, is a fixed object of predetermined size. Thus,
it is quite conceivable that a stack may outgrow the array
that was set aside to contain it. This occurs when the array
is full, that is, when the stack contains as many elements
as the array and an attempt is made to push yet another
element onto the stack. The result of such an attempt is
called an overflow.

152
Chapter 6: Stacks and Queues. _________________________________________

Assume that the array s.items is full and that the C push
routine is called. Remember that the first array position is
0 and the arbitrary size (STACKSIZE) chosen for the array
s.items is 100. The full array is then indicated by the
condition s.top == 99, so that position 99 (the 100th
element of the array) is the current top of the stack. When
push is called, s.top is increased to 100 and an attempt is
made to insert x into x.items[100].

Of course, the upper bound of s.items is 99, so that this


attempt at insertion results in an unpredictable error,
depending on the contents of the memory location
following the last array position. An error message may be
produced that is unlikely to relate to the cause of the
error.

The push procedure must therefore be revised so that it


reads as follows:

void push(struct stack *ps, int x)


{
if (ps->top == STACKSIZE -1)
{
cout << “stack overflow”;
exit(l);
}
else
ps->items[++(ps->top)] = x;

153
Chapter 6: Stacks and Queues. _________________________________________

return;
} /* end push */

Here, we check whether the array is full before attempting


to push another element onto the stack. The array is full if
ps -> top == stacksize - 1.

6.1.4.4 Implementing stacktop Operation

Let us now look at our last operation on stacks,


stacktop(s), which returns the top element of a stack
without removing it from the stack. As noted in the last
section, stacktop is not really a primitive operation because
it can be decomposed into the two operations:

x = pop(s);

push (s, x);

However, this is a rather awkward way to retrieve the top


element of a stack. Why not ignore the decomposition
noted above and directly retrieve the proper value? Of
course, a check for the empty stack and underflow must
then be explicitly stated, since the test is no longer
handled by a call to pop.

154
Chapter 6: Stacks and Queues. _________________________________________

We present a function stacktop for a stack of integers as:

int stacktop(struct stack *ps)


{
if (empty(ps))
{
cout << “stack underflow”;
exit(l);
}
else
return(ps->items[ps->top]);
} /*end stacktop */

You may wonder why we bother writing a separate routine


stacktop when a reference to s.items[s.top] would serve
just as well. There are several reasons for this.

First, the routine stacktop incorporates a test for


underflow, so that no mysterious error occurs if the stack
is empty.

Second, it allows the programmer to use a stack without


worrying about its internal makeup.

Third, if a different implementation of a stack is


introduced, the programmer need not comb through all
the places in the program that refer to s.items[s.top] to
make those references compatible with the new
implementation. Only the stacktop routine would need to
be changed.

155
Chapter 6: Stacks and Queues. _________________________________________

6.2 Queue Data Structures

This section introduces the queue and the priority queue,


two important data structures often used to simulate real
world situations.

6.2.1 Definition

A queue is an ordered collection of items from which


items may be deleted at one end (called the front of the
queue) and into which items may be inserted at the other
end (called the rear of the queue).

Figure 6.4a illustrates a queue containing three elements


A, B, and C. A is at the front of the queue and C is at the
rear. In Figure 6.4b an element has been deleted from the
queue. Since elements may be deleted only from the front
of the queue, A is removed and B is now at the front. In
Figure 6.4c, when items D and E are inserted, they must
be inserted at the rear of the queue.

Since D was inserted into the queue before E, it will be


removed earlier. The first element inserted into a queue is
the first element to be removed. For this reason a queue is

156
Chapter 6: Stacks and Queues. _________________________________________

sometimes called a fifo (first-in, first-out) list as opposed


to a stack, which is a lifo (last-in, first-out) list. Examples
of a queue abound in the real world. A line at a bank or at
a bus stop and a group of cars waiting at a toll booth are
all familiar examples of queues.

Front

A B C

Rear
(a)
Front

B C

Rear
(B)
Front

B C D E

Rear
(c)

Figure 6.4: The Queue

6.2.2 Primitive Operations

Three primitive operations can be applied to a queue. The


operation insert(q, x) inserts item x at the rear of the

157
Chapter 6: Stacks and Queues. _________________________________________

queue q. The operation x = remove(q) deletes the front


element from the queue q and sets x to its contents. The
third operation, empty(q), returns false or true
depending on whether or not the queue contains any
elements. Assuming the queue is initially empty, the queue
in Figure 6.4 can be obtained by the following sequence of
operations:

insert(q, A);

insert(q, B);

insert(q, C); (Figure 6.4a)

x = remove(q); (Figure 6.4b; x is set to A)

insert(q, D);

insert(q, E); (Figure 6.4c)

The insert operation can always be performed, since


there is no limit to the number of elements a queue may
contain. The remove operation, however, can be applied
only if the queue is nonempty; there is no way to remove
an element from a queue containing no elements. The
result of an illegal attempt to remove an element from an
empty queue is called underflow. The empty operation
is, of course, always applicable.

158
Chapter 6: Stacks and Queues. _________________________________________

6.2.3 Queues Representation

How shall a queue be represented in C/C++? One idea is


to use an array to hold the elements of the queue and to
use two variables, front and rear, to hold the positions
within the array of the first and last elements of the
queue. We might declare a queue q of integers by

#define MAXQUEUE 100


struct queue
{
int items[MAXQUEUE];
int front, rear;
} q;

Of course, using an array to hold a queue introduces the


possibility of overflow if the queue should grow larger than
the size of the array. Ignoring the possibility of underflow
and overflow for the moment, the operation insert(q, x)
could be implemented by the statements

q.items [++q.rear] = x;

and the operation x = remove(q) could be implemented by

x = q.items [q.front++];

Initially, q.rear is set to - 1, and q.front is set to 0. The


queue is empty whenever q.rear < q.front. The number of

159
Chapter 6: Stacks and Queues. _________________________________________

elements in the queue at any time is equal to the value of


q.rear - q.front + 1.

Let us examine what might happen under this


representation. Figure 6.5 illustrates an array of five
elements used to represent a queue (that is, MAXQUEUE
equals 5).

q.items q.items
4 4
3 3
2 2 C q.rear = 2
1 1 B
q.front = 0
0 0 A q.front = 0
q.rear = -1
(a) (b)

q.items q.items
4 4 E q.rear = 4
3 3 D
2 C q.front = q.rear = 2 2 C q.front = 2
1 1
0 0

(c) (d)

Figure 6.5

Initially, the queue is empty (Figure 6.5a). In Figure 6.5b


items A, B, and C have been inserted. In Figure 6.5c two
items have been deleted, and in Figure 6.5d two new

160
Chapter 6: Stacks and Queues. _________________________________________

items, D and E, have been inserted. The value of q.front is


2, and the value of q.rear is 4, so that there are only 4-
2+1 =3 elements in the queue. Since the array contains
five elements, there should be room for the queue to
expand without the worry of overflow.

However, to insert F into the queue, q.rear must be


increased by 1 to 5 and q.items[5] must be set to the
value F. But q.items is an array of only five elements, so
that the insertion cannot be made.

Raised Problem:

It is possible to reach the absurd situation where the


queue is empty, yet no new element can be inserted (see
if you can come up with a sequence of insertions and
deletions to reach that situation). Clearly, the array
representation outlined in the foregoing is unacceptable.

Solution:

One solution is to modify the remove operation so that


when an item is deleted, the entire queue is shifted to the
beginning of the array. The operation x = remove(q)
161
Chapter 6: Stacks and Queues. _________________________________________

would then be modified (again, ignoring the possibility of


underflow) to

x = q.items[0];

for (i = 0; i < q.rear; i++)

q.items[i] = q.iterms[i+1];

q.rear--;

The queue need no longer contain a front field, since the


element at position 0 of the array is always at the front of
the queue. The empty queue is represented by the queue
in which rear equals -1.

This method, however, is too inefficient. Each deletion


involves moving every remaining element of the queue. If
a queue contains 500 or 1000 elements, this is clearly too
high a price to pay. Further, the operation of removing an
element from a queue logically involves manipulation of
only one element: the one currently at the front of the
queue. The implementation of that operation should reflect
this and should not involve a host of extraneous operations

Another solution is to view the array that holds the


queue as a circle rather than as a straight line. That is, we
imagine the first element of the array (that is, the element

162
Chapter 6: Stacks and Queues. _________________________________________

at position 0) as immediately following its last element.


This implies that even if the last element is occupied, a
new value can be inserted behind it in the first element of
the array as long as that first element is empty.

Let us look at an example. Assume that a queue of a five


element array contains three items in positions 2, 3, and 4.
This is the situation of Figure 6.5d reproduced as Figure
6.6a.

q.items q.items
4 E q.rear = 4 4 E
3 D 3 D
2 C q.front = 2 2 C q.front = 2
1 1
0 0 F q.rear = 0

(a) (b)

q.items q.items
4 E q.front = 4 4 E q.front = 4
3 3
2 2
1 1 G q.rear = 1
0 F q.rear = 0 0 F

(c) (d)

q.items
4
3
2
1 G q.rear = 1
0 F q.front = 0

(e)

Figure 6.6

163
Chapter 6: Stacks and Queues. _________________________________________

Although the array is not full, its last element is occupied.


If item F is now inserted into 'he queue, it can be placed in
position 0 of the array, as shown in Figure 6.6b The first
item of the queue is in q.items[2], which is followed in the
queue by q.items[3], q.items[4] and q.items[0]. Figure
6.6c, d, and e show the status of the queue as first two
items C and D are deleted, then G is inserted, and finally E
is deleted.

Unfortunately, it is difficult under this representation to


determine when the queue is empty. The condition q.rear
< q.front is no longer valid as a test for the empty queue,
since Figure 6.6b. c, and d all illustrate situations in which
the condition is true yet the queue is not empty.

One way of solving this problem is to establish the


convention that the value of q.front is the array index
immediately preceding the first element of the queue
rather than the index of the first element itself. Thus since
q.rear is the index of the last element of the queue, the
condition q.front = = q.rear implies that the queue is
empty.

A queue of integers may therefore be declared and


initialized by

164
Chapter 6: Stacks and Queues. _________________________________________

#define MAXQUEUE 100


struct queue
{
int items[MAXQUEUE];
int front, rear;
};
struct queue q;
q.front = q.rear = MAXQUEUE-1;

Note that q.front and q.rear are initialized to the last index
of the array, rather than to -1 or 0, because the last
element of the array immediately precedes the first one
within the queue under this representation. Since q.rear
equals q.front, the queue is initially empty.

6.2.4 Implementing Queue Operations

Empty Operation:

The empty function may be coded as

int empty(struct queue *pq)


{
return ((pq->front ==pq->rear) ? TRUE : FALSE);
} /* end empty */

165
Chapter 6: Stacks and Queues. _________________________________________

Once this function exists, a test for the empty queue is


implemented by the statement

if (empty(&q))
/* queue is empty */
else
/* queue is not empty */

Remove Operation:

The operation remove(q) may be coded as

int remove(struct queue *pq)


{
if (empty(pq))
{
printf("queue underflow");
exit(l);
} /* end if */

if (pq->front == MAXQUEUE-1)
pq->front = 0;
else
(pq->front)++;
return (pq->items[pq->front]);

} /* end remove */

166
Chapter 6: Stacks and Queues. _________________________________________

Note that pq is already a pointer to a structure of type


queue, so the address operator "&" is not used in calling
empty within remove. Also note that pq-> front must be
updated before an element is extracted.

Of course, often an underflow condition is meaningful and


serves as a signal for a new phase of processing. We may
wish to use a function remvandtest, whose header is

void remvandteststruct (queue *pq, int *px, int *pund)

If the queue is nonempty, this routine sets *pund to FALSE


and *px to the element removed from the queue. If the
queue is empty, so that underflow occurs, the routine sets
*pund to TRUE. The coding of the routine is left to the
reader.

Insert Operation

The insert operation involves testing for overflow, which


occurs when the entire array is occupied by items of the
queue and an attempt is made to insert yet another
element into the queue. For example, consider the queue
of Figure 6.7.

167
Chapter 6: Stacks and Queues. _________________________________________

q.items q.items
4 E q.rear = 4 4 E
3 D 3 D
2 C 2 C
1 q.front = 1 1 q.front = 1
0 0 F q.rear = 0

(a) (b)

q.items
4 E
3 D
2 C
1 G q.front = q.rear = 1
0 F

(c)

Figure 6.7

There are three elements in the queue of Figure 6.7a: C,


D, and E in q.items[2], q.items[3], and q.items[4],
respectively. Since the last item of the queue occupies
q.items[4], q.rear equals 4. Since the first element of the
queue is in q.items[2], qfront equals 1. In Figure 6.7b and
c, items F and G are inserted into the queue. At that point,
the array is full and an attempt to perform any more
insertions causes an overflow. But this is indicated by the
fact that q.front equals q. rear, which is precisely the
indication for underflow.

One solution is to sacrifice one element of the array and to


allow a queue to grow only as large as one less than the

168
Chapter 6: Stacks and Queues. _________________________________________

size of the array. Thus, if an array of 100 elements is


declared as a queue, the queue may contain up to 99
elements. An attempt to insert a hundredth element into
the queue causes an overflow. The insert routine may then
be written as follows:

void insert(struct queue *pq, int x)


{
/* make room for new element */
if (pq->rear == HAXQUEUE-1)
pq->rear = 0;
else
(pq->rear)++;

/* check for overflow */


if (pq->rear = pq->front) {
cout << “queue overflow”;
exit(l);
} /* end if */

pq->items[pq->rear] = x;
return;

} /* end insert */

The test for overflow in insert occurs after pq→rear has


been adjusted, whereas the test for underflow in remove
occurs immediately upon entering the routine, before
pq→front is updated.

169
Chapter 6: Stacks and Queues. _________________________________________

6.3 Drawbacks of using sequential storage

What are the drawbacks of using sequential storage to


represent stacks and queues? One major drawback is that
a fixed amount of storage remains allocated to the stack or
queue even when the structure is actually using a smaller
amount or possibly no storage at all. Further, no more
than that fixed amount of storage may be allocated, thus
introducing the possibility of overflow.

Assume that a program uses two stacks implemented in


two separate arrays, s1.items and s2.iterns. Further,
assume that each of these arrays has 100 elements. Then
despite the fact that 200 elements are available for the
two stacks, neither can grow beyond 100 items. Even if
the first stack contains only 25 items, the second cannot
contain more than 100.

170
References. __________________________________________________________

REFERENCES
1- D. S. Malik, and Dr. D. S. Malik, “Data Structures
Using C++”, Course Technology, 2003.

2- Adam Drozdek, “Data Structures and Algorithms in


C++”, Second Edition, Brooks/Cole, A Division of
Thomson Learning, 2001.

3- Y. Langsam, M. J. Augenstein, and A. M. Tenenbaum,


“Data Structures Using C and C++”, Second Edition,
Prentice-Hall International Inc., 1995.

4- M. A. Weiss, “Data Structures and Algorithms


Analysis in C++”, Addison-Wesley Publishing
Company, 1994.

171

You might also like