Leecture Notes On CPP For Java Programmers PDF
Leecture Notes On CPP For Java Programmers PDF
Jonathan G. Campbell
Department of Computing,
Letterkenny Institute of Technology,
Co. Donegal, Ireland.
email: jonathan dot campbell (at) gmail.com, jonathan.campbell@lyit.ie
URL:http://www.jgcampbell.com/cpp4jp/cpp4jp.pdf
Report No: jc/09/0011/r
Revision 3.0, minor edits, new references, new later chapters, 2009-08-12
12th August 2009
Contents
1
Introduction
1.1 Scope . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.2 Recommended Reading . . . . . . . . . . . . . . . . . . . . . . . . . .
1.2.1 Programming and Object-oriented Programming through C++
1.2.2 Games Software Engineering . . . . . . . . . . . . . . . . . . .
1.2.3 General Software Engineering . . . . . . . . . . . . . . . . . .
1.2.4 Design Patterns . . . . . . . . . . . . . . . . . . . . . . . . . .
1.2.5 The C Programming Language . . . . . . . . . . . . . . . . . .
1.2.6 Games Programming using C++ . . . . . . . . . . . . . . . .
1.2.7 Cross Platform Windows Programming . . . . . . . . . . . . .
1.2.8 Specification and Correctness . . . . . . . . . . . . . . . . . .
1.3 Plan for the course . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Tutorial Introduction to C++
2.1 Get Started . . . . . . . . . . . . . . . . . . . .
2.1.1 Your First C++ Program . . . . . . . . .
2.2 Variables and Arithmetic . . . . . . . . . . . . .
2.3 Do While . . . . . . . . . . . . . . . . . . . . .
2.4 Exceptions . . . . . . . . . . . . . . . . . . . . .
2.5 For Loop . . . . . . . . . . . . . . . . . . . . . .
2.6 Symbolic Constants and the Preprocessor . . . .
2.7 Character Input and Output . . . . . . . . . . .
2.7.1 File copying . . . . . . . . . . . . . . . .
2.7.2 Character counting . . . . . . . . . . . .
2.7.3 Line Counting . . . . . . . . . . . . . . .
2.7.4 Word Counting . . . . . . . . . . . . . .
2.8 Arrays . . . . . . . . . . . . . . . . . . . . . . .
2.9 Functions in more detail . . . . . . . . . . . . .
2.9.1 Declaration of functions . . . . . . . . .
2.9.2 Program Split into Modules . . . . . . .
2.10 Creation of Executables . . . . . . . . . . . . . .
2.10.1 Some Basics Compiling and Linking . .
2.10.2 Other Libraries . . . . . . . . . . . . . .
2.10.3 Static versus Shared Libraries . . . . . .
2.10.4 Make and Makefile . . . . . . . . . . . .
2.10.5 Interpreted Languages . . . . . . . . . .
2.10.6 Java Compiler AND Interpreter . . . .
2.10.7 Java Dynamic Linking . . . . . . . . .
2.10.8 Java Enterprise Edition (J2EE) and .NET
01
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
1
1
1
1
2
2
3
3
3
3
3
3
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
1
1
2
6
8
8
10
11
11
12
14
15
16
18
20
22
24
26
26
27
27
28
29
29
30
30
30
31
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
1
1
1
2
2
5
5
5
6
7
7
8
8
10
10
10
11
13
14
17
17
19
20
20
20
20
21
21
21
22
22
22
.
.
.
.
.
.
.
.
.
.
.
.
1
1
1
2
2
2
3
3
3
4
4
4
5
Control Flow
4.1 Introduction . . . . . . . . . . . . .
4.2 Statements and Blocks . . . . . . .
4.3 Selection . . . . . . . . . . . . . . .
4.3.1 Two-way Selection if else
4.3.2 Multi-way Selection - else if
4.3.3 Multi-way Selection switch
4.4 Repetition . . . . . . . . . . . . . .
4.4.1 while . . . . . . . . . . . . .
4.4.2 for . . . . . . . . . . . . . .
4.4.3 do - while . . . . . . . . . .
4.5 break and continue . . . . . . . . .
4.6 goto and Labels . . . . . . . . . . .
. . . .
. . . .
. . . .
. . . .
. . . .
- case
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
02
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
03
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
1
1
1
2
3
3
3
4
5
5
6
7
8
8
9
10
10
12
13
13
14
15
16
16
17
18
20
21
22
22
24
24
24
25
25
.
.
.
.
1
1
1
2
4
.
.
.
.
.
.
1
1
1
1
2
4
6
7.5
.
.
.
.
.
.
.
8
9
10
12
13
14
17
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
1
1
2
2
4
5
9
9
9
10
10
11
14
15
15
18
20
25
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
1
1
2
2
4
8
9
13
13
13
14
14
15
15
16
16
18
10 Operator Overloading
10.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
10.2 Lead-in Add Functions for Array . . . . . . . . . . . . . . . . . . . . . . . . .
10.3 Chaining calls to member functions . . . . . . . . . . . . . . . . . . . . . . . . . .
1
1
1
6
3D Affine Transformations . . . . . . . . . . . . . .
7.5.1 Homogeneous 3D Vector Vector4D.h . .
7.5.2 Homogeneous 3D Vector Vector4D.cpp .
7.5.3 Test for Vector4D.cpp . . . . . . . . . . . .
7.5.4 Homogeneous 3D Vector Transformations
7.5.5 Homogeneous 3D Vector Transformations
7.5.6 Test for Transform4D.cpp . . . . . . . . . .
Inheritance
8.1 Introduction . . . . . . . . . . . . . . . . . . . . . .
8.2 Example 1: Class Cell extended via inheritance . . .
8.2.1 First Attempt . . . . . . . . . . . . . . . . .
8.2.2 Protected . . . . . . . . . . . . . . . . . . .
8.2.3 Virtual Functions and Dynamic Binding . . .
8.3 Overriding (virtual functions) versus Overloading . .
8.4 Strong Typing . . . . . . . . . . . . . . . . . . . . .
8.5 Inheritance & Virtual Functions Summary . . . .
8.6 Inheritance versus Inclusion, is-a versus has-a . . . .
8.7 Inheritance & Virtual Functions and Ease of Software
8.8 Example 2: a Person class hierarchy . . . . . . . . .
8.9 A Student Class . . . . . . . . . . . . . . . . . . . .
8.10 Use of the Person Hierarchy . . . . . . . . . . . . .
8.10.1 PersonT1.cpp . . . . . . . . . . . . . . . . .
8.10.2 PersonT2.cpp . . . . . . . . . . . . . . . . .
8.11 Standard Library Vector . . . . . . . . . . . . . . . .
8.12 Marks . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
Transform4D.h .
Transform4D.cpp
. . . . . . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
Extension
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
04
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
10.4 Operators . . . . . . . . . . . . . . . . . . . . . . .
10.5 Member versus Non-member Functions, Conversions
10.5.1 Introduction . . . . . . . . . . . . . . . . . .
10.5.2 A String Class . . . . . . . . . . . . . . . . .
10.5.3 Member versus Non-member functions . . .
10.5.4 Coercion of Arguments . . . . . . . . . . . .
10.5.5 Constructors for conversion explicit . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
7
14
14
14
19
22
22
11 Templates
11.1 Introduction . . . . . . . . . . . . . . . . . .
11.2 Template Functions . . . . . . . . . . . . . .
11.2.1 Overloaded Functions recalled . . . .
11.2.2 Template Function . . . . . . . . . .
11.3 Polymorphism Parametric . . . . . . . . . .
11.4 Template Array . . . . . . . . . . . . . . . .
11.4.1 Class declaration and implementation
11.4.2 A simple client program . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
1
1
2
2
2
4
4
4
7
12 Array Containers
12.1 Introduction . . . . . . . . . . . . . . . .
12.2 An Array class . . . . . . . . . . . . . . .
12.2.1 Major points to note in Array.h .
12.2.2 Iterators . . . . . . . . . . . . . .
12.3 A Simple Client Program for Array . . . .
12.4 The Big-Three . . . . . . . . . . . . . .
12.4.1 Defence against naive defaults . .
12.5 Overloading the Stream Output Operator
12.6 std::vector . . . . . . . . . . . . . . . . .
12.6.1 Points to note in vectort1.cpp . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
1
1
1
8
10
12
16
16
17
17
20
.
.
.
.
.
.
.
.
.
.
.
.
1
1
3
3
5
7
11
15
19
20
24
31
31
1
1
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
13 Linked Lists
13.1 Introduction . . . . . . . . . . . . . . . . . . . . .
13.2 A Singly Linked List . . . . . . . . . . . . . . . . .
13.2.1 Class Declaration . . . . . . . . . . . . . .
13.2.2 Dissection of List . . . . . . . . . . . . . .
13.2.3 Class Implementation . . . . . . . . . . . .
13.3 A simple List client program . . . . . . . . . . . .
13.4 Doubly Linked List . . . . . . . . . . . . . . . . .
13.4.1 Brief Discussion of the Doubly Linked List
13.4.2 Simple Test Program, ListT1.cpp . . . . .
13.4.3 Doubly Linked List Implementation . . . .
13.5 Arrays versus Linked List, Memory Usage . . . . .
13.6 Arrays versus Linked List, Cache issues . . . . . . .
05
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
1
1
4
5
6
6
8
10
10
11
17
18
B Analysis of Algorithms
B.1 O Notation (Big-oh) . . . . .
B.2 Estimating the Growth Rate of
B.2.1 Simple Statements . .
B.2.2 Decision . . . . . . . .
B.2.3 Counting Loop . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
1
2
3
3
3
4
. . . . . . . .
an Algorithm
. . . . . . . .
. . . . . . . .
. . . . . . . .
06
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
Chapter 1
Introduction
1.1
Scope
These are course notes for a lecture series on C++ for Java Programmers, given in second year of
of the course BSc in Computer Games Programming, School of Computing, Letterkenny Institute
of Technology.
This course originated in a course on Object-oriented Programming using C++ developed at
University of Ulster during 19971999. In revising the course during 20062009, I have done my
best to modernise the C++ to include more the standard library (STL) and other additions to
C++ that have occurred since 1999. In that respect, the course is strongly influenced by (Koenig
& Moo 2000) and (Stroustrup 2009).
We note that the C++ standard library allows one to program at a much higher level. Roughly
translated, higher level means that program statements: (a) do a lot more than the equivalent low
level statement; (b) are less error prone; (c) are easier for humans to read and understand. Our
chief encounter with standard library features will be in the use of collections like vector and with
algorithms with which one can operate on those collections. Java programmers who have used
List and ListArray and algorithms such as sort will have no difficulty becoming familiar with
this aspect of C++.
The earlier course contained a few chapters on the principles of object-oriented programming
(OOP); Im leaving those out, preferring to believe that students of this course will already know
that, or know nothing other than OOP, or will get a dose of OOP theory from some other lecturer.
If you participated in the Games Programming 1 team project, you have encountered nearly as many
principles of OOP as you will ever need.
1.2
1.2.1
Recommended Reading
Programming and Object-oriented Programming through C++
If I had to recommend a teach-yourself modern C++ book, Id go for either (Stroustrup 2009) or
(Koenig & Moo 2000). (Glassborow 2006) is also useful. (Dawson 2004) is a good introduction
to the basics from a games point of view,
11
If you need a reference on C++, see (Stroustrup 1997a), (Lippman 2005), and (Lischner 2003).
The C++ FAQ (Cline, Lomow & Girou 1999a) is useful, and there is an online version (use Google
to find it).
I have learned a lot of what I know about OOP and C++ from (Budd 1997a). Budd produces
very fine books. I can also recommended his book on Java (Budd 1999b) and his book on general
object-orientation (Budd 1997b).
Budds book on conversion from C++ to Java (Budd 1999a) is useful for this course.
You will already be familiar with Horstmanns book Java Concepts (Horstmann 2005) (or Big Java
which is the same with four chapters added). Horstmanns (with Budd) Big C++ (Horstmann &
Budd 2005) is a fine book.
We note Horstmanns handy website (Horstmann accessed 2006-08-28b) provides another good
reference for those who need to move between C++ and Java; and Horstmanns C++ Pitfalls
website at (Horstmann accessed 5006-08-28a), though some of the items paint C++ is a very bad
light.
There are very useful guidelines in: (Meyers 2005) and (Meyers 1996); a companion book (Meyers
2001) covers now the C++ standard library.
Regarding the standard library and algorithms, I note that we have a third year module on Algorithms
and Data Structures for Games Programmers (Campbell 2009); two of the chapters at the end of
these notes overlap with the beginning chapters of the notes for that module.
Standard Library (STL) When Im using the C++ Standard Library (STL) I always have (Reese
2007), (Josuttis 1999), and (Lischner 2003) at my right hand.
Other Books Other top class C++ books that I regularly use are (in no particular order): (Sutter
& Alexandrescu 2005) (Dewhurst 2005) (Wilson 2004) (Romanik & Muntz 2003) (Sutter 1999)
(Sutter 2002) (Sutter 2004) (Dewhurst 2003) (Josuttis 1999) (Eckel 2000) (Eckel 2003).
If you want fancy (general programming) ideas for a final year project and to see the way C++
and OOP is headed, see: (Alexandrescu 2001) and (Czarnecki & Eisenecker 2000).
1.2.2
The following cover software engineering and design principles applied to games: (McShaffry 2005)
(Dickheiser 2007) (OLuanaigh 2006).
1.2.3
Apart from the good advice in the programming books, see (McConnell 2004) and (Maguire 1993).
12
1.2.4
Design Patterns
Software design patterns are important. Horstmanns (Horstmann 2006) and (Freeman & Freeman
2004) are good, even though the examples are in Java. See also (Budd 1999b, chapter 5), the
original gang-of-four book (Gamma, Helm, Johnson & Vlissides 1995) and (Vlissides 1998).
1.2.5
When we come to OpenGL we will sometimes write C programs; C and C++ are different. The way
we will do it is learn C++ first and then pint out the major differences. (Kernighan & Ritchie 1988)
is the bible of C; (Harbison & Steele 2005) is a good reference.
1.2.6
The following are as much on principles and software engineering as on construction of specific
games and game components: (McShaffry 2005), (OLuanaigh 2006) and (Dickheiser 2007).
(Dawson 2004) would be a good way to learn basic C++ (i.e. just plain text programming) before
getting into the difficulties of video games.
(Penton 2003) is a good way to learn simple video game programming; at the end of this course
we will work on a simple 2D sprite game from that book.
1.2.7
wxWindows (Smart & Csomor 2005) and Qt (Blanchette & Summerfield 2008); (Blanchette &
Summerfield 2008) contains a nice chapter on using OpenGL (the API that we use for our graphics
modules) in a Qt framework.
1.2.8
For future reference, I note that specification and correctness are important issues. Meyer uses
the contract metaphor Design by Contract (Meyer 1996); see also (Mitchell & McKim 2002),
(Tennent 2002) and (Fitzgerald & Larsen 1998).
1.3
The first thing to be done is get comfortable with the basics of C++ and with whatever C++
compiler we choose; we will probably be Visual Studio Express, see chapter 2 and we will use the
examples in chapter 2.
We will have a quick look at designing procedural programs, see Appendix A.
13
Then we will look at some more details of C++ up to programming simples classes; then more
complex classes: a dynamic array and a linked list.
Finally we will look at a C++ version of the game software we finished off with last year, for
example Sprites and Animations just to see how C++ handles objects, rather than develop real
game code.
14
Chapter 2
Tutorial Introduction to C++
This chapter is based on the first chapter of Kernighan & Ritchies book on C (Kernighan &
Ritchie 1988), but with the code changed to C++. In this chapter and the next few, you will be
exposed to the basic syntax of C++ mostly the underlying imperative language features.
In the early days, most programmers had learned C before they came to C++, hence, it was often
assumed that the best way to learn C++ was to first learn C. It is fairly clear now that this is
not ideal; indeed, there is reason to believe that object-oriented programming can be quite difficult
for experience imperative language programmers to grasp. Moreover, there are difficult parts of C
that can be avoided until much later in C++ and if proper design is used, they can be fairly well
hidden in localised parts of the code.
2.1
Get Started
Objectives
Get used to editor, compiler, linker etc. In our case the Visual Studio Express IDE (free) or
Visual Studio Professional.
Get a simple C++ program running.
Dissect this simple C++ program.
All the programs in these notes and programs mentioned in practicals and assignments, plus other
bits & pieces will be available in my public folder. For example, programs from this chapter will be
in P:/cpp4jp/progs/ch02.
21
2.1.1
The program hello.cpp is a C++ program that prints a simple message on the screen.
//----- hello.cpp ---------------------------------//Your first C++ Program illustrating stream output.
//------------------------------------------------#include iostream
/* this is a begin-end comment*/
int main()
Dissection of hello.cpp
1. Every program must have a function called main, execution starts there. main may call other
functions. As with Java or any other high-level language, these functions can be taken from
one of the following sources:
As written by the programmer in the same file as main.
As written by the programmer in separate file(s).
Called from predefined libraries.
2. C++ allows you to create programs without any class; thus the function main above does
not need to be enclosed in a class called Hello;
3. C++ takes the view that main is called by the operating system; the operating system may
pass arguments to main, as well as receive return values (e.g. return 0;); however, above,
we choose to make main take no arguments either main(), or verbmain(void)+ means
takes no arguments. Note, in C, the explicit void is essential if that is what you want.
4. /*... */ is a comment that has start and end delimiters. The C++ standard says that
comments cannot be nested. Also, comments like these must be terminated explicitly, i.e.
newline does not terminate them this is a common source of compiler errors, and, for the
unwary, can be very difficult to trace.
5. // comments end at end of line.
6. iostream is the header for the hierarchy of stream classes which handle buffered and unbuffered I-O for files and devices. cout is an object, corresponding to the standard output
stream.
7. Although you #include iostream which contains declarations of cout and endl, these
are contained in what is called a namespace; that namespace is std, for standard library.
8. A namespace is a bit like a Java package, but not completely.
22
9. If you want to avoid using the std:: qualifier, you can insert a using namespace std;
directive at the top of your program. using namespace std; has some similarities to a
Java import.
10. #includes like #include iostream occur rather like Java imports, but, deep down, they
are quite different.
11. Well not go into the full details of #include and namespace here; they full story will
eventually become clear.
12. The topic we are touching on here, but avoiding for the meantime, is called scope; well
address that in detail in a later chapter (chapter 5); essentially, the scope of a variable or
of a function is the parts of a program where the name of the variable can be used (i.e. is
meaningful). Because std is a namespace all the variables and functions in it are hidden
to the outside world, except if one uses the qualifier std::, or gives the overall directive
using namespace std.
13. So the following program is equivalent to the previous hello.cpp.
//----- hello.cpp ---------------------------------#include iostream
using namespace std; // brings whole std namespace into scope
int main()
14. C++ allows you to write functions as operators; so, for example, you can write your own +
operator instead of a function called plus.
15. The operator is one such operator; note, it is nothing to do with bit shifting; the bit shifting
operator has the same name but it does something very different; the program context allows
the compiler to select between operators of the same name as context allows resolution
between functions of the same name.
16. The operator (call it put to) takes the value that follows it (the string) and puts it to
the object cout.
17. Recall Javas System.out.print.
cout Hello from C++.n;
is very similar to
System.out.print(Hello from C++.n);
where cout is equivalent to System.out and the method (function) print is equivalent to
(the operator) .
23
Error signifies a serious flaw in the source code, which the compiler cannot circumvent. In the
event of any error, the compiler can produce no usable object code.
Warning, as the name suggests, is the compilers way of saying are you sure about this; examples
are: variables defined but never used (fairly benign), type conversions that look a bit flaky, etc.
I maintain that C and C++ compiler warnings must never be ignored, and insist that students heed
this principle, e.g. I refuse to help to check faulty code until the code compiles without warnings.
The good news is that C++ has much stricter type checking than C, so that many warnings in C,
become errors in C++.
25
2.2
Program ptog.cpp shows some simple arithmetic, involving a function, and a while loop.
//----- ptog.cpp ---------------------------//Convert pounds to grams
//------------------------------------------#include iostream
using namespace std;
const double pToG = 453.592; // const == Java final
double convert(double p)
return p*pToG;
int main()
return 0;
Dissection of ptog.cpp
1. All variables must be declared. They may be declared anywhere before they are used. They
stay in scope until the end of the function or block ... in which they were declared.
2. In C declaration can be done only at the beginning of a function or block.
3. Some programmers retain the C style (declarations at beginning); however, when large objects
are involved, which may use large complex constructors, declaration at the beginning may
26
avoid duplication of effort because, at the beginning, appropriate initialisation may not be
available, and the object will be initialised twice.
4. Built-in types in C++:
int: integer, can be 16-bits, usually 32-bits.
float: floating point, normally IEEE format, 32 bits.
char: single text character; really it is a small integer taking on values in [0..255]; also
C++ allows arithmetic on chars.
short: short integer, possibly 8 bits, maybe 16.
long : long integer 32 bits.
double: double precision floating point more significant digits, larger exponent.
C++ allows you to qualify integer types a unsigned, meaning that they range from
0 to some large value; for example a 16-bit unsigned int would range 0 to 65536,
whereas a 16-bit int (signed) would range 32768 to +32767.
5. const qualifier means compiler disallows modification.
6. pToG is declared outside any function; it has global scope; it also has static lifetime; well
discuss lifetime in some detail later.
7. Stream I/O can handle a variety of simple values without needing additional formatting
information.
8. Pronounce as get-from standard-input stream, cin.
9. Assignment statement uses =; note possible confusion with test for equality which is ==
in C++; but, unlike Java, something like if(x = y) is in fact syntactically legal, but most
likely a logical error; more later about that.
10. while loop:
condition is tested;
if condition is true body is executed;
go back to beginning;
if condition is false execution continues at the statement following the loop.
11. Note the textual layout style used: as mentioned earlier, my suggestion is to indent each
block by 2 characters some use the next tab, but with that you very quickly get to the
right-hand side of the page; on the other hand, we have to be able see what is part of a block
and what isnt.
12. For me it is essential that the closing brace lines up with w of while.
13. Some like to move the begin to the next line to line up with the w of while; Okay by
me.
14. Mixing of operands is allowed in C++, e.g. gint=g;; most compilers will generate a warning.
Normally, it is good practice to make type conversion explicit, this can be done with a type
cast, thus: gint=int(g);
15. More about casts later.
27
2.3
Do While
ptog.cpp is better written using do ... while; we can cut out repetition of the input and output.
//----- ptogdw.cpp ---------------------------//Convert pounds to grams -- do .. while
//------------------------------------------#include iostream
using namespace std;
const double ptog = 453.592;
double convert(double p)
return p*ptog;
int main()
int pds;
do
cout Input pounds: endl;
cin pds;
double g;
g = convert(pds);
cout nWeight = g grams. endl;
while (pds 0);
return 0;
2.4
Exceptions
I doubt if you have covered exceptions yet in Java, but it will happen soon.
As I mentioned, if in ptogdw or ptog you type something like 2.5, the program will probably give
the answer for 2 and then fail in some way; that is because it attempts to decode . as an int
and consequently get confused.
The least we can do is detect the failure and inform the user. Later we will show how to make
programs like this detect the error and request a new input.
28
int main()
try
int pds = 0;
do
cout Input pounds: endl;
cin pds;
if(not cin) throw std::exception();
double g = convert(pds);
cout nWeight = g grams. endl;
while (pds 0);
catch(...)
cerr An exception was thrown.n;
return 0;
Brief Dissection
1. if(not cin) throw std::exception(); As well as getting data from the input stream,
decoding it, and assigning the result to pds, cin also returns a boolean value that is true
when cin is in an error-free state; false if in an error state like when it attempts to decode
a decimal point in an expected int.
2. if(not cin) tests to see if what is returned is false.
3. if(cin==false) is equivalent;
4. if(!cin) is equivalent; not is relatively new to C++; before we always used !.
5. The two possible values of a boolean variable are true, false. bool is the type name.
6. The exception message is not very helpful, but its a lot better than the program acting like
a dog whose toe you trod on.
7. Exceptions are relatively new to C++, so some textbooks may not contain them.
29
2.5
For Loop
return p*ptog;
int main()
cout Pounds ttgrams tt kg. endl;
for(int pds = 0; pds 10; pds++)
double g=convert(pds);
int gint = int(g);
cout pds tt gint tt g/1000 endl;
return 0;
Dissection
1. Note use of tab t to align the table; this is one occasion where tabs are actually useful.
2. Nothing new here for Java programmers; however, Im going to go into detail just in case
people need revision.
3. For statement and loop:
4. There are three statements contained within the (.,.,.) in a for statement:
1 int pds=0 initialisation.
2 pds10 loop continuation test; evaluate the condition if true execute the body
otherwise finish.
3 pds++ iteration; do this after the first iteration; for subsequent loops, do it before
evaluation of the continuation condition;
5. That is, for(initialisation; continuation test; iteration);
6. The for(...) body may be a single statement or a block / compound-statement ....
210
2.6
2.7
(Well see how much of this section we need to cover; we may run through it rather rapidly. Also,
for brevity, Im leaving exceptions out of these programs.)
As with C, C++ input-output, including files, deals with streams of characters, or text-streams.
This is the UNIX model of files which includes keyboard and screen.
A text-stream is a sequence of characters divided into lines.
A line is zero or more characters followed by a newline character, n.
The standard input-output library must make each input-output stream conform to this model
no matter what is in the physical file.
Echoed input. When you type a character on the keyboard, the computer input-output
system immediately echoes it to the screen; immediately means before it is presented to the
reading program, see buffered, below. Incidentally, this means that the input-output system,
itself, does not display the typed character what is displayed is what is echoed from the
host computer).
Buffered input. While a program is reading from a keyboard or a file, the computer stores
all the input characters in a buffer (array) and presents the array (line) of characters to the
reading program only after Enter has been typed.
211
Buffered output. While output is being produced, the computer stores all the characters in
a buffer (array) and presents the array (line) of characters to the output device only after
new li ne or endl has been reached.
Terminal input-output and I-O Redirection C++, C, and UNIX have unified view of text inputoutput. Reading from the keyboard is like reading from a file device stdin in C. So, when we
talk about file I/O below, we include terminal I/O. NB. ctrl-d is end-of-file for a Linux / UNIX
keyboard; I think it might be ctrl-z on Windows cmd.
If you want to test the programs using files, you can use input-output redirection.
Then, $$ prog test.dat (assuming the prompt is $$).
reads from test.dat; i.e. the redirects the program to read from the file instead of the keyboard.
If you want to send the output to a file, say testout.dat,
prog test.dat testout.dat
2.7.1
File copying
Program cio1.cpp shows how to copy from an input file (or keyboard) to an output file (or
screen).
//----- cio1.cpp -----------------------------------// copy input to output, version 1.
// j.g.c. 1/2/97, 2006-09-06
//--------------------------------------------------#include iostream
using namespace std;
int main()
char c;
while(cin.get(c))
cout c;
return 0;
You can execute it simply by cio1 and typing characters. Terminate by ctrl-d end-of-file, but
make sure the program is executing before you do this otherwise ctrl-d logs you out!
Alternatively, create a file test.dat, e.g. simply use
copy cio1.cpp test.dat
and then use input-output redirection, see 2.7:
cio1 test.dat testout.dat
212
Dissection
1. while(cin.get(c)) both reads the next character into c and tests whether cin.get()
returns true OK, or false, indicating some problem, e.g. end-of-file.
2. When false is returned, we quit the loop.
3. C programmers please note, its char c;, not int c;, since in C++, there is no need to
code end-of-file in the character itself.
What is the problem with cin c as in cio2.cpp?
If you run cio2.cpp you will find that cin c loses white-spaces, i.e. newline, tab, and space
itself.
//----- cio2.cpp -----------------------------------// copy input to output, version 2, wrong!
// j.g.c. 1/2/97, 2006-09-06.
//--------------------------------------------------#include iostream
using namespace std;
int main()
char c;
while(cin c)
cout c;
return 0;
213
2.7.2
Character counting
Program ctch1.cpp shows how to count the characters in the input stream, stopping at end-of-file
and presenting the count.
//----- ctch1.c ------------------------------------//counts input chars; version 1
// j.g.c. 1/2/97, 2006-09-06
//based on K&R p.18.
//--------------------------------------------------#include iostream
using namespace std;
int main()
char c;
long nc=0; //long to make sure it is 32-bit int.
while(cin c)
++nc;
cout nc;
return 0;
Dissection
1. ++ operator increment by one; -- is decrement by one. On their own, post-increment ++nc
and pre-increment nc++ are equivalent, but give different values when used in expressions.
E.g.
int i,n=6;
i=++n; /*POST-increment gives i==7, and n==7*/
but
int i,n=6;
i=n++; /*POST-increment gives i==6, and n==7*/
2. long (int) is at least 32 bits long.
214
2.7.3
Line Counting
Program clines.cpp shows how to count the lines in the input stream, stopping at end-of-file
and presenting the count.
//----- clines.cpp -----------------------------------// count lines in input.
// j.g.c. 1/2/97, 2006-09-06.
//--------------------------------------------------#include iostream
using namespace std;
int main()
char c;
int nl = 0;
while(cin.get(c))
if(c==n)++nl;
cout nl endl;
return 0;
Dissection
1. if statement:
Tests the condition in (..).
If true executes statement or group .. following.
Otherwise (false) skips them.
Or, can add else, e.g. else cout char not a newline;
2. == denotes comparison operator is-equal-to. Caution: = in place of == can be syntactically
correct and so need not trapped by compiler a nasty hard-to-find error results!
3. Character constants, e.g. n represents a char value equal to the value of the character
in the machines character set; In ASCII, e.g.: n == 10 decimal, A == 65 decimal. You
should never use numeric values, they might change, and render your program non-portable.
215
2.7.4
Word Counting
For the purposes of this program, a word is any sequence of characters that does not contain a
white-space character i.e. blank-space, tab, or new-line.
Program clwc.cpp shows how to count lines, words, chars in the input stream, stopping at endof-file and presenting the counts.
//----- clwc.cpp -----------------------------------// count lines, words, and chars in input.
// as in K&R -- a word is simply some chars delimited by
// whitespaces
// j.g.c. 1/2/97
//--------------------------------------------------#include iostream
using namespace std;
int main()
char c;
int nl,nw,nc;
bool out = true; //outside a word
nl = nw = nc = 0;
while(cin.get(c))
++nc;
if(c==n)
++nl;
if(c== c==nc==t)
out=true;
else if(out)
out=false;
++nw;
cout nl nw nc endl;
return 0;
Dissection
1. nl = nw = nc = 0; In C++, an assignment has a value, i.e. nc=0; has the value 0, which
is, in turn, assigned to nw etc..
2. denotes Boolean or
3. && denotes Boolean and
4. Note form of if...then...else you do not write then.
216
if(expression)
statement1
else
statement2
As usual statement can be a compound statement ...
217
2.8
Arrays
Program cdig.cpp shows a program to count the occurrences of each numeric digit, of whitespaces and of all other characters together without using 12 named counters!
//----- cdig.cpp -----------------------------------// counts digits, white-spaces, others
// after K&R chapter 1
// j.g.c. 2/2/97, 2006-09-06.
//--------------------------------------------------#include iostream
using namespace std;
int main()
char c;
int i, nWhite = 0, nOther = 0;
int nDigit[10];
for(i = 0; i 10; i++)
nDigit[i] = 0; //variable need to be explicitly initialised
while(cin.get(c))
if(c=0 && c=9) //is it a digit?
++nDigit[c-0];
else if(c== c==nc==t) // a white space?
++nWhite;
else
++nOther; //otherwise!
Dissection
1. int ndigit[10]; defines an array of 10 ints.
2. Unlike in Java, C++ array is not an object;
3. Notice no int[] nDigit = new int[10];
4. for(...) loops must go 0,1 . . . 9, since, in C++, array subscripts must start at 0; same as
Java.
5. The C++ code pattern to loop over the first n elements of an array is:
for(int i=0; in; i+)+.
218
219
2.9
Program Tr5a.cpp shows a program based on Tr5.cpp from Appendix A, with a few modifications.
Well use it as a case-study to exemplify many aspects of the use of functions.
/*----- Tr5a.cpp --------------------------------j.g.c. 2003/11/29, 2006-09-06.
based on Tr5.cpp, see that program for proper comments
and analysis
--------------------------------------------------*/
#include iostream
using namespace std;
void nl()
cout n;
int stars(int n)
int nc = 0;
for(int i= 0; i n; i++)
cout *; nc++;
int spaces(int n)
for(int i= 0; i n; i++)
cout ;
return n;
int main()
int h = 5;
int nSp = 0;
int nSt = 0;
int nNl = 0;
for(int j= 0; j h; j++)
nSp += spaces(h - 1 - j);
nSt += stars(j + 1);
nl(); ++nNl;
220
Dissection
1. A function definition consists of:
return-type function-name(parameter list -- if any)
221
2.9.1
Declaration of functions
Program Tr5b.cpp shows a version with altered layout; here weve kept main at the beginning,
and functions at the end; consequently, weve had to declare functions stars etc. before they are
called.
C++ demands that functions be declared before they are called.
/*----- Tr5b.cpp --------------------------------j.g.c. 2003/11/29, 2006-09-06.
based on Tr5a.cpp
--------------------------------------------------*/
#include iostream
using namespace std;
void nl();
int stars(int n);
int spaces(int n);
int main()
int h = 5;
int nSp = 0;
int nSt = 0;
int nNl = 0;
for(int j= 0; j h; j++)
nSp += spaces(h - 1 - j);
nSt += stars(j + 1);
nl(); ++nNl;
void nl()
cout n;
int stars(int n)
int nc = 0;
for(int i= 0; i n; i++)
cout *; nc++;
int spaces(int n)
for(int i= 0; i n; i++)
222
cout ;
return n;
Dissection
1. int stars(int n) declares the type of stars; this is called the prototype for stars;
2. int stars(int n) is a declaration; declares the type of stars;
3. On the other hand,
int stars(int n)
int nc = 0;
for(int i= 0; i n; i++)
cout *; nc++;
return nc;
223
2.9.2
Program Tr5c.cpp shows the new main program; here, we #include the function declarations
in funs.h, but N.B. not their definitions / implementations).
Now, the program and functions are compiled and loaded using:
Notice that .cpp files are compiled, and not #included. .h files, on the contrary, are #included,
but not separately compiled; (a compiler may choose to (pre-)compile .h header files, but thats
an optimisation that well ignore here.
/*----- Tr5c.cpp --------------------------------j.g.c. 2003/11/29, 2006-09-06.
based on Tr5b.cpp, split into modules / files.
--------------------------------------------------*/
#include funs.h
using namespace std;
int main()
int h = 5;
int nSp = 0;
int nSt = 0;
int nNl = 0;
for(int j= 0; j h; j++)
nSp += spaces(h - 1 - j);
nSt += stars(j + 1);
nl(); ++nNl;
Files funs.h and funs.cpp show, respectively, the function declarations and the definitions (implementations).
/*------ funs.h --------------------used by Tr5c.cpp
j.g.c. 2006-09-06
------------------------------------*/
#ifndef FUNSH
#define FUNSH
#include iostream //good idea to have this here, funs.cpp needs it
void nl();
int stars(int n);
int spaces(int n);
#endif
224
int stars(int n)
int nc = 0;
for(int i= 0; i n; i++)
cout *; nc++;
int spaces(int n)
for(int i= 0; i n; i++)
cout ;
return n;
Dissection
1. When using a library or external module, you must get into the habit of producing a header
.h file that contains declarations of the functions.
2. Not unreasonably, C++ demands that you declare functions before you call them.
3. The prototypes of standard library functions and standard classes are contained in header
files, e.g. iostream is just an ordinary text file containing the stream classes and functions.
4. .h files get compiled as part of any file in which they are included. Recall, the C++ Preprocessor executes all # commands before the compiler proper sees the source code.
When you have separate modules or compilation units, obviously, the compilation & linking procedure must be modified.
More of this in section 2.10.4.
Im going to use command line here; the same will happen in Visual Studio, but the details will be
hidden.
225
2.10
Creation of Executables
2.10.1
Source programs like Tr5b.cpp, Tr5c.cpp etc. are not directly executable. There are a number
of stages in creation of the executable, and executing it.
Compilation
The first stage of creating an executable is to compile Tr5c.cpp into object code.
g++ -c Tr5c.cpp
g++ -c funs.cpp
This compiles and puts the object code into a files Tr5b.o and funs.o. This object code is
essentially machine code. It has most the building blocks, except the system code, which is in
libraries.
Linking The second stage is to link the machine code in Tr5c.o with appropriate library code:
the put the building blocks together.
This produces the executable file Tr5c, or Tr5c.exe in Windows.
Execution
Tr5c.
This reads the contents of exe into memory, and starts execution at an appropriate start address.
Compilation & Linking Summary The situation may be made more clear-cut if we look at
the two module program Tr5c.cpp, funs.cpp. Recall,
g++ -c Tr5c.cpp
g++ -c funs.cpp
# yields Tr5c.o
# - funs.o
226
+----------+
library
+----------+
compiler
+---------+
g++ -c
+--------+
Tr5c.
+----------------------- Tr5c.o
cpp
+---------+
+--------+
+---------+
g++ -c +--------+
+---------+
+--------+
V
V
V
+--------------------------------+
linker
+--------------------------------+
V
executable: Tr5c.exe
2.10.2
Other Libraries
Not all system functions are in the default libraries that are searched by g++ and ld. For example,
maths functions like sqrt. If you were to call sqrt in Tr5c, you would have to explicitly mention
the mathematics library, using -lm:
g++ -o Tr5c.exe Tr5c.cpp funs.cpp -lm
You must also remember to #include the appropriate header file, e.g. #include math.h.
Likewise for libraries to do with OpenGL functions.
2.10.3
In section 2.9.2, the code for stars, spaces, and nl is linked statically, i.e. the object code for
each of the functions is copied into the file Tr5c.exe. For common functions like cin.get this
can become wasteful; hence shared libraries, in which the linker inserts just a pointer to shared
code that is already in the operating system. Windows DLLs are a bit like this.
227
2.10.4
When you get to more complicated systems of multi-module programs, the utility make can become
useful. In fact, IDEs like Visual Studio often use make to do a build.
make is like Ant that we used with Java projects.
make executes the file Makefile; i.e. the same as Ant executing build.xml; but maybe you never
noticed that.
The following shows a Makefile for Tr5c.cpp.
CC = g++ -W -Wall -ansi -pedantic -Wformat-nonliteral
-Wcast-align -Wpointer-arith -Winline -Wundef
-Wcast-qual -Wshadow -Wconversion -Wwrite-strings
-ffloat-store -O2
all: Tr5c
funs.o: funs.cpp funs.h
Tr5c.o: Tr5c.cpp funs.h
Tr5c: Tr5c.o funs.o
test: all
./Tr5c
clean:
rm -f *.o * *.t *.gch *.exe Tr5c
2.10.5
Interpreted Languages
The chief difference between a compiler and an interpreter is as follows. A compiler translates
from one language source to some other object language. At its simplest, an interpreter takes a
program and executes it.
When you create an interpreted language program (lets call the language Basic, you get a choice:
you can compile and create an .exe file much as described above.
If you run the Basic program in interpreted mode, then something quite different happens. There
is an interpreter program (lets call it basinterp) which reads the source and executes it directly.
basinterp has its own fetch-decode-execute cycle running.
We have something like the following, grossly simplified, assuming that we are dealing only with
instructions of the form result = num1 operation num2 .
int operand1, operand2, result;
String line;
f= open(prog1.bas);
while(NOT end-of-file(f))
line = readLine(f); /*fetch next line*/
decode line to produce:
- operation
- operand1, operand 2
/* execute */
if(operation==+)result= operand1 + operand2;
else if(operation==-)result= operand1 - operand2;
else if(operation==/)result= operand1 / operand2;
else if(operation==*)result= operand1 * operand2;
else if(operation==F)result= fred(operand1, operand2);
else /* etc */
assign res to actual result variable.
and go back for more ...
2.10.6
Java is interpreted, but its interpretation is a little different from that described for Basic. Java
programs go through a compiler and an interpreter. When you compile a Java program (e.g.
prog1.java), you produce a file prog1.class which contains Java byte-code. Java byte-code
229
is sort-of like machine code, which is interpreted by a Java interpreter program called Java Virtual
Machine(JVM).
So, we have a Java compiler, which produces the byte-code; then we have the Java interpreter
program, the JVM, which applies its version of the fetch-decode-execute cycle; Of course, the
Java Virtual Machine is software.
Since the JVM is software, any computer can execute Java byte-code as long as they have the
JVM interpreter program.
From the point of view of Operating Systems, you could think of the JVM as another layer on top of
the operating system, and below the applications. Indeed, the JVM provides operating-system-like
facilities, like support for concurrency (multitasking).
Another operating-system-like facility provided by the JVM is its treatment of applets. Applets
are Java byte-code (executable) programs meant primarily for downloading over the Internet by
web-browsers and executed directly by the browser running the JVM. Now, this would normally
be a recipe for disaster, and you would fear all sorts of viruses and malicious programs. However,
rather like an operating system with its privileged and user modes, the JVM has a special mode
for executing applets. For example, applets may not access disk files.
2.10.7
You will notice that a Java .class file may be a lot smaller than the equivalent C++ executable.
This is because Java does not build a great big executable that contains everything including
local and system library code; Java keeps the class files separate and loads them into memory only
as needed dynamic linking.
2.10.8
A JVMs runs on a single machine. In these days of the Internet, people and enterprises often want
to do something called distributed computing, i.e. a set of cooperating processes, (see Operating
Systems course), executing on separate machines connected via a network (such as the Internet).
This is all the easier if the processes are all running on JVMs. Add a bit of Internet glue and you
have J2EE.
Where are Microsoft in all this? When Java (developed and owned by Sun Microsystems) became
successful, Microsoft did not like it. They attempted to create an extended Java of their own; but
Sun forbade them. Then came the success of Java distributed computing. So Microsoft designed
their own version of J2EE called .NET. C# (pronounced C-sharp) is Microsofts attempt at
Java. Like Java, C# is compiled to a sort of byte-code, and executed on a virtual machine. The
is a .NET version of VisualBasic.
2.10.9
If you think about it, in computing, interpreters (in general) crop up everywhere. In addition to
the examples we have mentioned:
230
The operating system shell (e.g. Windows cmd) fetches and decodes your commands, and
then gets the kernel to do the executing;
The hardware processor itself is an interpreter; in fact, if you look at the microprogram that
runs Mac-1, you will clearly see that the program performs a fetch-decode-execute cycle.
And even if the processor is controlled by hardware rather than a microprogram, then the
hardware will, in its own way, be performing interpretation.
2.11
Summary
This chapter has given a quick tour of C++ syntax, certain basic facilities, and compilation and
linking and make.
231
Chapter 3
Variables, Types, etc. in C++
3.1
Introduction
This chapter discusses the elementary types provided by C++ (int, float etc.), together with
operators and expressions involving them. We call these built-in types to distinguish them from
types formed by classes.
We mention also the standard library types (C++ classes) string (very like Javas String) and
vector (very like Javas ArrayList).
We leave record (aggregate) types, i.e. structs), until a later chapter.
3.2
Variable names
A variable name is made up of letters and digits; it must start with a letter; underscore is also
allowed. Note C++ keywords, you cannot use them for names, though they can be part of names.
The world of programming is sharply divided by peoples preferences in variable naming, especially,
multi-word names; some choices:
(i) Underscore separators, e.g. multiwordname;
(ii) Uppercase for inner names, e.g. multiWordName;
(iii) Hungarian notation favoured by Microsoft, which is a special case of (ii), with the first few
letters used to code the type and any special use of the variable.
I now prefer (ii), with partial use of (iii) for pointers each pointer name begins in p.
Note that style and consistency affect readability and that is as important as syntactic correctness.
C++ is case sensitive. Although case has no syntactic significance, typical uses of case are:
As above, lower-case for first word, upper-case for leading letter of remaining words if any;
31
3.3
The C++ built-in type system is less rich than some other programming languages. If we ignore
pointers, there are just two basic categories:
Integer types, which represent whole numbers, both positive and negative.
Floating point types, which represent numbers that have fractional parts.
Within these two categories, the different types differ only by the range of numbers represented
which depends on the size of the representation (number of bytes / bits used) and whether the
representation is signed or unsigned. In allowing the signed qualifier, C++ departs from what you
are accustomed to in Java.
3.3.1
Integer types
Integer types can be either signed (default) or unsigned. A signed type has the range [min..max], e.g. signed char [-128..+127]; an unsigned has the range [0..max], signed char
[0..+255].
signed is the default, so that char c has the range [-128..127].
char. Typically represented by a byte (8-bits). Suitable for holding single text characters or
small integers. Hence, there is nothing to stop you doing arithmetic on them.
int. Typically represented by either 16-bits (range [-32,768..+32,767]) or 32-bits. Again,
signed is the default, so that signed int i; is equivalent to int i;
long int, or simply long. Typically represented by 32-bits. As usual, signed is the default.
bool. Also an integer type. Although bool has values false, true, C++ does little to disguise
that these are represented by values 0, 1, respectively. If you assign a bool to an int, you
get either 0 or 1. If you assign an int to a bool, any value other than 0 will convert to
true, while 0 will convert to false. This is demonstrated by program bool.cpp.
32
bool b; int i;
b = true;
cout bool b = true: b endl;
b = false;
cout bool b = false: b endl;
b += 22;
cout b+=22: b endl;
i = false + 22;
cout i=false+22: i endl;
i = true + 22;
cout i=true+22: i endl;
return 0;
Output.
bool b = true: 1
bool b = false: 0
b+=22: 1
i=false+22: 22
In general, in C++ you must be careful about overflowing integer types. In the example shown in
program char.cpp, the variable unsigned char u increments satisfactorily between 0 and 255.
But char c and signed char s go badly wrong at 127; if you are careless of problems like this,
they can lead to very confusing errors.
//----- char.cpp ---------------------------------// tests overflow of char, unsigned char and signed char.
// also shows some limits
// j.g.c. 2006-09-13
//------------------------------------------------#include iostream
33
#include climits
#include cfloat
using namespace std;
int main()
char c,x; unsigned char u; signed char s;
int i;
cout CHARMAX = CHARMAX endl;
cout CHARMIN = CHARMIN endl;
cout SCHARMAX = SCHARMAX endl;
cout SCHARMIN = SCHARMIN endl;
cout UCHARMAX = UCHARMAX endl;
cout SHRTMAX = SHRTMAX endl;
cout SHRTMIN = SHRTMIN endl;
cout INTMAX = INTMAX endl;
cout INTMIN = INTMIN endl;
cout INTMAX = UINTMAX endl;
cout FLTMAX = FLTMAX endl;
cout FLTMIN = FLTMIN endl;
cout DBLMAX = DBLMAX endl;
cout DBLMIN = DBLMIN endl;
for(i=s=c=u=0; i16;i++)
for(int j=0;j16;j++,u++,c++,s++)
cout u = int(u);
cout , c = int(c);
cout , s = int(s) endl;
return 0;
u = 14, c = 14, s = 14
u = 15, c = 15, s = 15
type any char + enter:
But when we get to 127 odd things happen:
u = 125,
u = 126,
u = 127,
type any
u = 128,
u = 129,
u = 130,
3.3.2
c = 125, s = 125
c = 126, s = 126
c = 127, s = 127
char + enter:c
c = -128, s = -128
c = -127, s = -127
c = -126, s = -126
Floating-point types
3.3.3
Implementation Dependencies
climits, cfloat contain symbolic constants that specify the limits and sizes of the integer
and floating point types on the current implementation, see char.cpp above. If you really want
to bullet-proof your software against differing representations to make it portable you may
need to know about these.
3.4
References
A reference, serves as an alternative name for the object with which it has been initialised. The
definition of a reference must specify an initialisation.
Example:
int x = 10; //plain int
int& r = x; //reference
x is defined as an ordinary int; subsequently, r is defined as a reference, and initialised with x.
Think back to Java.
35
c = a + b;
3.5
Arrays
Multi-dimensional arrays
int j[4][15];
declares an array with 4 elements, and each of these is itself an array of 15 ints.
The tenth element of the second (again counting from zero!) is indexed as:
j[2][10]
N.B. NOT j[2,10].
Unfortunately, the latter is legal, but a different thing altogether.
Pointers and arrays As noted in section 3.6.1, in C++ pointers and arrays are very closely
related. In fact, the previous fragment can legally be replaced by
for(int i=0; i10; i++)
*(x+i) = float(i);
But try to avoid this style it makes your program hard to read, and it does not make it run faster.
Arrays as function parameters When an array, e.g. int a[10] is passed to a function, e.g.
fred(a), it decays to a pointer; this is one way of viewing the fact that arrays are not passed by
value.
3.6
3.6.1
Pointers
Introduction
In any high-level language, pointers are something of a necessary evil. In some languages, their
role is quite restricted: they are used only as a method of accessing memory allocated dynamically
off the heap, the pointer is used as a reference for the allocated memory which is otherwise
anonymous. Java has managed to avoid them altogether because, in Java, all identifiers, except
those which identify elementary variables (int, float, etc.) are references (rather than values).
In C++, a pointer can point to any sort of memory, they are the C++ embodiment of machine
language addresses. It is common to use the term address as a synonym for pointer ; nevertheless,
taken literally, this is usually unhelpful.
37
For many reasons, including the syntax for declaring them, pointers in C++ and C are known to
be difficult and error prone.
There are two primary uses for pointers in C++:
As references for anonymous memory allocated dynamically off the heap using operator
new; in C, the equivalent is malloc.
In C++, as in C, pointers are closely related to arrays. For example, as already mentioned
in the previous section, when an array is passed to a function the array reference decays to a
pointer. There are other cases where arrays and pointers are indistinguishable, but to dwell
on this matter would introduce unnecessary detail and possibly recommend bad habits.
In C, pointers had to be used to provide a sort of programmed pass-by-reference for functions.
Fortunately, C++ has references, see above, which are much more suitable for this purpose.
3.6.2
3.6.3
Reference Operator & When applied to a variable, & generates a pointer-to the variable. Some
books say address-of Okay so long as you dont take it too literally, e.g. attempt to use the
address as a numeric address; this may be more natural, owing to the aampersand sign.
Example:
int* p;
int c;
p = &c; // causes p to point-to c.
38
Dereference operator * * dereferences a pointer, i.e. it obtains the value of variable / object
that a pointer points-to it simply reverses the action of &; i.e. d = *(&c); is equivalent to
d = c;.
Example:
int* p; int c, d;
p = &c;
d = *p; //d now has the same value as c.
Beware of Confusion
Uninitialised and dangling pointers Defining a pointer creates the pointer itself, but it does not
create the object pointed-to. Moreover, defining a pointer does not initialise it, hence it can point
anywhere. Thus,
int *p;
creates a pointer, which is initialised to some random value, i.e. it could point at a location in free
memory, part of your program, or anywhere. Now,
*p=10;
causes the value 10 to be written to the location p points to. Unless this is free memory, this may
cause your world as you know it may come to an abrupt end. Fortunately, the memory management
of Linux will usually trap a severe violation, resulting in program halt and the message segmentation
error.
A dangling pointer is similar in effect. Consider the function fred:
int* fred(int a)
int* p; int b;
b=a+33;
p=&b; /*p points to b */
return p;
caller:
39
3.6.4
As mentioned in section 3.6.1, pointers are sometimes used interchangeably with arrays; not the
best thing, but it happens and you must be able to read code that does it.
Example.
int* pi;
int x, z[10];
pi
x
pi
x
=
=
=
=
It is crucial to understand that pi=pi+4 doesnt add 4 to whatever address value of pi; if you
must think in addresses, it adds 4 x sizeof(int) to pi, where sizeof(int) gives the size of
the memory cell used by int.
So, if you increment a pointer p that points to a float (say), or. more properly an array of float,
then the compiler will increment p appropriately.
Note: &z[4] and z+4,
have exactly the same pointer values.
3.7
3.7.1
Strings in C++
Introduction
In C, there is no native text-string type; and that was also the case in C++ until ten years ago.
The was a convention to use a null-terminated (0) array of char; these we call a C-strings;
C-strings present some problems, all of them to do with Cs (and C++s) low level view of arrays,
see section 3.6.1.
The C++ standard library has improved the situation by including a string class, which provides
a much safer and higher-level text-string implementation. I will introduce standard string in the
subsection following.
I advise that standard string be used where possible, but for the sake of tradition and because
there are situations where they may still be heavily used, Id better cover C-strings in some detail.
310
3.7.2
C-strings
We note again that C-string, e.g. this is a string, is not a native type. There are two sets
of support for this convention:
The strXXX functions, whose prototypes are in cstring, that use null-terminated character arrays as strings, eg. strlen(s) returns the length of s, i.e. the number of characters
up to the null terminator.
Strings delimited by double-quotes, e.g. qwerty, are accepted as string constants.
char s[] = qwerty; is represented in memory as seven characters:
q w e r t y 0; dont forget the 0 a very common error.
We have not yet covered pointers, but it is worth pointing out that char *s;
has a lot in common with
char s[7];
Example:
char s[10];
s[0]=a; s[1]=b;
cout s endl;
s[2]=0;
c=f[i];
t[i]=c;
while(c!=0)
c=f[i];
t[i]=c;
i=i+1;
int i=0;
while((t[i]=f[i])!=0)
i++;
Using pointers.
void strcpy(char *t, char *f)
while((*t=*f)!=0)
t++; f++;
while((*t++=*f++)!=0)
;
Finally making use of the fact that 0 has the value 0 and that 0 is false.
void strcpy(char *s, char *t)
while(*s++=*t++)
;
When using character arrays as strings, you must take the utmost care to ensure that you do not
overflow the array. Indeed, it is a common error to treat an uninitialised char pointer (see above)
as a character array / string the problem is that, whilst it is acceptable as the equivalent to an
array, it is of zero length!
312
3.8
As mentioned in the previous subsection, standard string is now, with the advent of the Standard
Library, becoming a more common way of handling text-strings.
Since string is a class, we again touch on classes before we have dealt with them in detail. But
dont worry, except for a minor point, string can be treated largely like a native type. In chapter
13, we will mention the development of our own string class.
Program hellostd.cpp shows a version of Hello, world! that uses the standard string.
//----- hellostd.cpp -----------------------------//Hello, World using Std lib. string
//j.g.c. 5/1/98
//------------------------------------------------#include iostream
using namespace std;
int main()
string s1=Hello;
string s2=world;
string s3;
cout s1 endl;
cout s2 endl;
cout s3 endl;
s3= s1 + s2;
cout s3 endl;
s3+= s1;
cout s3 endl;
return 0;
Dissection of hellostd.cpp
1. string s1=Hello; defines an object s1 that is an instance of standard class string.
It initialises it with Hello. Hello happens to be a C-string, but C++ can handle the
conversion just as it can convert int to float.
2. string s3; defines a string s3 and initialises it to an empty string.
3.
cout s1 endl; writes s1 to the screen. We will see more details of this later, but
suffice to say that string provides an overloading of the operator ; similarly .
4. s3= s1+ s2; and s3+= s1; are further examples of operator overloading, in this case + is
overloaded as a concatenation operator.
313
3.9
Vector
The name vector here has almost nothing to do with the vectors that we use in graphics.
In addition to string and a pile of other useful types, the C++ standard library includes an array
class called vector. Java programmers should think of ArrayList. In general, vector is a lot
safer to use than a built-in array.
When you declare a vector container you must declare the type of its contents, for example,
vectordouble v1;, i.e. the declaration has a type parameter. The use of the type parameter is
an example of something called template in C++ and generic in Java. The next section contains
a brief introduction to templates.
The program below gives a comparison of built-in arrays with vectors; it show also how to use
iterator s and the sort algorithm.
//------ VectorTest2.cpp -------------------------// tests of std::vector
// j.g.c. 2003/02/20, 2006-09-13
//------------------------------------------------#include iostream // for cout etc.
#include algorithm // for sort
#include vector
#include cstdlib // for rand
using namespace std;
int main()
cout endl;
vectordouble v1;
for(int i= 0; i n; i++)
v1.pushback(d[i]);
cout endl;
cout nIterating through vector, another way n endl;
for(int i = 0; i n; i++)
cout v1[i] ;
cout endl;
sort(v1.begin(), v1.end());
cout nSorted vectorn endl;
for(int i = 0; i n; i++)
cout v1[i] ;
cout endl;
reverse(v1.begin(), v1.end());
cout nReverse sorted vectorn endl;
for(int i = 0; i n; i++)
cout v1[i] ;
cout endl;
cout nFront, back elements of vector, and size()n endl;
cout v1.front() v1.back() v1.size();
cout endl;
cout nYou can modify the vector (x 10.0)n endl;
for(int i = 0; i n; i++)
v1[i] = v1[i]*10.0;
cout endl;
cout nYou can have vectors of stringsn endl;
vector string v2;
v2.pushback(The); v2.pushback(quick);
v2.pushback(brown); v2.pushback(fox);
v2.pushback(jumped); v2.pushback(over);
v2.pushback(the); v2.pushback(lazy);
v2.pushback(dogs); v2.pushback(back);
for(unsigned int i = 0; i v2.size(); i++)
cout v2[i] ;
315
cout endl;
sort(v2.begin(), v2.end());
cout nSortedn endl;
for(unsigned int i = 0; i v2.size(); i++)
cout v2[i] ;
cout endl;
cout nYou can assign vectors n endl;
vectorstring v12 = v2;
for(unsigned int i = 0; i v12.size(); i++)
cout v2[i] ;
cout endl;
cout nYou can remove elements from vectors n endl;
remove(v12.begin(), v12.end(), lazy);
for(unsigned int i = 0; i v12.size(); i++)
cout v12[i] ;
cout endl;
cout nYou can search vectors n endl;
vectorstring::iterator it;
it= find(v12.begin(), v12.end(), fox);
cout*it endl;
cout n... and then erase that element n endl;
v12.erase(it);
for(unsigned int i = 0; i v12.size(); i++)
cout v12[i] ;
cout endl;
vectorstring v22;
cout nYou can copy using copy, but use resize first n endl;
v22.resize(4);
copy(v12.begin(), v12.begin()+3, v22.begin() );
for(unsigned int i = 0; i v22.size(); i++)
cout v22[i] ;
cout endl;
vector vectorstring vv;
cout nYou can have vectors of vectors, but use in decl. n endl;
vv.pushback(v2);
vv.pushback(v12);
316
vv.pushback(v22);
for(unsigned int i = 0; i vv.size(); i++)
cout vector i: ;
for(unsigned int j = 0; j vv[i].size(); j++)
cout vv[i][j] ;
cout endl;
cout endl;
return 0;
3.10
When you declare a vector container you must declare the type of its contents, for example,
vectordouble v1;, i.e. the declaration has a type parameter. The use of the type parameter
is an example of something called template in C++ and generic in Java. This section contains a
brief introduction to templates.
The major use of templates is in collection classes; we are not yet ready to develop our own
collection class, so for the meanwhile well have to get by with the template examples here and in
the section that introduces vector (section 3.9).
3.10.1
Template Functions
Overloaded Functions
The two overloaded swap functions shown below clearly invite use of a type parameter.
void swap(int& a, int& b)
int temp = a; a = b; b = temp;
317
Template Function
A template version of swap is shown below:
template class T void swap(T& p, T& q)
T temp = p; p = q; q = temp;
The program swapt.cpp shows use of this swap. Here, we are able to swap ints, floats, and
String objects. In the case of user defined classes, the only requirement is that the assignment
operator = is defined.
//--- swapt.cpp ---------------------------// j.g.c. 10/4/97, 2/5/98, 14/1/99
//-------------------------------------------#include iostream
#include string // using std string
template class T void swap(T& p, T& q)
T temp = p; p = q; q = temp;
int main()
318
3.11
Polymorphism Parametric
Templates are another form of polymorphism; using templates one can define, for example a
polymorphic List a List of int, or float, or string, etc... Likewise we can define a polymorphic
swap. Unlike the polymorphism encountered in chapter 11 (Person and Cell class hierarchies),
the type of each instance of a generic unit is specified at compile time statically.
Hence, the polymorphism offered by templates is called parametric polymorphism.
319
3.12
3.13
const qualifier Declaring a variable const means it cannot be the target of an assignment; eg.
const int limit = 999;, any later statement of the form limit = ... will generate a compiler
error.
3.14
Arithmetic Operators
+, -, *, /, %+;
% is modulus remainder after division; e.g. 5%3 - 2.
/ is division; it means something different (or sort of different) whether we are using it on int or
float; in integer, it truncates the result; e.g. 5/3 - 1.
3.15
Relational operators
, =, , =, ==, !=
320
Logical operators
on logical (bool):
inclusive or.
&& and.
! unary complement. !true - false.
3.16
C, and to a lesser extent, C++, are quite liberal about mixed-mode expressions, there
is automatic promotion from narrower to wider ; the promotion ranking is something like:
char, int, long, float, double.
In an expression, or a sub-expression delimited by ( ... ), all components are implicitly cast to
the widest type that is present in the expression.
It is often better, from the point of view of readability and for debugging, to use an explicit type
conversion, e.g. float(2).
Casts versus conversion functions Whilst C used cast operators, e.g (float)2, C++ has, in
addition, conversion functions, e.g. float(2). C++ has other casts that we will deal with later.
3.17
3.18
The pre-increment expression ++n is equivalent to n+=1 (see above), which in turn is equivalent to
n = n + 1.
Recall: any assignment statement is also an expression, e.g. a = b = 0;
The post-increment expression n++ is different, but only subtly. Compare:
321
n = 5;
n = 5;
x = n++;
x = ++n;
//here x==5 the old value
//here x==6 the new value of n
//n==6 in both cases
There are also -- (decrement) operators.
3.19
Conditional Expressions
if(ab)
z = a;
else
z = b;
can be replaced with:
z = (a b) ? a : b;
The general form for a conditional expression is:
(logical expression) ? expr1:expr2
If the logical expression is true, the value of the expression is expr1, otherwise the value is expr2.
3.20
Bitwise Operators
3.21
My advice is, if in the slightest doubt, use brackets. If necessary, see (Stroustrup 1997b).
Note: when you overload an operator in a class, e.g. + (covered later), the operator retains its
original precedence.
322
Chapter 4
Control Flow
4.1
Introduction
I think that this chapter has very little that is new for Java programmers.
As in Java and other block structured programming languages, we normally have the following flow
of control constructs:
Sequence The normal stepping through from one statement to the next;
Selection To enable selection, for the next statement or block, from amongst a number of possibilities.
Repetition Repetition of a single statement or of a block. These may be deterministic, such as
for(...)), or non-deterministic, such as while(...), and do...until.
C++ also has goto, break, and continue which can be used for unstructured interruptions of
control flow.
4.2
Block A block, or compound-statement is a group of statements enclosed in .... Syntactically, a block may replace a single statement. Actually, in C++, as in C, a block has an associated
activation and scope; in that sense it is like a function, but without parameters.
In what follows, we will tend to use the term statement to signify either compound or single
statement always understanding that a compound statement can take the place of a single
statement.
In addition, statement includes selection and repetition statements including the statements or
blocks that they govern.
It is worth noting too that, syntactically, ; is a statement a null statement, that does nothing.
41
4.3
Selection
4.3.1
if (logical expression)
statement1 //performed if expression true
else
statement2 //performed if false
Remark: recall that C++ doesnt really differ between numeric values and logical values: any
non-zero (including negative) value is taken as true, a zero value (including a NULL pointer value)
is taken to be false.
4.3.2
Actually, else if is not a separate construct, it is just an else that happens to be governing an
if statement,
The following is a multi-way selection: if expr1 is true, stmt1 gets executed; then expr2
is evaluated and if true, stmt1 gets executed;
...
Finally, if none of the previous is true, stmt4 gets executed.
if(expr1)
stmt1
else if (expr2)
stmt2
else if (expr3)
stmt3
else
stmt4 //otherwise -- default
The previous construct a so-called if-else ladder is well worth learning off many programmers
prefer it to the more obvious, but more difficult to use switch, see below.
Note the indentation style suggested; since each rung of the ladder is essentially equal, even though
they are evaluated sequentially, there is no reason to further indent each else, indeed, to my mind,
to do so would confuse the reader of the program.
42
4.3.3
switch (expr)
case const-exp1: stmnt(s)1 [break;]
case const-exp2: stmnt(s)2 [break;]
case const-exp3: stmnt(s)3 [break;]
default: stmnt(s)d; [break;]
expr must evaluate to an integer; in addition, the case expressions must be a constant integer.
The switch is much the same as the if-else ladder in the previous section; there is, however, one
big difference, and one that can be a big trap to the unwary: upon executing (say) stmnt(s)1,
above, control will fall-through into stmnt(s)2 and stmnt(s)3, etc. It will execute these
without evaluating any condition: it is not the case expressions that alter the flow of control, but
the switch.
If you dont want this to happen, you must use the break; statement, which causes control to
jump to the end of the switch to just outside the at the end.
4.4
Repetition
4.4.1
while
while(expr)
statement
First, expr is evaluated, if true statement is executed, then expr is evaluated again, and
so on . . . .
while(true) is do-forever normally with a selection involving some form of exit, e.g. break.
This often appears as while(1).
The following program sums the first n integers,
#include iostream
int main()
int i=1;sum=0;n=4;
while(i=n)
43
sum+=i;
++i;
4.4.2
for
for(expr-init;expr-continue;expr-iter)
statement
is equivalent to
expr-init
while(expr-continue)
statement
expr-iter
for(;;)... is allowable and often used for do-forever, i.e. the same as while(true).
int a[n],i;
for(i = 0; i n; i++)a[i] = 0;
is the C++ idiom for traversing n elements of an array since the array indices go
0, 1, 2, ..., n-1.
4.4.3
do - while
do
statement
while (expr);
statement is always executed at least once;
4.5
break causes the innermost enclosing repetition loop or switch to be exited immediately to just
outside the at the end of the block.
continue causes the remainder of an iteration block to be bypassed and the next iteration to be
started immediately, i.e. avoiding the remainder of the current repetition.
44
4.6
goto can cause a jump to any labelled statement, anywhere in the entire function. For example,
goto label1;
...
label1: i = 0;
...
In case you havent heard, goto is frowned upon, and in the vast majority of cases it can be avoided.
45
Chapter 5
C++ Program Structure
5.1
Introduction
As we know already, a C++ program can be constructed entirely of functions, i.e. unlike Java,
which requires all functions/methods to be members of classes. Java also requires execution to
proceed via object creation and object method calls; in C++ we can call functions on their own.
This chapter covers the definition, declaration and calling of functions.
5.2
Basics
Example.
int add(int a, int b) // a and b are also local variables
51
return expression;
returns the value of expression to the calling point, with expression being converted to
return-type as necessary.
The caller can ignore the returned value it is not a compilation error. Hence,
int x= 10,y= 20;
p = add(x, y);
add(x, y); //syntax okay, but not much use!
are both legal, i.e. syntactically correct, but the second isnt very sensible semantically.
In a definition, you must explicitly state void for return type if no value is returned; in this
case, return; is optional.
If there are no parameters, the parameter list can be replaced by and empty parameter list ()
or by explicit (void). In C, the (void) is essential.
5.3
In C++, all functions must be declared before they are called analogous to variables.
Syntax:
return-type function-name(parameter declarations);
Example.
int add(int a, int b); //note the ;
in the declaration
Header Files for Libraries and Classes Its a nuisance to have to declare many functions at the
top of a program, so its common practice to include all prototypes for a library or class in a header
file, and #include the appropriate header file where any of the functions is used.
In Chapter 2, we have already encountered a small library of functions contained in a source file
funs.cpp. In that case we put our prototypes in funs.h.
Then, we #include funs.h at the declaration part of all source files that use any of the
functions in funs.
Thus, (a) you are saved a lot of typing, but more importantly, (b) you need maintain only one set
of prototypes.
52
5.4
Function parameters
5.4.1
Parameters
5.4.2
Pass-by-value parameters
In C++, the default parameter passing method is pass-by-value or sometimes called pass-by-copy.
The following function adds two floats and returns the result in a float:
float addf(float a, float b)
float c=a+b;
a = a + 10; // to demonstrate pass-by-value
return c;
5.4.3
Pass-by-reference parameters
int temp=a;
a=b;
b=temp;
swap has an effect on the variables x, y in the caller as it must do if it is to be of any use. Thus:
int x = 10, y = 20, z = 0;
swap(x, y);
cout x, y endl;
will result in 20, 10
Notice that the caller need not make any special indication of the reference nature of the arguments
that is all handled by the definition of the function: int& a, int& b.
There is no equivalent in Java; in Java all primitive types are pass-by-value.
Just to drive the point home, let us examine a wrong swap which uses pass-by-value.
void swapSilly(int a, int b) //wrong, pass by value
int temp=a;
a=b;
b=temp;
Now, swapSilly does not have an effect on the arguments x, y in the caller, and the function
has no effect. Thus:
int x = 10, y = 20, z = 0;
swapSilly(x, y);
cout x, y endl;
will result in 10, 20
54
5.4.4
Just for completeness, we will include a version of swap which demonstrates how pass-by-reference
can be programmed via pointers. This is how it must be done on C, which does not have a
reference type qualifier.
The following function pswap uses pointer parameters:
void pswap(int* pa, int* pb)
Notice that it is what pa, pb point-to that are swapped, not pa, pb themselves, i.e. *pa, *pb
dereferenced.
pswap has an effect on the variables pointed-to by the caller arguments. Thus:
int x = 10, y = 20;
pswap(&x, &y);
cout x, y endl;
will result in 20, 10
Note that in this case the caller must pass pointers to the variables, i.e. &x, &y are passed. In
addition to the extra complexity of the function, this programmed pass-by-reference error-prone
and generally less satisfactory than proper pass-by-reference.
It is worth noting that in the case of pswap the pointer values are still passed-by-value its just
that the values are pointers, and so can be dereferenced to access the variables that they reference
in the caller!
5.4.5
The distinctions between the previous examples can be clearly defined by considering the detailed
semantics of parameter passing by value and by reference.
Pass-by-value Recall the example of pass by value. Here x, y are local variables in the caller;
when they are created, they are initialised with the values 10, 20.
int x = 10, y = 20;
When swapSilly is called, swapSilly(x, y); the following happens: variables local to
swapSilly are created (int a, int b) and these are initialised with the values of x, y
almost as if we had the definition: int a = x, int b = y. Hence, computations involving a, b
are on these entirely separate and local variables.
55
Pass-by-reference Using the same example, x, y are local variables in the caller; when they are
created, they are initialised with the values 10, 20. Now we can define a reference int& rx and
initialise it with x not the value of x, rx is an alias for x whatever is assigned to rx is assigned
to x: x and rx refer to the same object in memory.
int x = 10, y = 20; int& rx = x;
Likewise when void swap(int& a, int& b) is called, swap(x, y); the following happens: reference variables are created (int& a, int& b) and these are initialised with variables x, y
almost as if we had the definition: int& a = x, int& b = y. Hence, computations involving
a, b also involve x, y.
Pass-by-reference via pointer Again using the same example, x, y are local variables in the
caller; when they are created, they are initialised with the values 10, 20.
Likewise when void pswap(int* pa, int* pb) is called, pswap(&x, &y); the following happens: temporary pointer variables are created, and these are initialised with pointers to variables
x, y, i.e. as if (int *px = &x, int* py = &y).
Pointer variables local to pswap are created (int* pa, int* pb) and these are initialised with the
values of temporaries px, py almost as if we had the definition: int* pa=px, int* pb=py.
Computations involving pa, pb are on these entirely separate and local pointer variables, but,
being pointer variables, they can be dereferenced to access the actual variables x, y. Hence,
computations involving *pa, *pb also involve x, y.
Java pass by value only Well, sort-of. All elementary types, e.g. int, float, double are
passed by value only. Thus, a function like swap cannot be done in Java.
On the other hand, just like arrays in the next section, in Java all non-elementary objects are always
references.
5.4.6
Arrays as Parameters
...
s[i]=c;// or *(s+i)= c;
...
As we have already noted in chapter 6, C++ arrays are closely related to pointers, hence the
following is entirely equivalent:
56
...
*(s+i)=c; // or, s[i]=c;
...
5.4.7
Default parameters
// body ...
Call:
57
5.5
5.6
return a + b;
return a + b;
However, in C++, through overloading of function names, we can use the more natural name
add for both, and the linker will bind the appropriate version according to the types of the
arguments:
float add(float a, float b)
return a + b;
return a + b;
58
Caller:
int i,j,k;
float x,y,z;
...
z = add(x, y); // float add(float a, float b) is called
k = add(i, j); // float add(int a, int b) is called
Function name overloading is made possible by allowing the parameter types to become part of
functions identity. Note: the return type is not used in this disambiguation.
Function name overloading finds extensive use in classes:
A class may have a number of constructors, all with the same name, but each having a
different parameter list.
To enable classes, especially within a class hierarchy, to exhibit uniformity of behaviour; e.g.
trivially, many classes can have an overloaded print function.
5.7
Inline functions
When function add is called, a certain processing overhead is incurred: local variables a, b must
be created and initialised, and, likewise, the return value must be copied to the point of call. In
the case of add this overhead may amount to more than the simple a + b. With larger objects,
the overhead may be more severe.
int add(int a, int b)
return a + b;
C++ was developed with one eye on efficiency and performance, and allows the specifier inline
as a recommendation to the compiler. If we have
inline int add(int a, int b)
return a + b;
a call to add(.,.) may cause add to be expanded at the point of call; i.e. a + b is inserted at
the point of call, instead of a call.
Programmers are always warned about overly liberal use of inline; apparently small functions can
involve large amounts of code, which, if inline is used, would be replicated at each call, leading
to a large executable program.
Notice: inline allows trade-off of space code replicated for efficiency lack of call / return
overhead.
59
5.8
External variables
An external variable is defined once, and once only, outside any function, e.g. int globalx;
Then, declared anywhere it must be used, it is brought into scope by declaring it, e.g.
extern int globalx;
Example. A program in two files prog.cpp, funs.cpp:
file prog.cpp:
int globalx; /*defined OUTSIDE any function*/
#include funs.h
int main()
void f1(void)
External / global variables have static lifetime, see section 5.12, i.e. they are created before the
program starts executing, and are not destroyed until the execution is completed.
5.9
Scope of variables
The scope of a variable (or function) is those parts of a program where the name can be used to
access the variable (or function); simply, scope is the range of instructions over which the name is
visible.
Lifetime is a related but distinct concept, see section 5.12.
510
The scope of automatic / local variables defined in a function or block is from the definition
beginning until the end of the function or block: they have local scope, they are private to that
function; thus, functions have a form of encapsulation.
Local names that are reused, in the same or different source files, or in different functions or blocks,
are unrelated.
Example.
int value;
value=a+b;
return value;
scope of x, y ..a, b
scope of a, b, value
Blocks are scope units Like functions, blocks are scope units. Thus, within a block, you can
declare a variable and its scope will be from the point of declaration to the end brace () of the
block.
Thus:
int fred(int a)
int b;
//c is in scope only in this little block
int c; c = 22;
b= a + 10;
return b;
Scope Hiding
Consider:
511
The outer d the one declared first is hidden in the block governed by the for, by the inner
declaration.
Scope of functions All functions, everywhere (at least in libraries that are included in the linking
process and which are declared in the program file (e.g. in a .h file) ), are in scope in every part
of all functions. This is called external (or global) scope. So, functions have external / global
scope by default, whereas variables are local by default.
Note: functions have global scope independent of declarations by #include.
Java has a much more uniform and safe approach to scope: no externals are in scope, unless they
are imported from their package/class;
This global scope can cause name clash problems where you are using libraries from multiple
vendors; it has been addressed by the recent introduction of namespace, see section 5.10.
Class scope C++ classes offer their own form of encapsulation and scope, see later chapters,
that is more or less the same as Java.
Class members that are declared private are in scope only in member functions of the class, or
in functions or classes which have been declared friends of the class see later sections.
On the other hand, classes themselves have the same external scope as functions.
5.10
Namespaces
As we have mentioned in the previous section, the global scope of functions and classes may cause
problems where you are using library software from multiple sources and in which the name name
has been used.
For example, we have already used class string which is declared using #include string. In
a later chapter, we develop our own class String. Now the fact that one is string and the other
is String is sufficient to avoid a name clash.
But let us assume that our string is also called string and, moreover, that we want to use them
together e.g. to test relative performances. In that case, the declarations:
512
#include string
#include string.h
will cause the compiler to complain about multiple declarations of string.
The way around it is to declare our own string within its own namespace:
//---- string.h ------------------------------------// string class.
// j.g.c. ... 23/3/97
//---------------------------------------------------#ifndef STRINGH
#define STRINGH
#include iostream
namespace bscgp2
class string
public:
String(const int len=0);
//etc...
// end of namespace
In addition, all standard library stuff will have been declared under namespace std.
Now, we can use the two strings together and use the scope resolution operator :: to discriminate
between them:
#include string
#include string.h
std::string s1;
// standard lib string
bscgp2::string s2; // our own string
The use of the scope resolution operator :: may appear clumsy, so, in cases where we are not
troubled with name conflicts, we can use the declaration using namespace in the user program.
using namespace bscgp2;
// now no need for bscgp2::
5.11
5.11.1
Introduction
Up to now, we have become used to memory management being done automatically. Thus:
513
int fred(int a)
Sometimes, but only in very special cases, we may need to take direct control of the creation and
deletion. In the example, b and a is allocated on the stack.
Two significant and related factors differentiate so-called heap or free memory, and the more
familiar so-called stack memory used by local variables.
Here, also, we find the primary use of pointers in C++ as references for (initially anonymous)
memory created on the heap.
Unlike stack variables, which are created and destroyed automatically, heap the memory
management (creation/deletion) of heap variables must be programmed.
Once created, heap memory continues to exist until it is deleted by an explicit delete command.
Heap variables are created using operator new, and are destroyed using operator delete.
5.11.2
Operator new
new is similar to Javas new; Javas new returns a reference to the object that was created (on the
heap); C++s new returns a pointer.
Operator new creates a variable on the heap and returns a pointer to it. Here is a somewhat toy
example:
int* readAndCreate()
int n;
cout Give size: endl;
cin n;
int* p = new int[n];
// prompt read n ints
for(int i = 0; in; i++)
cout Enter an int:;
cin *(p+i); //or p[i]
return p;
int* pa = readAndCreate();
514
Function readAndCreate() creates an int array variable of size n, and returns a pointer to the
caller.
Although p is destroyed upon return from readAndCreate(), what it points to is not, and pa in
the caller, can legitimately continue to reference the array.
In fact, once created, this array variable continues to exist until explicitly de-allocated, using delete.
Consequently, the lifetime of a heap variable is from its explicit creation, until its explicit deletion.
If new cannot accomplish the creation, e.g. if a larger block of memory is needed, than is available,
then new will return the NULL pointer 0. Or, if #include new.h is present, it will call an error
function specified in that typically resulting in an error message followed by termination of the
program. Generally, programmers must be careful to consider the consequences of running out of
heap memory.
Dangling Pointers At this point you should very carefully note the complete inadequacy of the
following version of readAndCreate():
int* readAndCreateDangling()
int n;
cout Give size: endl; cin n;
int ar[1000]; // we assume 1000 is always greater than n
// prompt and read n ints
...
// this is partially okay; 1. a pointer to the first element of ar
// will be returned
// but 2. ar[] will be deleted as soon as that happens!
return ar; //or return &ar[0]
5.11.3
Operator delete
int* pa = readAndCreate();
// do something useful with the array
delete [] pa;
515
The [] is necessary to signify to delete that the target is an array. In the case that the variable
is not an array, the [] must be avoided.
int* p = new float;
delete p;
5.11.4
Anonymous variables
In
int* p = new float;
the variable initially created by new is anonymous it has no name however new returns a
pointer by which it may be referenced, and, of course, this is assigned to the pointer p, which
subsequently may be used to reference the variable.
5.11.5
Garbage
Garbage refers to the situation of anonymous heap memory, see section refsec:anon, whose reference has been deleted before the heap variable itself. Once this reference is lost, the heap memory
may never again be accessed, even to de-allocate it!
Java has automatic garbage collection Java can detect when an object can no longer be referenced and it then deletes the object. C++ does not have garbage collection.
Thus, garbage is in some ways the opposite to dangling or uninitialised references in which the
reference exists, but what it references does not.
Again in comparison to dangling references, garbage may be more benign it can cause program
failure only by repeated memory leak leading eventually to the supply free memory becoming
exhausted.
Note: do not be confused by the English connotation of the word, garbage does not refer to
uninitialised variables or incorrect data.
Java In Java, all non-elementary variables (all objects and arrays arrays are considered to be
objects) are allocated on the heap. A consequence of this is that objects obey reference semantics
rather than value semantics; beware, this is more subtle than may appear at first for the references
are passed-by-value to functions!
In addition, Java has garbage collection. Thus, whilst you need to create objects with new you
do not delete them. When the connection between a Java reference and its object is eventually
broken by the reference going out of scope, or by the reference being linked to another object
the object is subjected to garbage collection.
516
5.12
Lifetime of variables
The lifetime of a variable is the interval of time for which the variable exists; i.e. the time from
when it is created to when it is destroyed; duration, span, or extent are equivalent terms for the
same thing.
It is common to find confusion between scope and lifetime though they are in cases related,
they are entirely different notions: lifetime is to do with a period of time during the execution of a
program, scope is to do with which parts of a program text. In C and C++, lifetime is dynamic
you must execute the program (or do so in a thought experiment) in order to determine it. Scope
is static determinable at compile time, or by reading the program text.
In the case of local variables (local to blocks or functions), and where there are no scope-holes,
lifetime and scope correspond: scope is the remainder of the block / function after the variable
definition; lifetime is the whole time that control is in that part of the program from the definition
to the end of the block / function.
Example, local / automatic variables.
.
Thus, c, and a, b exist only for the duration of the call to function fred. For each call, entirely
new variables are created and destroyed.
If you wanted to retain the value of c from call to call, you would have to use the static type
modifier.
Static lifetime
static int c;
/*when control passes into fred: local
variables a,b, are created at this
time -- their LIFETIME starts then; however,
since c is static, it was already created at
517
1 Internal static. In a function, a static variable provides permanent storage within the
function; i.e. as in the example above. This is by far the most common.
2 External static. Applied to an otherwise global / external variable or a function in a source
file, static limit the scope of that variable or function to the remainder of that source.
Thus, this use of static signifies private.
3 Static class member. In a class definition, a data member (variable) can be declared static,
in which case this variable is shared amongst all instances of the class! Normally avoid!
Of course, all global /external variables have static lifetime they exist for the full life of the
program; they are created before the program starts executing, and destroyed only when it halts.
5.12.1
It is possible to summarise the various lifetime classes by classifying them according to increasing
degrees of persistence, from transient very short lifetime to persistent very long lifetime:
Temporary variables Used in evaluation of an expression, e.g.
y = x * (x + 1.0) + 2.0 * x;
will almost certainly involve temporary variables, e.g. a, b and c, which exist only during
the evaluation of the expression:
a = x + 1.0; b = x * a; c = 2 * x;
y = a + b + c;
Local variables Their lifetime is from entry to their definition to exit from their block / function.
These are called automatic in C / C++.
Heap variables Their lifetime is from allocation to de-allocation. Sometimes called dynamic variables.
Static variables including global. Their lifetime is from the start of execution of the complete
program, until it stops.
518
Variables held in files Their lifetime is over many program executions; they are persistent. Persistence of objects is of significant interest for database applications object-oriented databases.
Some readers may gain a better understanding of lifetime by reading section 5.13 which gives a
model of the run-time structure of a C++ program.
519
5.13
See (Sethi 1996), (Louden 1993). This is a model; exact implementation detail may differ depending on compiler, machine architecture and operating system.
The diagram below shows the overall structure.
Low memory address
0
+-----------------------------+ --
Program
+-----------------------------+ static,
Global and
compile / link-time
Static Data,
part
constants,
literals
v
+-----------------------------+ --
Stack
grows up
dynamic,
run-time
v
+-----------------------------+ ---
Top of memory
Memory Layout of a C++ Program
520
Next, we show a typical function environment as implemented using an activation record, probably
via a stack-frame.
+-----------------------------+
. . .
incoming parameter 2
incoming parameter 1
+-----------------------------+
program counter
+ other registers
Frame Pointer-+-----------------------------+
local variables
+-----------------------------+
temporary variables
+-----------------------------+
return value
+-----------------------------+
A Stack Frame -- Activation Record
It is a matter of opinion whether a stack-frame model of an environment is helpful, useful discussions
of environments that avoid any notion of implementation detail, and given in (Bornat 1987).
5.14
Initialisation
Only extern and static are guaranteed to be initialised implicitly to some base value, e.g. for
numbers, zero; Automatic or register will contain arbitrary data unless they are explicitly initialised.
If extern or static are initialised in the definition statement, the initialiser must be a constant
expression, e.g.
int x=1; char c=a;
For auto or register storage classes there is no restriction to constant..
In array initialisation, the compiler will fill [] (array size) according to size of the list, if no array
length specified, e.g.
int dayPerMonth[]=31,28, ...,30,31;
will define an array of 12 ints.
521
5.15
Register Variables
It is possible to advise the compiler that a variable, e.g. x will be heavily used and should, if
possible, be placed in scratch-pad register storage.
Example.
register int x;
This may be ignored by the compiler; without register declaration the compiler will make up its
own mind; in my (JC) opinion its best to leave it that way! On such matters, most compilers are
cleverer than most programmers.
5.16
Recursion
C++ functions may be called recursively. File fac.cpp shows set of factorial functions in two
styles:
Non-recursive.
Recursive.
/*----- fac.cpp -------------------------------j.g.c. 6/3/95 j.g.c. 14/2/97
comparison of functional and procedural styles
and erroneous use of static variable in a recursive function
------------------------------------------------*/
#include iostream
int facp(int n) //imperative style
int i,f= 1;
for(i=1;i=n;i++)f=f*i;
return f;
if(n=0)return 1;
else return n*fac1(n-1);
int main()
int n,ff;
522
cin n;
cout ff endl;
cout ff endl;
cout ff endl;
523
n=0
returns 1 ----+
By a similar tracing, you will find that fac2 always gives the result 0 because the static i N.B.
only one copy gets overwritten by 0 in the last call; it is then 0 when it is multiplied in all of the
statements return i*fac2(.).
5.17
The C pre-processor (cpp) used to be very much part of C, though reliance on it is greatly diminished
in C++.
The pre-processor is quite distinct from the compiler; all files are run through the pre-processor
before they are submitted to the compiler. There are four main features offered by the preprocessor.
5.17.1
File inclusion
5.17.2
e.g.
524
#define true 1
#define false 0
In fact you can define any replacement text:
#define BEGIN
#define END
would allow you to partially pretend you were using Modula-2.
5.17.3
Macro substitution
5.17.4
You can effectively program the preprocessor to skip sections of code. You will become very
familiar with this in header files.
Example.
#ifndef HDR
#define HDR
//declarations here.
#endif
525
The scheme above could be used to ensure that a header file, if invoked more than once, will have
effect only on the first invocation.
Conditional compilation is also very useful for building in portability across a number of compilers
or target computers. E.g.
#ifdef WINDOWS
// WINDOWS code
#elif LINUX
//Linux code
#endif
526
Chapter 6
Struct, class, union
6.1
Introduction
Long before we had object-oriented programming and classes, we had records or aggregate types;
i.e. type which allowed you to group together more than one value; records contained only data
values. Object-oriented programming extended records to contain also functions/methods.
In C++, records (aggregate types) are called struct or class.
Essentially, in C++, class and struct are equivalent except for an insignificant detail: in a struct
members default to public, whilst, in a class, members default to private; this will not matter
to most programmers, for the normal practice is to explicitly include private / public as required.
In this usage, class, struct are entirely equivalent.
In this chapter we describe the use of struct as record, i.e. without member functions and
encapsulation.
6.2
A Point struct
61
.
p1.y
= 50 + . . . . . . . . . . . . .+ p1 = (150, 50)
y v
The Point (150, 50)
If pp is a pointer to a Point:
Point *pp, p1;
int a;
6.3
Operations on Structs
62
Copy A struct can be copied as a complete unit, i.e. in pass by value to functions and returning
values by return.
Pointer You can take a pointer-to a struct with the & operator.
Access members Access members with ., or -, for a pointer.
File points.cpp shows a program which uses Point.
//---- points.cpp -------------------------------// j.g.c. 20/3/95, 17/2/97, 14/11/98
// exercises Point struct
//-----------------------------------------------struct Point int x;
int y;
;
#include iostream
#include math.h //for sqrt()
Point readPoint();
void printPoint(Point p);
float distPoint(Point p, Point q);
int main()
printPoint(*pp);
d=distPoint(p2, *pp);
cout distance p2 to *pp (actually p1) = d endl;
pp-y = 5;
cout npointer-member is one way of dereferencingn;
cout a pointer to a struct & accessing a membern;
cout After pp-y = 5; *pp = ; printPoint(*pp);
cout n(*pointer).member is another way of dereferencingn;
cout a pointer to a struct & accessing a membern;
(*pp).x = 6;
cout After (*pp).x = 6; *pp = ; printPoint(*pp);
cout endl;
return 0;
Point readPoint()
Point p;
cout enter point
cin p.x p.y;
return p;
void printPoint(Point p)
6.4
Unions
A union is declared using the same syntax as struct. Although they may look similar, they are
quite different in meaning.
Whilst a struct contains at all times each and every component declared within it, i.e. it is a
record / tuple, a union may contain but one of its components at a time; which component it is
64
byte0 byte1 2
3
+-----+-----+-----+-----+
c[0] c[1] c[2] c[3]
+-----+-----+-----+-----+
i
-- assuming 4 byte int
+-----+-----+-----+-----+
l
-- assuming 4 byte long int
+-----+-----+-----+-----+
f
-- assuming 4 byte float
+-----+-----+-----+-----+-----+-----+-----+-----+
+-----+-----+-----+-----+-----+-----+-----+-----+
So, if you assign something to i, c will change, and all the others too.
Now if you assign to f, and examine c[0], c[1] etc, you will get nonsense; i.e. between assignments, what a union stands for must not change.
On the other hand, a union can be used to break-into the type system: in the case mentioned
above, we can assign a float and examine its contents as (four) chars.
In the case of struct, all the components co-exist. Hence, of course the struct uses more
memory, and the union can conserve memory where you want to use only one of the components
at a time.
The example program below (strun.cpp) compares and contrasts struct and union.
65
The big problem with unions is that they are not discriminated, that is there is no mechanism
66
67
Chapter 7
Introduction to Classes and Objects
7.1
Introduction
This chapter introduces the basics of C++ classes and objects. For the moment, we will avoid
inheritance, polymorphism and dynamic / run-time binding. We start with a very simple class,
Cell, which reduces the class/object concept its bare essentials. At the end of the chapter we
give the code for vectors and transformations in 3D.
Recall that a data type, such as the native data types int, float, etc., is characterized by:
1. A set of values that can be assumed by objects of the type; for example in the C++ type
char, the set of values is 128, 127, . . . , 1, 0, 1, . . . 126, 127.
2. A set of operations (functions) that can be legitimately performed on objects of the type;
for example, for int, some of the functions are: +, -, *, /.
Users of the type do not concern themselves with the representation of the values, and, certainly,
they are not encouraged to fiddle with the representation they interact with the variables only
through the legitimate operations. Hence private data and public operations (methods).
You probably reckon that you already know all this; fine, but it will do no harm to revise it and
while we are at it we can point out the significant differences between C++ and Java.
7.2
7.2.1
This class does nothing more than represent a single int value. As with types we are interested
in: (a) values the set of states an object may take on, and (b) operations behaviour, what
an object can do.
State
We would expect a Cell object to be able to store the current state, i.e. its integer value.
71
Behaviour
7.2.2
Class Cell
Class Cell class is shown below, and below that a test program. Note: in these examples, we try
to keep white space to a minimum so that it will be possible to get programs on a single OHP
slide. Note also that we are (i) stuffing everything in one file the class and the user program, (ii)
we are including the implementation of the methods in the class Cell declaration; normally, for
example, the declaration of the class would be in Cell.h and the implementation of the methods
would be given in Cell.cpp.
//----- Cell0.cpp ------------------------------------// j.g.c. 12/2/98, 8/1/99, 2003/11/29
//---------------------------------------------------#include iostream using namespace std;
class Cell
public:
Cell()val= 0;
void set(int val)val= val;
int get()return val;
void print()coutvalue= val endl;
private:
int val;
;
int main()
Cell c;
c.set(123);
c.print();
pc-print();
72
Dissection
1. Public interface. First, we have the interface-functions or methods which provide the
behaviour ; these are declared public.
2. public means that the members can be directly accessed by client programs as:
instance.member e.g. c.set(123); where c is an instance of Cell.
3. Constructors must have the same name as the class; often, we will have multiple constructors, all with the same name; the name sharing is allowable due to function name overloading
functions may share the same name, as long as they are resolvable by their parameter list
(their signature).
4. Private by default. Had we left out the keyword public, the interface functions would have
been inaccessible by user programs.
5. After the interface functions, we have the representation of the state; this is private; though
the private representation is visible in the text, it is still encapsulated and invisible to client
programs.
Encapsulation As a consequence of encapsulation we can view objects, e.g. of class as
capsules, containing the representation data, in this case a single datum int val, but these
data may be accessed only through the interface functions (methods). This is shown diagrammatically:
+----------------------------------+
private: (hidden data)
Public interface
functions (methods)
+---------+
int val;
------ set()
+---------+
+---------+
------ get()
+---------+
+----------------------------------+
6. After previously specifying public, it is necessary to revoke this directive using private.
7. private means that the member v cannot be directly accessed by client programs. I.e.
c.val = 22;// illegal -- compiler error
This is called encapsulation and provides information hiding.
8. Generally, the syntax for calling a method (member function), i.e. sending a message to an
object is: object.method(argument), e.g.
c.set(123);
73
7.3
Normally, we will want to write classes so that they can be used by a great many programs
all without needing to have a copy of the class in each of them. To enable this we need two
separate class files: (a) the declaration of the class: Cell.h; this declares how to use the class;
(b) the definition of the class: Cell.cpp; this defines the workings of the class. Cell.cpp can be
compiled separately and the result stored in a library (as an object file). These files, and the new
(separate) test program, are shown below.
Notice how the declaration Cell.h must be #included in the .cpp files. As an exercise, remove
#include Cell.h from one of the .cpp and see what the compiler has to say.
Why is this necessary? Answer. If Cell.h is not #included in a .cpp file, when the compiler sees
Cell, it has no idea what you are talking about.
74
75
Cell c;
c.set(123);
cout Cell c: endl;
Cell* pc= &c;
cout Cell* pc = &c: endl;
c.print();
pc-print();
return 0;
7.4
Exercises
You will find the programs introduced earlier in the chapter in my public folder
cpp4jpprogscell.
1. Write a little program CellT4.cpp which uses Cell (Cell.h, Cell.cpp):
(i) Declare two Cells, ca initialised by the default constructor, cb initialised to state (value)
of 4094;
(ii) Print out the values of ca, cb;
(iii) Next, use set to change the value of cas state to 3056; print out its value to confirm.
2. Add a statement to CellT4.cpp as follows:
ca.val = 39;
See what the compiler says about that. Explain how to legally set the state of ca to 39 and
correct the statement above and add a statement to print the result.
3. Add statements to CellT4.cpp as follows:
ca = cb;
cout after ca = cb; endl;
ca.print(); cout endl;
Notice how you can assign objects.
76
7.5
3D Affine Transformations
This section is added because we may use these classes for some of our graphics course. You
will note that minor optimisations are in place (e.g. the data is public, so that we can set or get
without a method; this because I know exactly what it will be used for, and that it will not need
to be modified or extended.
You will note that we have not yet covered (i) operators, e.g. the put-to and get-from operators
and and the friend qualifier. Operators are like ordinary methods except you call them in
infix mode. The friend qualifier gives access, by non members, to the private area of a class.
For example, below is an ostream operator, not a Vector4D operator, but it needs to acces
the private data of Vector4D. When you have a operator you can output an object using the
normal cout object style. Likewise for output to files and for cin object.
// friends are not members, but have access to private data
friend std::ostream& operator(std::ostream& os, const Vector4D& v);
friend std::istream& operator(std::istream& os, Vector4D& v);
78
7.5.1
//-------------------------------------------------------// Vector4D.h
// j.g.c. 2005-03-28, 2006-10-31
//-------------------------------------------------------#ifndef Vector4DH
#define Vector4DH
#include string #include iostream
#include cstdio // for toString/sprintf
#include cmath
#define N 4
class Vector4D
// friends are not members, but have access to private data
friend std::ostream& operator(std::ostream& os, const Vector4D& v);
friend std::istream& operator(std::istream& os, Vector4D& v);
public:
Vector4D();
Vector4D(double d[]);
Vector4D(double x, double y, double z, double w);
Vector4D(std::istream& is);
void setAll(double val);
Vector4D add(Vector4D v);
Vector4D sub(Vector4D v);
Vector4D scale(double s);
double length();
std::string toString() const;
//note below public
double d[N];
sizet n;
;
#undef N
#endif
79
7.5.2
//-------------------------------------------------------// Vector4D.cpp
// j.g.c. 2005-03-28, 2006-10-31
//-------------------------------------------------------#include Vector4D.h
#define N 4
using namespace std;
Vector4D::Vector4D(): n(N)
setAll(0.0);
Vector4D Vector4D::add(Vector4D v)
Vector4D v1= Vector4D();
for(sizet i= 0; i n; i++)v1.d[i]= d[i] + v.d[i];
return v1;
Vector4D Vector4D::sub(Vector4D v)
Vector4D v1= Vector4D();
for(sizet i= 0; i n; i++)v1.d[i]= d[i] - v.d[i];
return v1;
Vector4D Vector4D::scale(double s)
Vector4D v1= Vector4D();
for(sizet i= 0; i n; i++)v1.d[i] = s*d[i];
return v1;
710
double Vector4D::length()
double len= 0.0;
for(sizet i= 0; i n; i++)len+= d[i]*d[i];
return sqrt(len);
711
7.5.3
/**
* Vector4DT1.cpp
* tests Vector4D
* author j.g.c.
* version 1.1; 2005-03-28, 2006-10-31
*/
#include iostream // for cout etc.
#include string
#include fstream // for files - ofstream, ifstream
#include Vector4D.h
using namespace std;
int main()
double d[4]= 1., 2., 3., 4.;
Vector4D x1= Vector4D(d);
coutVector4DT1 endl;
cout x1 = x1.toString() endl;
Vector4D x2= Vector4D();
x2.setAll(10.0);
cout x2 = x2.toString() endl;
Vector4D x3 = x2.add(x1);
cout x3 = x2.add(x1): x3.toString() endl;
Vector4D x4 = x3.scale(0.25);
cout x4 = x3.scale(0.25): x4.toString() endl;
double len = x1.length();
cout len = x1.length(): len endl;
712
7.5.4
//-------------------------------------------------------// Transform4D.h
// j.g.c. 2005-03-28, 2006-10-31
//-------------------------------------------------------#ifndef Transform4DH
#define Transform4DH
#include
#include
#include
#include
string
iostream
cstdio // for toString/sprintf
Vector4D.h
#define N 4
class Transform4D
// friends are not members, but have access to private data
friend std::ostream& operator(std::ostream& os,
const Transform4D& v);
friend std::istream& operator(std::istream& os, Transform4D& v);
public:
Transform4D();
Transform4D(double d[N][N]);
Transform4D(std::istream& is);
void
void
void
void
void
void
void
setAll(double val);
setIdentity();
setTrans(Vector4D t);
setRotZ(double theta);
setRotX(double theta);
setRotY(double theta);
setScale(Vector4D s);
7.5.5
//-------------------------------------------------------// Transform4D.cpp
// j.g.c. 2005-03-28, 2006-10-31
//-------------------------------------------------------#include Transform4D.h
#define N 4
using namespace std;
Transform4D::Transform4D() : n(N)
setAll(0.0);
void Transform4D::setIdentity()
setAll(0.0);
for(sizet i = 0; i n; i++)m[i][i] = 1.0;
void Transform4D::setTrans(Vector4D t)
setIdentity();
sizet n= n-1;
for(sizet c = 0; c n; c++)m[c][n] = t.d[c]; //x, y, z
714
void Transform4D::setRotZ(double theta)
setIdentity();
double cs = cos(theta);
double sn = sin(theta);
m[0][0] = cs;
m[0][1] = -sn;
m[1][0] = sn;
m[1][1] = cs;
void Transform4D::setScale(Vector4D s)
setIdentity();
sizet n= n-1;
for(sizet i = 0; i n; i++)m[i][i] = s.d[i]; //x, y, z
/**
* returns pre-multiplication of this*parameter
* @param v a vector to be multiplied by this
* @return product b = this*v
*/
Vector4D Transform4D::mpy(Vector4D a)
Vector4D b = Vector4D();
double dot;
for (sizet r = 0; r N; r++)
dot = 0;
for (sizet c = 0; c N; c++)
715
dot += m[r][c]*a.d[c];
b.d[r] = dot;
return b;
/**
* returns pre-multiplication of parameter*this
* @param a transform to be multiplied by this
* @return product b = a*this
*/
Transform4D Transform4D::mpy(Transform4D a)
Transform4D b = Transform4D();
double dot;
for (sizet r = 0; r N; r++)
for (sizet c = 0; c N; c++)
dot = 0.0;
for (sizet i = 0; i N; i++)
dot += a.m[r][i]*m[i][c];
b.m[r][c] = dot;
return b;
s+= ];
return s;
return is;
#undef N
7.5.6
/**
* Transform4DT1.cpp
* tests Transform4D
* @author j.g.c.
* version 1.0; 2005-03-28
*/
#include iostream // for cout etc.
#include string
#include fstream // for files - ofstream, ifstream
#include Transform4D.h
#include Vector4D.h
using namespace std;
class Transform4DT1
public:
Transform4DT1();
;
Transform4DT1::Transform4DT1()
cout Transform4DT1 endl;
double d[4]= 1.0, 2.0, 3.0, 4.0;
Vector4D xa= Vector4D(d);
cout xa = + xa.toString() endl;
Transform4D f = Transform4D();
f.setIdentity();
cout f.setIdentity(): n+ f.toString() endl;
Transform4D g = Transform4D();
g.setIdentity();
cout g.setIdentity(): n g.toString() endl;
717
719
Chapter 8
Inheritance
8.1
Introduction
dynamic binding. The point of being closed to modification is that you dont have to modify and
retest working software; the point of open to extension is that software can be extended as is
always necessary.
Liskov Substitution Principle It is important that inheriting classes respect the Liskov Substitution Principle; this states that it must be possible to substitute a derived object (of an inheriting
class) instead of a base object without breaking the software. For example, see below, we want
to be able to substitute a Student object (or a Lecturer object) for a Person object and the
software should still work. One everyday language way of expressing this is that a Student is a
Person. Is a is always a good thought test to run when you are thinking of using inheritance.
8.2
We will approach the solution in three stages; first, we show inheritance, but without virtual
functions. (If you want your code to objects as in Java, you need virtual functions.) Next we
demonstrate the use of protected as private but with public to deriving classes; Java programmers should be comfortable with the latter. Finally, we introduce virtual functions and the
full effect of dynamic binding and polymorphism.
8.2.1
First Attempt
Program CellR1.cpp presents class ReCell, an extension of class Cell. It extends Cell to have
a backup state that stores the last state when set() is used.
The extension comprises the following:
Constructor This initialises the backup value, and, via the base (Cell) constructor, the Cell part,
i.e. val;
Overloaded set() This first backs-up the current value. Then is uses the Cell version of set();
Overloaded print() print() is also overloaded;
The :: qualifier Cell::set(val) insists that set from the base class Cell is to be used; in Java
you would write super.set(val);
Additional behaviour A function restore is provided;
Additional state int backup stores the backup state.
That is nearly all. A ReCell object has whatever a Cell has, plus:
additional behaviour;
revised behaviour;
additional state memory (data).
But there is a little more, as we see below.
82
c.print();
83
Dissection of CellR1.cpp
1. Firstly, note that class Cell is unchanged.
2. ReCell contains an additional field backup. This means that a ReCell object has two
fields: val and backup.
3. c= rc; is a legal assignment, even though they are defined as different types.
This is because, owing to the inheritance rc is-a Cell in addition to being a ReCell.
4. Slicing. In the assignment c= rc;, slicing occurs, and only the Cell part of rc is copied. If
you like, think of it like this: c would have nowhere to put the additional backup part.
5. pc= &rc; is a also a legal assignment; however, this time, slicing does not occur although
we note that, in any case, no member data are copied (pc is a pointer) and pc takes on
the dynamic type ReCell* (pointer-to- ReCell). Java programmers will say big deal, Java
does this all the time.
6. However, pc-print(); acts as if slicing has taken place; this is because the static type of
pc is Cell* and, consequently, it is Cell::print() that is bound at compile-time.
In the third version of ReCell below, we will see how virtual functions can rectify the
matter. virtual in the declaration of a method means that it is capable of dynamic or
run-time binding. Java uses dynamic binding by default.
In C++, the default is compile time binding (linking); note: compile-time = static. To get
dynamic (run-time) binding, you must use the keyword virtual when declaring a method.
7. Inheritance is announced by class ReCell: public Cell. This is public inheritance;
private inheritance is possible, but is so uncommon that we are never going to mention it
again.
8. Notice that, in ReCell, we have been able to refrain from referencing the private members
of Cell. Later, we will see that protected, which allows a reduced privacy, can be used if
derived classes must access private members of base classes.
8.2.2
Protected
Program CellR2.cpp is the same as CellR1.cpp with the simple modification that Cells val
field is now protected instead of private. protected gives limited external visibility to inheriting classes only. Hence, protected is half-way privacy: visible to classes related by inheritance,
but otherwise hidden.
Thus, ReCell functions are at liberty to reference val.
Whether protected is good or bad is a moot point. Some people take the view that it dilutes the
main point of object-orientation encapsulation and information hiding.
As we have seen, in Cell, ReCell it is possible to avoid. In more complex classes, efficiency and
other concerns may make it unavoidable.
84
8.2.3
Finally, in CellR3.cpp we get to the complete ReCell. Already, in the introduction, we have
previewed the need to make print() a virtual function. In fact, for the purposes of demonstration, we have made two print() functions, one non-virtual as before, the other named printV()
virtual.
85
Dissection
1. First, notice the use of the virtual qualifier in the declaration of set and print. Note
again: virtual means capable of dynamic binding.
2. Now, let us jump to main(). In the following,
c= rc; cout c= rc, c.print() endl; c.print();
cout c= rc, c.printV() endl; c.printV(); cout endl;
Slicing again takes place and the following is output:
c= rc, c.print()
value= 789
c= rc, c.printV()
V: value= 789
c is of type Cell and nothing can make it otherwise.
3. However, as already indicated in 8.2.1, pc takes on the dynamic type pointer-to-ReCell.
Still, in the following,
pc= &rc;
pc = &rc, pc-print()
value= 789
4. But, in the following, we finally see the effect of virtual,
cout endl ...finally, we see the effect of *virtual* ... endl;
cout pc = &rc, pc-printV() endl; pc-printV();
The output is:
...finally, we see the effect of *virtual* ...
pc = &rc, pc-printV()
V: value= 789 backup= 456
Here, the dynamic type of pc (pointer-to-ReCell) is respected and ReCell::printV() is
called.
87
More on Dynamic Binding In function calls like c.print() etc., and even c.printV() conventional static-binding is used. I.e. the appropriate function is chosen & bound at compile-time
using the best choice of (overloaded) function the compiler can make: the one indicated by the
static (declared) type of the calling object.
However, in pc-printV() quite different code is generated: extra run-time selection & dispatcher
code is generated. At run-time, this code detects the run-time (or dynamic) type of the pointer,
and calls (dispatches) the appropriate function.
In summary, four points are worth making:
Dynamic binding is possible only for pointers or references;
We must be dealing with objects related via inheritance;
The static type of the pointer must be pointer-to-base or reference-to-base;
The method must have been declared virtual.
Why does C++ not simply allow dynamic binding by default (like Java) and so get rid of any need
for the virtual distinction? The answer is the desire to optimise for run-time efficiency by default.
The run-time selection & dispatcher code mentioned above takes extra time and extra memory.
It used to be that games programmers frowned upon inheritance and virtual functions because their
potential performance penalty. But that is no longer the case.
88
8.3
When we specialise a virtual function is provided, we call this overriding. On the other hand,
when we specialise a non-virtual function, we call this overloading.
The difference between overriding and overloading is that:
The binding (selection) of an overridden function is done at run-time;
An overloaded function is bound at compile time.
8.4
Strong Typing
Many discussions on strong typing implicitly or explicitly equate strong typing to:
Every variable is statically (at compile time) bound to a single type.
Calls to operations on variables including operations that combine two or more variables
can be statically checked for type compatibility.
To these we can add, for languages with dynamic typing:
The type of value held in a dynamically typed variable can be determined at run time, i.e.
dynamically, thus, it is possible to select at run-time an appropriate polymorphic function /
operator.
Even where dynamic binding is employed, it is still possible to determine type compatibility at
compile time. This is because dynamic binding is restricted to the family of classes related by
inheritance, and, since derived / inheriting classes are merely extensions of their base(s), a derived
class is type-compatible with a base class.
For the above situation to be semantically safe, we must ensure that the type system provided
by the class hierarchy respects the Liskov Substitution Principle: if function f() is defined for a
parameter of class B, and given a class D derived from B, then calls to f(), with an argument of
class D, should work properly.
8.5
If we declare a function, say VF(), as virtual in a base class, say B. Then, in a derived class, say
D,VF() can be overridden. Of course, D::VF() must have matching signature/prototype.
Now, a pointer to an object of class D, pd, can be assigned to a pointer of class B, pb, without
explicit type conversion.
89
Then, when invoked with a pointer to the base class, the selection from the overridden virtual
functions VF() is done dynamically, i.e. delayed until run-time, when the dynamic type of the
pointer will be known.
Thus:
B b;
B* pb = &b;
D d;
pb = &d;
pb-VF(); //Ds VF selected - dynamically
//if VF is *not* virtual - Bs VF would have been
//selected --- i.e. statically bound, at compile-time.
In the absence of a derived class VF(), of course, the base class virtual function will be used.
8.6
Without care it is easy for beginners to abuse inheritance through confusion of the class relationships: is-a, versus has-a. Inheritance is a mechanism by which a base class may be extended to
yield a derived class; inclusion (composition) is the normal manner in which we can build a class
using objects of other classes (composition, inclusion).
Since inheritance provides extension, it is crucial to understand that the relationship between a
derived class and a base class is is-a: a derived class object is-a base class object.
In the Person class hierarchy, described below, in which Student and Lecturer are derived from
(inherit from) Person, it is clear that an Student object certainly does qualify for is-a Person.
But please note that the is-a relationship is not transitive; a Person need not necessarily be a
Student or Lecturer. Careful design of classes will mean that they will obey normal everyday
semantics of relationships between categories, e.g. Persons, and sub-categories, e.g. Students.
On the other hand, the relationship between class Person and Address is has-a: a Person object
has-a Name and has-an Address. Given our strict interpretation of has-a and is-a, on no account
could we say that a Person is-an Address.
8.7
Suppose you sell, or give, precompiled versions of the Person class hierarchy, to someone else,
together with a main() calling program without giving them the source code. You envisage
them extending the program by building other classes based on your hierarchy.
810
Without virtual functions and overriding, you would have to, in the main() program, predict all the
different operations that would ever be required (impossible) and provide them in a giant stitch
statement.
With inheritance and virtual functions, anyone can extend the class hierarchy, so long a they override
appropriate virtual functions. Moreover, the main() program can call code that is developed long
after it.
This leads to the aphorisms mentioned earlier:
Old code can call new code which contrasts with the non-object-oriented solution of new code
being written to call old code provided in libraries.
Upside-down libraries i.e. the calling framework remains fixed, while the called functions may
be modified. Virtual functions are C++s object-oriented way of avoiding callback functions
such as those use in OpenGL.
And, we recall the open-closed principle: software modules (here classes) must be open to extension, but must be closed to modification.
8.8
It is always difficult to explain the software design process in notes. Normally, the notes contain
the finished article without any discussion of the design and implementation steps.
We started off requiring classes to represent a course (now module) in the institute. Our first
attempt was the Student class below.
First, we thought the a single string for Address was inadequate so we designed an Address class.
Then it became clear that we needed a Lecturer and possibly other people classes. Consequently,
we designed a Person class into which we could factor all the data and behaviour common to
Lecturer and Student, and, presumably, other people classes that may arise.
class Student
public:
Student() : fn(), ln(), id(), ad()
Student(string fn, string ln, string id, string ad, int m) :
fn(fn), ln(ln), id(id), ad(ad)
Student(string);
string first() return fn; ;
string last() return ln; ;
string ident() return id;;
string address() return ad;;
string toString() const;
private:
string fn;
string ln;
string id;
string ad;
;
811
File Person.h now shows a Person class. It has been deliberately kept simple, and thus, other
than as a demonstration of inheritance, it is rather limited and unremarkable.
// ---------------- Person.h --------------------------// j.g.c version 1.0; 2007-08-27
// ----------------------------------------------------#ifndef PERSONH
#define PERSONH
#include string
#include StringTokenizer.h
#include Address.h
class Person
private:
std::string firstName;
std::string lastName;
Address address;
public:
Person(std::string& firstName, std::string& lastName,
Address& address);
// see Address.h for comment on = ...
Person(std::string str = blank,blank;blank,blank,blank,blank);
virtual std::string toString() const;
;
#endif
// ---------------- Person.cpp --------------------------// j.g.c version 1.0; 2007-08-27
// ----------------------------------------------------#include Person.h
#include iostream
Person::Person(std::string& firstName, std::string& lastName,
Address& address)
: firstName(firstName), lastName(lastName),
address(address)
Person::Person(std::string str)
// first split into name ; address
StringTokenizer st1 = StringTokenizer(str, ;);
std::string sName = st1.token(0);
std::string sAddress = st1.token(1);
address = Address(sAddress);
// now split name
StringTokenizer st2 = StringTokenizer(sName, ,);
812
firstName = st2.token(0);
lastName = st2.token(1);
Address
// ---------------- Address.h --------------------------// j.g.c version 1.0; 2007-08-26
// ----------------------------------------------------#ifndef ADDRESSH
#define ADDRESSH
#include string
#include StringTokenizer.h
class Address
private:
std::string street; // street = first line of address
std::string city;
std::string region; // county or state
std::string country;
public:
Address(std::string& street, std::string& city,
std::string& region, std::string& country);
// = does for default constructor
// which C++ wants but will never be used
Address(std::string str = blank,blank,blank,blank);
std::string toString() const;
;
#endif
Dissection of Person.h
1. There are two constructors; the first is the standard initialising constructor; the second
constructs from a string. The latter is nice if we want to read from a file.
2. We avoid needing a default constructor (Person() with the default argument in
Person(std::string str = blank,blank;blank,blank,blank,blank);
If we ever have Person(), the Person(std::string) constructor will be called with those
default arguments. This is fine; in fact we never deliberately use a default constructor but
C++ implicitly inserts one in certain situations and the compiler will complain if it doesnt
exist.
813
8.9
A Student Class
File Student.h shows an Student class which is derived from Person it inherits from Person.
// ---------------- Student.h --------------------------// j.g.c version 1.0; 2007-08-27
// ----------------------------------------------------#ifndef STUDENTH
#define STUDENTH
#include string
#include Person.h
class Student: public Person
private:
std::string id;
public:
Student(std::string& firstName, std::string& lastName,
Address& address, std::string& id);
// see Address.h for comment on = ...
Student(std::string str = blank,blank;blank,blank,blank,blank;blank);
virtual std::string toString() const;
;
#endif
814
Dissection of Student
1. class Student : public Person... signifies that Student inherits from Person.
2. Student exhibits the same behaviour as Person; well, the only common behaviour is the
toString method!
3. As well as id Student, via inheritance, contains all the data members of Person;
4. As a consequence of public inheritance, only the public parts of Person are visible within
Student.
8.10
First of all we show a simple test program for Person alone; then we show a test that uses al three
classes in the hierarchy ( Person, Student, Lecturer and demonstrates polymorphism.
File PersonT1.cpp shows a program which exercises Person. You will have access to similar
programs for Student and Lecturer; the latter two are extremely similar to PersonT1.cpp and
not worth printing here.
8.10.1
PersonT1.cpp
PersonT1.cpp demonstrates the use of std::vector, std::list, and std::iterator and the
random number generator. These may be worth some discussion in class.
A further example of std::vector is given in section 8.11 at the end of this Chapter.
815
regions.pushback(string(Green County));
regions.pushback(string(Old County));
vectorstring countries = vectorstring();
countries.pushback(string(Ireland));
countries.pushback(string(Wales));
countries.pushback(string(England));
countries.pushback(string(Scotland));
listPerson personList = listPerson();
string street, city, region, country, lastName, firstName;
unsigned int rnseed= 139;
srand(rnseed); // initialise random number generator.
int n = 10, j;
for(int i = 0; i n; ++i)
cout i = i endl;
j = rand()%streets.size();
street = streets.at(j);
j = rand()%cities.size();
city = cities.at(j);
j = rand()%regions.size();
region = regions.at(j);
j = rand()%countries.size();
country = countries.at(j);
j = rand()%firstNames.size();
firstName = firstNames.at(j);
j = rand()%lastNames.size();
lastName = lastNames.at(j);
Address a = Address(street, city, region, country);
cout a.toString() endl;
Person p = Person(firstName, lastName, a);
cout p.toString() endl;
personList.pushback(p);
;
817
8.10.2
PersonT2.cpp
vector
list
iostream
iterator
cstdlib // for rand
sstream
j = rand()%streets.size();
street = streets.at(j);
j = rand()%cities.size();
city = cities.at(j);
j = rand()%regions.size();
region = regions.at(j);
j = rand()%countries.size();
country = countries.at(j);
j = rand()%firstNames.size();
firstName = firstNames.at(j);
j = rand()%lastNames.size();
lastName = lastNames.at(j);
Address a = Address(street, city, region, country);
//cout a.toString() endl;
int type = rand()%4; //randomly choose type of Person
if(type 2)
Person *p = new Person(firstName, lastName, a);
personList.pushback(p);
else if(type == 2)
int k = i + 10;
ostringstream ossid;
ossid id1 k;
string id = ossid.str();
Student *st = new Student(firstName, lastName, a, id);
personList.pushback(st);
else
j = rand()%offices.size();
string office = offices.at(j);
Lecturer *l = new Lecturer(firstName, lastName, a, office);
personList.pushback(l);
;
819
Dissection of PersonT2.cpp
1. In order to use polymorphism, we now use a listPerson* a list of pointer to the base
class.
2. This means that (*itr)-toString() will act polymorphically and bind the appropriate
toString (Person, Student, or Lecturer) according to the dynamic type of what itr has
extracted from the list; for this to happen, toString must be virtual, see below.
3. If we had used listPerson (plus obvious changes), all objects inserted into the list would
have been sliced to produce a Person object and the additional parts in Student and Lecturer
would have been discarded.
4. If we had used listPerson* as above, but had left the virtual qualifier off toString,
then compile time (static) binding would have been used to and in (*itr)-toString()
Person::toString() would have been linked and all the objects would have been displayed
as if they were (plain) Person.
That is, slicing would not have taken place; the extended id and office are there alright,
but Person::toString() knows nothing about them.
Make sure you understand the difference between slicing and static binding in the context
above.
8.11
The program in VectorTest1.cpp demonstrates the usefulness of the standard library vector
class and the associated standard library algorithms.
//------ VectorTest1.cpp -------------------------// tests for std::vector
// j.g.c. 2003/02/20
//------------------------------------------------#include iostream // for cout etc.
#include string
#include fstream // for files - ofstream, ifstream
#include algorithm
#include vector
#include cstdlib // for rand
#include Marks.h
using namespace std;
820
int main()
const int n= 10; unsigned int rnseed= 139;
double d[n];
srand(rnseed); // initialise random number generator.
//cout Max. random number= RANDMAX endl;
for(int i= 0; i n; i++)
d[i]= (double)rand()/(double)RANDMAX; // numbers in 0.0 to 1.0
cout endl;
vectordouble v1;
for(int i= 0; i n; i++)
v1.pushback(d[i]);
cout endl;
cout nIterating through vector, another way n endl;
for(int i = 0; i n; i++)
cout v1[i] ;
cout endl;
sort(v1.begin(), v1.end());
cout nSorted vectorn endl;
for(int i = 0; i n; i++)
cout v1[i] ;
cout endl;
reverse(v1.begin(), v1.end());
cout nReverse sorted vectorn endl;
for(int i = 0; i n; i++)
cout v1[i] ;
cout endl;
cout nFront, back elements of vector, and size()n endl;
cout v1.front() v1.back() v1.size();
821
cout endl;
cout nYou can modify the vector (x 10.0)n endl;
for(int i = 0; i n; i++)
v1[i] = v1[i]*10.0;
cout endl;
cout nYou can have vectors of stringsn endl;
vector string v2;
v2.pushback(The); v2.pushback(quick);
v2.pushback(brown); v2.pushback(fox);
v2.pushback(jumped); v2.pushback(over);
v2.pushback(the); v2.pushback(lazy);
v2.pushback(dogs); v2.pushback(back);
for(unsigned int i = 0; i v2.size(); i++)
cout v2[i] ;
cout endl;
sort(v2.begin(), v2.end());
cout nSortedn endl;
for(unsigned int i = 0; i v2.size(); i++)
cout v2[i] ;
cout endl;
cout nYou can assign vectors n endl;
vectorstring v12 = v2;
for(unsigned int i = 0; i v12.size(); i++)
cout v2[i] ;
cout endl;
cout nYou can remove elements from vectors n endl;
remove(v12.begin(), v12.end(), lazy);
for(unsigned int i = 0; i v12.size(); i++)
cout v12[i] ;
cout endl;
cout nYou can search vectors n endl;
vectorstring::iterator it;
it= find(v12.begin(), v12.end(), fox);
cout*it endl;
822
cout endl;
vectorstring v22;
cout nYou can copy using copy, but use resize first n endl;
v22.resize(4);
copy(v12.begin(), v12.begin()+3, v22.begin() );
for(unsigned int i = 0; i v22.size(); i++)
cout v22[i] ;
cout endl;
vector vectorstring vv;
cout nYou can have vectors of vectors, but use in decl. n endl;
vv.pushback(v2);
vv.pushback(v12);
vv.pushback(v22);
for(unsigned int i = 0; i vv.size(); i++)
cout vector i: ;
for(unsigned int j = 0; j vv[i].size(); j++)
cout vv[i][j] ;
cout endl;
cout endl;
cout nYou can have vectors of your own objectsn endl;
vector Marks v3;
int ex, ca;
for(unsigned int i = 0; i 8; i++)
ex= rand()%100; ca= rand()%100;
Marks m= Marks(ex, ca);
v3.pushback(m);
cout endl;
sort(v3.begin(), v3.end());
cout nSortedn endl;
for(unsigned int i = 0; i v3.size(); i++)
cout v3[i] ;
823
cout endl;
cout nYou can then save that vector to a filen endl;
ofstream fileout(xyz.dat);
if(!fileout)
cerr cannot open file xyz.datn;
return -1;
fileout.close();
cout n... and read some of them backn endl;
vectorMarks v4;
ifstream filein(xyz.dat);
if(!filein)
cerr cannot open file xyz.datn;
return -1;
Marks tmp;
for(unsigned int i=0; i 5; i++)
filein tmp;
v4.pushback(tmp);
filein.close();
for(unsigned int i = 0; i v4.size(); i++)
cout v4[i] ;
cout endl;
return 0;
824
8.12
Marks
Notice that if you ever want to apply the sort algorithm to a collection of objects, then the class
must have a (ordering) operator:
bool operator(const Marks& lhs, const Marks& rhs)
return lhs.overall() rhs.overall();
825
Marks::Marks(istream& is)
// assumes that fields are separated by whitespace
string tmp;
is tmp; // throw away decoration (Marks:)
is ex;
is ca;
826
Chapter 9
Classes that manage memory
9.1
Introduction
The classes that we have used so far Cell, ReCell, Person and the graphics Vector classes
have avoided any use of heap memory. In each case, member variables are stack memory based
and memory is managed automatically, i.e. object creation and deletion is done automatically by
the language.
On the other hand, many practical classes/objects require, for one reason or another, to employ
heap memory; the sort of objects that require the flexibility of heap storage are containers: lists,
stacks, queues, trees, and dynamically sizable arrays and strings.
Fortunately, the C++ standard library now makes available an extensive range of containers, so
that the development of heap based classes is no longer as necessary as it used to be. That is
fortunate, because, as we repeat below, development of classes that need to manage memory is
difficult and error prone.
Make sure you recall heap and stack variables. In summary: normally declared/defined variables
are constructed (created) on the stack; they are constructed when control reaches the point of
declaration and they are destroyed when control reaches the end of the scope unit in which they
were declared. Thus, programmers need only declare them and the language takes care of the
rest the lifetime of the variables is handled automatically. Variables that are constructed using
new reside on the heap; as well as requiring to be explicitly constructed (using new), they must be
destroyed explicitly (using delete).
The important distinction between heap-based objects and stack-based objects is the same for
objects as for variables. That is, the compiler will automatically and implicitly generate memory
management and similar functions for the simpler stack objects, but for heap based objects
those objects whose state is stored in heap memory and accessed through pointers, the class
developer must explicitly program memory management, see section 5.11, i.e. constructors and
destructors, and associated functions such as copy.
Memory management is never easy. However, we can develop patterns or templates (taking
the general meanings of these terms) that show how to provide the memory management facilities
generally needed for heap-based classes. We have briefly covered heap memory management in
section 5.11; maybe you should review that now.
91
9.2
9.2.1
Dissection of Array1.h
Copy constructor A copy constructor always has the signature T(const Type& source);, where
Type is the class name.
Array(const Array& source);
Destructor There is only ever one destructor, and it must have the name of the class preceded
by (tilde).
Array();
92
93
Destructors cannot be invoked explicitly they are implicitly invoked when an object must
be destroyed, i.e. when exiting the block or function in which it was defined, i.e. when
execution leaves the portion of the program in which it is in scope.
Assignment operator
Private function copy Note that copy is made private because it is used only internally
it is not part of the class interface.
void copy(const Array& source);
9.2.2
The implementation code for the Array class is shown in Figures 9.2 and 9.3.
Dissection of Array1.cpp
1. We have added print statements to some of the significant functions; this is to enable tracing
of calls to them. As we shall see in the next subsection, you may be surprised what is going
on during, for example a call to a function which takes an object as a parameter.
However, in the interests of clarity, these prints statements have been removed in the following
discussions.
2. Default constructor.
Array::Array(unsigned int len) : len(len)
if(len==0)dat = 0; return;
dat = new int[len];
assert(dat!= 0);
for(unsigned int i=0; i len; i++)set(0, i);
(a) If length is zero, no need to allocate, set dat pointer to null and return.
(b) Otherwise allocate an array of len ints on heap. new returns a pointer to this data
block, and this is assigned to dat.
(c) Use assert to ensure that the allocation was successful; new returns a null pointer if it
is unsuccessful, e.g. due to resources of free memory having become exhausted.
(d) Then initialise the memory to 0.
3. Initialising constructor.
Array::Array(unsigned int len, int val) : len(len)
if(len==0)dat = 0; return;
dat = new int[len];
assert(dat!= 0);
for(unsigned int i=0; i len; i++)set(val, i);
94
Array::Array()
cout *Array()* endl;
delete [] dat;
return *this;
cout endl;
96
This is the same as the default constructor except for the initialising value.
4. Function copy.
void Array::copy(const Array& source)
len= source.length();
if(len== 0)dat = 0; return;
dat = new int[len];
assert(dat!= 0);
for(unsigned int i= 0; i len; i++)dat[i] = source.get(i);
(a) This is very similar to the default constructor except that this time we have passed
another object to be copied.
(b) Notice that we have passed a reference (Array& source). If we dont well end up
constructing many multiple copies, each of which must also be destroyed. And when
the object becomes large, as may be the case for an Array the performance drain can
be considerable.
(c) Of course, we guarantee the safety of the referenced object (in the caller) by making
the reference const.
5. Copy constructor.
Array::Array(const Array& source)
copy(source);
Here, all the work is done by copy. Again notice the use of reference and const.
6. Destructor.
Array::Array()
delete [] dat;
return *this;
(a) Let us say we have two Array objects, x, y and x = y. Then this assignment is exactly
equivalent to
97
x.operator=(y);
(b) Again notice the use of a reference and const.
(c) Since we can envisage x = x, however improbable, we have to be careful to check
whether this is the case and if it is, do nothing.
(d) this is an implicit pointer variable which points at the object itself.
Thus
if(this!= &source) checks if the calling object and the source object share the same
memory location and so are the same object!
(e) return *this; returns the object (actually a reference, see next comment).
(f) Why does operator= return Array&? (a) In C and C++ is is standard for an assignment
to have the value of the object assigned, e.g.
int x = y = 10;
we want the same for objects.
And, as usual, we want the efficiency of reference passing.
The next chapter has a more extensive discussion of operator overloading.
9.2.3
Assertions
C++ provides a macro function, assert, which evaluates a Boolean expression and if it evaluates
to false generates a run-time error message and halts the program. assert is defined in cassert.
The following shows an example use of assert to check the legality of the array index operator;
len is the length of the array.
void Array::set(int val, unsigned int i)
assert(ilen);
dat[i]= val;
#define NDEBUG
Note also the assertion which tests whether memory allocation has been successful:
dat = new int[len];
assert(dat!= 0);
Use of assertions like this is an example of defensive programming and can be very useful in testing
and can greatly simplify debugging.
9.2.4
The program in Figure 9.4 demonstrates the use of the Array class.
Dissection The most interesting activities are those involving the constructors, the assignment
operator and the destructor. To examine these, it is useful to examine the output generated by
Array1T1.cpp.
1. This code
Array c1;
cout Array c1: ; c1.print();
outputs:
*1.Array(unsigned int)*
Array c1: length= 0:
i.e. the default constructor is called, and, when printed, the array is seen to have length 0.
2. This code
Array c2(3);
cout Array c2(3): ; c2.print();
outputs:
*1.Array(unsigned int)*
Array c2(3): length= 3: 0 0 0
i.e. the constructor is called with argument 3, and, when printed, the array is seen to have
length 3 and initial values 0.
99
return b;
int main()
cout heap based array ... endl;
Array c1; cout Array c1: ; c1.print();
Array c2(3); cout Array c2(3): ; c2.print();
//c2.set(200, 10); demonstrates assert
Array c3(5, 7); cout Array c3(5, 7): ; c3.print(); cout endl;
cout c3.length() c3.length() endl;
int len= c3.length();
for(int i= 0; i len; i++)
c3.set(i+20, i);
3.
4.
5.
Array c5(c3);
cout Array 5(c3): ; c5.print(); cout endl;
outputs:
3.*Array(const Array&) -- copy constructor*
Array c5(c3): length= 5: 20 21 22 23 24
i.e. the copy constructor is called with argument c3; in this case this is more expected.
912
9.3
If we have to reference the object itself in a member function, we can do it through an implicit
pointer this; e.g. below in operator=:
Array& Array::operator=(const Array& source)
if(this!= &source) // beware a= a;
delete [] dat;
copy(source);
return *this;
9.4
Usually, copy constructors must be provided for classes which use heap memory. As already
described, if the developer of the class does not provide one, the compiler will do so.
9.4.1
The problem is that the compiler provided default copy constructor will merely perform a nave
member-wise or shallow copy constructor. In the case of Array this means that only members
len and dat will be copied.
The deficiency of the default copy constructor is seen in the following example:
Array a(5,7), b(a);
Array c(a); //OK so far, but b and a share the same
//data array -- referenced by pdat
b.set(125, 2); // 2nd element of b becomes 125
int x= a.get(2); // what value of x? 7?
// *no* it is 125!
// because a.pdat and b.pdat are aliases for one
//another
A worse problem is caused if a gets destroyed before b, or vice versa; in this case b.dat will
become a dangling reference.
913
9.4.2
This provides a so-called deep copy, i.e. copy not just the explicit members, but also what they
point to.
9.4.3
Already, we have given a detailed account of constructor activity during the following function call:
call:
c2= fred(c3);
Array fred(Array a)
Array b(a);
// ...
return b;
914
9.5
Assignment
Assignment is subject to the same requirements as copy constructors, and the discussion of section
9.4; i.e. a non-trivial assignment operator must usually be provided for classes which use heap
memory. If such is not provided, again the compiler will provide a default one, which uses shallow
copy.
Copy constructors and assignment operators perform very similar tasks, and so their code often
closely mirrors that of the other. In the following example, the only difference is (a) the check
against mistaken deletion of an objects memory if it is assigned to itself, and (b) the use of
delete [] dat; to free whatever memory the object is currently using.
Array& Array::operator=(const Array& source)
cout *operator=* endl;
if(this!= &source) // beware a= a;
delete [] dat;
copy(source);
return *this;
9.6
Destructor
As repeatedly mentioned, destructors are called, automatically, when program control reaches the
end of the block in which the object was defined (and hence constructed).
The destructor for an object which references heap memory is subject to the same memory management demands as copy constructor and assignment operator. The compiler will always provide
adequate destruction of stack-based objects, but, for heap-based objects, proper destructor memory management must be provided if garbage and memory-leaks (or worse, dangling pointers) are
to be avoided.
Thus, the Array destructor:
Array::Array()
delete [] dat;
915
9.7
The Big-Three
The C++ FAQs (Cline et al. 1999b) uses the term the Big-Three for the previous three functions:
copy constructor, assignment operator, and destructor.
This is because, for any class for which you must provide one, it is almost certainly necessary to
provide all three.
9.8
I hope that the discussion in subsection 9.4.3 will have given the (correct) impression that function
fred may be generating an awful lot of unnecessary work.
In many cases, a better solution is the following:
call:
Here we have dispensed with one call to a copy constructor a. But the others remain creation
of b and creation of the temporary in the caller. Actually, as mentioned above, we could equally
well use:
Array fred11(Array a)
// a is a local *copy*
// do things to a
return a;
Reference return It would now be tempting to continue the trend and return a reference to b
(or a if fred11).
In this circumstance, we cannot easily do that. Since b, or a are local variables, they are destroyed
upon return. Thus,
call:
c2= fred21(c3);
Array& fred2err(Array a)
// a is a local *copy*
916
// do things to a
return a;
is very seriously flawed; upon return, a reference (remember a reference is just an alias to a
is returned (to a temporary); next, a is destroyed, so that when, in the caller, the assignment has
a dangling reference on its right-hand-side!.
This is subtle. Just because an object references heap-memory, it itself is not a heap object unless
it is created using new.
Returning a reference to a local object is a cardinal sin.
Pointer return Obviously, if b is to become a revised version of the argument (c3), we cannot
avoid a constructor for b. However, what about the temporary? This can be accomplished by the
code in fred2:
//Warning: ugly code follows
Array* pc2;
call:
return *pb;
The price to pay is that b, or rather pb, must reference a heap object; to create this, we must use
new.
The real disadvantage of fred2 is that is creates a heap object that someone else must take
responsibility for destroying i.e. potential garbage. However, you may be relieved to hear that
this example is quite artificial, and that properly designed software will normally use new only within
a class, and that a corresponding delete will be provided (by the class designer).
Return value optimisation Actually, the following version, which is by far the prettiest, may turn
out to be the most efficient due to return value optimisation.
Array fred11(Array a)
// a is a local *copy*
// do things to a
return a;
917
When the compiler notices a local Array a being created, followed by a return a, it knows that
considerable optimisation can achieved by creating a in the callers space.
9.9
Up to now, we have been careful to use, where possible, accessor functions such as get in member
functions, e.g. in copy:
void Array::copy(const Array& source)
len= source.length();
if(len== 0)dat = 0; return;
dat = new int[len];
assert(dat!= 0);
for(unsigned int i= 0; i len; i++)dat[i] = source.get(i);
918
Chapter 10
Operator Overloading
10.1
Introduction
Already, in the previous chapter, we have glanced at the provision of operator= for the Array
class. We have also noted that these so-called overloaded operators have exactly the same meaning
as an equivalent traditionally named function.
Here, we go into a little more detail, and, at the end of the chapter, we discuss some of the
consequences of the asymmetry cause by the difference between implicit and explicit arguments,
e.g. implicit: object1 versus explicit: object2 in object1.memberfunction(object2).
It should be said that operator overloading is not favoured by everyone. As we have stated,
operators calls are no different to function calls. Neither, as we show below is there any magic
involved, nor any performance advantages. Java does not allow operator overloading; in my opinion
that was a wise design decision. However, whether you like operator overloading or not, there are
at least three good reasons to know about it: (i) for some classes, you need to be able to create
an assignment operator (operator=) if, like the Array classes in the previous chapter and in
this chapter, the classes are heap based, then you have no choice, you must provide an assignment
operator; (ii) many programs that you will use or modify will employ operator overloading; (iii) if
you want use standard library containers and you want to use sort and other algorithms on objects
of a class, you will need to supply the comparison operator, operator.
In addition, some argue that it is beneficial to be able to use + when we want to add, e.g. Arrays,
just as adding ints or doubles; the claim is that this makes the language more expressive.
10.2
As a lead-in to our discussion on operator overloading, we first provide add functions for
Array; subsequently, we will replace these with overloaded operators +, +=. We present
Array2.h, Array2.cpp and ArrayT2T1.cpp almost without comment except for the nonmember function add there should be nothing new. See Figures 10.1, 10.2, 10.3 and 10.4.
101
102
Array::Array()
delete [] dat;
return *this;
103
cout endl;
104
105
10.3
106
10.4
Operators
In this section, we show how to replace the add functions of the previous section with overloaded
operators:
addTo replaced by operator+=
add replaced by operator+; as with add, operator+ is declared as a non member.
In addition, we provide an overloaded operator so that we can output Array objects using
cout; this operator also must be declared as a non-member.
operator is really handy to have; as you will notice, I include it in many of the classes I
write; it makes use of the classes very easy, especially testing.
Finally, we include an indexing operator[] which replaces get and set.
See Figures 10.5, 10.6, 10.7 and 10.8.
107
108
Array::Array()
delete [] dat;
return *this;
109
os endl;
return os;
1010
c3+= c2;
//previously c3.addTo(c2);
cout c3+= c2: c3 endl endl;
cout c2: c2;
Dissection of Array3
1. Here we see operator+= compared to addTo
c3+= c2;
//previously c3.addTo(c2);
2. Here we see that we can call operator+= using normal function-call syntax:
c3.operator+=(c2);
//c3+= c2;
// c4 = c2 + c3;
5. Here we show use of operator[] and how it replaces both put and get:
for(int i= 0; i len; i++)
c6[i]= i+50; // replaces c6.put(1+50, i);
1012
Friend functions and operators If non-member functions (or operators) need to access
verb+private+ data (not the case here, but will be the case in later chapters), we have a problem
do we have to make the data public? No, we can declare the function/operator as friend in
the class, e.g. let us say that operator needed to access the private members of Array, the
following declaration, in the class, would give it the appropriate access:
friend ostream& operator(ostream& os,const Array& a);
1013
10.5
10.5.1
Introduction
The objective of this section is to discuss circumstances in which it is better to make functions or
operators non-member (even if we need to make them friend, and to discuss the related matters
of implicit arguments (the object itself) and conversion / coercion. We use a String class as an
example for some of these phenomena.
I have changed this section very little since 1997; I include it here just to point out some quite
subtle aspects of member versus non-member functions and conversions. You will not be examined
on any of this, but I would feel uncomfortable if I hadnt pointed out some of these pitfalls.
10.5.2
A String Class
Although the standard library provides a perfectly good string class, we need the following homegrown one to furnish examples for the later discussion.
len=len;
1015
p=new char[len+1];
p[0]=0;
// copy constructor
String::String(const String& other)
len = other.length();
p=new char [len+1];
strcpy(p, other.p);
return *this;
String::String()
delete [] p;
p=0;
strcpy(&(pp[oldl]), other.p);
p = pp;
return *this;
1017
//reads up to n
void String::gets()
char c,s[81];
int i = 0;
while(std::cin.get(c))
if(c==n)break;
s[i] = c;
i++;
s[i]=0;
*this = String(s);
s[i]=0;
*this = String(s);
s1.gets();
cout s1: length = s1.length() endl; s1.print();
cout endl;
cout get(): enter a string -- end with any white-space:;
s4.get();
cout s4: length = s4.length() endl; s4.print();
cout endl;
cout s2: length =
cout endl;
cout s3: length =
cout endl;
s2 += s1;
cout s2 += s1;
cout s2: length =
cout endl;
endl;
s2.length() endl; s2.print();
s4 = concatNM(s1, s2);
cout s4 = concatNM(s1, s2); endl;
cout s4: length = s4.length() endl; s4.print();
cout endl;
s4 = s1 + s2;
cout s4 = s1 + s2; endl;
cout s4: length = s4.length() endl; s4.print();
cout endl;
s4 = s1.concatM(s2);
cout s4 = s1.concatM(s2); endl;
cout s4: length = s4.length() endl; s4.print();
cout endl;
s4 = s1 / s2;
cout s4 = s1 / s2; endl;
cout s4: length = s4.length() endl; s4.print();
cout endl;
return 0;
10.5.3
Assume that we want to write a function that concatenates two Strings to produce a third, and
leaving the two sources intact. There are at least four ways we can do it:
1. Member function.
1019
Member function
The member function version, concatM, is defined as follows:
String String::concatM(const String& source2)
String s = *this;
s += source2;
return s;
Notice that it uses operator +=; further, notice that it returns a String by value, it is tempting
to return by reference (String&), but this would lead to a dangling reference, since String s is
destroyed on return from concatM.
We notice too that the first argument is implicit it is the object itself, as can be seen from the
call:
String s1, s2, s4;
s4 = s1.concatM(s2);
The lack of symmetry in the call is not appealing, we would expect s1 and s2 to be treated
uniformly. This can be corrected by developing concat as a non-member function.
Non-member function
The non-member function version, concatNM, is defined as follows:
String concatNM(const String& source1, const String&source2)
String s = source1;
s += source2;
return s;
String s = source1;
s += source2;
return s;
10.5.4
Coercion of Arguments
There is a very subtle difference between the non-member operator+ and the member operator /
above. The relevant rule is the following: if an operator function is a member, its first argument
must match the class type. The second argument is not so restricted, since, if one is available, an
appropriate constructor will be invoked to perform a coercion (an implicit conversion).
In the case of a non-member operator, coercion can be applied to either argument. Example:
String a, b, c;
c = abcd + xyz ; //OK if non-member +;
//constructor String(char *) is used to convert
c = a + asdf; //OK in either case, non-member,
c = a / asdf; // and member.
c = abdcef + b; //OK only if non-member + .
10.5.5
1022
Chapter 11
Templates
11.1
Introduction
There are many examples of cases where we develop a class, e.g. Array, or List, which manipulates objects of a particular type, e.g. int, only to find that we need to adapt it to work for float
or other types, or, indeed, for user defined classes. In the Array and List examples in Chapter 12
the element-type is the only part which changes; thus, it seems a pity to have to have to develop
additional classes.
The same goes for functions, e.g. swap: all swap functions have the same functional requirements
and usually they are implemented as three assignments. If you were to adapt an int swap function
to float, or even string, you would merely exchange all occurrences of int for float or
String, etc..., as the case may be.
In such cases, what we require is often termed generic software; we have been using standard
library templates in the previous chapter; Java introduced generic (template) collection classes in
Java 1.5.
This chapter discusses templates. With templates, we can develop a module or function in which
the type / class of one or more elements is given as a parameter. The actual value of the type
parameters is specified at compile time.
Of course, the main objective is reuse. As we have argued before, the least acceptable form of
reuse is duplicate-and-modify simply because each duplication creates an new, additional, module
to be tested, documented and maintained.
First we will give a quick discussion of the implementation of a template Array the int version of
which we have become very familiar with. Although the syntax of template classes looks complex at
first, we will see that there is little new to be learned beyond what was learned in the development
of the original Array(int) class. In fact, the main advice is: design your data abstraction (array,
list, queue, whatever), then add the template bits using an established template class as an
example (a template, in fact!).
First, we provide an example of a template function.
111
11.2
Template Functions
11.2.1
The two overloaded swap functions shown below clearly invite use of a type parameter.
void swap(int& a, int& b)
int temp = a; a = b; b = temp;
11.2.2
Template Function
112
T temp = p; p = q; q = temp;
The program swapT.cpp shows use of this swap. Here, we are able to swap ints, floats, and
string objects. In the case of user defined classes, the only requirement is that the assignment
operator = is defined.
//--- swapT.cpp ---------------------------// j.g.c. 14/1/99, 2007-02-11
//-------------------------------------------#include iostream
#include string
using namespace std;
// there is a std::swap, so we call this one swap1
template class T void swap1(T& p, T& q)
T temp = p;
p = q;
q = temp;
int main()
113
return 0;
11.3
Polymorphism Parametric
Templates are another form of polymorphism; using templates one can define, for example a
polymorphic List a List of int, or float, or string, etc... Likewise we can define a polymorphic
swap. Unlike the polymorphism encountered in chapter 11 (Person and Cell class hierarchies),
the type of each instance of a generic unit is specified at compile time statically.
Hence, the polymorphism offered by templates is called parametric polymorphism.
11.4
Template Array
11.4.1
The declaration for the template Array class is shown in file arrayt.h.
The first thing to note about templates, at least using the GNU C++ compiler, is that both
declaration and implementation (normally .cpp file) must be #included. Hence we include them
in the same file, here arrayt.h.
//----- ArrayT.h ---------------------------------// j.g.c. 9/1/99, 2007-02-11
// template array
//---------------------------------------------------#ifndef ARRAYTH
#define ARRAYTH
#include iostream
#include cassert
using std::ostream; using std::endl;
template class T class Array
public:
Array(const unsigned int len= 0);
Array(const unsigned int len, const T& val);
Array(const ArrayT& source);
Array();
Array& operator=(const ArrayT& source);
void copy(const ArrayT& source);
T& operator[](unsigned int i) const;
unsigned int length() const;
114
private:
unsigned int len;
T* dat;
;
template class T ostream& operator(ostream& os, const ArrayT& a);
template class T ArrayT::Array(const unsigned int len) : len(len)
if(len== 0)dat = 0; return;
dat = new T[len];
assert(dat!= 0);
T zero(0);
for(unsigned int i=0; i len; i++)dat[i]= zero;
return *this;
return dat[i];
os endl;
return os;
#endif
Dissection of ArrayT.h
1. The class-type parameter T. As already mentioned, Array is now a a parameterised class
the type/class of its element is given as a class parameter, T. Just as in declaration
of a function with parameters (variables formal parameters), we must announce this fact,
and give the (as yet unknown) parameter an identifier. This is done as follows:
template class T class Array
Here we are saying: Array uses an as yet unspecified type/class T.
2. Instead of class T, we may use typename T;
3. When we want to define an actual Array in a program, we do so as, e.g., int:
Array int c1(5);
Array int c2(5, 127);
or double:
Array double c3(3, 3.14159);
or even an Array of Arrayint (a matrix):
Array Arrayint c4(3);
Thus, as with (parameterised) functions, we can invoke the abstraction with any number of
(different) parameters.
116
4. Likewise, if we ever need to declare a Array object as a parameter, we use the form:
Array(const ArrayT & other);
i.e. other is a reference to a Array object of classT elements.
5. Instantiation. Just as variables and objects are instantiated (created), so are template classes.
In the case of template classes and functions, this is done at compile time.
6. In GNU C++, the Arrayint class, for example, is instantiated, at compile time, by a form
of macro-expansion. This is the reason that function implementations must also be in the
.h file.
11.4.2
117
Chapter 12
Array Containers
12.1
Introduction
We call this chapter array containers for want of a better term. We describe sequence containers
which remedy some of the shortcomings of the basic C++ array (e.g. int a[50];).
The two most common sequence containers that we encounter in C++ are std::vector and
std::list; we use the general term sequence to signify that the elements are held in strict
sequential (linear) order.
The main difference between array-like sequence containers (e.g. std::vector and Array that
we develop here) and linked (list-like) sequence containers are the two related characteristics:
(i) array-like have random access, e.g. std::cout a[i];, whereas lists must be sequentially
accessed; and (ii) array-like use contiguous storage, whereas lists used linked storage (e.g. singly
and doubly linked lists).
Roughly speaking, you can do anything with an array-like sequence that you can with a linked
sequence and vice-versa, but the allocation of storage and random access issues mean that there
are major performance drawbacks if you choose the wrong type for your application i.e. the
wrong type may work, but work comparatively very slowly.
Java programmers are aware of the distinction, they have ArrayList and LinkedList.
In this chapter we will comment on the inadequacy of the C++ basic array; we will develop an Array
class that performs rather like std::vector; in doing that we will develop some understanding
of what goes on inside std::vector, so that when you use std::vector you will have some
sympathy for performance issues. Then we will examine use of std::vector itself.
In developing Array, we will identify some inadequacies of contiguous storage that lead to the
need for linked storage. We will cover (linked) lists in Chapter 13.
The Array class here is more or less identical to the vector class in (Budd 1997a).
12.2
An Array class
In the lecture, we will discuss the inadequacies of plain-vanilla arrays (int a[25];) for the task
of sequence container. For example, just look in Array.h at what needs to be done when we
121
insert (add) an element into the middle of a list, i.e. moving everything else up one. To ask a
programmer who is concentrating of doing sequence operations to remember this every time is to
ask for increased errors and poor productivity.
Figure 12.1 shows the interface of Array. Figures 12.2 to 12.5 give the implementation.
122
123
template class T
ArrayT::Array(uint sz) : sz(sz), cap(sz)
if(cap== 0)dat = 0; return;
dat = new T[cap];
assert(dat!= 0);
T zero = T();
for(uint i = 0; i sz; ++i)dat[i]= zero;
template class T
void ArrayT::reserve(uint cap)
//std::cout *reserve* cap = cap cap = cap std::endl;
if(cap = cap)return;
T* newdat = new T[cap];
assert(newdat != 0);
for(uint i = 0; i sz; ++i)newdat[i] = dat[i];
delete [] dat;
dat = newdat;
cap = cap;
template class T
void ArrayT::resize(uint sz)
assert(sz= cap);
if(sz = sz)return;
else if(sz = cap)
T zero = T();
for(uint i = sz; i sz; ++i)dat[i] = zero;
sz = sz;
else
T* newdat = new T[sz];
assert(newdat != 0);
for(uint i = 0; i sz; ++i)newdat[i] = dat[i];
T zero = T();
for(uint i = sz; i sz; ++i)newdat[i] = zero;
delete [] dat;
dat = newdat;
cap = sz = sz;
124
template class T
ArrayT::Array(uint sz, const T& val)
: sz(sz), cap(sz)
if(cap == 0)dat = 0; return;
dat = new T[cap];
assert(dat!= 0);
for(uint i=0; i sz; ++i)dat[i]= val;
template class T
ArrayT::Array(const ArrayT& source)
copy(source);
template class T
ArrayT::Array()
delete [] dat;
dat = 0;
template class T
ArrayT& ArrayT::operator=(const Array& source)
//std::cout copy ctor std::endl;
if(this!= &source) // beware a= a;
delete [] dat;
copy(source);
return *this;
template class T
void ArrayT::copy(const ArrayT& source)
sz= cap = source.size();
if(cap== 0)dat = 0; return;
dat = new T[cap];
assert(dat!= 0);
for(uint i= 0; i sz; ++i)dat[i] = source.dat[i];
template class T
void ArrayT::insert1(uint pos, uint n, const T& val)
uint sz = sz + n;
assert(cap= sz);
uint id= sz - 1; // dest
uint is= sz - 1; // source
for(; is pos-1; --is, --id)dat[id] = dat[is];
for(id = pos; id pos+n; id++)dat[id] = val;
sz = sz;
template class T
uint ArrayT::erase(uint pos)
for(uint i = pos + 1; i sz; ++i)dat[i - 1] = dat[i];
--sz;
return pos;
template class T
void ArrayT::insert(Iterator pos, uint n, const T& e)
uint sz = sz + n;
assert(cap= sz);
Iterator itrd= dat + sz - 1; // dest
Iterator itrs= dat + sz - 1; // source
for(; itrs!= pos-1; --itrs, --itrd)*itrd = *itrs;
for(itrd = pos; itrd!= pos + n; ++itrd)*itrd = e;
sz = sz;
template class T
void ArrayT::pushback(const T& val)
//std::cout *pushback* cap = cap sz = sz std::endl;
if(!(cap sz))reserve(2*cap);
dat[sz] = val;
++sz;
template class T
void ArrayT::popback()
assert(sz 0);
--sz;
template class T
T ArrayT::back() const
assert(sz 0);
return dat[sz - 1];
template class T
T& ArrayT::operator[](uint i) const
assert(i sz);
return dat[i];
template class T
uint ArrayT::size() const
return sz;
template class T
bool ArrayT::empty() const
return sz == 0;
template class T
uint ArrayT::capacity() const
return cap;
os endl;
return os;
#endif
Figure 12.5: Array implementation, part 4
127
12.2.1
1. Note the difference between size(), sz and capacity(), cap; size(), sz is the
used size of the sequence, while capacity(), cap is what it can grow to before we need
to allocate more memory.
2. When using Array (and std::vector) it is always a good idea to use reserve to allocate
either the size that you know you will need, or a decent chunk at the beginning.
3. Notice the difference between reserve and resize.
4. Notice that when pushback detects that we are at full capacity, it reserves double the
current capacity; this might work well or badly, depending on the application. Im not sure
how std::vector handles this.
[Here we repeat some messages from Chapter 9.]
5. Destructors. A destructor is called, automatically, when control reaches the end of the block
in which the object was declared, i.e. when the object goes out of scope. The compiler
will always provide adequate destruction of stack-based objects, but, for heap-based objects,
proper destructor memory management must be provided if garbage and memory-leaks (or
worse, dangling pointers) are to be avoided.
6. Copy constructor. A copy constructor is called when the object is passed (by value) to and
from functions. Again, for classes which use stack memory, the compiler will always provide
an adequate copy constructor. For heap-based objects the case is quite analogous to that of
the destructor: proper constructor memory management must be provided.
7. Assignment. Assignment operator (the = operator) needs treatment similar to the copy
constructor.
8. The Big-Three. The C++ FAQs (Cline et al. 1999a) uses the term the Big-Three for these
three functions: copy constructor, assignment operator, and destructor. More on this in
section 12.4.
This is because, for classes that use heap storage, it is almost certainly necessary to explicitly
program all three. If they are not programmed, the compiler will provide default versions
which will probably not do what you would wish; moreover the inadequacies of these defaults
may be most subtle, and may require quite determined and skilled testing to detect.
9. We use assert to report errors if allocations were unsuccessful; new returns a null pointer if
it is unsuccessful, e.g. due to resources of free memory having become exhausted. This may
not be ideal, but the error message that assert issues is a lot more helpful than what will
happen if we charge on and ignore the fact that we have run out of memory.
10. Function copy.
void ArrayT::copy(const ArrayT& source)
sz= cap = source.size();
if(cap== 0)dat = 0; return;
dat = new T[cap];
assert(dat!= 0);
for(uint i= 0; i sz; ++i)dat[i] = source.dat[i];
128
(a) This is very similar to the default constructor except that this time we have passed
another object to be copied.
(b) Notice that we have passed a reference (Array& source). If we dont well end up
constructing many multiple copies, each of which must also be destroyed. And when
the object becomes large, as may be the case for an Array the performance drain can
be considerable.
(c) Of course, we guarantee the safety of the referenced object (in the caller) by making
the reference const.
11. Copy constructor.
Array::Array(const Array& source)
copy(source);
Here, all the work is done by copy. Again notice the use of reference and const.
12. Destructor.
Array::Array()
delete [] dat;
return *this;
(a) Let us say we have two Array objects, x, y and x = y. Then this assignment is exactly
equivalent to
x.operator=(y);
(b) Again notice the use of a reference and const.
(c) Since we can envisage x = x, however improbable, we have to be careful to check
whether this is the case and if it is, do nothing.
(d) this is an implicit pointer variable which points at the object itself.
Thus
if(this!= &source) checks if the calling object and the source object share the same
memory location and so are the same object!
(e) return *this; returns the object (actually a reference, see next comment).
129
12.2.2
Iterators
Because of the fact that the internal representation is a C++ built-in array, we can do everything we
want to using array subscripting, overloading the array subscript operator [] e.g. a[index] = 10;
and integer subscripts.
But the STL collections use the more general concept of an iterator. We will see that iterator s
are made to behave like pointers, i.e. you can dereference an iterator, and you can do pointer-like
arithmetic on them.
1. Normally an iterator is implemented as a class, but in the case of Array it is easy to declare
an iterator as
typedef T* Iterator;
2. Because Iterator is declared within the scope of Array, when we define an iterator we use
the scope resolution operator
::
Arrayint::Iterator itr;
3. We need an iterator that points to the beginning of the array
Iterator begin() return dat;
It simply returns a pointer to the first element of data array.
4. We need an iterator that points one element past the end of the array;
Iterator end() return dat + sz;
5. One element past the end of the array is the convention; you never dereference this iterator
value. The usual pattern for iterating over a collection is as follows:
1210
Arrayint::Iterator itr;
for(itr = c5.begin(); itr!= c5.end(); ++itr)
cout *itr ;
itr!= c5.end() is our way of checking that we are not at the end really one element
past the end.
This is very like the normal array pattern:
int a[n];
for(int i = 0; i n; ++i)
cout a[i] ;
or even:
int a[n];
for(int* pos = &a[0]; pos!= &a[0]+ n; ++pos)
cout *pos ;
or:
int a[n];
for(int* pos = a; pos!= a + n; ++pos)
cout *pos ;
1211
template class T
void ArrayT::insert(Iterator pos, uint n, const T& e)
uint sz = sz + n;
assert(cap= sz);
Iterator itrd= dat + sz - 1; // dest
Iterator itrs= dat + sz - 1; // source
for(; itrs!= pos-1; --itrs, --itrd)*itrd = *itrs;
for(itrd = pos; itrd!= pos + n; ++itrd)*itrd = e;
sz = sz;
Well have a good deal more to say about iterators when we get to Chapter 13.
12.3
Figures 12.6 and 12.7 give a program, ArrayT1.cpp which uses Array.
1212
cout endl;
c1.reserve(20);
cout c1.reserve(20): c1;
c1.resize(10);
cout c1.resize(10): c1;
c1.pushback(22);
cout c1.pushback(22): c1;
c1.insert1(2, 5, 33);
cout c1.insert1(2, 5, 33): c1;
c1.pushback(44);
cout c1.pushback(44): c1;
c1.popback();
cout c1.popback(): c1;
uint j = c1.erase(4);
cout j = c1.erase(4): j = j c1 = c1;
for(uint i = 0; i 20; ++i)
c1.pushback(i);
cout c1.pushback(i = i ): c1;
cout endl;
// continued ....
1213
Figure 12.6: ArrayT1.cpp part 1
Arrayint c5;
c5.reserve(10);
for(uint i = 0; i 5; ++i)
c5.pushback(i);
cout c5.pushback(i = i ): c5;
cout endl;
cout Arrayint::Iterator itr1 = c5.begin() + 2 endl;
cout c5.insert(itr1, 5, 33) endl;
c5.insert1(2, 5, 33);
cout c5;
cout endl;
return 0;
1214
c4[0]
, [5,
, [0,
3
3, 4
+ 2
3, 4
1215
12.4
The Big-Three
The C++ FAQs (Cline et al. 1999a) introduced the term the Big-Three for the three functions:
copy constructor, assignment operator, and destructor.
This is because, for classes that use heap storage, it is almost certainly necessary to explicitly
program all three. If they are not programmed, the compiler will provide default versions which
will probably not meet the requirements of client programs. Nevertheless, the inadequacy of these
defaults may be most subtle, and may require very detailed testing to detect.
The lack of a proper destructor would be particularly difficult to detect it simply causes garbage,
whose effect is to leak memory, which may not be detected until the class is used in some application
which runs for a long time, e.g. an operating system, or an embedded control program.
Other than the Big-Three care must be exercised with comparison, e.g. equality check ==. If left
to its own devices the compiler will provide a shallow compare, which compares just the explicit
member(s).
12.4.1
If the developer of a heap-based class is quite sure that assignment, =, will never be required, it
still may be quite dangerous to trust that client programmers will never be tempted to use it; and,
normally, as we have said above, the compiler will provide a naive default but silently, with no
warning of its possible inadequacy.
Fortunately, there is a simple and sure defence; this is to declare a stub of the function, and to
make it private, i.e.
private:
Array& operator = (const Array & rhs);
This means that any client program which unwittingly invokes an assignment will be stopped by a
compiler error; client programs are not allowed to access private members.
This would work also for compare, ==.
Nevertheless, it is hard to envisage a class which can operate successfully without a proper copy
constructor; likewise destructor. These will have to be programmed.
1216
12.5
os endl;
return os;
12.6
std::vector
As stated previously, the chief reason for developing Array is as an educational experience.
std::vector will do everything that Array does only faster and with much less chance of a
bug lurking somewhere in its implementation.
If you read games programming books of more than five years ago, you may find ambivalence to
the standard library (STL) for a start, not all compilers included it, and in addition there was
suspicion about its efficiency.
Thats in the past. I cannot imagine any reason why anyone would ever roll their own array class
except for educational or some very special reasons.
Figures 12.8 and 12.9 show a program which uses std::vector in almost the same way as
Figure 12.6 uses Array.
1217
cout endl;
cout endl;
cout endl;
cout endl;
cout endl;
cout endl;
c1.insert(c1.begin() + 2, 5, 33);
cout c1.insert(2, 5, 33): ;
copy(c1.begin(), c1.end(), ostreamiteratorint(cout, ));
cout endl;
cout endl;
cout endl;
cout endl;
cout endl;
vectorint::iterator it;
for(it = c1.begin(); it!= c1.end(); ++it)
cout *it ;
1219
12.6.1
1. #include vector.
2. Unless you want to fully qualify the type as std::vector,
using namespace std.
3. You can randomly access elements of vector using [] notation, for example cout c1[2],
see Figure 12.9.
4. You can access elements of vector using and iterator, for example
vectorint::iterator it;
for(it = c1.begin(); it!= c1.end(); ++it)
cout *it ;
5. We declare the iterator as vectorint::iterator it; because that iterator is declared within the scope of vector; recall ArrayT::Iterator above; :: is the scope
resolution operator.
6. When we come to using std::list, we will have to use an iterator because std::list
does not support random access.
7. An iterator is meant to behave rather like a pointer: (a) ++it and --it move the iterator
forwards and backwards; (b) *it access the element that it refers to.
8. c1.begin() is an iterator that references the first element of c1.
9. c1.end() is an iterator that references one element past the last element of c1. Hence
it!= c1.end().
10. Although we show explicit use of iterator and [] indexed random access, the standard
library gives us alternatives, for example
copy(c1.begin(), c1.end(), ostreamiteratorint(cout, ));
11. The standard library algorithms have been written in such a manner that they can be applied
to pointers as well as iterators.
12. Notice how to sort a vector:
sort(c1.begin(), c1.end());
1220
Chapter 13
Linked Lists
13.1
Introduction
The two most common form of sequence data structures are (a) contiguous array-based as covered
in Chapter 12 and (b) linked-list-based that we cover in this Chapter. In what follows, when
comparing them, well use the term array for the former and linked-list for the latter. Apart from
sequence data structures, we will encounter other forms of collection, e.g. trees and graphs; some
of these use linked representations, some use arrays, and there are other implementations.
std::vector (and likewise Array) that we covered in Chapter 12 do a fine job for many applications. However, they run into performance difficulties in two major circumstances: (i) you need
to insert one or more elements into the array at other than the back; in fact std::vector has no
pushfront function because it is reckoned that to use pushfront on on array would be rather
silly; (ii) you get it wrong when you declare the initial size of the array (or when you use reserve).
These difficulties can be seen by examining Figure 13.1.
First, pushback works fine until we run out of space, but then we must reserve more memory
(an new array), and, (not shown) copy all data from the old array to the new array; that makes
pushback an O(N) operation in the worst case, and if, unlike the example, we reserve only what
extra space that is immediately needed, then N pushbacks will grow as O(N 2 ).
Next, if we look at insert, we see that the first thing that has to be done is to copy all data that
are above the insert position, n memory positions up the array to make space for the n inserts. This
is an O(N) operation, so that if we want to insert all N array elements in this manner, constructing
the array will be O(N 2 ).
A linked-list operates by treating every element in a collection as a semi-independent entity but
linked to other elements in the collection so that the sequence can be maintained. Using this
representation, most insert and delete operations take constant time, i.e. O(1). The one downside
is that accessing the i -th item in the list (sequence) takes O(N) sequential access, whilst an
array allows random access and the same can be done in O(1).
As we have noted earlier, the purpose of developing our own list classes, just as for the array class
in Chapter 12, is mostly to develop some sympathy with how and why they work. When it comes
to application development, we will almost always use STL collections and algorithms.
131
template class T
void ArrayT::insert(Iterator pos, uint n, const T& e)
uint sz = sz + n;
assert(cap= sz);
Iterator itrd= dat + sz - 1; // dstination
Iterator itrs= dat + sz - 1; // source
for(; itrs!= pos; --itrs, --itrd)*(dat + itrd) = *(dat + itrs);
for(itrd = pos; itrd!= pos + n; ++itrd)*(dat + itrd) = e;
sz = sz;
template class T
void ArrayT::pushback(const T& val)
if(!(cap sz))reserve(2*cap);
dat[sz] = val;
++sz;
Figure 13.1: Extracts from Array.h showing drawback of Array and std::vector
132
13.2
Here we develop the simplest form of linked list a singly linked list. This works fine for
most list applications, but suffers from the disadvantage that inserting and deleting at the back
(pushback, popback) take O(N). Later, we will see how a doubly linked list can remedy this
particular drawback.
13.2.1
Class Declaration
The declaration of the singly linked SList class is shown in Figure 13.2.
133
#ifndef LISTTH
#define LISTTH
#include cassert
#include iostream
template class T
class List;
template class T
std::ostream& operator(std::ostream& os, const ListT& l);
template class T class Link;
template class T class List
friend std::ostream& operator T(std::ostream& os, const ListT& l);
public:
List();
List(const ListT & other);
List();
List & operator = (const ListT & rhs);
T front() const;
void pushfront(T e);
void popfront();
T back() const;
void pushback(T e);
void popback();
void clear(); // empty the list and delete all elements in it
bool empty() const;
int size() const;
private:
void copy(const ListT & other); // local utility only, users use =
LinkT* first;
;
template class Tclass Link
friend class ListT;
friend std::ostream& operator T(std::ostream& os, const ListT& l);
private:
Link(T e, Link *next=0);
T elem;
LinkT* next;
;
Figure 13.2: Declaration of Singly Linked List.
134
13.2.2
Dissection of List
136
13.2.3
Class Implementation
The implementations of selected List class functions are shown in Figures 13.4 to 13.6. Most of
these are straightforward, but it will be useful to ensure that we discuss them properly in lectures;
diagrams will do a lot to aid your understanding. You should also read appropriate sections pf
(Penton 2003) and execute his programs which have interactive graphics representations of singly
linked and doubly linked lists.
137
template class T
LinkT::Link(T e, LinkT *next)
elem = e;
next = next;
template class T
ListT::List()
first = 0;
template class T
void ListT::copy(const ListT & other)
if(other.empty())first = 0;
else
LinkT *pp = other.first; //cursor to other
LinkT *pt = new LinkT(pp-elem, 0);
first = pt;
while(pp-next != 0)
pp = pp-next;
pt-next = new LinkT(pp-elem,0);
pt = pt-next;
template class T
ListT::List(const ListT& other)
copy(other);
template class T
ListT & ListT::operator = (const ListT& rhs)
if(this != &rhs) //beware of listA=listA;
clear();
copy(rhs);
return *this;
template class T
ListT::List() clear();
template class T
void ListT::pushfront(T e)
LinkT *pt= new LinkT(e, first);
assert(pt != 0);
first=pt;
// continued ...
Figure 13.4: Implementation code for Singly Linked List, part 1.
138
template class T
T ListT::front() const
assert(!empty());
return first-elem;
template class T
void ListT::popfront()
LinkT* pt= first;
first= pt-next;
delete pt; pt = 0;
template class T
void ListT::pushback(T e)
//std::cout pushback std::endl;
if(empty())
first = new LinkT(e, 0);
assert(first != 0);
else
LinkT* pp = first;
// walk to the back
while(pp-next != 0)pp = pp-next;
// and add a new Link with e in it and next = null
pp-next = new LinkT(e, 0);
assert(pp-next != 0);
template class T
void ListT::popback()
//std::cout popback std::endl;
assert(!empty());
if(first-next == 0)
/*kludge for one element */
delete first;
first = 0;
return;
template class T
T ListT::back() const
//std::cout back std::endl;
assert(!empty());
LinkT* pp (first);
// walk to the back
while(pp-next != 0)pp = pp-next;
return pp-elem;
template class T
void ListT::clear()
LinkT *next,*pp(first);
while(pp != 0)
next = pp-next;
pp-next = 0; // why did Budd include this?
delete pp;
pp = next;
first = 0;
template class T
bool ListT::empty() const
return (first == 0);
template class T
int ListT::size() const
int i = 0;
LinkT *pt = first;
while(pt != 0)
pt = pt-next;
++i;
return i;
template class T
std::ostream& operator (std::ostream& os, const ListT& lst)
osf[ ;
LinkT *pp = lst.first; //cursor to lst
while(pp != 0)
if(pp != lst.first)os, ;
os pp-elem;
pp = pp-next;
os ]bstd::endl;
return os;
1310
Figure 13.6: Implementation code for Singly Linked List, part 3.
13.3
1311
x.pushfront(3.3);
x.pushfront(2.2);
ListD y(x);
ListD z = x; //NB. equiv. to ListD z(x); see prev. line
cout x.front = x.front() endl;
//note that the following destroys the List
cout List x =endl;
cout x.size() = x.size() endl;
while(!x.empty())
cout x.front() endl;
x.popfront();
1312
x.pushfront(1.1);
ListD v; v = y;
v.popfront();
cout List v (v = y; v.popfront();) =endl;
cout v endl;
ListI li; li.pushfront(3); li.pushfront(2); li.pushfront(1);
cout List li via operator endl;
cout li endl;
li.pushback(22);
li.pushback(33);
ListS ls;
ls.pushfront(abcd);
ls.pushfront(cdefgh);
ls.pushback(back);
cout ls endl;
return 0;
1313
x.front = 1.1
List x =
x.size() =4
1.1
2.2
3.3
4.4
x.size() now = 0
List y =
f[ 1.1, 2.2, 3.3, 4.4 ]b
List z =
f[ 1.1, 2.2, 3.3, 4.4 ]b
List v (v = y; v.popfront();) =
f[ 2.2, 3.3, 4.4 ]b
List li via operator
f[ 1, 2, 3 ]b
li.pushback(22), li.pushback(33)
f[ 1, 2, 3, 22, 33 ]b
back(), pop.back()
33
22
3
2
1
f[ cdefgh, abcd, back ]b
Figure 13.9: Output of ListT1.cpp.
1314
13.4
In this section, we develop a doubly linked list. The declaration is shown in Figures 13.10, 13.11
(Link) and 13.12 (ListIterator).
1315
#ifndef LISTTH
#define LISTTH
#include cassert
#include iostream
template class T
class List;
template class T
std::ostream& operator (std::ostream& os, const ListT& l);
template class T
class Link;
template class T
class ListIterator;
template class T class List
friend std::ostream& operator T(std::ostream& os, const ListT& l);
public:
typedef ListIteratorT Iterator;
List() : first(0), last(0) ;
List(const ListT & other);
List();
List & operator = (const ListT & rhs);
T& front() const;
void pushfront(const T& e);
void popfront();
T& back() const;
void pushback(const T& e);
void popback();
void clear(); // empty the list and delete all elements in it
bool empty() const;
int size() const;
Iterator begin();
Iterator end();
Iterator insert(Iterator& itr, const T& val);
void insert(Iterator& itr, int n, const T& val);
void erase(Iterator& itr);
void erase(Iterator& start, Iterator& stop);
private:
void copy(const ListT & other); // private utility only, users use =
LinkT* first;
LinkT* last;
;
Figure 13.10: Doubly linked list declaration, part 1
1316
1317
1318
13.4.1
The doubly linked list in Figures 13.10 to 13.12 is very similar to the singly linked list described
earlier in the chapter. The chief differences are:
1. There are now two List data members (the singly linked list had just a pointer to the front;
now we have pointers to front and back:
LinkT* first;
LinkT* last;
This means that when we want to pushback or popback, we can go directly there, via
last, rather than having to sequentially walk there as in the singly linked list example.
2. Link now has three data members, first the element; then, as before, one pointing to the
next link, and now a new pointer pointing to the previous link.
T elem;
LinkT* next;
LinkT* prev;
In the singly linked list, we could traverse the list (iterate) only in the front towards back
direction via next; now we can traverse in both directions, back towards front, using the
prev pointer.
3. The other major difference is that we have equipped the List with an iterator. This iterator
has the same effect as the Array iterator in Chapter 12, i.e. it looks the same to client
programs, but it is slightly more complicated to implement.
4. ListIterator has two data members:
ListT* list;
LinkT* cLink;
a pointer to the List itself, and a pointer one of the Links.
1319
13.4.2
Figures 13.13 and 13.14 show a simple test program. The only difference between this program
and the one above in Figures 13.7 and 13.8 is that we have added code to exercise the additional
functions in the doubly linked list, and the iterator.
As pointed out before, we note that this doubly linked list, the singly linked list above, and std:list
appear identical in client programs; the only differences (and there should be none) is that in the
lists here, we have chosen to implement only a subset of the functions of std::list and in the
singly linked list, the subset is even smaller. As we keep saying, the objectives of the list classes
here is not to replace std::list, but to get some feeling how std::list might be implemented.
The output of ListT1.cpp is shown in Figure 13.15
1320
x.pushfront(3.3);
x.pushfront(2.2);
x.pushfront(1.1);
ListD y(x);
ListD z = x; //NB. equiv. to ListD z(x); see prev. line
cout x.front = x.front() endl;
//note that the following destroys the list
cout List x =endl;
cout x.size() = x.size() endl;
while(!x.empty())
cout x.front() endl;
x.popfront();
1321
ListS ls;
ls.pushfront(abcd);
ls.pushfront(cdefgh);
ls.pushback(back);
cout ls endl;
ListI c5;
for(uint i = 0; i 5; ++i)
c5.pushback(i);
cout c5.pushback(i = i ): c5;
x.front = 1.1
List x =
x.size() =4
1.1
2.2
3.3
4.4
x.size() now = 0
List y =
f[ 1.1, 2.2, 3.3, 4.4 ]b
List z =
f[ 1.1, 2.2, 3.3, 4.4 ]b
List v (v = y; v.popfront();) =
f[ 2.2, 3.3, 4.4 ]b
List li via operator
f[ 1, 2, 3 ]b
li.pushback(22), li.pushback(33)
f[ 1, 2, 3, 22, 33 ]b
back(), pop.back()
33
22
3
2
1
f[ cdefgh, abcd, back ]b
c5.pushback(i = 0): f[ 0 ]b
c5.pushback(i = 1): f[ 0, 1 ]b
c5.pushback(i = 2): f[ 0, 1, 2 ]b
c5.pushback(i = 3): f[ 0, 1, 2, 3 ]b
c5.pushback(i = 4): f[ 0, 1, 2, 3, 4 ]b
using Iterator
itr == itrb
itr == itrb
0 1 2 3 4 ListI::Iterator itr2 = c5.begin(), ++, ++
c5.insert(itr2, 5, 33)
f[ 0, 1, 33, 33, 33, 33, 33, 2, 3, 4 ]b
Figure 13.15: Output of ListT1.cpp (double linked version)
1323
13.4.3
Here we give the complete implementation of the doubly linked list. Since the principles involved
are the same as for the singly linked list, we provide no discussion. However, it will be worthwhile
to spend some time in class on the implementation, especially that of ListIterator.
// --------- ListIterator --------------------------------// postfix form of increment
template class T
ListIteratorT ListIteratorT::operator ++ ()
// clone, then increment, return clone
ListIteratorT clone (list, cLink);
cLink = cLink-next;
return clone;
1324
template class T
void ListT::copy(const ListT & other)
if(other.empty())first = 0;
else
LinkT *pp = other.first; //cursor to other
LinkT *pt = new LinkT(pp-elem);
first = pt;
while(pp-next != 0)
pp = pp-next;
pt-next = new LinkT(pp-elem);
pt = pt-next;
template class T
ListT::List(const ListT & other)
copy(other);
template class T
ListT & ListT::operator = (const ListT & rhs)
if(this != &rhs) //beware of listA=listA;
clear();
copy(rhs);
return *this;
template class T
ListT::List()
clear();
1325
template class T
void ListT::copy(const ListT & other)
if(other.empty())first = 0;
else
LinkT *pp = other.first; //cursor to other
LinkT *pt = new LinkT(pp-elem);
first = pt;
while(pp-next != 0)
pp = pp-next;
pt-next = new LinkT(pp-elem);
pt = pt-next;
template class T
ListT::List(const ListT & other)
copy(other);
template class T
ListT & ListT::operator = (const ListT & rhs)
if(this != &rhs) //beware of listA=listA;
clear();
copy(rhs);
return *this;
template class T
ListT::List()
clear();
1326
template class T
void ListT::pushfront(const T& e)
LinkT *newLink= new LinkT(e);
assert(newLink != 0);
if(empty())first = last = newLink;
else
first-prev = newLink;
newLink-next = first;
first= newLink;
template class T
T& ListT::front() const
assert(!empty());
return first-elem;
template class T
void ListT::popfront()
assert(!empty());
LinkT* tmp = first;
first= first-next;
if(first!= 0) first-prev = 0;
else last = 0;
delete tmp;
1327
template class T
void ListT::pushback(const T& e)
LinkT *newLink= new LinkT(e); assert(newLink != 0);
if(empty()) first = last = newLink;
else
last-next = newLink;
newLink-prev = last;
last = newLink;
template class T
void ListT::popback()
assert(!empty());
LinkT* tmp = last;
last= last-prev;
if(last!= 0) last-next = 0;
else first = 0;
delete tmp;
template class T
T& ListT::back() const
assert(!empty());
return last-elem;
template class T
void ListT::clear()
LinkT *next, *first(first);
while(first != 0)
next = first-next;
delete first;
first = 0;
first = next;
template class T
bool ListT::empty() const
return (first == 0);
template class T
int ListT::size() const
int i = 0;
LinkT *pt = first;
while(pt != 0)
pt = pt-next;
++i;
return i;
1328
Figure 13.20: Doubly Linked List Implementation, part 5
template class T
ListIteratorT ListT::begin()
return Iterator(this, first);
template class T
ListIteratorT ListT::end()
return ListIteratorT(this, 0);
template class T
void ListT::erase(ListIteratorT& itr)
erase(itr, itr);
1329
else
prev-next = last;
if (last == 0)last = prev;
else last-prev = prev;
template class T
std::ostream& operator (std::ostream& os, const ListT& lst)
osf[ ;
LinkT *pp = lst.first; //cursor to lst
while(pp != 0)
if(pp != lst.first)os, ;
os pp-elem;
pp = pp-next;
os ]bstd::endl;
return os;
#endif
Figure 13.22: Doubly Linked List Implementation, part 7
1330
13.5
On a 32-bit machine, int and pointer typed variables normally occupy four 8-bit bytes.
Ex. 1. If we have an Arrayint such as that in Chapter 12 (and std::vector will be the same)
whose size and capacity are 1000, how many memory bytes will be used?
Ex. 2. Do the same calculation for a singly linked list which contains 1000 elements.
Ex. 3. Do the same calculation for a doubly linked list.
Ex. 4. If we define efficiency as actual useful data memory divided by total memory used, what
can we say about the efficiency obtained in Ex. 1., Ex. 2. and Ex. 3. above.
Ex. 5. As N, the number of elements, increases to a very large number, what can we say about
the efficiency in the three cases: (i) array, (ii) singly linked list, (iii) doubly linked list.
13.6
All CPU chips these days have cache memory ; cache memory is extra-fast memory close to the
CPU. Cache memory has access times one tenth to one twentieth of main memory.
Usually, there are two separate caches, data cache, and instruction cache.
In the case of data cache, when you access a memory location, 4560, say, the cache system will
bring memory locations 4560 to 4581 (32 bytes it could be more, depends on the CPU) into
cache; the first memory access will be at the speed of main memory; however, if you access memory
4564, it will already be in cache and this memory access will be at the much faster cache speed.
Hence on machines that have cache, it makes sense to access memory in a orderly manner: e.g.
4560, 4564, 4568, . . . .
If you hop about through memory: e.g. 4560, 200057, 26890, . . . , you will lose the speed of the
cache memory.
Ex. In connection with cache memory, what performance penalty might a program incur when
using a linked list instead of an array?
1331
Chapter 14
Case Study Person, Student class
hierarchy
14.1
Introduction
This is a case study involving a number of classes; it is useful to see how a methodical objectoriented design approach can tame a problem that looks very difficult at first glance. A secondary
goal of the case study is to gain confidence in using the standard library.
Unfortunately, the software that is presented here is the result of a few iterations of design; thus,
like software that you see in some books, it is hard to depict the thought process that resulted in
the what you see here.
The mini-project started off with the requirement for a set of classes related to Institute staff and
students; hence we think immediately of Student and Lecturer. It is relatively obvious that these
have a lot in common; the common stuff can be factored into a Person class.
A Person will have set of names and an address. After a bit of thought, it becomes obvious that a
separate Address part (class) is sensible; the latter for two reasons: (i) it cuts down the size and
complexity of the Person class; (ii) an Address class will be useful if we ever need to work with
companies and other entities that need an address.
The remainder of this chapter presents the classes and test programs.
The assignment questions at the end of the chapter helps identify some of the key learning objectives of this chapter.
The commentary here is kept to a minimum this software will be the subject of many lectures
and discussions, plus an assignment.
141
142
city + ,
143
int j = rand()%streets.size();
street = streets.at(j);
j = rand()%100;
ostringstream ossnum;
ossnum j;
num = ossnum.str();
j = rand()%cities.size();
city = cities.at(j);
j = rand()%regions.size();
region = regions.at(j);
j = rand()%countries.size();
country = countries.at(j);
Address a = Address(num, street, city, region, country);
addList.pushback(a);
sort(addList.begin(), addList.end());
coutSorted address list size is: addList.size() endl;
cout Sorted address list is: endl;
for(int i = 0; i!= addList.size(); ++i)
cout addList.at(i).toString() endl;
;
/** The main method constructs the test */
int main()
AddressT1();
145
146
147
regions.pushback(string(Oldshire));
regions.pushback(string(Newshire));
regions.pushback(string(Green County));
regions.pushback(string(Old County));
vectorstring countries = vectorstring();
countries.pushback(string(Ireland));
countries.pushback(string(Wales));
countries.pushback(string(England));
countries.pushback(string(Scotland));
vectorPerson personList = vectorPerson();
string num, street, city, region, country, lastName, firstName;
unsigned int rnseed= 139;
srand(rnseed); // initialise random number generator.
int n = 10, j;
for(int i = 0; i n; ++i)
j = rand()%100;
ostringstream ossnum;
ossnum j;
num = ossnum.str();
cout i = i endl;
j = rand()%streets.size();
street = streets.at(j);
j = rand()%cities.size();
city = cities.at(j);
j = rand()%regions.size();
region = regions.at(j);
j = rand()%countries.size();
country = countries.at(j);
j = rand()%firstNames.size();
firstName = firstNames.at(j);
j = rand()%lastNames.size();
lastName = lastNames.at(j);
Address a = Address(num, street, city, region, country);
cout a.toString() endl;
Person p = Person(firstName, lastName, a);
cout p.toString() endl;
personList.pushback(p);
sort(personList.begin(), personList.end());
coutSorted address list size is: personList.size() endl;
cout Sorted address list is: endl;
for(int i = 0; i!= personList.size(); ++i)
cout personList.at(i).toString() endl;
;
/** The main method constructs the test */
int main()
PersonT1();
1410
1411
1412
class StudentT1
public:
StudentT1()
vectorstring firstNames = vectorstring();
firstNames.pushback(string(Seamus));
// etc. same as PersonT1.cpp
vectorStudent sList = vectorStudent();
string num, street, city, region, country, lastName, firstName;
unsigned int rnseed= 139;
srand(rnseed); // initialise random number generator.
int n = 10, j;
string id1(L200702);
for(int i = 0; i n; ++i)
j = rand()%100;
ostringstream ossnum;
ossnum j;
num = ossnum.str();
cout i = i endl;
j = rand()%streets.size();
street = streets.at(j);
j = rand()%cities.size();
city = cities.at(j);
j = rand()%regions.size();
region = regions.at(j);
j = rand()%countries.size();
country = countries.at(j);
j = rand()%firstNames.size();
1413
firstName = firstNames.at(j);
j = rand()%lastNames.size();
lastName = lastNames.at(j);
Address a = Address(num, street, city, region, country);
cout a.toString() endl;
int k = i + 10;
ostringstream ossid;
ossid id1 k;
string id = ossid.str();
Student st = Student(firstName, lastName, a, id);
sList.pushback(st);
string s =
string(James,Mulligan;Hillview,Lough Salt,Kilmacrennan,Donegal,Ireland;L20079
Student st(s);
sList.pushback(st);
s = string(
James,Mulligan;Hillview,Lough Salt,Kilmacrennan,Donegal,Ireland;L200789);
Student st1(s);
sList.pushback(st1);
st = Student();
sList.pushback(st);
coutThe Student list size is: sList.size() endl;
cout The Student list is: endl;
for(int i = 0; i!= sList.size(); ++i)
cout sList.at(i).toString() endl;
sort(sList.begin(), sList.end());
coutSorted list size is: sList.size() endl;
cout Sorted list is: endl;
for(int i = 0; i!= sList.size(); ++i)
cout sList.at(i).toString() endl;
1414
string s1 = string(
Jim,Dillon;10,Fourth Avenue,Raphoe,Donegal,Ireland;L200791);
Person p(s1);
ppList.pushback(&p);
coutThe Person pointer list size is: sList.size() endl;
cout The Person pointer (contents) list is: endl;
for(int i = 0; i!= ppList.size(); ++i)
cout ppList.at(i)-toString() endl;
fileout.close();
cout n... and read some of them backn endl;
vectorStudent v4;
ifstream filein(students.dat);
if(!filein)
cerr cannot open file students.dat for readingn;
exit(-1);
string str1;
for(unsigned int i=0; i 5; i++)
getline(filein, str1);
v4.pushback(Student(str1));
filein.close();
for(unsigned int i = 0; i v4.size(); i++)
cout v4[i].toString() endl;
;
/** The main method constructs the test */
int main()
StudentT1();
1415
1416
string s =
string(James,Mulligan;19, First Road,Kilmacrenan,Donegal,Ireland;L20
Student st(s);
sList.pushback(Student st(s));
In addition to using the constructor
Student(std::string str =
blankf,blankl;blanknum,blanks,blankc,blankr,blankc;blankid);
you should asl show examples of use of the constructor
Student(std::string& firstName, std::string& lastName,
Address& address, std::string& id);
There should be at least eight (8) students in your list. Demonstration.
[20 marks]
(b) Commented printout of StudentT2.cpp in hand-in report.
[20 marks]
(c) Commented printout of lists before and after sorting in hand-in report.
[20 marks]
6. (a) Copy LecturerT1.cpp to LecturerT2.cpp and use vector rather than list. Compile
and execute. Demonstration.
[20 marks]
(b) Add code to LecturerT2.cpp to sort the vector and print out the sorted list. Demonstration.
[20 marks]
(c) Add code to LecturerT2.cpp to sort the vector according to office number ; print out
the sorted list. See StudentT1 for a closely related example. Demonstration.
[20 marks]
(d) Commented printout of relevant parts of LecturerT2.cpp in the hand-in report.
[20 marks]
(e) Commented printout of the unsorted and sorted tables in hand-in report.
[20 marks]
1417
1418
Bibliography
Alexandrescu, A. (2001). Modern C++ Design, Addison Wesley.
Blanchette, J. & Summerfield, M. (2008). C++ GUI Programming with Qt7, 2nd edn, Prentice
Hall. ISBN: 0-13-235416-0.
Bornat, R. (1987). Programming from First Principles, Prentice-Hall.
Budd, T. (1997a). Data Structures in C++ Using the Standard Template Library, Addison Wesley.
Budd, T. (1997b). An Introduction to Object-oriented Programming, 2nd edn, Addison Wesley.
Budd, T. (1999a). C++ for Java Programmers, Addison Wesley.
Budd, T. (1999b). Understanding Object-oriented Programming with Java, (updated for Java 2),
Addison-Wesley.
Campbell, J. (2009). Algorithms and Data Structures for Games Programmers, Technical report,
Letterkenny Institute of Technology. URL. http://www.jgcampbell.com/adsgp/.
Cline, M., Lomow, G. & Girou, M. (1999a). C++ FAQs 2nd ed., 2nd edn, Addison Wesley.
Cline, M., Lomow, G. & Girou, M. (1999b). C++ FAQs 2nd ed., 2nd edn, Addison Wesley.
Czarnecki, K. & Eisenecker, U. (2000). Generative Programming, Addison Wesley.
Dawson, M. (2004). Beginning Game Programming, Thompson Course Technology. ISBN: 159200-206-6.
Dewhurst, S. C. (2003). C++ Gotchas: Avoiding Common Problems in Coding and Design,
Addison Wesley.
Dewhurst, S. C. (2005). C++ Common Knowledge : Essential Intermediate Programming, Addison Wesley. ISBN: 0-321-32192-8.
Dickheiser, M. J. (2007). C++ for Game Programmers, 2nd edn, Charles River Media. ISBN:
1-58450-452-8. This is the second edition of a first edition by Llopis.
Eckel, B. (2000). Thinking in C++, vol. 1, Prentice Hall.
Eckel, B. (2003). Thinking in C++, vol. 2, Prentice Hall.
Fitzgerald, J. & Larsen, P. (1998). Modelling Systems: Practical Tools and Techniques in Software
Development, Cambridge University Press.
Freeman, E. & Freeman, E. (2004). Head First Design Patterns, OReilly. ISBN: 0-596-00712-4.
141
Gamma, E., Helm, R., Johnson, R. & Vlissides, J. (1995). Design Patterns: Elements of Reusable
Object-oriented Software, Addison-Wesley.
Glassborow, F. (2006). You Can Program in C++, Wiley. ISBN: 0-470-01468-7.
Harbison, S. & Steele, G. (2005). C: a Reference Manual, 5th edn, Prentice-Hall.
Horstmann, C. (2005). Java Concepts, 4th edn, Wiley.
Horstmann, C. (2006). Object Oriented Design and Patterns, 2nd edn, Wiley.
Horstmann, C. (accessed 2006-08-28b).
http://horstmann.com/ccj2/ccjapp3.html.
Moving
Horstmann,
C.
(accessed
5006-08-28a).
http://horstmann.com/cpp/pitfalls.html.
from
C++
java
to
c++.
pitfalls.
online:
online:
Penton, R. (2003). Data Structures for Games Programmers, Premier Press / Thompson Course
Technology. ISBN: 1-931841-94-2.
Reese, G. (2007). C++ Standard Library Practical Tips, Charles River Media. ISBN: 1-58450400-5.
Romanik, P. & Muntz, A. (2003). Applied C++: practical techniques for building better software,
Addison Wesley. ISBN: 0321108949.
Sethi, R. (1996). Programming Languages: Concepts and Constructs, Addison-Wesley.
Smart, J. & Csomor, S. (2005). Cross-platform GUI programming with WxWidgets, Prentice Hall.
ISBN: 0131473816.
Stroustrup, B. (1997a). The C++ Programming Language, 3rd edn, Addison-Wesley.
Stroustrup, B. (1997b). The C++ Programming Language, 3rd ed., 3rd edn, Addison-Wesley.
Stroustrup, B. (2009). Programming: Principles and Practice using C++, Addison-Wesley. ISBN:
0-321-54372-6.
Sutter, H. (1999). Exceptional C++, Addison Wesley.
Sutter, H. (2002). More Exceptional C++: 40 More Engineering Puzzles, Programming Problems,
and Solutions, Addison Wesley.
Sutter, H. (2004). Exceptional C++ style, Addison Wesley. ISBN: 0201760428.
Sutter, H. & Alexandrescu, A. (2005). C++ Coding Standards : 101 Rules, Guidelines, and Best
Practices, Addison Wesley. ISBN: 0-321-11358-6.
Tennent, R. (2002). Specifying Software, Cambridge University Press.
Vlissides, J. (1998). Pattern Hatching: Design Patterns Applied, Addison-Wesley.
Wilson, M. (2004). Imperfect C++, Addison Wesley. ISBN: 0321228774.
Appendix A
Where do procedural programs come from?
A.1
Introduction
At this stage, some of you may think that designing and writing programs is some sort of God-given
talent and that programs spring fully formed from the fingers of a chosen few. This opinion may
be confirmed by the fact that, in books and in notes, you see only finished working programs.
In another chapter we will take a quick look at object-oriented analysis and design. In this chapter
we look at one way of deriving (procedural) programs by appropriate analysis of the requirements
leading to a design.
For certain types of problem, one way is to make your program follow the same structure as your
input or output data. (When the structures of output data differs from that of input data, there is
a little more work to be done, but we wont worry about that here.) An example is that if we were
writing a program to process student marks, we would expect to see a for loop over all students
and inside that a for loop over all assessment items (courseworks and examination results).
Here, we look at a problem which has only output data: patterns on a screen. If we look at patterns
in data, we may be able to identify:
Sequence;
Selection;
Repetition.
Any program can be constructed from these three.
In addition, based on groupings of patterns and repetitions of these groupings, we may identify the
need for:
Procedures, (methods, functions, subprograms);
Procedures with parameters.
A1
During the lectures we will discuss the analogies between certain computer program structures
and cookery recipes. We will show that cookery recipes are frequently composed of patterns
involving: sequence, selection, repetition, subprograms (recipes within a recipe), and subprograms
with parameters (for example a sauce recipe which can be changed by supplying different colours
or different flavours).
See Figure A.1 below.
A2
Figure A.1: Computer programs have much in common with recipes (recipes from B. Nilsson, The
Penguin Cookery Book)
A3
A.2
*
*
*
*
*
*
**
Next, make it general enough to take a parameter h (height); the example has h = 5. Were going
build up to it which is sometimes a great way to solve a large problem by chipping away at it
until it gets smaller and smaller and finally solved.
Note, however that by chipping away I do not mean typing some rubbish and then altering random
parts until it works which is never. If you ever find yourself or anyone else taking this approach,
shout stop! and go for help.
//----- Stars1.cpp --------------------------------// j.g.c. 2003/11/29
// prints single * on screen
//-------------------------------------------------#include iostream
using namespace std;
int main()
cout *;
cout n;
return 0;
A4
A.3
Sequence
cout *;
cout *;
cout *;
cout *;
cout *;
cout n;
return 0;
Here is Line5.cpp formatted differently to show that the program replicates the pattern of the
data.
//----- Line51.cpp --------------------------------// j.g.c. 2003/11/29
// prints 5 * line on screen
// now formatted to show similarity between data pattern
// and program pattern
//-------------------------------------------------#include iostream
using namespace std;
int main()
A5
A.4
Repetition
int n= 5;
for(int i= 0; i n; i++)
cout *;
cout n;
return 0;
A.4.1
Sequence of Repetitions
int n= 1;
for(int i= 0; i n; i++)
cout *;
cout n;
A6
n= 2;
for(int i= 0; i n; i++)
cout *;
cout n;
n= 3;
for(int i= 0; i n; i++)
cout *;
//etc ...
A7
A.4.2
int maxj= 5;
for(int j= 1; j= maxj; j++)
//----- this loop does the stars
for(int i= 0; i j; i++)
cout *;
//-----cout n; // and newline after every line
return 0;
A8
cout *;
cout *;
cout *;
cout *;
cout *;
cout
cout
cout
cout
cout
n;
*; cout n;
*; cout *; cout n;
*; cout *; cout *; cout n;
*; cout *; cout *; cout *; cout n;
return 0;
A9
A.5
A.5.1
void p1()
cout *;
void p2()
cout *; cout *;
void p3()
cout *; cout *; cout *;
void p4()
cout *; cout *; cout *; cout *;
void p5()
cout *; cout *; cout *; cout *; cout *;
int main()
p1(); nl();
p2(); nl();
p3(); nl();
p4(); nl();
p5(); nl();
return 0;
A10
A.5.2
void stars(int n)
for(int i= 0; i n; i++)
cout *;
void spaces(int n)
for(int i= 0; i n; i++)
cout ;
int main()
int height= 5;
for(int j= 1; j= height; j++)
stars(j);
nl();
return 0;
A11
Equipped with these procedures, we are now in business, and can create nearly any pattern in very
quick time. The little bit of investment in designing and implementing procedures will pay back in
a major way. The same is true of spending time designing, implementing, and testing OOP classes
(objects); once you have a good library of classes, you can produce new programs in no time. In
the games context, I suppose the next stage would be game engine once you have a good game
engine, creation of a new game is easy ;-).
//----- Tr4.cpp --------------------------------// j.g.c. 2003/11/29
// now onto something more complex; see Tr3r.cpp
//*****
//****
//***
//**
//*
// Analysis:
//
Stars
// Line 0
5
//
1
4
//
2
3
//
etc ...
// Generalising: let h be height
// Line j no. stars = h -j
//-------------------------------------------------#include iostream using namespace std;
void nl()
cout n;
void stars(int n)
for(int i= 0; i n; i++)
cout *;
void spaces(int n)
for(int i= 0; i n; i++)
cout ;
// notice how little change from Tr3r.java, just //1, //2
int main()
int h= 5;
for(int j= 0; j h; j++) //1
stars(h - j); //2
nl();
return 0;
void stars(int n)
for(int i= 0; i n; i++)
cout *;
void spaces(int n)
for(int i= 0; i n; i++)
cout ;
int main()
int h= 5;
for(int j= 0; j h; j++)
spaces(h - 1 - j);
stars(j + 1);
nl();
return 0;
A13
A14
void stars(int n)
for(int i= 0; i n; i++)
cout *;
void spaces(int n)
for(int i= 0; i n; i++)
cout ;
int main()
int h= 5;
for(int j= 0; j h; j++)
spaces(j);
stars((h-j)*2);
nl();
return 0;
return 0;
A.6
Selection
void spaces(int n)
for(int i= 0; i n; i++)
cout ;
int main()
int h= 6;
for(int j=0; j h; j++)
spaces(j);
if(j%2==0)stars((h-j)*2); // even number if remainder of j/2 == 0
else dashes((h-j)*2);
nl();
return 0;
A17
A.7
Exercises
1. Write a C++ Program to display an arbitrary filled rectangle of *s specified by h(eight) and
w(idth);
2. Write a C++ Program to display a rectangle outline, 5 5.
3.(a) Write a C++ function, printHexDigit(int n) which will print a Hexadecimal digit 0,
1, . . . 9, 10 (A), . . . 15 (F) as follows. Design your own font for 3 onwards.
*****
* **
* * *
** *
*****
*
*
*
*
*
*****
*
*****
*
*****
*****
*
*****
*
*****
(b) If you were writing programs that needed to generate sequences of these digits, can you think
of additional procedures that would be useful, or would you make do with stars, dashes, spaces,
and nl that we already have.
4. Having a nice library of procedures saves programming time (designing the program, typing,
. . . ) for programmers. Can you think of any other benefits? Hint: (i) what do you do after you
have written what you think is a correct program? (ii) will a program that has procedures like
one, two, etc. be more or less understandable by a human than one constructed from just stars,
dashes, spaces, and nl? (iii) will a program that has procedures like one, two, etc. have more
or less source lines than on constructed from stars, dashes, spaces, and nl.
A18
Appendix B
Analysis of Algorithms
This chapter is a slight modification of notes provided by Robert Lyttle of Queens University
Belfast.
In considering the trade-offs among alternative solutions to problems, an important factor is the
efficiency. Efficiency, in this context, is measured in terms of memory use and time taken to
complete the task. Time is measured by the number of elementary actions carried out by the
processor in such an execution. In the interests of brevity, this course discusses only time efficiency.
It should be noted that there is often a trade-off between time an memory; often, you can buy
time performance (speed) by using extra memory.
It is difficult to predict the actual computation time of an algorithm without knowing the intimate
details of the underlying computer, the object code generated by the compiler, and other related
factors. But we can measure the time for a given algorithm, language compiler and computer
system by means of some carefully designed performance tests known as benchmarks.
It is also helpful to know the way the running time will vary or grow as a function of the problem
size a function of the number of elements in an array, the number of records in a file, and so
forth. Programmers sometimes discover that programs that have run in perfectly reasonable time
for the small test sets they have used, take extraordinarily long when run with real world sized data
sets or files. These programmers were deceived by the growth rate of the computation.
For example, it is common to write programs whose running time varies with the square of the
problem size. Thus a program taking, say, 1 second to sort a list of 1000 items will require not
two (2), but four (4) seconds for a list of 2000 items. Increasing the list size by a factor of 10, to
10,000 items, will increase the run-time to 102 = 100 seconds. A list 100,000 items will require
10,000 (104 ) seconds, or about 3 hours, to complete. Finally, 1,000,000 items (e.g. a telephone
directory for a small country) will need 106 seconds (almost two weeks) to finish! This is a long
time compared to the one second taken by the 1000 item test.
This example shows that it makes sense to be able to analyse growth rates and to be able to
predict (even roughly) what will happen when the problem size gets large.
B1
B.1
O Notation (Big-oh)
Algorithmic growth rates are expressed as formulae which give the computation time in terms of
the problem size N. It is usually assumed that system dependent factors, such as the programming
language, compiler efficiency and computer speed, do not vary with the problem size and so can
be factored out.
Discussions of growth rate normally use the Big-oh notation (growth rate, order of magnitude).
The most common growth rates we will encounter are the following:
O(1), or constant;
O(log N), or logarithmic (logarithm usually taken to the base 2);
O(N), or linear (directly proportional to N);
O(N log N), pronounced N log N;
O(N 2 ), or quadratic (proportional to the square of N).
The table that follows shows the value of each of these functions for a number of different values
of N. It shows that as N grows, log N remains quite small with respect to N and N log N grows
fairly rapidly, but not nearly as large as N 2 .
In (Campbell 2009) we will see that simple searching grows as O(N) (linear), but a binary search
grows as O(log N). We also see that most good sorting algorithms have a growth rate of
O(N log N) and that the slower, more obvious, ones are O(N 2 ).
log2(N) N
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
1
2
4
8
16
32
64
128
256
512
1024
2048
4096
8192
16384
32768
65536
N log2 N
0
20.000E-1
80.000E-1
24.000E+0
64.000E+0
16.000E+1
38.400E+1
89.600E+1
20.480E+2
46.080E+2
10.240E+3
22.528E+3
49.152E+3
10.650E+4
22.938E+4
49.152E+4
10.486E+5
N2
1
4
16
64
256
1024
40.960E+2
16.384E+3
65.536E+3
26.214E+4
10.486E+5
41.943E+5
16.777E+6
67.109E+6
26.844E+7
10.737E+8
42.950E+8
2N
N!
20.000E-1
10.000E-1
40.000E-1
20.000E-1
16.000E+0
24.000E+0
25.600E+1
40.320E+3
65.536E+3 20.923E+12
42.950E+8 26.313E+34
18.447E+18 12.689E+88
34.028E+37 38.562E+214
11.579E+76
*
13.408E+153
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
B.2
In estimating performance we can take advantage of the fact that algorithms are developed in a
structured way that is, they combine simple statements into complex blocks in four useful ways:
sequence, or writing one statement below the other;
selection, or the well known if-then or if-then-else;
repetition, including counting loops, while loops, etc.;
method calls.
In the rest of this section, some typical algorithm structures are considered and their O() estimated.
The problem size is denoted by N throughout.
B.2.1
Simple Statements
B.2.2
Decision
When estimating performance, the then clause and the else clause of a conditional structure are
considered to be independent, arbitrary structures in their own right. Then the larger of the two
individual big-Ohs is taken to be the big-Oh of the decision.
A variation of the decision structure is the switch structure, really just a multi-way if-then-else.
Thus in estimating performance of a switch, we just take the largest big-Oh of all of the switch
alternatives.
Performance estimation can sometimes get a bit tricky. For example, the condition tested in a
decision may involve a method call, and the timing of the method call may itself vary with problem
size.
B3
B.2.3
Counting Loop
A counting loop is a loop in which the counter is incremented (or decremented) each time the
loop is iterated. This is different from some loops we shall consider later, where the counter is
multiplied or divided by a given value.
If the body of a simple counting loop contains only a sequence of simple statements, then the
performance of the loop is just the number of times the loop iterates. If the number of times the
loop iterates is constant i.e. independent of the problem size then the performance of the
whole loop is O(1). An example of such a loop is:
for (int i = 0; i 5; i++)
statements with O(1)
the number of times the loop iterates depends on N, so the performance is O(N).
Here, s1, and s2 are performed only once; s3, s4 and s5 are performed N times. Hence, associating
a time tj with instruction j, we have:
ttot = t1 + t2 + N(t3 + t4 + t5 )
Normally, it will be the case that s5 will be the most expensive; however, just to be fair, let us
assume that all instructions take the same time 1 unit, e.g. 1 microsecond. Let us see how ttot
behaves as N get large.
N
Ttot
1
10
100
1000
5
32
302
3002
B4
Hence, we see that it is the term N(t3 + t4 + t5 ) which becomes dominant for large N. In other
words, we can write:
ttot = cN + negligible terms
and that the algorithm has a running time of O(n).
Consider the double counting loop:
for (int i = 0; i N; i++)
for (int j = 0; j N; j++)
statements with O(1)
The outer loop is iterated N times. But the inner loop iterates N times for each time the outer
loop iterates, so the body of the inner loop will be iterated N N times, and the performance of
the entire structure is O(N 2 ).
The next structure:
for (int i = 0; i N; i++)
for (int j = 0; j i; j++)
statements with O(1)
looks deceptively similar to that of the previous loop. Again, the outer loop iterates N times. But
this time the inner loop depends on the value of i (which depends on N): if i = 1, the inner loop
will be iterated once, if i = 2 it will be iterated twice, and so on, so that, in general, if i = N,
the inner loop will iterate N times. How many times, in total, will the body of the inner loop be
iterated? The number of times is given by the sum:
0 + 1 + 2 + 3 + ............... + (N 2) =
PN2
i=1
P
2
(N+1)N
Noting that N
, the summation above is equivalent to (N2)(N1)
= N 3N+2
. The
i=1 i =
2
2
2
2
performance of such a structure is said to be O(N ), since for large N the contributions of the 3N
2
and 22 terms are negligible.
As a general rule: a structure with k nested counting loops loops where the counter is just
incremented (or decremented) by 1 has performance O(N k ) if the number of times each loop
is iterated depends only on the problem size. A growth rate of O(N k ) is called polynomial.
In (Campbell 2009) we will show that simple divide-and-conquer algorithms like binary search grow
as log N.
B5