First | Next | Previous | Last | Glossary | About |
We have almost finished the introductory stage of OOP in C++ and to wrap it up we need to revisit constructors and look at two other topics.
You've been using functions for a while now and you know that we can hand arguments over to functions using one of two mechanisms:
The first case is the case most often used and we tend to make use of the second case only when it is necessary to return values from the called function and the return statement isn't sufficient.
The mechanism for passing arguments is similar in both cases, it's just the "thing" passed that is different. A pass by value results in the called function getting a local copy of the argument. When the function terminates the local copy no longer exists. A pass by reference results in the called function getting a copy of an address, usually the address of the actual argument.
The same options are available when we are using objects as the arguments to functions. A pass by value means that the called function gets a copy of the object, a pass by reference means that the called function gets a reference to the actual object.
If the argument being passed by value is a simple type or structured type then, in most cases, there is no problem. However if the argument is an object then there could be some problems. Our object could be guilty of misbehaviour. Remember that an object contains things other than data, it can contain methods.
Code | Explanation |
class A {public: static int instances; A() { instances++; } ~A(); }; |
Here is a simple class. It has a static data member and each time we construct an instance of A we increment the instance count. |
int A::instances = 0; |
Remember that a static member must be initialised in this way. |
int z(A x); |
A function prototype. The function will be used to demonstrate some problem behaviour with our class. |
int main() { A a; cout << A::instances << endl; cout << z(a) << endl; cout << A::instances << endl; return 1; } |
The first statement in main() constructs an instance of A. This will increment the instance counter and we display that at second statement. At this point instances is 1. Now we call z(a) in the stream statement. Note that we are doing a pass by VALUE. We are handing over a copy the object a to the function z. We return from the function, display instance again and find it is -1! |
int z(A x) { A zb; return x.instances; } |
The argument x is a copy by value, this means that a new object exists that is a copy of the actual parameter. A local instance of A is also constructed which means the instance count is incremented. At the completion of the function zb goes out of scope and is destroyed. A's destructor decrements the instance count. |
Although we have, apparently, only created two objects (a and zb), the instance count indicates that three were destroyed. The problem is due to the mechanism used to pass objects by value.
The truth is that three objects a, x and zb, not two, were created but only two of them, a and zb were created by A's constructor. The third object x was created by a copy constructor which was supplied by default by the compiler.
The default copy constructor does a byte for byte copy of the object it is copying. As you saw, with this example, this is sometimes not the best way to get a copy of an object.
class A {public: static int instances; A() { instances++; } A(A &) { instances++; } ~A() { instances--; } };
The solution is to define a copy constructor and we do this by declaring that the class uses a constructor with a reference argument.
You can see in this example that the second constructor contains a reference to it's own class. Now whenever we do a copy it will ensure that the proper initialisation happens.
Most of the problems we might strike when using copies of objects occur because of the default behaviour of the copy constructor.
It's worth summarising a few issues concerning function arguments which are implicit in the things we have been doing but which having been spelled out clearly.
Efficiency and function arguments. In some cases a pass by reference is more efficient than a pass by value.
Data type | size |
char | 1 |
bool | 1 |
int | 4 |
float | 4 |
double | 8 |
largeStruct | 400 |
char * | 4 |
bool * | 4 |
int * | 4 |
float * | 4 |
double * | 4 |
largeStruct * | 4 |
largeStruct is declared as follows:
typedef struct largeStruct { int x[100]; };
Imagine we have the following function prototypes, each one is designed to return the appropriate data type either via a return instruction or via a reference argument:
|
|
You can see from the table that an address is 4 bytes and that, with the exception of char and bool, types is smaller than or least no bigger than the relevant data type. In the case of the functions involving simple data types the overhead in terms of machine instructions is typically one additional instruction for the functions using references. However the memory requirement for these functions is always the same. It will vary with the value functions according to the data type of the arguments and the return value. A significant difference though is seen with the functions using the struct. A pass by value will involve copying a relatively large amount of data but a pass by reference still only involves an address, irrespective of the size of the base data type.
class A {public: static int instances; A() { instances++; } ~A(); };
1.1 You have a program which uses the class shown here.
Given that object fred exists, what is wrong with the statement:
A bert = fred;
How you will fix the problem?
The term overloading refers to the notion of a symbol, especially an operator, having several meanings. If you have ever programmed in Borland Pascal you might remember that the + operator can be used to do arithmetic in the conventional sense but it can also be used to concatenate strings. Here + is operating on two distinctive data types.
The same occurs in C and C++ but is not so obvious. You know that the + operator can be used to add integers and floats. We use one operator to represent the process of addition for different data types.
#include <iostream> #include <string.h> #define STRLEN 256 class dbStr {public: dbStr(char *); //Constructor void operator +(char *); char * getStr(); private: char str[STRLEN]; }; dbStr::dbStr(char * astr) { strcpy(str, astr); } void dbStr::operator +(char * astr) { strcat(str, astr); } char * dbStr::getStr() { return str; } void main(void) {dbStr fname("David"); cout << fname.getStr() << " "; fname + " does C++ "; cout << fname.getStr() << endl; } |
C++ extends operator overloading enormously by providing the means for defining how an operator will function for a given class. This is a very useful and very powerful feature of C++.
Here is an example.
We have a class called dbStr. It has a constructor which accepts a pointer to a char, a method (getStr()) which returns a pointer to a char, a private str member, a char array and something new - the statement void operator +(char *). This is a member function which defines the meaning of addition for our dbStr class.
The member function takes a char * argument and uses the C library strcat function to concatenate the argument to the dbStr str member.
NOTE: The dbStr class uses functions from the C string library. You will probably want to read about in the The GNU C Library - String and Array Utilities.
We use the operator+ function like this:
fname + " does C++ ".
It looks odd but keep in mind it is a member function call; fname refers to
the object, + is the member function and " does C++ " is the argument.
class dbStr {public: dbStr(char *); void operator + (dbStr &); void operator += (dbStr &); int getLen(); char * getStr(); private: char str[STRLEN]; }; void dbStr::operator +(dbStr & astr) { char * x; int len; len = strlen(str); x = astr.getStr(); strncat(str, x, STRLEN - len); } void dbStr::operator +=(dbStr & astr) { char * x; x = astr.getStr(); strcat(str, x); }
The next example develops Example 1 by adding the operator += and making some changes to the + operator.
You can see the complete progam here.
As it stands the program isn't very robust. The main() function contains this for loop:
for (int i = 0; i < 3; i++) { fname += surname; cout << fname.getStr() << endl; }
As it is it will function OK but if you modify the for loop so that it does 10 iterations the program will crash. Likewise if you do a + or += operation with a string larger than STRLEN characters the program will crash. Why is that?
The reason is that all to soon we overrun the memory available for the data member. We have set a limit of 256 characters for the data member str and any operation that exceeds this will bring the program tumbling down. That's a cue for some tutorial work.
2.1 Get a copy of Example 2 and see if you can modify it so that it doesn't crash whenever the STRLEN value is exceeded. You will need to fix anything that adjusts the length of str.
2.2 Add a find() method to the dbStr class.
2.3 Add an extract method which uses your find() method. The extract() method should copy a substring from a string and create a new object from the substring.
2.4 Add a delete method to delete a substring from a string.
2.5 Add a replace method to replace a substring in a string.
NOTE: You will probably want to read about in the various C string functions The GNU C Library - String and Array Utilities.
First | Next | Previous | Last | Glossary | About |
Copyright © 1999 - 2001
David Beech