Documenti di Didattica
Documenti di Professioni
Documenti di Cultura
Table of contents:
1. Forward 2. Introduction 3. Variables 4. Constants 5. Functions 6. The string Class 7. Arrays 8. Pointers 9. More On Pointers 10. Dynamic Memory Allocation 11. Structures 12. Introduction to Classes 13. The UML 14. Classes 15. Running Examples 16. Constructors 17. Copy Constructors 18. Inheritance 19. Ad Hoc Polymorphism 20. Parametric Polymorphism 21. Virtual Functions 22. Testing 23. Extra notes on virtual functions
Forward Acknowledgement
The author would like to acknowledge and thank the Office Of Learning Technology of the Canadian Employment Insurance Commission for the generous support provided for this project.
Forward
These notes represent a significant milestone culminating five years of work by Theo Norvell and myself. Little did we know when we started out how long the road would be. I had been teaching C++ since the late eighties and had been growing more and more dissatisfied with the tools at my disposal. A solution to the problem had been slowly taking shape in my mind but I didn't have the expertise to tackle it alone. One day, over coffee, I sketched out my ideas to Theo, and, Glory Be!, he got interested. He threw in a few ideas of his own and before we were properly aware of it, the Teaching Machine was born. The fundamental proposition was quite simple. It was a matter of observation that beginning programmers often throw code at problems, using what worked last week, or for a friend, or for a different problem. For many of them programming too often seems like magic and there's more a sense of rather desparate invocation than of crafting. We refer to this as superstitious coding. There's sometimes some grains of truth in the code but there's an awful lot of mumbo-jumbo, too. To a skilled programmer, every line of code is meaningful. If pressed most would probably say that a piece of code is just a set of instructions to an underlying machine. But when one is using a higher level language like C++ or Java, what does that machine really look like? We believe it looks like an amalgam of the compiler and the microprocessor. The Teaching Machine represents an abstraction of how we think experienced programmers regard computers. Thus we represent memory quite physically as an array of bytes, much as the microprocessor does, but organize them into stores, an altogether higher level concept. Again, our model for mathematical computation is something we call the expression engine which is a combination of an ALU (which fetches data from memory and executes) and an parser (which takes apart C expressions).
Somewhere along the way WebWriter++ was added to make it easy for instructors to build the Teaching Machine into web pages. The next step seemed inevitable. Moving an entire course into HTML so that the TM could be integrated totally into the notes. This site is the result. Although it was designed to be used in lectures, student feedback indicates it also functions pretty well as a standalone site. I can conceive a number of things that would improve its performance in that direction, most notably a set of videos demonstrating the TMexamples. I am contemplating their creation even as this is written. And once again, I would like to thank The Office of Learning Technology for their kind support.
Lihong Zhang
Office: EN-3031
Office Hours: Thursday, 15:30-17:30 (to be confirmed) Tel: 864-4863 email: lzhang@mun.ca
Marking Scheme
Assignments: 12% Mid-Term: 30% Final: 58%
Click here to download a copy of the current Teaching Machine jar file. (The jar file is the java executable. In order to run it you must have the Java run time system installed on your machine. Instructions for getting it arehere.) Midterm Exam : Wednesday, Oct. 16, 3:00-3:50 pm (tentatively) Text: Frank L. Friedman and Elliot B. Koffman, Problem Solving, Abstraction and Design Using C++, fifth edition, Addison Wesley, 2006. ISBN: 0-32145005-1 (same text as was used in ENGI 1020). ( Note: The textbook is not mandatory. It is specified only as a backup. The lecture notes are the primary resource and are available on the web in a form that may readily be printed.) Software: Eclipse with the C++ development tools (CDT). The software installation guide can be found here.
Assignments
Weekly, due Fridays at 8:55 am. Posted, on the web, approximately a week before the due date. If not otherwise informed, submitted over the network using the WebSubmit procedure developed by ECS. The submit directory for each assignment will be closed at 8:55 am sharp. One programming problem each week. Unless otherwise specified, compiled and linked with a grading program which will test it automatically.
Hand graded by a TA for style: 2 out of 10 pts. (TA's will be looking for mnemonic names, clean structure and good commenting).
Laboratory
There is a laboratory on Wednesdays from 10:00-11:50 in EN-3000/3029. There is no separate lab assignment as such. We will use this period to explore your weekly programming assignments. It is not anticipated that you will be able to complete your assignment during the lab period. Students are expected to do their own assignments. Group work is not permitted.
Pointers: notation, equivalence of pointer notation to array notation, passing by pointer, pass-by-reference. Structures: declaration, definition and usage, pointers to structures, structure assignment, abstract data types. Classes: data and function hiding, member functions, access control, objects, declaration versus instantiation, static members, inline functions. Object Creation: constructors and destructors, default constructors, overloading, default arguments, copy constructors, shallow vs. deep copying. Ad Hoc Polymorphism: conversions, binary operator overloading, unary operator overloading, friend functions, assignment and subscript overloading. Inheritance: code reuse, derived and base classes, the protected access control, code reuse, virtual functions, abstract base classes, multiple inheritance. Parametric Polymorphism: function templates, signature matching, class templates, inheritance. Although not formally taught, examples and assignments will emphasize and explore such fundamental data structures as arrays, stacks, and queues.
Objectives
At the end of this course you should 1. Have a fairly deep understanding of the processes underlying programming in general and object-oriented programming in particular. 2. Be able to read & understand well-written programs in C & C++. 3. Be able to code in C++ at the class level. 4. Have a working familiarity of Abstract Data Types, and basic Data Structures.
First Program
introDemo2.cpp
/* The mandatory "Hello world!" program */
/* In C & C++ input and output (i/o) are not part of the language. Instead they are included in the standard library of functions. The next line of code tells the precompiler to include the iostream part of the library */ #include <iostream> /* There are now thousands of names in the libraries so they need to be qualified. Just like there are lots of Johns, Marys & Michaels in the world. This is like saying "use lastName Bruce-Lockhart". Then Michael always refers to Michael Bruce-Lockhart*/
using namespace std; int main() { cout << "Hello, world!\n"; return 0; }
The standard "Hello world!" program. 5 lines of code, including An instruction to the preprocessor An instruction to the compiler to use the std namespace A main function A function call to insert the string into the standard output stream. Every piece of code has a specific meaning!
Variables
Attributes of Variables
1. Name
What the programmer uses to refer to the variable. Associated with the name are the following sub-attributes:
Scope
The range of the progam over which the name of the variable is known
Visibility
Whether a variable name is known at a particular point in a program. Subtly different from scope because of occlusion
2. Type
The category to which a piece of computer data is assigned. All data of a particular type will conform to a set of properties determined by that type, for example the amount of storage required to hold a piece of data or a range of acceptable values. Types may be categorized as
Built-in Scalar
single-valued. e.g. int, double, char
Compound
multiple-valued, either array or structure
User-Defined
In C++ we use classes to build user defined or Abstract Data Types (referred to as ADT's)
3. Value
the actual piece of data currently stored in the variable
4. Location
A physical place in memory where data is stored. There are sub attributes, vis
Reference
The way the location is referred to
Amount
The quantity of the storage used at the location for the data
Storage Class
Where the storage gets allocated.
5. Lifetime
How long a piece of data exists, generally 1. temporary 2. permanent (or static)
Variables1.cpp
// A do nothing program which illustrates various attributes of variables // copyright(c) 2002 by Michael Bruce-Lockhart #include <iostream> using namespace std; int factorial(int c); double x; // These are external variables int count = 3; int main(){ int myCount; x = 3.14159; myCount = count++; int fact = factorial(myCount); cout << "The factorial of " << myCount << " is " << fact << endl; return 0; } int factorial(int c){ if (c < 0) return -1; int fact = 1; for (int i = 2; i <= c; i++ ) fact *=i; return fact; }
Scope
Scope is a sub-attribute of name. It is the range of the program over which the name is known. The scope of a variable is controlled by where it is declared. Scope always starts at the point of declaration. Variables declared inside of functions (internal variables) In C: must be declared at the beginning of the function and extend to the end of the function. In C++: may be declared anywhere in a function and are known until the end of the smallest block enclosing the declaration. Variables2.cpp
int main () { int q; for(int i=0;i<10;i++) { // Not legal in C q++; //This is silly cout << i << q;
int p = 12; // Also useless, just illustrative } cout << p; // The compiler will complain! return 0; }
The scope of q is throughout main The scope of i the for statement only (includes block). The scope of p is exactly 1 line! Variables declared outside all functions (external variables) Scope extends from point of declaration to the end of the file.
Visibility
Refers to where in a program a name (of a variable) is available to the code. Generally, variables in scope are visible. Unless two variables with the same name are both in scope In which case only the variable with the innermost scope is visible. Variables3.cpp
int x1, a[10]; int main() { int i, x2; //...code for main } sub1() { int s1, s2; //...code for sub1 } int locl1, locl2; sub2() { int s21, s22, x1; //...code for sub2 } sub3() { int s31, s32; //...code for sub3 }
As an exercise, try filling in the following table, putting checkmarks in each column where a variable is visible. Variables x1 (outer) a x1 (inner) i, x2 s1, s2 locl1, locl2 s21, s22 s31, s32 main sub1 sub2 sub3
Lifetime
The length of time a variable lasts. Permanent variables last the lifetime of the program Temporary variables last only for the duration of a function.
Storage Class
Characterizes the type of storage where the variable is allocated. Closely related to lifetime. Automatic: variables that are stored on the stack and are temporary by definition. This is the default for internal variables. Static: variables that are stored in long term memory (the data store). Static is the usual term but means many other things in C++ so we will refer to these as allocations in the data store.
Variables4.cpp
// A variation on our do nothing program which illustrates how the stack works // copyright(c) 2002 by Michael Bruce-Lockhart #include <iostream> using namespace std; int factorial(int c); double x; // These are external variables int count = 3; int main(){ int myCount; x = 3.14159; myCount = count++; int fact = factorial(myCount); cout << "The factorial of " << myCount << " is " << fact << endl; return 0; } int factorial(int c){ if (c < 0) return -1; if (c < 2) return 1; return c*factorial(c-1); }
All external variables are put in the data store. Internal variables can be put in by preceding the declaration with static.
Basic Rules
1. Internal (declared inside a function) variables are automatic unless declared static. 2. External variables are always static. 3. Static variables are permanent 4. Automatic variables are temporary 5. Internal variables should normally be used
Constants
User-Defined Types
In C & C++ you can create your own types typedef int number;
This declares number to be a new type, the same as int. Then, I can use type number in declarations, as
int i,j,k; number n; Later on, if I find all numbers really should have been long, it is easily changed. typedef long number;
Changes all instances of number throughout the program. Scoping rules the same as for external variables.
Enumerated variables
Any variable can be declared as enumerated. enum{BLACK,WHITE,RED,GREEN,YELLOW,BLUE} c1; enum{SOLID,DOTTED,DASHED,INVISIBLE} line1,line2; declares c1 as a variable of a distinct integral type with named constants -BLACK(0), WHITE(1)...BLUE(5) Of limited utility as it stands. We often want to create an enumerated type enum daysOfTheWeek { SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY }; then, later in the code (within scope of daysOfTheWeek). daysOfTheWeek workDays; Alternatively, both can be done at once enum colour {BLACK,WHITE,RED,GREEN}c1,c2; daysOfTheWeek & colour are now special integral types, each with its own constants. We've also declared two variables of type colour.
enumerated constants are assigned consecutive values, starting from 0. These may be overridden enum linestyle {SOLID=1,DOTTED,DASHED,INVISIBLE=0}; (DOTTED & DASHED are 2 & 3 respectively).
is equivalent to the first three declarations. You will often see the following in old code (usually in header files): #define GEORGE 0 #define MARY 1 #define JOE 2
This is not the same and we won't use it because compiler does no type checking on defines. const keyword is used with variable names to tell the compiler the value is not to be changed (however it is to be type checked).
Functions
Functions consist of a declaration and a definition (or implementation). Functions can be called from anywhere in the file after their declaration. The compiler will allow a function to be called even if it hasn't been implemented (defined).
functions1.cpp
double function1(double arg1, double arg2); int main(){ double x=3., y= 17.5; double w, z; w = function1(x,y); z = function1(y,w); return 0; } double function1(double arg1, double arg2) { if (arg1 > arg2) return arg1*arg2; else return -arg2; } // Definition // Call // Another call // Declaration
Function declarations are always external. Function names have the same properties as external variable names. Functions don't have to return a value in which case the return type should be void.
The declaration is a template specifying 1. the return type 2. the name of the function 3. the number and type of the arguments to be passed (via a formal parameter list)
functions2.cpp
double function1(double arg1, double arg2); int main(){ double x=3., y= 17.5; double w, z; w = function1(x,y); z = function1(y,x); return 0; } double function1(double a1, double a2){ if (a1 > a2) return a1*a2; else return -a2; }
The names in the formal parameter lists of the declaration and definition do not have to be the same. 1. Compiler matches lists, in order of argument. 2. Types and number of arguments must be the same. 3. Names are not even required in the declaration
functions3.cpp
double function1(double, double); int main(){ double x=3., y= 17.5; double w; w = function1(x,y); // More code here } double function1(double a1, double a2){ //code for function1 }
Argument Passing
Arguments in C are passed by value. This was done to avoid side effects. Pass-by-value means the function cannot succesfully change one of its arguments
functions4.cpp
//Consequences of Pass-by-value #include <iostream> using namespace std; void intSwap(int x1, int x2); int main(){ int a = 2; int b = 3; intSwap(a,b); cout << "a is " << a << " and b is " << b << endl; return 0; } // A totally useless function!!! void intSwap(int x1, int x2){ int temp; temp = x1; x1 = x2; x2 = temp; }
Arguments in C++ can also be passed by reference. Pass-by-reference allows arguments to be modified
functions5.cpp
//Passing by reference #include <iostream> using namespace std; void intSwap(int& x1, int& x2); int main(){ int a = 2; int b = 3; intSwap(a,b); cout << "a is " << a << " and b is " << b << endl; return 0; } // A useful function!!! void intSwap(int& x1, int& x2){ int temp; temp = x1; x1 = x2; x2 = temp; }
Notice that the call to the two different swap functions is exactly the same. You cannot tell from a call whether a function is pass-by-value or pass-by-reference. You must look at the declaration to find out.
stringClass.cpp
string first, last, name; // declare a few strings string greetings = "Hello"; // declare and initialize
However, as objects of class string they also have operations available which can be invoked using the member-of operator "." .
stringClass.cpp
cout << "Here are the characters in our greeting: " << endl; for (int i = 0; i < greetings.length(); i++) cout << greetings.at(i) << ' '; cout << endl;
Here, the length() function of the string class returns the length of the string that is used to invoke it, in this case greetings. The at(i) function returns the character at the i'th position where i must be between 0 and greetings.length()-1 See table 7.3 on page 193 of your text for the definition of some other string class functions. Assignment may also be used with string objects.
stringClass.cpp
string first, last, name; // declare a few strings first = "Michael"; // Assignment cout << "And now its length is "<< first.length() << endl; last = "Bruce-Lockhart"; name = first + ' ' + last; cout << name << endl;
Assignment is subtly different from initialization in that the length of first when it is declared is 0 (because it was not initialized and so is set to a null string). After the assignment, it becomes 7.
The + operator may also be used to concatenate strings. Technically what has been done is to overload the assignment operator and the addition operator for string objects. Operator overloading will be covered in detail towards the end of the course. To be consistent, the += operator is also used to add something to the end of an existing string.
stringClass.cpp
greetings += " world!"; cout << greetings << endl;
Arrays
arrays1.cpp
int a,c[10]; double x[22],y[22][10]; /* y[row][col] */
Dimensionality of the array must be declared so that compiler knows how much storage to set aside. Multidimensional arrays are declared as arrays of arrays (in row-column order). Beware of declaring large arrays as temporary variables - temporary storage is extremely limited. Declare as external or as static internal.
arrays2.cpp
void myFunction1(int x, int y) { double myArray1[1000]; // Dangerous! Just asked for 8000 bytes // ...etc // from stack } double myArray2[1000]; // Safe. All external variables are static void myFunction2(int x, int y) { // ...etc } void myFunction3(int x, int y) { static double myArray3[1000]; // ... etc } // Safe. Is in static store.
arrays3.cpp
const int n = 25; int grades[n]; // Grades array shared between these routines
bool changeGrade(int mark, int i){ if ( (i < 0) || (i >= n) ) return false; grades[i] = mark; return true; } double classAverage() { int i, sum; for (i=0, sum = 0; i<n;i++) sum += grades[i]; return sum / n; }
Initialization of Arrays
arrays4.cpp
int a[5] = {0,3,2,4,1}; int m[3][4] = { {19, 27, 1, 6}, { 1, 0, 14, 17}, {-4, 3, 21, 0} };
Element References
arrays5.cpp
a[0] // Element zero of the array a[i] // Element i (the i'th element) a[3] // Element 3 m[2][3] // The element at row 2, column 3.
arrays6.cpp
/************* Duplicating an array *******************/ int a[5] = {0,3,17,51,-6}; int b[5]; b = a; /* This is ILLEGAL!!! Instead */ for (int i = 0;i<5;i++) b[i] = a[i];
In fact, we will see later on that it is possible to develop code in C++ which will allow arrays to be copied by the statement b = a where b and a are both arrays (even if they are a different size!). Passing arrays by value is problematic 1. Large amounts of data may have to be copied - slow! 2. The stack (temporary data store) may get over-run. 3. Preclude indefinite limits on array size to avoid (2). Arrays are passed by reference. In function prototypes why is n passed in? void dummy(int grades[], int n) is used to declare grades as an array of integers
C Style Strings
In C a string is an array of characters terminated by a null character ( '\0'). For example char stringArray[15] = "Hi!\n";
declares stringArray to be a an array of 15 characters. initializes it to the constant string "Hi\n" which is permanently embedded in the code.
How many characters do you think are in the constant string? In other words, how small could I make stringArray and still be able to hold all of "Hi!\n"? A function to return the length of a string
arrays7.cpp
long strlen(char str[]){ long i=0; while (str[i++]) ; return (i - 1); }
arrays8.cpp
double sumOf(double xArray[],int r, int c) { double sum; for (int i = 0;i<r;i++) for (int j=0;j<c;j++) sum += xArray[i*c+j]; return sum; }
In other words, multidimensional arrays are stored as follows: x[0] x[1] x[2] x[3] x[4] x[5] x[6] x[7] x[8] x[9] x[10] x[11]
Pointers
C++ Passes by Value
Recall the default for C/C++ is to pass scalars by value. The function
../review/functions4.cpp
// A totally useless function!!! void intSwap(int x1, int x2){ int temp; temp = x1; x1 = x2; x2 = temp; }
x1 and x2 are true variablescan be used on the left hand side of an expression-but they are temporary copies of the original variables! Only the values of the original, external arguments are available to the function.
Note that y = sin(x) and y = sin(4) are equivalent. In the first case, the current value of the variable x is passed, in the second, the value 4 is passed directly.
Pass-by-Reference
In 1020, you learned that the solution to this problem is to pass parameters by reference
../review/functions5.cpp
// A useful function!!! void intSwap(int& x1, int& x2){ int temp; temp = x1; x1 = x2; x2 = temp; }
intSwap(int & x, int & y) intSwap(int &x, int &y) are all acceptable but the first is preferred. Call to intSwap looks exactly the same as before
pointers1.cpp
int a=2,b=3; intSwap(a,b); cout << a << ", " << b;
Pointers
C++ has a third, passing mechanism, similar to (but more primitive than) references. First we introduce two new operators & unary operator: references (gives the address of) an object. p = &c assigns the address of c to the variable p. p references or points to c. * indirection or dereferencing operator: Applied to a pointer, it accesses the object pointed to.
pointers2.cpp
int x = 1, y = 2; int* ip; // ip is a pointer-to-int ip = &x; // set the value of ip to the address-of-x y = *ip; // y is now 1 *ip = 0; // x is now 0 ++*ip; // increment x - unary ops right to left (*ip)++; // increment it again *++ip; // increments the pointer, then derefs it
Note the last usage is potentially erroneous. Incrementing the pointer means you have no idea where it is pointing (what it addresses) unless it is pointing into a well defined structure (such as an array). The declaration int* ip;
sets storage aside for a pointer to type integer. ip is not an integer --- it is a pointer to an integer no storage space is made for an integer (because there is no integer variable).
pointersIntSwap.cpp
void intSwap(int* px,int* py){ int hold; hold = *px; *px = *py; *py = hold; }
pointersIntSwapTM.cpp
Both calls to intSwap are legal and both are the same. The case is just like sin(x) vs sin(4). In either case C passes the value, which happens to be an address. Note difference between swap code at end of main and in intSwap function. What confuses neophyte C users is the difference between the function prototype intSwap(int* px,int* py) and the function callintSwap(&x,&y); but they are perfectly logical.
The prototype specifies that the values to be passed in will point at integers (ie, that they will be addresses). The call passes the addresses of the integers.
A second point of confusion is that to build functions like intSwap that operate on input variables, you have to operate indirectly, whereas if the code were not buried in a function, you could operate directly. The difference has been highlighted by carefully using different names for the pointer variables. This is something you would do well to copy.
pntrToEndBad.cpp
void toEnd(char* pString){ while(*pString) pString++; }
This example doesn't work because pointers are also passed by value! What is needed instead is
pntrToEndGood.cpp
void toEnd(char*& pString){ while(*pString) pString++; }
This says that pString is a pointer to char (char*) and it is passed by reference!
Pointer Arithmetic
Pointers allow us to work with computer addresses without ever knowing what the addresses are. This independence of actual addresses is important because
It allows code and data to be relocated when it is first loaded into the computer. It will allow data to be allocated dynamically at run time (for example, by using the new operator).
Pointer arithmetic supports address independence by allowing us to work with pointers relative to some base address. For example, another way to move a pointer to the end of a string would be to write
pointers3.cpp
void endStr(char*& beg) { beg += strlen(beg); }
Whenever pa points to a then pa+i is the address of a[i] and *(pa+i) is the contents of a[i]. The expression pa+i is an example of pointer arithmetic. pa is a pointer variable whose value is an address. pa+i is an expression whose value is the address held in pa plus i times the size in bytes of the object pa points at. Pointers work in terms of base units. Thus
1
are all equivalent. We use the first one because it highlights that the type of p1 is pointer-to-int. Unhappily, it leads to confusion in the following case:
int* p1,i;
which appears to declare both p1 and i as pointers-to-int. In fact, it is actually declaring p1 as a pointer-to-int and i as an int. For this reason, some people prefer the third form
int *p1, *p2;int *p3, j, k;
which makes it clear that p1 and p2 and p3 are pointers to int while j and k are int. We will stick with the first notation and insist that pointers be declared one at a time on their own lines, thus
int* p1; int* p2; int* p3; int j,k;
pointers4.cpp
#include <iostream> using namespace std; const int ASIZE = 7; int main(){ double array[ASIZE]; // Also recall that array is a synonym for &array[0] double* pBeg = &array[0]; double* pEnd = array + ASIZE - 1; while (pBeg <= pEnd) cout << *pBeg++; return 0; }
creates an array of doubles and a pair of pointers which are intialized to the first and last elements of the array respectively pEnd is initialised to array + ASIZE - 1. array is the same as &array[0] and is the address of the 0'th element of the array. ASIZE - 1 is an offset between the initial address of array[0] and the final address of array[SIZE - 1]. It actually represents (ASIZE-1) x the number of bytes required to hold a double
Strings Revisited
Although strings are arrays of characters, pointer notation is almost always used in connection with them. We will use string examples a lot. Students sometimes find this obsessive. (a) You MUST know both notations (both on exams) (b) We usually use pointer notation (the hardest one!) So what's the big deal? Are strings really that important? Well, Yes. But that's not why we study them. They are the fastest route into the heart of C. If you understand string routines, you have a big part of C. String length revisited
strLenArray.cpp
long strlen(char str[]){// Array notation long i=0; while (str[i]) i++; return i; }
strLenPointer.cpp
long strlen(char* p){// Pointer notation long i=0; while (*(p+i)) i++; return i; }
Note that all of these work in a function call strlen("Hello world"); /* string constant - Pointer is passed!*/ strlen(array); strlen(ptr); Note, finally, that although pointer notation and array notation can be regarded as equivalent, pointers are not arrays! The following two declarations do very different things: int myArray[20]; int* p = myArray;
More On Pointers
more1.cpp
+1 if first string > second string (comes later in a dictionary), -1 if first string < second string */ int cmpStr(const char* pS1, const char* pS2){ while (*pS1 ==*pS2 && *pS1) {pS1++; pS2++;} if (*pS1==*pS2) return 0 ; else if (*pS1>*pS2) return 1 ; else return -1 ; }
Comment at beginning to explain what function does. Needed as it is not selfexplanatory. const in front of pointers pS1 & pS2 says they are read only pointers. Check loop runs as long as characters are equal and we haven't run out of string 1. (What about string 2 ??) Notice, we didn't even need a temporary variable.
Constant Pointers
Compiler will not allow programmer to write through pS1 and pS2. But it can't stop the following
more2.cpp
int cmpStr(const char* pS1, const char* pS2){ char* pS3 = pS1; // copy string1 while (*pS1 ==*pS2 && *pS1) {pS1++; pS2++;} while (*pS3) *pS3++ = ' '; // overwrite string1!!!! return (*pS1==*pS2) ? 0 : (*pS1>*pS2) ? 1 : -1 ; }
To quote one author, This is evil, pure and simple! The coder has maliciously broken the contract implicit in the declaration.
Implicit in the declaration of strCpy is that the calling code will provide a pointer to somewhere to put the copy. salutation points to a constant string (put automatically by the compiler in the constant store) buffer is actually a pointer and it points nowhere!
/* A function to report a single error line */ char* errorReport(int e) { char buffer[81]; // A place to put the string char* pBuff = buffer; // A pointer to the string strCpy(buffer,"Error #"); // Start with "Error " moveToEnd(pBuff); // Do some other stuff here we don't need to worry about return buffer; }
Static Memory
Allocated when code is initialized (Main started up) Lifetime is permanent. Addresses are all known-pointers aren't required.
The Stack
Allocation is controlled by scope data created when function called Temporary lifetime Stack builds downwards-only a single pointer required
The Heap
Allocation is on the flywhenever the program requests it. Allocation is randommemory is allocated wherever a large enough chunk is available. Lifetime is still temporary, but completely under program control. A pointer is required for every chunk of memory granted.
Now when Memory is allocated, it may not be able to use the hole because it is too big
Eventually, as memory is allocated and released, the heap becomes fragmented. Lots of hidden management problems requires a Heap Manager.
How does heap manager know memory is allocated or free? How does heap manager know the size of a chunk? What does it do about fragmentation?
C++ provides two operators for run-time memory management. newallocates memory deletereleases memory Syntax is new(nothrow) object or
new(nothrow) object [size] where the second syntax is for declaring an array of objects. e.g. int* p; p = new(nothrow) int [20]; allocates an array of 20 integers. new returns a pointer to the chunk allocated on the heap. If no chunk large enough is available, it returns a null pointer. The nothrow that appears in parentheses after the new operator is technically called an allocator. Its function is to prevent the new operator from throwing an exception (which we haven't studied) if no memory is available. To release the memory back to the heap the syntax is delete pointer; or delete [] pointer; // for arrays (Be sure the pointer you delete still points into the heap)
division by 0 or of a very large number by a very small one such that overflow occurs being unable to supply memory when requested.
Programs can't ignore these problems. They have to deal with them. One solution is to throw an exception when a problem occurs. It is then up to the programmer to detect the exception (which is called catching it) and do something about it. The standard defines two ways of dealing with a failure of new to allocate the memory. 1. Throw an exception. 2. return null. It is compiler-dependent as to which technique is the default. We will use null returning in this course. Unhappily, this can be done in two ways
Structures
Arrays are compound data in which all the data in the package is homogeneous that is, of the same type. Also possible to bundle up data of different types in a single package. This type of compound variable is known as a structure. In C++, the template for the structure is generally defined before any variables of the structure are created. Effectively, programmers get to create a new type of their own.
account.cpp
struct Account{ int number; double balance; bool overdraftAllowed; }; bool transaction(Account& anAccount, double amount){ bool allowed; if (amount < 0) { // then a debit if (anAccount.balance + amount < 0 && anAccount.overdraftAllowed) { anAccount.balance = anAccount.balance + amount; allowed = true; } else allowed = false; // not enough money } else { // deposit anAccount.balance = anAccount.balance + amount; allowed = true; } return allowed; }
In this example, the programmer has created a new date type called Account by declaring Account as a structure (the keyword is actually struct). account bundles up three pieces of data by enclosing them in a block surrounded by curly brackets. The data in the block are declared just like normal variables. They are not normal variables, however. The only variables in the program are johnsAccount and myAccount, two separate bundles of data of type account, each containing their own three values (plus the bool allowed in the transaction function). What appear to be variables inside the block structure are actually known as fields. When the transaction function gets called, note we have elected to pass anAccount in by reference. While this is done automatically for arrays (so the ampersand is not required), structures may be passed-by-value.
Which of course means that a copy would have to be made. That might or might not be expensive depending on the structure. As the programmer, you have to know.
Declaration of Structures
structure2.cpp
struct complex { double re; double im; };
There are variations permitted on this declaration technique, but we will concentrate on this one. Now we can declare variables of same structure struct complex z; complex z; /* C Declaration */ // C++ Declaration
in C, we can also use a typedef typedef struct { double re,im; }complex; complex x,y,z; Here we have defined the type complex as being an un-named structure. Note that, in C, future references to the tag complex must also include the word struct struct complex z So the explicit typedef declaration is preferred. In both C & C++, typedef can be used for other things. e.g. typedef int number; Here we've defined a new type, number, which is equivalent to an int. Later on, we could quickly change every number in the program to a long or a double just by changing the typdef.
Complex Structures
a student record might contain a name, a mun no., and a set of courses and grades organized by term. This can quickly get complex! student record --> name (a string) mun id (a 7 digit string) a list of term records --> date (a date string) term (an integer in [1,8]) average (a real on [0.0,100.0] a list of marks --> course no. (a course id string) grade (integer [0, 100]) Note how things are embedded We've got arrays inside our structure & structures inside the arrays!
structure4.cpp
struct mark { char course[5]; int grade; }; struct term { char startDate[11]; int termno; double avg; mark marks[6]; }; struct student { char name[20]; int id; term terms[10]; }; student mary,john; strCpy(mary.name,"Mary Mathias"); strCpy(john.terms[3].marks[2].course,"3891"); john.terms[3].marks[2].grade=65; strCpy(mary.terms[3].startDate,"09.08.1992");
Introduction to Classes
Introduction
You may have heard it said that C++ is an Object Oriented Language. Classes are the means by which we create the objects that are so central to the language. But what is a class? And what is an object? As you learn more and more about OOPS ( Object Oriented Programming Style) you will hear a number of different definitions of classes and their relationship to objects. At bottom, however, the fundamental ideas are these: A Class is a category. An Object is a particular instance of a class. For example
Instance me, you, John, Mary, mom, my doctor, the weather man my account, your account, IBM's account General Hospital, my house, 1412 Main St., Wal-Mart this window, Word window, C++ window, debug window
Categorization is fundamental to our understanding of the world. A category is a generalization, an observation that, at some level, there is a sameness about a group of things. For example,all bank accounts 1. can have money deposited into them 2. or withdrawn from them 3. will carry a balance (summarizing the total past history of our transactions to and from the account). Yet each account is different. Your account might have a thousand dollars in it while mine might be overdrawn. You certainly wouldn't want the bank to mix them up! Thus we say that 1. the behaviour of all accounts is the same (if you deposit money the balance increases)
2. but their individual states are different (your account currently has more money in it than mine). Similarity of behaviour is what defines a category or class Individuality of state defines an instance or object.
Modeling
While classes and objects represent the central mechanism of OOPS programming, its central objective is really modeling. That might seem strange and way too passive for something like, say, controlling a nuclear reactor. But think about it. You don't want just anybody controlling a nuclear reactor. Homer Simpson notwithstanding, control room operators have to be highly trained. They have to, at least at some level, have an understanding of the process they are controlling, or disaster ensues. That understanding does not have to be absolute. The physicist who conceives of a new type of reactor, The engineer who designs it and The operator who controls it, all work at a different level of understanding. Each of them carries around in their head a view of the reactor that, while it is particular, is nonetheless necessary for them to do their jobs. In other words, each of them carries a model of the reactor in their heads. If we want to build software to control a reactor we will also have to build into it some model of how the reactor works. In the OOPS methodology, classes are the mechanism we use to build models. If we were building a reactor control program we would probably uses classes to represent the reactor, the cooling rods, the piping, and a whole host of other things that I don't know about because I know very little about reactors. I lack, what is known as domain expertise. Without domain expertise it is impossible to build good programs. So let's switch our attention to things we do all know about.
Modeling is Specific
Modeling is not an absolute. For example, consider a car. You could probably list off all kinds of attributes of cars. Here are some examples: Car attributes: wheels (four), steering wheel, engine, transmission, serial no., make, model, year, upholstery, condition. What others can you think of? If I were to write a program that involved cars, these could all be useful things to know. But notice, their usefulness depends upon the type of program I'm writing. For example, if I'm writing a program to track parking permits (say at a large company or university) then I will want to know information such the colour, make, model, year and license number of each car. But I won't care about the fact that it has a spare tire or what its odometer setting is. On the other hand, if we're building a sort of Janes Guide to Automobiles, we'll care about model and make and engine specifications but we won't see a red 1938 Bugatti as being any different from a green one. A program to manage the inventory of a used car dealership would need different information again. Now we definitely want the odometer setting! So each of these programs would undoubtedly have a class Car. But the classes would be quite different. We're making different models.
The UML
The UML is the Unified Modeling Language, a language that OOPS designers use to design object-oriented systems. We'll hardly touch on it here, but some of its basic constructs are useful even at this level. In particular, we'll introduce you to
Class Diagrams
The first Class diagram is the simplest possible. It shows a single class, representing the way a used car dealership program might view a car. A class is represented by a box divided into three compartments The top compartment contains the name of the class. By convention, we will capitalize the first letter. i.e. the class for a car is called Car. The middle compartment contains attributes which are type-name pairs. For example, model, which is a String. The bottom compartment contains the operations which can be carried out on objects of the class. For example, any particular car in the inventory can be sold by calling its sell() operation.
Although we are showing only one class in this system ( a real one would undoubtedly contain many more), there would be many objects. Every automobile in a dealer's inventory would constitute a separate object, with it's own values for its attributes. Thus, while there might be three blue 1998 Cavalier coupes in the system, each would likely have a different odometer reading and perhaps different purchase and sticker prices. We say that the set of all values for the attributes of an object constitute the current state of that object.
Parking Control
The class diagram shown for a parking control system is a little more complex. Here we are showing three classes (again, real systems would have many more) and defining some relationships, called associations, between them.
Classes
C++ is an Object-Oriented Language. Classes are the means by which we create objects. Classes are central to the true OOPS. 1. many programmers who claim to program in C++ don't. 2. true C++ programming is programming classes OOPS represents a genuine paradigm shift.
Because classes are so important you will hear many views of what they are:Classes are object factories Classes are the means of encapsulating data and functions Classes are categories Classes are types For example
classes1.cpp
class Complex { double re,im; public: double getReal(); // accessors double getImag(); void set(double r, double i); // mutator };
declares a class Complex that encapsulates both the real variables re and im as well as three function declarationstwo accessors: getReal() and getImag() which read data and one modifier: set(.) which changes data
Access Control
Members of a class (both data and functions) may be public or private default for classes is private Access may be switched back & forth-e.g.
classes2.cpp
class PubPriv{ int a,b; double f1(); public: int c; double f2(int a1); private: int e; int f3(double a1,double a2); }; // a,b and function f1 are private // by default
However, this is not very good style, particularly for small classes. Without a very good over-riding reason, stick to one private section and one public in each class. Normally, data is private, functions are public.
classes3.cpp
class WellStyled { public: bool setData(int a, int b); double whatIsX(); private: int v1, v2; double x; bool why; }
This is the embodiment of encapsulation. Data is hidden. Only the class functions are allowed access to it.
classes4.cpp
class Complex { public: double getReal(){return re;} double getImag(){return im;} void set(double r, double i){re = r; im = i;} private: double re,im; };
2. Alternatively, functions may be defined in the normal way except that they need a scope resolution operator. In such a case we first declare the class
classes5.cpp
class Complex { public: double getReal(); double getImag(); void set(double r, double i); private: double re,im; };
classes5.cpp
double Complex::getReal(){ return re; } double Complex::getImag(){ return im; } void Complex::set(double r, double i){ re = r; im = i; }
Note that functions defined outside the class operate normally, whereas inline functions are really more like macros. A call doesn't actually generate a call. InsteadThe code for the function is written in at every point it is used. Efficient for small functions since function calls take a lot of time.
Using Classes
Given the declaration above
classes5.cpp
int main() { Complex x,y; // Declares objects of class Complex x.set(1.,1.); y.set(-1.5,3.); double z = sqrt( x.getReal()*x.getReal() + x.getImag()*x.getImag() );
/* use real member function attached to objects of class Complex */ cout << x.getReal() << " + j" << x.getImag(); return 0; }
classes7.cpp
Complex x; int main() { Complex y; // etc. .... } int f1(){ Complex x,y; // ... }
x is a global object f1 and main both have their own objects y f1 has its own object x
classes8.cpp
int f1(){ Complex x,y; double z; z = ::x.getReal(); //... }
This use of the scope resolution operator :: to access globals works in C++ for variables as well as objects
classes9.cpp
/* The interface for class Complex */ class Complex { public: double getReal(); double getImag(); // ... };
Why Encapsulate?
Consider a case where a complex no. is not changed often but both kinds of co-ordinates are requested frequently.
classes10.cpp
#include <math.h> class Complex { public: void set(double real,double imag); double getReal(); double getImag(); double getMag(); double getAngle(); private: double re,im; //rectangular co-ordinates double mag, ph; // polar co-ordinates }; int main(){ Complex x,y,z; x.set(1.0,0.0); y.set(1.0,-1.0); z.set(3.2, 1.0); } void Complex::set(double real, double imag){ re = real; im = imag; mag = sqrt(re*re + im*im); ph = atan2(re, im); } double double double double Complex::getReal(){return re;} Complex::getImag(){return im;} Complex::getMag(){return mag;} Complex::getAngle(){return ph;}
Designer has opted to carry the overhead of remembering (instead of recalculating) magnitude and phase. Why is the user not allowed to tamper with the data? What does the code for set have to do? Designer can later change design of private part without affecting how the public part works.
We haven't shown the code for the functions, yet, because it is quite straightforward. Notice an immediate difference between the C module approach and the C++ approach. I can declare more than 1 queue in C++. Once the class is declared, I can declare as many Objects of the class as I like. Queue q1, q2, q3; // 3 queues of 100 integers.
Running Examples
We are going to create three classes that we will improve as we go along. An array class, a string class and an Account class. Array and string will have to wait until the next topic.
account1.cpp
/* The interface for a class to model bank accounts */ class Account { public: // Accessor functions double getBalance(); // Mutator functions void initialize(); // call once when account created void deposit(double amount); void withdraw(double amount); void changeOD(bool allowed); void setRate(double iRate); void setMonthly(double charge); void setItem(double charge); void monthEnd(); // The data that we must track for each account object private: double balance; double minBalance; bool overDraftAllowed; double interestRate; double itemCharge; double monthCharge; }; #include <iostream> using namespace std; int main(){ Account mine, yours; mine.initialize(); mine.changeOD(true); mine.deposit(100); mine.withdraw(200); mine.deposit(1500); mine.monthEnd(); cout << "my Balance is " << mine.getBalance() << endl;
yours.initialize(); yours.deposit(100); yours.withdraw(200); yours.deposit(1500); yours.monthEnd(); cout << "your Balance is " << yours.getBalance() << endl; return 0; } double Account::getBalance(){return balance;} // This function must be called ONCE when an ccount object is // first created void Account::initialize(){ balance = 0; minBalance = 0; overDraftAllowed = false; itemCharge = .25; monthCharge = 2.00; interestRate = .0175; } // assertion: amount is non-negative void Account::deposit(double amount) { if (amount >= 0) balance = balance + amount; } // assertion: amount is non-negative void Account::withdraw(double amount) { if (amount < 0) return; //Assertion failure if (!overDraftAllowed && balance - amount - itemCharge < 0) return; // overdraft not allowed balance = balance - amount - itemCharge; if (balance < minBalance) minBalance = balance; } void Account::changeOD(bool allowed){ overDraftAllowed = allowed; } void Account::setRate(double iRate){ interestRate = iRate; } void Account::setMonthly(double charge){ monthCharge = charge; } void Account::setItem(double charge){ itemCharge = charge; } void Account::monthEnd(){ withdraw(monthCharge - itemCharge); if (minBalance < 0) withdraw(interestRate*(-minBalance)); minBalance = balance; }
In C, we are able to bundle data and functions together into structures. Thus you built a model of an account that had data for the account and some functions that manipulated it. Now, in C++, our unit of modularization is the class. The account class lets us declare as many accounts (account objects) as we want. Each account has its own data. (Objects have their own state) The functions are used to manipulate the data of any of the account objects. (Classes are characterised by commonality of behaviour)
Constructors
Creating Objects
Automatic Storage: Requires that objects declared internally within a scope block must be created and destroyed each time the block is entered and left. That creation and destruction is effected by functions, called respectively, constructors & destructors.
Constructors
Have the same name as the Class May take arguments of any type except of their own class. Never have a return type, not even void.
constructors1.cpp
class Complex { public: double real() const; double imag() const; Complex(double r=0.,double i=0.); private: double re,im; };
Complex is the constructor Note the use of default arguments. Complex may be created with zero, one or two arguments.
constructors2.cpp
int main(){ Complex x; Complex y(3.4); Complex z(1.0,-1.0);
cout << "The real part of z is "; cout << z.real(); return 0; }
y gets intialized to (3.4 + j0.) z gets intialized to (1.0 - j1.0) Take care to avoid ambiguity
constructors3.cpp
class Complex { public: double real() const; double imag() const; Complex(double r,double i=0.); private: double re,im; };
This is legal, but the constructor takes one or two arguments. Thus Complex x(); Complex y(3.4),z(1.,-1.); the declaration of x is now illegal.
Default Constructors
Note that we have been able to make plain declarations like Complex z before constructors were ever mentioned. What changed? 1. In the absence of a user defined constructor the compiler will create a default constructor of the form classname () i.e., the default constructor takes no arguments. 2. Once a programmer-defined constructor is created for a class, no default constructor will be created by the compiler even if the class has not been equipped with a constructor for no arguments. 3. The no argument constructor (often, confusingly, referred to as the default constructor whether created by compiler or programmer) is needed to create arrays of objects and new objects from the heap.
Final Thought
Note that the programmer provides the code for the constructor(s) and the compiler takes care of calling them.
Destructors
Are always called by the class name preceded by a ~ e.g. ~Window() is the destructor for a class Window Never have a return type or any arguments. Called automatically when an object goes out of scope
Running Examples
Now that we have contructors we can create reasonable versions of all our running examples. First we improve the Account class. Here is the declaration of the original class
../classes/account1.cpp
/* The interface for a class to model bank accounts */ class Account { public: // Accessor functions double getBalance(); // Mutator functions void initialize(); // call once when account created void deposit(double amount); void withdraw(double amount); void changeOD(bool allowed); void setRate(double iRate); void setMonthly(double charge); void setItem(double charge); void monthEnd(); // The data that we must track for each account object private: double balance; double minBalance; bool overDraftAllowed; double interestRate; double itemCharge; double monthCharge; };
And here is how we had to use it. Recall that before we actually could do anything with an Account object, we had to remember to initialize it.
../classes/account1.cpp
int main(){ Account mine, yours; mine.initialize(); mine.changeOD(true); mine.deposit(100); mine.withdraw(200); mine.deposit(1500); mine.monthEnd(); cout << "my Balance is " << mine.getBalance() << endl; yours.initialize(); yours.deposit(100); yours.withdraw(200); yours.deposit(1500); yours.monthEnd(); cout << "your Balance is " << yours.getBalance() << endl; return 0; }
Now we replace our initialize() function with a constructor Unlike the initialize() function the constructor is called automatically when we declare an account object
account2.cpp
/* The interface for a class to model bank accounts */ class Account { public: // Constructors Account(); Account(bool od); // Accessor functions double getBalance(); // Mutator functions void deposit(double amount); void withdraw(double amount); void changeOD(bool allowed); void setRate(double iRate); void setMonthly(double charge); void setItem(double charge); void monthEnd(); // The data that we must track for each account object private: double balance; double minBalance; bool overDraftAllowed; double interestRate; double itemCharge;
double monthCharge; };
One small problem with this is that if you examine the implementation code for the two constructors it is virtually identical
account2.cpp
Account::Account(){ balance = 0; minBalance = 0; overDraftAllowed = false; itemCharge = .25; monthCharge = 2.00; interestRate = .0175; } Account::Account(bool od){ balance = 0; minBalance = 0; overDraftAllowed = od; itemCharge = .25; monthCharge = 2.00; interestRate = .0175; }
This is a maintenance problem. It would be better to make it common. We re-introduce initialize() but as a private function
account3.cpp
class Account {
/* The interface for a class to model bank accounts */ public: // Constructors Account(); Account(bool od); // Accessor functions double getBalance() const; double getMinBalance() const; // Mutator functions void deposit(double amount); void withdraw(double amount); void changeOD(bool allowed); void setRate(double iRate); void setMonthly(double charge); void setItem(double charge); void monthEnd();
// The data that we must track for each account object private: double balance; double minBalance; bool overDraftAllowed; double interestRate; double itemCharge; double monthCharge; // a private initializer void initialize(bool od); };
initialize can only be called by the implementation code of the other functions in the class
account3.cpp
Account::Account(){ initialize(false); } Account::Account(bool od){ initialize(od); } // Common private initializer function void Account::initialize(bool od){ balance = 0; minBalance = 0; overDraftAllowed = od; interestRate = .0175; itemCharge = .25; monthCharge = 2.00; }
constructorsArray.cpp
class Array{ public: Array(int s); ~Array(); int getSize() const; double read(int i) const; void write(int i, double item); private: double* mpData; int mSize; };
So far, pretty simple minded Array class. However, Array objects now know their own size.
Array objects only have 2 pieces of data, the mpData pointer and mSize. The constructor requests an allocation of space from the heap for the actual array data. the const keyword placed after a member function declaration says that the function is readonly. It will not alter the state of any object to which it is applied.
constructorsArray.cpp
// Implementation Array::Array(int s){ if (s > 0 && (mpData = new double[s])) { mSize = s; for (int i=0; i < mSize; i++) mpData[i] = 0.; } else { mpData = 0; mSize = 0; } } Array::~Array(){ delete[] mpData; } // valid even if mpData is null
int Array::getSize() const { return mSize;} double Array::read(int i)const { if (i<0 || i >= mSize) return 0.0; // poor error detection return mpData[i]; } void Array::write(int i, double item){ if (i<0 || i >= mSize) return; mpData[i]=item; }
Implementation of the class. How could we get the index to run from 1 to mSize instead of 0 to mSize-1?
A String Class
Again, C style strings are pretty primitive so we will roll our own.
myString.cpp
class MyString{ public: MyString(const char* p); // Construct using a standard string MyString(); // "default" (no arguement) constructor ~MyString(); // standard destructor to deal with heap
// Accessor functions - used to read object data without changing it int length() const; char getChar(int i) const; // get char at location i void get(char* buff) const; // Get the string & put it into user buff // Mutator functions - used to change string objects void setChar(const int i, const char c); // Change char at i to c private: char* mPtr; int mLength; }; // pointer into the heap where the actual string will be // length of the string
myString.cpp
MyString::MyString(const char* p){ mLength = strLen(p); if (mPtr = new char[mLength+1]) strCpy(mPtr,p); else mLength = 0; } MyString::MyString(){ mLength = 0; if(mPtr = new char[1]) *mPtr = '\0'; } MyString::~MyString(){ delete []mPtr; }
Now we have a string class with some power! By going to the heap, we can create a string of any size at run time. There are only two actual data members of each string object, taking up very few bytes. The constructors take care of finding the actual space for a string object.
Copy Constructors
It is often the case that we would like to make an exact copy of an object This is often called cloning the object Making a copy means basically requires 1. we construct a new object (the clone) 2. we set its initial state to the current state of the object being copied. For this purpose we use a special form of constructor called a Copy Constructor We have already seen that C++ often makes copies automatically (pass-by-value) Copy constructors are automatically invoked three places 1. Initialization 2. Passing arguments in to a function by value 3. Returning values from functions. The latter two are implicit calls, hidden from the unwary user.
Format
recognized by its unique signature
copycons1.cpp
class X { public: X(); // Null constructor X(int,int); // Two argument constructor X(X&); // Copy constructor private: int f; int g; };
The above is a declaration for class X, stripped to its bare essentials. The copy constructor has as argument a reference to an object of its own class
(Remember - a normal constructor may not take an object of its own class as argument.) Question: Why a reference to an object of its own class? Why not just an object of its own class?
Definition
Here are the definitions for class X as they might be found in a separately compiled file:
copycons1.cpp
// constructor implementations X::X(){ // default f = g = 0; } X::X(int a1, int a2){ // 2 arg f = a1; g = a2; } X::X(X& orig){ f = orig.f; g = orig.g; } // copy constructor
The null constructor (no arguments) sets the f and g members of the object to 0 The two argument constructor sets the f member to the first argument and the g member to the second one The copy constructor copies f and g from the corresponding f and g of the object passed in.
copycons1.cpp
int main(){ X one; // one.f = one.g = 0 X two(1,2); // two.f = 1, two.g = 2 //X three = {3,4}; // three.f = 3, three.g = 4 X four(two); // four.f = 1, four.g = 2 // X five = three; // five.f = 3, five.g = 4 return 0; }
The compiler calls the copy constructor as the only one allowed to have an object of its own class as argument.
The compiler calls the copy constructor for this initialization . In order to utilize the = sign for assignment, the = operator must be formally overloaded (see later).
copycons2.cpp
class X { public: X(); // Null constructor X(int,int); // Two argument constructor ~X(); // destructor needed to correct count int howMany() const; private: int f; int g; static int count; //Keep track of how many objects there are };
In a class, a static variable means there is only one count for the entire class, instead of one for each object of the class. static variables are used for things such as object counting, as here Here is the implementation code for the class:
copycons2.cpp
// constructor implementations int X::count = 0; //defines the class integer count X::X(){ f = g = 0; count ++; } // default
X::X(int a1, int a2){ // 2 arg f = a1; g = a2; count++; } X::~X(){ count--; } // destructor
The static variable has to be defined (implemented) just like a function of the class. Here we set it to 0. Each constructor adds one to the count The destructor decrements the count.
copycons2.cpp
X foo(X anX); int main(){ X one; // one.f = one.g = 0 cout << one.howMany() << endl; X two(1,2); // two.f = 1, two.g = 2 cout << two.howMany() << endl; foo(two); // just returns two cout << two.howMany() << endl; return 0; }
In this case we've gotten rid of our copy constructor and let the compiler build one Notice, when we call our silly fuction foo, count gets fouled up. The problem is that the compiler doesn't know about our counting objects, so it doesn't increment count. However, there is no copy destructor. The normal destructor gets used.
Another Example
As another example of implicit calling, we put a (rather poorly designed) add function in the class Array.
copyConsArray.cpp
class Array{ public: Array(int s); int getSize() const; double read(int i) const; void write(int i, double item); Array add(Array other); // add other to this and return sum private: double* mpData; int mSize; };
copyConsArray.cpp
int main(){ Array smallArray(10); Array otherArray(10);
for (int i = 0; i < smallArray.getSize(); i++) { smallArray.write(i,i*i); // An array of squares otherArray.write(i,i); } smallArray.add(otherArray); // But what do I do with the sum?
The call smallArray.add(otherArray) is a request to add the elements of otherArray to those of smallArray and return a sum array. The implementation code for add is relatively straightforward.
copyConsArray.cpp
Array Array::add(Array other){ if (other.mSize != mSize) return *this; Array sum(mSize); for (int i = 0; i < mSize; i++) sum.mpData[i] = mpData[i] + other.mpData[i]; return sum; }
Because other is passed by value, it is actually a copy of the original object otherArray. Because sum is destructed at the end of the add function and only its value is returned, the return object is actually made by copying sum.
If the designer doesn't provide a copy constructor, the compiler creates one automatically. The compiler's copy constructor is of the form Array::Array(const Array& original){ mpData = original.mpData; mSize = original.mSize; } In other words, the default copy constructor just copies the member data fields In the Teaching Machine you can see this somewhat indirectly. Since there is no actual function to call, it doesn't step into a copy constructor function, per se. Instead, if you step through the expression engine at the calling point you will see the member fields being copied. There is no copy destructor. Instead the regular destructor is called.
Recall that the constructor goes to the heap to get the memory required to restore the array. In the next example, we mimic the compiler's behaviour by providing a copy constructor that is the same as the default constructor the compiler would provide if we didn't. Here is the declaration of Array
shallowCopy.cpp
class Array{ public: Array(int s); // regular constructor Array(Array& original); // Copy constructor ~Array(); // destructor - used for both int getSize() const; double read(int i) const; void write(int i, double item); private: double* mpData; int mSize; };
shallowCopy.cpp
// Just does a straight copy of all member data fields Array::Array(Array& original){ mSize = original.mSize; mpData = original.mpData; }
Now let's check out what happens when we try to use it.
shallowCopy.cpp
int main(){ Array A(10); set(A,3); Array B(A); set(B,7);
// B is a clone of A
We've created an array A then set all it's elements to 3 Then we cloned A to create B and set all B's elements to 7 Look what happens when we output A Be sure to check it in the linked view.
The difficulty is that our objects of Array class has two kinds of storage. 1. The member data, which is compiler controlled. 2. The actual array storage (which is on the heap) which is constructor and destructor controlled. In building the default copy constructor, the compiler only knows about the first kind of memory, so that's all it clones. In copying the pointer into the heap exactly, both objects end up sharing the same heap space. It's not a true clone. It's more like a pair of siamese twins with a single heart or head. We call such a copy a shallow copy.
Deep Copy
In order to clone an array properly we need to give the clone it's own heap space. Then the clone is completely independent of the original. We call such a copy a deep copy. Here's a copy constructor for Array that does a deep copy.
deepCopy.cpp
// Implementation Array::Array(int s){ if (s > 0 && (mpData = new double[s])) { mSize = s; for (int i=0; i < mSize; i++) mpData[i] = 0.; } else { mpData = 0; mSize = 0; } } // A "good" copy constructor, that is one which // does a proper deep copy. Note contrast to constructor Array::Array(const Array& original){ if (mpData = new double[original.mSize]){ // request same amt. of space mSize = original.mSize; // success! we got the space for (int i = 0; i < mSize; i++) // Copy original's heap data *(mpData+i) = *(original.mpData+i); } else mSize = 0; // Oops! no space. Note the problem } Array::~Array(){ // Works for both kinds of constructor
delete[] mpData; }
Note both the similarities to and differences from the regular constructor. The copy constructor 1. requests new storage from the heap (same size as the original) 2. copies the data from the original's heap area to the clone's heap area. In running the example below make sure to look at both the linked view as well as the heap and stack.
deepCopy.cpp
// A function to set every element of an array to value void set(Array array, double value); int main(){ Array A(10); set(A,3); Array B(A); set(B,7);
// B is a clone of A
Do you see some unexpected behaviour? The set function, which worked fine before does not seem to be working. Here is the corrected version. What's the difference? Why did the "incorrect" version work before, but not now?
deepCopyFixed.cpp
// A function to set every element of an array to value void set(Array& array, double value); int main(){ Array A(10); set(A,3); Array B(A); set(B,7);
// B is a clone of A
The incorrect version of set passes in an Array object by value. This means that a copy of the object gets made and the value gets set in that copy. This worked in the case of a shallow copy because the copy and the original Array shared the same data in the heap. So when that data area got set in the copy, it was set in the original as well. Once we created a true deep copy, the data area was set in the copy but not in the original and the set data was lost once the set function was ended. Passing the Array object to set by reference solves the problem.
Running Classes
We need to ammend our running classes to take copy constructors into account. We've already taken care of the Array class. The Account class doesn't need a copy constructor because it just uses standard memory so the compiler (default) version works fine. This leaves the MyString class. Like Array, it gets it's actual storage from the heap. Actually, there's a simple test for the need to build your own copy constructor. Did you need a destructor? If so, you virtually always require a copy constructor. Array and MyString needed a destructor to clean up the heap and our X class required one when we added a static Object count (to decrement the count whenever an object was destroyed). All of them need copy constructors. Account didn't need a destructor and it doesn't need a copy constructor. The defaults work fine. So, without more ado, here is the declaration for a MyString class with a proper copy constructor.
myStringCopyCon.cpp
class MyString{ public: MyString(char* p); // Construct using a standard string MyString(); // "default" (no arguement) constructor MyString(MyString& orig); // Copy constructor ~MyString(); // standard destructor to deal with heap
// Accessor functions - used to read object data without changing it int length() ; char getChar(int i) ; // get char at location i void get(char* buff) ; // Get the string & put it into user buff bool compare(MyString& other); // true if equal // Mutator functions - used to change string objects void setChar(int i, char c); // Change char at i to c void changeTo(char* newString); // Change the whole string void changeTo(MyString newString); // Function overload private: char* mPtr; int mLength; }; // pointer into the heap where the actual string will be // length of the string
Another feature has been added as well. The function changeTo( ) allows a whole string to be changed (even if the new string is a different length) Note there are two different versions of changeTo void changeTo(const char* newString); void changeTo(const MyString newString); The first one lets us provide a conventional C string The second version lets us use a MyString object to effect the change. We can overload conventional functions as well as constructors.
Implementation code
Here are the constructor implementations
myStringCopyCon.cpp
MyString::MyString(){ mLength = 0; if(mPtr = new char[1]) *mPtr = '\0'; } MyString::MyString( char* p){ mLength = strLen(p); if (mPtr = new char[mLength+1]) strCpy(mPtr,p); else mLength = 0; } MyString::MyString( MyString& original){ if (mPtr = new char[original.mLength + 1]){ // Request heap space mLength = original.mLength; // Success strCpy(mPtr, original.mPtr); // Copy actual string }
else mLength = 0; }
Notice again how similar the constructor and the copy constructor are. That is not a surprise since they fundamentally have to do the same thing build a valid MyString object. However there are subtle differencesthese stem from the fact that the source of the information required to build the object is different. Finally, although we could have used array notation (after all, conventional C strings are arrays of characters) pointer notation has been used, just to keep you in touch with it. Notice that mPtr is never actually moved (pointer + offset notation is used). mPtr marks the beginning of our heap storage and we never move it because our MyString objects could be destructed at any time. Here is the destructor
myStringCopyCon.cpp
MyString::~MyString(){ delete []mPtr; }
When delete is called on mPtr, we want it pointing at the beginning of our storage or we will confuse the heap manager.
A Compare Function
We also added a function to allow us to compare two MyString objects (If you invoke the TM here, just step into the compare function for now)
myStringCopyCon.cpp
bool MyString::compare(MyString& other){ if (mLength != other.mLength) return false; for (int i = 0; i < mLength; i++){ if (*(mPtr+i) != *(other.mPtr+i)) return false; // exit as soon as a difference } return true; // everything the same if arrive here }
This one only tells us if the objects are the same or not (later on we'll tackle the problem of which one is "greater") Notice we do a cheap test first. If the strings have different lengths they can't be the same.
myStringCopyCon.cpp
void MyString::changeTo(char* newString){ long l = strLen(newString); if (l != mLength){ // if diff, re-allocate memory char* pTemp = new char[l+1]; if (!pTemp) return; //punt!! delete [] mPtr; // Release old storage mPtr = pTemp; // hook-up to new } strCpy(mPtr, newString); }
It takes a conventional string as argument. Since that string might be of a different length than our existing string we have to take care of that. The code looks similar to the copy constructor (look back and see). There is one crucial difference We have to deal with the fact that our object owns a pre-existing string. If we're going to create new space on the heap (because the length is different) we must make sure we release the old storage properly! Notice how careful we are. We first create a temporary pointer, pTemp, and ask the heap manager for space (by invoking new) Only if we succeed do we release the old space and reset mPtr to the new space. Here's the version of changeTo overloaded to accept a MyString object as the source of its new string.
myStringCopyCon.cpp
// Overloaded version of changeTo to allow the string to // be changed to equal another MyString object void MyString::changeTo(MyString newString){ changeTo(newString.mPtr); // Just convert to conventional call }
Notice how simple it is. All it does is call the first version, passing it the internal mPtr of the source object. Why did we bother? In our test code we have the following:
myStringCopyCon.cpp
MyString password("bfltpx"); password.changeTo("btfplk"); password.changeTo(unlucky); // Hah! Get that one. //Oops! I spelled it wrong // Heck with it, I'll never remember it!
When we wanted to use the unlucky MyString object to change our password why not just give it the pointer directly? password.changeTo(unlucky.mPtr); // Heck with it, I'll never remember it!
The answer, of course is that we can't. mPtr is private! We're not allowed to manipulate it from code outside the class code. But the class code can. So we provide a simple overload that passes the pointer in for us.
user.cpp
// A class of users for network management class User{ public: User(char* n, char* u, char* p); // Accessors MyString name(); int allocation(); // return mName // Disk space allowed
MyString password(); // return value MyString uname() ; bool confirm(const MyString& uName, const MyString& pass); // Mutators bool setPassword(const MyString& p1, const MyString& p2); void setAllocation(int a); private: MyString mName; // Notice attributes that are OBJECTS! MyString mUname; MyString mPassword; int mAllocation; // Memory allocation, in MBytes };
Notice there is only a constructor. We haven't supplied either a destructor or a copy constructor. (Remember, if we need one, we need the other. Conversely, if we don't need one, we don't the other.) But do we need them? First let's focus on the constructor. We introduce a new formatthe initialization list.
user.cpp
User::User(char* n, char* u, char* p) : mName(n), mUname(u),mPassword(p){ }
the list is in the form : mName(n), mUname(u),mPassword(p) and it precedes the body of the constructor (which is empty in this case although it doesn't have to be) Remember, the compiler allocates space for the object's attributes before it calls the constructor That gives us a problem because the attributes are objects. Allocating space for them means calling their constructors! C++ gets around the problem by setting up the initializer list. It's meaning is simple. The arguments passed into the User constructor, n, u, and p (which are pointers to conventional C strings) are passed straight to the mName, mUname and mPassword constructors. Since they are all MyString objects, it's actually three calls to the MyString constructor, applied to the three different objects. Why don't we just step into the User constructor once or twice and watch this work.
user.cpp
User mpbl("Michael Bruce-Lockhart", "mpbl", "bftplx"); User theo("Theo Norvell", "theo", "charl1eB");
user.cpp
User mpblClone(mpbl);
user.cpp
//mpblClone.setPassword(MyString("unlucky"), MyString("unlucky")); MyString noLuck("unlucky"); mpblClone.setPassword(noLuck, noLuck);
Now they are built just at the point we need them (the call to setPassword) and disappear like smoke when we're done with them.
Summary
Copy constructors are used to create new objects by cloning an existing object Their signature is the same as a constructors except they take a reference to an object of the same class as their argument. Pass-by-value (either when entering a function or returning a value) both involve copying and therefore call the copy constructor. If you don't provide a copy constructor, the compiler will build one for you (it will simply copy the attributes of the original object). If a constructor allocates extra memory on the heap and the copy constructor only copies the original object's attributes, it is called a shallow copy. Copying both the extra heap space as well as the original attributes is known as making a deep copy. If you need a destructor, you need a copy constructor (and vice versa). A static class variable means there is only a single copy of the variable for the whole class (objects don't get their own copy). Static variables are used for class maintenance (like object counting). The presence of static variables or the need to go to the heap for extra space are strong indicators that a destructor and copy constructor are required. Any function in C++ can be overloaded. Classes may use objects of other classes as attributes. It is possible to create anonymous objects on the fly.
Inheritance
Inheritance is one of the most powerful tools in the Object Oriented Programming Style (OOPS) It is one (of a number) of techniques for making code reusable It allows one to extend a piece of existing code by specialising it. It models a very particular kind of relationship between classes known as polymorphism. This is often known as the "is-a" relationship. Consider two of the classes we have been working with recently. Account and User Both are representative of a kind of generic category. Consider the table below.
Savings and checking accounts and lines of credit are all special kinds of accounts. They all share common characteristics, such as a balance and an ability to put money in or take it out. But they all have special characteristics as well. Lines of credit normally have negative balances and large overdraft limits, while savings accounts pay interest. Similarly, different kinds of computer accounts have different policies applied to them. For example, in our institition, undergraduates pay for printing while faculty don't. Graduate students fall in between, getting an allocation large enough to do a thesis, but paying if they exceed the allocation. This kind of organization scheme is quite general. Remember that, at bottom, classes represent categories. Human categorization of knowledge is not confined to single categories. Rather we use sub categories and more sub categories, organizing knowledge into hierarchies. Thus human beings are hominids which are primates which are mammals That is humans are a kind of hominid which in turn is a kind of primate which is a kind of mammal. All mammals have warm blood.
All primates have large brains and opposable thumbs. As mammals they also have warm blood. (In the OOPS world we would say they inherit the characteristic of warm bloodedness.) All hominids walk on two legs. (They also have opposable thumbs and large brains, because they are primates, and warm blood because primates are mammals.) Humans have a soft palate. (They also walk on two legs because but you get the picture) In OOPS, a sub-category is called a sub-class. And objects of sub-classes inherit all the attributes and methods of their super class(es). Before turning our attention to Users and Accounts let's look at a very simple minded system.
simpleInherit.cpp
class A { public: void aFunc() ; protected: int x1; private: int x2; } ; class B : public A { public: void bFunc() ; protected: int y1; private: int y2; } ; class C : public B { public: void cFunc() ; private: int z ; } ;
This system forms a hierarchy with three generations. You can think of A as the grandparent, B as the parent and C as the child. We say that bObj is a B (an object of class B). But it is also an A. Likewise, cObj is a C as well as a B as well as an A. Thus an object of class C has five attributes (its own z plus y1 and y2 from B and x1 and x2 from A) And it has three methods (its own cFunc() plus bFunc() from B and aFunc() from A). We have also introduced a new access modifier, protected. Protected members of a class are accessable to sub-classes of the class but not to the outside.
Here is the implementation code for the three methods declared in the classes A, B and C.
simpleInherit.cpp
void A::aFunc() { x2 = 53; } void B::bFunc() { x1 = 7; y2 = 9; aFunc(); } void C::cFunc() { x1 = 13; y1 = 72; z = 99; bFunc(); }
can set z directly (because it belongs to C) can set y1 and x1 directly (because, although they belong to classes B & A respectively, they are protected and thus available to C objects) must go through the public interface function bFunc() to set x2 and y2 as they are private.
Access Modifiers
Let's look at our simple hierarchial class declarations one more time
simpleInherit.cpp
class A { public: void aFunc() ; protected: int x1; private: int x2; } ; class B : public A { public: void bFunc() ; protected: int y1; private: int y2; } ; class C : public B { public: void cFunc() ;
private: int z ; } ;
Note that the syntax class B : public A means that class B is publicly derived from A It is also possible to derive a class privately from another. To see what that means consider the following table:
Access in Derived Class public can't access protected private can't access private
Notice that the derived class cannot loosen the security defined by its parent. It can only tighten it. Thus public members stay public if publically derived but become private if privately derived. Private members of the parent class are inaccessable by children. Protected mebers of the parent class stay protected if publicly derived but become private if privately derived.
access.cpp
class A { int i,j; protected: int k; public: int george(); }; class B: public A { int l; public: int m; int john(); };
What variables may john(), george() & alice() access? What access is permitted for the objects of classes A, B & C?
../constructors/account3.cpp
class Account {
/* The interface for a class to model bank accounts */ public: // Constructors Account(); Account(bool od); // Accessor functions double getBalance() const; double getMinBalance() const; // Mutator functions void deposit(double amount); void withdraw(double amount); void changeOD(bool allowed); void setRate(double iRate); void setMonthly(double charge); void setItem(double charge); void monthEnd(); // The data that we must track for each account object private: double balance; double minBalance; bool overDraftAllowed; double interestRate; double itemCharge; double monthCharge; // a private initializer void initialize(bool od); };
When you start to think in terms of a hierarchy of classes, it is best to rethink them from scratch.
First, what is common to all accounts? That's what should go into the base class.
accountInherit.cpp
// The base class - distills what is common to all accounts class Account { public: // Constructors Account(); // Accessor functions double getBalance() const; // Mutator functions void credit(double amount); void debit(double amount); // The data that we must track for each account object protected: double balance; };
The base class is quite stripped down. It just has a balance and four functions. Moreover, the names of deposit and withdraw have been changed to the more generic credit and debit (which are better terms for charge accounts, like VISA). These functions are implemented in a very generic way
accountInherit.cpp
Account::Account(){ balance = 0; } double Account::getBalance() const {return balance;} // Generic approaches to mutators. Not right for all accounts // Assertion: amount is non-negative void Account::credit(double amount) { if (amount >= 0) balance = balance + amount; } // Assertion: amount is non-negative void Account::debit(double amount) { if (amount > 0 && amount <= balance ) balance = balance - amount; }
accountInherit.cpp
class Checking: public Account{ /* Checking is derived from Account Checking accounts are accounts that permit overdrafts and charge interest on negative minimum monthly balances */ public: Checking(bool od); double getMinBalance() const; void setODPolicy(bool allowed); void monthEnd(); void debit(double amount); // Over-ride default version in account // Set rates for all checking accounts static void setRate(double iRate); static void setMonthly(double charge); static void setItem(double charge); protected: bool odAllowed; double minBalance; // All checking accounts will use the same rates static double interestRate; static double monthlyCharge; static double itemCharge; };
Checking is publicly derived from class Account. It adds the notion of minimum balance as well as various charges. It over-rides the debit function because the generic one can't do the job. But not the credit function because the generic one in Account works just fine. Notice, however, all the charges are static. This means that there is one set of charges for all Checking account objects. The member functions to set charges are also static. What does this mean? After all there's only one actual member function for a whole class anyway. Static functions are called with respect to the whole class not a particular object of the class. That is to invoke setRate for the Checking class I would write Checking::setRate(.02); The real benefit of this is that we don't have to create any Checking objects before invoking the function.
With regular member functions, you must have an object before you can call the function. Here's the extension to savings accounts
accountInherit.cpp
class Savings: public Account{ /* Savings is derived from Account Savings accounts are accounts that permit no overdrafts but pay interest on positive minimum monthly balances */ public: Savings(); double getMinBalance() const; void debit(double amount); // generic function override void monthEnd(); static void setRate(double iRate); static void setMonthly(double charge); static void setItem(double charge); protected: double static static static };
It's quite similar but the policies will be different. For example there is no bool odAllowed attribute because no savings accounts are ever allowed to go into overdraft. Here is the implementation code for Checking objects.
accountInherit.cpp
double Checking::interestRate = .0175; double Checking::monthlyCharge = 2.00; double Checking::itemCharge = 0.0; // overdrafts
void Checking::setRate(double iRate){interestRate = iRate;} void Checking::setMonthly(double charge){monthlyCharge = charge;} void Checking::setItem(double charge){itemCharge = charge;} Checking::Checking(bool od){ // Account constructor called 1st odAllowed = od; minBalance = 0.; } double Checking::getMinBalance() const {return minBalance;}
void Checking::setODPolicy(bool od){ odAllowed = od; } void Checking::debit(double amount){ if (amount < 0) return; // assertion failure if (!odAllowed && balance - amount - itemCharge < 0 ) return; // overdraft not allowed balance = balance - amount - itemCharge; if (balance < minBalance) minBalance = balance; } void Checking::monthEnd(){ // over-rides Account version debit(monthlyCharge-itemCharge); if (minBalance < 0) balance = balance + interestRate*minBalance; minBalance = balance; }
Note that the debit function is much more complex than the one in Account since it has to take both charges and overdrafts into account. Finally here is some code that utilizes the various kind of accounts.
accountInherit.cpp
Account mine, yours; // "Generic" accounts
// With static functions we can set rates for all savings accounts // without actually having declared any Savings Object yet Savings::setItem(.50); Savings::setMonthly(1.50); Savings::setRate(.002); // Now we declare some Savings Accounts Savings mySavings, yourSavings; Checking myChecking(true), yourChecking(false); mine.credit(200); mine.debit(100); mine.credit(1500); cout << "my Balance is " << mine.getBalance() << endl; mySavings.credit(200); mySavings.debit(100); mySavings.credit(1500); mySavings.monthEnd(); cout << "my Savings Balance is " << mySavings.getBalance() << endl; // How come we didn't get any interest?? mySavings.debit(100); mySavings.credit(2000); mySavings.monthEnd(); cout << "my Savings Balance is " << mySavings.getBalance() << endl; myChecking.credit(100); myChecking.debit(200); myChecking.credit(1500); myChecking.monthEnd();
cout << "my Checking Balance is " << myChecking.getBalance() << endl; yours.credit(100); yours.debit(200); yours.credit(1500); cout << "your Balance is " << yours.getBalance() << endl; // Let's try using the transaction function with different Accounts transaction(mine, 200.); transaction(mine, -100.); transaction(mySavings, 200.); transaction(mySavings, -100.); return 0; }
Pay particular attention to the constructor calling sequences (by stepping into them in the TM)
userInherit.cpp
// A class of users for network management class User{ public: User(char* n, char* u, char* p); // Accessors MyString name() const; int allocation() const; // Disk space allowed MyString password() const; MyString uname() const; bool confirm(const MyString& uName, const MyString& pass) const; // Mutators bool setPassword(const MyString& p1,const MyString& p2); void setAllocation(int a); protected: MyString mName; // Notice attributes that are OBJECTS! MyString mUname; MyString mPassword; int mAllocation; // Memory allocation, in MBytes };
The only change is to use the new access control protected where we had private before. This allows the class to be extended, giving member functions of derived classes direct access to this data.
Students have id numbers and they have to pay for printing so two attributes have been added mId records their id mPrintPennies keeps track of how much money they have in their print accounts a number of operations have been included to handle these extra attributes.
Constructor Chaining
Recall that a Student object is a User object so that constructing a Student object also implies constructing the User part of it. The constructors are chained. when a Student object is constructed its User part must be constructed first. That is, declaring a Student object 1. invokes the Student constructor which in turn 2. invokes the User constructor which, when it has finished 3. returns to the Student constructor which then does its initialization As you can see, the Student constructor takes four arguments. Three of them, however, are needed by the User constructor. How do they get passed along? One again the answer is to employ an initializer list
userInherit.cpp
User::User(char* n, char* u, char* p) : mName(n), mUname(u),mPassword(p),mAllocation(0){ } Student::Student(char* n, char* id, char* u, char* p) : User(n,u,p),mId(id){ // Initialization - data to User constructor mAllocation = 5; mPrintPennies = 0; }
Here we show the User constructor, which as was seen previously, uses an initializer list to pass arguments on to the various MyString constructors. Displayed right after it for comparison is the Student constructor. It uses the initializer list to pass the three arguments needed by the User constructor As well as to pass the id string to the MyString constructor to build the mId object Finally, in the body of the constructor, the allocation and number of print pennies are initialized.
(The arguments are actually passed as pointers. We show strings pointed at for clarity)
Faculty members get free printing but a record of total pages is kept. It can be cleared from time to time so the record is actually the number of pages printed since the last clearing date. This requires the addition of attributes mPages which tracks pages mCleared which is a MyString object which records the last time pages were cleared plus new functions for manipulation. The constructor issues are similar to those for class Student. Here is a little bit of driver code to try it out.
userInherit.cpp
int main(){ Faculty mpbl("Michael Bruce-Lockhart", "mpbl", "bftplx", "11/11/2002"); Student cullam("Cullam Bruce-Lockhart", "2001001762", "cullam", "oBiewan"); // Faculty mpblClone(mpbl); mpbl.setAllocation(100); cullam.setAllocation(10); cullam.buy(1000); // $10.00 printing credit return 0; }
Use the Teaching Machine to step through the constructor chains when objects of class Student and Faculty get declared.
Again, watch how automated the process is. We take care of implementing the code correctly. The compiler worries about which constructors have to be called when.
Student::Student(const Student& original) : User(original) { // Extra code for student copy construction goes here }
Again, the idea is to reutilize the copy constructor code for the User object, then just add whatever extra is needed to copy construct a Student object. If the copy constructor code for Student is to be written from scratch, simply omit the initilizer invocation of User.
Ad Hoc Polymorphism
Polymorphism: n. 1. Biology. The occurrence of different forms, stages or colour types in individual organisms. 2. Chemistry. Chrystallization of a compound in at least two different forms. To which we add3. OOPS. The ability to give different meanings to the same message. For example a + b could result in addition when applied to integers but concatenation when applied to strings. How the message is to be interpreted depends upon the type of the data it is being applied to. ad hoc: Latin. With respect to this (particular thing); for a specific purpose, case or situation.
The signature does not include the return type Order of arguments does matter.
overload1.cpp
/* A pair of declarations with identical signature. The compiler will complain! */ int f1(int i); // signature is int double f1(int a); // signature is int // Order of arguments does matter. // double // int,double // double,int
void f2(double a); void f2(int a,double b); void f2(double a,int b);
1. Use an exact match if found 2. Try standard type promotions. 3. Try standard type conversions. 4. Try user-defined conversions
5. Use a match to ellipsis if found (we won't even talk about this one)
overload1.cpp
f2(3.14159); // exact match to double f2(6); // promotes and uses double f2(3.7,9); // matches double,int f2(1,13.1); // matches int,double long i = 3; f2(i,4.1); // converts and uses int,double f2(3,4); // ambiguous, no match f2(1.7,-3.2); // ambiguous, no match f2(3,4,5); // no match for three arguments/*#HB*/
overloadSwap.cpp
// The declarations void swap(char& left, char& right); void swap(int& left, int& right); void swap(long& left, long& right); void swap(float& left, float& right); void swap(double& left, double& right);
Here are a couple of the implementations Look how similar they are!
overloadSwap.cpp
void swap(char& left, char& right){ char temp; temp = left; left = right; right = temp; } void swap(int& left, int& right) { int temp; temp = left; left = right; right = temp; }
overloadSwap.cpp
int i1 = 3; int i2 = 5; char c1 = 'a'; char c2 = 'z'; float f1 = 3.14; float f2 = 0.; double d1 = 3.14; double d2 = 0.; swap(i1,i2); swap(c1,c2); swap(d1,d2); swap(d1,i2); swap(f1,f2);
//
You might consider what would happen to the last call if we hadn't defined a version of swap for the rarely used float type.
Operator Overloading
In order to overload operators you must be allowed to write your own operator functions. the names of such functions always consist of the word 'operator' immediately followed by the actual operator, for example operator+ operatoroperator& operator[]
All C++ operators except the member operator ., the member object selector operator .* (not covered yet), the conditional operator ? :, the sizeof operator and the scope resolution operator :: may be overloaded. Some operators are unary (take one operand): operator++ Some operators are binary (take two operands): operator&& Some operators can be unary or binary, depending upon the context: operator-, operator* we consider binary & unary operators separately
Binary Operators
We'll start by overloading the + and - operators for class Complex. As with any function there are three contexts in which a function can appear. Declaration, definition (implementation) and call. These three contexts are sufficiently important that member functions are sometimes given distinct names depending upon the context, as follows:
operation: the name given to a member function in a class declaration (or interface). Operation suggests what we want done method: the name fiven to a member function when it is being implemented. Method suggests how we do it. message: the name being given to a member function when it is being called. Message suggests we are telling the object to do it. Unfortunately, not all authors observe these distinctions. Sometimes the names get used interchangeably. Here is class Complex with a couple of operator operations declared.
complex1.cpp
class Complex { public: // These three should be replaced by // Complex(double r = 0., double i = 0.); TM unsupported Complex(double r, double i); Complex(double r); Complex(); Complex operator+(const Complex& rhs); Complex operator-(const Complex& rhs); private: double re,im; };
The declaration Complex operator+(Complex& rhs) says that we want a plus operation that will add rhs to the calling object and return a sum object Of course, we infer that the object returned is the sum, but it makes sense given the name. Use comments to clarify and define. The operators only take one argument because, as member functions, there is an implicit this argument in the form of the object attached to the method when it is invoked. Now let's look at the implementation of the operator methods.
complex1.cpp
Complex Complex::operator+(const Complex& rhs){ return Complex(re+rhs.re,im+rhs.im); } Complex Complex::operator-(const Complex& rhs){ return Complex(re-rhs.re,im-rhs.im); }
The implicit argument is taken to be the left operand of the operator and the explicit argument, the right operand. Notice we are required to return a Complex object. Since that is returning a value it would normally invoke the copy constructor. This is supressed here, however, because we are constructing (anonymously) the new sum (or difference) object right in the return statement. The interesting issue is how do the calls work? (How do we send the add and subtract messages?)
complex1.cpp
int main(){ Complex x(-1., -1.); Complex y(2.2); Complex z; z = x + y; z = x - y; return 0; }
The crucial statements are z = x + y; and z = x - y; They are conventional add and subtract operations. What does that have to do with our operations? The compiler automatically maps the expression x + y x.operator+(y) and x - y x.operator(y) That is, the general form
leftOperand rightOperand
is mapped by the compiler to
leftOperand.operator(rightOperand)
Where
Step into the code above with the TM and watch what happens when you step through the expression and reach the plus sign.
friend functions
Unfortunately, the mapping above has a fairly serious limitation. Sometimes, you want to do operations between types. For example, it makes sense to scale a Complex number by a real no. This we could do as follows: Complex operator*(double rhs) Thus in a set of statements like Complex x(2., -3.4); Complex z; double a; z = x*a; The term x*a would get mapped to x.operator*(a) as expected. Expectation is the problem, though. A user of your class, not as familiar with the intricacies of overloading as all of you are, might reasonably expect to be able to write z = a*x; However the standard mapping would produce a.operator*(x) which doesn't work at all. Not only is that form not declared, but it can't be declared. a is a double, not an object, so we can't define operations or methods for it. Enter friend functions It is possible to declare a function to be a friend of a class.
complexFriend.cpp
friend friend friend friend friend friend Complex Complex Complex Complex Complex Complex operator+(const Complex& lhs, const Complex& rhs); operator-(const Complex& lhs, const Complex& rhs); operator+(double lhs, const Complex& rhs); operator+(const Complex& lhs,double rhs); operator-(double lhs, const Complex& rhs); operator-(const Complex& lhs,double rhs);
friend functions are not members of a class but they do have access to the private members of a class (just as normal member functions do) a function which is to be a friend of the class must be declared as such within the class. binary friend functions require two arguments because, as a non-member function of the class, there is no implicit argument. Here is the implementation of the functions
complexFriend.cpp
Complex operator+(const Complex& lhs, const Complex& rhs){ return Complex(lhs.re+rhs.re, lhs.im+rhs.im); } Complex operator-(const Complex& lhs, const Complex& rhs){ return Complex(lhs.re-rhs.re, lhs.im-rhs.im); } Complex operator+(double lhs, const Complex& rhs){ return Complex(lhs + rhs.re, rhs.im); } Complex operator+(const Complex& lhs, double rhs){ return Complex(lhs.re + rhs, lhs.im); } Complex operator-(double lhs, const Complex& rhs){ return Complex(lhs - rhs.re, rhs.im); } Complex operator-(const Complex& lhs, double rhs){ return Complex(lhs.re - rhs, lhs.im); }
Because they are not members of the class, friend functions are implemented just like normal functions. Complex:: is not in the header Notice that the moment we start mixing types, we generate a lot of possible operations.
Explicit arguments permit mixed operations. Operators can be declared friends of more than one class (again supporting mixed ops)
Iterator Overload
Remember our Array class?
../constructors/copyConsArray.cpp
class Array{ public: Array(int s); int getSize() const; double read(int i) const; void write(int i, double item); Array add(Array other); // add other to this and return sum private: double* mpData; int mSize; };
The read and write functions are clumsy. Wouldn't it be nice to use standard [] array notation with it?
array.cpp
class Array{ public: Array(int s); // regular constructor Array(const Array& original); // Copy constructor ~Array(); // destructor - used for both int getSize() const; // operator overloads double& operator[](int i); // This replaces read AND write! Array operator+(const Array& rhs); private: double* mpData; int mSize; };
We've replaced the add function with an overload of operator+. We've declared an overload of operator[] (known as the iterator). It replaces both read and write. First consider the overload of the operator+
array.cpp
Array Array::operator+(const Array& rhs){ if (rhs.mSize != mSize) return Array(0); Array sum(mSize); for (int i = 0; i < sum.mSize; i++) sum.mpData[i] = mpData[i] + rhs.mpData[i]; return sum; }
Note the design Returns a valuei.e. a temporary object Only works if a and implicit object are the same size. Note the implementation (Code) Returns a null Array if there is a size mismatch (i.e., something sensible must be done with errors) sum.size is used as the bound in the for loop in case there was not enough space in the heap to create the needed temporary object. Utilizes the existing code of the copy constructor to do half the work! On to the iterator overload!
array.cpp
// Iterator overload double dummy; // a static dummy variable double& Array::operator[](int i) { // error if outside array. return harmless dummy location if (i<0 || i>(mSize-1)) return dummy; return mpData[i]; // Note array notation used with pointers }
If i is an illegal index, an error should be thrown. Because of TM limitations, a dummy variable is returned, fulfilling the contract and doing no further harm If it's legal the i'th element of the Array is returned. Because it's returned as a reference, it can be used to write to Thus, the code below is all legal.
array.cpp
int main(){ Array smallArray(10); Array otherArray(10); for (int i = 0; i < smallArray.getSize(); i++) { smallArray[i] = i*i; // An array of squares otherArray[i] = i; } for (int j = 0; j < smallArray.getSize(); j++) cout << smallArray[j]<< " "; return 0; }
When writing to smallArray, all that is done is smallArray[i] = i*i; in other words, put i*i into location i of smallArray. When reading it, cout << smallArray[j]<< " "; or read the ith location of smallArray and output it. Notice the special mapping The compiler maps smallArray[i] smallArray.operator[](i) The i goes between the square brackets in the standard expression but gets mapped to the parenthesis in the operator[] function. You were told this stuff was ad hoc!
complexFull.cpp
class Complex { public: // These three should be replaced by // Complex(double r = 0., double i = 0.); TM unsupported Complex(double r, double i); Complex(double r); Complex(); double real() const; double imag() const; Complex operator+(const Complex& rhs); Complex operator-(const Complex& rhs); Complex operator*(const Complex& rhs); Complex operator/(const Complex& rhs); Complex& operator=(const Complex& rhs); bool operator==(const Complex& rhs); bool operator!=(const Complex& rhs); friend ostream& operator<<(ostream& s,const Complex& c); private: double re,im; };
You haven't seen all the overloads before but most of them are fairly straightforward.
Not so the insertion and extraction operators. Nevertheless, we just show them here with no comment or explanation. When I want to use them I just look up an example like this and use it as a template.
complexFull.cpp
/* Overloading the insertion operator for class complex */ ostream& operator<<(ostream& s,const Complex& c) { s <<'('; if (c.real()) s << c.real(); if (c.imag()) if (c.imag() > 0) s << " + " << c.imag() << "j"; else s << " - " << -c.imag() << "j"; if ( !(c.imag()) && !(c.real()) ) s << "0"; s << ')'; return s; // Allows concatenation of stream op }
Assignment Overload
For objects of class Complex we would think nothing of writing the following:
assignFrags.cpp
Complex A(-3.1,4.2); Complex B(1.35,11.1); Complex D; D = A + B;
A and B are initialized to specific values but we don't bother for D, because we're going to set D to the sum. It happens in our design that D will be initialized for us by the no-argument constructor to {0.,0.} but even if it weren't, it would make no difference since we're going to reset it at once. It would be nice to be able to assign arrays in the same way.
assignFrags.cpp
Array A(10); Array B(10); Array D; load(A); load(B); D = A + B; // input data to A // input data to B
Whoops! The meaning is different. A and B are arrays of 10 elements. A set function is being called to initialise them somehow. We don't bother with D because we're going to set D to the sum of A & B But this means D will have to change size Enter the Assignment overload
arrayAssignment.cpp
Array& operator=(const Array& rhs); // Assignment
operator= is a binary operator A reference to an Array is returned to let the left side operand get written into. Remember D = sum gets mapped by the compiler to D.operator=(sum) Here is the implementation
arrayAssignment.cpp
Array& Array::operator=(const Array& rhs){ if (mSize != rhs.mSize) { // Oops, not the same size delete []mpData; // release old storage if (mpData = new double[rhs.mSize]) mSize = rhs.mSize; storage else mSize = 0; } for(int i=0;i<mSize;i++) // copy rhs data to lhs mpData[i] = rhs.mpData[i]; return (*this); // i.e. return the object itself } // This allows MULTIPLE assignment
arrayAssignment.cpp
Array::Array(const Array& original){ if (mpData = new double[original.mSize]){ // request same amt. of space mSize = original.mSize; // success! we got the space for (int i = 0; i < mSize; i++) // Copy original's heap data *(mpData+i) = *(original.mpData+i); } else mSize = 0; // Oops! no space. Note the problem }
Notice
DESIGN: Assignment of unequal Arrays is not an error. Instead the old destination Array is destroyed and a new one created the same length as the source. How assignment differs from initialization. There is a pre-existing Array while in initialization, you are constructing the Array for the first time
arrayAssignment.cpp
int main(){ Array A(10); Array B(10); for (int i = 0; i < A.getSize(); i++) { A[i] = i*i; // An array of squares B[i] = i; } Array C = A + B; Array D; D = A + B; for (int j = 0; j < D.getSize(); j++) cout << D[j]<< " "; return 0; }
Thus the line Array C = A + B is initialization (so the Copy Constructor gets called) while the lines Array D; D = A+B; constitute assignment, so the assignment overload is called.
(One exception would be automatic counting of objects. If the only reason you need a copy constructor & destructor is to properly handle the updating of a static variable, the assignment overload will not be required, since assignment does not change the no. of objects in existence, it merely reassigns the value of a pre-existing object.)
Type Conversion
Users can define their own type conversions Single argument constructors are inherently considered type conversions. Complex z(3.); // declares/constructs Complex {3. + j0} Array a(5); // declares/constructs 5 element Array The first example is clearly a type conversion. The second example doesn't look anything like a sensible type conversion. The two examples are semantically equivalent. The compiler cannot distinguish them & must either treat both or neither as type conversions. Consider the following version of class Complex which is overloaded for +, -, double() and <<
complexTypeConv.cpp
class Complex { public: // These three should be replaced by // Complex(double r = 0., double i = 0.); TM unsupported Complex(double r, double i); Complex(double r); Complex(); double real() const; double imag() const; Complex operator+(const Complex& rhs); Complex operator-(const Complex& rhs); operator double() const; friend ostream& operator<<(ostream& s,const Complex& c); private: double re,im; };
complexTypeConv.cpp
Complex::operator double() const { return sqrt(re * re + im * im); }
int max(int a, int b) { return (a>b?a:b);} double max(double a, double b) { return (a>b?a:b);} Complex max(const Complex& a, const Complex& b) { return (a>b?a:b); }
First two are clear but why does the third work? We haven't overloaded the > operator for class Complex. We have provided an overload for the (double) typecast. In evaluating expressions the compiler invokes type conversions automatically and so casts a>b to (double)a>(double)b. This conversion does not apply to the second pair, so a Complex is returned.
complexTypeConv2.cpp
int main(){ Complex x(-1., -1.); Complex y(2.2); Complex z; double a = 4.1, b = 7.2; int i = 3; z = x + y; z = a + y; z = i + y; return 0; }
(It has no assignmemt overload but, as one of the Big Three, the compiler builds a perfectly adequate one for us). The first assignment gets mapped by the compiler to z = x + y z.operator=(x.operator+(y)); The second case is quite tricky. 1. The compiler cannot find the explicit signature operator+(double,Complex&) 2. It can find no built-in promotion from Complex to double 3. It can find no built-in conversion from Complex to double 4. It can find a user defined conversion the Complex::double() and the constructor with one real argument (semantically equivalent to a conversion)
z = a + y z.operator=(Complex(a + double(y))) The third case is like the second except that the compiler has to convert i from int to double.
Caveats
Operator overloading is very powerful, very tempting & very difficult to get right. It is obviously very subtle (remember Complex max(Complex a,Complex b){return a>b?a:b;} as well as addition examples just previous). You can do nothing to alter operator precedence e.g. If the C++ exclusive OR operator, ^, is overloaded for exponentiation, a^b evaluates as expected but a^b+c does not because, in C++, + has higher precedence than ^.
Parametric Polymorphism
Remember swap?
cswapfrag.cpp
void intSwap(int *a1,int *a2){ int temp = *a1; *a1 = *a2; *a2 = temp; }
This is the old C style. We had to create different swaps for different data types and give them different names (like dbleSwap(double *,double *), charSwap(char *, char *)). C++ improved this by allowing us to overload the swap function, so we could always use the same name. (The compiler then mangles the names to create different ones automatically.) But we still have to write separate functions for each (note switch to C++ pass-by-reference as well)
C++SwapOverload.cpp
void swap(int& a1,int& a2){ int temp = a1; a1 = a2; a2 = temp; } void swap(char& a1,char& a2){ char temp = a1; a1 = a2; a2 = temp; }
A Generic Swap
swapTemplate.cpp
template <class T>
/* Restrictions on T: 1. must be capable of being an l-value 2. must copy construct properly 3. must assign properly */ void mySwap(T& a1,T& a2) { T temp = a1; a1 = a2;
a2 = temp; }
swapTemplate.cpp
int i=3,j=5; const int ONE =1; const int TWO = 2; char a='c',b='\n'; double pi=3.14159,x=1/3.; char *s1="Hello ", *s2="world!"; mySwap(i,j); // legal, integer version mySwap(pi,x); // legal, double version mySwap(a,b); // legal, character version mySwap(i,a); // illegal, no mixed version, can't convert references mySwap(s1,s2); // legal, only the pointers are swapped mySwap(ONE, TWO); // illegal, 1 & 2 can't be l-values
// //
If we really want to swap strings (instead of their pointers) we can write a special overload ( an ad hoc polymorphism) on top of the template overload (a parametric polymorphism ).
stringSwap.cpp
int mySwap(char* s1,char* s2) { // special string swap int l1=strlen(s1),l2=strlen(s2); l1 = (l1>l2)?l1:l2; // l1 is length of longest char* temp = new char[l1+1]; strcpy(temp,s1); // Now swap the actual strings strcpy(s1,s2); // Note, fails badly if location strcpy(s2,temp); // holding shorter string can't delete[] temp; // hold the longer one!!!! return 0; }
Given both kinds of polymorphism, the compiler looks first for a match to the ad hoc(special) definitions. i.e. special definition over-rides the template definition.
Template Arguments
Here's the lazy person's way to be able to equate arrays. You normally can't equate arrays in C++ without writing special code (mainly because the compiler can not be assured the arrays are the same mSize) However structure variables and objects can be equated and the privilege extends to an embedded array. Cute, eh! (Of course, the embedded arrays are always the same mSize because you have to declare the mSize of embedded arrays, so the compiler is happy).
arrayTemplate.cpp
/* Here we also get type checking over arrays of different types. */ #include <iostream> using namespace std; template <int n, class T> struct Array{ T a[n]; }; int main(){ Array<10,double> x,y; // x & y are arrays of 10 doubles for (int i=0;i<10;i++) x.a[i] = 1./(i+1); y = x; // Type safe. Compiler will flag problems for (int i=0;i<10;i++) cout << x.a[i] <<", "<< y.a[i] << "\n"; return 0; }
The declaration looks a little strange. The angle brackets flag the compiler which kind of object to create. This is because the normal declaration Array x; gives no clue as to whether x is an array of integers, doubles, etc.
stackTemplate.cpp
template <class type> class Stack{ public: Stack(int s=10); // Constructor & null constructor Stack(const Stack &s); // Copy constructor bool push(type d); type pop(); bool empty() const; ~Stack(); private:
Notice, we have only used 1 parameter here, the type. In the declaration, the only things parameterized are the push and pop operations and the declaration of the two pointers, mpTos and mpStart. These are the only things in the class declaration that deal with what is being held in, or moved on or off, the stack. Now we have to implement the class, creating templates for each function affected.
stackTemplate.cpp
template <class type> Stack<type>::Stack(int s){ if ((mSize=s) < 0 || !(mpStart = new type[mSize]) ){ mpStart = 0; mSize = 0; } mpTos = mpStart; } template <class type> Stack<type>::Stack(const Stack<type> &s){ if (mpStart=new type[s.mSize]) { mSize = s.mSize; int offset = s.mpTos-s.mpStart; for (int i=0;i<offset;i++) *(mpStart+i)=*(s.mpStart+i); mpTos = mpStart + offset; } else mSize = 0; } template <class type> bool Stack<type>::push(type d){ if (mpTos==mpStart+mSize) return false; // stack full *mpTos++=d; return true; } template <class type> type Stack<type>::pop(){ if (mpTos>mpStart)mpTos--; // Check something in stack return *mpTos; }
In definitions external to the class, Stack<type> always replaces Stack. Even the destructor and the empty function have to have templates created for them even though neither function has anything to do with what is on the stack.
stackTemplate.cpp
template <class type> bool Stack<type>::empty() const{return (mpTos==mpStart);} template <class type> Stack<type>::~Stack(){delete[]mpStart;}
The reason is that a stack of doubles is a different class than a stack of ints and so each will have its own destructor and empty function. Note that this parameterized stack handles ADTs as well as built in types. We'll define a struct complex after the template has been created.
stackTemplate.cpp
struct complex{ double re,im; complex(double r=0.,double i=0.){re=r;im=i;} }; ostream& operator<<(ostream& s, complex c);
stackTemplate.cpp
int main(){ Stack<double> ds(5); Stack <int> is(4); Stack <complex> cs; int x = 0, i = 0, j = 0;
for(x=0; ds.push(x); x++) //Push until full ; for(i=0; is.push(i);i++) ; for (j=0; cs.push(complex(j,-j));j++) ; // Now empty each stack, outputting as we go cout <<"There were "<<x<<" items on the double stack\n"; while (!ds.empty()) cout << ds.pop() << " "; cout <<"\n\nThere were " << i << " items on the integer stack.\n"; while (!is.empty()) cout << is.pop() << " "; cout << "\n\nThere were " << j << " items on the complex stack.\n"; complex temp; while (!cs.empty()){ cout << cs.pop() << " } cout << "\n"; return 0; }
";
The compiler was quite happy to have complex declared after class Stack<type>. In essence the template tells the compiler how to build the correct Stack class when it is needed. The class is Stack<type> not Stack. That is Stack<int> is a different class from Stack<complex>.
Virtual Functions
Abstract Classes
Consider our User class with its hierarchy of sub-classes.
../inheritance/userInherit.cpp
// A class of users for network management class User{ public: User(char* n, char* u, char* p); // Accessors MyString name() const; int allocation() const; // Disk space allowed MyString password() const; MyString uname() const; bool confirm(const MyString& uName, const MyString& pass) const; // Mutators bool setPassword(const MyString& p1,const MyString& p2); void setAllocation(int a); protected: MyString mName; // Notice attributes that are OBJECTS! MyString mUname; MyString mPassword; int mAllocation; // Memory allocation, in MBytes };
../inheritance/userInherit.cpp
class Student: public User{ public: Student(char* n, char* id, char* u, char* p); // Accessors MyString id() const; // Mutators void buy(long amount); bool approvePrint(int pages); protected: MyString mId; long mPrintPennies; };
../inheritance/userInherit.cpp
class Faculty: public User{ public: Faculty(char* n, char* u, char* p, char* today); // accessor functions long pages() const; MyString lastCleared() const; // mutator functions bool approvePrint(int pages); // increases page count void clear(char* d); // clears count & notes date protected: long mPages; // Pages used since MyString mCleared; // last date cleared };
In writing code to handle printing we have to know that student accounts must be charged but faculty pages must simply be counted. Actually, we need to do something else for graduate students since they get a certain no. of pages, after which they must pay. It would be nicer if all print requests for users were invoked similarly! First we add a pure, virtual function to User
userVirtual.cpp
// A class of users for network management class User{ public: User(char* n, char* u, char* p); // Accessors MyString name() const; int allocation() const; // Disk space allowed MyString password() const; MyString uname() const; bool confirm(const MyString& uName, const MyString& pass) const; // Mutators bool setPassword(const MyString& p1,const MyString& p2); void setAllocation(int a); virtual bool printRequest(int pages) = 0; protected: MyString mName; // Notice attributes that are OBJECTS! MyString mUname; MyString mPassword; int mAllocation; // Memory allocation, in MBytes };
bool printRequest(int pages)=0
This means that inheritors of class User (e.g. Student) can have the same function and that that will be the one used if a User object is actually a Student The = 0 means it is a pure virtual function (I don't like the notation either). This means it is not implemented in Class User Any class that has at least one pure virtual function is known as an Abstract class. 1. It is not possible to instantiate objects of abstract classes. That is there can be no User objects per se (but there can be Faculty or Student objects). 2. The reason this is so is that all user objects are supposed to be able to handle printRequest() calls, but pure User objects cant because we are not going to provide any code for them
Concrete Classes
Classes which either have no virtual functons or alternatively have available implementations for all virtual functions are called concrete classes. Another way to say this is that concrete classes contain no pure virtual functions . Concrete classes can instantiate objects. We want Student, Faculty and GradStudent to be concrete classes so we will implement the virtual printRequest function for all of them. First here are the new declarations for the three derived classes. The Student class:
userVirtual.cpp
class Student: public User{ public: Student(char* n, char* id, char* u, char* p); // Accessors MyString id() const; // Mutators void buy(long amount); virtual bool printRequest(int pages); protected: MyString mId; long mPrintPennies; };
userVirtual.cpp
class Faculty: public User{ public: Faculty(char* n, char* u, char* p, char* today); // accessor functions long pages() const; MyString lastCleared() const; // mutator functions virtual bool printRequest(int pages); void clear(char* d); // clears count & notes date protected: long mPages; // Pages used since MyString mCleared; // last date cleared };
userVirtual.cpp
// A skeletal grad student class class GradStudent: public Student{ public: GradStudent(char* n, char* id, char* u, char* p, Faculty* pa); Faculty* getAdvisor() const; int freePages() const; // accessors
virtual bool printRequest(int pages); // Mutators void addToFreePages(int pages); protected: Faculty* mpAdvisor; int mFreePages; };
Student print requests are authorized only if there are enough printPennies in their
account. If there are enough, the account is debited by the cost of the job.
userVirtual.cpp
bool Student::printRequest(int pages){ assert(pages >=0); int cost = pages * PAGE_COST; if (cost > mPrintPennies) return false; mPrintPennies -= cost; return true; }
Faculty print requests are authorized as a matter of course but the mPages counter is
incremented to track printer usage.
userVirtual.cpp
bool Faculty::printRequest(int pages){ assert(pages>=0); mPages+=pages; return true; }
GradStudent print requests are more complex since grad students receive a free page
allowance to cover their thesis printing.
userVirtual.cpp
bool GradStudent::printRequest(int pages){ assert(pages >=0); int cost; if (pages <= mFreePages) { mFreePages -= pages; return true; } cost = (pages-mFreePages) * PAGE_COST; if (cost <= mPrintPennies) { mFreePages = 0; mPrintPennies -= cost; return true; } return false; }
Late Binding
One advantage of virtual functions is they don't even have to be known at compile time. The decision as to which version of printRequest(int) to call is made at run time!
void printCall (const User& user, PrintJob job){ if (user.printRequest(job.pages())) job.queueIt(user); else cout << Print job for << user.name() << refused.\n; }
We have promised to pass printCall a User object There are no User objects, per se. There can only be Student objects, Faculty objects or GradStudent objects. If, at run time, I call printCall with a user, it will find the right printRequest. If
This is done automatically, at run time. You can compile the file with printCall in it and then add new subclasses of users later. printCall will handle them properly without recompiling. If printRequest were not a virtual function, then the binding would be done by the linker. That is, when the program is assembled the linker would have to decide which printRequest function should be called inside of printCall. Since the only thing it can use is the declaration, User would have to have its own printRequest function and that is what would always be used regardless of the subtype. What the actual subtype is is only known at run time as printCall could be called many times during a program, with many different kinds of User objects. Late binding allows the decision as to whichprintRequest function is used to be delayed until an actual user object arrives, while the code is running.
flamingos.cpp
class Flamingo{ public: virtual void fly(); bool standOnLawn(); virtual void steal()=0; protected: bool isPink; private: int appendages; }; class Pig : public Flamingo{ public: virtual void steal (); }; class Elephant : public Flamingo{ public: virtual void fly(); bool standOnLawn();
Can implementation code for Elaphants and Pigs tell (i) if an Elephant or Pig is pink? (ii) how many appendages it has? State your reasoning.
Yes,isPink is protected and so available to implementation code of derived classes No because appendages is private and available only to implementation code for Flamingo.
(i) (ii)
Software for Lawn Ornament Hijacking, Inc. includes the following function
hijack.cpp
void jack(Flamingo& flamingo){ if (flamingo.standOnLawn()) flamingo.steal(); }
What version of standOnLawn() and steal() get called when someone tries to jack a pig? An elephant? (State your reasoning).
Pig.steal() & Elephant.steal() because they are virtual. Flamingo.standOnLawn() because it is not.
Virtual Destructors
If you are creating a class that you expect to be subclassed (i.e. that other classes will uses as a base class) You should very carefully consider creating a virtual destructor, even if your class does not itself need a destructor. The following example was found by Song Qing, to whom my thanks.
destructorCorruption.cpp
#include <iostream> class CBase{ public: CBase(){ data = new char[64];
} ~CBase(){ delete [] data; } char *data; }; class CFunction{ public: CFunction(){} ~CFunction(){} }; class CFunctionEx : public CFunction{ public: CFunctionEx(){} ~CFunctionEx(){} private: CBase m_cbase; }; int main(){ CFunction *pCFun = new CFunctionEx; delete pCFun; return 0; }
It looks a little strange since all the code has been inlined (which is often done for examples of particular iussues, like this). Can you figure out why it results in a heap corruption?
When we apply delete to pcFun we are causing the destructor to be invoked. Since the destructor is not virtual, the compiler figures out which destructor should be called. The only thing it has to go by is the declared type ofpcFun, which is a pointer to a CFunction object. So the CFunction destructor gets called and it doesn't know anything about what the CFunctionEx constructor did.
Testing
Testing Implementation Code
Test code is as important as implementation code. Writing it well constitutes a separate skill. Consider everything that could break the original code Special cases are particularly important.
Here is some of the test code used on your recent Container class assignment (adapted slightly for presentation in class). We'll focus on the Queue subclass. queueDemo.cpp
bool testQueue(){ const int SIZE = 5; Queue queue(SIZE); bool passed = !queue.inError(); queue.fetch(); // queue empty, can't fetch passed = queue.inError(); // rotate Queue a bit queue.add(0); queue.add(0); queue.add(0); queue.fetch(); queue.fetch(); queue.fetch(); passed = passed && !queue.inError(); if (passed) { int count = 0; while(queue.getStatus() < FULL) { queue.add(count*count-count); count++; passed = passed && !queue.inError(); } if (passed) { queue.add(100.0); // error passed = passed && queue.inError(); count = 0; while (queue.getStatus() > EMPTY) { passed = passed && (queue.fetch() == count*count - count) && !queue.inError(); count++; } passed = passed && count == SIZE; } } return passed; }
Concepts Reviewed
Constructor Chaining Step into the declaration of the queue object and watch how the Container constructor is utilised. Destructor Chaining Step into the destructor calls that are automatically generated when the testQueue function ends and queue goes out of scope. Note that although there is no destructor for the Queue class per se, Queue objects are also Container objects so the Container destructor is called. Passing Pointers by Reference Because objects of the queue class are circular, advancing either head or tail pointer may require wrapping the address back around. Hence we use an incPtr function. What would happen if we hadn't passed the pointers by reference? Pointer Arithmetic Watch the Expression Engine when the incPtr function does wrap the pointer for a nice example of pointer arithmetic.
Virtual Functions
Consider our User class with its children (sub-classes), Student & Faculty & even its grandchild (sub-sub-class)
class User{ public: User(const Name& n, const char* u, const char* p); // Accessors Name name()const; int allocation()const; // Disk space allowed MyString password()const; MyString uname() const; bool confirm(const MyString& uName, const MyString& pass); // Mutators bool setPassword(const MyString& p1, const MyString& p2); void setAllocation(int a); protected: Name mName; MyString mUname; MyString mPassword; int mAllocation; // Memory allocation, in MBytes }; class Student: public User{ public: enum Term{A,B,ONE,TWO,THREE,FOUR,FIVE,SIX,SEVEN,EIGHT,WORK,GRAD}; Student(const Name& n,const MyString& id,const char* u,const char* p); MyString id() const; Term term() const; void set(Term t); void buy(long amount); bool debit(long amount); // accessors // mutators Print control
protected: // The data MyString mId; Term mTerm; // mTerm is a member VARIABLE of type TERM long mPrintPennies; };
mpbl 12/14/2013
Virtual Functions
class Faculty: public User{ public: enum Discipline{CIVIL,MECHANICAL,NAVAL,ELECTRICAL}; Faculty(const Name& n,const char* u,const char* p,Discipline d,Date today); long pages() const; Date cleared()const; void printed(long p); void clear(Date d); // accessor functions // Mutators Print control // clear page record, record date
protected: long mPages; // Number of pages printed since Date mCleared; // the last date cleared Discipline mDiscipline; }; // A skeletal graduate student class class GradStudent: public Student{ public: GradStudent(const Name& n, const MyString& id, const char* u, const char* p, Faculty* pa); Faculty advisor() const; protected: Faculty* mpAdvisor; }; // accessors
In writing code to handle printing we have to know that student accounts must be charged but faculty pages must simply be counted. Actually, we need to do something else for graduate students since they get a certain no. of pages, after which they must pay. It would be nicer if all print requests for users were invoked similarly!
VIRTUAL - 2
Virtual Functions
bool printRequest(int pages) is declared as a virtual function in class User This means that inheritors of class User (e.g. Student) can have the same function and that that will be the one used if a User object is actually a Student It is also declared as a pure function (I dont like the notation either). This means it is not implemented in Class User Any class that has at least one pure virtual function is known as an Abstract class. 1. It is not possible to instantiate objects of abstract classes. That is there can be no User objects per se (but there can be Faculty or Student objects). The reason this is so is that all user objects are supposed to be able to handle printRequest() calls, but pure User objects cant because we are not going to provide any code for them
2.
VIRTUAL - 3
Virtual Functions
Concrete classes
class Student: public User{ public: enum Term{A,B,ONE,TWO,THREE,FOUR,FIVE,SIX,SEVEN,EIGHT,WORK,GRAD}; Student(const Name& n,const MyString& id,const char* u,const char* p); MyString id() const; Term term() const; // Accessors
void set(Term t); // Mutators void buy(int amount); virtual bool printRequest(int pages); protected: MyString mId; Term mTerm; int mPrintPennies; }; // Now the data // mTerm is a member VARIABLE of type TERM
class Faculty: public User{ public: enum Discipline{CIVIL,MECHANICAL,NAVAL,ELECTRICAL}; Faculty(const Name& n, const char* u, const char* p,Discipline d,Date today); int pages() const; Date cleared()const; // accessors
virtual bool printRequest(int pages); // mutators void clear(Date d); // clear page record, record date protected: long mPages; // Number of pages printed since Date mCleared; // the last date cleared Discipline mDiscipline; };
VIRTUAL - 4
Virtual Functions
// A skeletal graduate student class class GradStudent: public Student{ public: GradStudent(const Name& n, const MyString& id, const char* u, const char* p, Faculty* pa); Faculty advisor() const; int freePages() const; // accessors // Mutators
virtual bool printRequest(int pages); void incFreePages(int pages); protected: Faculty* mpAdvisor; int mFreePages; };
VIRTUAL - 5
Virtual Functions
Late Binding
One advantage of virtual functions is they don't even have to be known at compile time. The decision as to which version of printRequest(int) to call is made at run time!
void printCall (const User& user, PrintJob job){ if (user.printRequest(job.pages())) job.queueIt(user); else cout << Print job for << user.name() << refused.\n; }
We have promised to pass printCall a User There are no users, per se. There can only be Student objects, Faculty objects or GraduateStudent objects. If, at run time, I call printCall with a user, it will find the right printRequest. If User is a Student GraduateStudent Faculty printCall will invoke Student::printRequest(int) GraduateStudent::printRequest(int) Faculty::PrintRequest(int)
This is done automatically, at run time. You can compile the file with printCall in it and then add new subclasses of users later. PrintCall will handle them properly without recompiling
VIRTUAL - 6
Virtual Functions
An Alternative example
class Account { public: double balance() const; virtual void withdraw(double a); void deposit(double a); constructors, etc. protected: double mBalance; } class Chequing:Account{ public: virtual void withdraw(double a); void deposit(double a); constructors, etc. } class Savings:Account{ public: virtual void withdraw(double a); void deposit(double a); constructors, etc. protected: static double rate; }
Now, suppose I declare a function whose formal parameter is a reference to base class
void transaction(Account& pA, const double amount);
Is this legal? Sure it is. By definition, an object of class derived is also an object of the base class. However, consider the following within the body (definition) of transaction
void transaction(Account& a, const double amount) { if (amount < 0) // a withdrawal a.withdraw(-amount); // virtual call. Depends on a at run time else a.deposit(amount); // calls Account::withdraw() }
VIRTUAL - 7
Virtual Functions
Which functions will the following calls find? When will the decision be made?
Savings michaelsAccount; Account marysAccount; Checquing michaelsOther; transaction(michaelsAccount,100.); transaction(michaelsAccount,-50.); transaction(michaelsOther,50.); transaction(marysAccount,200.); transaction(michaelsOther,-20.); transaction(marysAccount,-40.);
All the positive amounts result in deposits (at run time). But at compile time, the compiler will have inserted a call to Account::deposit() into the code because deposit is not a virtual function and the class being input to transaction has been declared to be Account. This is all the compiler wants to know. All the negative amounts result in withdrawals(at run time). But the withdrawal call is to a virtual function so the compiler defers the decision as to which version to call & it is made at run time instead.
transaction(michaelsAccount,-50.); Savings::withdaw() transaction(michaelsOther,-20.); Checquing::withdraw() transaction(marysAccount,-40.); Account::withdraw()
VIRTUAL - 8
Virtual Functions
Virtual Classes
It is often the case that we don't ever want to instantiate an object of a base classfor example banks normally wouldnt allow just an Account to be opened, it must be a Savings or Checquing account.
class Account { public: double balance() const; virtual void withdraw(double a)=0.; void deposit(double a); constructors, etc. protected: double mBalance; } class Chequing:Account{ public: virtual void withdraw(double a); void deposit(double a); constructors, etc. } class Savings:Account{ public: virtual void withdraw(double a); void deposit(double a); constructors, etc. protected: static double rate; }
Now withdraw() is a pure virtual function in Account (I dont like the notation either) It is not implemented in Account Account is an abstract class. (Any class with at least one pure virtual function is an abstract class) It is not possible to instantiate objects of an abstract class (how could you withdraw from an Account object it has no withdraw code).
Although legal, the design is poor. Why do we have deposit in all three classes? If it must be overridden in Savings & Checquing why implement it in Account? Either make it virtual as well or
VIRTUAL - 9
Virtual Functions
class Account { public: double balance() const; virtual void withdraw(double a); void deposit(double a); constructors, etc. protected: double mBalance; } class Chequing:Account{ public: virtual void withdraw(double a); constructors, etc. }
class Savings:Account{ public: virtual void withdraw(double a); constructors, etc. protected: static double rate; }
Here, all deposits are handled the same way, so they are done in Account, while withdraw() is handled depending on the type of account.
VIRTUAL - 10