MSc in Mathematics and Finance, Imperial College London Dr Robert Nu ̈rnberg
Introduction
• C only:
• C++:
– Tony Zhang and John Southmayd, Teach Yourself C in 24 Hours, 2000
– Bradley Jones and Peter G. Aitken, Teach Yourself C in 21 Days, 2003
– Brian W. Kernigham and Dennis M. Ritchie, The C Programming language, 1988 – Richard Johnsonbaugh and Martin Kalin, C for Scientists and Engineers, 1996
– Steven R. Lerman, Problem solving and computations for scientists and engineers,
an introduction using C, 1993
– publications.gbdirect.co.uk/c book (The C Book online)
– Steve Oualline, Practical C++ Programming, 1995
– Herbert Schildt, Teach yourself C++, 1992
– Jesse Liberty, Teach yourself C++ in 24 hours, 1999
– www.cplusplus.org, www.cplusplus.com, www.cppreference.com
Computing in C++
Part I : An introduction to C
Dr Robert Nu ̈rnberg
This course will give an introduction to the programming language C. It runs in the autumn term and is addressed to students with no or little programming experience in C. Attending this course will enable you to write simple C/C++ programs and complete possible practical assignments of other courses that run alongside this course. In this term, there will be no separate coursework assignments for the course “Computing in C++”.
The course will be followed in the second term by an introduction to Object Oriented Programming in C++. That is why from the start, we will be using a C++ compiler, even though the programs are really written in C or at least in “C-like C++”. Concepts that are unique to C++ and do not form part of the C programming language will be marked with a footnote like this (⇤). This will help you adapt your code to a C compiler, if you ever have to.
Throughout this script, new concepts and ideas will be illustrated with easy to understand example programs. These examples can be compiled as they are and, if your pdf-viewer allows you to, you can even copy and paste the code into your editor without having to type it in yourself.
These lecture notes are organised as follows. Chapter 1 gives a short overview over the main techniques and features of C and is designed to enable you to start programming straight away. The remaining chapters will then cover each topic in more detail.
Recommended books and sites
Compilers
• Windows: · Microsoft Visual C++ .NET with Integrated Development Environment (IDE) ·GNUC++ compiler(g++)aspartofCygwinorMinGW,withoutIDE
· Dev-C++ – free compiler/IDE that is GNU compatible
• Linux: · GNU C++ compiler (g++) – part of any distribution · Intel C++ compiler (icc) – free for students
• Mac: · Xcode – free compiler/IDE that is GNU compatible
• Windows/Linux/Mac: · Code::Blocks – free compiler/IDE that is GNU compatible
· NetBeans – free compiler/IDE that is GNU compatible ⇤A footnote to highlight parts that are not pure C.
Computing in C++ , Part I Dr Robert Nu ̈rnberg Contents
1 Basics
4
1.1 “HelloWorld!” …………………………………… 4 1.1.1 Waitingforinput………………………………. 4
1.2 Comments……………………………………… 4
1.3 Variablesandtypes…………………………………. 5
1.4 Userinput……………………………………… 5
1.5 Controlflow…………………………………….. 6
1.6 Basicoperators…………………………………… 7
1.7 Arrays……………………………………….. 7
1.8 Functions ……………………………………… 8 1.8.1 Mathematicalfunctions …………………………… 9
1.9 Outputcontrolcharacters ……………………………… 9
1.10FileI/O………………………………………. 10 1.11Namespaces…………………………………….. 11
2 Variables 11
2.1 Basictypes …………………………………….. 11
2.2 Internalrepresentationofvariables …………………………. 12 2.2.1 Precisionproblemsandroundingerrors …………………… 12 2.2.2 Typeconversion ………………………………. 13 2.2.3 Roundingfunctions……………………………… 14
2.3 Namesofvariables …………………………………. 14
2.4 Initializationofvariables………………………………. 15
2.5 Scopeofavariable…………………………………. 15 2.5.1 Globalvariables ………………………………. 16
2.6 Arraysandstrings …………………………………. 16 2.6.1 Initializationofarrays ……………………………. 16 2.6.2 StringsinC…………………………………. 17 2.6.3 Multidimensionalarrays…………………………… 18
2.7 Constants……………………………………… 19
3 Control flow 19
3.1 Thewhileloop…………………………………… 20 3.2 Theforloop ……………………………………. 20 3.3 Thedo-whileloop…………………………………. 21 3.4 Theif-elsestatement ………………………………. 21 3.5 Theswitchstatement……………………………….. 22 3.6 breakandcontinue………………………………… 23
4 Operators 24
4.1 Arithmeticoperators………………………………… 24
4.2 Precedenceofoperators ………………………………. 25
4.3 Assignmentoperator………………………………… 25
4.3.1 Specialassignmentoperators ………………………… 25
4.4 Logicaloperators………………………………….. 26
4.5 Unaryoperators ………………………………….. 27
5 Functions 27
5.1 Passingbyvalue ………………………………….. 29 5.2 Thereturnstatement……………………………….. 29 5.3 Passingarrays……………………………………. 29 5.4 Passingbyreference ………………………………… 30 5.5 Keywordconst…………………………………… 31 5.6 Staticvariables…………………………………… 31 5.7 Declarationoffunctions ………………………………. 32
Computing in C++ , Part I Dr Robert Nu ̈rnberg
6 Pointers 33
6.1 Pointersyntax …………………………………… 33
6.2 Passingbypointer …………………………………. 34 6.2.1 Keywordconst……………………………….. 34
6.3 TheNULLpointer………………………………….. 35 6.3.1 ifstatementandpointers………………………….. 35
6.4 Pointersandarrays…………………………………. 35 6.4.1 Pointerarithmetic ……………………………… 35 6.4.2 Di↵erencesbetweenpointersandarrays …………………… 36
6.5 Pointerstopointers…………………………………. 36
6.6 Dynamicmemoryallocation…………………………….. 37 6.6.1 Segmentationfaultsandfatalexceptionerrors . . . . . . . . . . . . . . . . . . . . 38 6.6.2 Dynamicallocationofmultidimensionalarrays . . . . . . . . . . . . . . . . . . . . 38
6.7 Pointerstofunctions………………………………… 40
7 Structures 41
7.1 Structuresyntax ………………………………….. 41 7.2 Operationsonstructures………………………………. 42 7.3 Pointersandstructures ………………………………. 42 7.4 Passingstructures …………………………………. 42 7.5typedef………………………………………. 42
7.5.1 typedefand#define……………………………. 43
7.6 Selfreferentialstructures………………………………. 43
7.7 Examples ……………………………………… 45
7.7.1 Sparsematrices……………………………….. 45 7.7.2 Binarytrees…………………………………. 46
8 Advanced material 47
8.1enumandunion…………………………………… 47
8.2 Conditionals…………………………………….. 48
8.3 Preprocessordirectives……………………………….. 48
8.3.1 #define…………………………………… 48 8.3.2#undef…………………………………… 49 8.3.3 #if,#endif,#elseand#elif………………………. 49 8.3.4 #ifdefand#ifndef…………………………….. 50
8.4 Headerfiles…………………………………….. 50
8.5 Commandlineparameters……………………………… 51
Computing in C++ , Part I Dr Robert Nu ̈rnberg 1 Basics
A C/C++ program is nothing other than a text file with a very special syntax. In order to distinguish these files from other text files, they are usually given the ending .cpp (for windows) or .cc (for linux). These files are also called source files, as they are the source for the actual program.
A C/C++ compiler is then needed to translate the source files into an executable (or binary) file format, that can be executed by the operating system. This binary file is then the actual program. When working under windows in most cases you will not be aware of the executable file, as the compiler will allow you to run the code immediately after compilation.
The following syntax guidelines you need to know, before you can start to write your first C++ program. Each C/C++ program needs to have a main() program. Inside this main() program we define what the code is supposed to do. The commands that are to be executed are grouped together inside some “{ }” brackets. Each statement in C must be terminated by a semi-colon “;”. See the first example programs in the next subsection.
1.1 “Hello World!”
There are two ways to output things on the screen. One uses the C++ stream cout (on the left) and the other uses the C function printf() (on the right). (⇤)
#include
using namespace std;
int main() {
cout << "Hello World!" << endl;
return 0;
}
#include
int main() {
printf(“Hello World!\n”);
return 0;
}
During the course, I may make use of both of these possibilities and you are free to use whatever you prefer. Note, however, that in the second term we will almost exclusively be using the C++ stream cout.
1.1.1 Waiting for input
Under Windows, the terminal screen often disappears quickly after the last output of the program. To prevent this, you can make the program wait until you press Enter as follows.
#include
using namespace std;
int main() {
cout << "Press Enter to continue" << endl;
cin.get();
return 0;
}
1.2 Comments
#include
int main() {
printf(“Press Enter to continue\n”);
scanf(“%*c”);
return 0;
}
Comments are parts of the code that will be ignored by the compiler. They are used to add descriptions and explanations to the source code. One can either comment parts or all of a single line with //, or comment bigger parts of the code by using /* at the beginning and */ at the end. Here an example. (†)
#include
using namespace std;
/***************************************
* Example for using Comments in C/C++ *
***************************************/
⇤The stream cout is unique to C++. Also note that when including header files in a pure C program, the syntax is e.g. #include
†Commenting code with // is not part of ANSI C. However, most C compilers accept it.
Computing in C++ , Part I
Dr Robert Nu ̈rnberg
1.3
int main() {
int i;
int sum;
sum = 0;
for (i = 0; i<10; i++) {
sum = sum + i; }
cout << "The sum is " << sum << endl;
return 0; }
Variables and types
// an integer
/* used to add up i’s */
// a for loop
Variables are the very basis of every programming language. Without them you could not do computa- tions and you could not program anything meaningful.
Variables allow you to store and recall numerical values, much like the memory register on modern hand- held calculators. Only in C you can have almost infinitely many of these registers, and you can also name them whatever you like. All you have to do is to declare any variable you want to use, before actually using it.
In C there is a strong notion of separation of variables from di↵erent types. For instance, by default you can only assign to a variable values of the same type. So, when you introduce a variable, you have to declare its type.
A variable can be declared with the following syntax.
type variable_name;
Here type specifies the variable’s type, and variable_name defines the name under which you can use the variable in your program.
The two most important types in C are int and double. The former is used to store integer numbers and is used for e.g. counters. The latter stores floating point numbers and is used for scientific computations. Here is an example of how to use them.
#include
#include
using namespace std;
int main() {
int i,j;
double x,y;
i=5; j=2*i+3;
x = 3.14;
y = sin(x) * exp(-5*x);
// include math.h
// a declaration of two integers …
// … and two doubles
// an assignment
// use value of i for assignment of j
// an assignment of a floating point number
cout << "The value of i is " << i << ", while y = " << y << endl;
return 0; }
Note that before the variables are used they are declared. Once a variable has been declared with its name and its type, it can be assigned values, and these values can be recalled or updated later on in the program.
1.4 User input
The C++ input stream cin can be used to allow the user to type in values from the keyboard. Here is an example to illustrate this. (⇤)
⇤The cin stream is unique to C++.
Computing in C++ , Part I #include
using namespace std;
int main() {
int i, number;
double x;
double sum = 0.0;
Dr Robert Nu ̈rnberg
// initialize sum with 0.0
// input an integer
// a for loop
// input a double
cout << "How many numbers do you want to add up: ";
cin >> number;
for (i = 0; i < number; i++) {
cout << "Input next number: ";
cin >> x;
sum = sum + x;
}
cout << "Final sum: " << sum << "." << endl;
return 0;
}
If you do not want to make use of the C++ streams cin and cout, then the above program would have to look as follows.
#include
int main() {
int i, number;
double x;
double sum = 0.0;
// initialize sum with 0.0
// input an integer
// a for loop
// input a double
printf(“How many numbers do you want to add up: “);
scanf(“%d”, &number);
for (i = 0; i < number; i++) {
printf("Input next number: ");
scanf("%lf", &x);
sum = sum + x;
}
printf("Final sum: %6.4e.\n",sum);
return 0;
}
Note that when using the C function scanf(), you have to pass the address of the variable you want to assign a value to. For example, you have to pass &x instead than simply x. We will learn more about addresses of variables when we discuss pointers in Chapter 6.
1.5 Control flow
Although the while loop is the simplest loop in C, the most frequently used loop is the for loop. The syntax for the two loops is
while (continuation) {
instructions;
}
and
}
respectively. Both loops repeat the given instructions over and over again, until the continuation evaluates to false. Note that if continuation is already false before the loop begins, then the instructions inside the loop will not be executed at all. In addition, the for loop allows the pro- grammer to initialize a variable before the first execution of the loop and to increment a variable after it reaches the end of the loop, before the next evaluation of continuation. That is why the for loop is
for (initialization; continuation; increment) {
instructions;
Computing in C++ , Part I Dr Robert Nu ̈rnberg
used when the processing is sequential and when there is a well defined notion of an increment between one pass through the loop and the next.
The following example shows how to use the two types of loops. It also includes an example of another control flow construction, the if-else statement.
#include
using namespace std;
int main() {
double rate, pounds, euro, sum;
int i;
cout << "Please enter current exchange rate for GBP to EURO: ";
cin >> rate;
pounds = 1.0;
while (pounds > 0.0) {
cout << "Enter amount in pounds to convert (0 to quit) : ";
cin >> pounds;
if (pounds > 0.0) {
euro = pounds * rate;
// any positive value
// while loop
// if – then
cout << pounds << " GBP are " << euro << " in EURO." << endl;
}
else {
cout << "Now quitting this part." << endl;
} }
sum = 0.0;
for (i = 0; i < 1000; i++) {
sum = sum + 1.0 / (i + 1.0);
}
// else
// for loop
cout << "The sum 1 + 1/2 + 1/3 + ... + 1/1000 is " << sum << endl;
return 0; }
More details on control flow can be found in Chapter 3.
1.6 Basic operators
In our example programs, we have already come across the assignment operator =, the logical operator < and the arithmetic operators *, /, + and -.
The meaning of these operators is fairly clear. The assignment operator = evaluates the expression on the right hand side and assigns the value to the variable on the left hand side. Strictly speaking, the variable on the left and the expression on the right always have to be of the same type. For example,
int i;
i = 1.234;
is not allowed.
Any logical operator, like ==, <=, >=, > or !=, compares the two operands and returns either true or false. This result can then be used in order to control the program flow (see the previous section for details). Note also that results from logical operators can be combined with the logical and and or statements. In C/C++ they are represented by && and ||, respectively.
Finally, some less obvious operators we have come across already are the so called inserter operator <<, that is used to direct output to the output stream cout, and the postfix increment operator ++, as in the statement i++. The latter statement is equivalent to writing i = i + 1.
More information on operators can be found in Chapter 4.
1.7 Arrays
Arrays are a special form of variables in C and they are the natural representations of vectors in C. An array is declared with the syntax
Computing in C++ , Part I Dr Robert Nu ̈rnberg type name[SIZE];
where type is any of the basic types and SIZE is a constant integer. You cannot declare an array with a variable size. The technical term for this is that arrays are statically allocated, and not dynamically. We will see more on this later.
For example, the statement
double x[100];
allocates in memory the space for 100 double numbers, and these are stored sequentially in memory. They are referred to as x[0] to x[99], and inside the “[ ]” brackets any integer expression can be used. Note that x[100] refers to the first space in memory after the array, so it is not well defined. Trying to access this number can lead to catastrophic results when running the program. Unfortunately, the compiler will not warn you about this. So extra care has to be taken when using arrays in C.
The following program shows an example of how to use arrays.
#include
using namespace std;
int main() {
int i;
double x[100], y[100];
for (i = 0; i < 100; i++) {
x[i] = 2.0*i;
y[99-i] = i; }
// declaration of two arrays
for (i = 0; i < 100; i++)
cout << i << ". x = " << x[i] << ", y = " << y[i] << endl;
return 0; }
1.8 Functions
Functions or subroutines are parts of the code, that perform a certain well defined task within the program. Defining functions allows you to re-use these parts of the code in other programs. Moreover, your program will be better structured and your source code will be easier to read.
Functions can have a return value. An example for this is the call to the sin() function in the example on page 5. The function returns a value of type double and this is used to compute the variable y.
If a function is not expected to return a value, then it needs to be declared as void. Moreover, each function takes a fixed number of arguments. The number and the type of these arguments has to be declared when defining the function. Here is a short example.
#include
using namespace std;
void welcome() {
cout << "Hi there." << endl;
}
int input() {
int in;
cout << "Please enter a number: ";
cin >> in;
return in;
}
void goodbye(int number) {
cout << "You typed in: " << number << "." << endl;
cout << "Thanks and goodbye!" << endl;
}
int main() {
int i;
Computing in C++ , Part I
Dr Robert Nu ̈rnberg
welcome();
i = input();
goodbye(i);
return 0; }
// call the void function welcome()
// get value for i from function input()
// call void function goodbye() with value i
Note that in this example the three functions welcome(), input() and goodbye() are defined. Note also, that two of the functions do not return any result value, and that two of the functions do not take a parameter.
You can also see that a function can be called simply by giving its name, followed by all the expected parameters inside round “( )” brackets. In case that a function does not take any parameters, these brackets are still necessary, although they are left empty.
Note furthermore that the statements to be executed in a function are again grouped together with the “{ }” brackets, as is the case for the main() program. It is worth mentioning that the main() program is simply a very special function inside every C/C++ program. In fact, the main() program is called by the operating system of your computer in much the same way that you call functions inside your C++ program.
1.8.1 Mathematical functions
C provides an extensive library of mathematical functions. The definitions can be found in the system header file math.h, which under C++ can be included with the line
#include
at the beginning of your program, see the example in §1.3.
Having done that, you can access a wide selection of mathematical functions, such as sqrt(), sin(), cos(), exp(), log(), pow() etc. See www.cplusplus.com/ref/cmath for details.
1.9 Output control characters
There are several control characters, that you can use between quotes “” in output via both cout and printf(). Here is a complete list.
character meaning
\n newline
\t horizontal tab \v vertical tab
\b backspace
\r carriage return \f formfeed
\a audible bell
Here is a small example program.
#include
using namespace std;
int main() {
cout << "Hello\t you\n\nWhy\t do you look\t like that?\n";
cout << "Hello\v you\n\nWhy\v do you look\v like that?\n";
cout << "Hello\b you\n\nWhy\b do you look\b like that?\n";
cout << "Hello\r you\n\nWhy\r do you look\r like that?\n";
cout << "Hello\f you\n\nWhy\f do you look\f like that?\n";
cout << "Hello\a you\n\nWhy\a do you look\a like that?\n";
}
Note that using cout << "\n" is equivalent to writing cout << endl;.
In addition, when using the C++ output stream cout, you have several output manipulators that you can use. The following program shows the usage of some of them. Note that you have to #include
Computing in C++ , Part I
Dr Robert Nu ̈rnberg
1.10
#include
#include
using namespace std;
int main() {
double x = 12.345678;
cout << "Floating point number" << endl;
cout << "Default: " << x << endl;
cout << setprecision(10);
cout << "Precision of 10: " << x << endl;
cout << scientific << "Scientific notation: " << x << endl << endl;
cout << setprecision(5) << fixed << "Back to fixed notation: " << x << endl;
cout << "With setw(35) and right: " << setw(35) << right << x << endl;
cout << "With width(20) and left: ";
cout.width(20);
cout << left << x << endl << endl;
bool b = true;
cout << "Boolean Number" << endl;
cout << "Default: " << b << endl;
cout << boolalpha << "BoolAlpha set: " << b << endl << endl;
return 0; }
File I/O
Input and output to files in C++ is very straightforward. You can write to a file in exactly the same way that you write to the output stream cout, and similarly you can read from a file as if you read from the input stream cin. Take a look at the following example. (⇤)
#include
#include
using namespace std;
int main() {
ofstream fout(“output.txt”);
if (! fout.is_open()) {
// include fstream for file I/O
// creates an ofstream called fout
// test that file is open
cout << "Error opening output file." << endl;
return -1; }
fout << "Hello World" << endl;
fout.close();
ifstream fin("input.txt");
if (! fin.is_open()) {
cout << "Error opening input file." << endl;
return -1; }
int number;
fin >> number;
fin.close();
cout << "Read the number: " << number << endl;
return 0;
}
// close ofstream fout
// creates an ifstream called fin
// test that file is open
// close ifstream fin
Note that we closed each file stream after its use, although C++ will close all open file streams automati- cally at the end of the program. But it is good programming practice to close your streams yourself, also
⇤File I/O in C is slightly more complicated. You need the variable type FILE *f; and can e.g. open files for writing with the command f = fopen("output.txt","w");. Then use fprintf(f,"...") instead of printf("...") and close the file with fclose(f);.
Computing in C++ , Part I Dr Robert Nu ̈rnberg
because the machine your program is running on will slow down if too many file streams are open at the same time.
Moreover, when dealing with variable length input data files, it is beneficial to note that the insertion operator >> itself evaluates to true if the last reading operation has been successful. In C++ this is the recommended way to check for correct input, rather than using e.g. the functions fin.eof() and fin.good() for the input file stream fin, which return true when the end of a file has been reached, and when the last input via >> was successful, respectively.
1.11 Namespaces
So far, we have used the command using namespace std; in each of our programs, without really knowing why.
Namespaces are a feature of C++ and roughly speaking they group together certain variables and functions under one name. We will learn more about namespaces in the second part of this course. For now, it su ces to know that the namespace std groups together all the objects needed for input and output that are provided by the system library iostream.
It is possible to not make use of the namespace std. For the sake of completeness, here is what e.g. the first program in §1.4 would then look like.
#include
// no ’using namespace std’ ! Watch out for ’std::’.
int main() {
int i, number;
double x;
double sum = 0.0;
// initialize sum with 0.0
// input an integer
// a for loop
// input a double
std::cout << "How many numbers do you want to add up: ";
std::cin >> number;
for (i = 0; i < number; i++) {
std::cout << "Input next number: ";
std::cin >> x;
sum = sum + x;
}
std::cout << "Final sum: " << sum << "." << std::endl;
return 0;
}
2 Variables 2.1 Basic types
For each variable that is used inside a program, a suitable type has to be found. This choice will depend on the meaning of the variable, e.g. whether it is an integer or a floating point number, but also on the required accuracy or range of the variable.
When a variable is used by a program, it occupies some part of the main memory of the machine the program is running on. The amount of space it will occupy depends on the type of the variable. It is considered good programming practice to not “waste” the computer’s memory by using variable types that use more memory than is actually needed for the purpose of the variable. However, this really only plays a role if huge amounts of data are used.
The amount of space a variable occupies is measured in bytes. One byte consists of 8 bits, and hence can store up to 28 = 256 di↵erent states. The following table gives an overview of the basic types, what they can store and their range of values. The list is not exhaustive.
The size of any type can be assessed with the sizeof() function. Note that if for input and output you want to use the C functions printf() and scanf(), you will need di↵erent format descriptors, like %d for int and %e, %g for double. A full list can be found in Kernighan & Ritchie, p244. See also www.cplusplus.com/ref/cstdio/printf.html and www.cplusplus.com/ref/cstdio/scanf.html.
⇤The type bool is only defined in C++.
Computing in C++ , Part I
Dr Robert Nu ̈rnberg
type
bytes
range
example
int
short int unsigned short int long int
float
double
long double
char
bool (⇤)
4 2 2 8 4 8
16 1 1
2147483648 . . . 2147483647
32768 . . . 32767
0...65535 9223372036854775808 . . . 9223372036854775807 ±1.4013 ⇥ 10 45 to ±3.402823 ⇥ 1038 ±4.94065 ⇥ 10 324 to ±1.797693 ⇥ 10308 ±6.4752 ⇥ 10 4966 to ±1.18973 ⇥ 104932 characters like ’a’,’Z’,’@’
0 . . . 1 or “true”, “false”
i = -5;
i = 4;
i = 55555;
i = -1234567890; x = -1.23e-2;
x = 3.1e-15;
x = 0.12e1054; c = ’b’; read = false;
Table 1: Basic types and their representation.
2.2 Internal representation of variables
We have seen in the previous section that each variable that is declared for a certain type occupies some space in the memory of the computer. Now we will look at exactly how the values of a variable are stored in memory.
When memory is declared for a short int, 16 bits (or 2 bytes) are allocated in the memory of the machine. One of these bits is used to store the sign of the integer, and the remaining bits store the digits of the integer in binary representation. Thus the largest 16bit integer which can be stored is 215 1 = 32767. Note that the unsigned version does not need to store the sign of the number, and can hence represent integers up to 216 1 = 65535. Analogues hold true for the 32bit integer type int.
A char is very similar to an int, but since on most machines the character set consists of less than 256 characters, and 28 = 256, 8 bits are su cient to store a character.
Slightly more complicated is the representation of floating point numbers. The IEEE single-precision floating point standard states that such a number should be represented by 32 bits. The first bit stores the sign of the number, and we call it s. The next 8 bits are interpreted as an integer e, called the exponent. The last 23 bits, b1b2 . . . b23, are interpreted as 0.b1b2 . . . b23 in binary expansion. This is called the mantissa, and we represent it with m. The entire floating point number x is then given by
x = ( 1)s 2e 127 (1.m)2.
Note that zero is treated specially, and is given by m = e = 0. Apart from zero, no other number is allowed to have e = 0. Hence the smallest positive number that can be written in the above format is defined by m = 0 and e = 1, giving 2 126 or 1.175⇥10 38. However, on most systems so called subnormal numbers are defined. These cover the case, where e = 0 and m 6= 0. Then the implicitly defined leading 1 is changed to a 0 and the formula is adapted to
x = ( 1)s 2e 126 (0.m)2.
These subnormal numbers are machine representable but are less accurate in computation than the normalizable values discussed earlier.
Double-precision numbers (type double) are represented in a similar way, except that both the exponent and the mantissa are represented using a greater number of bits. In particular, a double element has a sign bit, an 11-bit exponent and a 52-bit mantissa. So the representation for a normalizable double number is
x = ( 1)s 2e 1023 (1.m)2, while a subnormal number (with e = 0 and m 6= 0) is interpreted as
x = ( 1)s 2e 1022 (0.m)2. 2.2.1 Precision problems and rounding errors
As a consequence of this mechanism for representing floating point numbers, the number stored in memory
is only an approximation to its true value in the real world. Only those floating point numbers with small
binary expansions can be stored precisely, for instance 1 and 1 . 28
Moreover, the value which is stored depends on the way the number was computed. For example
Computing in C++ , Part I Dr Robert Nu ̈rnberg (1.0 / 3.0) * 2.5 == 5.0/6.0
is false, since the floating point representation of 5 obtained by dividing the two floating point numbers 61
5.0 and 6.0 is not the same as the representation which is arrived at by first taking 3 and then multiplying by 2.5.
This example illustrates that one should never test floating point numbers for equality, and one should never rely on equality for termination of a while or a for loop. For termination tests, a strict inequality like < should be used, while in place of an equality test between floating point numbers one should use something like
if (x - y < tol && x - y > – tol) // x and y are equal up to tol
where tol is a given tolerance, e.g. 10 16.
Another precision problem is the occurrence of so called overflows and underflows. The former are events, where the maximum range of a type is reached. This can cause unpredictable outcomes for your program. Underflows, on the other hand, are events where information that was stored in floating point numbers is lost, either because the lower end of the range of the type was reached, or because a much larger value was added, say, to a small number. Since only a finite number of digits can be stored for every number, some or all digits of the smaller number will be erased and lost in the process.
The number of significant decimal digits for the discussed types are 7 digits for the type float and 16 digits for double, respectively. For long double this value is approximately 33.
2.2.2 Type conversion
It is clear by now that if C/C++ in an assignment involving di↵erent types simply equated the values stored in memory, this would have undesired e↵ects. Often the amount of memory needed to store the di↵erent types is not even the same. Hence a meaningful conversion has to take place, when e.g. an int value is assigned to a double variable, and vice versa.
This is done via type conversion, and in most cases, the programmer need not worry about them. That is because the compiler invokes these conversions automatically, wherever the desired conversion is obvious. Here is a program with a list of examples.
#include
using namespace std;
int main() {
int i = 7;
unsigned int j = i;
short int k = i;
double x = i;
// from int to unsigned int
// from int to short
// from int to double
cout<
using namespace std;
int main() {
double x = 1.999;
int i = (int) x;
char c = ’a’;
cout <
#include
using namespace std;
int main() {
double i, j;
double x = 3.14, y = -3.14;
i = floor(x); j = floor(y);
cout << i << " " << j << endl;
i = ceil(x); j = ceil(y);
cout << i << " " << j << endl;
i = round(x); j = round(y);
cout << i << " " << j << endl;
i = trunc(x); j = trunc(y);
cout << i <<""<
// ’4 -3’
// round to nearest int
// ’3 -3’
// nearest int i with |i| < |x|
// ’3 -3’
Other than these names, you can choose any names of reasonable length for variables and functions. The names must begin with a letter, and then can contain letters, numbers and underscores.
It is also possible to start names with an underscore, but because the compiler and many system libraries use this for their variables and functions, this should be avoided at all costs.
Computing in C++ , Part I Dr Robert Nu ̈rnberg 2.4 Initialization of variables
In contrast to some other programming languages, it is important to know that a variable that is declared in C/C++ is by default not given any value. This is the task of the programmer. All that happens in C/C++ is, that the appropriate amount of memory is reserved for the declared variable. But no value is written to that place in memory. You can test this behaviour with the following small program.
2.5
Scope of a variable
#include
using namespace std;
int main() {
double x;
double y = 0.0;
// x is not initialized
// y is initialized with 0
// gibberish
// 0.0
// first assignment to x //0.1
cout << "The value of x is " << x << endl;
cout << "The value of y is " << y << endl;
x = 0.1; cout<<"Nowthevalueofxis"<
using namespace std;
void foo(double x) {
cout << "foo() got the value x = " << x << endl;
}
int main() {
int i;
for (i = 0; i < 5; i++) {
char z;
cout << "Please enter a character: ";
cin >> z;
}
z = ’c’; {
short int k;
k = 5;
cout << "k = " << k << endl;
// the scope of x ends here
// i’s scope is the whole main() program
// z only defined within for loop
// the scope of z ends here
// this will result in compiler error!
// k’s scope are these 3 lines
}
Computing in C++ , Part I Dr Robert Nu ̈rnberg return 0;
}
Variables, whose scope is a single user defined function, are also called local variables, see Chapter 5. One consequence of the above concept is, that in none of your subroutines can you directly make use of variables that were defined in the main() program. Variables that you want to use inside of subroutines have to be passed to them as parameters. See Chapter 5 for more details.
2.5.1 Global variables
The exceptions to the rule are so called global variables. These are variables, that have as scope the whole source file in which they are defined. That means that all the subroutines inside the source file — including the main() program — have access to these variables.
However, the use of global variables is considered a bad programming practice and their use should be avoided if at all possible. The main reason being that programs that make use of global variables are di cult to debug and di cult to read, since it is not immediately clear where they have been changed last, for instance.
For the sake of completeness, here is an example for the use of global variables.
2.6
Arrays and strings
#include
using namespace std;
int max_number;
void initialize() {
max_number = 10;
void print() {
int i;
for (i = 0; i < max_number; i++)
cout << "*";
}
// this is a global variable
// max_number is global
// max_number is global
}
int main() {
initialize();
print();
cout << "Global variable max_number = " << max_number << endl;
return 0;
}
As mentioned in §1.7, arrays are a special form of variables in C/C++. They are the natural implemen- tation of fixed length vectors in C. When an array like double x[100] is declared in C, the space for 100 double variables is reserved in the memory of the machine. As we know from §2.1, in this case this amounts to 800 bytes. The space for the array is allocated sequentially in memory, and the elements of the array can be referred to as x[0], x[1], . . . , x[99]. It is important to remember that the element x[100] no longer belongs to the array x. Instead, it points to the first 8 bytes in memory after the array x. This could be anything from other data of your program, your program code itself, or data from another program that is running on the machine at the time. Recall the program in §1.7 for an example on how to use arrays.
2.6.1 Initialization of arrays
As is the case with normal variables, by default arrays are not initialized when they are declared. The compiler only allocates the necessary space in memory, without checking or changing the contents of that part of the memory.
However, as with variables it is possible to initialize arrays with meaningful values, if this is what the programmer wants. Check the following example for the syntax.
Computing in C++ , Part I #include
using namespace std;
void print(double a[], int length) {
int i;
for(i=0;i
#include
#include
using namespace std;
int main() {
char s[10] = “Hi there.”, r[20];
char *t;
cout << "Second character of s is ’"
<< s[1] << "’." << endl;
strcpy(r, s);
cout << "r = " << r << endl;
strcat(r, s);
cout << "r = " << r << endl;
t = strstr(r, "re.");
cout << "t = " << t << endl;
// include string.h for C strings
// include stdio.h for printf
// C strings
// pointer to char
// [] works as for other arrays
// "Hi there."
// "Hi there.Hi there."
// "re.Hi there."
// not initialized
// ’1 2 3 0 0 0 0 0 0 0’
// ’0 0 0 0 0 0 0 0 0 0’
Computing in C++ , Part I
Dr Robert Nu ̈rnberg
2.6.3
printf("Value of string s = ’%s’\n",s.c_str()); // Pass s as a C string
s = r; // works fine with C++ strings
return 0; }
Multidimensional arrays
printf("Value of string s = ’%s’\n",s);
strcpy(s, r);
return 0; }
// "Hi there."
// causes program termination
// because r is longer than s
We have seen in the previous example, that handling C strings can be di cult and that it can lead to an unwanted termination of the program. A safer way to use strings is o↵ered by C++. We will look into this in more detail in Part II of this course. For now we provide an equivalent program to the above, that uses the C++ string class.
#include
#include
#include
using namespace std;
int main() {
string s = “Hi there.”, r, t;
cout << "Second character of s is ’"
<< s[1] << "’." << endl;
r = s;
cout << "r ="<
using namespace std;
int main() {
int i,j,k,l;
double A[3][3] = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};
double x[3] = {10, 12, -6};
double y[3];
double M[2][2][2][2];
for (i = 0; i < 3; i++)
for (y[i] = j = 0; j < 3; j++)
y[i] += A[i][j] * x[j];
cout << "y = A*x = ";
for (i = 0; i < 3; i++)
cout << y[i] << " ";
cout << endl << "M = ";
for (i = 0; i < 2; i++)
for (j = 0; j < 2; j++)
for (k = 0; k < 2; k++)
// matrix A
// vector x
// vector y
// 4 dimensional
for (l = 0; l < 2; l++) {
Computing in C++ , Part I
Dr Robert Nu ̈rnberg
M[i][j][k][l] = 1000*i + 100*j + 10*k + l;
cout << M[i][j][k][l] << " ";
}
cout << endl;
return 0; }
2.7 Constants
A constant is similar to a variable in the sense that it has a type and it represents a memory location. The di↵erence is, of course, that it cannot be reassigned a new value after initialization. I.e. you cannot change its value after the constant was declared and initialized.
In general, constants are a useful feature that can prevent program bugs and logic errors. Unintended modifications to a supposedly constant value in your program are prevented from occurring. The compiler will catch attempts to reassign new values to constants.
In order to declare a constant, you only have to add the keyword const to the definition. This will turn a variable into a constant. For instance,
const double pi = 3.14159265;
will create the constant double value pi. You can now use the name pi anywhere in the scope of this constant, instead of the numerical value 3.14159265.
Note that in C++ you can use integer constants to define the length of an array. (⇤) However, if you work with C compilers, this does not work. That is because in many ways, the compiler still treats constants as variables. So while a constant is really only initialized once the program is executed and that line of code is reached, the dimension of an array, on the other hand, has to be known at compile time already. An alternative way to have a constant expression in your source code that defines the length of an array, that also works in C, is to use the #define preprocessor directive. The preprocessor is a program that modifies your source file just prior to compilation. Another preprocessor directive that we have already seen is the #include directive. The #define directive is used to define macros, e.g. as follows.
#define LENGTH 100
Now, wherever the macro LENGTH appears in your source file, the preprocessor replaces it by its value. So, every “LENGTH” in your source code will be replaced by “100”. The compiler will only see the value 100 in your code, not “LENGTH”.
Here is an example program to demonstrate the use of the described techniques.
#include
using namespace std;
#define LENGTH 100
int main() {
int i;
const double pi = 3.14159265;
double x[LENGTH];
for (i = 0; i < LENGTH; i++) {
x[i] = pi*i;
}
for (i = 0; i < 100; i++)
cout << i << ". x = " << x[i] << endl;
return 0;
}
3 Control flow
// define macro LENGTH
// LENGTH used here
// LENGTH used here
Most of the control flow statements we have already come across by now. See §1.5 for a short introduction. We will now look at all of the control flow constructs available in C/C++ in a bit more detail.
⇤This only works in C++. In C you need to use macros. Some C++ compilers, e.g. g++, even allow you to use integer variables to define the length of an array. But this is a nonstandard addition. g++ -pedantic deactivates this.
Computing in C++ , Part I Dr Robert Nu ̈rnberg Any control flow construct enables you in some way to control how the execution of the program flows
from one instruction to the next, hence their name.
3.1 The while loop
The while loop is the simplest loop in C/C++. The syntax is
while (continuation) {
instructions;
}
and the instructions inside the loop are executed until the boolean expression continuation is false. Note that the while loop will not be executed at all, if this expression is false on encountering the while loop for the first time.
Here is a short example program for a while loop.
#include
using namespace std;
void important_stuff() {
// some important code
}
int main() {
char answer;
cout << "Do you want to start the program? (y/n) : ";
cin >> answer;
while (answer == ’Y’ || answer == ’y’) {
important_stuff();
cout << "Do you want to continue? (y/n) : ";
cin >> answer;
}
return 0; }
3.2
and similar to the while loop, the given instructions are repeated, until the continuation evaluates to false. In addition, this loop allows the programmer to initialize a variable or several variables before the first execution of the loop. Furthermore, the programmer can execute certain statements after each execution of the loop, before the next evaluation of continuation.
Both initialization and continuation may include more than one statement. In this case, di↵erent statements are separated by commas.
Here is an example.
#include
#include
using namespace std;
int main() {
double sum;
int i;
for (i = 0, sum = 0.0; i <= 100; cout << i << endl, i++) {
sum += pow(2, - (double) i);
}
cout << "The sum of 1/2^i for i = 0,...,100 is " << sum << endl;
The for loop
The for loop can be viewed as a specialized version of the while loop. The syntax is
for (initialization; continuation; increment) {
instructions;
}
Computing in C++ , Part I
Dr Robert Nu ̈rnberg
for (int j = 10; j > 0; j–) {
cout << j << endl;
}
return 0; }
// scope of j ends here
The second example shows another important feature of the for loop in C++. It is possible, to declare a variable inside the initializations, that is only defined inside the for loop. That means, that the variable will run out of scope (see §2.5) as soon as the for loop is finished. This should be the preferred way of defining counter variables for for loops, as this prevents any confusion with other counter variables you might have defined elsewhere in your code. (⇤)
3.3 The do-while loop
The syntax for the do-while statement is
do {
instructions;
} while (continuation);
There is little di↵erence between do-while and while. The only di↵erence is that in the do-while loop the continuation condition is evaluated at the end of the loop, rather than at the beginning. This means that the instructions enclosed between the braces are always executed at least once.
Here is an example, where a do-while loop makes sense.
#include
#include
using namespace std;
double f(double x) { return x + (2-x*x) / 2.0 / x; }
int main() {
double x_old, x_new, tol = 1e-8;
cout << "Enter an initial guess : "; cin >> x_new;
do {
x_old = x_new;
x_new = f(x_old);
cout << setprecision(10) << x_new << endl;
} while (x_new - x_old > tol || x_old – x_new > tol);
cout << "The square root of 2 is approximately equal to " << x_new << endl;
return 0; }
This works well as a do-while loop, because one evaluation of the function f() is inevitable. 3.4 The if-else statement
This is perhaps the most common of all control flow statements. It is also one of the simplest. The syntax is
if (expression) {
instructions;
} else {
alternative_instructions;
}
If expression evaluates to true, then instructions are executed, otherwise alternative_instructions. If the else part is not supplied, then no action is taken unless expression evaluates to true.
The if-else statement can be joined together with other if-else statement to form bigger constructs like
⇤This is not possible in C. In C you have to define the counter variable used in the for loop before the loop itself.
Computing in C++ , Part I
Dr Robert Nu ̈rnberg
if (expression1) {
first_instructions;
}
else if (expression2) {
second_instructions;
}
else if (expression3) {
third_instructions;
} else {
default_instructions;
}
Here first_instructions are executed if expression1 is true. If it is false, however, and only if it is false, expression2 is tested. If expression2 is true, then second_instructions are executed, otherwise expression3 is tested and so on. In any case, only one set of instructions will be executed. No else actions are investigated once an expression has evaluated to true.
Note here and throughout, that the boolean expression true and false are simple placeholders for the integer values 1 and 0. Moreover, the C/C++ compiler treats any integer value di↵erent from 0 as true. Here is a small example program for the if-else statement.
#include
using namespace std;
int main() {
int a, b;
cout << "Enter two integers : ";
cin >> a >> b;
if (a < b)
cout << "The smaller number is a = " << a << endl;
else if (b < a)
cout << "The smaller number is b = " << b << endl;
else
cout << "The numbers are equal a = b = " << a << endl;
return 0;
}
Note that if the instructions to be executed inside the if or else branch only consist of a single statement, the curly brackets can also be omitted.
3.5 The switch statement
In certain situation one can avoid heavily nested if-else statements as above with the switch statement. However, this applies only if one wants to test one integer expression for equality with several constant values. The syntax is as follows.
switch (expression) {
case constant-expression1 : statements1;
case constant-expression2 : statements2;
...
case constant-expressionn : statementsn;
default : default_statements;
}
The meaning is best explained with an example. Take the following switch statement, where the variable grade is of type char.
switch (grade) {
case ’A’ : cout << "Excellent" << endl;
case ’B’ : cout << "Good" << endl;
case ’C’ : cout << "OK" << endl;
case ’D’ : cout << "Mmmmm...." << endl;
case ’F’ : cout << "You must do better than this" << endl;
default : cout << "Cannot recognize grade." << endl;
}
Computing in C++ , Part I
Here, if the grade is ’A’ then the output will be
Excellent
Good
OK
Mmmmm....
You must do better than this
Cannot recognize grade.
Dr Robert Nu ̈rnberg
This is because in the C switch statement, once a positive match has been made, execution continues on into the next case clause if it is not explicitly specified that the execution should exit the switch statement at that point. The correct syntax for that would be:
switch (grade) {
case ’A’ : cout << "Excellent" << endl;
break;
case ’B’ : cout << "Good" << endl;
break;
case ’C’ : cout << "OK" << endl;
break;
case ’D’ : cout << "Mmmmm...." << endl;
break;
case ’F’ : cout << "You must do better than this" << endl;
break;
default : cout << "Cannot recognize grade." << endl;
break;
}
The break command tells your program to leave the switch statement after executing the instructions for the relevant case. Note that the break command is not really necessary for the default clause (or the last clause in general), but it is good programming practice to put it in anyway.
There is an important instance, where it makes sense not to put a break command. That is, when you want to execute the same instructions for several of the constant expressions that you test your variable for.
The following program includes an example for that.
#include
using namespace std;
int main() {
int i;
cout << "Enter a number < 10 : "; cin >> i;
if (i > 0 && i < 10)
switch (i) {
case 2 :
case 3 :
case 5 :
case 7 : cout << "You have entered a prime number." << endl;
break;
default: cout << "The number you entered is not prime." << endl;
}
else
cout << "Your number was out of range." << endl;
return 0;
}
3.6
break and continue
The main usage of the break statement, as we have already seen, is for the switch construct. But it can be used similarly in any of the loops that we have come across. It will cause the program flow to exit the loop immediately, and continue with the first statement after the loop.
It is almost always easily possible to avoid using break, and this will make your code easier to read and easier to understand. However, sometimes it might be the most natural choice, especially when dealing with very large loops.
Computing in C++ , Part I Dr Robert Nu ̈rnberg
The continue statement is similar to the break statement, in that it also terminates the body of a loop immediately. But, whereas the break statement causes the program flow to jump out of the loop, continue simply finishes this cycle of the loop, ready to start the next one. You can think of this as jumping to just after the last statement inside the loop, but staying inside the loop itself.
So, continue in a while loop returns to the head of the loop and checks to see whether the test is still true and, if it is, resumes execution at the start of the loop. In a do-while loop, it jumps to the foot of the loop where the logical test is performed, and in a for loop, it jumps to the foot of the loop, in the sense that the increment expression is executed before the next logical test of continuation.
Also the continue statement should be used with care. It can often make sense in for loops, if there are some members in the processed sequence that are not required. In all other loops, however, using continue is not advisable, as it can lead to infinite loops.
We round this section o↵ with another example program.
#include
using namespace std;
int main() {
double temp = 0.0;
char answer;
while (temp < 37.0) {
cout << "Do you want to continue? (y/n) : ";
cin >> answer;
if (answer == ’n’ || answer == ’N’)
break;
cout << "Give me your temperature : ";
cin >> temp;
if (temp >= 37.0)
// break
cout << "You have temperature and should see a doctor." << endl;
}
cout << "Which number is missing here ... ?" << endl;
for (int i = 0; i < 10; i++) {
if (i == 5) continue;
cout<
using namespace std;
Computing in C++ , Part I
Dr Robert Nu ̈rnberg
int main() {
int a,b,i;
double x[7];
a = 17;
b = 8;
x[0] =
x[1] =
x[2] =
x[3] =
x[4] = (double) (a / b);
x[5] = (double) a / b;
x[6] = a / (double) b;
for (i = 0; i < 7; i++)
cout << x[i] << endl;
return 0;
}
// ’0’
// ’0.5’
// ’0.5’
// ’2’
// ’2’
// ’2.125’
// ’2.125’
(integer)
(integer)
(integer result cast to double)
1/2; 1.0/2; 1/2.0; a/b;
Note that type cast operations take precedence over binary operations, and hence x[5] is evaluated to the floating point number 2.125.
Finally, a%b evaluates to a modulo b, e.g. 14%4 evaluates to 2 since 14 ⌘ 2 (mod 4). For this operator, a and b must be of integer type. It is an error for either of them to be of any other type. Sensible results are not guaranteed if either a or b are negative. An error will occur if b is zero.
4.2 Precedence of operators
We have seen in the example above that, just as in mathematics, the precedence of operators is important in determining the result, and brackets can be used to force a di↵erent precedence from the default. Table 2 gives a complete list of operators showing their precedence. Not all of the operators have been discussed yet.
Operators at the top of the table have a higher precedence than those lower down.
When in doubt, use brackets to indicate what you intend to happen. The associativity indicates in what order operators of equal precedence in an expression are applied. Operators of equal precedence appear between horizontal lines.
4.3 Assignment operator
In many of our examples we have already seen the assignment operator =. As discussed earlier, in general the types of the expression on the right hand side and the variable on the left hand side should be the same. When this is not the case, the compiler tries to use some form of type casting, see §2.2.2.
It is worth mentioning that the assignment operator in C/C++ is treated like any other operator, which means that in particular it evaluates to a result. For instance, the assignment
a = b;
in addition to setting a equal to b, actually evaluates to b. In this particular example, the result is discarded, but one can use this feature of C/C++ in multiple assignments like
a = b = c;
Here, a is set equal to the result of the assignment b = c, while the assignment b = c sets b to take the value of c and evaluates to c. Hence a is also set equal to c.
4.3.1 Special assignment operators
Arithmetic operations are often combined with an assignment, e.g. a = a + b; x = 2 * x / y;. C/C++ provides a special type of operator that combines arithmetic operations with some kinds of these types of assignments.
These new operators are +=, -=, *=, /= etc. The general rule is that statements of the form
x #= y;
where # is a binary operation such as +, are interpreted as x = x # y;
Computing in C++ , Part I Operator
()
[]
.
->
++ —
+ –
! ~
(type)
*
& sizeof * /% + – << >> < <=
Description
Parentheses (grouping)
Brackets (array subscript)
Member selection via object name
Member selection via pointer
Unary preincrement/predecrement
Unary plus/minus
Unary logical negation/bitwise complement Unary cast (change type)
Dereference
Address
Determine size in bytes Multiplication/division/modulus Addition/subtraction
Bitwise shift left, Bitwise shift right
Relational less than/less than or equal to Relational greater than/greater than or equal to Relational is equal to/is not equal to
Bitwise AND
Bitwise exclusive OR
Bitwise inclusive OR
Logical AND
Logical OR
Ternary conditional
Assignment
Addition/subtraction assignment Multiplication/division assignment Modulus/bitwise AND assignment
Bitwise exclusive/inclusive OR assignment Bitwise shift left/right assignment
Comma (separate expressions)
Table 2: Operator precedences
Dr Robert Nu ̈rnberg
Associativity left-to-right
right-to-left
left-to-right left-to-right left-to-right left-to-right
left-to-right left-to-right left-to-right left-to-right left-to-right left-to-right right-to-left right-to-left
left-to-right
>
==
&
^
|
&&
||
?:
=
+= -= *= /= %= &= ^= |= <<= >>= ,
>= !=
so that, for examplex+=3is equivalent tox=x+3.
The result is slightly more e cient code, because using x += 3 the machine simply evaluates the right hand side and adds it to the left, whereas x = x + 3 involves making a copy of x.
4.4 Logical operators
By now we have come across most of the logical operators in C/C++. You can check Table 2 for a complete list of relational and logical operators.
The following program shows some examples of these operators being used, as well as the usage of the variable type bool.
#include
using namespace std;
int main() {
bool small;
double x = 1.2, y = 1.1;
small = 2 < 3;
if (small && x > y) {
if (y > 0.5 || x < -3)
cout << "Then." << endl;
}
if (!small) cout << "Not." << endl;
// store boolean value
// logical AND
// logical OR
// logical NOT
Computing in C++ , Part I Dr Robert Nu ̈rnberg
cout << small << endl; // ’1’
return 0; }
The compiler converts any logical expression internally into an integer number, either 0 (false) or 1 (true). This means, that these predefined values for bool variables are simple placeholders for the two numbers 0 and 1. Moreover, any integer number di↵erent from 0 is also treated as true. Hence, one has to be careful when using the equality operator == and not confuse it with the assignment operator =. Here is an example for this classic mistake.
#include
using namespace std;
int main() {
int a = 1, b = 2;
if (a == b)
cout << "This will not be printed." << endl;
if (a = b)
cout << "This will be printed." << endl;
return 0; }
// equality test
// assignment !
Note that the last line will be printed because “a = b” evaluates to 2, which is interpreted as true. 4.5 Unary operators
In addition to the binary operators we have looked at so far, there are also some unary operators. A unary operator is an operator that takes only one argument or operand. A familiar example is the - operator, as in x = - y;, which simply negates the expression which follows it.
There are two other arithmetic unary operations defined in C/C++: the increment operator ++ and the decrement operator --. The former adds 1 to the operand and is often used in for loops, while the latter subtracts 1.
Like all the other operators, ++ and -- also evaluate to a result after they have performed the addition/- subtraction. Now, the result of this evaluation depends on whether the operator is used in the postfix notation, as in y = x++; or in the prefix notation, as in y = ++x;. In the first case, 1 is added to x only after the value of x was assigned to y. In the prefix case, however, the increment takes place first, followed by the assignment to y.
The following program demonstrates this behaviour.
#include
using namespace std;
int main() {
int i = 0, j;
j = i++;
cout << i << " " << j << endl;
j = ++i;
cout << i << " " << j << endl;
return 0;
}
// ’1 0’
// ’2 2’
Other unary operators include the type cast operator (type) and the logical “not” operator !, see Table 2. 5 Functions
In this chapter, we will take a closer look at how to use functions in C. Although many programs can be written with a single main() function, i.e. without defining separate functions, it is usually better to split up the code into a number of di↵erent parts, each one performing a well defined task. This will enable you to modify the code easily, adapt it to di↵erent problems and understand readily what it is doing.
Consider the following code to integrate the ordinary di↵erential equation dy = x + sin(y) from x = 1 to
x = 2 with initial condition y(1) = 1.
dx x
Computing in C++ , Part I
Dr Robert Nu ̈rnberg
#include
#include
using namespace std;
/* Solve ODE dy/dx = f(x,y)
with forward Euler */
double f(double x, double y)
{ // Take x and y and return f(x,y)
double val;
val = x + sin(y) / x;
return val;
}
int main() {
int i, n = 1000;
double x, y, dx;
dx = 1.0 / n;
x = y = 1.0;
for (i = 0; i < n; i++) {
y += dx * f(x,y);
x += dx; }
cout << "y(" << x << ")" << " = " << y << endl;
return 0; }
In this example, the user has defined the function f(), that represents the right hand side of the ODE. This makes it easy to identify the part of the code that needs to be changed, in order to adapt the program to a slightly di↵erent ODE. Similarly, one could define a function for the integration scheme itself and thus provide the possibility to adapt the code to other schemes like Euler backward, or Runge-Kutta methods.
Notice that before the user defined function f() can be used in the main() function, it has to be declared. In our example, we have combined the declaration of the function with the implementation of the function itself. There is another way of doing this, see §5.7 for details.
The declaration of the function comes down to the line
double f(double x, double y)
and is here followed be the implementation of f(). The declaration of a function has to state the number and the type of arguments it takes, as well as the type of the variable it returns. Functions that do not return a value have to be declared as void.
We must now examine in detail the behaviour of the program when the function is called. Although these details appear technical, their understanding is crucial when starting to do non-trivial C/C++ programming.
The line y += dx * f(x,y); is interpreted by the compiler as “pass the values of x and y to the function f(), give control of the program to the function f(), then take the value returned from function f(), multiply it by dx and add it to y”.
When control passes to the function f(), the compiler interprets the function as “accept the values of two double numbers from the calling function, store these values in two local variables x and y, then introduce another local variable val, assign to this variable the value of x + sin(y) / x, and finally return the value of val to the calling function.”
Note that the scope (refer to §2.5) of the variable val is the function f() itself. That means, this variable is local to that function. In particular, the main() function has no knowledge of that variable, and the only way that the function f() can pass the value of that local variable to the main() function is via the return statement. In a similar way, the variables inside the main() function are local to that function, and the only way the function f() can get information about them, is via the parameters that are passed to it.
There are two important concepts here that need to be well understood.
Computing in C++ , Part I Dr Robert Nu ̈rnberg 5.1 Passing by value
The parameters of the function f() in the previous example are passed by value. That means, that it is the values of x and y only that are passed from main() to the function f(). In particular, the function f() can only read the values of the two arguments, but it cannot change the values of the two variables x and y in the main() function.
To ensure this, the compiler forces the function f() to only work on copies of the variables x and y. Hence, upon execution of the function f(), the function creates a local copy for each of the parameter variables that are passed to it, then it assigns the values of the outside parameters to these local copies and henceforth works on these copies. As a consequence, the values of the variables in the function call from main() remain unchanged.
5.2 The return statement
The return statement has a few special features. When a function is called, execution starts at the first line of the function and continues until either a return statement is reached, or until the last line of the function is reached. This means that we can leave a function before the last line, if we use a return statement before.
Consider the following example.
#include
using namespace std;
double sign(double x) {
if (x > 0) return 1.0;
if (x < 0) return -1.0;
return 0.0;
}
int main() {
double a = -1.349;
cout << "The sign of a is: " << sign(a) << endl;
return 0;
}
Note that the function sign() can be defined without using any else statement, simply because the return statement signifies the end of the function, and hence the remaining commands will only be executed if the condition in the if statement was false.
A function may return no value. In this case, the function must be declared to be of type void. For a void function, a return statement is optional. If it is used, the syntax is “return;”.
Any function foo(), no matter whether it returns a value or not, can be called with the syntax foo(var1,var2,...);
If you want to use the return value of the function, the corresponding syntax is
a = foo(var1,var2,...);
Here var1,var2,... are the arguments which foo() requires, and the return value is assigned to a. 5.3 Passing arrays
A notable exception to the concept of passing by value are arrays. When passing arrays to a function in a fashion that looks like they are passed by value, in reality a concept called pointers is used, see Chapter 6 for details. For now it su ces to know that when arrays are passed in this way, no local copy of the array is created and hence any change that is done to the elements in the array inside the function, will take e↵ect outside the function as well. I.e. the function can write directly do the space in memory, that is allocated for the array.
The syntax for passing the array double x[100] to a function void foo(), say, is either void foo(double x[100])
or
void foo(double x[])
Computing in C++ , Part I Dr Robert Nu ̈rnberg
In the first case, the function foo() can only be called for arrays of length 100, whereas in the second case arrays of any length can be used.
The following example program illustrates this. Note also that the array y is changed by the function call add_to(y,x).
#include
using namespace std;
void print(double a[], int length) { // variable length arrays for(inti=0;i
using namespace std;
void add_to_byvalue(double x) { x++; }
void add_to_byreference(double &x) { x++; }
int main() {
double z = 4.5;
add_to_byvalue(z); cout << z << endl;
add_to_byreference(z); cout << z << endl;
return 0;
}
// does not change x outside
// does change x outside
// ’4.5’
// ’5.5’
Note that the function where z is passed by value does not change the value of z. Only the function where z is passed by reference does change the value, in this case adding 1 to it.
5.5 Keyword const
We have seen that both variables that are passed by reference and arrays that are passed to functions can be changed inside the function. In certain situations it might be desirable to prevent any changes to variables even though they are passed in this way.
Using the keyword const allows for this. Once placed before the type definition of a parameter to a function, the compiler will reject any statement inside the function that tries to change the value of this parameter. For example, on defining
void foo(const double &x)
the compiler will not allow statements like x += 1.0; inside the function foo(). The same holds true
for arrays, that is
void foo(const double x[], int n)
will not allow statements like x[1] = 4.1; inside foo().
The following example program demonstrates this.
#include
using namespace std;
void print_array(const double a[], int length) { for(inti=0;i
using namespace std;
void foo() {
int a = 0;
static int b = 0;
a++; b++;
cout << a << " " << b << endl;
}
void clever() {
static bool first = true;
if (first) {
// a gets reset every time
// b is static, initialized only once
// detects if function was called before
5.7
Declaration of functions
cout << "You have called me for the first time!" << endl;
first = false;
}
else
cout << "You have called me before." << endl;
}
int main() {
foo(); //’11’
foo(); //’12’
foo(); //’13’ clever(); // ’first time’ clever(); // ’called before’ return 0;
}
So far, we have always declared a function together with its definition or implementation. In general, this does not always have to be the case.
In order to compile your source code, the compiler only needs to know the declaration of your function, i.e. the name, return type and the parameters it takes. The implementation of the function is only needed once the executable is produced (this process is called linking) and can be given elsewhere, for instance in another file. We will discuss this in more detail when we learn about header files, see §8.4.
Another possibility is to give the implementation of the function in the same source file, but after the main() program. The following example demonstrates this.
#include
using namespace std;
void welcome();
int input();
void goodbye(int );
int main() {
int i;
welcome();
i = input();
goodbye(i);
// here the functions are only declared
// no variable name needs to be given
// functions are called as usual
return 0;
Computing in C++ , Part I }
/****************************************************/
/* The implementations of the functions follow here */
/****************************************************/
void welcome() {
cout << "Hi there." << endl;
}
int input() {
int in;
cout << "Please enter a number: ";
cin >> in;
return in;
}
void goodbye(int number) {
cout << "You typed in: " << number << "." << endl;
cout << "Thanks and goodbye!" << endl;
}
6 Pointers
Dr Robert Nu ̈rnberg
We have seen so far that any variable that is declared in C/C++ occupies a certain amount of space in memory. In order to read from and write to that space in memory, we can use the variable name and assign values to it or read values from it.
On the other hand, every variable has its unique address in memory, where its values are stored. This address will only be fixed at run-time, at the exact time when the variable is declared and when the memory is allocated for it. As long as the program remains within the scope of the variable, this address will not change. Once the variable has run out of scope, the memory that was allocated to it will be freed, and the old address will be no longer valid. That is, the part of memory that the address points to is no longer guaranteed to hold meaningful information.
So another way of changing the variable’s value is to write directly to the part of memory that is allocated to it. Because we can get hold of the variable’s address, we can read from and write to that particular part of memory in the knowledge that this will always correspond to the original variable.
In the following we will learn exactly how to achieve that.
6.1 Pointer syntax
A pointer can hold the address of a variable. Since it is important in C/C++ to distinguish between variables of di↵erent types, and since it is even more important to know the variable’s type before trying to write values to it, there is a unique kind of pointer for every variable type.
The syntax to declare a pointer for a certain type is
type *pointer_name;
This will create a pointer named pointer_name to type. You can also think of pointer_name as a variable of type type*.
A pointer can only hold the address of a variable of type. To assign the address of a variable to a pointer, you need the address operator &. Moreover, in order to read the value of a variable with the help of the pointer, you need the dereferencing operator *. Dereferencing means that instead than being interested in the address the pointer points to, you would rather want to know about the value that is stored at that address in memory.
Take a look at the following example program.
#include
using namespace std;
int main() {
int i;
// an integer
Computing in C++ , Part I int *ip;
i = 5; ip = &i;
cout<<"Thevalueofiis"<<*ip<
using namespace std;
void add_to_byvalue(double x) { x++; }
void add_to_bypointer(double *x) { (*x)++; }
int main() {
double z = 4.5;
add_to_byvalue(z);
cout << z << endl;
add_to_bypointer(&z);
cout << z << endl;
return 0;
}
// does not change x outside
// does change x outside
// pass z
// ’4.5’
// pass &z, i.e. the address
// ’5.5’
Note carefully that we had to use brackets in the expression (*x)++, because of the precedences of the involved operators. Otherwise the meaning of the statement would have been *(x++), something which will produce garbage and will probably cause your program to terminate with an exception. What that statement would mean in detail we will discover once we know about pointer arithmetic, see §6.4.1.
6.2.1 Keyword const
In §5.5 we have seen how changes to parameters that were passed by reference can be suppressed. The same holds true for passing by pointer. In particular, if an argument of type pointer was declared as const, then the compiler will disallow any statement that might change the variable that the pointer points to.
The syntax is
Computing in C++ , Part I Dr Robert Nu ̈rnberg void foo(const double *x)
and possible examples can be derived as in §5.5. 6.3 The NULL pointer
Like any other type of variable, by default when a pointer is declared it is not initialized. That means that the pointer will hold an arbitrary value that could point to just about any address in memory. In order to be able to give pointers a value that signals that they have not yet been given a variable’s address to point to, the NULL pointer was introduced. The actual numerical value of NULL is indeed 0, but this should never be used. Instead, you should always use NULL if you want to initialize pointers with an “empty” value.
Note also that many system routines use the NULL pointer to signal that a desired operation involving a pointer has failed, see §6.6.
6.3.1 if statement and pointers
One consequence of the above is that one can simply test whether a pointer holds an address or is defined
as NULL by writing
if (p)
cout << "Pointer p is defined." << endl;
else
cout << "Pointer p is NULL." << endl;
If p is a NULL pointer, then the above if statement will fail because the numerical value for NULL is 0, which means false. Similarly, testing for (!p) means to check if the pointer p is NULL.
The same technique can be used for while and for loops.
6.4 Pointers and arrays
In C, there is a strong relationship between pointers and arrays. First of all, when an array is declared, its name serves as a pointer to the first element of the array. Thus
double x[100], *xptr;
xptr = x; // equivalent to xptr = &x[0];
declares an array of 100 elements of type double and then sets xptr, which is of type pointer to double, to point to the head of the array x. It is important to realize that since x itself is nothing other than the address of the first element of the array x, you can pass x directly to the pointer xptr.
6.4.1 Pointer arithmetic
Now, if xptr points to a particular element in an array, then xptr+1 always points to the next one. This feature of C is called pointer arithmetic. For instance, one can define another pointer and assign this new address to it.
double x[100], *xptr, *xp2;
xptr = x;
xp2 = xptr + 1; // pointer arithmetic
Now xp2 will point to the second element of the array x, i.e. xp2 is equal to &x[1].
Similarly, if xptr points to the first element of an array, then xptr+i points to element i in the array. Hence *(xptr+i) and x[i] are equivalent. Notice again that brackets are needed in the pointer version. Referring to the precedence table (Table 2), we see that dereferencing takes precedence over addition. As a consequence *xptr+i written without brackets means “dereference *xptr and then add i to the result”, whereas *(xptr+i) means “add i to xptr and then dereference the result”. For this reason, special care is needed when arithmetic operations and dereferencing are performed in the same statement.
We should also note that *(x+i) means the same as x[i]. Indeed, the compiler will always interpret x[i] as *(x+i). Similarly, xptr[i] is also a legal expression. (But even experienced programmers may be surprised to learn that also i[x] is legal, and is the same as x[i].)
Pointer arithmetic is not restricted to adding integer values to a pointer. Similarly, you can subtract integer values from a pointer. Furthermore, you can use operators like +=, -= and ++, -- to achieve the same e↵ect. Finally, it is also possible to subtract two pointer values. As long as they both point to
Computing in C++ , Part I Dr Robert Nu ̈rnberg
elements belonging to the same array, the result is the number of elements separating them. You can also check (again, as long as they point into the same array) whether one pointer is greater or less than another. A pointer is “greater than” another pointer if it points beyond where the other one points. You can also compare pointers for equality and inequality. Two pointers are equal if they point to the same variable or to the same cell in an array, and are unequal if they don’t. The latter testing can also be done for two pointers that do not point into the same array.
See the following program for some examples.
#include
using namespace std;
int main() {
double x[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8 , 9};
double *xp, *xp_end;
6.4.2
if (xp == xp_end) {
cout << endl << "for loop was successful." << endl;
}
return 0; }
Di↵erences between pointers and arrays
xp = &x[4];
*(xp+2) += 10;
*(xp-2) = 0;
xp_end = &x[10];
for (xp = x; xp < xp_end; xp++)
cout << *xp << " ";
// ’6’ -> ’16’
// ’2’ -> ’0’
// points to after x[9]
// xp++ to increase pointer
// ’0 1 0 3 4 5 16 7 8 9’
So far, we have only looked at similarities between pointers and arrays. But there are two very important di↵erences between the two.
Let’s look at the previous example again.
double x[100], *xptr;
xptr = x; // equivalent to xptr = &x[0];
First, a pointer declaration such as double *xptr; allocates only the space required to store an address. It does not allocate any space that could be used to store an array of double precision numbers. This is only natural, since the compiler at this point does not know how big an array we want xptr to point to later.
Second, if we declare an array as double x[100], for example, the variable x is what is called a pointer constant, recall §2.7. That means that once the array is declared, the value of the pointer x is fixed, and we cannot change it or assign anything to x. Therefore, although xptr = x; is legal, x = xptr; is not legal.
6.5 Pointers to pointers
We can declare pointers to any type of variable. A pointer is itself a variable and we can have another set of variables which point to the pointers.
This can be useful when dealing with a large list of pointers, or when using multidimensional arrays. Here is an example program.
#include
using namespace std;
int main() {
char *name = “John”;
char *surname = “Smith”;
char *list[2];
list[0] = name;
// pointer to char
// array of pointers to char
Computing in C++ , Part I
Dr Robert Nu ̈rnberg
6.6
Dynamic memory allocation
list[1] = surname;
char **l = list;
for (int i = 0; i < 2; i++)
cout << l[i] << " ";
return 0;
}
// pointer to pointer to char
The main reason why we need to know about pointers is the possibility to dynamically allocate memory at run time. When declaring arrays, we have to know their dimensions in advance. Very often we are not in a position to know in advance how much memory our algorithms and programs are going to need. For this reason, C/C++ support dynamic memory allocation, which is the ability to allocate memory at run time.
In C++, the operators new and delete are defined for this purpose. The operator new is used to allocate memory of a given size at run time. The syntax is either
pointer = new type;
to allocate memory for a single variable of type, or
pointer = new type[SIZE];
to allocate memory for SIZE elements of type. The operator new returns a pointer of type type* that points to the first element of the newly allocated memory. (⇤)
The most important di↵erence between using an array and allocating memory with new is that the size of an array must be a constant value, which limits its size to what we decide at the moment of designing the program before its execution, whereas the dynamic memory allocation allows the allocation of memory during the execution of the program using any variable or constant expression or a combination of both as the size to be allocated.
The dynamic memory is generally managed by the operating system, and in multitask interfaces it can be shared between several applications. So there is a possibility that the memory of the machine exhausts. If this happens and the operating system cannot assign the memory that we request with the operator new, a NULL pointer will be returned. For that reason it is recommended to always check to see if the returned pointer is NULL after a call to new.
Once you have used the dynamically allocated memory in your program, you probably want to make it available again so that yours and other programs can use it for other things. The command to release dynamically allocated memory in C++ is delete. The syntax is as follows.
delete pointer; or
delete [] pointer;
The first expression should be used to delete memory allocated for a single variable, and the second one for memory allocated for multiple elements.
Here is a program that demonstrates the use of new and delete.
#include
using namespace std;
int main() {
double *x;
int n;
cout << "How many numbers would you like to input : ";
cin >> n;
x = new double[n];
if (x == NULL) {
cout << "Could not allocate memory" << endl;
return 1; }
for (int i = 0; i < n; i++) {
cout << i+1 << ". number: ";
// pointer to double
// allocate memory
// check if successful
⇤new and delete are unique to C++. In C you have to #include
Computing in C++ , Part I cin >> x[i];
Dr Robert Nu ̈rnberg
// usage like array
// free memory
}
for (int i = 0; i < n; i++)
cout << x[i] << " ";
delete [] x;
return 0;
}
Note that it is not possible to use new and delete in C. The easiest way to allocate memory in C is to use the malloc() function. The general syntax is
pointer = (type *)malloc((unsigned long) SIZE*sizeof(type));
This will allocate memory for SIZE elements of type. Note the usage of the sizeof() operator that was briefly discussed in §2.1. Note also that the two type casts to (type *) and to (unsigned long) are crucial to make this approach work.
Again, malloc() will return a NULL pointer, if the allocation was not successful.
The following program shows how the previous example would have to be written in C.
#include
#include
int main() {
double *x;
int n, i;
printf(“How many numbers would you like to input : “);
scanf(“%d”, &n);
// include stdlib.h
// pointer to double
x = (double *)malloc((unsigned long) n*sizeof(double)); // allocate memory
if (x == NULL) {
printf(“Could not allocate memory\n”);
return 1; }
for (i = 0; i < n; i++) {
printf("%d. number: ", i+1);
scanf("%lf", &x[i]);
}
for (i = 0; i < n; i++)
printf("%6.4e ", x[i]);
free(x);
return 0; }
// check if successful
// usage like array
// free memory
Apart from malloc() you can also use calloc() to dynamically allocate memory and initialize the elements at the same time, or realloc() to dynamically change the size of the memory a pointer points to. See www.cplusplus.com/ref/cstdlib for more details.
6.6.1 Segmentation faults and fatal exception errors
Pointers are responsible for almost all of the program crashes that a programmer will encounter when developing a project. Depending on the operating system, these crashes will either be called Segmentation fault (Linux/Unix) or Fatal exception errors (Microsoft Windows).
These errors will occur whenever a pointer is used that does not hold a valid memory address. For instance, when a pointer is used without being properly initialized, or when a NULL pointer is used in trying to access a location in memory.
Most often, however, it will have to do with dynamically allocated memory and a violation of the allocated array’s boundaries. I.e. the programmer tries to read from or write to an element which is no longer part of the allocated space. So special care has to be taken when working with pointers.
6.6.2 Dynamic allocation of multidimensional arrays
There is a very important di↵erence between fixed sized multidimensional arrays and dynamically allo- cated arrays.
When declaring a fixed sized array like
Computing in C++ , Part I Dr Robert Nu ̈rnberg type name[N1][N2]...[Nn];
then in e↵ect, the compiler will allocate space for all the elements in one big vector and name will be a pointer to type. Take the following example.
int x[100][20];
Then the compiler will allocate space for 100 ⇥ 20 = 2000 sequential elements and x[0] will point to the first element of this vector. In particular, this means that x itself, which is nothing other than x[0], is of type int * and not int **.
Furthermore, when addressing an element like x[i][j], the compiler computes the position of the element as “take the address of x, add i times 20 + j to it”. Notice that the length of the fastest varying index was necessary for the success of the operation. In general, the dimensions of all the indeces apart from the first one are needed.
On the other hand, if x is declared to be of type int **, then x[i][j] is obtained by “take the address of x and add i to it, then take the value stored at that address as a new address, add j to it and return the value thus addressed”. This latter operation is closest to the spirit of matrix manipulation, and it is much preferable to the first for any scientific computation. Note also, that this method has to be used wherever variable sized multidimensional arrays are to be used.
When allocating memory for a multidimensional structure with the help of pointers, it is probable that several di↵erent pointer tables have to be set up. In order to allocate the above 100⇥20 array, one would first have to allocate a table of length 100 for the type int*. Then for each of the entries of that table, one needs to allocate an array of length 20 for the type int.
See also the following example program, in which an n⇥m matrix is dynamically allocated and initialized. #include
using namespace std;
double **new_matrix(int n, int m) {
double **mat;
mat = new double * [n];
if (!mat) return NULL;
// allocate space for n pointers to double
// if unsuccessful, return NULL
// now for each row, allocate m entries
for (int i = 0; i < n; i++) {
mat[i] = new double[m];
if (!mat[i]) {
} }
for (int j = i-1; j >= 0; j–) // if unsuccessful, free allocated space
delete [] mat[j];
delete [] mat;
return NULL;
// and return NULL
// if successful, return pointer to space
return mat;
}
int main() {
double **matrix;
int n, m;
cout << "Enter dimensions for matrix: ";
cin >> n >> m;
matrix = new_matrix(n,m);
if (!matrix) return -1;
for (int i = 0; i < n; i++)
for (int j = 0; j < m; j++)
cin >> matrix[i][j];
// etc
return 0; }
// allocate matrix
For higher dimensional arrays, pointers of the types double ***, double ****, etc and similar functions to allocate the necessary structure have to be used.
Computing in C++ , Part I Dr Robert Nu ̈rnberg 6.7 Pointers to functions
The beauty of pointers is that they cannot only hold the address of variables, they can also hold the address of functions. Once your source code is compiled and run, all your functions live in some part of the memory. In particular, they also have an address, that can be used to call and execute them.
Cases where this might indeed be useful occur when you want to use a certain piece of code in many di↵erent situations. For instance, a routine to solve an equation using Newton’s method, or a method to integrate a given function over a specified range using the trapezoidal rule. In these cases, the algorithm is defined in terms of a general function. The user can then supply that function – via a pointer – when the code is called.
When declaring a pointer to a function, whether as a standalone variable or as a passed parameter to a function, the return type and the parameters the function takes have to be supplied. For example
double (*func)(double);
declares a pointer to a function that returns a double value and takes a single double parameter.
Note that the brackets in the declaration are necessary, since the function evaluation has a higher prece- dence than the dereferencing operator *, see Table 2. Hence, double *func(double); is interpreted as double *(func(double)); which would mean to do the function evaluation first, and then return the result as a pointer to double. This clearly makes no sense. In order to override this default, we must write double (*func)(double); which now has the intended result.
Suppose that we have defined a function
double foo(double);
in our source code. Then the pointer func can be given the address of foo() by writing
func = &foo;
or
The latter syntax is valid, since it is impossible to actually assign the function itself to the pointer. Hence the C/C++ compiler knows that we really only want to have the address of foo(). Similarly, there are two ways to call the function with the help of the pointer func. Either you use the dereferencing operator and write
y = (*func)(x);
or you omit the dereferencing and simply write
y = func(x);
The following example demonstrates how pointers to functions can be used as parameters for other
func = foo;
23 functions. It uses the trapezoidal rule to approximate the integral R ⇡ sin1 (x) dx.
0
double trapez(double a, double b, int n, double (*func)(double)) {
// integrate func over [a,b] using n intervals
double x = a;
const double dx = (b – a) / (double) n;
double val = 0.5 * func(x);
x += dx;
for (int i = 1; i < n; i++, x+= dx)
val += func(x);
val += 0.5 * func(x);
return val*dx;
}
int main() {
int n;
cout << "Integrating sin^1/3 (x) over [0,PI/2]." << endl;
cout << "How many steps to take for the integral: ";
#include
#include
using namespace std;
double my_f(double x) { return pow(sin(x), 1.0/3.0); }
Computing in C++ , Part I
Dr Robert Nu ̈rnberg
cin >> n;
cout << "The integral using " << n << " steps is "
<< trapez(0.0, 0.5*M_PI, n, my_f) << ".\n";
return 0;
}
// pass my_f
Now all that is needed to use the implemented trapezoidal rule for a di↵erent integrand, is to supply the new function definition and to call the function trapez() with a pointer to that new function. In particular, the implementation of the function trapez() need not be changed.
7 Structures
A structure is a collection of one or more variables grouped together under a single name for convenient handling. Here the variables that are grouped together can have di↵erent types. Structures help to organise complicated data, that is best described by more than a single variable or array, because they allow a group of related variables to be treated as a unit instead of as separate entities.
A natural example is an employee record that consists of the name, date of birth, address, salary, ID number etc. of the person involved. A structure allows the programmer to group all these properties into one unit, and treat that new structure as a new kind of variable type in the program.
There are also many naturally occurring examples of structures in scientific programming. Here are a few examples:
• Complex numbers: A complex number is naturally defined as a structure containing two doubles.
• Vectors: A vector consists of a pointer to type and an integer to give the length of the vector.
• Matrices: A matrix is a pointer to pointer to type and two integers to give the dimensions of the matrix.
• Points: A point is a structure containing all its coordinates.
• Triangles: A triangle is a structure with three edges and three vertices.
7.1 Structure syntax
Consider first the structure required for complex numbers. Such a structure would be expected to have two members, say one called re and the other called im. The structure is then defined as follows.
struct complex {
double re, im;
};
This definition does not declare any variables. It rather defines a new type of variables that can now be used within the program. Structure definitions usually come before function declarations in C/C++ programs, because if functions refer to structures then the structures must have been defined before the function declarations are reached.
Variables of type struct complex can now be declared as struct complex z1, z2;
meaning that two variables, called z1 and z2 are now declared, i.e. space has been allocated for them in memory.
Members of a structure can then be referred to using the struct member operator “.”. So, to set z = 3 + 4i, we can use the following syntax.
struct complex z;
z.re = 3.0;
z.im = 4.0;
Notice from the precedence table (Table 2) that the struct member operator is at the highest precedence. That means that names of structure members can be used as freely as any other variable name. For example,
struct complex z;
double *p = &z.re;
works without brackets, and the address of the real part of z is indeed passed to the pointer p.
Computing in C++ , Part I Dr Robert Nu ̈rnberg 7.2 Operations on structures
Although you can define any number of structures of all types, not all elementary operations are supported on structures in C. The only operations which are built in to the C language are:
• Accessing a member of a structure
• Taking the address of a structure
• Copying a structure for the purpose of: (i) assigning one structure to another one of the same type, (ii) passing a structure as an argument to a function, (iii) returning a structure a a result of a function call
This means that z1 = z2; is legal – it copies the value of every element of z2 into the same place in z1 – whereas z1 = 2.0*z2; is not legal. For the latter to make sense we have to use C++, and we will learn about that in part II of this course.
The reason why not more operations are defined in C is simply this. Structures can contain any mixture of variables of di↵erent types and there is no way that any operations other than direct copying could be guaranteed to make sense.
However, it is a simple matter to write your own functions for manipulating complex numbers. For example, to multiply two complex numbers, we can use the following code.
7.3
struct complex c_mult(struct complex z1, struct complex z2) {
struct complex val;
val.re = z1.re * z2.re - z1.im * z2.im;
val.im = z1.im * z2.re + z1.re * z2.im;
return val; }
Pointers and structures
Pointers to structures can be defined as for any other variable type. For instance, the previous imple- mentation of the function c_mult() can be changed to
struct complex c_mult(const struct complex *z1, const struct complex *z2) {
struct complex val;
val.re = z1->re * z2->re – z1->im * z2->im;
val.im = z1->im * z2->re + z1->re * z2->im;
return val; }
i.e. the two parameters z1 and z2 are now passed by pointer.
Notice the use of the -> operator. z1->re is entirely equivalent to (*z1).re, which means “dereference the address z1 to give a structure, then find the element re”. It is not equivalent to *z1.re, which assumes that z1 itself is a structure, with a member re and this member is then treated as a pointer and dereferenced by the * operator. Obviously, this wouldn’t make sense at all in our case.
To do away with the need for brackets when using pointers to structures, the -> operator was introduced.
7.4 Passing structures
Recall that whenever a variable is passed by value to a function, and whenever a function returns a function value, an extra local copy of the variables involved is created and the necessary values of the variables are copied across. Clearly this should be avoided when large structures are involved.
Hence large structures should always be passed by pointer or – when using C++ – by reference. Similarly, large structures should not be returned as function values. Instead, the result should be returned as an argument that is passed by pointer or by reference and that is then changed by the function.
7.5 typedef
As we have seen, the syntax for structures is rather clumsy, so it is convenient to define struct complex, for example, to be its own type. That means we want to give this new type a name and then be able to refer to this struct type by its new name. In C++ this is possible straight away. For instance, after defining the struct complex variables of this type can be declared as (⇤)
⇤In C this is not possible and one has to use typedef.
Computing in C++ , Part I Dr Robert Nu ̈rnberg complex z1, z2;
Note the omission of the keyword struct. In C, however, this is not possible. However, it is possible to give a structure a new name under which it can be referred to as a new type. This is done with the typedef command. So, to declare the new type Complex, one can write
typedef struct complex Complex;
This says that the type struct complex can now be referred to as a single keyword Complex. It is worth noting that typedef cannot only be used for structures, but also for any other type that you want to give a new name.
The typedef instruction can occur anywhere in the source file before it is used. It can even come ahead of the definition of the structure itself. Thus, in C we can re-write our complex number type as follows.
typedef struct complex Complex;
struct complex {
double re, im;
};
But as indicated before, when using C++ the definition of a new name for a struct type is not necessary. Hence in C++ the following example program shows how to implement the types and functions discussed earlier.
#include
using namespace std;
struct complex {
double re, im;
};
complex c_mult(const complex &z1, const complex &z2) {
complex val;
val.re = z1.re * z2.re – z1.im * z2.im;
val.im = z1.im * z2.re + z1.re * z2.im;
return val; }
int main() {
complex z1, z2, z3;
z1.re = 1.0; z1.im = 2.0;
z2.re = -1.0; z2.im = 0.2;
z3 = c_mult(z1, z2);
cout << "Result: " << z3.re << " + " << z3.im << "i." << endl;
return 0;
}
Note that the two arguments to c_mult() are passed by reference. 7.5.1 typedef and #define
You can think of typedef as a very special #define preprocessor directive. And in fact, most typedef instructions could be replaced by a corresponding #define directive. However, there are cases where using #define would give wrong results. The code below declares two variables of type pointer to char. Replacing the typedef with #define MY_STRING char * would cause the second variable to be of type char, rather than pointer to char.
typedef char * MY_STRING;
MY_STRING s, r;
In general, one should be very careful when using typedef for pointer types. This can easily lead to programming errors that will be hard to detect, as it will be unclear which level of dereferencing is necessary for a given object, especially when passed through a function. It is therefore advisable to only typedef pointer types in situations where this cannot lead to confusion.
7.6 Selfreferential structures
A very useful feature of structures is that they can refer to other members of their own type. This makes it possible to implement objects such as linked lists, where each member of the list contains a number of
Computing in C++ , Part I Dr Robert Nu ̈rnberg
attributes and a pointer to the next member of the list, and binary trees, where each member has up to two pointers to children elements “below”.
The syntax for this is very straightforward.
Suppose we have a number of airline passengers arriving for a flight. Each passenger possesses certain attributes, such as their check-in number, their final flight destination, the number of checked bags they have and so on. The structure for a list of passengers could hence look like this.
struct passenger {
int checkin_id;
char dest[3];
int bags[2];
int f_class;
string name;
passenger *next;
};
// check-in number
// 3 letter airport code
// bag label numbers for their 2 bags
// 1 = 1st, 2 = business, 3 = economy
// C++ string class for passenger name
// pointer to structure of next passenger
The whole passenger list will be represented by a pointer to passenger, which will hold the address of the first list element. Then each element points to the successor in the list, until the last element, where the pointer next should be set equal to NULL.
The beauty of this approach is that the number of passenger does not have to be known in advance. Furthermore, it is simple to add more passengers to the list as they arrive for the flight. Here we simulate adding a passenger treating the passenger list as a “stack”, so that the last member added is on top of the stack.
The following function would be called when a new passenger arrived.
passenger *new_passenger(passenger *p) {
passenger *new_p = new passenger;
/* Ask check-in clerk to enter details, or get them from a file
e.g. new_p->name = ….; new_p->f_class = …; etc */
new_p->next = p;
return new_p;
}
The function new_passenger() would be called with pass_list = new_passenger(pass_list);
since the function takes as an argument the head of the linked list of passengers and returns the new head of the linked list. Prior to the arrival of any passenger, the pointer pass_list must be set equal to NULL, so that the first call to new_passenger() assigns the value NULL to the next member of this passenger. This will subsequently mark the end of the linked list.
To see how many passenger are checked in for the flight in a given class, we would use a simple function such as the following.
int n_pass(passenger *p, int c) {
int count = 0;
while (p) {
if (p->f_class == c) count++;
p = p->next; }
return count;
}
The function traverses the list from beginning to end. It reads through the list of passengers and after dealing with each passenger moves on to the next one. This happens for as long as the while is true. However, when we get to the last passenger, which will have the value NULL stored at the member next, this value will be assigned to p and the while loop will terminate.
It is clear that a certain amount of care is needed to ensure that the ends of linked lists always have their next member as NULL. If this is not the case, the program will keep traversing through memory, trying to interpret whatever it sees as a structure. It will almost certainly produce garbage, and it will probably cause the program to crash.
Computing in C++ , Part I Dr Robert Nu ̈rnberg
7.7 Examples
7.7.1 Sparse matrices
A very good example for the use of pointers in scientific computing is the implementation of sparse matrices. Sparse matrices have only very few non-zero entries, usually of order O(N), where N ⇥ N is the dimension of the matrix. Hence the amount of data necessary to store the matrix can be greatly reduced, if only the non-zero elements of the matrix are stored. Furthermore, implementations of, say, matrix-times-vector multiplications will now only need O(N) operations, compared to O(N2) if all the elements of the matrix were stored explicitly.
The idea for the implementation considered here is to store for each row of the matrix the non-zero elements in that row. So the matrix itself is an array of pointers, and for each row the pointer points to a list of non-zero entries.
A simple implementation can then look like this.
#include
using namespace std;
struct entry {
int j;
double val;
entry *next;
};
// column number
// value of entry
// pointer to next entry
entry **create_matrix(int dim) {
entry **m;
m = new entry * [dim];
for (int i = 0; i < dim; i++) m[i] = NULL;
return m;
}
void add_entry(entry **m, int dim, int i, int j, double v) {
// adds entry A_ij to sparse matrix m
entry *p;
if (i < 0 || i >= dim) return;
for (p = m[i]; p; p = p->next) if (p->j == j) break;
if (!p) {
p = new entry; p->j = j; p->val = 0.0;
p->next = m[i]; m[i] = p;
}
p->val += v; }
void matrix_vector(double *y, entry **m, int dim, const double *x) {
// return y = A*x
for (int i = 0; i < dim; i++) {
y[i] = 0.0;
for (entry *p = m[i]; p; p = p->next) y[i] += p->val * x[p->j];
}
}
int main() {
entry **sparse_matrix;
double a, *x, *y;
int dim, i, j;
cout << "Dimension of matrix: "; cin >> dim;
sparse_matrix = create_matrix(dim);
x = new double[dim]; y = new double[dim];
for (i = 0; i < dim; i++) add_entry(sparse_matrix, dim, i, i, 1.0);
do {
cout << "Add entries to matrix A. Give i,j and A_ij: ";
cin >> i >> j >> a;
if (i >= 0 && j >= 0) add_entry(sparse_matrix, dim, i, j, a);
Computing in C++ , Part I Dr Robert Nu ̈rnberg
else cout << "Done." << endl;
} while (i >= 0 && j >= 0);
for (i = 0; i < dim; i++) { cout << i+1 << ". coordinate of x: "; cin >> x[i]; }
matrix_vector(y, sparse_matrix, dim, x);
for (i = 0; i < dim; i++) cout << i+1 << ". coordinate of y = " << y[i] << endl;
return 0;
}
Of course, there is a more sophisticated way. You could define the rows of the matrix to be a list of pointers itself. In this way, you would not store an empty row, like you would in the previous implementation. However, as this case occurs only for singular matrices, in practice the above implementation is usually good enough.
7.7.2 Binary trees
Binary trees can be used for binary sorting and for binary search in data. The idea is simple. A binary tree is made of nodes, where each node contains a left pointer, a right pointer, each of them pointing to a child node, and a data element that holds the data to be stored. The root pointer points to the topmost node in the tree, and hence defines the tree itself, similarly to the head pointer defining a linked list. The left and right pointers recursively point to smaller subtrees on either side. A NULL pointer represents a binary tree with no elements, i.e. the empty tree. A node with two NULL pointers is called a leaf.
Here is a simple sketch of a tree with 8 nodels, of which 3 are leaves. 5
2 @@ 7 l @l
@ @@ 4 l @ @@ 9 l l l @l
struct node {
Data *data;
int key;
node *left, *right;
};
// pointer to some important data
// the search key
// pointers to left and right child
@@
3 8 10
A binary search tree is a type of binary tree where the nodes are arranged in a certain order. This allows the tree to be used for sorting data and for searching in a data base. The order is defined as follows. Each node holds a search key that determines its position with respect to other nodes. This key can be a date of birth, a name or an internal id number. Then for each node, all elements in its left subtree hold a key that is less or equal to the node’s key, and all the elements in its right subtree hold a key greater than the node. The tree shown above is a binary search tree, the root node is 5, and its left subtree nodes (2, 3, 4) are smaller, while its right subtree nodes (7, 8, 9, 10) are bigger. Moreover, each of the subtrees below the root also obeys the binary search tree constraint.
A natural implementation of a binary tree in C would use the following structure.
The tree itself will be defined by its root, i.e. by a pointer
node *root;
Now all the methods for a binary search tree can be implemented with the help of recursive algorithms. For instance, to print the elements of the tree in ascending sorted order one can use the following function.
void print_tree(node *node) {
// prints binary search tree in ascending order
if (node == NULL) return;
print_tree(node->left);
cout << node->key; print_data(node->data);
print_tree(node->right);
}
Computing in C++ , Part I Dr Robert Nu ̈rnberg
This way of traversing the tree is also called “in-order”, because the node itself is inspected in between its two subtrees. Other ways of traversal are pre-order and post-order. The above function can be invoked with print_tree(root); where root is the previously defined pointer to the root node of the tree. Similarly, an algorithm to search for a certain entry can be implemented as follows.
node *find_node(node *node, int target) {
// searches binary search tree for node with key == target
if (node == NULL) return NULL; // target not found
else {
if (target == node->key) return node; // target found
else {
if (target < node->key) return find_node(node->left, target);
else return find_node(node->right, target);
}
} }
Finally note that sorting with the help of a binary search tree simply amounts to inserting the to be sorted elements into an initially empty tree. Of course, the crucial point is that all the elements are inserted in such a way, that the binary search tree criterion is not violated.
8 Advanced material 8.1 enum and union
Enumerations can be used by the programmer to define new types. They can make the code more readable and provide extra type checking. Very often integer constants are used in programs to di↵erentiate between di↵erent situations. Consider a situation where you want to assign a colour as an attribute to a certain object, say a car. One way of implementing this would be as follows.
const int white = 0;
const int blue = 1;
const int red = 2;
int car_colour = red;
Although this works perfectly fine, there is no intrinsic type checking taking place. In particular, the compiler does not know that you want to restrict the values for the integer variable car_colour to only the few valid colours. Hence assignments like car_colour = -123; are accepted by the compiler, although they might not make sense in your program.
But you can use an enum to have the compiler enforce this sort of type checking. In this example, one would define
enum colour {white, red, green, blue, black};
colour car_colour = red;
The compiler will now reject assignments like car_colour = 4. Depending on your compiler you may just get a warning, or you may get an error.
Internally, the enum values will be represented by some integer values. Usually, they will start with 0 for the first item, 1 for the second one and so on. The programmer does not really need to know about them. But in some cases, the programmer might want to assign special numerical values to each item so that they can switch between computations involving the numerical value of the enumerations and their name definitions. In this case, one can define the enumeration as follows.
enum colour {white = 0, red = 13, green = 27, blue = 31, black = 32};
colour car_colour = red;
Of course, the numerical values have to be distinct.
A union is a very technical feature of C/C++ and you will probably never have to use it. A union is similar to a struct in that it can hold several di↵erent data types. But in contrast to a struct, for a union at any one time only one of the data entries is active. Moreover, the entry that was assigned a value last is the only one that is valid, and it deletes any previous information held by the union. Declaring a union is like declaring a struct.
Computing in C++ , Part I
Dr Robert Nu ̈rnberg
union robot {
char *name;
int ammo;
int energy;
};
The size of a union is the size of its largest member. This is because a su cient number of bytes must be reserved for the largest sized member.
The unusual behaviour of a union is demonstrated by the following example.
#include
using namespace std;
union robot {
char *name;
int ammo;
int energy;
};
int main() {
robot r;
r.ammo = 20;
r.energy = 100;
cout << "Ammo = " << r.ammo << endl;
cout << "Energy = " << r.energy << endl;
return 0;
}
// ’100’
// ’100’
Note that the assignment to r.energy overrides any previous information held by the union. There are not many practical applications of the union concept. They mostly appear in source codes for system libraries, so you are unlikely to ever come across them.
8.2 Conditionals
A very e cient way to write some conditionals is to use the ? operator. The ? operator is a ternary operator, i.e. it takes three operands. The syntax is
expression1 ? expression2 : expression3
and this defines a conditional expression. If expression1 is true, then it evaluates to expression2, otherwise it evaluates to expression3. That means, some if-else statements can be written in a much shorter and more e cient way. For instance, the expression
z = a > b ? a : b;
is the same as
if (a > b) z = a;
else
z = b;
The operand expression2 in the conditional expression may be omitted. Then if expression1 is nonzero, its value is the value of the conditional expression. Therefore, the expression x ? : y has the value of x if that is nonzero, otherwise the value of y. This example is equivalent to x ? x : y, of course. The only useful application of this feature is when the expression1 itself is a function call, and this function should not be called twice. An example could be
z = count_students() ? : no_one_there();
8.3 Preprocessor directives
8.3.1 #define
We have already seen how to use the #define preprocessor directive in order to define simple macros.
But it can also be used to generate macro functions. For instance
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define SQR(a) ((a)*(a))
Computing in C++ , Part I Dr Robert Nu ̈rnberg defines a macro function that will return the maximum of two given value, and a macro that will return
the square of a given number. E.g.
double x = 3.1;
double y = MAX(x, 5.4);
double z = SQR(x);
Like any other preprocessor directive, these macro function definitions are expanded by the preprocessor just prior to the compilation of the source code. The #defined macros are active in the source file from the line where they were defined, and there they will be replaced with their intended meaning. It is important to enclose with brackets any appearance of a parameter for the macro in the definition of the macro. Otherwise the parameter name is going to be interpreted literally, and it is not going to be replaced by the value of the argument in the macro function call.
Note that you cannot use the same name for both a simple macro and a macro with arguments. Fur- thermore, note that by convention all macro names are capitalised. This makes it easier to identify them within the source code, and once again it makes debugging easier.
In the definition of a macro with arguments, the list of argument names follows in brackets and this must follow the macro name immediately with no space in between. If there is a space after the macro name, the macro is defined as taking no arguments, and all the rest of the line is taken to be the expansion. For example,
#define FOO(x) – 1 / (x)
defines FOO to take an argument and expand it into minus the reciprocal, whereas
#define BAR (x) – 1 / (x)
defines BAR to take no argument and always expand into “(x) – 1 / (x)”. Of course, the latter will not compile unless x is a valid variable wherever the macro BAR is used.
If you want to define a macro that is longer than a single line, you can use the “\” character to mark the end of a line. Here is an example
#define PRINT(a) { cout << "In what follows, we will output a parameter " \
<< "to a macro function." << endl; \
cout << "The parameter is: " << (a) << endl; }
8.3.2 #undef
It is also possible to undefine a macro, i.e. to cancel its definition. This is done with the #undef directive.
Here are two examples for an undefinition.
#undef PI
#undef MAX
Like definitions, an undefinition occurs at a specific point in the source file, and it applies starting from that point. The name ceases to be a macro name, and from that point on it is treated by the preprocessor as if it had never been a macro name. I.e. the macro name is always taken literally.
The main use for #undef is when redefining macros. Macros can simply be redefined with another #define directive, but this will usually result in a compiler warning. Using #undef before the redefinition will prevent that error message. For example,
8.3.3
#define LENGTH 100
double x1[LENGTH];
#undef LENGTH
#define LENGTH 200
double x2[LENGTH];
#if, #endif, #else and #elif
These directives allow to discard part of the code of a program if a certain condition is not fulfilled. Con- ditionals in the preprocessor are similar to if-else statements in C/C++, only that here the conditionals are evaluated only at compile time. Depending on their outcome, di↵erent versions of the program are compiled.
Typically preprocessor conditionals are used to allow a program to be compiled for di↵erent machine architectures and operating systems, or to be able to switch on/o↵ time consuming consistency checks and output of debugging information.
The #if directive in its simplest form consists of
Computing in C++ , Part I Dr Robert Nu ̈rnberg
#if expression
// some C code
#endif
Of course, expression may not contain any variable as it will have to be evaluated at compile time. Furthermore, it is restricted to a C expression of integer type and it may contain previously defined macros and all the known relational operators, like <, >, == etc..
The #else directive can be added to a conditional to provide alternative text to be used if the condition is false. This is what it looks like:
#if expression
code-if-true
#else
code-if-false
#endif
If expression is nonzero, and thus the code-if-true is active, then the code-if-false is ignored. Otherwise, the opposite is true.
Finally, the #elif directive can be helpful in nested conditionals. It simply means #else #if and is usually used to check for more than two possible alternatives. For example, you might have
#if DIM == 1
// code for 1d
#elif DIM == 2
// code for 2d
#elif DIM == 3
// code for 3d
#else
cout << "Error: not implemented for dimension " << DIM << endl;
#endif
8.3.4
parameter has been defined, independently of its value. Its operation is:
#ifdef name
// code here
#endif
For example:
#ifdef LENGTH
double x[LENGTH];
#endif
In this case, the line double x[LENGTH]; is only considered by the compiler if the defined constant LENGTH has been previously defined, independently of its value. If it has not been defined, that line will not be included in the program.
#ifndef serves for the opposite: the code between the #ifndef directive and the #endif directive is only compiled if the macro name that is specified has not been defined previously. For example:
#ifndef LENGTH
#define LENGTH 100
#endif
double x[LENGTH];
Here, if when arriving at this piece of code the macro LENGTH has not yet been defined it would be defined with a value of 100. If it already existed it would maintain the value that it had before.
8.4 Header files
Another preprocessor directive is the #include directive that we have already used in order to use system libraries. The syntax is either
#include
#ifdef and #ifndef
#ifdef allows that a section of a program is compiled only if the macro name that is specified as the
Computing in C++ , Part I Dr Robert Nu ̈rnberg #include “filename”
In both cases the compiler looks for a file called filename to be included in the source code. The only di↵erence between both expressions is the directories in which the compiler is going to look for the file. In the first case, the compiler searches for the file in the directories that it is configured to look for for the standard header files. In the second case, the file is looked for in the directory of the source file itself. Only in case that the header file cannot be found there, the compiler looks for the file in the default directories where it usually searches for standard header files.
Including a header file is used in order to be able to call functions that were defined elsewhere. A typical example are system libraries like
When the preprocessor comes across an #include directive, it simply inserts the contents of the given header file into the source file where it finds the directive. Usually a header file contains function declarations and macro definitions that you want to share between di↵erent projects, or it contains all the definitions belonging to a certain module of your project.
Here is an example. Let the file “matrix.h” contain the following declarations.
double **new_matrix(int n, int m);
double *new_vector(int n);
double *read_vector(int n);
void y_A_x(double *y, int n, double **A, double *x); // y = Ax
void print_vector(int n, double *x);
Then, assuming that the source file matrix.cc implements these functions appropriately, the header file can be used in the following fashion.
#include
#include “matrix.h”
using namespace std;
// use header file
int main() {
int n, m;
double **A, *x, *y;
cout << "Dimension of matrix A : "; cin >> n >> m;
A = new_matrix(n,m);
x = read_vector(n);
y = new_vector(n);
y_A_x(y, n, A, x);
print_vector(n, y);
return 0;
}
The advantages of using header files are obvious. Each time you have a group of related declarations and macro definitions all or most of which are needed in several di↵erent source files, it is a good idea to create a header file for them. We have seen that including a header file produces the same results as copying the header file into each source file that needs it. Such copying would be time consuming and error prone. With a header file, the related declarations appear in only one place. If they need to be changed, they can be changed in one place, and programs that include the header file will automatically use the new version when they are compiled next. The header file eliminates the labour of finding and changing all the copies as well as the risk that a failure to find one copy will result in inconsistencies within a program.
Note that when using di↵erent source and header files within your projects that only one of the source files may define a main() program.
8.5 Command line parameters
In C/C++ it is possible to accept command line arguments. Command line arguments are parameters that are passed to a program when it is called by the user. This is standard practice in linux/unix, but it is also possible under Windows. In oder to use this feature in C/C++, you need to know that the ANSI definitionfordeclaringthemain()functioninC/C++ iseither
// allocate matrix
// allocate vector
// read in vector
Computing in C++ , Part I Dr Robert Nu ̈rnberg int main()
or
This second version allows arguments to be passed from the command line. The integer parameter argc is an argument counter and contains the number of parameters passed from the command line, including the name of the program. The parameter argv is the argument vector which is an array of pointers to C-strings that represent the actual parameters passed. Here argv[0] is the name of the program itself, or an empty string if the name is not available. After that, every element number up to argc-1 are command line arguments. You can use each argv element just like a C-string. Note that argv[argc] is a NULL pointer.
The following example simply prints the name of the program followed by all the given arguments.
#include
using namespace std;
int main(int argc, char *argv[])
{
cout << "You called program " << argv[0] << " with arguments " << endl;
for (int i = 1; i < argc; i++)
cout << argv[i] << " ";
cout << endl;
return 0;
}
Finally, here is a common example for how command line arguments can be used. The given program takes the name of a (text) file and outputs the contents of the file onto the screen.
#include
#include
using namespace std;
int main(int argc, char *argv[])
{
if (argc != 2)
cout << "Usage: " << argv[0] << "