Você está na página 1de 220

Lecture Notes on Object-oriented Programming using C++

Jonathan G. Campbell Department of Computing, Letterkenny Institute of Technology, Co. Donegal, Ireland. email: jg.campbell@ntlworld.com, jonathan.campbell@lyit.ie URL:http://homepage.ntlworld.com/jg.campbell/lyitoop/ Report No: jc/04/0003/r Revision 2.1 (draft) 19th April 2004

Contents
1 Introduction and Overview 1.1 Scope and Warning(!) . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.2 Overview . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.3 Recommended Reading . . . . . . . . . . . . . . . . . . . . . . . . . . 1.3.1 Object-oriented Programming . . . . . . . . . . . . . . . . . . . 1.3.2 Programming and Object-oriented Programming through Java 1.3.3 Programming and Object-oriented Programming through C++ 1.3.4 General Software Engineering . . . . . . . . . . . . . . . . . . . 1.3.5 Design Patterns . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.3.6 Specication and Correctness . . . . . . . . . . . . . . . . . . . 2 Introduction to Object-oriented Programming 2.1 Java and C++ Mixture of Paradigms . . . . . . . . . . . . . . . . . 2.2 The Question Again, Why OO . . . . . . . . . . . . . . . . . . . . . . 2.3 Software Engineering and its Requirements and Goals . . . . . . . . . 2.3.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.3.2 Software Crisis . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.4 The Waterfall Life-cycle and Structured Systems Analysis and Design 2.4.1 Waterfall Life-cycle . . . . . . . . . . . . . . . . . . . . . . . . . 2.5 Structured Systems Analysis . . . . . . . . . . . . . . . . . . . . . . . 2.6 Structured Systems Design . . . . . . . . . . . . . . . . . . . . . . . . 2.7 Structured Systems Implementation, Programming . . . . . . . . . . . 2.8 Shortcomings of Waterfall and Top-down Decomposition . . . . . . . . 2.9 Object-orientation to the Rescue . . . . . . . . . . . . . . . . . . . . . 2.10 Key Application: Event-driven Interaction . . . . . . . . . . . . . . . . 2.11 Cornerstones of Software Engineering and Object-oriented Systems . . 2.11.1 Modules and Data Abstraction . . . . . . . . . . . . . . . . . . 2.11.2 Abstract data types . . . . . . . . . . . . . . . . . . . . . . . . 2.11.3 Classes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.11.4 The Primacy of Behaviour Interfaces . . . . . . . . . . . . . 2.11.5 Inheritance . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.11.6 Multiple Interfaces . . . . . . . . . . . . . . . . . . . . . . . . . 2.11.7 Late Binding . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.11.8 Open-closed Modules . . . . . . . . . . . . . . . . . . . . . . . . 2.11.9 Design-by-Contract . . . . . . . . . . . . . . . . . . . . . . . . . 2.12 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 Classes and Objects 3.1 Introduction . . . . . . . . . . . . . . . . . 3.2 A Cell Class . . . . . . . . . . . . . . . . . 3.2.1 Informal Specication of the Class 3.2.2 Class Cell . . . . . . . . . . . . . . 3.3 Using Separate Files . . . . . . . . . . . . 3.4 A Coordinate Class . . . . . . . . . . . . . 3.4.1 Informal Specication of the Class 3.4.2 Class Interface, Methods . . . . . . 3.4.3 Class implementation code . . . . 3.4.4 A Client Program . . . . . . . . . 3.5 Exercises . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 . 7 . 7 . 9 . 9 . 9 . 9 . 9 . 10 . 10 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11 12 12 13 13 13 14 14 15 16 16 17 19 20 21 21 21 21 22 22 22 23 23 23 24 25 25 25 25 26 27 29 29 29 31 32 33

4 Variables, Values, Scope, Lifetime 4.1 Types, Variables, Values . . . . . . . . . 4.1.1 Introduction . . . . . . . . . . . 4.1.2 Data Types . . . . . . . . . . . . 4.1.3 Values . . . . . . . . . . . . . . . 4.1.4 Variables . . . . . . . . . . . . . 4.1.5 L-Values and Values . . . . . . . 4.1.6 Aliases . . . . . . . . . . . . . . . 4.1.7 Dangling References . . . . . . . 4.1.8 Uninitialized Pointers . . . . . . 4.1.9 Garbage . . . . . . . . . . . . . . 4.2 Scope of variables . . . . . . . . . . . . . 4.3 Namespaces . . . . . . . . . . . . . . . . 4.4 Heap memory management . . . . . . . 4.4.1 Introduction . . . . . . . . . . . 4.4.2 Garbage . . . . . . . . . . . . . . 4.5 Lifetime of variables . . . . . . . . . . . 4.5.1 Lifetime of variables summary

. . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

36 36 36 36 37 37 38 38 39 39 39 40 41 42 42 42 42 43 44 44 44 45 46 47 50 50 50 51 51 52 52 53 53 54 57 58 58 59 61 63 63 64 65 66 66 67 68 69 71 72 73 73 75 75 75 77

5 Inheritance 5.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . 5.2 Example 1: Class Cell extended via inheritance . . . . . . . 5.2.1 First Attempt . . . . . . . . . . . . . . . . . . . . . . 5.2.2 protected . . . . . . . . . . . . . . . . . . . . . . . 5.2.3 Virtual Functions and Dynamic Binding . . . . . . . 5.3 Dynamic Typing, Run-time Binding and Polymorphism . . 5.4 Overriding (virtual functions) versus Overloading . . . . . . 5.5 Strong Typing . . . . . . . . . . . . . . . . . . . . . . . . . 5.6 Inheritance & Virtual Functions Summary . . . . . . . . 5.7 Inheritance versus Inclusion, is-A versus has-a . . . . . . . . 5.8 Reuse the Real Power of Inheritance & Virtual Functions 5.9 Example 2: a Person class hierarchy . . . . . . . . . . . . . 5.10 A Person Class . . . . . . . . . . . . . . . . . . . . . . . . . 5.11 A Student Class . . . . . . . . . . . . . . . . . . . . . . . . 5.12 Use of the Person Hierarchy . . . . . . . . . . . . . . . . . . 5.13 Polymorphism . . . . . . . . . . . . . . . . . . . . . . . . . . 6 Tutorial Introduction to C++ 6.1 Get Started . . . . . . . . . . . . . . . . . 6.1.1 Your First C++ Program . . . . . 6.2 Variables and Arithmetic . . . . . . . . . 6.3 For Loop . . . . . . . . . . . . . . . . . . 6.4 Symbolic Constants and the Preprocessor 6.5 Character Input and Output . . . . . . . 6.5.1 File copying . . . . . . . . . . . . . 6.5.2 Character counting . . . . . . . . . 6.5.3 Line Counting . . . . . . . . . . . 6.5.4 Word Counting . . . . . . . . . . . 6.6 Arrays . . . . . . . . . . . . . . . . . . . . 6.7 Functions . . . . . . . . . . . . . . . . . . 6.7.1 Declaration of functions . . . . . . 6.7.2 Program Split into Modules . . . . 6.8 Creation of Executables . . . . . . . . . . 6.8.1 Basics Compiling and Linking . . 6.8.2 Other Libraries . . . . . . . . . . . 6.8.3 Static versus Shared Libraries . . . 6.8.4 Make and Makele . . . . . . . . . 6.9 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

7 Variables, Types, Operators and Expressions 7.1 Introduction . . . . . . . . . . . . . . . . . . . . . 7.2 Variable names . . . . . . . . . . . . . . . . . . . 7.3 Elementary Data Types and Sizes . . . . . . . . 7.3.1 Integer types . . . . . . . . . . . . . . . . 7.3.2 Floating-point types . . . . . . . . . . . . 7.3.3 Implementation Dependencies . . . . . . . 7.4 References . . . . . . . . . . . . . . . . . . . . . . 7.5 Arrays . . . . . . . . . . . . . . . . . . . . . . . . 7.6 Pointers . . . . . . . . . . . . . . . . . . . . . . . 7.6.1 Introduction . . . . . . . . . . . . . . . . 7.6.2 Declaration / Denition of Pointers . . . 7.6.3 Referencing and Dereferencing Operators 7.6.4 Pointers and Arrays . . . . . . . . . . . . 7.7 Strings in C++ . . . . . . . . . . . . . . . . . . . 7.7.1 Introduction . . . . . . . . . . . . . . . . 7.7.2 C-strings . . . . . . . . . . . . . . . . . . 7.8 Standard String Class . . . . . . . . . . . . . . . 7.9 Constants and their Types . . . . . . . . . . . . . 7.10 Declaration versus Denition . . . . . . . . . . . 7.11 Arithmetic Operators . . . . . . . . . . . . . . . 7.12 Relational and Logical Operators . . . . . . . . . 7.13 Type Conversions and Casts . . . . . . . . . . . . 7.14 Assignment Operators and Expressions . . . . . . 7.15 Increment and Decrement Operators . . . . . . . 7.16 Conditional Expressions . . . . . . . . . . . . . . 7.17 Bitwise Operators . . . . . . . . . . . . . . . . . 7.18 Precedence and Order of Evaluation of Operators 8 Control Flow 8.1 Introduction . . . . . . . . . . . . . . . . . 8.2 Statements and Blocks . . . . . . . . . . . 8.3 Selection . . . . . . . . . . . . . . . . . . . 8.3.1 Two-way Selection if else . . . . 8.3.2 Multi-way Selection else if . . . . 8.3.3 Multi-way Selection switch - case 8.4 Repetition . . . . . . . . . . . . . . . . . . 8.4.1 while . . . . . . . . . . . . . . . . . 8.4.2 for . . . . . . . . . . . . . . . . . . 8.4.3 do - while . . . . . . . . . . . . . . 8.5 break and continue . . . . . . . . . . . . . 8.6 goto and Labels . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

78 78 78 79 79 80 80 80 81 82 82 83 83 84 85 85 85 87 88 88 89 89 89 89 90 90 90 90 91 91 91 91 91 92 92 93 93 93 94 94 94 95 95 95 96 96 96 97 97 98 98 99 100 100 101 102 102 103 104 105

9 Functions and Program Structure 9.1 Introduction . . . . . . . . . . . . . . . . . . . . . . 9.2 Basics . . . . . . . . . . . . . . . . . . . . . . . . . 9.3 Declarations of Functions Prototypes . . . . . . . 9.4 Function parameters . . . . . . . . . . . . . . . . . 9.4.1 Parameters . . . . . . . . . . . . . . . . . . 9.4.2 Pass-by-value parameters . . . . . . . . . . 9.4.3 Pass-by-reference parameters . . . . . . . . 9.4.4 Programmed pass-by-reference via pointers 9.4.5 Semantics of parameter passing . . . . . . . 9.4.6 Arrays as Parameters . . . . . . . . . . . . 9.4.7 Default parameters . . . . . . . . . . . . . . 9.5 Function return values . . . . . . . . . . . . . . . . 9.6 Overloaded function names . . . . . . . . . . . . . 9.7 Inline functions . . . . . . . . . . . . . . . . . . . . 9.8 External variables . . . . . . . . . . . . . . . . . . 9.9 Scope of variables . . . . . . . . . . . . . . . . . . . 9.10 Namespaces . . . . . . . . . . . . . . . . . . . . . . 9.11 Heap memory management . . . . . . . . . . . . . 3

9.12 9.13 9.14 9.15 9.16 9.17 9.18

9.11.1 Introduction . . . . . . . . . . . . . . . . 9.11.2 Operator new . . . . . . . . . . . . . . . . 9.11.3 Operator delete . . . . . . . . . . . . . . . 9.11.4 Anonymous variables . . . . . . . . . . . . 9.11.5 Garbage . . . . . . . . . . . . . . . . . . . Lifetime of variables . . . . . . . . . . . . . . . . 9.12.1 Lifetime of variables summary . . . . . Memory layout of a C++ program a model . . Initialisation . . . . . . . . . . . . . . . . . . . . Register Variables . . . . . . . . . . . . . . . . . Block Structure . . . . . . . . . . . . . . . . . . . Recursion . . . . . . . . . . . . . . . . . . . . . . The C++ Preprocessor . . . . . . . . . . . . . . 9.18.1 File inclusion . . . . . . . . . . . . . . . . 9.18.2 Symbolic constants and text substitution 9.18.3 Macro substitution . . . . . . . . . . . . . 9.18.4 Conditional compilation / inclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. 105 . 106 . 107 . 107 . 108 . 108 . 109 . 110 . 111 . 111 . 112 . 112 . 113 . 113 . 114 . 114 . 114 . . . . 116 116 116 117 119

10 Records struct, class, union 10.1 Introduction . . . . . . . . . . 10.2 A Point record . . . . . . . . 10.3 Operations on Structs . . . . 10.4 Unions . . . . . . . . . . . . .

11 Objects That Use Heap Memory 11.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11.2 An Heap-based Array Class . . . . . . . . . . . . . . . . . . . 11.2.1 Class Declaration . . . . . . . . . . . . . . . . . . . . . 11.2.2 Class implementation code . . . . . . . . . . . . . . . 11.2.3 A simple client program . . . . . . . . . . . . . . . . . 11.3 The this pointer . . . . . . . . . . . . . . . . . . . . . . . . . 11.4 Copy Constructors and Parameter Passing and Value Return 11.4.1 Na ve member-wise constructor, shallow copy . . . . . 11.4.2 Proper deep copy constructor . . . . . . . . . . . . . 11.4.3 Copy constructor and parameter passing and return . 11.5 Assignment . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11.6 Destructor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11.7 The Big-Three . . . . . . . . . . . . . . . . . . . . . . . . . . 11.8 Reference parameters and reference return . . . . . . . . . . . 11.9 Privacy is class-based, not object-based . . . . . . . . . . . . 12 Operator Overloading 12.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . 12.2 Lead-in Add Functions for Array . . . . . . . . . . 12.3 Chaining calls to member functions . . . . . . . . . . 12.4 Operators . . . . . . . . . . . . . . . . . . . . . . . . 12.5 Member versus Non-member Functions, Conversions 12.5.1 Introduction . . . . . . . . . . . . . . . . . . 12.5.2 A String Class . . . . . . . . . . . . . . . . . 12.5.3 Implicit Arguments . . . . . . . . . . . . . . . 12.5.4 Coercion of Arguments . . . . . . . . . . . . 12.5.5 Constructors for conversion explicit . . . . 13 A List Class 13.1 Introduction . . . . . . . . . . . . . . . . . . . 13.2 Friend Functions and Classes . . . . . . . . . 13.3 A List Class . . . . . . . . . . . . . . . . . . . 13.3.1 Informal Specication of the Class . . 13.3.2 Class Interface . . . . . . . . . . . . . 13.3.3 Dissection of Listint.h . . . . . . . . . 13.4 List implementation code . . . . . . . . . . . 13.5 Deep Copy: Copy Constructor & Assignment . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

122 . 122 . 123 . 123 . 124 . 127 . 130 . 130 . 130 . 130 . 131 . 131 . 132 . 132 . 132 . 134 135 . 135 . 135 . 139 . 139 . 144 . 144 . 144 . 148 . 150 . 150 . . . . . . . . 151 151 151 153 153 153 154 155 157

13.5.1 Copy constructor . . . . . . . . . . 13.5.2 Deep-copy assignment . . . . . . . 13.5.3 Destructor for heap objects . . . . 13.6 The Big-Three . . . . . . . . . . . . . . . 13.6.1 Defence against naive defaults . . . 13.7 Overloading the Stream Output Operator 13.8 A simple client program . . . . . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . .

157 158 158 159 159 160 160

14 Correctness and Specication 14.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . 14.2 Syntactic Specication . . . . . . . . . . . . . . . . . . . . . 14.3 Semantic Specication . . . . . . . . . . . . . . . . . . . . . 14.3.1 Introduction . . . . . . . . . . . . . . . . . . . . . . 14.3.2 Assertions . . . . . . . . . . . . . . . . . . . . . . . . 14.3.3 Axiomatic Specication . . . . . . . . . . . . . . . . 14.3.4 Model-based Specication: Pre- and Post-conditions 14.4 Design-by-Contract . . . . . . . . . . . . . . . . . . . . . . . 14.5 Pre-conditions and Defensive Programming . . . . . . . . . 15 Templates 15.1 Introduction . . . . . . . . . . . . . . . . . . . 15.2 Template Functions . . . . . . . . . . . . . . 15.2.1 Overloaded Functions recalled . . . . . 15.2.2 Template Function . . . . . . . . . . . 15.3 Polymorphism Parametric . . . . . . . . . . 15.4 Alternative Approaches to Generiticity . . . . 15.5 Template Array . . . . . . . . . . . . . . . . . 15.5.1 Class declaration and implementation 15.5.2 A simple client program . . . . . . . . 15.6 Template List . . . . . . . . . . . . . . . . . . 15.6.1 Class Interface . . . . . . . . . . . . . 15.6.2 Dissection of Listt.h . . . . . . . . . . 15.6.3 Implementation of functions . . . . . . 15.6.4 Dissection of functions . . . . . . . . . 15.7 A simple client program . . . . . . . . . . . . 16 Queue and Stack Classes 16.1 Introduction . . . . . . . . . . . 16.2 A Queue Class . . . . . . . . . 16.2.1 Informal Specication of 16.2.2 Class Interface . . . . . 16.3 Queue implementation code . . 16.4 A simple client program . . . . 16.5 A Stack Class . . . . . . . . . . 16.5.1 Informal Specication of 16.5.2 Class Interface . . . . . 16.6 Stack implementation code . . 16.7 A simple client program . . . . . . . . . . . . . . . . the Class . . . . . . . . . . . . . . . . . . . . . . . . the Class . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

162 . 162 . 163 . 164 . 164 . 164 . 165 . 166 . 166 . 167 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 168 168 169 169 169 170 170 170 170 173 173 173 174 175 175 175 177 177 177 177 177 181 181 182 182 182 184 184 186 186 186 189 192 195 196 201

17 A Multi-class Program Graphics 17.1 Introduction . . . . . . . . . . . . . 17.2 Matrix class . . . . . . . . . . . . . 17.3 Image class . . . . . . . . . . . . . 17.4 Figure Class . . . . . . . . . . . . . 17.5 Open-Closed Principle . . . . . . . 17.6 Figure Classes . . . . . . . . . . . . 17.7 Program using Figure hierarchy . .

A Where do procedural programs come from? A.1 Introduction . . . . . . . . . . . . . . . . . . . . . . A.2 Patterns and Programs . . . . . . . . . . . . . . . . A.3 Sequence . . . . . . . . . . . . . . . . . . . . . . . . A.4 Repetition . . . . . . . . . . . . . . . . . . . . . . . A.4.1 Sequence of Repetitions . . . . . . . . . . . A.4.2 Repetitions of Repetitions Nested . . . . A.5 Subprograms Procedures, Methods . . . Functions A.5.1 Without Parameters . . . . . . . . . . . . . A.5.2 Procedures with Parameters . . . . . . . . . A.6 Selection . . . . . . . . . . . . . . . . . . . . . . . . A.7 Exercises . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

204 204 206 206 208 208 209 210 210 211 216 216

Chapter 1

Introduction and Overview


1.1 Scope and Warning(!)

These are course notes for a lecture series on Object-oriented Programming, given as part of of the course Diploma in Computing, School of Computing, Letterkenny Institute of Technology. Chapters 1 to 5 and Appendix A on Where do procedural programs come from? have been revised recently. However (warning), chapters 6 onwards are unaltered since an earlier version developed at University of Ulster during 19971999. That is, a bit before the STL (Standard Library containers and algorithms) became mainstream. If I was to teach the course now, Id include a lot more (what I call) STL, i.e. Id be strongly inuenced by (Koenig & Moo 2000). Since, at this writing (2004-04-19), you are liable to encounter silly problems, or maybe you want access to the programs (all are available), the best thing is for me to give my email address jg.campbell@ntlworld.com; you can be assured that I will reply promptly.

1.2

Overview

Chapter 2 puts object-oriented programming in context: software engineering and its goals; modularity as a means of taming complexity. We argue for modularity based on data rather than on procedure, process. Thus, types and abstract data types. We identify some mottoes of object-orientation: OO motto 1 (Meyer 1997): Ask not rst what the program does, ask what it does it to; OO motto 2 (Budd 1999b): Ask not what you can do to your data structures, but what your data structures can do for you; OO motto 3 (Budd 1999b): Old code can call new code ; OO motto 4 (Budd 1999b): Upside down libraries ; OO motto 5 (Meyer 1997): Modules which are closed to modication, yet open to extension; OO motto 6 (John Vlissides, C++ Report, Feb 1997): a hallmark of the object-oriented paradigm is that change is additive, not invasive. That is, you should modify and extend a system by adding code rather than by changing any of the existing code (which would mean that everything that went before would have to be retested again rather than just the extension).

Chapter 3 introduces the programming details of object-oriented programming classes and objects. An object has: state (data, representation); behaviour (methods, interface functions); state and behaviour bound together in one program unit (encapsulation, information hiding). If you know no C++ at all, you may need to have a look at chapters 6 to 10 before you attempt this. Chapter 4 introduces other programming language issues: types; variables; values; references; aliasing (one object which has two or more names); lifetime and garbage; scope; heap memory management. Chapter 5 introduces inheritance: class and program extension via inheritance (modules are open to extension, yet closed to modication ; the primacy of interfaces ; classes which implement multiple interfaces; multiple inheritance in C++; composition versus inheritance (because inheritance is OO there is a temptation when plain old composition would do ne. In addition, we discuss genericity if we write a sort routine for a list of numbers, wouldnt it be nice to be able to reuse it for a list of students. Genericity equates to templates in C++. Now we go back to imperative C++. Maybe this should be before the OOP stu. Chapters 6 to 10 introduce the major features of imperative C++, i.e.avoiding object-orientation. If you know ANSI C, there will not be too much new here. Chapters 6 to 9 follow the same pattern as chapters 1 to 4 of (Kernighan & Ritchie 1988), but with code that is fully C++. In chapter 9, there is one mention of objects and classes, when we introduced the string class; string is such an improvement over C-string (char *) that it is important to introduce it at this early stage. Chapter 10 introduces struct, via a two-dimensional Point, and union. Chapter 11 introduces class memory management (heap memory) using a development of an array class to cope with variable length arrays. Here we address the necessity for copy constructors, destructors, and assignment operator. Operator overloading is introduced. Chapter 12 describes operator overloading, including the stream output operator <<; in addition it addresses some subtle issues connected with the necessity of sometimes using non-member functions and the consideration of conversion constructors. Chapter 13 develops a (linked) list class and further discusses issues to do with memory management. Chapter 14 briey discusses methods of specication of classes with a view to correctness. Chapter 15 introduces template functions and template classes and develops a template Array class and a List container class. Chapter 16 develops template stack and queue classes. Chapter 17 concludes the course by developing a signicantly sized (yet conceptually simple) multi-class program based on graphics and images. We introduce abstract classes in the form of a graphics Figure class hierarchy. We demonstrate reuse via a simple framework or upside-down library. Appendix A is a discussion on the design of procedural programs. It gives a discussion on how a programmer might design simple procedural programs. That is how do you dream up algorithms? One solution is: the algorithm matches the data structure; for collections of data we have: sequence, selection, repetition, procedures, procedures with parameters. There are many similarities between computer programs and cookery recipes or between cookery recipes and any sort of detailed plans. Originally, a raft of self-assessment and practical exercises and programs accompanied this document; if you think these may be of interest, contact: jg.campbell@ntlworld.com. These notes are based on previous notes: on object-oriented programming in C++ (Campbell 1999) and on algorithms and data structures (using Java) (Campbell 2001).

1.3
1.3.1

Recommended Reading
Object-oriented Programming

If I was recommending a book on the foundations of OOP, Id probably go for (Meyer 1997) it was the rst edition of that book that revealed OOP to me. However, that book uses Eiel. (Budd 1997b) is another very good book on general OOP and covers a number of languages. (Abadi & Cardelli 1996) is an advanced treatise on the computer science of OO.

1.3.2

Programming and Object-oriented Programming through Java

If I wanted to recommend a Java book, the choice would be between (Eckel 2003) and (Schmidt 2002). (Liskov 2001) is more advanced, but may appeal to some. Other worthwhile books on learning programming and object-oriented programming via Java are: (Charatan & Kans 2001), (Budd 1999b), and (Lewis & Loftus 1998). On Java itself, Java in a Nutshell (Flanagan 2002) is a very handy and encyclopedic reference. And (Sesoft 2002) is very nice and has the advantage of being only 120 pages. For a set of tips and guidelines towards better Java programming, see (Bloch 2001). If you program a lot in Java, the two volume series (Cornell & Horstmann 1999a) and (Cornell & Horstmann 1999b) may be worth having; some parts are on the web, see http://developer.java.sun.com/developer/Books/corejava/index.html. For conversion between Java and C++, see (Budd 1999a). Network programming in Java is nicely covered in (Harold 2000).

1.3.3

Programming and Object-oriented Programming through C++

If I was to recommend a teach-yourself modern C++ book, Id go for (Koenig & Moo 2000). If you need a reference on C++, see (Stroustrup 1997a). The C++ FAQ (Cline, Lomow & Girou 1999a) is useful, and there is an online version (use Google to nd it). I have learned a lot of what I know about OOP and C++ from (Budd 1994). See also the more recent (Budd 1997a). Budd produces very ne books, he is a real computer scientist, that is, there is evidence of science underlying his programs and descriptions. I have already recommended his book on Java (Budd 1999b) and his book on general object-orientation (Budd 1997b) and his book on conversion from C++ to Java (Budd 1999a). On that last topic, Horstmanns book (Horstmann 1997) provides another good reference for those who need to move between C++ and Java. There are very useful guidelines in: (Meyers 1998) and (Meyers 1996); a companion book (Meyers 2001) covers the STL (now the C++ standard library). There is a course on OOP and C++ developed by me in (Campbell 1999). If you want ideas for a nal year project and to see the way C++ and OOP is headed, see: (Alexandrescu 2001) and (Czarnecki & Eisenecker 2000).

1.3.4

General Software Engineering

Apart from the good advice in the programming books, see (McConnell 1993) and (Maguire 1993) which provide a Microsoft and C++ perspective.

1.3.5

Design Patterns

Software design patterns have attracted much interest recently; we will mention one or two along the way; see (Budd 1999b, chapter 5), the original gang-of-four book (Gamma, Helm, Johnson & Vlissides 1995), (Vlissides 1998) and (Buschmann, Meunier, Rohnert, Sommerlad & Stal 1996).

1.3.6

Specication and Correctness

For future reference, I note that specication 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).

10

Chapter 2

Introduction to Object-oriented Programming


The question always arises: is object-oriented programming a new paradigm ? i.e. is it based on a radically dierent model of computing, a totally new way of programming and thinking about programming? An analysis of everyday meaning of the words object-oriented programming would lead us to equate it to simply programming with abstract-data-types. However, to achieve what is now understood as object-orientation, we must add inheritance, and late binding (run-time binding). Programming based on ADTs, without inheritance, is what is called object-based programming. However, for many problems, simple object-based programming is sucient on its own. Inheritance and late binding facilitate reuse, in the sense that software modules classes, or families of classes may be extended without modifying the original. Here, I use the term module in a general sense to signify a fundamental unit of software; the components of a module, e.g. functions, individual statements, should not normally be separated from the module. The concept of reuse is sometimes poorly dened. It doesnt mean simple use without any modication, e.g. as in a library, or library function that form of use is successfully used all the time. Really, ease of reuse means easily extended, or modied to cope with some change of requirements; indeed, we can also include ease of maintenance, since this, also, comes about chiey due to change of requirements. Reuse via extension without (invasive) modication, is highly signicant: A module which is modied must be tested (again). What about current users of the module? How are they going to accommodate any change of behaviour? In essence, a module which is modied is a new module, however small the modication. One can attempt to avoid any modication by anticipating all possible eventual requirements, but this turns out to be quite impossible; the only sure way of avoiding change in a software system, including unanticipated change, is to ensure that nobody uses it! A quotation from John Vlissides (C++ Report, Feb 1997) sums it up: Ive said it before and Ill say it again: A hallmark if not the hallmark of good object-oriented design is that you can modify and extend a system by adding code rather than by hacking it. In short, change is additive, not invasive. We will see that the reuse concepts mentioned here are the embodiment of the open-closed principle (Meyer 1997, ?). The open-closed principle allows a module to satisfy two apparently irreconcilable requirements: That, to be usable by other than just the module developer, a module must be closed to modication. 11

That to be reusable, the same module must be open to extension. Of course, I must not be seen to diminish the signicance the abstraction and encapsulation oered by simple classes and modules. These oer great benets on their own, and are major engineering principles in their own right. On the other hand, some of the greatest problems in software development lie in the illusion that software is easy to modify after all, the very name soft ware seems to suggest malleability like plasticine, as opposed to hard ware. Actually, software is easy to create, and to modify, to hack together a quick solution, but only in certain circumstances : The programmer is the user; The software development team is one person, or not much more; The software is not documented; The software has no impact on safety; etc. These can be summarised by contrasting large-scale software projects with small-scale and personal software projects: programming-in-the-small, and opposed to programming-in-the-large, (Parnas 1972). When software teams attempt to carry out large projects using the same practices that they used in personal software projects, the contrasts become apparent, the phrase software-crisis, see e.g (Brooks 1995), takes on a grim reality.

2.1

Java and C++ Mixture of Paradigms

Both Java and C++ are based on C, an imperative/procedural programming language, To some extent, bot Java and C++ are C with object-orientation extensions. The perceptive reader will already have wondered: how are methods implemented? Surely, eventually, one must get to machine code which denitely imperative. Here is they key to getting to grips with the apparent paradox, how can Java be object-oriented and procedural at the same time? The answer is that, in Java and C++, even in a strongly object-oriented design, a typical method / function is implemented in an procedural language most of which diers little from C. As a consequence of its mixture of features, it is perfectly possible to write Java and C++ programs in a style that is rather procedural like pure C. On the other hand, both languages permit fully object-oriented designs.

2.2

The Question Again, Why OO

If we know that the underlying machine is procedural, why bother with OO? The answer is, whilst procedural programming suits the computer, object-oriented programming suits the programmer. It is easier for programmers to think object-oriented ; hence programming becomes easier, less error prone, easier to test, . . . . Object-oriented programming ts nicely with object-oriented analysis and design. In analysis, we always like to stay close to the language of the user and the problem. Object-oriented analysis allows us to do this the cognitive distance between the user requirements and the design can be reduced, thereby reducing the chance of errors. Thats analysis. Its when it comes to design, implementation, testing and maintenance that much greater benets of object-orientation become apparent. 12

2.3
2.3.1

Software Engineering and its Requirements and Goals


Introduction

In this and following subsections I will develop a justication of object-orientation as a solution to some of the requirements and goals of software engineering today. Unless you have worked on the production of software for sale, it may be dicult to grasp some of the goals of software engineering and software production companies; hence, I will attempt to relate much of what I say to student experience. When you set about doing a programming assignment you are likely to be primarily focused on three issues: Get it done quickly. In a software house, this maps to cost and schedule, but, ultimately cost, and closely related productivity. Do it well, so that it satises the assignment specication and works properly, and hence gets you good marks. Here we are talking of quality. To this I could add re-usability you would like some parts of your software to be capable of adaption for a later assignment. Looking at the matter from an other point of view that of a consumer, what do you look for in a software package? Presumably: That it does what you expect; that it doesnt crash; i.e. quality. It is cheap to buy; i.e. we hope that the software house produced it for low cost, and that they have passed that on to their consumers. That it is relatively ecient in terms of speed and performance and use of resources. Even today, you may be a little disappointed if a word-processor demands 256 Megabytes, and a 2-GHz Pentium, to run properly. If the software you had bought was source code, or object code you might expect it to be adaptable to some perform some task you had not envisaged when originally purchasing. Hence, from the consumer and the producer point of view, we have recurring goals: cost, productivity, quality, schedule, eciency, reuse. One can lump together cost, schedule, reuse, and productivity, as simply productivity : how quickly, and with as few programmers as possible, can I produce my new Internet killer-app ? In addition, how quickly and cheaply can I extend it to run on a recently introduced Pentium III with super-charged-MMX-12-speed-pipelined-over-drive? Quality? I am going to lump eciency in with it. Quality can be dened simply as meeting customer expectations, (Crosby 1979). Moreover, we can quote the title of Crosbys book: Quality is Free, (Crosby 1979), to link quality with productivity; at its simplest, the less time spent xing faults, the greater the productivity.

2.3.2

Software Crisis

The phrase software crisis is often used to encapsulate all the ills of the software industry, particularly poor productivity, and thus to justify constant pleas for reuse. Actually, there are many issues involved here; e.g. maybe software engineers are just as productive as any other engineers. Or more so? Or, if software engineers are unproductive, by what standard are they measured? Nevertheless, there is good reason to believe that some software engineering methods may be immature, and that the techniques and practices associated with object-orientation do represent progress in the industry or profession. 13

2.4

The Waterfall Life-cycle and Structured Systems Analysis and Design

As well as being a programming (implementation) technique, object-orientation very much also impacts on analysis and design, and hence the whole of management of software projects, (Yourdon 1989). Those software projects of the late 1970s and 1980s that were managed (as opposed to hacked!), were probably managed on the basis of some avour of the waterfall life-cycle, and structured analysis and design (SSAD) (Yourdon 1989). Incidentally, let me add here, that while I believe that management both project and people is a crucial ingredient in any signicantly sized software project, it is nonsense to think that good management alone can succeed in the absence of good engineering techniques.

2.4.1

Waterfall Life-cycle

The waterfall life-cycle seems the natural way to run an engineering project: The project proceeds along clearly dened phases. A preceding phase must be completed before the next starts. Phase completion is judged by the outcome of the phase matching the requirements dened by the previous phase. Note: where you have nothing executable, e.g. early phases, this is most dicult, and is usually where pretense starts, and the seeds of disaster are sown. Though it never mentions structured analysis and design, these are tacitly assumed; other analysis, design methods may be incompatible, e.g. if analysis and design are not clearly segregated. The phases in a typical project are: 1. Study feasibility. Preliminary exploration of possible solutions. Take a hard look at benets versus drawbacks and costs of candidate solutions. 2. User Denition of System Requirement. The user documents as much as he knows about the job the system must do. Often they dont know what they want, and, even if they do, they cannot express it with any precision. 3. Developer Denition of System Requirement. Developer analyses users requirement and, performs further investigation of requirements, produces developers version of requirements: System Requirement Document (SRD). The SRD is now a specication and part of the contract. Prepare project plan, including costing and schedule. 4. High-Level Design. Decompose into subsystems, perhaps modules. Allocate to team or individuals. Usually called Architectural Design or Preliminary Design. Output Architectural Design Document (ADD). Design tests for sub-systems. 5. Detailed Design. Decompose further into subsystems (units or components) such that one person can cope. Specify these subsystems. Design tests for (sub)subsystems. Outcome: Detailed Design Document (DDD). 6. Implementation coding. Code and test components each programmer. 7. Integration and Test. Components and modules are brought together to form higher level systems. And tested. You should now have a working system! 8. System Test. Assess the quality of the system as dened in SRD, above. Simple denition of quality : meets customer requirements / expectations. 9. Operations and Maintenance.

14

Although this scheme is better than nothing, e.g. all hands to the keyboards, with no planning, it, along with structured analysis and design, has serious shortcomings, some of which were already noted: Except for well understood cases, it may dicult to completely dene requirements at the beginning. Hence, prototyping is often prescribed. Closely related to the previous item, it is almost impossible to accommodate, at a late stage, changes to requirements. After all, the requirements were agreed and frozen in the SRD. Some hope! However, the only software projects for which the requirements dont change are dead ones! It is almost impossible to t reprogramming (maintenance), or reuse, into this format.

2.5

Structured Systems Analysis

This example given here trivializes the methodology, nevertheless it identies the characteristics I want to examine. Informal requirement: we need a program to manage the marks for a course. For obvious reasons, Im attempting to avoid the word class. When I want to talk about a class of students, Ill attempt to stick to the term group. Marks can be entered as the coursework and examinations are marked; hence, we identify the terms current marks for marks already entered, and new marks for marks to be entered for the rst time. In the analysis part of structured systems analysis and design (SSAD), we would rst identify the environment diagram as in Figure 2.1.

Current marks

New marks

Manage course marks

New report

Updated marks

Figure 2.1: Manage course marks Environment diagram. That would be followed by a top-level data-ow diagram (DFD), Figure 2.2. Most real top level DFDs would be more interesting than Figure 2.2. In a DFD, arrows represent data ows and circles represent processes. Processes consume data and produce new data. Data ows are dened using data dictionaries. Users of object-orientation could learn a lot from data dictionaries. We can add further detail by exploding Figure 2.2 to get Figure 2.3. The things with two closely spaced horizontal lines are les. And so on with each process at one level becoming a full diagram at the next level. For example, if there was detailed verication of input, it might be necessary to explode the Input process, to arrive at a number of subprocesses, e.g. four processes: Input1 ---> Input2 ---> Input3 ---> Input4 15

Current marks

New marks

Manage course marks

New report

Updated marks

Figure 2.2: Manage course marks Level-1 DFD. When you get to a process which merits no further decomposition, you dene its activity using some sort of pseudo-code, in a so-called mini-spec. Each statement in the mini-spec might correspond to one or more program statements, or maybe to a system function call. The idea of structured systems analysis is that the complete system requirements are captured in the combined: a data ow diagrams; b the data dictionary which describes the (owing) data; c mini-specs which dene the smallest processes.

2.6

Structured Systems Design

The hierarchy of data-ow diagrams can now be rather easily used to form a (program) structure chart. From the example above, we can produce a structure chart as in Figure 2.4. Figure 2.4 gives a top-down decomposition.

2.7

Structured Systems Implementation, Programming

Implementation in code is now a straightforward matter. There is a main program, which corresponds to mainProgram and what it calls is based on the structure chart, which in turn was based on the level-1 data ow diagram Figure 2.2. int main(){ //Declare variables. getCurrent(); validateCurrent() 16

Current Marks Get current marks Validate database marks New marks Generate report Input new marks Course report Formatted marks

Validate new marks

Figure 2.3: Manage course marks Level-2 DFD. inputNew(); validateNew(); makeReport(); return 0; } The structure chart decomposition of inputNew is reected likewise in its code: void inputNew(){ // Declare local variables. inputFun1(); inputFun2(); return; } If inputFun1, . . . are at the bottom of the hierarchy, their code is developed from whatever is specied in their mini-spec s. Otherwise, we go further down the decomposition. Typically, procedures at each layer are arranged in libraries.

2.8

Shortcomings of Waterfall and Top-down Decomposition

Firstly, we must give credit where credit is due: the waterfall life-cycle is an awful lot better than nothing. When I started computing in the 1970s is wasnt widely known or practiced, and, certainly, when I became aware of it, it solved a lot of problems, and was light years of progress over: lets start programming at the beginning, and keep going until were nished!

17

mainProgram

getCurrent

validate Current

inputNew

validate New

makeReport

...

inputFun1 ...

inputFun2 ...

Figure 2.4: Manage course marks structure chart. The waterfall project life-cycle and top-down decomposition are so intertwined that use of one more or less demands the other. Hence we can discuss their shortcomings together, although this means that we are intermingling technical, managerial, and social issues: a For any signicantly complex problem, it unlikely that it will be possible to agree on exact requirements at the beginning of the project. b Real systems have no top (Meyer 1997). This is true in many systems: concurrent systems, any system dependent on the graphics-user-interface (GUI), see the discussion below on event-driven systems. In fact, instead of the top-down and library structure, what is more suitable is a framework or upside-down library (Budd 1997a, ?). c Although the components of the top-down decomposition seem to be independent and decoupled and hence we would expect that changes to one, would aect none or very few of the others this is far from the case. They may in fact be tightly coupled via the (eectively) global data owned by mainProgram. This coupling is all the more damaging for its subtlety you dont know its there until you start to make changes, and you have no plan of escape from its grasp. The insidious coupling via data is shown in Figure 2.5.

data

data

data

getCurrent

validate Current

inputNew

validate New

makeReport

inputFun1

inputFun2

Figure 2.5: Coupling via data.

18

2.9

Object-orientation to the Rescue

Figure 2.6 shows part of a solution based on an object-oriented architecture. Here we have the problem (and the solution) modelled using objects. The objects have internal hidden state (data) and external visible methods (interface, interface functions). For example, a StudentInfo object might have a String name an array of double marks[10]. The CourseReport object might be a bit like a spreadsheet and have names and summary marks for each student.

StudentInfo object

CourseReport object

data

data

method1

method3

method2

method4

Figure 2.6: Object-oriented architecture. The program proceeds by (or in our object-oriented model proceeds by) objects communicating between oneanother via their methods. Data are sent via methods, data are requested and returned via methods. Typically, data are wrapped up as objects. This execution of an object-oriented program is shown in Figure 2.7.

StudentInfo object

data

compute Overall Mark overallMark

CourseReport object

data

Figure 2.7: Program executes via message passing. A class is simply a blueprint / plan for an object.

19

2.10

Key Application: Event-driven Interaction

(Foley, vanDam, Feiner & Hughes 1990). Traditionally programmed interaction works as in the following: begin Prompt user; Read input; Do processing; Display result; end; A little thought will indicate that this will not work for any GUI (graphics-user-interface) like Windows, nor, indeed, for any direct-manipulation interface: What do you prompt? the user can do any of a large range of actions; the user is in charge, not the program. What input device do you read? Any of a number (mouse, keyboard, other pointing device) may be legal, and appropriate. What do you expect to read? A number? A character string? etc... The basic structure of most Macintosh application programs (and probably Windows too) is as in: begin Initialise; -- build windows on screen While (not Done) SystemTask; if GetNextEvent(eventMask,theEvent) -- an event from a queue HandleEvent(theEvent); -- e.g. mouse button press CleanUp; end; Event driven input-output The important point is this: HandleEvent (the application part) is driven by the event from the uncontrollable, unpredictable user, not the other way round. For example this can allow an interface to be mode-less : the program identies the mode from the event. It is dicult to identify any Top in such a system. The top-down decomposition can be done with only extreme diculty and with extreme prejudice to the system architecture and any thought of extendibility or reuse. Budd, (Budd 1997a) points out the complete contrast between a top-down architecture and the framework or upside-down libraries architecture favoured by the likes of our event-driven interaction example. He uses the term upside-down library to highlight the contrast, as follows: In a top-down architecture, programming is concentrated on provision of the main program and the higher layers. The lower layers are more likely to be provided by system libraries: input-output, string manipulation, etc. However, in the event-driven interaction example, the main program the framework is the same for all programs. It comes with the system like traditional libraries used to. It is the lower-level functions, like HandleEvent that must be programmed. Hence, upside-down library the library is at the top, though not in the simple sense of top-down decomposition. It has been said that, had it not been invented before, object-oriented programming would have had to be invented for construction of GUIs. 20

2.11

Cornerstones of Software Engineering and Object-oriented Systems

Already, see section 2.3, we have identied quality, re-usability and productivity as key goals of software engineering. We have produced some samples and evidence to indicate that the re-usability and productivity goals ultimately demand architectural design techniques and languages that produce decentralised designs, comprised of modules which communicate with other modules via well-dened, narrow interfaces, (Meyer 1997). As pointed out by Meyer, this is strongly suggestive of an object-oriented approach.

2.11.1

Modules and Data Abstraction

Systems engineering has long understood the benets of abstraction and modular design, especially for large complex systems. The systems engineering concept of the black-box is archetypal abstraction: the (sealed) blackbox has some inputs, and some outputs that are clearly dened functions of the inputs. The designer who employs the black-box as a sub-system is concerned only with the function provided, and with the allowable range of inputs; he is absolutely entitled (and obliged) to consider only the external behaviour of the sub-system. Although abstraction in software was already well accepted in the form of high-level languages and subprogram libraries, (Parnas 1972) gave the rst persuasive argument for software modularisation on the basis of data structures (data abstraction); he also pointed clearly to encapsulation - softwares version of the sealed box - when he identied the crucial (but subtle) importance of information hiding : the user of a module benets from knowing just enough to use the module, the provider of the module benets from knowing just enough to implement the module. More knowledge on either part can only increase coupling and, hence, complexity.

2.11.2

Abstract data types

A data type, such as the native data types int, oat, . . . is characterised by: 1. A set of values that can be assumed by objects of the type. 2. A set of operations that can be performed on objects of the type. Clearly, internal representation should concern only compiler writers the type is a sealed black-box. I hope it will be clear to you already that any language with a modicum of type checking does not allow objects of native types to be accessed via their representation just via their dened operations. The ADT concept can best be described by proceeding from the notion of a type ; more prosaically, it is a user dened type that obeys the rules of a type set out in the previous paragraph particularly that users interact with the objects strictly via the supplied operations.

2.11.3

Classes

Object-oriented languages have special modules, usually called classes, which are specically used for dening ADTs. Objects are instances of classes. That is, objects are to classes what variables are to types: in C++ or Java, int i; denes the variable i, an instance of the type int. Syntactically, classes are like super-records (using record to mean a collection data structure without any coupled methods/denition of behaviour). In addition to data, they contain operations in the form of functions. Normally, the data (representation) part is private and so inaccessible to user programs. Interaction with the object must be via the public operations: 21

Private data, public interface functions. Private representation, public interface. An object should be characterised entirely by its behaviour

2.11.4

The Primacy of Behaviour Interfaces

If users of a class concentrate on the behaviour of an object (interface functions), as they must do if the data are private, and the interface is designed with care, then we have a very robust design. The development of the class is totally decoupled from the development of the code that uses the class. Hence, the internals of the class may be changed at will only the class and the class programmers will be aected.

Example : The year 2000 problem. The year 2000 problem was cause not just by programmers using an inappropriate two-digit representation of year, but by all users of that representation having access to the data. See above, coupling via data Figure 2.5.

2.11.5

Inheritance

Inheritance allows a derived type (class) to be dened as an extension, or specialisation of a base type; i.e. derived objects inherit all the features of base, and add their own specialisations. Inheritance allows us to build complex types/classes by reference to simpler classes. This extension to a simple type is achieved by adding extra data elds, and adding operations or replacing existing ones. However, since we do this by referring to the simpler class, rather than by altering it, the integrity of the simpler type is unaected. There are at least two objections to the extension of existing software modules by alteration, reuse by duplication : It creates duplicated copies which must be separately maintained; Altered software, however small the alteration, demands the full treatment of testing, documentation, etc. Suppose an application program deals with people there are various categories of Person: Customers, Employees, Directors, etc. Inheritance allows a (derived/extended) class, e.g. Employee, to be dened in terms of the (base) Person class. Moreover, with overloading of interface functions, client programs can treat objects of the two classes in a uniform way.

2.11.6

Multiple Interfaces

There are cases where an object may wish to oer dierent behaviours in dierent situations. Thus in the course example, StudentInfo might oer an typically student-information interface: take-marks; return average marks; return honours-pass-fail grade, etc. However, in addition, in a sort routine which produces a ranking list, it may wish also to behave as a Comparable (number) object. Java handles this situation by allowing a class to implement multiple interfaces. In C++ we use multiple inheritance to provide multiple behaviours.

22

2.11.7

Late Binding

Late binding of functions allows client programs to take advantage of extended classes without alteration of the client code. Note: Late binding is synonymous with dynamic binding and run-time binding. This contrasts with early binding static binding, compile-time binding in which a function call is bound to a denite function at compile time. Late binding becomes operable when the type of a reference (variable) may change during the execution of a program according to the type of the last value assigned to it. Inheritance, with overloaded interface functions, will allow client programs to treat all classes in the inheritance family in a uniform way polymorphism. However, with plain inheritance, all objects will be statically bound to a single class and, thereby, to interface functions of that class. Inheritance, together with late binding permits the following: Old code can call new code!. Read that again! Old code can call new code. Normally, with libraries, new code, which must be upper-level functions, is the caller and the called code the library functions is the old code. But how does old code know about the new code? When maybe the new code didnt exist when the old code was written. With dynamic binding, when an overloaded operation is invoked, the language takes care of choosing the appropriate version at run-time; thus, run-time binding. Hence, new code can be bound to a polymorphic object at run time. Hence, systems can be extended without knock-on requirements for reworking of the older parts of the system. This is the same as the upside-down library eect mentioned above. Here we nd another form of polymorphism : objects may exhibit a sort-of dynamic typing, in which and object takes the type of the last object assigned to it. These notions are subtle but should become clear with exposure to examples.

2.11.8

Open-closed Modules

(Meyer 1997) For a module to be usable by a client whether outside the organisation it was be closed to changes of behaviour. Nevertheless, it must be open to possible extension. This remarkable property, open-closed, is facilitated by inheritance and run-time binding.

2.11.9

Design-by-Contract

Meyer, (Meyer 1997), see also (Mitchell & McKim 2002), suggests a form of contract as the only proper basis for design of modules (classes). The supplier of the module agrees with the client : 1. What services the module should provide. These can be expressed as post-conditions. 2. Exclusions what the module is not expected to cope with. These exclusions can be expressed as preconditions. It takes little imagination, or experience of software development, or of life, to see the good sense in design-bycontract. Responsibility-driven-design, (Budd 1999b) is a similar concept. 23

2.12

Summary

Complexity of large software projects programming in the large. Software crisis. Quality. Productivity. Design for change and reuse. OOP languages. OOP paradigm. Goals of software engineering. Summary of waterfall life-cycle and structured systems analysis and design. Limitations of waterfall life-cycle and structured systems analysis and design. Object-oriented model/architecture. Event driven interaction. Cornerstones of modern software engineering and object-oriented programming.

24

Chapter 3

Classes and Objects


3.1 Introduction

This chapter introduces the basics of classes and objects. We describe how the C++ class construct may be used to implement an ADT (abstract-data-type), i.e. class as ADT. For the moment, we will avoid inheritance, polymorphism and dynamic / run-time binding. Hence, this chapter could be said to be concerned with object-based programming, this is the term used for software development based on just the plain ADT aspect of classes. Nevertheless, it is essential that we properly and completely introduce the concepts of class and object; this is an essential foundation. Moreover, it is worth remarking that object-based programming is a colossal advance over programming without ADTs. In spite of what is left out, by the time you have completed this chapter, you will know an awful lot about objects! We start with a very simple class, Cell, which reduces the class/object concept its bare essentials. Then we will proceed to a class Time, which is use to store an manipulate clock times. 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 ddle with the representation they interact with the variables only through the legitimate operations.

3.2
3.2.1

A Cell Class
Informal Specication of the Class

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. 25

Behaviour

How would we like a Cell object to behave?

Constructors Since Cell is to be used in a computer program we need to be able to declare, dene and create object of the class. We will dene a single constructor, the default constructor, which initializes objects to have a state 0. In addition, we provide an initializing constructor which overload the default constructor name. Inspector We should be able to obtain the state but only via an interface function. Mutator We should be able to modify the state but again only via an interface function. Input-output We require more for the purposes of demonstration than anything else facilities to convert a Cell object into a humanly readable format; for this we provide a toString() function.

3.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. //----- Cell0.cpp ------------------------------------// j.g.c. 12/2/98, 8/1/99, 2003/11/29 // minimal class, encapsulating an int value //---------------------------------------------------#include <iostream> using namespace std; class Cell{ public: Cell(){val_= 0;} void set(int val){val_= val;} int get(){return val_;} void print(){cout<<"value= " << val_<< endl;} private: int val_; }; int main() { Cell c; c.set(123); cout<< "Cell c: "<< endl; Cell* pc= &c; cout<< "Cell* pc = &c: "<< endl; //c.val_= 345; return 0; }

c.print();

pc->print();

// remove the first "//" and see if the program compiles

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.

26

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 v, but these data may be accessed only through the interface functions (methods). This is shown diagrammatically: +----------------------------------+ | private: (hidden data) | Public interface | | functions (methods) | | +---------+ int v; | ----->-| set() | | +---------+ | | | +---------+ | -----<-| get() | | +---------+ | + Cell(), toString() etc... | +----------------------------------+ 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.v = 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); Message to c: set your state to 123. 9. Notice that member data of the object itself can be accessed without any . operator. 10. In the client program, notice how Cell c denes an object same as dening a variable. Class type, object variable.

3.3

Using Separate Files

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 les: (a) the declaration of the class: Cell.h; this declares how to use the class; (b) the denition of the class: Cell.cpp; this denes the workings of the class. Cell.cpp can be compiled separately and the result stored in a library (as an object le). These les, and the new (separate) test program, are shown below. Notice how the declaration Cell.h must be #included in the .cpp les. 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 le, when the compiler sees Cell, it has no idea what you are talking about. 27

//----- Cell.h ------------------------------------// j.g.c. 12/2/98, 8/1/99, 2003/11/29 // minimal class, encapsulating an int value // .h file contains just declarations //---------------------------------------------------class Cell{ public: Cell(); void set(int val); int get() const; void print() const; private: int val_; }; //----- Cell.cpp ------------------------------------// j.g.c. 12/2/98, 8/1/99, 2003/11/29 // minimal class, encapsulating an int value //---------------------------------------------------#include <iostream> using namespace std; #include "Cell.h" Cell::Cell(){ val_= 0; } void Cell::set(int val){ val_= val; } int Cell::get() const{ return val_; } void Cell::print() const{ cout<<"value= " << val_<< endl; } //----- CellT.cpp ------------------------------------// j.g.c. 12/2/98, 8/1/99, 2003/11/29 // tests Cell class //---------------------------------------------------#include <iostream> using namespace std; #include "Cell.h" int main() { Cell c; c.set(123); cout<< "Cell c: "<< endl; Cell* pc= &c; cout<< "Cell* pc = &c: "<< endl; return 0; }

c.print();

pc->print();

28

3.4
3.4.1

A Coordinate Class
Informal Specication of the Class

We want to design a type capable of representing the coordinates that would describe the position of a point in two-dimensional computer graphics. We will call the class Coord as short for coordinate. In computer graphics you may be accustomed to referring to two-dimensional coordinates as (x, y): x, the horizontal axis, and y, the vertical axis. In the eld of image processing, it is more common to use column, row; in addition the coordinate pair is usually ordered dierently: (row, column). Here, because we will later use Coord in an image processing context we use row, column (r, c) terminology. The (row, column) image coordinate system is shown below. c=0 cMax +-----------------------+---> c r=0 | + im(1,1) + im(1,2) | | | + im(r,c) y | (r)| | rMax + + im(cMax, rMax) | Graphics Image Coordinate System We would expect a Coord object to be able to store the current state of a point, i.e. its r value, c value. How would we like a Coord object to behave? Constructors Since Coord is to be used in a computer program we need to be able to declare, dene and create Coord objects. We will dene two constructors: one, the default constructor, by which we dene an object which is initialised to the coordinate axis positions r = 0, c = 0. Inspector We should be able to obtain the individual coordinate axis positions. Inspector We should be able to compute the distance from another coordinate / point. Mutator We should be able to shift the coordinates along both axes we specify this as addition. Input-output We require more for the purposes of demonstration than anything else facilities to print a Coord object on the screen, and to read one from a keyboard.

3.4.2

Class Interface, Methods

The interface for the Coord class is shown below. //--- Coord.h -----------------------------------------------// j.g.c. 7/1/97, 4/3/97, 4/4/97, 2003/11/30 // simple co-ordinate class //-----------------------------------------------------------#ifndef COORDH #define COORDH #include <iostream> #include <math.h> //for dist() - sqrt() 29

class Coord{ public: //--- coordinate data abstraction --Coord(); Coord(double r, double c); double getR() const; double getC() const; void setR(double r); void setC(double c); //--- input-output -----------------void read(); void print() const; //--- operations --------------------double dist(const Coord& other) const; Coord& add(const Coord& other); Coord& sub(const Coord& other); private: double r_; double c_; }; #endif

Dissection of Coord.h 1. Public interface. First, we have declarations of 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, just as record members, as: instance.member e.g. p1.print(); where p1 is an instance of Coord 3. There are two constructors, both have the same name, which must be the class name; the name sharing is allowable due to function name overloading mentioned in chapter 7 functions may share the same name, as long as they are resolvable by their parameter list. 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 positions; these are private ; though the private representation is visible in the text, it is still encapsulated and invisible to client programs. 6. After previously specifying public, it is necessary to revoke this directive using private. Commonly, you will nd member data declared at the beginning of a class, and private left out this is possible because members are private by default. 7. private means that the members r_, c_ cannot be directly accessed by client programs. I.e. p1.r_ = 22;// illegal -- compiler error This is called encapsulation and provides information hiding. 8. Actually, we could also have avoided dening Coord() the system would have implicitly synthesised a similar constructor, only we would not have been guaranteed initialisation of r_=0, c_=0. 9. void print() const; Here the const means that print() guarantees to leave its calling object unaltered and the compiler will check.

30

3.4.3

Class implementation code

The implementation code for the Coord class is shown in Coord.cpp. //--- coord.cpp -----------------------------------------------// j.g.c. 7/1/97, 3/4/97, 4/4/97, 2003/11/30 // simple co-ordinate class //-----------------------------------------------------------#include "Coord.h" using namespace std; Coord::Coord() { //cout<< "default ctor\n"; r_=0; c_=0; } Coord::Coord(double r, double c) { //cout<< "initialising ctor\n"; r_ = r; c_ = c; }

double Coord::getR() const { return r_; } double Coord::getC() const { return c_; } void Coord::setR(double r) { r_ = r; } void Coord::setC(double c) { c_ = c; } void Coord::read() { cin>> r_; cin>> c_; } void Coord::print() const { cout<< "("<< r_<< ", "<< c_<< ")"; } double Coord::dist(const Coord& other) const { double dr = r_ - other.getR(); double dc = c_ - other.getC(); return sqrt(dr*dr + dc*dc);

31

} Coord& Coord::add(const Coord& other) { r_ += other.getR(); c_ += other.getC(); return *this; } Coord& Coord::sub(const Coord& other) { r_ -= other.getR(); c_ -= other.getC(); return *this; }

Dissection of Coord.cpp 1. The default constructor Coord() simply sets r, c to 0. 2. Notice that member data of the object itself can be accessed without any . operator. 3. For demonstration purposes, since constructors are called implicitly, we have included a message to signal that is has been called. This is doubly instructive, for constructors are often called when, naively, one might not expect. 4. The implementation of the initialising constructor Coord(double r, double c) should contain no surprises. Again, we include a distinguishing message. 5. Notice how, in dist, we access the x-coordinate using getR(): double dr = r_ - other.getR(); 6. We could have accessed it using other.r_, since other.r_ is visible here even though this code is being executed on behalf of another object. Whilst other.x_ would have been correct, it is always a good idea, where possible, to use the interface functions this limits the ripple eect of any change to the representation. 7. In this version of Coord, we have deliberately used pass-by-value to functions dist, addTo. Since passby-value entails making a copy of the passed object, we will nd that when larger objects are involved, it is usual to prefer pass-by-reference, which does not require copy.

3.4.4

A Client Program

The program CoordT.cpp demonstrates the use of the Coord class. //--- CoordT.cpp--------------------------------------------// j.g.c. 7/1/97, 30/12/97, 2003/11/30 // tester for Coord class //-------------------------------------------------------#include "Coord.h" using namespace std; int main() { Coord p1, p2(2,3), p3 = Coord(1,2), p4; p4= Coord(4,5); cout<< "Coord p1 = "; p1.print(); cout<< endl; cout<< "Coord p2 = "; p2.print(); cout<< endl; cout<< "Coord p3 = "; p3.print(); cout<< endl; 32

cout<< "Coord p4 = "; p4.print(); cout<< endl; cout<< "p3= p4 ... "; p3= p4; cout<< "Coord p3 = "; p3.print(); cout<< endl; cout<< "distance p2 to p3 = "; cout<< p2.dist(p3); cout<< endl; p4.add(p2); cout<< "p4.add(p2) ... "; cout<< "Coord p4 = "; p4.print(); cout<< endl; cout<< "p1.read(); ... r <space> c : "; p1.read(); cout<< "Coord p1 = "; p1.print(); cout<< endl; return 0; }

Dissection of CoordT.cpp 1. Denition of instances: Coord p1, p2(2,3), p3=Coord(1,2), p4; Note the syntax the same as dening native type variables. 2. The denition Coord p1 implicitly calls the default constructor, Coord() to create instance p1; likewise the denition of p3, p4. 3. The denition Coord p2(2,3) calls the initialising constructor Coord(double x, double y); and so does Coord p3=Coord(1,2). Both are simply dierent ways of calling the same constructor. 4. If you uncomment the cout lines in the constructors, the following lines: Coord p1, p2(2,3), p3=Coord(1,2), p4; p4=Coord(4,5); result in the following output, showing ve calls to constructors: default ctor initialising ctor: x, y = 2, 3 initialising ctor: x, y = 1, 2 default ctor initialising ctor: x, y = 4, 5 5. Generally, the syntax for calling a member function sending a message to an object is: object.method(argument), e.g. p4.addTo(p2); p4.print();

3.5

Exercises

You will nd the programs introduced earlier in the chapter in: http://homepage.ntlworld.com/jg.campbell/lyitoop/progs/.

33

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 conrm. 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. 1. Based on Cell, implement a class Pair which holds two state values one we will call key and has type char and the other value which has type double. If you need help, see Coord. Pair should have the following methods: (i) two constructors one default, the other initialising; (ii) void set(char k, double v); (iii) double get(char k); (iv) a print method; (v) I may have forgotten something add as necessary. Give separate Pair.h and Pair.cpp 2. Write a small test program for Pair PairT.cpp. 3. Write a small test program for Coord CoordT1.cpp as follows: (i) declare four Coords: a, b, c, d; (ii) Set a to (0, 0); b to (3, 4); c to (1, 3); and d to (10, 10); (iii) Add b to c (c.add(b); and print the result; (iv) Compute the distance from a to b and print the result; is it correct? It should be 5.0. (v) Subtract b from c (c.sub(b); and print the result. Did you get back to what c was originally? 4. Develop a class Marks which will be capable of holding student marks (coursework and examination) along the following lines: //-------------------------------------------------------// Marks.h - StudentInfo project // j.g.c. 2003/02/20 //-------------------------------------------------------#ifndef MARKSH #define MARKSH #include <string> #include <iostream> #include <cstdio> // for toString/sprintf using namespace std; class Marks{ public: Marks(int ex, int ca); Marks(); int overall() const; void print() const; private: int ex_; int ca_; }; #endif 34

Method overall should compute the overall mark as follows: double x = ex_*0.7 + ca_*0.3; return (int)x; 5. Write a small test program for Marks (MarksT.cpp) which declares two Marks objects and initialises them with (exam 40, ca 60), (exam 80, ca 50); print them, and compute and print the overall marks for each. 6. (i) Add a method to Marks (bool isPass()) which tests whether a Marks object is pass or not (pass is overall mark >= 40.0); (ii) Include another Marks object in MarksT.cpp initialised to (exam 25, ca 40); print out all three objects along with pass/fail indication.

35

Chapter 4

Variables, Values, Scope, Lifetime


4.1
4.1.1

Types, Variables, Values


Introduction

The purpose of this section is to bring some order and formality to the notions of type, variable and value in programming languages. For the purposes of a course on object-oriented programming, we can make the following close analogies: Nearly all that we say about types is relevant to classes. Likewise, variables are closely linked to objects. And, values to the state held by an object. Finally, type operations are closely analogous to class interface functions. Indeed, object-oriented programming usefully can be seen as programming types, and classes simply as an enrichment of the native type system. When we develop a class, we want to be able to use it in client programs with all the freedom that we use a native type. When you have studied this chapter you should be familiar with the concept of a data type, know why a type system is useful, be able to distinguish between value and variable, be aware of the constituents of a variable, know the signicance of static & dynamic typing, and be aware of potential problems associated with aliases, dangling references, and garbage, and know how to guard against these. In addition, you will be able to connect the concepts of types, variables, values, etc. with classes, objects, object state, etc.

4.1.2

Data Types

A data type, or simply type, is made up of two constituents: 1. A set of values, e.g. boolean: {false, true}, byte: {-128..127}

2. The set of operations or functions that can be applied to the members of the set, e.g. for int, +, -, *, /, etc.

36

Since assembler programmers get away without types (or just one or two types e.g.. byte and word). What are the advantages conferred by including a type system? 1. Invisibility of the underlying internal representation. That is, abstraction away from (for example) the fact that a float is represented as four contiguous bytes. This promotes: Easier readability. Easier write-ability: the programmer can think about float numbers rather than having to think about numbers one moment and having to switch to bits and bytes the next. Portability and modiability: if the representation changes, application programs will not have to be changed. 2. Correct use of variables can be checked: by the compiler if static typing, by the run-time system if dynamic typing. 3. Disambiguation of operators and functions: again, if static typing, at compile time, or if dynamic typing, at run-time.

4.1.3

Values

A value is an element of the set associated with a type. E.g. 3 is a value from the set .., -1 ,0, 1, 2, 3, .. associated with the type int in Java.

Characteristics of values In order to contrast values with variables we note the following: Values do not have lifetime. They can neither be created nor destroyed; no concept of state can be associated with them; the are not mutable. Values are abstractions. The value 2 describes the general concept of twoness of all pairs of anything. Values they are read-only ; i = 3; is O.K., but not 3 = 4;! Values exhibit referential transparency, i.e. there is no danger that the value 5 will change depending on when, or how it is referenced in an expression; int i, j; i = 5; will always assign 5 to i; however, i = j; depends on the history of j.

4.1.4

Variables

In traditional imperative programming languages, a variable has associated with it: An identier or name, e.g. a in int a;. A type, e.g. int. A location the address in memory where it is stored. A value.

Static Typing Usually, an identier is statically bound to a type, i.e. the type of the variable is known at compile time, and is xed thereafter.

Dynamic Typing However, in some languages, the binding of type is dynamic, in which case the type may change according to the last assignment, e.g. APL, Smalltalk, Objective-C, and to a limited extent, C++ and Java, as we shall see later. 37

4.1.5

L-Values and Values

In imperative languages, when a variable must be referenced, its identier gets translated into a location (address), i.e. the program must read from or write to the associated memory cell. There is a subtle dierence, however, whether the variable is on the receiving end, or on the delivering end of an assignment. The dierence lies in the semantics of the expressions, for example, a, and b in: b = a; // C++ or, in general, <expression1> <assignment operator> <expression2> in any procedural language. The dierence is that expression1 must evaluate to a location / address, whilst expression2 evaluates to a value. In C/C++ it has become usual to use the term, L-value from Lefthand-value, for a location/address. Sometimes, but less frequently, the term R-value from righthand-value is used for a true value. Example. In C++, int a = 25, b; b = a; /* e.g.. a is at address 1000, and 1000 contains 25, b is at 1001 */ translates to: get the value contained in cell address 1000, [1000] in some notations, put that value in 1001 (b); i.e. the expression a on the right-hand side translates to a value (25), whilst the expression b on the left-hand side evaluates to an address (1000). In the case of pointer variables, the values are themselves addresses.

4.1.6

Aliases

Two variables are aliases if they share the same memory / data object. An example, in C++: Cell c1 = Cell c2 = Cell *pa, pa= &c1; Cell(456); Cell(123); *pb; // pointers to Cell pb= &c2;

Cell reference/pointer Cell object +---------+ +---------+ | pa +-------------->| c1 456 | +---------+ +---------+ +---------+ +---------+ | pb +-------------->| c2 123 | +---------+ +---------+ pa= pb; Now c1, c2 refer to (point to) the same object and the Cell object with 456 in it becomes garbage. 38

+---------+ +---------+ | pa +--------+ | c1 456 | +---------+ | +---------+ | +---------+ +----->+---------+ | pb +-------------->| c2 123 | +---------+ +---------+ From now on, anything you do to pa you (eectively) do to pb.

4.1.7

Dangling References

In C and C++ it is possible for references to become dangling : int *pa,x; { int a; /*local to this block!*/ a=22; pa=&a; /*pa points at a*/ } /*a is now destroyed, and pa is DANGLING*/ x = *pa; /* x <- rubbish*/ *pa= 22; /*22 gets written to where a was - WORSE*/

4.1.8

Uninitialized Pointers

Uninitialized pointers closely resemble dangling pointers. They are a big problem in C/C++. Example in C++: Cell *pa; pa is a pointer to a Cell object we havent indicated any object yet. In fact, Cell *pa; will contain a null reference. In C/C++, it may even be possible that pa could have contained a random reference/address, and any attempt to write to it could write to some part of memory not intended like part of the program!

4.1.9

Garbage

Garbage is the opposite problem to dangling pointers; a dangling pointer points at something that eectively doesnt exist, garbage is memory location(s) that has been allocated, but whose reference has been destroyed, therefore it has no name and is eectively lost. Example in C++: int fred(int a, int b) //not a very useful function { int* pi; int i; pi = new int; *pi = a + b; i = *pi; //should have delete pi here! return i; } 39

as soon as the function returns, pi is destroyed; but the piece of heap memory that new created remains allocated and cannot ever again be referenced, even to deallocate/free (delete) it. On the other hand, in languages like Eiel and Java, that oer garbage collection, garbage is never a problem when an object becomes stranded, the garbage collector can detect this, and the memory that was used can be recycled/deallocated automatically.

4.2

Scope of variables

The scope or visibility 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 9.12. The scope of automatic / local variables dened in a function or block is from the denition 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 dierent source les, or in dierent functions or blocks, are unrelated. Example. int fred() { int x,y,z,a,b; /* (1)*/ x=1;y=1;a=32;b=33; z=add(x,y); return z; } int add(int a, int b) { int value; value=a+b; return value; }

| |scope of x, y ..a, b | | | |

| | scope of a, b, value | | these a,b NOT related to above a,b |

Blocks are scope units Rather 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; }

40

Scope Hiding Consider: void fred(int n, float b) { int c=33,d=16,i; for(i=0;i<=n;i++){ float d; // note these d, c are different from outer d=22.5; int c=49; } // here c==33, d==16. } The outer d the one declared rst is hidden in the block governed by the for, by the inner declaration.

4.3

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 developed a class Cell which is declared using #include "Cell.h". Let us assume that Cell is a common class name (e.g. like string) and that there is a Cell class in the C++ standard library. How do we resolve the nameclash? Even better, how can we use the two together. The way around it is to declare our own Cell within its own namespace: namespace lyitdip{ class Cell{ public: Cell(); void put(int val); int get() const; void print() const; private: int val_; }; } // end of namespace In addition, we note that all standard library stu is declared under namespace std. Now, we can use the two Cells together and use the scope resolution operator :: to discriminate between them: #include <cell> #include "Cell.h" std::Cell c1; // standard library Cell lyitcourse::Cell c2; // our own Cell The use of the scope resolution operator :: may appear clumsy, so, in cases where we are not troubled with name conicts, we can use the declaration using namespace xyz in the user program. Note our use of using namespace std earlier. using namespace lyitdip; // now no need for lyitdip::Cell, just Cell will do. 41

4.4
4.4.1

Heap memory management


Introduction

If you deal only with types like int, char, double (but not String or arrays which are object, you can become used to memory management being done automatically. Thus: int fred(int a) { int b; //here b created automatically b= a*10+2; return b; //here the value of b is returned, and b deleted (destroyed) } 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. Unlike stack variables, which are created and destroyed automatically, heap the memory management (creation) of heap variables must be programmed. (In C, C++, which have no garbage collection you must also be very careful about deletion of heap objects). Heap variables are created using new.

4.4.2

Garbage

Garbage refers to the situation of heap memory, whose reference has been deleted or made to refer to elsewhere. Once this reference is lost, the heap memory may never again be accessed by the user program. Thus, garbage is in some ways the opposite to dangling or uninitialized 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 uninitialized variables. And, the phrase garbage-in garbage-out refers to an entirely dierent notion.

Java In Java, all non-elementary variables (all objects and arrays well, 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 rst 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.

4.5

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 nd confusion between scope and lifetime though they are in cases related, they are entirely dierent notions: lifetime is to do with a period of time during the execution of a program, scope is to do with 42

which parts of a program text. In Java, 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 denition; lifetime is the whole time that control is in that part of the program from the denition to the end of the block / function. Example, local variables. int fred(int a, int b) { int c; /*when control passes into fred: local variables a,b, c are created at this time -- their LIFETIME starts then*/ c= 22; ... }

/*when control reaches here, locals are destroyed*/

. 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.

4.5.1

Lifetime of variables summary

It is possible to summarize 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 denition to exit from their block / function. Heap variables Their lifetime is from allocation to until deallocation which, in Java, is done by the garbage collector only when there is no reference whatsoever pointing to the object. Variables held in les Their lifetime is over many program executions; they are persistent. Persistence of objects is of signicant interest for database applications object-oriented databases.

43

Chapter 5

Inheritance
5.1 Introduction

Using inheritance, a derived class can be dened as an extension of a base class. The inheriting derived class inherits all the behaviour and state memory of the base class. In addition, it is free to add its own member data and functions so-called extensions or specialisations. Note. The use of specialisation instead of extension has potential for confusion. The same goes for the use of the terms superclass and subclass for base class, and derived class, respectively. Thus, we will attempt to standardise on the terminology: base class and derived class, where the derived class is the extended (inheriting) one; the derived class inherits from the base class. As well as adding member functions and data, a derived class may overload base functions and operators to make them appropriate for itself. An example is the overloading of a print function, that is capable of printing any extra elds introduced in the derived class. However, the real power of inheritance comes from polymorphism and run-time binding via virtual functions. Run-time binding allows a class (hierarchy) to be extended after the program which uses it has been written leading to the comment that: old code can call new code ; this refers to the reversal of the traditional roles, when, generally new calling programs are based on (calling of) old library code. The situation also leads to the description upside-down libraries, see chapter 1. Another view of the same property is expressed in the open-closed principle of object-oriented programming: to be useful to client programmers, a class must be closed to modication; yet, using inheritance together with run-time binding, the class is also open to extension. In this chapter we cover nearly everything twice once from the point of view of the Cell class hierarchy, and once from the point of view of the Person class hierarchy. The repetition is intentional: the concepts here are important, and crucial to an understanding of the theory and practice of OOP.

5.2

Example 1: Class Cell extended via inheritance

We will approach the solution in three stages; rst, we show inheritance, but without virtual functions; next we demonstrate protected as private but with public to deriving classes; nally, we introduce virtual functions and the ll eect of dynamic-binding and polymorphism.

44

5.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 is comprised of the following: Constructor This initialises the backup value, and, via the base (Cell) constructor, the Cell part, i.e. val_. Overloaded set() This rst backs-up the current value. Then is uses the Cell version of set(). Overloaded print() print() is also overloaded. :: qualier Cell::set(val) insists that set from the base class Cell is to be used. Additional behaviour A function restore is provided. Additional state int backup_ stores the backup state. That is nearly it. 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. //----- CellR1.cpp ------------------------------------// inheritance etc. -- first version; // see CellR2 etc for modifications // j.g.c. 12/2/98, 8/1/99, 2003/12/02 // based on Abadi & Cardelli, 1996, Th. of Objects, Springer. //---------------------------------------------------#include <iostream> using namespace std; class Cell{ public: Cell() : val_(0){} void set(int val){val_= val;} int get() const {return val_;} void print() const {cout<<"value= " << val_<< endl;} private: int val_; }; class ReCell: public Cell{ public: ReCell() : Cell(), backup_(0){}; void set(int val){backup_= get(); Cell::set(val);} void restore(){set(backup_);} void print() const {cout<< "value= " << get()<< " backup= " <<backup_ << endl;} private: int backup_; }; int main() 45

{ Cell c; c.set(123); cout<< "Cell c: "<< endl; c.print();

Cell* pc= &c; cout<< "Cell* pc = &c: "<< endl; pc->print(); ReCell rc; rc.set(456); rc.set(789); cout<< "ReCell rc: "<< endl; rc.print(); rc.restore(); cout<< "rc.restore() "<< endl; rc.print(); cout<< endl; cout<< "slicing: c= rc "<< endl; c= rc; c.print(); cout<< "pc= & rc "<< endl; pc= &rc; pc->print(); cout<< endl; return 0; }

Dissection of CellR1.cpp 1. Firstly, and most importantly, note that class Cell is unchanged. 2. ReCell contains an additional eld backup_. This means that a ReCell object has two elds: val_ and backup_. 3. c= rc; is a legal assignment, even though they are dened as dierent types. This is because, owing to the inheritance rc is-a Cell in addition to being a ReCell. 4. Slicing. However, 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). However, in pc->print(); it may be thought that slicing has taken place; this is because the static type of pc is Cell* and, consequently, it is Cell::print() that is used. In the third version of ReCell we will see how virtual functions can rectify the matter. virtual means capable of dynamic/run-time binding . Normally, functions are statically bound (linked) at compile-time. 6. Inheritance is announced by class ReCell: public Cell. This is public inheritance; private inheritance is possible, but is not at all common. 7. 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.

5.2.2

protected

Program CellR2.cpp is the same as CellR1.cpp with the simple modication that Cells val_ eld 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_. 46

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, eciency and other concerns make make it unavoidable. //----- Cellr2.cpp ------------------------------------// from CellR1 -- using protected // j.g.c. 12/2/98, 8/1/99, 2003/12/02 //---------------------------------------------------#include <iostream> using namespace std; class Cell{ public: Cell() : val_(0) {} void set(int val){val_= val;} int get() const {return val_;} void print() const {cout<<"value= " << val_<< endl;} protected: int val_; // using protected, derived classes have access }; class ReCell: public Cell{ public: ReCell() : Cell(), backup_(0) {} void set(int val){backup_= val_; val_= val;} void restore(){val_= backup_;} void print() const {cout<< "value= " << val_<< " backup= " <<backup_ << endl;} private: int backup_; }; int main() { Cell c; c.set(123); cout<< "Cell c: "<< endl; c.print(); ReCell rc; rc.set(456); rc.set(789); cout<< "ReCell rc: "<< endl; rc.print(); return 0; }

5.2.3

Virtual Functions and Dynamic Binding

Finally, in CellR3.cpp we get to the complete ReCell. Already, in section 5.2.1, 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.

47

//----- CellR3.cpp ------------------------------------// inheritance etc. -- intro. virtual functions // j.g.c. 12/2/98, 8/1/99, 2003/12/03 // from CellR1.cpp //---------------------------------------------------#include <iostream> using namespace std; class Cell{ public: Cell() : val_(0) {} virtual void set(int val){val_= val;} int get() const {return val_;} void print() const {cout<<"value= " << val_<< endl;} virtual void printV() const {cout<<"V: value= " << val_<< endl;} private: int val_; }; class ReCell: public Cell{ public: ReCell() : Cell(), backup_(0) {} virtual void set(int val){backup_= get(); Cell::set(val);} void restore(){set(backup_);} void print() const {cout<< "value= " << get()<< " backup= " <<backup_ << endl;} virtual void printV() const {cout<< "V: value= " << get()<< " backup= " <<backup_ << endl;} private: int backup_; }; int main() { Cell c; c.set(123); cout<< "Cell c, c.print() "<< endl; c.print(); cout<< "Cell c, c.printV() "<< endl; c.printV(); cout<< endl; Cell* pc= &c; cout<< "Cell* pc = &c, pc->print() "<< endl; pc->print(); cout<< "Cell* pc = &c, pc->printV() "<< endl; pc->printV(); cout<< endl; ReCell rc; rc.set(456); rc.set(789); cout<< "ReCell rc:, rc.print() "<< endl; rc.print(); cout<< "ReCell rc:, rc.printV() "<< endl; rc.printV(); cout<< endl; c= rc; cout<< "c= rc, c.print() "<< endl; c.print(); cout<< "c= rc, c.printV() "<< endl; c.printV(); cout<< endl; pc= &rc; cout<< "pc = &rc, pc->print() "<< endl; pc->print();

cout<< endl<< "...finally, we see the effect of *virtual* ..."<< endl; cout<< "pc = &rc, pc->printV() "<< endl; pc->printV(); cout<< endl; cout<< "Cell& cc= rc; cc.printV() ..."<< endl; Cell& cc= rc; cc.printV(); return 0; }

Dissection

48

1. First, notice the use of the virtual qualier in the declaration of set and print. Note again: virtual means capable of dynamic/run-time binding . Normally, functions are statically bound (linked) at compile-time. 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 5.2.1, pc takes on the dynamic type pointer-to-ReCell. Still, in the following, pc= &rc; cout<< "pc = &rc, pc->print() "<< endl; pc->print();

Cell::print() is statically bound (compile-time) to pc, whose static type is pointer-to-Cell, and the following is output: pc = &rc, pc->print() value= 789 4. But, in the following, we nally see the eect 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(). This is called dynamic binding.

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 dierent 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 to objects and references to objects; 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 functions must have been declared virtual. 49

5.3

Dynamic Typing, Run-time Binding and Polymorphism

Dynamic typing refers to the property that a variable can adopt a type according to run-time demands; in C++ dynamic typing is restricted to pointers and references to a base class. It means that a base class pointer or reference can be made to reference a derived class object. Thus, the type of the base class pointer is eectively dynamic it may change type during run-time, to the type of a pointer assigned to it. An immediate consequence of dynamic typing is that the familiar and usual compile time type checking and function binding (compile time binding = early binding = static binding ) is no longer possible. Nevertheless, see section 5.5, this does not mean that we have lost strong typing despite the fact that some type binding must be postponed to run time. The term polymorphism, Greek many forms is used in connection with issues connected to dynamic binding; however, if used without care, we have found that the term causes confusion mainly because there are dierent types of polymorphism; hence, we will leave a discussion of it until later, section 5.13. As we have already admitted, C++ dynamic typing is rather watered down: it merely allows a base class pointer or reference to reference base class objects or any objects of any class derived from that base. At this point it is worth remarking that Smalltalk, and Objective-C take dynamic typing much further. In these languages: 1. every class is derived from a fundamental Object class; 2. all object variables are actually references (pointers if you like). Hence, every object may be dynamically typed.

5.4

Overriding (virtual functions) versus Overloading

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 dierence between overriding and overloading is that: The binding / selection of an overridden function is done at run-time. The overloaded is selected at compile time.

5.5

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. 50

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 / subtype system provided by the class hierarchy respects the Liskov Substitution Principle, (Martin 1996): if function f() is dened for a parameter of class B, and given class D (subtype), derived from B, then calls to f(), with an argument of class D, should work properly.

5.6

Inheritance & Virtual Functions Summary

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. 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.

5.7

Inheritance versus Inclusion, is-A versus has-a

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 is the normal manner in which a record may be composed of elds containing simpler types (including other records). 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 Employee are derived from (inherit from) Person, it is clear that an Student object certainly does qualify for is-a Person. However, please note that the is-a relationship is not transitive ; a Person need not necessarily be an Student nor Employee. 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 classes Name and Address is has-a : a Person object has-a Name and has-a n Address. Given our strict interpretation of has-a and is-a, on no account could we say that a Person is-a Name.

51

5.8

Reuse the Real Power of Inheritance & Virtual Functions

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. Without virtual functions and overriding, you would have to, in the main() program, predict all the dierent operations that would ever be required (impossible) and provide them in a giant case statement. With inheritance and virtual functions / dynamic binding, 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 xed, while the called functions may be modied. And, we recall the open-closed principle : software modules (here classes) must be open to extension, but must be closed to modication. We will see this in action in a later chapter.

5.9

Example 2: a Person class hierarchy

This example may appear more real-world, but when we get to implementing it, we nd repetition of precisely the same principles that we covered in the previous section. If you nd this class hierarchy large and complicated for starters, return to the Cell, ReCell classes and have another look at those. There is nothing here about OOP that isnt in those smaller examples except perhaps for a hint of real application. Suppose an application program deals with Person data; there are various categories of Person, Students, Employees, etc. Inheritance allows a derived / extended) class, e.g. Student, to be dened in terms of the base Person class. With overloaded interface functions, client programs can treat objects of the two classes in a uniform way. Moreover, as mentioned in the introduction, with careful use of virtual interface functions which allow run-rime binding, and dynamic typing / polymorphism, classes may be extended without aecting the calling program. Again, by appropriate use of virtual functions and polymorphism we can develop software which never has to make an explicit choice whether it is dealing with a Person object, or a Student object, or, for that matter, any of the descendants of Person. Thus, there is no need for a selection statement, e.g. switch, switch(type){ case(Person): person code; case(Student): student code; etc. } This has led to the aphorism: Switch statement considered harmful, as a slogan for object-oriented programming, much as Dijkstras GOTO considered harmful championed structured programming in the early 1970s.

52

5.10

A Person Class

File Person.h shows a Person class which, with some derived classes, demonstrates inheritance. It has been deliberately kept simple, and thus, other than as a demonstration of inheritance, it is rather limited and unremarkable. //-------------------------------------------------------// Person.h - Person class // j.g.c. 12/4/99, 3/5/99, 2003/12/03 //-------------------------------------------------------#ifndef PERSONH #define PERSONH #include <string> #include <iostream> #include "Name.h" #include "Address.h" class Person{ public: Person(Name n, Address a); Person(); void read(); virtual void print(); void printX(); virtual ~Person(); virtual int compare(const Person& other); private: Name n_; Address a_; }; #endif

Dissection of Person.h 1. There are two constructors, one default, the other initialising; 2. Person is composed of a Name and Address. This may appear more complicated than necessary; however, if we hadnt decomposed (broken down) Person like this, it would have started to become overly complicated; as it is, Name and Address and small and simple and easy to write and test. Then, in turn, once Name and Address are created and tested, Person, too, becomes rather small and simple; 3. In the declaration of print() we have used the keyword virtual; this is highly signicant since it releases the possibility of dynamic binding of print(); 4. We have deliberately included another function printX() which is non-virtual (hence printX), whose static binding we can contrast with the dynamic binding of print(). We will refrain from describing the implementation of Person, for there is nothing novel in it: the implementation of a virtual function does not dier from that of an equivalent non-virtual function; the only dierence is in the use of virtual in the declaration.

5.11

A Student Class

File Student.h shows an Student class which is derived from Person it inherits from Person.

53

//----- Student.h -------------------------------------// j.g.c. 28/4/99, 2003/12/02 //------------------------------------------------------#ifndef STUDENTH #define STUDENTH #include "Person.h" class Student : public Person{ public: Student(Person p, std::string course, int id); Student(Name n, Address a, std::string course, int id); Student(); void read(); virtual void print(); virtual ~Student(); private: std::string course_; int id_; }; #endif

Dissection of Student.h 1. class Student : public Person{... signies that Student inherits from Person. For the purposes of this course, it suces to say that public inheritance is the only useful form of inheritance; 2. Student exhibits the same behaviour as Person; this uniformity of behaviour is a typical feature of derived classes; since an Student is-a Person, they should have uniform behaviour. This uniformity of behaviour is a typical feature of classes related by inheritance; 3. There are two print() functions one virtual, the other not virtual, (printX); as before, printX is there just for illustrative purposes; 4. As well as id Student, via inheritance, contains in addition all the data members of Person; 5. We will see later that the implementation of print() for Student has been specialised to recognise the extensions that Student has. Likewise, printX(); 6. As a consequence of public inheritance, only the public parts of Person are visible within Student.

5.12

Use of the Person Hierarchy

File PersonT5.cpp shows a program which uses the Person class hierarchy. //------ PersonT5.cpp -------------------------// tests for Person class hierarchy // j.g.c. 12/4/99, 2003/12/03 //------------------------------------------------#include <iostream> #include <string> #include "Person.h" #include "Student.h" #include "Employee.h" #include "PersonDb.h" using namespace std; int main() { Person *pp1= new Person(Name("Ms.", "Lotfi", "Zadeh"), Address("USC", "Berkeley", "USA"));

54

Person *pp2= new Person(Name("Mr.", "Joe", "Bloggs"), Address("4 July St.", "Los Angeles", "USA")); Student *ps1= new Student(Name("Ms.", "Julie", "Jones"), Address("40 Fourth St.", "Reno", "USA"), "Science", 1234); Employee *pe1= new Employee(Name("Mr.", "Vincent", "Murphy"), Address("3 Gortlee Cres.", "Raphoe", "Ireland"), string("Sales")); cout<< "\nNow demonstrating virtual and non-virtual methods\n\n"; Person *pp = pp1; pp->print(); pp->printX(); pp = pp2; pp->print();

pp->printX();

cout<< "\n Notice in the next two the difference between\n"; cout<< "virtual and non-virtual methods\n\n"; pp = ps1; cout<< "----- virtual print ----\n"; pp->print(); cout<< "----- non-virtual print (notice parts sliced off) ----\n"; pp->printX(); pp = pe1; cout<< "----- virtual print ----\n"; pp->print(); cout<< "----- non-virtual print (notice parts sliced off) ----\n"; pp->printX(); // PersonDb demonstrates that it does not have to be changed // even if we add another class to the Person hierarchy PersonDb db; db.put(pp1); db.put(pp2); db.put(ps1); db.put(pe1); cout<<"\n\n -- Original database --"<< endl; db.print(); cout<< endl; cout<<" -- Sorted --"<< endl; db.sort(); db.print(); cout<< endl; cout<<" -- Second removed --"<< endl; db.remove(1); db.print(); cout<< endl; cout<<" -- Sorted again --"<< endl; db.sort(); db.print(); cout<< endl; return 0; }

Dissection of PersonT5.cpp

55

1. The following creates a Person object on the heap (new) and makes pp1 point to it; Person* pp1= new Person(Name("Ms.", "Lotfi", "Zadeh"), Address("USC", "Berkeley", "USA")); 2. The following declares a pointer-to-Person and makes it point to the rst Person object and then the second; Person *pp = pp1; pp->print(); pp->printX(); pp = pp2; pp->print(); The output is: Ms. USC Ms. USC Lotfi Zadeh Berkeley USA Lotfi Zadeh Berkeley USA

pp->printX();

Mr. Joe Bloggs 4 July St. Los Angeles USA Mr. Joe Bloggs 4 July St. Los Angeles USA In this case, print and printX are equivalent. 3. The following makes pp point to a Student object and uses both the virtual print and the non-virtual printX to print it. cout<< "\n Notice in the next two the difference between\n"; cout<< "virtual and non-virtual methods\n\n"; pp = ps1; cout<< "----- virtual print ----\n"; pp->print(); cout<< "----- non-virtual print (notice parts sliced off) ----\n"; pp->printX(); Output from print(): Ms. Julie Jones 40 Fourth St. Reno USA Course: Science Id: 1234 Because print() is virtual, it takes into account the dynamic type of pp. The static type pp is pointerto-Person and in the earlier code, the dynamic type is the same, pointer-to-Person. However, when the assignment, pp = ps1; takes place, the dynamic type changes to pointer-to-Student. print() uses run time binding (dynamic binding ) to call the appropriate print. Output from printX(): Ms. Julie Jones 40 Fourth St. Reno USA Because printX() is not virtual, it ignored the dynamic type of pp and uses compile time (static ) binding. 4. The reminder of the program and PersonDb are there to demonstrate that PersonDb does not have to be changed if we add another class to the Person hierarchy. We can discuss this in more detail. 56

5.13

Polymorphism

In programming languages, there exist dierent forms of polymorphism. Unless we are careful, these may lead to confusion. A seminal paper, (Cardelli & Wegner 1985), identied and named and characterised these. Ad Hoc Polymorphism Here, and in a previous chapter, we have dealt with overloading of function names, e.g. int add(int a, int b){return a + b;} float add(float x, float y){return x + y;} This is called ad hoc polymorphism. Inclusion Polymorphism In the examples above, we have shown that base class pointers can take on the type of derived class pointers they are polymorphic variables. Moreover, an overridden virtual function is capable of providing polymorphic behaviour appropriate to the type that the variable has when the virtual function is invoked. Both these, the polymorphism of the pointer variable, and the polymorphism of the function, are called inclusion polymorphism, because a subtype (extended / derived) is (included as) a base (super) type. Parametric Polymorphism In chapter 16 (another course), we introduce parametric polymorphism in the form of templates. We will show how to develop a generic Array where the element type is a parameter. Also, we will develop Stack, List, etc. . . . container classes whose element type is given as a parameter. E.g. Array <int> ai; Array <double> ad; We can also, again via templates, dene parametrically polymorphic functions. For example, the add function above dened using a type parameter: template <class T> T add(T a, T b) { return a + b; } int main() { int i = 10, j = 20, k; float x = 1.5, y 3.5, z; k = add(i, j); // <int> add called z = add(x, y); // <float> add called //...

57

Chapter 6

Tutorial Introduction to C++


This chapter is based on the rst chapter of Kernighan & Ritchies book on C (Kernighan & Ritchie 1988). 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 rst learn C. It is fairly clear that this is not ideal, (Stroustrup 1997b, ?). Indeed, there is reason to believe that object-oriented programming can be quite dicult for experience imperative language programmers to grasp. Moreover, there are dicult 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.

6.1

Get Started

Objectives Get used to editor, compiler, linker etc. Get a simple C++ program running. Dissect this simple C++ program. All the programs in this course and other bits & pieces will be in some network accessible directory, probably (this information is repeated here for convenience for correct URLs etc. see chapter 1): ~jgc/public_html/oop/notes/ ~jgc/public_html/oop/progs/chN programs associated with chapter N. ~jgc/public_html/oop/pract/wkX practical work for week X. ~jgc/public_html/oop/t/ old tests and exams. ~jgc/public_html/oop/ta/ answers for tests and exams where available. Look at your weekly practical sheets for up-to-date information. These are all available on the web at http://www.infm.ulst.ac.uk/~jgc/oop/

58

6.1.1

Your First C++ Program

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 an old style C comment*/ int main() { cout << "Hello from C++.\n"; cout << "Hello from C++." << endl; return 0; }

Dissection of hello.cpp 1. Every program must have a function called main, execution starts there. main may call other functions. As with Modula-2 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 le as main. As written by the programmer in separate le(s). Called from predened libraries. 2. C++ does not have Modules (Modula-2), or Programs (Pascal), just functions and classes. 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 C style comment. 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 dicult to trace. 5. // is the C++ comment symbol; comment ends at end of line. 6. iostream is the header for the hierarchy of stream classes which handle buered and unbuered IO for les and devices. cout is an object, corresponding to the standard output stream. 7. The operator << passes the message put the value that follows it (the string) to the object cout. 8. Pronounce << as put to. 9. The two output lines are equivalent, except that endl ensures that the stream buer is ushed. 10. Default extension for C++ program source les is .cpp. 11. Header les, e.g. iostream contain function declarations and the like. 12. #include <filename> simply inserts the contents of lename instead of the include preprocessor statement. 13. <filename> tells the preprocessor to search for lename in a system directory. 14. "filename" tells the preprocessor to search in the current directory, where this source le is. 15. The preprocessor acts on the source le before the compiler. 16. Typically, one has a xxx.h le which contains the declarations for the xxx.cpp le, which contains the executable code (implementation).

59

17. Preprocessor includes and the relationship between the .h and the .cpp are much more informal than the relationship between .def and .mod in Modula-2. 18. The xxx.h le is included. It is not compiled separately, though, obviously, it is compiled as part of the program into which it is included. 19. The xxx.cpp le is compiled, and its object code linked. 20. For your own sanity the heading comment should include: Name of the program to correspond to the lename, Authors name even if copied! Date. Brief indication of function and purpose of the program, e.g. assignment number, or project, and what it does. 21. Program layout is very important; I suggest two spaces indentation for each block; I suggest that you avoid tabs certainly avoid having the program pressed up against the right margin, with all the white space on the left hand side, 22. In the denition of a function, () encloses the argument list; in this case main expects no arguments; (void) denotes takes no arguments, or, as mentioned, empty brackets, () signify the same thing.. 23. However, empty parenthesis () is taken to signify the same thing. Beware, not the case in C!. 24. {...} enclose the statements in a function or block same as BEGIN ... END in Modula-2, 25. \n represents a single new-line character. 26. \ is an escape character announces a control character. 27. Other control characters are: \t tab. \a alert beep, bell. \" double-quote. \r carriage-return. \\ backslash itself. 28. return 0; As mentioned above, in C++, programs can return values to the operating system; in UNIX, 0 signies that the program terminated normally; other values indicate error conditions. This is useful if you intend putting your program in a UNIX shell script; likewise DOS, Windows .bat les. Exercise 1. This may look trivial and boring, but you must complete it before you proceed; doing so will advance you signicantly far up the C++ learning-curve. In fact, when you need to do C programming on an unfamiliar system, it is always a good idea to get hello.cpp working rst just to make sure that you and the system are operating on the same wavelength! Linux version: Type in the program hello.cpp; if you are familiar with the Wordstar -style editor in BorlandC, or Topspeed, you may nd the joe editor easy to use. Compile it: g++ -c hello.cpp -pedantic-errors -Wall (o)bject code goes to hello.o g++ is the GNU C++ compiler.

60

-pedantic-errors tells the compiler to be very strict. Use this get the compiler to do most of your debugging for you!! Link it: g++ -o exe hello.o ^ file exe is the executable Used as a linker, g++ really invokes something called ld, but thats no real concern of ours. Or, compile & link together: gcc -o hello hello.c ^ source file ^ output to file hello If you fail to specify -o hello the output goes to a default le a.out this is a UNIX convention.

Safety and Warnings As with most compilers, g++ issues both error messages and warning messages. Error signies a serious aw 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 dened but never used (fairly benign), type conversions that look a bit aky, 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++.

6.2

Variables and Arithmetic

Program ptog1.cpp shows some simple arithmetic, involving a function, and a while loop. //----- ptog1.cpp ---------------------------//Convert pounds to grams j.g.c. 1/2/97 //------------------------------------------#include <iostream> const float ptog = 453.592; float convert(float p) { return p*ptog; } int main() { int pds; cout << "Input pounds: "<< endl; cin >> pds; while(pds>0){ float k,g; int gint; g=convert(pds); gint=int(g); // could have used simply gint=g; cout << "\nWeight = "<< gint << " grams."<< endl; 61

k=g/1000; cout << "\nWeight = "<< k << " Kg."<< endl; cout << "Input pounds: "<< endl; } return 0; } cin >> pds;

Dissection of ptog1.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 avoid duplication of eort 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 (as in Linux). float: oating point, normally IEEE format, 32 bits. char: single text character; really it is a small integer taking on values in [0..255], and, unlike Modula-2, C++ allows arithmetic on chars. short: short integer, possibly 8 bits, maybe 16. long : long integer 32 bits. double: double precision oating point more signicant digits, larger exponent. 5. const qualier means compiler disallows modication. 6. Stream I/O can handle a variety of simple values without needing additional formatting information. 7. Pronounce >> as get-from standard-input stream, cin. 8. Assignment statement uses =; note possible confusion with test for equality which uses == in C++. 9. 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. 10. 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. 11. For me it is essential that the closing brace lines up with w of while. 12. Some like to move the begin \{ to the next line to line up with the w of while; Okay by me. 13. Mixing of operands is allowed in C++, e.g. gint=g; Usually, it is better to make type conversion explicit, this can be done with a type cast, thus: gint=int(g);

62

6.3

For Loop

A variation on the pounds-to-grams program is shown in program ptogf.cpp. //----- ptogf.cpp ---------------------------//Convert pounds to grams - for loop - table //------------------------------------------#include <iostream> const float ptog = 453.592; float convert(float p) { return p*ptog; } int main() { cout<< "Pounds \t\tgrams \t\t kg."<< endl; for(int pds=0;pds<10;pds++){ float k,g; int gint; g=convert(pds); gint=int(g); cout << pds << "\t\t"<< gint<< "\t\t"<< g/1000 << endl; } return 0; }

Dissection 1. Note use of tab \t to align the table. 2. For statement and loop: 3. There are three statements contained within the (.,.,.) in a for statement: 1 int pds=0 initialisation. 2 pds<10 loop continuation test; evaluate the condition if true execute the body otherwise nish. 3 pds++ iteration; do this after the rst iteration; for subsequent loops, do it before evaluation of the continuation condition; 4. That is for(initialisation; continuation test; iteration); 5. The for(...) body may be a single statement or a block / compound-statement {...}.

6.4

Symbolic Constants and the Preprocessor

Traditional C would have replaced const float ptog = 453.592; with #define ptog 453.592 The general form of a symbolic denition is: #define <symbolic-name> <replacement-text>. 63

Note, no = or ; The C++ preprocessor replaces all occurrences of <symbolic-name> with <replacement-text> just like an editor global-replace command. The source code is passed through the preprocessor before it reaches the compiler proper. You can do mighty funny things with the preprocessor, but its wide use is not encouraged in C++. In fact, probably the only valid use is in in connection with header les and the #include directive: \#include <iostream> which replaces itself with the contents of le iostream. This le contains declarations of stream input-output classes and functions, and some constants. Also, ifndef . . . define . . . endif is used to ensure that led are #included no more than once.

6.5

Character Input and Output

As with C, C++ input-output, including les, deals with streams of characters, or text-streams. This is the UNIX model of les 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 le.

Buered and Echoed Input

On most computers, input from the keyboard is echoed, and buered.

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 buered, 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). Buered input. While a program is reading from a keyboard or a le, the computer stores all the input characters in a buer (array) and presents the array (line) of characters to the reading program only after Enter has been typed. Buered output. While output is being produced, the computer stores all the characters in a buer (array) and presents the array (line) of characters to the output device only after new line or endl has been reached.

Terminal input-output and I-O Redirection C++, C, and UNIX have unied view of text input-output. Reading from the keyboard is like reading from a le device stdin in C. So, when we talk about le I/O below, we include terminal I/O. NB. ctrl-d is end-of-le for a Linux / UNIX keyboard. If you want to test the programs using les, you can use input-output redirection. Note: If using the Borland IDE, rst, you must get the compiler to compile to an .exe le, say prog.exe . Second, you must leave the Borland C environment (obviously no need if using UNIX, Linux, or VAX-VMS). Third, create a text le, say test.dat. Then, prog < test.dat

64

reads from test.dat; i.e. the < redirects the program to read from the le instead of the keyboard. If you want to send the output to a le, say testout.dat, prog < test.dat > testout.dat

6.5.1

File copying

Program cio1.cpp shows how to copy from an input le (or keyboard) to an output le (or screen). //----- cio1.cpp -----------------------------------// copy input to output, version 1. // j.g.c. 1/2/97 //--------------------------------------------------#include <iostream> 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-le, but make sure the program is executing before you do this otherwise ctrl-d logs you out! Alternatively, create a le test.dat, e.g. simply use cp cio1.cpp test.dat and then use input-output redirection, see 6.5: cio1 < test.dat > testout.dat

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-le. 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-le in the character itself. What is the problem with cin >>c as in cio2.cpp? If you run cio2.cpp you will nd that cin >> c loses white-space s, i.e. newline, tab, and space itself. //----- cio2.cpp -----------------------------------// copy input to output, version 2. // j.g.c. 1/2/97 //--------------------------------------------------#include <iostream> 65

int main() { char c; while(cin >> c){ cout<< c; } return 0; }

6.5.2

Character counting

Program ctch1.cpp shows how to count the characters in the input stream, stopping at end-of-le and presenting the count. //----- ctch1.c ------------------------------------//counts input chars; version 1 // j.g.c. 1/2/97 //based on K&R p.18. //--------------------------------------------------#include <iostream> 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 dierent 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.

6.5.3

Line Counting

Program clines.cpp shows how to count the lines in the input stream, stopping at end-of-le and presenting the count.

66

//----- clines.cpp -----------------------------------// count lines in input. // j.g.c. 1/2/97 //--------------------------------------------------#include <iostream> int main() { char c; int nl; 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-nd 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.

6.5.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 end-of-le 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> 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; 67

if(c== ||c==\n||c==\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. if(expression) <statement1> else <statement2> As usual <statement> can be a compound statement {..}.

6.6

Arrays

Program cdig.cpp shows a program to count the occurrences of each numeric digit, of white-spaces 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 //--------------------------------------------------#include <iostream> int main() { char c; int i,nwhite,nother; int ndigit[10]; nwhite=nother=0; for(i=0;i<10;i++) ndigit[i]=0; while(cin.get(c)){ if(c>=0 && c<=9) //is it a digit? ++ndigit[c-0]; else if(c== ||c==\n||c==\t) // a white space? ++nwhite; else 68

++nother; //otherwise! } cout<< "digit counts = "; for(i=0;i<10;++i) cout<< ndigit[i]<< ", "; cout<<"\nwhite spaces "<< nwhite<<"\nothers "<<nother<< endl; return 0; }

Dissection 1. int ndigit[10]; denes an array of 10 ints. 2. for(...) loops must go 0,1 . . . 9, since, in C++, array subscripts must start at 0. 3. The C++ code to loop over the rst n elements of an array is: for(int i=0; i<n; i+)+ 4. && denotes logical and. 5. c-0 assumes that 0, 1, ... 9 have successive values. Actually, there is a function isdigit(char c) which is a better way. If you use isdigit(), you must have #include <ctype.h>. 6. c-0 is an integer expression so its OK for a subscript. 7. The following is the model for a multi-way decision; you can have any number else if. Note the default. 8. Note: there is no elsif, it is just else followed by another statement, which may be if. if(condition1) statement1; else if(cond2) stmt2; ......... else stmtn; //default -- if none of above true 9. There is another switch-case multi-way construct that we will encounter later. But, anything you can do with it, you can do with an if...else ladder. 10. Note indenting style; all the elses are of equal status, hence they should be aligned; and, we dont want to run o the right-hand edge of the paper.

6.7

Functions

We have already encountered functions that have been written for us, e.g. getchar, putchar. Program math1.cpp shows a program that reads ints and floats and does some arithmetic with them. Well use it as a case-study to exemplify many aspects of the use of functions. //------- math1.cpp ----------------------------// j.g.c. 2/2/97 -- from math1.c // demo of functions -- all in the same file //-------------------------------------------#include <iostream> int getInt(void) { int i; 69

cin>> i; return i; } float getFloat() { float f; cin>> f; return f; } float addf(float a, float b) { float c=a+b; a = a + 10; // to demonstrate pass-by-value return c; } int main() { float x,y,z,w; int i,j,k; cout<< "enter first int:"; i=getInt(); cout<< "enter second int:"; j=getInt(); k=i+j; cout<< i<<", "<< j<< ", "<< k<< endl; cout<< "enter first float:"; x=getFloat(); cout<< "enter second float:"; y=getFloat(); z=addf(x,y); cout<< x<<", "<< y<< ", "<< z<< endl; return 0; }

Dissection 1. A function denition consists of: return-type function-name(parameter list -- if any) { definitions of local variables; statements; } 2. If the function has no parameters, you may use either (void) or () 3. The functions may be all in the same le, along with main, or may be spread across many les. 4. The variables a, b, c are local to addf; they are invisible elsewhere. 5. Unless the function returns a value (some dont) return is not essential, but if the function declaration indicates that the function returns a value, then there must be an appropriate return statement. 6. The calling function may ignore the returned value. 7. The default for arguments in C++ is pass-by-value. Notice that a = a + 10; // pass-by-value+ has no effect on x! 70

it is the value of x that is passed. 8. Reference parameters (VAR in Modula-2) are implemented as follows. Here a becomes an alias for x and now x really does have 10 added to it. 9. In the calling program, reference parameters are treated the same way as value parameters. The special reference symbol & is used only in the function declaration or denition. float addf(float& a, float b) { float c=a+b; a = a + 10; // to demonstrate pass-by-reference return c; }

6.7.1

Declaration of functions

Program math2.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 getInt etc. before they are called. C++ demands that functions be declared before they are called. //------- math2.c ----------------------------// j.g.c. 2/2/97 -- from math1.cpp // demo of functions -- all in the same file, // but functions at end, and declared. //-------------------------------------------#include <iostream> int getInt(void); float getFloat(); float addf(float a, float b); int main() { float x,y,z; int i,j,k; cout<< "enter first int:"; etc ... } int getInt(void) { int i; cin>> i; return i; } float getFloat() { float f; cin>> f; return f; } float addf(float a, float b) { 71

float c=a+b; return c; }

Dissection 1. int getInt(void); declares the type of getInt(); this is called the prototype for getInt; 2. getInt has type: void -> int 3. addf has type: float, float -> float 4. Parameter names in prototypes are neither signicant nor necessary. But, they can provide good documentation. 5. e.g. float addf(float a, float b); and float addf(float, float); are equivalent.

6. Normally, it is a good idea to split a program into modules, e.g. the arithmetic functions weve written may be tested and stable, whilst the main program is subject to change; its nonsensical to have to re-compile the functions each time, and while they are in a le thats subject to change, we have no guarantee that errors or other changes could be introduced to them; so well put them in a separate le funs.c. 7. Actually, as we have seen in the Introduction, there are bigger and better reasons for modularity.

6.7.2

Program Split into Modules

Program mathmod.cpp shows the new main program; here, we \#include the function declarations in funs.h, but N.B. not their denitions / implementations). Now, the program and functions are compiled and loaded using: g++- -o exe mathmod.cpp funs.cpp -pedantic-errors Notice that .cpp les are compiled, and not #included. .h les, on the contrary, are #included, but not separately compiled. //------- mathmod.cpp -----------------------// j.g.c. 2/2/97 -- from math2.cpp // demo of separate modules //-------------------------------------------#include <iostream> #include "funs.h" int main() { float x,y,z; etc... Files funs.h and funs.cpp show, respectively, the function declarations and the denitions/implementations. //------- funs.h ----------------------------// j.g.c. 2/2/97 -- functions for mathmod.cpp // demo of separate modules //-------------------------------------------#include <iostream> int getInt(void); float getFloat(); float addf(float a, float b); 72

//------- funs.cpp ----------------------------// j.g.c. 2/2/97 -- functions for mathmod.cpp // demo of separate modules //-------------------------------------------#include "funs.h" int getInt(void) { etc...

Dissection 1. When using a library or external module, you must get into the habit of producing a header .h le that contains declarations of the functions. 2. Unlike C, C++ demands that you declare functions before you call them. 3. The prototypes of standard library functions and standard classes are contained in header les, e.g. iostream is just an ordinary text le containing the stream classes and functions. 4. .h les are not compiled they get compiled as part of any le 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 modied. The separate units can be compiled to object, as before: g++ -c mathmod.cpp g++ -c funs.cpp # yields mathmod.o # -> funs.o # yields executable exe

Now link g++ -o exe mathmod.o funs.o

As indicated earlier, you can do all this in one line g++ -o exe mathmod.cpp funs.cpp -pedantic-errors More of this in section 6.8.4.

6.8
6.8.1

Creation of Executables
Basics Compiling and Linking

Source programs like math1.cpp, mathmod.cpp etc. are not directly executable. There are a number of stages in creation of the executable, and executing it.

Compilation The rst stage of creating an executable is to compile math1.cpp into object code. g++ -c math1.cpp -pedantic-errors This compiles and puts the object code into a le math1.o. This object code is essentially machine code, with code to initialise main and the functions. It has most the building blocks, except the system code, which is in libraries. As I have said, the -pedantic-errors switch asks the compiler to be extra fussy.

73

Linking The second stage is to link the machine code in math.o with appropriate library code put the building blocks together: g++ -o exe math1.o This produces the executable le exe. In UNIX / Linux directory listings ls, you may notice that the le exe has a * appended, or that it appears as a dierent colour this indicates that it has executable permission. Even though we use g++, g++ actually invokes a command called ld to do the linking-loading loading refers to loading of library code. Here, the extraction of the code for cin.get etc. from the library is kept implicit; however, a library is nothing more than an object le, with appropriate indexes to each function.

Execution Finally, to execute exe, you type exe 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 mathmod.c, funs.c, introduced in section 6.7.2. Recall, g++ -c mathmod.cpp g++ -c funs.cpp # yields mathmod.o # -> funs.o # yields executable exe

Now link g++ -o exe mathmod.o funs.o And, you can do all this in one line

g++ -o exe mathmod.cpp funs.cpp -pedantic-errors

74

The following gure describes the process.

+----------+ | library | | | +----------+ compiler | +---------+ g++ -c +--------+ | | mathmod.+----------------------->| mathmod.o| | | cpp| | | | +---------+ +--------+ | | | +---------+ g++ -c +--------+ | | |funs.cpp +--------->| funs.o| | | | | | | | | +---------+ +--------+ | | | | | | | | V V V +--------------------------------+ | linker | | | +--------------------------------+ | V executable: exe

6.8.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 mathmod, you would have to explicitly mention the mathematics library, using -lm: g++ -o exe mathmod.cpp funs.cpp -pedantic-errors -lm You must also remember to #include the appropriate header le, e.g. #include <math.h>. Likewise for libraries to do with X-Window functions.

6.8.3

Static versus Shared Libraries

In section 6.7.2, the code for addf, getInt, and getFloat is linked statically, i.e. the object code for each of the functions is copied into the le 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.

6.8.4

Make and Makele

When you get to more complicated systems of multi-module programs, the utility make can become useful. Essentially, make executes the le Makefile. The following shows a Makefile for mathmod.cpp

75

#---------- makefile for mathmod ----------------------# j.g.c. 2/2/97 #------------------------------------------------------# here we define a macro CC as g++ -- GNU C++ compiler CC=g++ # how to create exe (the executable): # (1) mathmod.o, and funs.o math: mathmod.o funs.o must be up to date

# (2) create prog by linking mathmod.o, funs.o # note: *must* use TAB here, spaces no good $(CC) -o exe mathmod.o funs.o # specify what funs.o depends on # dont forget the TAB for the CC command! funs.o: funs.cpp funs.h $(CC) -c funs.cpp # dependencies for mathmod.o mathmod.o: mathmod.cpp $(CC) -c mathmod.cpp # # # # clean-up directory of .o files and other temporaries invoke as: make clean note that the rm command is on the line *after* the clean target, and, as usual, TABbed

clean: rm -f funs.o mathmod.o *.t *~ The standard practice is to name this le Makefile, then invoking make causes exe to be built : all programs compiled and linked. Make takes care of dependencies, e.g. if mathmod.o and funs.o already exist, make will not recompile them. On the other hand, if they exist, but if, say, mathmod.cpp has changed since the last compilation to mathmod.o, then make will recompile mathmod.c, but not funs.c. In the example above, make clean will delete the .o les, and other temporaries (I use .t for temporaries) and *~, the backup les from the joe editor. If you want to call Makefile something else, e.g. mathmod.m, maybe you want to keep many Makefiles in one directory though advanced make users have other solutions to that problem then you can invoke mathmod.m as make -f mathmod.m Make is very much part of UNIX, and is used for very much more than just C programs e.g. LaTeX. Another point, because of its complex syntax, people rarely create a Makefile from scratch; nd a template Makefile (one that works!) that is close to what you require and change bits to suit. Be careful with the places where the TAB syntax matters.

76

6.9

Summary

This chapter has given a quick tour of C++ syntax, certain basic facilities, and compilation and linking using g++.

77

Chapter 7

Variables, Types, Operators and Expressions


7.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 native to distinguish them from types formed by classes. We leave records (structs) to a later chapter.

7.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: a Underscore separators, e.g. multi_word_name. b Uppercase for inner names, e.g. multiWordName c Hungarian notation favoured by Microsoft (Petzold 1996), which is a special case of [b], with the rst few letters used to code the type and any special use of the variable. I now prefer [b], with partial use of [c] for pointers each pointer name begins in p. Note that style and consistency aect readability and that is as important as syntactic correctness. C++ is case sensitive. Typical uses of case are: As above, lower-case for rst word, upper-case for leading letter of remaining words if any. Upper-case for leading letter of class names. Upper-case for all letters of #define symbolic-constant, e.g. #define BIGNUM 1000. For class data members, I follow the FAQ (Cline, Lomow & Girou 1999b) and use a trailing underscore, e.g. int sec_;. 78

7.3

Elementary Data Types and Sizes

The C++ native 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 dierent types dier 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.

7.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. //----- bool.cpp ---------------------------------//tests char, unsigned char and signed char. //j.g.c. 9/2/97 //------------------------------------------------#include <iostream> int main() { 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; return 0; } Output: bool b = true: 1 bool b = false: 0 b+=22: 1 i=false+22: 22 79

In general, in C++ you must be careful about overowing 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 char, unsigned char and signed char. //------------------------------------------------#include <iostream> int main() { char c,x; unsigned char u; signed char s; int i; for(i=s=c=u=0; i<16;i++){ for(int j=0;j<16;j++,u++,c++,s++){ cout << "u = "<< int(u); cout << ", c = "<< int(c); cout << ", s = "<< int(s)<< endl; } cout<< "type any char + enter:"; cin>> x; } return 0; } Output around 127: u = 125, u = 126, u = 127, type any u = 128, u = 129, u = 130, 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

7.3.2

Floating-point types

float. Typically 32-bit IEEE oating point. double. Typically 64-bit IEEE oating point. These are always signed.

7.3.3

Implementation Dependencies

<limits.h>, <float.h> contain symbolic constants that specify the limits and sizes of the types on the current implementation; if you really want to bullet-proof your software against diering representations to make it portable you may need to know about these.

7.4

References

A reference, serves as an alternative name for the object with which it has been initialised. The denition of a reference must specify an initialisation. Example: 80

int x = 10; //plain int int& r = x; //reference x is dened as an ordinary int; subsequently, r is dened as a reference, and initialised with x. Dening a reference without initialisation results in a compiler error: int& r; //compiler error: reference not initialised All operations applied to the reference act on the variable to which it refers: r = r+ 2; adds 2 to x, now x == 12. Thus, a reference is a true alias ; since most computer science warns of the problems of aliases, why references? The only valid use for references, that I have come across, is in reference parameter passing in functions. Recall that C++ defaults to pass-by-value / pass-by-copy, so that in the example below, the parameters a, b are copies of the actual arguments: any alteration of a, b will not be reected in the calling program. However, c is dierent, it is a reference which is initialised with the actual parameter. Hence, c is an alias for the actual parameter, and any changes to c, will also be changes to the actual argument in the calling program. Thus, reference parameters are similar to Modula-2s VAR parameters. void addf(float& c, float a, float b) { c = a + b; } float x= 1.3, y= 4.5, z= 10.2; addf(z, x, y); // now z== 5.8 Reference parameters have an additional advantage that they remove any performance penalty caused by passing (copying) large objects. In such a case, some safety can be ensured: if a reference parameter is not modied within the function, it can be declared with the const modier.

7.5

Arrays

For any type T, the denition T a[n]; denes an array containing n elements of type T. e.g. float x[10];//10 floats. Array elements are indexed as, e.g x[5] = 5.0 places 5.0 in the fth element of x[]; but fth only if you start counting at zeroth : the indices of float x[10] go 0, 1, . . . , 8, 9. The C++ idiom for traversing such an array is: for(int i=0; i<10; i++){ x[i] = float(i); //for example }

81

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 dierent thing altogether.

Arrays and functions Arrays passed to functions are always passed by reference, see above, and chapter 7.

Pointers and arrays As noted in section 7.6.1, in C++ pointers and arrays are very closely related. Consequently, the previous fragment can legally be replaced by for(int i=0; i<10; 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.

7.6
7.6.1

Pointers
Introduction

In any high-level language, pointers are something of a necessary evil. In Modula-2 and Pascal, their role is quite restricted: they are used only as a method of accessing memory allocated dynamically o the heap, the pointer is used as a reference for the allocated memory which is otherwise anonymous. The Java programming language has managed to avoid them altogether because, in Java, all identiers, except those which identify elementary variables (int, float, etc.) refer to references (rather than values). Mostly due to the 0 syntax for declaring them, pointers in C++ and C are known to be dicult and error prone; 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. There are two primary uses for pointers in C++: As references for anonymous memory allocated dynamically o 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 reference s, see above, which are much more suitable for this purpose. 82

7.6.2

Declaration / Denition of Pointers

Pointers are dened as in the example below: int* pi; // pi is of type: pointer-to-int float* pf; // pf is of type: pointer-to-float char* s; // s is of type: pointer-to-char It is most important to realise that there are two parts to a pointer type declaration: The fact that it is a pointer. Its base type, e.g. pi pointer-to-int.

7.6.3

Referencing and Dereferencing Operators

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 a ampersand sign. Example: int* p; int c; p = &c; // causes p to point-to c.

Dereference operator * * de references 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. Thus, *p is similar to p^ in Modula-2.

Beware of Confusion The overloading of *, with two quite separate uses: Declare pointer-to in a declaration, e.g. int* p;. p is a pointer. Dereference in an expression, e.g. int i = *p;. p is a pointer, but *p is an int! is most confusing to those new to C++ and C.

83

Uninitialised and dangling pointers Dening a pointer creates the pointer itself, but it does not create the object pointed-to. Moreover, dening 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 eect. Consider the function fred: int* fred(int a) { int* p; int b; /*fred returns a pointer to int */

b=a+33; p=&b; /*p points to b */ return p; } caller: int*pa; int c,d; ... pa=fred(c); This looks ne, but, whenever control returns from fred to its calling point, b gets destroyed; consequently, pa points to something that doesnt exist.

7.6.4

Pointers and Arrays

Pointers are frequently used in handling arrays see the comments in section 7.6.1. Consider, int* pi; int x, z[10]; pi x pi x = = = = &z[0]; /*pi points to the first element of z*/ *(pi+2); /*same as x=z[2]*/ pi+4; /*pi now points at z[4]*/ *pi; /* x gets z[4] */

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. 84

7.7
7.7.1

Strings in C++
Introduction

Traditionally, there has been no native text-string type in C or C++. It is 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 7.6.1. In the past few years, a set of classes which use templates has been developed, previously called the Standard Template Library (STL) but now called simply the standard library; this standard library contains 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.

7.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 <string.h>, 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;

causes ab to be printed on the screen. Note that, string = "ab"; is not allowed, because whole arrays cannot be assigned, instead, you must use strcpy(string,"ab"); The following example implementations of strcpy, show, not only how null-terminated char arrays as used as strings, but also how char s[]; and char* s; are used interchangeable. char* == string is such an idiom in C++ and C that char * is synonymous with string. Nevertheless, we will have cause to develop a String class, with more secure properties. Simple char array. 85

/*--- copies f(from) to t(to) ----*/ void strcpy(char t[], char f[]) { int i=0; char c; c=f[i]; t[i]=c; while(c!=\0){ c=f[i]; t[i]=c; i=i+1; } } Using a common C / C++ idiom. void strcpy(char t[], char f[]) { int i=0; while((t[i]=f[i])!=\0) i++; } Using pointers. void strcpy(char *t, char *f) { while((*t=*f)!=\0){ t++; f++; } } Fancier still, and more cryptic. void strcpy(char *t, char *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 overow 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!

86

7.8

Standard String Class

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 //------------------------------------------------#include <string> #include <iostream> 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. #include <string> includes the declaration of the string class. 2. #include <CLASS>, rather than #include <CLASS.h> is gradually being introduced. In the current version of the GNU C++ compiler, #include <string> reads a standard string.h, whilst #include <string.h> would include the old C-string declarations. Eventually, .h will be needed in no #includes, but for backwards compatibility some overlap of referencing styles is inevitable. 3. string s1="Hello"; denes 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. 4. string s3; denes a string s3 and initialises it to an empty string. 5. cout<< s1<< endl; writes s1 to the screen. We will see more details of this later, but suce to say that string provides an overloading of the operator <<; similarly >>.

6. s3= s1+ s2; and s3+= s1; are further examples of operator overloading, in this case + is overloaded as a concatenation operator. Program tstring.cpp shows further use of the standard string class; in particular, the use of the overloaded == which properly tests the contents of the objects, and string method length() which returns the length of the string (number of characters). Note that length() is invoked using the object.method notation, whilst, +, +=, == are invoked using an inx style. Later, we will see that (overloaded) operators have much more in common than would appear from their syntax. 87

//----- tstring.cpp -----------------------------// tests on Std lib. string //------------------------------------------------#include <string> #include <iostream> int main() { string greet1="Hello"; string enquire="Give your name"; string greet2="How are you?"; string name; string vipname="J.Campbell"; cout<< greet1<< endl; cout<< enquire<< endl; cin>> name; cout<< greet2; if(name==vipname){ cout<<" Sir."<< endl; } else{ cout<<" "+name<< endl; } cout<< vipname+ ": no. chars (length) = "<< vipname.length()<< endl; cout<< name+ ": no. chars (length) = "<< name.length()<< endl; return 0; }

7.9

Constants and their Types

1234 is an int; 123456L (or l) is long. 0x42 is hexadecimal ; 037 is octal. x is signed char. x is a string constant, see above. Mixing up x and x is a common error and can be dicult to trace. 123.4 is double, i.e. oating-point constants default to double.

7.10

Declaration versus Denition

In C++ and C, it is common to make a distinction between declaration and denition. A declaration declares the properties of a variable or function. A denition both declares and allocates space for variables, or declares a function and denes it what does.

const qualier Declaring a variable const means it cannot be the target of an assignment; const int limit = 999;, any later statement of the form limit = ... will generate a compiler error.

eg.

88

7.11

Arithmetic Operators

+, -, *, /, %+; / / is overloaded between integer and oat; in integer, it truncates the result; e.g. 5/3 -> 1.

7.12

Relational and Logical Operators

Relational operators between arithmetic types: >, >=, <, <=, ==, !=

Logical operators on logical (bool): || inclusive or. && and. ! unary complement. !true -> false.

7.13

Type Conversions and Casts

C++, and 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).

7.14

Assignment Operators and Expressions

For most binary operators, e.g. +, -, *, /, there is a corresponding assignment operator. Thus, i+=2; is equivalent to i=i+2;. The general form is <op>=, where <op> is one of: +, -, *, /, %, <<, >>, &, ^, |. 89

7.15

Increment and Decrement Operators

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 dierent, but only subtly. Compare: 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.

7.16
Consider

Conditional Expressions

if(a>b) z = a; else z = b; this can be replaced by: 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.

7.17

Bitwise Operators

Bitwise and : &. Bitwise or : |. Bitwise exclusive-or : ^. Bitwise unary ones-complement : ~. Shift left by n bits: <<n. Shift right by n bits: >>n. Bitwise operators can be used only on operands of the integer family. Be careful to distinguish from logical - boolean operators: &&, ||.

7.18

Precedence and Order of Evaluation of Operators

My advice is, if in the slightest doubt, use brackets. If necessary, see (Stroustrup 1997b). Note: when you overload an operator, e.g. + in chapters 11 onwards, the operator retains its original precedence. 90

Chapter 8

Control Flow
8.1 Introduction

This chapter summarises the facilities available in C++ for determination of the control ow within a program. In block structured programming languages, we normally have the following ow 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. In addition, C++ also has goto, break, and continue which can be used for unstructured interruptions of control ow.

8.2

Statements and Blocks

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.

8.3
8.3.1

Selection
Two-way Selection if else

if (<logical expression>) 91

<statement1> //performed if expression true else <statement2> //performed if false

Remark: note that, deep down, C++ doesnt care to dier 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.

8.3.2

Multi-way Selection else if

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 o many programmers prefer it to the more obvious, but more dicult 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.

8.3.3

Multi-way Selection switch - case

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 dierence, and one that can be a big trap to the unwary: upon executing (say) <stmnt(s)1>, above, control will fall-through

92

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 ow 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.

8.4
8.4.1

Repetition
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 rst n integers, #include <iostream> int main() { int i=1;sum=0;n=4; while(i<=n){ sum+=i; ++i; } cout<< sum<< endl; }

8.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. 93

8.4.3
do

do - while

<statement> while (<expr>); <statement> is always executed at least once;

8.5

break and continue

break- causes innermost enclosing repetition loop or switch to be exited immediately to just outside the } at the end. continue causes the next iteration of a repetition loop to be started immediately, i.e. avoiding the remainder of the current repetition.

8.6

goto and Labels

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.

94

Chapter 9

Functions and Program Structure


9.1 Introduction

This chapter covers the denition, declaration and calling of functions.

9.2

Basics

We have covered most of this in Chapter 4, but its worth repeating. The syntax of the denition of a function is: <return-type> <function-name>(<parameter declarations>) { <declarations of local variables> <statements> } Example. int add(int a, int b) // a and b are also local variables { int value; //local variable value = a + b; return; } 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! 95

are both legal, but the second isnt very sensible semantically ! In a denition, 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) was essential.

9.3

Declarations of Functions Prototypes

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 ; i.e. it has no implementation / body. Function prototypes are quite similar to procedure specications in Modula-2. Signature would, in fact, better name. They are a declaration of the functions external properties. Actually, its a bit of a nuisance having to declare and dene and Java eliminates the separate declaration. in the declaration

Header Files for Libraries / 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 / class in a header le, and #include the appropriate header le where any of the functions is used.. For example, a library of functions contained in source le mylib.cpp, then you should have all prototypes in mylib.h. Then, #include "mylib.h" at the declaration part of all users of mylib. Thus, (a) you are saved a lot of typing, but more importantly, (b) you need maintain only one set of prototypes.

9.4
9.4.1

Function parameters
Parameters

You can pass to functions values of the following types: All elementary types int, char, float, etc. . . . Pointers to any type. References. Structs C++ records. Objects. Arrays. There is no limit to the number of parameters. 96

9.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 oats and returns the result in a oat: float addf(float a, float b) { float c=a+b; a = a + 10; // to demonstrate pass-by-value return c; } I have deliberately inserted the nonsense instruction a = a + 10; for demonstration purposes; whilst this has an eect on the local variable a it has no eect on the argument in the caller. Thus: float x=10, y=20, z=0; z = addf(x, y); cout<< x<<", "<< y<<", "<< z << endl; will result in 10, 20, 30

9.4.3

Pass-by-reference parameters

The following function swap uses reference parameters: void swap(int& a,int& b) { int temp=a; a=b; b=temp; } Now, swap does have an eect on the arguments in the caller as it must to be of any use. Thus: float 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 denition of the function: int& a,int& b. Thus, void swap(int& a,int& b) is equivalent to PROCEDURE swap(VAR:a, b:INTEGER); (*Modula-2*) Just to drive the point home, let us examine a naive swap which uses pass-by-value. 97

void swapSilly(int a,int b) //naive -- pass by value { int temp=a; a=b; b=temp; } Now, swapSilly does NOT have an eect on the arguments in the caller, hence the function has no eect. Thus: float x=10, y=20, z=0; swapSilly(x, y); cout<< x<<", "<< y<< endl; will result in 10, 20

9.4.4

Programmed pass-by-reference via pointers

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 qualier. The following function swapP uses pointer parameters: void swapP(int* pa,int* pb) { int temp=*pa; *pa=*pb; *pb=temp; } Notice that it is what pa, pb point-to that are swapped, not pa, pb themselves, i.e. *pa, *pb dereferenced. swapP has an eect on the variables pointed-to by the caller arguments. Thus: float x=10, y=20, z=0; swapP(&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. This, in addition to the extra complexity of the function, makes 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 swapP 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!

9.4.5

Semantics of parameter passing

The distinctions between the previous examples can be clearly dened by considering the detailed semantics of parameter passing by value and by reference. 98

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. float 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 denition: int a=x, int b=y. Hence, computations involving a, b are on these entirely separate and local variables.

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, recalling chapter 5, one can dene a reference float& 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. float x=10, y=20; float& 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 denition: 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 swapP(int* pa, int* pb) is called, swap(&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 swapP are created (int* pa, int* pb) and these are initialised with the values of temporaries px, py almost as if we had the denition: 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. On the other hand, just like arrays in the next section, in Java all non-elementary objects are always references.

9.4.6

Arrays as Parameters

Arrays are always pass-by-reference though implicitly so. When passing arrays as parameters, you need declare only that it is an array you need not give its length. For example, a function getline which reads a line of characters: int getline(char s[],int lim) { ... 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: 99

int getline(char *s, int lim) { ... *(s+i)=c; // or, s[i]=c; ... } Function getline can be called either as: int nchars; const int n=100; char s[101]; nchars = getline(s, n); // or nchars = getline(&s[0], n);

9.4.7

Default parameters

In a function denition a formal parameter can be given a default value. Example. void init(int count=0, int start=1, int end=100) { // body ... } Call:

init(); //is equivalent to init(0,1,100) //or init(22,23); //equivalent to init(22,23,100); Note: only trailing parameters can be defaulted: void init(int count=0,int start,int end); //ILLEGAL In normal programming you should use default parameters only if you have a compelling reason to do so. But, can be very useful in some class member functions, see later chapters.

9.5

Function return values

Functions can return the following values: All elementary types: int, char, oat, etc. . . . . Pointers to anything, but beware of dangling pointers, see chapter 6. References but beware of dangling references, see chapter 6. Structs. They cannot return arrays, but, of course, they can return a pointer to an array. Only one value can be returned. 100

9.6

Overloaded function names

Consider the following function addf, which adds floats. float addf(float a, float b) { return a + b; } If we want to provide an integer add, we might dene: int addi(int a, int 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; } int add(int a, int b) { return a + b; } Caller: int i,j,k; float x,y,z; ... z = add(x,y); // float add(float a, float b); called k = add(i,j); // float add(int a, int b); 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 nds extensive use in class es: A class may have a number of constructors, all with the same name, but each having a dierent 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.

101

9.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 penalising. int add(int a, int b) { return a + b; }

C++ was developed with one eye on eciency and performance, and allows the specier 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 tradeo of space code replicated for eciency lack of call / return overhead.

9.8

External variables

An external variable is dened 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 les prog.cpp, funs.cpp:

file prog.cpp: int globalx; /*defined OUTSIDE any function*/ #include "funs.h" int main() { extern int globalx; /* can declare here but not essential, as globalx is in SCOPE from its DEFINITION -- above, until the end of the file*/ f1(); return 0; }

File funs.cpp, separate from prog.cpp: 102

void f1(void) { extern int globalx; //bring globalx into SCOPE globalx=1; }

External / global variables have static lifetime, see section 9.12, i.e. they are created before the program starts executing, and are not destroyed until the execution is completed.

9.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 9.12. The scope of automatic / local variables dened in a function or block is from the denition 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 dierent source les, or in dierent functions or blocks, are unrelated. Example.

int add(int, int); int main() { int x,y,z,a,b; /* (1)*/ x=1;y=1;a=32;b=33; z=add(x,y); return 0; } int add(int a, int b) { int value; value=a+b; return value; }

| |scope of x, y ..a, b | | | |

| | scope of a, b, value | | these a,b NOT related to above a,b |

Blocks are scope units Rather 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; } 103

b= a+10; return b; } \end{varbatim} \paragraph{Scope Hiding} Consider: \begin{verbatim} void fred(int n, float b) { int c=33,d=16,i; for(i=0;i<=n;i++){ float d; // note these d, c are different from outer d=22.5; int c=49; } // here c==33, d==16. } The outer d the one declared rst is hidden in the block governed by the for, by the inner declaration.

Scope of functions All functions, everywhere well, in libraries that are included in the linking process and which are declared in the program le (e.g. in a .h le) are in scope in every part of all functions. This, see above, is called external scope. So, functions have global / external scope by default, whereas variables are local by default. Note: functions have global scope independent of declarations by #include. Modula-2 has a much more uniform and safe approach to scope: no externals are in scope, unless they are IMPORTed from their parent module; moreover, nothing can be IMPORTed that hasnt been EXPORTed explicitly by the (owner) module. This can cause problems where you are using libraries from multiple vendors and has been addressed by the recent introduction of namespace, see section 9.10.

Class scope Classes oer their own form of encapsulation, see chapter 9 onwards. 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.

9.10

Namespaces

[May be skipped at a rst reading may not yet be fully implemented in your C++ compiler]. 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 chapter 13, we develop our own class String. Now the fact that one is string and the other is String is sucient to avoid a name clash. But let us assume that the one in chapter 13 is called string and, moreover, that we want to use them together e.g. to test relative performances. In that case, the declarations:

104

#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, for BSc OOP //---------------------------------------------------#ifndef STRINGH #define STRINGH #include <iostream> namespace bscoop{ class String { public: String(const int len=0); //etc... } } // end of namespace In addition, all standard library stu 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 bscoop::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 conicts, we can use the declaration using namespace in the user program. using namespace std is included by default. using namespace bscoop; // now no need for bscoop::

9.11

Heap memory management

(Perhaps repeated elsewhere j.g.c. 2004-04-19.)

9.11.1

Introduction

Up to now, we have become used to memory management being done automatically. Thus:

105

int fred(int a) { int b; //here b created automatically // auto int b; -- is equivalent declaration; auto = automatic b= a*10+2; return b; //here the value of b is returned, and b deleted (destroyed) } 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 signicant and related factors dierentiate so-called heap or free memory, and the more familiar so-called stack memory used by local variables. Here, also, we nd 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.

9.11.2

Operator new

Operator new creates a variable on the heap and returns a pointer to it. Thus, a somewhat improbable example: int* readAndCreate() { int n; cout<< "Give size:"<< endl; cin>> n; int* p = new int[n]; // prompt read n ints for(int i = 0; i<n; i++){ cout<< "Enter an int:"; cin >> *(p+i); //or p[i] } return p; } int* pa = readAndCreate();

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 deallocated, 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 specied 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. 106

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] }

9.11.3

Operator delete

Operator delete deallocates heap memory destroys heap variables. Thus: int* readAndCreate() { int* p = new ... // etc... return p; } int* pa = readAndCreate(); // do something useful with the array delete [] pa; 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;

9.11.4
In

Anonymous variables

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.

107

9.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 deallocate it ! 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. And, the phrase garbage-in garbage-out refers to an entirely dierent notion.

Java In Java, all non-elementary variables (all objects and arrays well, 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 rst 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.

9.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 nd confusion between scope and lifetime though they are in cases related, they are entirely dierent 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 denition; lifetime is the whole time that control is in that part of the program from the denition to the end of the block / function. Example, local / automatic variables.

int fred(int a, int b) { int c; /*when control passes into fred: local (AUTOMATIC) variables a,b, c are created at this time -- their LIFETIME starts then*/ c= 22; ... }

/*when control reaches here, locals are destroyed*/

. 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 modier. 108

Static lifetime Example, static variable.

int fred(int a, int b) { 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 compile / link time, and it lives on until the program stops*/ c = 22; ... } /*when control reaches here, locals are destroyed but statics live on*/

Many meanings of static Most unfortunately, there are three overloadings of the static qualier: 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 le, static limit the scope of that variable or function to the remainder of that source. Thus, this use of {static signies private. 3 Static class member. In a class denition, 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.

9.12.1

Lifetime of variables summary

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 denition to exit from their block / function. These are called automatic in C / C++. Heap variables Their lifetime is from allocation to deallocation. Sometimes called dynamic variables. Static variables including global. Their lifetime is from the start of execution of the complete program, until it stops. Variables held in les Their lifetime is over many program executions; they are persistent. Persistency of objects is of signicant interest for database applications object-oriented databases. Some readers may gain a better understanding of lifetime by reading section 9.13 which gives a model of the run-time structure of a C++ program. 109

9.13

Memory layout of a C++ program a model

See (Sethi 1996), (Louden 1993). This is a model ; exact implementation detail may dier 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 | | ^ | | | | | V | | | grows up | dynamic, | ^ | run-time | Heap grows | down | part | | | | | | v +-----------------------------+ --Memory Layout of a C++ Program

Top of memory

Program Executable code. Global data etc. See sections 9.8, 9.12 and 9.9. These variables have lifetime that extends from when (or before) the program starts executing, until it stops executing. Hence, their lifetime is the same as that of the program code. Stack Local variables are created on the stack. The stack grows as functions and blocks create local environments, and diminishes when these functions / blocks are exited. An Environment is typically implemented using an activation record, probably via a stack-frame, see the stack-frame diagram below. Heap Heap memory is created by allocate command (new in C++), and is not destroyed until an appropriate deallocate command (delete in C++) is executed. Insucient care to deallocate heap variables can lead to garbage and memory leak.

110

Next, we show a typical function environment as implemented using an activation record, probably via a stackframe. +-----------------------------+ | . . . | | incoming parameter 2 | | incoming parameter 1 | +-----------------------------+ | Saved state -- caller: | | 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).

9.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 denition 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 ll [] (array size) according to size of the list, if no array length specied, e.g. int dayPerMonth[]={31,28, ...,30,31}; will dene an array of 12 ints.

9.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. 111

9.16

Block Structure

C++ is not fully block structured in the sense of Modula-2 and Pascal, because functions cannot be dened inside other functions. Example int fred1(int a, int b) { float fred2(float f) { return f*2.0; } //etc... return 0; }

//not allowed

Nevertheless, variables may be dened in any block, i.e. a block may create its own environment.

9.17

Recursion

C++ functions may be called recursively. File fac.cpp shows set of factorial functions in three styles: Non-recursive. Recursive. Recursive with an error due to inclusion of a static variable. /*----- 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; } int fac1(int n) // functional style { if(n<=0)return 1; else return n*fac1(n-1); } int fac2(int n) // erroneous with static { static int i=n; if(i<=0)return 1; else return i*fac2(i-1); } 112

int main() { int n,ff; cout<< "\nGive n:"; cin>> n;

ff=facp(n); cout<< ff<< endl; ff=fac1(n); cout<< ff<< endl; ff=fac2(n); cout<< ff<< endl; return 0; } Factorial -- comparison of styles You should note that a function creates a new environment every time it is called; thus, in fac1(), we have a new int n for every call indeed you should consider that there is a completely new copy of fac1() for each call. Consider fac1(3): 3x2x1 = 6; the execution of it proceeds as: fac1(3) 6<-------------------------------------------n=3 calls n*fac1(n-1) 2<----------------------------------3 2 n=2 calls n*fac1(n-1) 1<----------------------2 1 n=1 calls n*fac1(n-1) 1 <------------+ 1 0 | n=0 | returns 1 ----+ By a similar tracing, you will nd 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(.).

9.18

The C++ Preprocessor

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 les are run through the pre-processor before they are submitted to the compiler. There are four main features oered by the pre-processor.

9.18.1

File inclusion

#include <stdio.h> replaces that line with the contents of stdio.h. <..> tells the compiler to look in some standard include directory, #include "mylib.h" would search the current logged directory.

113

9.18.2
e.g.

Symbolic constants and text substitution

#define true 1 #define false 0 In fact you can dene any replacement text: #define BEGIN { #define END } would allow you to partially pretend you were using Modula-2.

9.18.3

Macro substitution

Recall the conditional expression: e = a>b ? a:b; // e gets max of a, b A MAX macro can be dened: #define MAX(A,B) ((A) > (B) ? (A) : (B)) then e=MAX(a,b); behaves like a inline function call the macro is inserted at the point of call. However, macros may carry certain safety penalties, and inline should normally be used instead. C provides a lot of functions this way, e.g. putchar(c); is actually a macro of putc(stdout,c); where stdout is a le pointer to the user standard output stream.

9.18.4

Conditional compilation / inclusion

You can eectively program the preprocessor to skip sections of code. You will become very familiar with this in header les. Example. #ifndef HDR #define HDR //declarations here. #endif

114

The scheme above could be used to ensure that a header le, if invoked more than once, will have eect only on the rst invocation. Conditional compilation is also very useful for building in portability across a number of compilers or target computers. E.g. #ifdef MS-DOS // MS-DOS code #elif Linux //Linux code #endif

115

Chapter 10

Records struct, class, union


10.1 Introduction

In C++, records are called struct or class. However, C++ has extended the notion of record to allow class / struct to be used as the whole basis of object-oriented programming. Essentially, in C++, class and struct are equivalent except for an insignicant 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

10.2

A Point record

In two-dimensional computer graphics, a point is dened by an x-coordinate (horizontal), and y-coordinate (vertical). We can dene a Point record, and dene instances p1, p2: struct Point {int x; int y; }; Point p1,p2; denes two points (pairs of int coordinates). We can then assign values to the members: p1.x = 150; p1.y = 50; and the result is as shown as follows:

116

p1.x = 150 +--------------------------+------> x | . | | . 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;

p1.x = 150; p1.y = 50; pp = &p1; then a = pp->x; //access the x member and is equivalent to, a= (*pp).x; the -> is provided for shorthand.

10.3

Operations on Structs

Assignment A struct can be assigned as a complete unit: Point p1, p2; p1.x=3; p1.y=2; p2=p1; /*copies p1 values into p2*/ is equivalent to: p2.x=p1.x; p2.y=p1.y; 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; }; 117

#include <iostream> #include <math.h> //for sqrt() Point readPoint(); void printPoint(Point p); float distPoint(Point p, Point q); int main() { Point p1, p2; float d; p1=readPoint(); cout<< "p1 = "; printPoint(p1); cout<< endl; p2=readPoint(); cout<< "p2 = "; printPoint(p2); cout<< endl; cout<< "Point that has largest x-value: "; if(p1.x > p2.x)printPoint(p1); else printPoint(p2); cout<< endl; d= distPoint(p1,p2); cout<< "distance p1 to p2 = "<< d<< endl; d= distPoint(p2,p1); cout<< "distance p2 to p1 = "<< d<< endl<< endl; p1.x = 3; cout<< "After p1.x = 3; p1 = "; printPoint(p1); d= distPoint(p2,p1); cout<< "Now distance p2 to p1 = "<< d<< endl<< endl; Point *pp; pp= &p1; //pp points at p1 -- no problems with unitialised ptr*/ cout<< "pp is a pointer to a Point, *pp= "; 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 dereferencing\n"; cout<<" a pointer to a struct & accessing a member\n"; cout<< "After pp->y = 5; *pp = "; printPoint(*pp); cout<< "\n(*pointer).member is another way of dereferencing\n"; cout<< "a pointer to a struct & accessing a member\n"; (*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) x,y (both ints space separating) : ";

118

{ cout<< "(x = "<< p.x<< ", y = "<< p.y<<")"<< endl; } float distPoint(Point p, Point q) { float dx = p.x - q.x; float dy = p.y - q.y; float dsquared = dx*dx + dy*dy; return sqrt(dsquared); }

10.4

Unions

A union is declared using the same syntax as struct. Although they may look similar, they are quite dierent 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 depends what was last assigned. Consider the example: union Manytype{ char c[4]; int i; long int l; float f; double d; }; Manytype u1,u2; Here we have declared two variables of union type, to contain char[4], or int, or long int, or float, or double, but only one of these at any time. Here we have declared a struct, that contains a tuple: char[4], and int, and long int, and oat, and double. struct Manyparts{ char c[4]; int i; long int l; float f; double d; }; Manyparts s1,s1; From the point of view of memory layout, a typical run-time memory layout for the union could be: u1 | | 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 +-----+-----+-----+-----+-----+-----+-----+-----+ | d -- assuming 8 bytes for double | +-----+-----+-----+-----+-----+-----+-----+-----+

119

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. //---- strun.cpp -------------------------------// j.g.c. 7/1/96, 17/2/97 // experiments with unions & structs //----------------------------------------------#include <iostream> union Manytypes{ char c[4]; int i; long int l; float f; double d; }; struct Manyparts{ char c[4]; int i; long int l; float f; double d; }; int main() { int j; Manytypes u1, u2; Manyparts s1, s2; cout<< "\n\t\tunion:\n"; for(j=0;j<=3;j++)u1.c[j]=j; cout<< "\nu.c[0..3] == \t"; for(j=0;j<=3;j++)cout<< " "<<(int)u1.c[j]; cout<< endl; u1.i = 0x12345678; cout<< "\nu.i == "<< u1.i; cout<< "\nNotice, the c array has been overwritten\n"; cout<< "\nu.c[0..3] == \t"; for(j=0;j<=3;j++)cout<< " "<< (int)u1.c[j]; cout<< endl<< endl; cout<< "\t\tstruct:\n\n"; for(j=0;j<=3;j++)s1.c[j]=j+2; cout<< "\nu.c[0..3] == \t"; for(j=0;j<=3;j++)cout<< " "<< (int)s1.c[j]; cout<< endl<< endl; s1.i = 0x12345678; cout<< "\ns.i == Hex"<< s1.i<< endl; cout<< "\nNotice, the c array is unchanged\n"; cout<< "\ns.c[0..3] == \t"; for(j=0;j<=3;j++)cout<< " "<< (int)s1.c[j]; cout<< endl<< endl; return 0; }

120

The big problem with unions is that they are not *discriminated* there is no mechanism within the type system to remember what was last assigned. Hence, it may be safer to bolt a discriminant onto a union by enclosing both within a struct. Here we dene a discriminant type for the union Manytypes: enum Utype {CHAR4,BYTE,INT,FLOAT,DBL}; Next, the union itself: union Manytypes{ char c[4]; int i; long int l; float f; double d; }; Then the discriminant and union aggregated together in a struct: struct Smany{ Utype d; Manytypes data; }; Finally, we show how to assign a float, and also an appropriate discriminant. Obviously, to be of real eect, struct Smany would have to be encapsulated in a proper class, with the data private, see the next chapter. Smany s; s.data.f = 1.25; // assign data s.d = FLOAT; // assign discriminant

121

Chapter 11

Objects That Use Heap Memory


11.1 Introduction

The classes that we have used so far the Cell and Coord classes in chapter 4 and the Cell, ReCell and Person hierarchies in chapter 5 have avoided any use of heap memory, see section 4.4. In each case, member variables are stack memory based. On the other hand, many practical classes/objects require, for one reason or another, to employ heap memory; for example: the sort of objects that require the exibility of heap storage are lists, stacks, queues, trees, and dynamically sizeable arrays and strings. The important distinction between heap-based objects and stack-based objects is the same 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 4.4, 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. This chapter will use the development of the Array class, already encountered in chapter 9 (error not in this version j.g.c. 2004-04-19). In addition, we will discuss in some detail: Destructors Destructors are called, implicitly, when an 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. Copy constructor Copy constructors are called when objects are passed (by value ) to and from functions. Again, for classes which use stack memory, the compiler will always provide an adequate copy constructor. For heapbased objects the case is quite analogous to that of the destructor: proper constructor memory management must be provided. Assignment Assignment (the = operator) needs similar treatment to the copy constructor. The Big-Three The C++ FAQs (Cline et al. 1999b) uses the term the Big-Three for these 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 quite scrupulous testing to detect.

122

11.2

An Heap-based Array Class

In chapter 9, we have already encountered a fairly simple Array class.

11.2.1

Class Declaration

Here, except for the inclusion of a copy constructor, a destructor, and an assignment operator, we nd that the class interface remains the same. Indeed, we surely hope that the interface remains the same for that is one of the major boasts of object-orientation: that users of objects should be concerned only with the interface. //----- array12.h ------------------------------------// j.g.c. 6/1/99, 8/1/99 // copied from array.h (ch9); now arbitrary length, // heap memory, and full complement of // copy constructor, destructor, assignment operator. //---------------------------------------------------#ifndef ARRAYH #define ARRAYH #include <iostream> #include <assert.h> class Array{ public: Array(unsigned int len= 0); // len defaults to zero if no arguments Array(unsigned int len, int val); Array(const Array& source); // copy constructor ~Array(); // destructor Array& operator=(const Array& source);// assignment operator void put(int val, unsigned int i); int get(unsigned int i) const; unsigned int length() const; void print() const; private: unsigned int len_; int* dat_; void copy(const Array& source); // used only internally }; #endif

Dissection of array12.h Copy constructor A copy constructor always has the signature T(const T& source);, where T 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(); 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 dened, i.e. when execution leaves the portion of the program in which it is in scope. Assignment operator Array& operator=(const Array& source); 123

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); // used only internally

11.2.2

Class implementation code

The implementation code for the Array class is shown in array12.cpp. //----- array12.cpp ---------------------------------// j.g.c. 6/1/99, 8/1/99 // copied from array4.cpp; heap memory //---------------------------------------------------#include "array12.h" Array::Array(unsigned int len) : len_(len) { cout<< "*1.Array(unsigned int)*"<< endl; if(len_==0){dat_ = 0; return;} dat_ = new int[len_]; assert(dat_!= 0); for(unsigned int i=0; i< len_; i++)put(0, i); } Array::Array(unsigned int len, int val) : len_(len) { cout<< "*2.Array(unsigned int, int)*"<< endl; if(len_==0){dat_ = 0; return;} dat_ = new int[len_]; assert(dat_!= 0); for(unsigned int i=0; i< len_; i++)put(val, i); } Array::Array(const Array& source){ cout<< "3.*Array(const Array&) -- copy constructor*"<< endl; copy(source); } Array::~Array(){ cout<< "*~Array()*"<< endl; delete [] dat_; } Array& Array::operator=(const Array& source){ cout<< "*operator=*"<< endl; if(this!= &source){ // beware a= a; delete [] dat_; copy(source); } return *this; } 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); } void Array::put(int val, unsigned int i){ 124

assert(i<len_); dat_[i]= val; } int Array::get(unsigned int i) const { assert(i<len_); return dat_[i]; } unsigned int Array::length() const { return len_; } void Array::print() const { unsigned int len= length(); cout<<"length= " << len<< ": "; for(unsigned int i=0; i<len; i++){ cout<< get(i)<< " "; } cout<< endl; }

Dissection of array12.cpp 1. We have added print statements to some of the signicant 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++)put(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++)put(val, i); } This is the same as the default constructor except for the initialising value. 4. Function copy.

125

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_; } (a) Here we use delete to release the allocated memory. (b) Owing to the fact that dat_ points to an array, we must use delete []. 7. Assignment operator =. Array& Array::operator=(const Array& source){ if(this!= &source){ // beware a= a; delete [] dat_; copy(source); } 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). (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 eciency of reference passing. The next chapter has a more extensive discussion of operator overloading.

126

11.2.3

A simple client program

The program in testar12.cpp demonstrates the use of the Array class. //----- testar12.cpp ---------------------------------// j.g.c. 6/1/99, 8/1/99, 13/1/99 // copied from testar4.cpp // g++ -o exe testar12.cpp array12.cpp // or mkar12 //---------------------------------------------------#include "array12.h" Array fred(Array a){ cout<< "Array a in fred: "; a.print(); cout<< endl; Array b(a); int len= b.length(); int x; for(int i= 0; i< len; i++){ x= b.get(i); b.put(x+100, i); } 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(); 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.put(i+20, i); } cout<< "c3 updated: "; c3.print(); cout<< endl<< endl; cout<< "c3.get: "; for(int i= 0; i< len; i++){ cout<< c3.get(i)<< " "; } cout<< endl<< endl; Array c4= c3; cout<< "Array c4= c3: "; c4.print(); cout<< endl; Array c5(c3); cout<< "Array c5(c3): "; c5.print(); cout<< endl; c2= c3; cout<< "c2= c3: "; c2.print(); cout<< endl;

127

c2= fred(c3); cout<< "c2= fred(c3): "; c2.print(); cout<< endl; cout<< "finishing ..."<< endl; return 0; }

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 testar12.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. 3. Array c3(5, 7); cout<< "Array c3(5, 7): "; c3.print(); cout<< endl; outputs: 2.Array(unsigned int, int)* Array c3(5, 7): length= 5: 7 7 7 7 7 i.e. the initialising constructor is called with arguments 5, 7, and, when printed, the array is seen to have length 5 and initial values 7. 4. Array c4= c3; cout<< "Array c4= c3: "; c4.print(); cout<< endl; outputs: 3.*Array(const Array&) -- copy constructor* Array c4= c3: length= 5: 20 21 22 23 24 i.e. the copy constructor is called with argument c3; this may be a little surprising in this circumstance, the compiler always knows to optimise out an unnecessary assignment. When printed, the array c4 is seen to have been appropriately copied from c3. 5. Array c5(c3); cout<< "Array 5(c3): "; c5.print(); cout<< endl; outputs: 128

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. 6. Now we get to an true assignment statement. c2= c3; cout<< "Array c2= c3: "; c2.print(); cout<< endl; outputs: *operator=* Array c2= c3: length= 5: 20 21 22 23 24 i.e. the assignment operator is called with argument c3. 7. Now we get to a function call with parameter passing; this generates a lot more activity than you would expect. c2= fred(c3); First, a copy of c3 is made to create local object a, and the print statement executed. 3.*Array(const Array&) -- copy constructor* Array a in fred: length= 5: 20 21 22 23 24 Next local Array b is created from a. (Actually, this is a little silly, since a is already a local copy; nevertheless, the code as given is a good general demonstration of the activity that surrounds function calls and parameter passing). 3.*Array(const Array&) -- copy constructor* Then, after b is modied (no output), return b causes the copy constructor to make a copy to an anonymous temporary object in the main program: 3.*Array(const Array&) -- copy constructor* Then both local objects, a, b are destroyed: *~Array()* *~Array()* Then the temporary is assigned to c2: *operator=* and the temporary is destroyed: *~Array()* Then c2 is printed, c2= fred(c3): length= 5: 120 121 122 123 124 8. Finally, we get to the end of main, but its not all over yet: finishing ... *~Array()* *~Array()* *~Array()* *~Array()* *~Array()* Here we see objects c1, c2 ..., c5 being destroyed. 129

11.3

The this pointer

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; }

11.4

Copy Constructors and Parameter Passing and Value Return

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.

11.4.1

Na ve member-wise constructor, shallow copy

The problem is that the compiler provided default copy constructor will merely perform a na ve member-wise or shallow copy constructor. In the case of Array this means that only members len and dat_ will be copied. The deciency 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.put(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.

11.4.2

Proper deep copy constructor

In the case of Array, we provide Array::Array(const Array& source){ cout<< "3.*Array(const Array&) -- copy constructor*"<< endl; copy(source); } 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); } 130

This provides a so-called deep copy, i.e. copy not just the explicit members, but also what they point to.

11.4.3

Copy constructor and parameter passing and return

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; } 1. Local object a is created using the copy constructor. 2. Then object b is created again the copy constructor. 3. On return, b must be copied to the caller; this is to a an anonymous temporary object, which, momentarily, replaces fred(c3). 4. Next, the assignment operator = is invoked to copy the temporary object to c2. I suppose the main point to me made here is that a faulty copy constructor is a great liability but, because of the implicitness of most of its use, it can cause problems that are very hard to trace.

11.5

Assignment

Assignment is subject to the same requirements as copy constructors, and the discussion of section 11.4; i.e. a nontrivial 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 dierence 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; } A more detailed account of operators is given in the next chapter. As mentioned earlier, you should carefully note that operator= returns a reference (Array&). This allows constructs of the form a = b = c;, which, if it is to be valid for built-in types like int, had better be valid for objects. Indeed, many container classes, e.g. some of those in the Standard Template Library (STL), depend on and demand this property.

131

11.6

Destructor

As repeatedly mentioned, destructors are called, implicitly, when an object goes out of scope. The destructor for an object which references heap memory is subject to the same 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_; }

11.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.

11.8

Reference parameters and reference return

I hope that the discussion in subsection 11.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:

c2= fred1(c3); // call remains the same

Array fred1(const Array& a){ Array b(a); // do things to b return b; } 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; }

132

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* // do things to a return a; } is very seriously awed; 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: pc2= fred1(c3); // now returns a pointer

Array* fred2(const Array& a){ Array* pb = new Array(a); int len= pb->length(); int x; for(int i= 0; i< len; i++){ x= pb->get(i); pb->put(x+100, i); } 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 articial, 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 ecient due to return value optimisation.

133

Array fred11(Array a){ // a is a local *copy* // do things to a return a; } 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.

11.9

Privacy is class-based, not object-based

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); } for(unsigned int i= 0; i< len_; i++)dat_[i] = source.get(i); Here we use get for access to source, but we go straight to dat_ for the object itself. However, we could easily rewrite this line as: for(unsigned int i= 0; i< len_; i++)dat_[i] = source.dat_[i]; Thus, sources private members are visible to the member functions of other objects of the same class. Privacy is class-based, not object-based. On the other hand, maybe the use of accessor functions is no harm at all; it further limits the eects of change of representation to even member functions. However, use of source.get(i) may oend the performace ultrafastidious since it may incur some small performance penalty. If we wanted to take use of interface functions to the limit, we could have: for(unsigned int i= 0; i< len_; i++)put(source.get(i), i); Here, put refers to the object itself the implicit argument.

134

Chapter 12

Operator Overloading
12.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 dierence between implicit and explicit arguments, e.g. implicit: object1 versus explicit: object2 in object1.member_function(object2). It should be said, at the outset, that operator overloading is not favoured by everyone. As we have stated, operators calls are no dierent to function calls. Neither, as we show below is there any magic involved, nor any performance advantages. Java does not allow operator overloading. On the other hand, some argue that it is benecial to be able to use + when we want to add, e.g. Arrays, just as adding ints the language is made more expressive.

12.2

Lead-in Add Functions for Array

As a lead-in to our discussion on operator overloading, we rst provide add functions for Array; subsequently, we will replace these with overloaded operators +, +=. We present array21.h, array21.cpp and testar21.cpp almost without comment except for non-member functions there should be nothing new.

135

//----- array21.h ------------------------------------// j.g.c. 6/1/99, 8/1/99 // copied from array12.h (ch12); // getting ready for operators. //---------------------------------------------------#ifndef ARRAYH #define ARRAYH #include <iostream> #include <assert.h> class Array{ public: Array(unsigned int len= 0); Array(unsigned int len, int val); Array(const Array& source); ~Array(); Array& operator=(const Array& source); void addTo(const Array& other); Array& addTo1(const Array& other); void copy(const Array& source); void put(int val, unsigned int i); int get(unsigned int i) const; int length() const; void print() const; private: unsigned int len_; int* dat_; }; //notice: not a member of Array. Array add(const Array& a1, const Array& a2); #endif

136

//----- array21.cpp ---------------------------------// j.g.c. 6/1/99, 8/1/99 // copied from array12.cpp; getting ready for operators //---------------------------------------------------#include "array21.h" Array::Array(unsigned int len) : len_(len) { if(len_== 1){ dat_ = 0; return;} dat_ = new int[len]; assert(dat_!= 0); for(unsigned int i=0; i< len_; i++)put(0, i); } Array::Array(unsigned int len, int val) : len_(len) { if(len== 1){ dat_ = 0; return;} dat_ = new int[len]; assert(dat_!= 0); for(unsigned int i=0; i< len_; i++)put(val, i); } Array::Array(const Array& source){ copy(source); } Array::~Array(){ delete [] dat_; } Array& Array::operator=(const Array& source){ if(this!= &source){ // beware a= a; delete [] dat_; copy(source); } return *this; } void Array::copy(const Array& source){ len_= source.length(); if(len_< 1){dat_ = 0; return;} dat_ = new int[len_]; assert(dat_!= 0); for(unsigned int i= 0; i< len_; i++)dat_[i] = source.dat_[i]; } void Array::addTo(const Array& other){ assert(len_== other.len_); for(unsigned int i= 0; i< len_; i++)dat_[i] += other.dat_[i]; } Array& Array::addTo1(const Array& other){ assert(len_== other.len_); for(unsigned int i= 0; i< len_; i++)dat_[i] += other.dat_[i]; return *this; } void Array::put(int val, unsigned int i){ assert(i<len_); dat_[i]= val; } int Array::get(unsigned int i) const {

137

assert(i<len_); return dat_[i]; } int Array::length() const { return len_; } void Array::print() const { unsigned int len= length(); cout<<"length= " << len<< ": "; for(unsigned int i=0; i<len; i++){ cout<< get(i)<< " "; } cout<< endl; } // notice no Array:: -- *not* a member Array add(const Array& a1, const Array& a2){ Array res= a1; return res.addTo1(a2); }

Dissection of array21.h, .cpp 1. Method AddTo performs addition of the argument object, to the calling object: array1.addTo(array2); 2. Method AddTo1 does the same as addTo, only it returns a value; thus, it can be used in the C/C++ style: result = array1.addTo1(array2); addTo1 will become a model for operator+=. 3. Notice that add is declared outside the class. Array add(const Array& a1, const Array& a2); This is because we want to call this function with its input arguments symmetrically: result = add(array1, array2); If it was a member function, one of the arguments would have to be implicit (the calling object), i.e. it would be declared exactly as like addTo1 and the call would be asymmetric: result = array1.add(array2); 4. Later, we will see that it is normal to dene a two argument function add (and operator+) in terms of AddTo1 (or operator-=-). Example uses of these functions are shown in testar21.cpp. //----- testar21.cpp ---------------------------------// j.g.c. 6/1/99, 8/1/99 // copied from testar4.cpp // g++ -o exe testar21.cpp array21.cpp -pedantic-errors // or mkar21 //---------------------------------------------------#include "array21.h" int main() { Array c2(5, 7); 138

cout<< "Array c2(5, 7): "; c2.print(); Array c3(5, 10); cout<< "Array c3(5): "; c3.print(); cout<< endl; Array c4(5); cout<< "Array c4(5): "; c4.print(); c3.addTo(c2); cout<< "c3.addTo(c2): "; c3.print(); cout<< endl<< endl; cout<< "c2: "; c2.print(); cout<< "c3: "; c3.print(); cout<< endl; c4= add(c2, c3); cout<< "c4= add(c2, c3): "; c4.print();cout<< endl; Array c5(5), c6(5); cout<< "Array c5(5): "; c5.print(); cout<< "Array c6(5): "; c6.print(); c6= c5.addTo1(c4); cout<< "c6= c5.addTo1(c4);"<< endl; cout<< "c5: "; c5.print(); cout<< "c6: "; c6.print(); cout<< endl; return 0; }

12.3

Chaining calls to member functions

As noted, AddTo1 returns a value; not only can it be used as follows: result = array1.addTo1(array2); but also in chained set of calls: result = array10.addTo1(array1.addTo1(array2)); We will see a similar principle in action when we require operator= to return a reference to its object; this allow the familiar C/C++ chaining of assignments such as: object1 = object2 = object3;

12.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 Finally, we include an indexing operator[] which replaces get and put.

139

//----- array22.h ------------------------------------// j.g.c. 6/1/99, 8/1/99, 14/1/99 // copied from array21.h // operators //---------------------------------------------------#ifndef ARRAYH #define ARRAYH #include <iostream> #include <assert.h> class Array{ public: Array(unsigned int len= 0); Array(unsigned int len, int val); Array(const Array& source); ~Array(); Array& operator=(const Array& source); Array& operator+=(const Array& other); void copy(const Array& source); int& operator[](unsigned int i) const; unsigned int length() const; private: unsigned int len_; int* dat_; }; Array operator+(const Array& a1, const Array& a2); ostream& operator<<(ostream& os, Array& a); #endif //----- array22.cpp ---------------------------------// j.g.c. 6/1/99, 8/1/99, 14/1/99 // copied from array21.cpp; // operators //---------------------------------------------------#include "array22.h" 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++)dat_[i]= 0; } 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++)dat_[i]= val; } Array::Array(const Array& source){ copy(source); } Array::~Array(){ delete [] dat_; } Array& Array::operator=(const Array& source){ if(this!= &source){ // beware a= a; 140

delete [] dat_; copy(source); } return *this; } 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.dat_[i]; } Array& Array::operator+=(const Array& other){ assert(len_==other.len_); for(unsigned int i= 0; i< len_; i++)dat_[i] += other.dat_[i]; return *this; } int& Array::operator[](unsigned int i) const { assert(i<len_); return dat_[i]; } unsigned int Array::length() const { return len_; } // notice no Array:: -- *not* a member Array operator+(const Array& a1, const Array& a2){ Array res= a1; return res+= a2; } ostream& operator<<(ostream& os, Array& a) { unsigned int len= a.length(); os<< "[" << len << "]{ "; for(unsigned int i=0; i< len; i++){ os<< a[i]; if(i!= len-1)os<<", "; } os<<"}"<< endl; return os; } //----- testar22.cpp ---------------------------------// j.g.c. 6/1/99, 8/1/99, 9/1/99 // copied from testar21 // g++ -o exe testar22.cpp array22.cpp -pedantic-errors // or mkar22 //---------------------------------------------------#include "array22.h" int main() { Array c2(5, 7); cout<< "Array c2(5, 7): "<< c2;

141

Array c3(5, 10); cout<< "Array c3(5): "<< c3<< endl; Array c4(5); cout<< "Array c4(5): "<< c4; c3+= c2; //previously c3.addTo(c2); cout<< "c3+= c2: "<< c3<< endl<< endl; cout<< "c2: "<< c2; cout<< "c3: "<< c3<< endl; c4 = c2 + c3;// previously c4= add(c2, c3); cout<< "c4 = c2 + c3: "<< c4<< endl; Array c5(5), c6(5); cout<< "Array c5(5): "<< c5; cout<< "Array c6(5): "<< c6; c6= c5+= c4;// previously c6= c5.addTo1(c4); cout<< "c6= c5+=c4;"<< endl; cout<< "c5: "<< c5; cout<< "Array c6: "<< c6<< endl; int len= c6.length(); for(int i= 0; i< len; i++){ c6[i]= i+50; } cout<< "Using cout<< c6[i]"<< endl; for(int i= 0; i< len; i++){ cout<< c6[i] << " "; } cout<< endl<< endl; cout<<"Now operators called in function manner"<< endl<< endl; c3.operator+=(c2); //c3+= c2; cout<< "c3.operator+=(c2);: "<< c3<< endl<< endl; cout<< "c2: "<< c2; cout<< "c3: "<< c3<< endl; c4= operator+(c2, c3); // c4 = c2 + c3; cout<< "c4= operator+(c2, c3);: "<< c4<< endl; for(int i= 0; i< len; i++){ c6.operator[](i)= i+50; } cout<< "Using cout<< c6.operator[](i)"<< endl; for(int i= 0; i< len; i++){ cout<< c6.operator[](i) << " "; } cout<< endl<< endl; return 0; }

Dissection 1. Here we see operator+= compared to addTo c3+= c2; //previously c3.addTo(c2); 142

2. Here we see that we can call operator+= using normal function-call syntax: c3.operator+=(c2); //c3+= c2;

3. Here we see operator+ compared to add: c4 = c2 + c3;// previously c4= add(c2, c3); 4. Here we show operator+ can also be called as a normal function: c4= operator+(c2, c3); // 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); } for(int i= 0; i< len; i++){ cout<< c6[i] << " "; // replaces c6.get(i); } 6. And here we show it called as a function: cout<< c6.operator[](i) << " "; 7. Use of operator[] in put (write) mode is a bit subtle. The operator returns int& a reference to an int. In verb+get+ (read) mode, thats understandable enough when the reference is returned to a temporary (anonymous) variable, as discussed before in the context of returning references, the = operator immediately copies the value to the destination: destination= source1 + source2;, and the temporary is destroyed. In the case of put (write), the reference is returned to a temporary (anonymous) variable, the value on the right-hand-side of the assignment is assigned to the variable that the reference aliases, i.e. the element of the array. Subtle! The stream output operator << needs special considerations.

Non-member operator, argument order We must declare operator<< outside class Array: ostream& operator<<(ostream& os,const Array& a); The operator << takes two arguments: an object of the class ostream (usually cout which is dened in iostream, hence #include <iostream>), and a Array object. This will be invoked as: Array x; cout<< x; hence, because of the convention for inx operators in C++, the rst argument must be ostream (actually a reference), and the second a Array.

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); 143

12.5
12.5.1

Member versus Non-member Functions, Conversions


Introduction

The objective of this section is to discuss circumstances in which it is better to make functions or operators nonmember (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.

12.5.2

A String Class

Although the standard library provides a perfectly good string class, we need the following home-grown one to furnish examples for the later discussion.

Class Interface, Methods The interface for the String class is shown in string.h. //---- string.h ------------------------------------// j.g.c. 23/12/92 - based on Turbo C example // j.g.c. 3/1/97 - tidied-up // j.g.c. 21/2/97 - added get() and gets() etc. // j.g.c. 8/3/97 - assign added. //----------------------------------------------#ifndef STRINGH #define STRINGH #include <iostream.h> #include <string.h> #include <ctype.h> class String { public: String(const int len=0); String(const char* cstr); String(const String& source); String& operator=(const String& source); ~String(); String& operator+=(const String& other); void insertCharAt(char c, unsigned int i); char charAt(unsigned int i); int length() const; int compare(const String& other) const; void print() const; void gets(); // reads up to \n, includes whitespaces void get(); // uses cin.get(c), reads up to any whitespace private: char* p_; // string data - C form, with \0. int len_; // length, incl. \0 }; #endif

144

Brief Dissection of string.h 1. There are three constructors, and, as it must be, they all have the same name the class name. Again, note that each is distinguishable from the other by its signature the types in parameter list. It is instructive to inspect each constructor in turn: String(const int len=0); This constructor is called when we dene a String object as: String s(22);. It doubles as a default constructor, since String s; is taken as String s(0); String(const char* cstr); This constructor is called when we dene a String object as: String s("abcdef");, i.e. it constructs a String object from a C-string. Copy constructor String(const String& source);. 2. Concatenate. String& operator+=(const String& other); is simply an operator presentation of concatenate. Thus, String s1("abc"); String s2("xyz"); s1 += s2 yields s1 == "abcxyz".

Class implementation code The implementation code for the String class is shown in string.cpp. //---- string.cpp ------------------------------------// j.g.c. feb 1992, 27/12/96, 10/3/97, 23/3/97 //----------------------------------------------------#include "string.h" #include <string.h> String::String(const int len){ len_=len; p_=new char[len_+1]; p_[0]=\0; } String::String(const char* cstr){ len_=strlen(cstr); //length of C string, excl. \0 p_=new char[len_+1]; strcpy(p_,cstr); } String::String(const String& source){ len_=source.length(); p_=new char [len_+1]; strcpy(p_,source.p_); } String& String::operator=(const String& source){ //check [Budd, 1994] p. 100 -- possibly better implem. if(this != &source) { // beware s=s - Stroustrup p.238 delete [] p_; //old text buffer len_=source.length(); p_=new char[len_+1]; strcpy(p_, source.p_); } return *this; } String::~String(){ delete [] p_; p_=0; } 145

String& String::operator+= (const String& other){ int oldl=length(); len_=oldl+other.length();//NB char count char* pp= new char[len_+1]; strcpy(pp, p_); delete [] p_; strcpy(&(pp[oldl]), other.p_); p_=pp; return *this; } String concatNM(const String& source1, const String& source2){ String s = source1; s += source2; return s; } String operator + (const String& source1, const String& source2){ String s = source1; s += source2; return s; } String String::concatM(const String& source2) { String s = *this; s += source2; return s; } String String::operator / (const String& source2){ String s = *this; s += source2; return s; } void String::insertCharAt(char c, int i){ if(i >= length())return; p_[i]=c; } char String::charAt(int i){ if(i >= length())return \0; else return p_[i]; } int String::length() const{ return len_; } int String::compare(const String & other) const{ char *s=p_; char *t=other.p_; return strcmp(s,t); } void String::print() const{ cout<< p_; } void String::gets(){ //reads up to \n

146

char c,s[81]; int i=0; while(cin.get(c)){ if(c==\n)break; s[i]=c; i++; } s[i]=\0; *this = String(s); } void String::get(){ // uses cin.get(c), reads up to any whitespace char c,s[81]; int i=0; while(cin.get(c)){ if(isspace(c))break; s[i]=c; i++; } s[i]=\0; *this = String(s); }

A very simple client program The program in strtst1.cpp demonstrates the use of the String class. //--- strtst1.cpp ----------------------------------------// tests String class // based on stringtst.cpp. // j.g.c. 8/3/97 //--------------------------------------------------------#include "string.h" int main() { String s1(0), s4; String s2("abcdef"); String s3(s2); cout<< "gets(): enter a string -- end with Enter:"; s1.gets(); cout<< "s1: length = "<< s1.length()<< endl; s1.print(); cout<< "get(): enter a string -- end with _any_ white-space:"; s4.get(); cout<< "s4: length = "<< s4.length()<< endl; s4.print(); cout<< "s2: length = cout<< "s3: length = s2 += s1; cout<< "s2 += s1;"<< cout<< "s2: length = return 0; } "<< s2.length()<< endl; s2.print(); "<< s3.length()<< endl; s3.print(); endl; "<< s2.length()<< endl; s2.print();

147

12.5.3

Implicit Arguments

When we invoke Strings print() function: String s; s.print(); void String::print() const { cout<< p_; } we notice that the argument the object itself is implicit in the function declaration. Now, 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. 2. Non-member function possibly friend. 3. Member operator, say +. 4. Non-member operator, say /; we name it / simply to avoid name conict with +.. Whether a function is a member or not has an obvious consequence on the way it is called: if it is a member we must use the object.member(); notation. For operators, the consequences are more subtle, but can be important.

Member function The member function version, concatM, is dened 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 rst 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.

148

Non-member function The non-member function version, concatNM, is dened as follows: String concatNM(const String& source1, const String&source2) { String s = source1; s += source2; return s; } The rst argument is no longer implicit, and the call is symmetric: String s1, s2, s4; s4 = concatNM(s1, s2); If concatNM had required to access private member data, which is actually not the case, then concatNM would have had to be been given friend privileges by including the following in the declaration of String, string.h: friend String concatNM(const String& source1, const String& source2);

Non-member operator The non-member operator version, +, is dened as follows: String operator + (const String& source1, const String& source2) { String s = source1; s += source2; return s; } The call is now both symmetric and inx: String s1, s2, s4; s4 = s1 + s2; Again we note that if + had required to access private member data, then we would have had to include the following in the declaration of String, string.h: friend String operator +(const String& source1, const String& source2);

149

Member operator For completeness, we give a member operator version, /, which is dened as follows: String String::operator / (const String& source2) { String s = *this; s += source2; return s; } The call appears both symmetric and inx: String s1, s2, s4; s4 = s1 / s2; Nonetheless, it has one slight asymmetry, what is really happening is: s4 = s1.(operator/)(s2); There are consequences, as pointed out in subsection 12.5.4.

12.5.4

Coercion of Arguments

There is a very subtle dierence between the non-member operator+ and the member operator / above. The relevant rule is the following: if an operator function is a member, its rst 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 + .

12.5.5

Constructors for conversion explicit

In subsection 12.5.4, we referred to the automatic invocation of the constructor String(char *) when e.g. "xyz" appears where a String object is expected. A recent addition to C++ allows us to forbid the use of a constructor as a conversion function: in string.h we can declare the constructor explicit, i.e. explicit String(const char* cstr); 150

Chapter 13

A List Class
13.1 Introduction

A list is commonly used where we have a collection of items, but where the number of items in the collection cannot be determined in advance. Hence, a xed length array, e.g. int a[100];, cannot be used because the 100 could be 2 or it could be 200, 000 and it just isnt feasible to pick the absolute maximum. Moreover, a run-time allocated array, e.g. int* pa = new int[n];, where n is variable, is no much better, because we dont know the size of the collection until it has been built, and, indeed, its size keeps varying. Most collections can be implemented in terms of a list. Although the order of the items is very much part of the list, often we ignore that order; for example, in a set, we want to insert items, remove items, and check if an item is present. Informally, the operations on a list are: create a new list, insert an item at the head of the list, remove the item that is the current head leaving the tail. The usual implementation of a list is as a linked-list ; however, there is nothing about an abstract list that requires that it be implemented as a linked structure. Nevertheless, list implementations so frequently use pointer based links, that the term linked list is almost synonymous with list. The List class presented here is implemented as a pointer-linked structure, with a list-component class providing the representation of the list-element, link pair. In the current chapter, we will introduce the major concepts of list behaviour and implementation via a xed-type List, i.e. a List of ints. In chapter 16 we will show how C++ templates may be used to provide a general List class, with the element-type supplied as a parameter. Eventually, we will discard this xed type list in favour of the template list. We will introduce additional topics that are needed for List, and which will occasionally come in useful elsewhere: friend classes and functions, overloading of the output-stream operator <<. Also, we will take the opportunity to reemphasize and review some issues of dynamically allocated (heap) objects already mentioned in connection with String in chapter 13: allocation using new, deallocation using delete, copy constructors, the destructor, and the assignment operator.

13.2

Friend Functions and Classes

Friendship can be used to allow specied (other) classes and functions access to the private section of a class. It is up to the class whose private section is to be made accessible to declare the friendship using the friend keyword. Although, at rst, it may seem that use of friend is an unwarranted licence to breach encapsulation, we will see two good reasons for using it: 151

Friend function or operator used where the syntax of operator or function call is not possible. Friend function or operator used where the syntax of operator or function call is neither natural nor appropriate. Friend class, where one class is constructed purely for the benet of another

Friend function, argument order Where it is convenient for a function or operator to access the private data of a class. Thus in class List, we declare: friend ostream& operator<<(ostream& os,const List& l);

The operator << takes two arguments: an object of the class ostream (usually cout which is dened in iostream.h, hence #include <iostream.h>), and a List object. This will be invoked as: List x; cout<< x; hence, because of the convention for inx operators in C++, the rst argument must be ostream (actually a reference), and the second a List.

object.member() syntax inappropriate If we want to concatenate two Lists to produce a third and not the way that C does: concatenate the second onto the rst the most natural way to invoke it would be: List x1, x2, x3; x3 = concat(x1, x2); or, using an operator +: x3 = x1 + x2; If concat() was declared as a public member of List, the rst call would have to be replaced with: x3 = x1.concatMemb(x2); or something similar, which is certainly not as natural as the rst. The declaration of concat would be: List& concat(List& source1, List& source2); We would still place the declaration in list.h, but outside the class. Likewise, we would place the denition in list.cpp. In the class itself, we would have: friend List& concat(List& source1, List& source2); giving concat the privilege of access to private members. There is a further discussion of these and related matters in chapter 13.

152

13.3
13.3.1

A List Class
Informal Specication of the Class

All we can say is that a List should store its elements in order. This can be expressed simply by: List = [] an empty list | e : List an element at the head of a list.

How would we like a List object to behave? Constructors As always, we need to be able to declare, dene and create List objects. Inspector We should be able to inspect the head of the list. Mutator We should be able to remove the head leaving the tail. Predicate We must be able to check if the list is empty. Input-output using an overloaded cout<< operator.

13.3.2

Class Interface

The interface for the List (of int) class is shown in listint.h. //-- listint.h -----------------------------------------------//j.g.c. 30/11/96, 31/12/96, 14/12/97 //linked list - int element //based on S.B. Lippmann, 1991, C++ Primer -- 2nd ed., Addison Wesley //and T.A. Budd, 1994, Classic Data Structures in C++, Addison Wesley //and my C ListADT, which is a C version of G. Rows Modula-2 List. //-----------------------------------------------------------#include <stdio.h> #include <assert.h> #include <iostream.h> class List; class ListCompt; typedef int ListEl; ostream& operator<<(ostream& os,const List& l); class List{ friend ostream& operator<<(ostream& os,const List& l); public: List(); List(const List& other); ~List(); List& operator = (const List & rhs); ListEl head() const; void insert(ListEl e); //insert element before head void remove(); //remove head void removeAll(); bool isEmpty() const; private: ListCompt* p_; }; class ListCompt{ 153

friend class List; friend ostream& operator<<(ostream& os,const List& l); private: ListCompt(ListEl element,ListCompt* l=0); ListEl e_; ListCompt* pnext_; };

class ListCompt The rst thing to note about List is an actually implementation detail: List is implemented using a class ListCompt, which represents single list-component. Dissection of ListCompt: 1. The representation comprises an element, e_ and a pointer to a ListCompt, i.e. the link to another component, pnext_. 2. friend class List; gives List objects the right to access the private members ListCompt objects. 3. friend ostream& operator<<(ostream& os,const List& l); likewise gives the operator << access to the private members. 4. There is one constructor, which constructs a ListCompt from an element, and a pointer. Notice that ListCompt* pnext=0 is defaulted, so that ListCompt(2); creates a list-component with an element 2, and with a pnext equal to the null-pointer value 0; in the implementation of List this is what we require. 5. Unusually, the constructor can private, since it will never be accessed except in class List, and that is a friend.

13.3.3

Dissection of Listint.h

1. We must forward declare classes List and ListCompt, class List; class ListCompt; because these appear as declarators, before the classes themselves are declared; the compiler can get a good way with knowing just that they are classes. 2. typedef int ListEl; declares ListEl and int to be type synonyms. 3. There are two constructors. As usual, note that each is distinguishable from the other by its signature. List() is the default constructor; it initialises the list object to be an empty list. List(const List& other); is the copy constructor and is used when constructing a List object from another String object, e.g. List x; followed by List y(x);. Again, we note the more subtle use of copy constructors : when an object is returned from a function. In addition, we reemphasise that copy constructors are important, and a common source of confusion and programming errors; if you need to revise copy-constructors, see chapter 12. 4. Assignment Operator List& operator=(const List& source);. = is called simply as x1 = x2;. Notice, that, as in String, chapter 13, that = returns a reference (List&) to the assigned string. This allows constructs of the form x1 = x2 = x3;. 5. We must repeat the importance of the assignment operator for heap objects like List and String: if they are not supplied by the programmer, the compiler will provide one which will na vely do a member-wise / shallow copy of just the apparent member ListCompt* p_;; almost certainly this is not what is desired, and disaster awaits, see later for a discussion on deep copy, section 13.5, and on the Big-Three, 13.6.

154

6. Destructor. ~List();. Again, because List objects use heap memory, rather than stack memory, we must be careful to provide a destructor that properly deallocates the memory used by List object. As with assignment and the copy constructor the compiler would have provided one, however, that would have deallocated only the member variables, and not, in the case of pointer p_, the linked structure that it points to. See section 13.6. 7. ListEl head() const; returns the element at the head of the list. It is non-destructive, i.e. the Listobject remains the same after, as before a call to head(); hence, we can declare it const the calling object remains constant. 8. void insert(ListEl e); inserts e to be the new head. 9. void remove(); removes the head element. 10. void removeAll(); completely empties the list 11. bool isEmpty() const; allows clients to check whether the list is empty. Note that isEmpty() may be declared + const+. 12. Representation. The private member used to represent the String is simply ListCompt* p_;, which points to the (heap based) next component, or, which is NULL, 0, if the list is empty.

13.4

List implementation code

The implementation code for the List class is described. The default constructor List::List() { p_=0; } creates empty lists, thus: List l1, l2; l1---> 0 (NULL) -- end of list l2---> 0 i.e. both are empty. Inserting items into the list: void List::insert(ListEl e) { ListCompt *pt= new ListCompt(e,p_); assert(pt!=0); p_=pt; } called as:

155

l1.insert(3); +---e--+next-+ l1---->| 3 | o--+----> 0 (end of list +------+-----+

l1.insert(7); +---e--+next-+ l1---->| 7 | o--+-----+ +------+-----+ | | +---------------------+ | | +--e---+next-+ +->| 3 | o--+----> 0 +------+-----+

Head: ListEl List::head() const { return p_->e_; } on l1, returns 7, and leaves l1 as it is. Remove: void List::remove() { ListCompt* pt=p_; p_=pt->pnext_; delete pt; } l1.remove(); causes the head element to be removed. +---e--+next-+ l1---->| 3 | o--+----> 0 +------+-----+ The predicate isEmpty(): bool List::isEmpty() const { return (p_==0); } simply checks whether the list pointer points to the end (0 / NULL). We discuss the remainder of the implementation in separate sections. 156

13.5
13.5.1

Deep Copy: Copy Constructor & Assignment


Copy constructor

The copy constructor: List::List(const List& source) { if(source.isEmpty())p_=0; else{ ListCompt* pp=source.p_; //cursor to source ListCompt* pt=new ListCompt(pp->e_,0); p_=pt; while(pp->pnext_!=0){ pp=pp->pnext_; pt->pnext_=new ListCompt(pp->e_,0); pt=pt->pnext_; } } } makes a complete copy of source into the calling object. Using diagrams like those in the previous section it is easy to demonstrate its operation. As we have noted, if a copy constructor is not programmed, the compiler will provide a default one which will will have the simplistic eect of member-wise or shallow copy only the explicit members are copied; i.e. the default copy constructor would be equivalent to the following: List::List(const List& source)//memberwise copy { p_=source.p_; } This may work in limited cases, but consider again the following example: List l1, l2; l1.insert(3); +---e--+next-+ l1---->| 3 | o--+----> 0 +------+-----+ l1.insert(7); +---e--+next-+ l1---->| 7 | o--+-----+ +------+-----+ | | +---------------------+ | | +--e---+next-+ +->| 3 | o--+----> 0 +------+-----+ l2 = l1; //default, shallow copy

157

l2---->+---e--+next-+ l1---->| 7 | o--+-----+ +------+-----+ | | +---------------------+ | | +--e---+next-+ +->| 3 | o--+----> 0 +------+-----+ With shallow copy, l2 simply becomes an alias for l1; hence, any changes to either is reected in the other. There are situations in which this is acceptable, but deep-copy is what client programmers would normally expect.

13.5.2

Deep-copy assignment

As we have noted in chapter 13 (Array), the deep-copy assignment operator has an almost identical implementation to a copy constructor: List& List::operator = (const List& source) { if(this!=&source){ //beware of listA=listA; removeAll(); if(source.isEmpty())p_=0; else{ ListCompt *pp=source.p_; //cursor to source ListCompt *pt=new ListCompt(pp->e_,0); p_=pt; while(pp->pnext_!=0){ pp=pp->pnext_; pt->pnext_=new ListCompt(pp->e_,0); pt=pt->pnext_; } } } return *this; } Notice the test if(this!=&source); otherwise the assignment does nothing. This is not, as might seem at a rst glance, for the sake of eciency (no use copying an object onto itself), but if the test were not present, the assignment: list1 = list1; would have disastrous consequences, since the rst task is to deallocate all the components of the target (left-handside of the assignment), which, in this case is also the source!

13.5.3

Destructor for heap objects

The same care must be taken for the destructor ~List(), as for assignment and copy constructor. List::~List() { removeAll(); }

158

void List::removeAll()//see [Budd, 1994], p.202 { ListCompt *pt,*pp; pp=p_; while(pp!=0){ pt=pp->pnext_; pp->pnext_=0; delete pp; pp=pt; } p_=0; } removeAll() moves from the head of the list, deleting components as it goes. Again, if no explicitly programmed destructor is provided, the compiler will oblige, but again with a na ve version: it will na vely deallocate the pointer p_, which, unlike the remainder of the object, resides on the stack like local variables; thus, the remainder of the list will become garbage still allocated, but cannot be referenced.

13.6

The Big-Three

The C++ FAQs (Cline et al. 1999b) uses the term the Big-Three for the three functions mentioned in the previous section, section 13.5: 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 quite scrupulous testing to detect. The lack of a proper destructor would be particularly dicult to detect it simply causes garbage, whose eect 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).

13.6.1

Defence against naive defaults

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: List& operator = (const List & 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. 159

13.7

Overloading the Stream Output Operator

The stream output operator << is overloaded as follows: ostream& operator<<(ostream& os,const List& source) { os<<"[ "; ListCompt *pp=source.p_; //cursor to source while(pp!=0){ if(pp!=source.p_)cout<<", "; os<<pp->e_; pp=pp->pnext_; } os<<" ]"<<endl;; } ostream is a output-stream class, so that the parameter os is an output-stream object; the familiar cout is one such object. It is important, to make it conform to its pattern of operation for the built-in types, that << returns a reference to the ostream object. This allows concatenated calls to it, e.g.: cout<< x<< y<< z<< ";"<< endl;

13.8

A simple client program

The program listtst.cpp demonstrates the use of the List class. //-- listst.cpp -----------------------------------------//j.g.c. 29/11/96, 31/12/96 //tester for List class //-------------------------------------------------------#include "listint.h" int main(void) { List x,w,v; x.insert(4); x.insert(3); x.insert(2); x.insert(1); List y(x); List z=x; w=x; v=x; cout<< "Head = "<< x.head()<< endl; //note that the following destroys x cout<< "List x ="<<endl; while(!x.isEmpty()){ cout<< x.head()<< endl; x.remove(); } cout<< "List y ="<<endl; while(!y.isEmpty()){ cout<< y.head()<< endl; 160

y.remove(); } cout<< "List z ="<<endl; while(!z.isEmpty()){ cout<< z.head()<< endl; z.remove(); } cout<< "List w ="<<endl; while(!w.isEmpty()){ cout<< w.head()<< endl; w.remove(); } cout<< "List v via operator <<"<<endl; cout<< v<< endl; return 0; }

161

Chapter 14

Correctness and Specication


14.1 Introduction

Up to now we have been very informal in our specication of classes and their interface functions. Usually we have started with an informal requirements specication based mainly on the desired behaviour, e.g. of a List, or Stack, or String, leading to specication of a set of interface functions. Since the main motivation for data-abstraction and classes is the provision of software components that can truly be used as components, i.e. black-box style. In other words, the supplier must provide, in addition to correct software, a clear and unambiguous specication of the class. Otherwise, the client programmer must make guesses about how to use the class, and about the behaviour of its functions. We will use the analogy of a simple electrical appliance a toaster. Taking the analogy to an extreme, we note some of the details we would like specied: (a) that it is a toaster (e.g. not a kettle), (b) what voltage it requires, (c) how many slices it takes, (d) has it a defrost mode. A related and equally important issue is testing. In order for a class and / or its functions to be testable, we must have specications: clearly stated, unambiguous, testable. Correctness and specication go hand in hand. We address a number of aspects of specication: Syntax: how each of the interface functions is called what are the types of the parameters, and of any return value. Semantics (meaning). We will address two semantics at two levels: Semantics of individual interface functions. These are usually specied in terms of (a) pre-conditions which specify constraints on the values of parameters, and on the state of the objects before the function is called, and (b) post-conditions which specify the state of the object, and any return values, after the function has nished. Ideally, pre- and post-conditions are specied using a formal mathematical model of the class. Often, however, we make do with less formal specications, using natural language or some pictorial model. Although formal specication is notably absent from most methodologies, we note two design heuristics employed in object-oriented analysis and design, namely, Design-by-Contract, (Meyer 1997), and Responsibility-driven Design, (Budd 1997b). Semantics of the class itself: an axiomatic / algebraic specication. In addition, we note that, at some stage, a specication of implementation may need to be provided. However, this is a lesser concern than the external specications. We have not time to discuss the diculties of software testing except to not that testing done as an after-thought of design is very dicult: it is often said that testing can reveal the presence of errors, but can never prove the absence of errors. We will show that axiomatic specication provides a rm basis for a test program. 162

14.2

Syntactic Specication

The syntax of a class (and of its interface functions) is given by the signature s of the interface functions (implemented as prototype s in C++). A signature may be dened abstractly as: add: int x int -> int which states that function add takes two ints and returns an int, i.e. all the information to call it properly. This can be implemented as a C++ prototype : int add(int, int); Given the declaration of the functions type, correctness of the syntax of a call to it can be checked by the normal compiler type-checking. Actually, strict type checking is a most signicant step in assuring the correctness of software, and should always be applied to a maximum i.e. use languages with strong type-checking ; in C++, always ensure that the compiler is switched to maximum fussiness e.g. -pedantic-errors -Wall. The syntax of a class its interface functions is specied in the interface / protocol given in a (#included) header le. The interface for a template Stack class is shown in public part of le stack.h. //--- stack.h -----------------------------------------------// template stack - using list // j.g.c. 2/1/97 //-----------------------------------------------------------#ifndef STACKH #define STACKH #include "../listt/listt.h" template <class T> class Stack; template <class T> ostream& operator<<(ostream& os,const Stack<T>& s); template <class T> class Stack{ friend ostream& operator<<(ostream& os,const Stack<T>& s); public: Stack(); Stack(const Stack<T>& source); ~Stack(); Stack& operator = (const Stack<T>& source); T top() const; void push(T e); void pop(); void removeAll(); bool isEmpty() const; int size() const; private: List<T> data_; }; #endif However, we note that there is no similarly straightforward way to specify semantics, i.e. the code in the function can perform just about any function, despite the name add. Specication of semantics is the subject of the remainder of the chapter.

163

14.3
14.3.1

Semantic Specication
Introduction

As we have mentioned, there are two quite distinct approaches to semantic specication: Axiomatic specication: which makes statements the axioms of the class / ADT about values and the interaction of values and operations. The axiomatic specication provides a good basis for a test module. Model based specication: which, based on a mathematical or other model, makes statements about the eect of operations. Usually this takes the form of specifying validity requirements on values of arguments (inputs) pre-conditions, and the actual eect of the operation post-condition. In practice, there are various degrees of formality with which the pre- and post-conditions can be specied. If a suitable mathematical model is unavailable, then maybe diagrams (e.g. showing a List before, and after, an insert) or even English (e.g. we cannot remove from an empty List) can be used. The notion of Design-by-Contract, proposed by Meyer, Martin and others, is founded on model-based specication the pre-conditions and post-conditions specify the respective responsibilities of, and expected benets to, both the supplier and client. As part of the contract that allows use of a function, a client (the caller) of a function must ensure that the pre-conditions are met, the function itself (supplier) may assume that these are met. The post-condition species what must be completed by the operation (suppliers obligation), and what benets the client may expect.

14.3.2

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 dened in assert.h. The following shows an example use of assert to check the pre-condition of an array index operator; h_ is the upper-bound of the array. template <class T> T& array<T>::operator[](Uint i) const { assert(i<=h_); return pdata_[i]; } When the following code is executed: int main() { array<int> a(9); int i,n=10; for(i=0;i<n;i++)a[i]=2*i+1; //a[2000]=10; // demonstrates assert the array index 2000 will not satisfy the condition <=h_ (9) and an error will be reported and the program halted: exe: array.h:71: int & array<int>::operator [](unsigned int) const: Assertion <=h_ failed. IOT trap/Abort 164

If, for performance or other reasons, the assertion can be disabled either by including the compiler switch -DNDEBUG, e.g.: g++ -o exe tarr.cpp -pedantic-errors -Wall -DNDEBUG or by inserting the pre-processor denition: #define NDEBUG in the source le.

14.3.3

Axiomatic Specication

We take the example of Stack; we express the axioms in C++, together with an English description. Normalisation axioms We have one normalisation axiom for each non-normal mutator, applied to each normal form (the normal forms for Stack are Stack<T> s the empty Stack, and s.push(e); from these any Stack may be generated): 1. popping an empty Stack generates an error. Stack <int> s; // s empty s.pop(); // error 2. popping a Stack, onto which an element has just been pushed, results in the original Stack. sold=s; s.push(e); s.pop(); assert(s==sold); // note that we have not implemented == for Stack Selector Axioms We have one selector axiom for each selector / accessor, applied to each normal form: 1. Taking the top of an empty Stack generates an error. Stack <int> s; // s empty int e = s.top(); // error 2. Taking the top a Stack, onto which an element has just been pushed, results in that element. s.push(e); assert(e==s.top()); Predicate Axioms We have one predicate axiom for each predicate, applied to each normal form: 1. A newly created (hence empty) Stack results in True for isEmpty(). Stack <int> s; // s empty assert(s.isEmpty()); 2. A Stack, onto which an element has just been pushed, results in False for isEmpty(). s.push(e); assert(!isEmpty());

165

14.3.4

Model-based Specication: Pre- and Post-conditions

We take the example of Stack; we express the pre- and post-conditions for each operation in C++, together with an English description. Constructor Pre-condition: none Post-condition: s.isEmpty(), where s is the Stack just constructed. Push Pre-condition: none Post-condition: e == s.top()), where e is the element just pushed Top Pre-condition: !s.isEmpty(). Post-condition: Stack remains unchanged. Pop Pre-condition: !s.isEmpty(). Post-condition: Impossible to make a veriable statement.

14.4

Design-by-Contract

The notion of Design-by-Contract uses the analogy of the contract that may be made between the supplier of a service or goods and the client (consumer). Take the example of a client / consumer who buys a toaster. We can specify the toaster in terms of obligations and benets as follows: Obligations Client (Pre-conditions) Client / user must ensure: - Attempt to toast only fresh, untoasted bread. - Connect to proper supply Benefits

(Post-condition) - toasted bread

Supplier (post-condition) Click-up when correct level of toasting reached

(supplier may assume pre-cond) Need not check if toast already toasted

We can similarly dene the Stack operation push(). Obligations Client (Pre-conditions) - none Supplier (post-condition) - correctly insert the element (Post-condition) - element on Stack (supplier may assume pre-cond) - none Benefits

166

14.5

Pre-conditions and Defensive Programming

The pre-condition for both top() and pop() is that the Stack is not empty (!s.isEmpty()). The caller breaks the contract if it applied either pop or top to an empty Stack. Consequently, the operations should not have to check the pre-condition. However, there are two reasons why it may be wise to do the check after all: If the consequences of performing the operation could be very costly, e.g. destruction of a database, corruption of your hard disk. If the check can be done at little expense of eciency or code size. If you decide that it would be necessary to provide an understandable error message. For example, in Linux, taking the top of an empty Stack would most likely result in a Segmentation Error this is hardly as revealing or useful as a message Cannot top and empty Stack halting, or one generated by an assertion (see above). In terms of our Design-by-Contract toaster metaphor, although it is a pre-condition that the client does not connect a 440 volt three-phase supply to the toaster, we think it is a sensible precaution for the supplier to t an appropriate fuse; the fuse is cheap compared to replacing a destroyed toaster, or an electrocuted client! On the other hand, although we could provide an sensor to check whether we were about to toast already nished toast, this may be much too expensive in comparison with the expense of the consequences merely a burnt slice of toast.

167

Chapter 15

Templates
15.1 Introduction

There are many examples of cases where we develop a module, e.g. Array, or List, which manipulates objects of a particular type, e.g. int, only to nd that we need to adapt it to work for float or other types, or, indeed, for user dened classes. In the Array and List examples, the element-type is the only part which changes; thus, it seems a pity to have to have to develop additional modules. 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 generiticity generic software. This chapter discusses templates, the C++ form of generiticity. 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 specied at compile time. Of course, the main objective is reuse. As we have argued before, the least acceptable form of reuse is duplicateand-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 rst, 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. Note: in this chapter there is some repetition between the sections on Array and List. We hope this isnt too o-putting; it will be rectied in the next revision of the document.

168

15.2
15.2.1

Template Functions
Overloaded Functions recalled

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; } void swap(float& a, float& b){ int temp = a; a = b; b = temp; }

Overloading function names Ad Hoc Polymorphism The overloading of function names is yet another example of polymorphism, in this case called ad hoc polymorphism. It is called ad hoc because, even though the name of the function (swap) is the same, there is nothing to ensure that they are similar in other than name; e.g. we could dene int swap(String s) which gives the length of the String!

15.2.2

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 dened classes, the only requirement is that the assignment operator = is dened. //--- 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() { int a = 10, b = 12; float x = 1.25, y = 1.30; cout<< "a, b = "<< a<< " " << b<< endl; swap(a, b); cout<< "a, b (swapped) = "<< a<< " " << b<< endl; cout<< "x, y = "<< x<< " " << y<< endl; swap(x, y); cout<< "x, y (swapped) = "<< x<< " " << y<< endl; string s1("abcdef"), s2("1234"); 169

cout<< "s1 = "<< s1<< endl; cout<< "s2 = "<< s2<< endl; swap(s1, s2); cout<< "swapped: "; cout<< "s1 = "<< s1<< endl; cout<< "s2 = "<< s2<< endl; return 0; }

15.3

Polymorphism Parametric

Templates are another form of polymorphism ; using templates one can dene, for example a polymorphic List a List of int, or float, or string, etc... Likewise we can dene 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 specied at compile time statically. Hence, the polymorphism oered by templates is called parametric polymorphism.

15.4

Alternative Approaches to Generiticity

Already in the xed element-type List described in Chapter 14, we saw that we could, to some extent, anticipate templates using: typedef int ListEl; //... void insert(ListEl e); This would allow us to create, at programming time, multiple List classes, e.g. Listint, ListFloat, ListString etc. But, the major shortcoming is that each List class must have a separate name, and, hence, must occupy partial status as a separate module hence needs some degree of separate documentation, testing and maintenance. A similar, slightly more sophisticated, scheme is possible in Modula-2, where separate ListElement modules may be dened. However, the problem mentioned above the need for separate List modules remains. Java has no templates. Instead, it relies on an extreme form of inclusion polymorphism. In Java, every object inherits from a base class called Object. This, coupled with the fact that all object identiers are references, allows Java to dene something analogous to Array (in Java, Vector) with the element type Object* (pointer-to-Object).

15.5
15.5.1

Template Array
Class declaration and implementation

The declaration for the template Array class is shown in le arrayt.h. The rst thing to note about templates, at least using the GNU C++ compiler, is that both declaration and implementation (normally .cpp le) must be #included. Hence we include them in the same le, here arrayt.h.

170

//----- arrayt.h ---------------------------------// j.g.c. 6/1/99, 8/1/99, 9/1/99 // copied from arrayt.cpp, arrayt.h; // templates //---------------------------------------------------#ifndef ARRAYTH #define ARRAYTH #include <iostream> #include <assert.h> template <class T> class Array{ public: Array(const unsigned int len= 0); Array(const unsigned int len, const T& val); Array(const Array<T>& source); ~Array(); Array& operator=(const Array<T>& source); void copy(const Array<T>& source); T& operator[](unsigned int i) const; unsigned int length() const; private: unsigned int len_; T* dat_; }; template <class T> ostream& operator<<(ostream& os, const Array<T>& a); template <class T> Array<T>::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; } template <class T> Array<T>::Array(const unsigned int len, const T& val) : len_(len) { if(len== 0){dat_ = 0; return;} dat_ = new T[len_]; assert(dat_!= 0); for(unsigned int i=0; i< len_; i++)dat_[i]= val; } template <class T> Array<T>::Array(const Array<T>& source){ copy(source); } template <class T> Array<T>::~Array(){ delete [] dat_; } template <class T> Array<T>& Array<T>::operator=(const Array& source){ if(this!= &source){ // beware a= a; delete [] dat_; copy(source); } return *this; } template <class T> void Array<T>::copy(const Array<T>& source){

171

len_= source.length(); if(len_== 0){dat_ = 0; return;} dat_ = new T[len_]; assert(dat_!= 0); for(unsigned int i= 0; i< len_; i++)dat_[i] = source.dat_[i]; } template <class T> T& Array<T>::operator[](unsigned int i) const { assert(i<len_); return dat_[i]; } template <class T> unsigned int Array<T>::length() const { return len_; } // notice no Array<T>:: -- *not* a member template <class T> ostream& operator<<(ostream& os, const Array<T>& a) { unsigned int len= a.length(); os<< "[" << len << "]{ "; for(unsigned int i=0; i< len; i++){ os<< a[i]; if(i!= len-1)os<<", "; } 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 identier. This is done as follows: template <class T> class Array Here we are saying: Array uses an as yet unspecied type/class T. 2. When we want to dene 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 Array<int> (a matrix): Array <Array<int> > c4(3); Thus, as with (parameterised) functions, we can invoke the abstraction with any number of (dierent) parameters. 3. Likewise, if we ever need to declare a Array object as a parameter, we use the form: Array(const Array<T> & other); 172

i.e. other is a reference to a Array object of classT elements. 4. 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. 5. In GNU C++, the Array<int> class, for example, is instantiated, at compile time, by a form of macroexpansion. This is the reason that function implementations must also be in the .h le.

15.5.2

A simple client program

As we have seen, there is little to templates. The program testart.cpp demonstrates the use of the template Array class instantiating, simultaneously, Arrays of both int, double and Array<int> (an array of int arrays). //----- testart.cpp ---------------------------------// j.g.c. 6/1/99, 8/1/99, 9/1/99 // copied from testar12 // g++ -o exe testart.cpp -pedantic-errors //---------------------------------------------------#include "arrayt.h" int main() { Array <int> c1(5); cout<< "Array <int> c1(5): "<< c1; Array <int> c2(5, 127); cout<< "Array <int> c2(5, 127): "<< c2; Array <double> c3(3, 3.14159); cout<< "Array <double> c3(3, 3.14159): "<< c3; Array <Array<int> > c4(3); cout<< "Array <Array<int> > c4(3): "<< c4; cout<< endl; return 0; }

15.6
15.6.1

Template List
Class Interface

The interface / declaration for the template List class is shown in le listt.h. The rst thing to note about templates, at least using the GNU C++ compiler, is that both declaration and implementation (normally .cpp le) must be #included. Hence we include them in the same le, here listt.h. However, below, we show only the declaration part of listt.h. //--- listt.h -----------------------------------------------//j.g.c. 30/11/96, 31/12/96, 1/1/97, 22/3/97 //linked list - template //based on S.B. Lippmann, 1991, C++ Primer -- 2nd ed., Addison Wesley //and T.A. Budd, 1994, Classic Data Structures in C++, Addison Wesley //and my C ListADT, which is a C version of G. Rows Modula-2 List. //-----------------------------------------------------------173

#ifndef LISTTH #define LISTTH #include <stdio.h> #include <iostream.h> template <class T> class List; template <class T> ostream& operator<<(ostream& os,const List<T>& l); template <class T> class ListCompt; template <class T> class List{ friend ostream& operator<<(ostream& os,const List<T>& l); public: List(); List(const List<T> & other); ~List(); List & operator = (const List<T> & rhs); T head() const; void insert(T e); //insert element before head void remove(); //remove head List<T> & tail(); void removeAll(); //empty the list bool isEmpty() const; int length() const; private: ListCompt<T> * p_; }; template <class T>class ListCompt{ friend class List<T>; friend ostream& operator<<(ostream& os,const List<T>& l); private: ListCompt(T e,ListCompt *pnext=0); T e_; ListCompt<T> * pnext_; };

15.6.2

Dissection of Listt.h

1. The class / type parameter. As already mentioned, Listt is a parameterised class the type / class of its element is given as a class parameter. 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 identier. This is done as follows: template <class T> class List Here we are saying: List uses an as yet unspecied type / class T. 2. When we want to dene an actual List in a program, we do so as, e.g.: List<int> s; List<float> t; List<String> x; Thus, as with (parameterised) functions, we can use the abstraction with any number of (dierent) parameters. 3. Likewise, if we ever need to declare a List object as a parameter, we use the form: List(const List<T> & other); 174

i.e. other is a reference to a List object of class or type T elements. 4. 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. 5. In GNU C++, the List<int> class, for example, is instantiated, at compile time, by a form of macroexpansion. This is the reason that function implementations must also be in the .h le. .

15.6.3

Implementation of functions

The implementations of selected List class functions are shown below as we have already noted, both declaration and implementation must be #included, i.e. both are in listt.h. //------------------------------------------------------------template <class T>ListCompt<T>::ListCompt(T e,ListCompt<T> *pnext) { e_=e; pnext_=pnext; } template <class T>List<T>::List() { p_=0; } template <class T>List<T>::List(const List<T> & source) { if(source.isEmpty())p_=0; else{ ListCompt<T> *pp=source.p_; //cursor to source ListCompt<T> *pt=new ListCompt<T>(pp->e_,0); p_=pt; while(pp->pnext_!=0){ pp=pp->pnext_; pt->pnext_=new ListCompt<T>(pp->e_,0); pt=pt->pnext_; } } }

15.6.4

Dissection of functions

1. As before, anywhere we mention List, we must mention that it depends on class / type parameter T: template <class T>List<T>::List(const List<T> & source) 2. In addition, anywhere we mention List we must give the type parameter in this case unknown T.

15.7

A simple client program

As we have seen, there is little to templates. The program listtst.cpp demonstrates the use of the template List class instantiating, simultaneously, Lists of both int and of float. 175

//--listtst.cpp--------------------------------------------//j.g.c. 29/11/96 //j.g.c. 31/12/96//j.g.c. 1/1/97, 5/1/97 //tester for template List class //-------------------------------------------------------#include "listt.h" int main(void) { List<float> x,w; x.insert(4.4); x.insert(3.3); x.insert(2.2); x.insert(1.1); List<float> y(x); List<float> z=x; //NB. equiv. to List<float> z(x); see prev. line w=x; List<int> i; i.insert(3); i.insert(2); i.insert(1); cout<< "Head = "<< x.head()<< endl; cout<< "List y ="<<endl; cout<< y<< endl; cout<< "List z ="<<endl; cout<< z<< endl; cout<< "List w ="<<endl; cout<< w<< endl; List<float> v; v=y.tail(); cout<< "List v (tail of y) ="<<endl;

cout<< v<< endl;

cout<< "List i via operator <<"<<endl; cout<< i<< endl; return 0; }

176

Chapter 16

Queue and Stack Classes


16.1 Introduction

This chapter introduces a Queue class, and a Stack class. Like List these are collection classes, i.e. they contain collections of objects. Also, like List, we implement them as template classes. A further similarity is in the implementation: we use linked data structures. Because of the relative correspondence of behaviour, we nd that we can implement a Stack using a List, i.e. a Stack is composed of a List, and we call insert(.), head() etc. in implementations of Stack interface functions. Since the implementations of these classes have so much in common with the two List classes, our descriptions can be kept brief. First we describe Queue, then Stack.

16.2
16.2.1

A Queue Class
Informal Specication of the Class

The Lists developed in chapters 14 and 16 are Last-In-First-Out (LIFO) containers; i.e. if we have: List<int> x; int i; x.insert(123); x.insert(456); i = x.head(); // results in 456

However, a Queue is First-In-First-Out (FIFO), i.e. an element joins the Queue at the rear, and leaves / removes from the front.

16.2.2

Class Interface

The interface for the template Queue class is shown in le qt.h. As with the template List, we must include the function implementations in the header le.

177

//--- qt.h -----------------------------------------------//j.g.c. 2/1/97 //j.g.c. 22/2/97 istream >> added. //queue - template //based G. Rows Modula-2 QueueADT, and on my template list. //-----------------------------------------------------------#ifndef QTH #define QTH #include <stdio.h> #include <assert.h> #include <iostream.h> template <class T> class QueueCompt; template <class T> class Queue; template <class T> ostream& operator<<(ostream& os,const Queue<T>& q); template <class T> istream& operator>>(istream& is, Queue<T>& q); template <class T> class Queue{ friend ostream& operator<<(ostream& os,const Queue<T>& q); friend istream& operator>>(istream& is,Queue<T>& q); public: Queue(); Queue(const Queue<T>& other); ~Queue(); Queue& operator = (const Queue<T>& source); T front() const; int length() const; void join(T e); //insert element at rear void remove(); //remove element from front Queue<T>& leave(); //as remove, value returning void removeAll(); //empty the list bool isEmpty() const; private: QueueCompt<T>* p_; }; template <class T>class QueueCompt{ friend class Queue<T>; friend ostream& operator<<(ostream& os,const Queue<T>& q); friend istream& operator>>(istream& is,Queue<T>& q); private: QueueCompt(T e, QueueCompt<T>* pnext=0); T e_; QueueCompt<T>* pnext_; }; //------------------------------------------------------------template <class T>QueueCompt<T>::QueueCompt(T e, QueueCompt<T>* pnext) { e_=e; pnext_=pnext; } template <class T>Queue<T>::Queue() { p_=0; }

178

template <class T>Queue<T>::Queue(const Queue<T>& source) { if(source.isEmpty())p_=0; else{ QueueCompt<T> *pp=source.p_; //cursor to source QueueCompt<T> *pt=new QueueCompt<T>(pp->e_,0); p_=pt; while(pp->pnext_!=0){ pp=pp->pnext_; pt->pnext_=new QueueCompt<T>(pp->e_,0); pt=pt->pnext_; } } } template <class T> Queue<T>& Queue<T>:: operator = (const Queue<T>& source) { if(this!=&source){ //beware of queueA=queueA; removeAll(); if(source.isEmpty())p_=0; else{ QueueCompt<T> *pp=source.p_; //cursor to source QueueCompt<T> *pt=new QueueCompt<T>(pp->e_,0); p_=pt; while(pp->pnext_!=0){ pp=pp->pnext_; pt->pnext_=new QueueCompt<T>(pp->e_,0); pt=pt->pnext_; } } } return *this; } template <class T>Queue<T>::~Queue() { removeAll(); } template <class T> void Queue<T>::join(T e) { if(isEmpty()) p_=new QueueCompt<T>(e,0); else{ QueueCompt<T>* pp = p_; while(pp->pnext_!=0)pp=pp->pnext_; pp->pnext_=new QueueCompt<T>(e,0); } } template <class T> T Queue<T>::front() const { return p_->e_; } template <class T> int Queue<T>::length() const { QueueCompt<T> *pp=p_; //cursor to components int i=0; while(pp!=0){

179

pp=pp->pnext_; i++; } return i; } template <class T> void Queue<T>::remove() { QueueCompt<T> * pt=p_; p_=pt->pnext_; delete pt; } template <class T> Queue<T>& Queue<T>::leave() { Queue<T>* panother = new Queue(*this); //Ex. why would Queue<T> another(*this); ... *not* work? panother->remove(); return *panother; } template <class T> void Queue<T>::removeAll() //see [Budd, 1994], p.202, and List::removeAll() { QueueCompt<T> *pt,*pp; pp=p_; while(pp!=0){ pt=pp->pnext_; pp->pnext_=0; delete pp; pp=pt; } p_=0; } template <class T>bool Queue<T>::isEmpty() const { return (p_==0); } template <class T> ostream& operator<<(ostream& os,const Queue<T>& source) { os<<"Queue: <"<< source.length()<< "> f[ "; QueueCompt<T> *pp=source.p_; //cursor to source while(pp!=0){ if(pp!=source.p_)cout<<", "; os<<pp->e_; pp=pp->pnext_; } os<<" ]r"<<endl<< endl; } template <class T> istream& operator>>(istream& is, Queue<T>& dest) { T e; char c; while(1){ int cnext=is.peek();

180

if(cnext==\n){ is.get(c);//to flush the \n break; } else if(cnext==EOF)break; else{ is>> e; dest.join(e); } } return is; } #endif

class QueueCompt As with List, Queue uses a node class QueueCompt. QueueCompt is exactly the same as ListCompt, except that we have provided istream& operator>>, i.e. so that Queue objects may be read using e.g. Queue<int> qi; cin>> qi.

16.3

Queue implementation code

As with the interface, the details should be easily understandable by those who have understood List implementation; if you are in any doubt, revise the descriptions of the List of int class given in chapter 14, and of the template List class given in chapter 16.

16.4

A simple client program

The program qttst.cpp demonstrates the use of the Queue class. //----- qttst.cpp ---------------------------------------//j.g.c. 2/1/97 //tester for template Queue class //-------------------------------------------------------#include "qt.h" int main(void) { Queue<float> x,w; x.join(4.4); x.join(3.3); x.join(2.2); x.join(1.1); Queue<float> y(x); Queue<float> z=x; w=x; Queue<int> i; i.join(3); i.join(2); i.join(1); cout<< "Front of x = "<< x.front()<< endl; //note that the following destroys the queue cout<< "Queue x ="<<endl; while(!x.isEmpty()){ cout<< x.front()<< endl; x.remove(); } cout<< "Queue y ="<<endl; cout<< y<< endl; cout<< "Queue z ="<<endl; cout<< z<< endl; 181

cout<< "Queue w ="<<endl;

cout<< w<< endl;

Queue<float> v; v=y.leave(); cout<< "List v (leave of y) ="<<endl; cout<< v<< endl; cout<< "Queue i ="<<endl; cout<< i<< endl; Queue<char> qc; cout<< "type queue of chars:"<< endl; cin>> qc; cout<< qc; Queue<int> qi; cout<< "type queue of ints:"<< endl; cin>> qi; cout<< qi; return 0; }

16.5
16.5.1

A Stack Class
Informal Specication of the Class

Like the Lists developed in chapters 14 and 16, a Stack is a Last-In-First-Out (LIFO) containers. We push elements onto the Stack; we can inspect the top element using a top function; the top element can be removed using pop; we also need the predicate isEmpty(), and some constructors. Since Stack objects will occupy heap storage, we need to be careful to provide the Big-Three : copy constructor, assignment operator, and destructor.

16.5.2

Class Interface

The interface for the template Stack class is shown in le stack.h. Again, as with all template classes, we include the function implementations in the header le. //--- stack.h -----------------------------------------------// template stack - using list // j.g.c. 2/1/97 //-----------------------------------------------------------#ifndef STACKH #define STACKH #include "../listt/listt.h" template <class T> class Stack; template <class T> ostream& operator<<(ostream& os,const Stack<T>& s); template <class T> class Stack{ friend ostream& operator<<(ostream& os,const Stack<T>& s); public: Stack(); Stack(const Stack<T>& source); ~Stack(); Stack& operator = (const Stack<T>& source); T top() const; 182

void push(T e); void pop(); void removeAll(); bool isEmpty() const; int size() const; private: List<T> data_; }; //------------------------------------------------------------template <class T>Stack<T>::Stack(): data_(){} template <class T>Stack<T>::Stack(const Stack<T> & source): data_(source.data_) {}

template <class T> Stack<T> & Stack<T>:: operator = (const Stack<T> & source) { data_=source.data_; return *this; } template <class T>Stack<T>::~Stack() { data_.removeAll(); } template <class T> void Stack<T>::push(T e) { data_.insert(e); } template <class T> T Stack<T>::top() const { return data_.head(); } template <class T> void Stack<T>::pop() { data_.remove(); } template <class T> void Stack<T>::removeAll() { data_.removeAll(); } template <class T>bool Stack<T>::isEmpty() const { return data_.isEmpty(); } template <class T>int Stack<T>::size() const { return data_.length(); } template <class T>

183

ostream& operator<<(ostream& os,const Stack<T>& source) { os<< "Stack - top "; os<< source.data_; } #endif Stack uses a List for its representation.

16.6

Stack implementation code

Stack functions simply call the appropriate List functions.

16.7

A simple client program

The program stacktst.cpp demonstrates the use of the Stack class. //--- stacktst.cpp --------------------------------------//j.g.c. 2/1/96 //tester for template Stack class //-------------------------------------------------------#include "stack.h" int main(void) { Stack<float> x,w; x.push(4.4); x.push(3.3); x.push(2.2); x.push(1.1); Stack<float> y(x); Stack<float> z=x; w=x; Stack<int> i; i.push(3); i.push(2); i.push(1); cout<< "top of x = "<< x.top()<< endl; cout<< "size of x = "<< x.size()<< endl; cout<< "Stack x <<"<<endl; cout<< x<< endl; x.pop(); cout<< "x popped ...size of x = "<< x.size()<< endl; cout<< "Stack x <<"<<endl; cout<< x<< endl; cout<< "x.isEmpty(): " << x.isEmpty()<< endl; x.removeAll(); cout<< "x removeAll ...size of x = "<< x.size()<< endl; cout<< "Stack x <<"<<endl; cout<< x<< endl; cout<< "x.isEmpty(): " << x.isEmpty() << endl; cout<< cout<< cout<< cout<< "Stack "Stack "Stack "Stack y z w i <<"<<endl; <<"<<endl; <<"<<endl; <<"<<endl; cout<< cout<< cout<< cout<< y<< z<< w<< i<< endl; endl; endl; endl;

184

return 0; }

185

Chapter 17

A Multi-class Program Graphics


17.1 Introduction

This chapter shows how a fairly complex program (involving computer graphics) can be built from a set of individually simple class components. We will introduce an abstract base Figure class this is similar to the Shape class used as an example in many books. The thing about an abstract base class is that it is used to establish the interface for a family of classes (e.g. Rectangle, Triangle, Circle, etc.). These derived classes inherit the interface from Figure, and supply their own versions of virtual interface functions overriding. We will develop an Image class to provide a pictorial representation for drawn graphics. We will develop this using our template Array class (chapter 16). Then we will develop a Matrix class as an array-of-arrays a matrix is a two-dimensional array. The Image class will use the Martix class as representation.

17.2

Matrix class

The interface for a template matrix class is shown in le matrix.h. We have not bothered to provide matrix with an assignment operator, but we have used the trick of declaring private operator= with blank body, to ensure that a default one is never used. //------ matrix.h -------------------------------------------//j.g.c. 10/1/99 //based on the Array class and matrix class of //T.A. Budd, Classic Data Structures in C++, Addison Wesley 1994 // see earlier matrix class (1997); now no need for generality // of non-zero index base //-----------------------------------------------------------#ifndef MATRIXH #define MATRIXH #include "../ch16/arrayt.h" template <class T> class Matrix{ public: explicit Matrix(unsigned int nr= 1, unsigned int nc= 1); Matrix(unsigned int nr, unsigned int nc, T val); Matrix(const Matrix<T>& other); ~Matrix(); 186

Array<T>& operator[](unsigned int r) const; unsigned int nrows() const; unsigned int ncols() const; private: Array<Array<T>* > prows_; Matrix<T>& operator= (const Matrix<T>& other){}; }; //--- non-member; but NB, doesn need to be a friend, since does not // directly access private data template <class T> ostream& operator<<(ostream& os, const Matrix<T>& a); //--------- member functions ---------------------------------------template <class T> Matrix<T>::Matrix(unsigned int nr, unsigned int nc) : prows_(nr) { for(unsigned int r=0; r< nr; r++){ prows_[r] = new Array<T>(nc); assert(prows_[r]!=0); } } template <class T> Matrix<T>::Matrix(unsigned int nr, unsigned int nc, T val) : prows_(nr) { for(unsigned int r=0; r< nr; r++){ prows_[r] = new Array<T>(nc, val); assert(prows_[r]!=0); } } template <class T> Matrix<T>::Matrix(const Matrix<T>& other) : prows_(other.nrows()) { //cout<< "in copy ctor"<< endl; unsigned int nrows= other.nrows(), ncols= other.ncols(); for(unsigned int r=0; r< nrows; r++){ prows_[r]= new Array<T>(ncols); assert(prows_[r]!=0); for(unsigned int c=0; c< ncols;c++){ (*this)[r][c]= other[r][c]; } } //cout<<"out copy ctor"<< endl; } template <class T> Matrix<T>::~Matrix() { unsigned int nrows= prows_.length(); for(unsigned int r=0; r< nrows; r++){ delete prows_[r]; } } template <class T> Array<T>& Matrix<T>::operator[](unsigned int r) const { assert(r<prows_.length()); return *prows_[r]; } template <class T> unsigned int Matrix<T>::nrows() const { return prows_.length(); } template <class T> unsigned int Matrix<T>::ncols() const { return (*prows_[0]).length();

187

} template <class T> ostream& operator<<(ostream& os, Matrix<T>& a) { unsigned int nrows= a.nrows(); os<<"[" << nrows<< "]{ "<< endl; for(unsigned int r= 0; r< nrows; r++){ os<< a[r]; } os<<"}"; return os; } #endif

188

17.3

Image class

When you look at an monochrome image on a computer screen, the picture that you see is composed of lots of small dots, pixels. These are arranged in an two-dimensional array i.e. a matrix. Each pixel may have a dierent gray-value, ranging from some value, e.g. 0, which corresponds to black, through to some value, e.g. 255, which corresponds to white. In a line-drawing image, you may have just two gray-levels: on (e.g. 255, or 1), and o (e.g. 0). Thus, an image can be stored / represented as a matrix of numbers. Incidentally, a colour image can be represented by three such matrices: one representing blue, one for green, and one for red. If we need to locate the pixels, we need a co-ordinate system. For this, we use the Coord class given in chapter 9. Note again that we use row (r), for the vertical axis, and column (c) for the horizontal axis. A further incompatibility with normal (x, y) coordinates is that we order them (r, c), where r, rows, is the vertical axis, and c, columns, is the horizontal axis. The interface and implementation for a template Image class is shown in le verb+image.h+. Regarding implementation, note that there is no need to program any of the Big-Three copy constructor, assignment operator, or destructor; this is because the compiler supplied shallow copies and deletes will perform adequately since they will invoke the appropriate matrix operations. We have provided a very simple character display; obviously this limits the size of images that we can usefully display. //------ image.h -------------------------------------------//j.g.c. 2/4/97, 10/1/99 mods to cater for new Matrix //uses matrix class //-----------------------------------------------------------#ifndef IMAGE_H #define IMAGE_H #include "matrix.h" template <class T> class Image{ public: Image(unsigned int nrows= 1, unsigned int ncols= 1); Image(unsigned int nrows, unsigned int ncols, T val); T& operator()(unsigned int r, unsigned int c); unsigned int nrows() const; unsigned int ncols() const; T min() const; T max() const; void display() const; void setAll(T val); private: Matrix<T> data_; }; //----------------------------------------------------template <class T> Image<T>::Image(unsigned int nr, unsigned int nc) : data_(nr, nc, 0) {} template <class T> Image<T>::Image(unsigned int nr, unsigned int nc, T val) : data_(nr, nc, val) {} template <class T> T& Image<T>::operator()(unsigned int r, unsigned int c) 189

{ assert((r<data_.nrows())&&(c<data_.ncols())); return data_[r][c]; } template <class T> unsigned int Image<T>::nrows() const { return data_.nrows(); } template <class T> unsigned int Image<T>::ncols() const { return data_.ncols(); } template <class T> T Image<T>::min() const { unsigned int nr= nrows(), nc= ncols(); T& d = data_[0][0]; T m = d; for(unsigned int r= 0; r< nr; r++){ for(unsigned int c= 0; c< nc; c++){ d = data_[r][c]; m = (m < d)? m : d; } } return m; } template <class T> T Image<T>::max() const { unsigned int nr= nrows(), nc= ncols(); T& d = data_[0][0]; T m = d; for(unsigned int r= 0; r< nr; r++){ for(unsigned int c= 0; c< nc; c++){ d = data_[r][c]; m = (m > d)? m : d; } } return m; } template <class T> void Image<T>::display() const { #define NI 10 char ichars[NI]={ ,.,,,-,=,+,/,X,B,M}; float dmin= float(min()), dmax=float(max()), dband=dmax-dmin; int nch=NI; char s[135]; unsigned int nc= ncols(), nr= nrows(), c, r; int i,z; if(dband<=1E-5)return; //axes box s[0]= ; s[1]=+; for(i= 2, c= 0;c< nc; c++, i++)s[i]=c%10+0; s[i]=+; s[i+1]= ;

190

s[i+2]=\0; cout<< s<< endl; for(i= 2, c= 0;c< nc; c++, i++)s[i]=-; s[i+2]=\0; cout<< s<< endl; //display Image int nlevel= nch; for(r= 0;r< nr; r++){ s[0]=r%10+0; s[1]=|; for(i= 2,c= 0; c< nc; c++, i++){ z=(int)( (float)(nlevel-1)*(data_[r][c]-dmin)/(dmax-dmin) ); s[i]= ichars[z]; } s[i]=|; s[i+1]=\0; cout<<s<< endl; } s[0]= ; s[1]=+; for(i= 2, c= 0; c< nc; c++, i++)s[i]=-; s[i]=+; s[i+1]=\0; cout<<s<< endl; for(i= 2, c= 0;c< nc; c++, i++)s[i]=c%10+0; s[i]=s[i+1]= ; s[i+2]=\0; cout<< s<< endl; } template <class T> void Image<T>::setAll(T val) { int nr= nrows(), nc= ncols(); for(int r= 0; r< nr; r++){ for(int c= 0;c< nc; c++){ data_[r][c] = val; } } } #endif

191

17.4

Figure Class

The interface of class Figure is shown in figure.h, and the implementation in figure.cpp. //------ figure.h -------------------------------------------//j.g.c. 3/4/97 //figure / graphics object class //based on Shape classes in FAQ and Stroustrup //-----------------------------------------------------------#ifndef FIGURE_H #define FIGURE_H #include "image.h" #include "coord.h" typedef unsigned char Byte; class Figure{ public: Figure(int r, int c){z_.setR(r); z_.setC(c);} virtual ~Figure() {} virtual void draw(Image <Byte>& im) const = 0; virtual void print() const = 0; void moveBy(int dr, int dc){z_.setR(dr+z_.getR()); z_.setC(dc+z_.getC());} void moveTo(int r, int c){z_.setR(r); z_.setC(c);} protected: Coord z_;//origin }; //provides dispatch of polymorphic draw(). void displayFigs(Figure* pfarr[], Image <Byte>& im); //provides dispatch of polymorphic print(). void printFigs(Figure* pfarr[]); void dragAndDrop(Figure& f, const Coord& newpos); #endif //------ figure.cpp ----------------------------------------//j.g.c. 3/4/97 //figure / graphics object class //-----------------------------------------------------------#include "figure.h" //provides dispatch of polymorphic draw(). void displayFigs(Figure* pfarr[], Image <Byte>& im) { Figure* p; int i=0; while((p=pfarr[i])!=0){ p->draw(im); i++; } } //provides dispatch of polymorphic print(). 192

void printFigs(Figure* pfarr[]) { Figure* p; int i=0; while((p=pfarr[i])!=0){ p->print(); cout<< endl; i++; } } void dragAndDrop(Figure& f, const Coord& newpos) { f.moveTo(newpos.getR(), newpos.getC()); }

193

Abstract class Figure is called an abstract class or abstract base class due to the presence of the functions: virtual void draw(Image <Byte>& im) const = 0; virtual void print() const = 0; The = 0; attached to these indicates that we do not intend to implement them for Figure. As a consequence, Figure is only ever used as a base class you cannot instantiate Figure objects. But you can derive from Figure, and that is the main purpose of abstract base classes to establish the interface for the complete family / hierarchy of classes derived from Figure.

Pure virtual functions The non-implemented ( = 0;) functions are called pure virtual functions. Any class which derives from Figure and which is intended to be used to instantiate objects, must implement versions of draw(), print(); i.e. as mentioned in the previous paragraph, the declarations of draw(), print() in Figure are merely establishing that any derived class must implement them. In many abstract base classes in textbooks you nd absence of: Data (representation); indeed this is the case in many Shape classes in textbooks. Since our model is that all self-respecting graphics Figures have a position, we have provided Coord representation of the origin (0, 0) point of the Figure: Coord z_;//origin Any implemented function; i.e. all interface functions are pure virtual. In the case of Figure, since we recognise that it has an origin (z_), we have provided implemented move functions: // move relative void moveBy(int dr, int dc){z_.setR(dr+z_.getR()); z_.setC(dc+z_.getC());} //move absolute void moveTo(int r, int c){z_.setR(r); z_.setC(c);}

194

17.5

Open-Closed Principle

Abstract base classes usually form good examples of the open-closed principle. Figure is such an example: both Figure.h and Figure.cpp are frozen (closed to modication), yet via inheritance it is open to extension below we extend to classes Point, Line, Rect,Circle, Fcircle. The following functions facilitate our demonstration of the open-closed principle: //provides dispatch of polymorphic draw(). void displayFigs(Figure* pfarr[], Image <Byte>& im); //provides dispatch of polymorphic print(). void printFigs(Figure* pfarr[]); void dragAndDrop(Figure& f, const Coord& newpos); Both displayFigs and printFigs take arrays of pointers to Figure; they call draw and print: virtual void draw(Image <Byte>& im) const = 0; virtual void print() const = 0; Since these are virtual, and since they are called by pointers: while((p=pfarr[i])!=0){ p->draw(im); // p is a pointer-to-Figure i++; } while((p=pfarr[i])!=0){ p->print(); // p is a pointer-to-Figure } the calls to print and draw will result in dynamic binding.

Old code can call new code As a consequence of dynamic binding, and of the frozen Figure class which provides the uniform interface, we have here a situation where displayFigs and printFigs can call (via dynamic / run-rime binding) print and draw functions that were written after figure.cpp was last compiled, i.e. old code can call new code.

Upside-down Library Moreover, we have the sort of framework / upside-down library mentioned in previous paragraphs: in displayFigs and printFigs we have calling programs that remain xed, whilst its called (virtual) functions: draw and print for all classes derived from Figure whether developed now, or in the future will be appropriately selected. Of course, with static binding this would not have been possible we need dynamic binding, i.e. via the virtual functions.

195

17.6

Figure Classes

Files point.h, line.h, rect.h, circle.h,fcircle.h, triang.h catalogue the interfaces and implementations of the descendants, concrete classes, as opposed to Figure which is abstract. For compactness of presentation, we have included implementations with the interfaces, i.e. both in the header (.h) les. //------ point.h -------------------------------------------//j.g.c. 3/4/97 //point object class //-----------------------------------------------------------#ifndef POINT_H #define POINT_H #include "figure.h" class Point : public Figure { public: Point(int r, int c) : Figure(r, c){} virtual void draw(Image <Byte>& im) const; virtual void print() const; }; void Point::draw(Image <Byte>& im) const { im(z_.getR(),z_.getC())=1; } void Point::print() const { cout<< "Point = "; z_.print(); } #endif //------ line.h -------------------------------------------//j.g.c. 3/4/97 //line object class //-----------------------------------------------------------#ifndef LINE_H #define LINE_H #include <stdlib.h> #include "figure.h" class Line : public Figure { public: Line(Coord s, Coord end) : Figure(s.getR(), s.getC()), dend_(end.sub(z_)) {} virtual void draw(Image <Byte>& im) const; virtual void print() const; private: Coord dend_;//displacement from z_ }; void lineDraw(const Coord& s, const Coord& e, Image <Byte>& im); void Line::draw(Image <Byte>& im) const { Coord end=z_; end.add(dend_); lineDraw(z_, end, im); 196

} void Line::print() const { Coord end=z_; end.add(dend_); cout<< "Line = {"; z_.print(); cout<< ", "; end.print(); cout<<"}"; } #endif void lineDraw(const Coord& st, const Coord& end, Image <Byte>& im) { // from DataLab imlineb - Bresenham line generator // ref. Foley, van Dam, Feiner and Hughes // Computer Graphics - principles and practice. // Addison-Wesley 1990. // notice: changed from (x,y) to (r,c) notation 4/4/97 int int int int int cc = st.getC(); cr = st.getR(); c = end.getC(); r = end.getR(); dc,dr,sc,sr,i,e;

dc=abs(c-cc); dr=abs(r-cr); sc=dc==0?0:(c-cc)/dc; sr=dr==0?0:(r-cr)/dr; if (dc>dr) { e=2*dr-dc; for(i=1; i<=dc; i++) { im(cr, cc)=1; if(e>=0) { cr+=sr; e+=-2*dc; } cc+=sc; e+=2*dr; } } else { e=2*dc-dr; for(i=1; i<=dr; i++) { im(cr, cc)=1; if(e>=0) { cc+=sc; e+=-2*dr; } cr+=sr; e+=2*dc; } } } //------ rect.h -------------------------------------------//j.g.c. 3/4/97 //rect object class //-----------------------------------------------------------#ifndef RECT_H 197

#define RECT_H #include <stdlib.h> #include "figure.h" #include "line.h" class Rect : public Figure { public: Rect(Coord z, Coord opp) : Figure(z.getR(), z.getC()) { dopp_ = opp.sub(z); } virtual void draw(Image <Byte>& im) const; virtual void print() const; protected: Coord dopp_;// **displacement** to opposite to origin }; void Rect::draw(Image <Byte>& im) const { // z_ +-----------------------+ c1 // | | // | | // c2 +-----------------------+ opp_ // Coord opp = z_; opp.add(dopp_); Coord c1(z_.getR(), opp.getC()); Coord c2(opp.getR(), z_.getC()); lineDraw(z_, c1, im); lineDraw(opp, c2, im); lineDraw(c1, opp, im); lineDraw(c2, z_, im);

} void Rect::print() const { Coord opp = z_; opp.add(dopp_); cout<< "Rect = {"; z_.print(); cout<< ", "; opp.print(); cout<<"}"; } #endif //------ circle.h -------------------------------------------//j.g.c. 3/4/97 //circle object class //-----------------------------------------------------------#ifndef CIRCLE_H #define CIRCLE_H #include <stdlib.h> #include "figure.h" void circleDraw(Coord z, int r, Image <Byte>& im); class Circle : public Figure { public: Circle(Coord z, int r) : Figure(z.getR(), z.getC()), r_(r) {} virtual void draw(Image <Byte>& im) const; virtual void print() const; protected: int r_;//radius };

198

void Circle::draw(Image <Byte>& im) const { circleDraw(z_, r_, im); } void Circle::print() const { cout<< "Circle = {"; z_.print(); cout<< ", "<< r_; cout<<"}"; } #endif //-----circle generation functions---------------------// j.g.c. 20/7/94 //-----------------------------------------------------static int putrc(Coord list[],int n,int maxpts,int r,int c) { n++; if(n>maxpts||n<0)return -1; list[n].setR(r); list[n].setC(c); return 0; } static int circleA(Coord list[],int n,int maxpts,int rc, int cc, int r, int c) { if(putrc(list,n++,maxpts,r+rc,c+cc)<0)return -n; if(putrc(list,n++,maxpts,c+rc,r+cc)<0)return -n; if(putrc(list,n++,maxpts,-c+rc,r+cc)<0)return -n; if(putrc(list,n++,maxpts,-r+rc,c+cc)<0)return -n; if(putrc(list,n++,maxpts,-r+rc,-c+cc)<0)return -n; if(putrc(list,n++,maxpts,-c+rc,-r+cc)<0)return -n; if(putrc(list,n++,maxpts,c+rc,-r+cc)<0)return -n; if(putrc(list,n++,maxpts,r+rc,-c+cc)<0)return -n; return n; } /*---------------------------------------------------circleDraw - Midpoint circle algorithm ref. Foley, van Dam, Feiner and Hughes Computer Graphics - principles and practice. Addison-Wesley 1990, p.85, sect. 3.3.1 j.g.c. 20/7/94. -----------------------------------------------------*/ void circleDraw(Coord z, int radius, Image <Byte>& im) { int r,c,n, maxpts, npts, i; float d, pi = 4.0*atan(1.0); Coord* list; int cc=z.getC(), rc=z.getR(); npts=(int(2.0*pi*radius)); if(npts<=0)return; maxpts=20+npts; /*just for luck*/ /*maxpts is size of array ie. 0..maxpts-1*/ list = new Coord[maxpts]; if(list==0)return; n=-1; /*points count - from 0*/ c=0; r=radius; d=1.25-radius;

199

n=circleA(list,n,maxpts,rc,cc,r,c); while(r>c){ if(d<0.0){ d+=2*c+3; c++; } else{ d+=2*(c-r)+5; c++; r--; } n=circleA(list,n,maxpts,rc,cc,r,c); } if(n==0)return; for(i=0;i<=n;i++){ im(list[i].getR(),list[i].getC())=1; } delete [] list; } //------ fcircle.h -------------------------------------------//j.g.c. 3/4/97 //FCircle - filled circle object class //-----------------------------------------------------------#ifndef FCIRCLE_H #define FCIRCLE_H #include <stdlib.h> #include "circle.h" #include "../matrix/image.h" void fCircleDraw(Coord z, int r, Image <Byte>& im); class FCircle : public Circle { public: FCircle(Coord z, int r) : Circle(z, r) {} virtual void draw(Image <Byte>& im) const; virtual void print() const; }; void FCircle::draw(Image <Byte>& im) const { fCircleDraw(z_, r_, im); } void FCircle::print() const { cout<< "Filled-Circle = {"; z_.print(); cout<< ", "<< r_; cout<<"}"; } /*---------------------------------------------------fCircleDraw uses extremely greedy algorithm! -----------------------------------------------------*/ void fCircleDraw(Coord z, int radius, Image <Byte>& im) { int rh=im.rhbound(), ch=im.chbound(); for(int r=im.rlbound(); r<=rh; r++){ for(int c=im.clbound(); c<=ch; c++){ if(int(z.dist(Coord(r,c)))<=radius)im(r,c)=1; } 200

} } #endif //------ triang.h -------------------------------------------//j.g.c. 5/4/97 //triangle object class //-----------------------------------------------------------#ifndef TRIANGLE_H #define TRIANGLE_H #include <stdlib.h> #include "figure.h" #include "line.h" class Triangle : public Figure { public: Triangle(Coord z, Coord p1, Coord p2) : Figure(z.getR(), z.getC()) { //dp1_ = fill as exercise // exercise } virtual void draw(Image <Byte>& im) const; virtual void print() const; private: Coord dp1_;// **displacement** to p1 Coord dp2_; // to p2. }; void Triangle::draw(Image <Byte>& im) const { // z_ +----------+ p1 // \ / // \ / // \ / // + p2 Coord p1 = z_; // write remainder as exercise } void Triangle::print() const { // fill as exercise cout<< "Triangle = {"; z_.print(); // fill as exercise cout<<"}"; } #endif

17.7

Program using Figure hierarchy

File figuret2.cpp shows a small test program for the Figure class hierarchy. //------ figuret2.cpp ------------------------------// j.g.c. 4/4/97 // test for figure etc. -- array used // one directory version 23/4/97 //-------------------------------------------------#include <iostream.h> #include "figure.h" #include "point.h" 201

#include "line.h" #include "rect.h" #include "circle.h" #include "fcircle.h" //#include "triangle.h" #include "coord.h" //#include "../listt/listt.h" int main() { Figure* pf[10]; Point p(1, 46); pf[0] = &p; Line l(Coord(4, 4), Coord(20, 4)); pf[1] = &l; Rect r(Coord(15,25), Coord(20,32)); pf[2] = &r; Circle c(Coord(8,16),5); pf[3] = &c; FCircle fc(Coord(10,34),4); pf[4] = &fc; //Triangle t(Coord(2,50), Coord(2,63), Coord(15,57)); //pf[5] = &t; pf[5]=0; //NULL terminator -- remove when you get triangle working pf[6] = 0; //NULL terminator -- for display Image <Byte> im(20, 64); displayFigs(pf, im); im.display(); printFigs(pf); return 0; }

202

Regarding the comments of the previous two paragraphs, although figuret2.cpp must refer explicitly to the individual classes (Point, Line, Rect, etc...), the following part: Image <Byte> im(20, 69); displayFigs(pf, im); im.display(); printFigs(pf); refers only to Figure. Add another class, e.g. Polygon, Ellipse, and this code will not need to change; moreover the functions displayFigs, printFigs do not change, and figure.cpp does not even need to be recompiled. Typical output from figuret2.cpp. +01234567890123456789012345678901234567890123456789012345678901234+ +-----------------------------------------------------------------+ 0| | 1| M | 2| MMMMMMMMMMMMMM | 3| MMMMM M M | 4| M M M M M | 5| M M M M M | 6| M M M MMMMM M M | 7| M M M MMMMMMM M M | 8| M M M MMMMMMMMM M M | 9| M M M MMMMMMMMM M M | 0| M M M MMMMMMMMM M M | 1| M M M MMMMMMMMM M M | 2| M M M MMMMMMMMM M M | 3| M MMMMM MMMMMMM M M | 4| M MMMMM MM | 5| M MMMMMMMM M | 6| M M M | 7| M M M | 8| M M M | 9| M M M | 0| MMMMMMMM | +-----------------------------------------------------------------+ +01234567890123456789012345678901234567890123456789012345678901234 Point = (1, 46) Line = {(4, 4), (20, 4)} Rect = {(15, 25), (20, 32)} Circle = {(8, 16), 5} Filled-Circle = {(10, 34), 4} Triangle = {(2, 50), (2, 63), (2, 63)}

203

Appendix A

Where do procedural programs come from?


A.1 Introduction

There are a great many ways of deriving (designing?) a program from requirements. In the next chapter we compare structured systems analysis and design (the method that was all the rage in the 1970s) with object-oriented analysis and design. However, 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 diers from that of input data, there is a little more work to be done, but we wont worry about that here.) Here, we look at a problem which has only output data: patterns on a screen. If we look at patterns in data, we will be able to identify: Sequence; Selection; Repetition. Based on groupings of patterns and repetitions of these groupings, we can identify the need for: Procedures, (methods, functions, subprograms); Procedures with parameters. 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 dierent colours or dierent avours). See Figure A.1 below.

204

Figure A.1: Computer programs have much in common with recipes (recipes from B. Nilsson, The Penguin Cookery Book)

205

A.2

Patterns and Programs

Write a program to display the following. * * * * * * ** * *

Next, make it general enough to take a parameter height. Lets build up to it. //----- Stars1.cpp --------------------------------// j.g.c. 2003/11/29 // prints single * on screen //-------------------------------------------------#include <iostream> using namespace std; int main() { cout<< *; cout<< \n; return 0; }

A.3

Sequence

Now, print: ***** //----- Line5.cpp --------------------------------// j.g.c. 2003/11/29 // prints 5 * line on screen //-------------------------------------------------#include <iostream> using namespace std; int main() { cout<< *; cout<< *; cout<< *; cout<< *; cout<< *; cout<< \n; return 0; }

206

Here is Line5.cpp formatted dierently 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() { cout<< *; cout<< *; cout<< *; cout<< *; cout<< *; cout<< \n; return 0; }

207

A.4

Repetition

//----- Line5r.cpp --------------------------------// j.g.c. 2003/11/29 // prints 5 * line on screen // now using repetition to replace a sequence //-------------------------------------------------#include <iostream> using namespace std; int main() { int n= 5; for(int i= 0; i< n; i++){ cout<< *; } cout<< \n; return 0; }

A.4.1

Sequence of Repetitions

//----- Tr1.cpp --------------------------------// j.g.c. 2003/11/29 // prints a backwards facing triangle //* //** //*** //**** //***** // Analysis: // Stars // Line 0: 1 // 1: 2 // 2: 3 etc... //-------------------------------------------------#include <iostream> using namespace std; int main() { int n= 1; for(int i= 0; i< n; i++){ cout<< *; } cout<< \n; n= 2; for(int i= 0; i< n; i++){ cout<< *; } cout<< \n; n= 3; for(int i= 0; i< n; i++){ cout<< *; //etc ... }

208

A.4.2

Repetitions of Repetitions Nested

We can improve a lot on Tr1.java. //----- Tr1r.cpp --------------------------------// j.g.c. 2003/11/29 // prints a backwards facing triangle //* //** //*** //**** //***** // Analysis: // Stars // Line 0: 1 // 1: 2 // 2: 3 etc... // i.e. generalising // Line j j stars //-------------------------------------------------#include <iostream> using namespace std; int main() { 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; }

Data Pattern Program Pattern //----- Tr2.cpp --------------------------------// j.g.c. 2003/11/29 // this is Tr1r.cpp written with the repetitions translated // to their sequence meaning; can you see that the program // has the same pattern as the data? //-------------------------------------------------#include <iostream> using namespace std; int main() { cout<< *; cout<< *; cout<< *; cout<< *; cout<< *; return 0; }

cout<< cout<< cout<< cout<< cout<<

\n; *; cout<< \n; *; cout<< *; cout<< \n; *; cout<< *; cout<< *; cout<< \n; *; cout<< *; cout<< *; cout<< *; cout<< \n;

209

A.5
A.5.1

Subprograms Procedures, Methods . . . Functions


Without Parameters

//----- Tr2p.cpp --------------------------------// j.g.c. 2003/11/29 // this is Tr2 (or Tr1r) now written with functions/procedures //-------------------------------------------------#include <iostream> using namespace std; void nl(){ cout<< \n; } 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; } Question. When is it wise to create a procedure?

210

A.5.2

Procedures with Parameters

//----- Tr3r.cpp --------------------------------// j.g.c. 2003/11/29 // this is Tr2p now written with parameterised functions // and repetitions //-------------------------------------------------#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<< ; } } int main() { int height= 5; for(int j= 1; j<= height; j++){ stars(j); nl(); } return 0; } Exercise: change h and see if it generalizes okay.

211

Equipped with these procedures, we are now in business, and can create nearly any pattern in very quick time. //----- 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; } Exercise: change h and see if it generalizes okay.

212

Now, getting more dicult . . . //----- Tr5.cpp --------------------------------// j.g.c. 2003/11/29 // getting more difficult still; see Tr4.cpp // * // ** // *** // **** //***** // Analysis: // Spaces Stars // Line 0 4 1 // 1 3 2 // 2 2 3 // etc ... // Generalising: let h be height // Line j h - 1 - j j + 1 //-------------------------------------------------#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<< ; } } int main() { int h= 5; for(int j= 0; j< h; j++){ spaces(h - 1 - j); stars(j + 1); nl(); } return 0; } Exercise: Based on Tr5.cpp, analyse, design and implement a program to do: ***** **** *** ** *

213

//----- Tr10.cpp --------------------------------// j.g.c. 2003/11/29 // getting more difficult again; see Tr5.cpp // Spaces Stars //************ 0 12 // ********** 1 10 // ******** 2 8 // ****** 3 6 // **** 4 4 // ** 5 2 // Let h be height and count from 0 // Spaces Stars // Line 0 0 h*2 // 1 1 (h-1)*2 // 2 2 (h-2)*2 // etc... // Generalise: line j: j spaces, (h-j)*2 stars //-------------------------------------------------#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<< ; } } int main() { int h= 5; for(int j= 0; j< h; j++){ spaces(j); stars((h-j)*2); nl(); } return 0; } Exercise: In Tr10.java, change h and see if it works okay.

214

//----- Tr20.cpp --------------------------------// j.g.c. 2003/11/29 // finally, we get to the original problem. // from Tr10.java // Analysis // Sp1 St Sp2 St // //* * 0 1 8 1 // * * 1 1 6 1 // * * 2 1 4 1 // * * 3 1 2 1 // ** 4 1 0 1 // And lets generalise (height= h) & count from 0 // Sp1 Sp2 // Line 0: 0 (h-1)*2 // 1: 1 (h-2)*2 // 2: 2 (h-3)*2 etc. // --------------------// i.e. // Line j: j (h-j-1)*2 //-------------------------------------------------#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<< ; } } int main() { int h= 5; for(int j= 0; j< h; j++){ spaces(j); stars(1); stars((h-j-1)*2); stars(1); nl(); } return 0; } Exercise: In Tr20.java, change h and see if it works okay.

215

A.6

Selection

//----- Tr10sel.cpp --------------------------------// j.g.c. 2003/11/29 // as Tr10.java, but selecting stars, stripes //************ // ---------// ******** // -----// **** // -//-------------------------------------------------#include <iostream> using namespace std; void nl(){ cout<< \n; } void stars(int n){ for(int i= 0; i< n; i++){ cout<< *; } } void dashes(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= 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; }

A.7

Exercises

1. Write a C++ Program to display an arbitrary lled rectangle of *s specied by h(eight) and w(idth); 2. Write a C++ Program to display a rectangle outline, 5 5.

216

3. 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. ***** * ** * * * ** * ***** * * * * * ***** * ***** * ***** ***** * ***** * *****

217

Bibliography
Abadi, M. & Cardelli, L. (1996). A Theory of Objects, Springer. Alexandrescu, A. (2001). Modern C++ Design, Addison Wesley. Bloch, J. (2001). Eective Java, Addison-Wesley. Bornat, R. (1987). Programming from First Principles, Prentice-Hall. Brooks, Jr., F. (1995). The Mythical Man-Month: essays on software engineering, 2nd edn, Addison Wesley. Budd, T. (1994). Classic Data Structures in C++, Addison-Wesley. 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. Buschmann, F., Meunier, R., Rohnert, H., Sommerlad, P. & Stal, M. (1996). A System of Patterns, Wiley. Campbell, J. (1999). Object-oriented programming in C++, Technical Report ifmg/99/0001/r, rev. 3.0, University of Ulster. Available at http://www.cs.qub.ac.uk/J.Campbell/myweb/oop/ /notes/oop.ps for PostScript version, /oophtml for the HTML version (note the translation to HTML is not great). Campbell, J. (2001). Algorithms and data structures csc 721, Technical Report Report No: jc/00/0007/r, Queens University of Belfast. Available at http://www.cs.qub.ac.uk/J.Campbell/csc721/. Cardelli, L. & Wegner, P. (1985). On understanding types, data abstraction, and polymorphism, Computing Surveys 17(4): 471522. Charatan, Q. & Kans, A. (2001). Java: the rst semester, McGraw-Hill. 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. Cornell, G. & Horstmann, C. (1999a). Core Java, Volume 1, Prentice-Hall. Cornell, G. & Horstmann, C. (1999b). Core Java, Volume 2, Prentice-Hall. Crosby, P. (1979). Quality is Free, Mentor Books. Czarnecki, K. & Eisenecker, U. (2000). Generative Programming, Addison Wesley. Eckel, B. (2003). Thinking in Java, 3rd edn, Prentice Hall. Fitzgerald, J. & Larsen, P. (1998). Modelling Systems: Practical Tools and Techniques in Software Development, Cambridge University Press. Flanagan, D. (2002). Java in A Nutshell, 4th edn, OReilly and Assoc. Foley, J., vanDam, A., Feiner, S. & Hughes, J. (1990). Computer Graphics: Principles and Practice, 2nd edn, Addison-Wesley. Gamma, E., Helm, R., Johnson, R. & Vlissides, J. (1995). Design Patterns: Elements of Reusable Object-oriented Software, Addison-Wesley. 218

Harold, E. (2000). Java Network Programming, 2nd edn, OReilly. Horstmann, C. (1997). Practical Object-Oriented Development in Java and C++, Wiley. Kernighan, B. & Ritchie, D. (1988). The C Programming Language, Prentice-Hall. Koenig, A. & Moo, B. (2000). Accelerated C++, Addison-Wesley. Lewis, J. & Loftus, W. (1998). Java Software Solutions, Addison-Wesley. Liskov, B. (2001). Program Development in Java, Addison Wesley. Louden, K. (1993). Programming Languages: Principles and Practice, PWS-Kent Publishing Company. Maguire, S. (1993). Writing Solid Code, Microsoft Press. Martin, R. (1996). Principles of object-oriented programming, Technical report, Available Object Mentor Assoc. http://www.oma.com. McConnell, S. (1993). Code Complete: A Practical Handbook of Software Construction, Microsoft Press. Meyer, B. (1996). An introduction to design by contract, Technical report, Interactive Software Engineering, inc. Available from www.eiel.com. Meyer, B. (1997). Object-oriented Software Construction, 2nd ed., 2nd edn, Prentice-Hall. Meyers, S. (1996). More Eective C++: 35 New Ways to Improve Your Programs and Designs., Addison-Wesley. Meyers, S. (1998). Eective C++, 50 Specic Ways to Improve your programs and designs, 2nd ed., second edn, Addison Wesley. Meyers, S. (2001). Eective STL, Addison-Wesley. Mitchell, R. & McKim, J. (2002). Design by Contract by example, Addison Wesley. Parnas, D. (1972). On the criteria to be used in decomposing systems into modules, Comm. ACM 5(12): 10531058. Petzold, C. (1996). Programming Windows 95, Microsoft Press. Schmidt, D. (2002). Programming Principles in Java, Kansas State University, USA. http://www.cis.ksu.edu/schmidt/ppj/. Available at

Sesoft, P. (2002). Java Precisely, MIT Press. An earlier version, plus the software in the book, is available from: http:www.dina.kvl.dk/sesoft/javaprecisely/. Sethi, R. (1996). Programming Languages: Concepts and Constructs, Addison-Wesley. Stroustrup, B. (1997a). The C++ Programming Language, 3rd edn, Addison-Wesley. Stroustrup, B. (1997b). The C++ Programming Language, 3rd ed., 3rd edn, Addison-Wesley. Tennent, R. (2002). Specifying Software, Cambridge University Press. Vlissides, J. (1998). Pattern Hatching: Design Patterns Applied, Addison-Wesley. Yourdon, E. (1989). Modern Structured Analysis, Prentice-Hall.

219

Você também pode gostar