Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Interfacing with C plus plus-programing communication with microcontrolers (K. Bentley, 2006)

.pdf
Скачиваний:
192
Добавлен:
12.08.2013
Размер:
3.18 Mб
Скачать

12 DATA ACQUISITION WITH OPERATOR OVERLOADING 367

When FindSum() is called it receives a copy of Sum and a copy of n. The copy of Sum is changed as expected inside this function. When the function exits, this copy is discarded and as a result the sum of ten integers evaluated within the function is also discarded. The outcome is that the variable Sum declared within the main() function remains unchanged (i.e. it still has the value 0).

This manner of passing parameters is known as pass by value, which is actually ‘pass by a copy’. The two disadvantages when passing parameters by value are; i) the time taken, and ii) memory space needed to make a copy. If the passed parameter is an object that occupies a large portion of memory, an equal amount of extra memory space will be needed to make the copy, and this will take time.

12.2.2 Passing Parameters to a Function by Reference

A different way of passing parameters to a function is by reference. Passing parameters by reference allows a function to effect changes to a variable being used in the calling environment. The program segment given in Section 12.2.1 has been reproduced below with an apparently minor change. In this modified example, when the function FindSum() changes the value of sum, it actually changes the variable Sum that was declared within the main() function – not a copy of it. The function directly uses the variable in the calling environment (to generate a correct result) rather than working with a copy of it.

#include <iostream.h>

void FindSum(int& sum, int n) // Function heading changed

{

for(int j = 0; j < n; j++) sum = sum + j;

}

void main()

{

int Sum = 0; int n = 10;

FindSum(Sum,n);

cout << "The sum of " << n << " integers is " << Sum << endl;

}

This program will print the following line on the screen:

The sum of 10 integers is 45

The change in the program is shown in bold typeface. Instead of declaring the first parameter sum as an int, it is now declared as reference to int by changing int

368 12 DATA ACQUISITION WITH OPERATOR OVERLOADING

to int&. Therefore, when the function is called, no copy is made, and the function carries out changes to the variable in the calling environment, i.e. the variable Sum declared within the main() function.

Passing parameters by reference is memory efficient and time efficient (no need to make a copy). It also allows the function to deliver a result through reference parameters and also through return values. The disadvantage is that the passed parameters are vulnerable to inadvertent changes carried out by the function.

Use of const with reference parameters

The keyword const can be added in the parameter declaration to prevent the function from making changes to the reference variable. The keyword const can also be added to parameters passed by value. In either case, statements within the body of the function are not allowed to change the value of the parameter.

Note that in the previous example we cannot use the function heading: void FindSum(const int& sum, int n)

for the simple reason that we want to change the value of sum to be able to obtain the correct result.

12.2.3 Preferred Ways of Passing Parameters

Passing parameters by value has the advantage of safeguarding the original values of the actual arguments in the calling environment. However, making a copy consumes time and memory. A more serious subtlety associated with pass by value is related to objects in a class hierarchy. This subtlety is demonstrated using the following example.

Consider the simple class hierarchy and the program shown in Listing 12-1.

Listing 12-1 Adverse effects of passing parameters by value.

//This program produces WRONG results! #include <iostream.h>

class Base

{

private:

int BaseClassData;

public:

Base(int baseclassdata)

{

BaseClassData = baseclassdata;

}

12 DATA ACQUISITION WITH OPERATOR OVERLOADING 369

virtual int GetClassData() const // Constant function.

{

return BaseClassData;

}

};

class Derived : public Base

{

private:

int DerivedClassData;

public:

Derived(int derivedclassdata,

int baseclassdata): Base(baseclassdata)

{

DerivedClassData = derivedclassdata;

}

int GetClassData() const // Constant function.

{

return DerivedClassData;

}

};

int GetData(const Base baseObject)//Pass by value

{

return baseObject.GetClassData();

}

void main()

{

Base* BasePtr; int ClassData;

BasePtr = new Base(100); ClassData = GetData(*BasePtr);

cout << "Base class data " << ClassData << endl; delete BasePtr;

BasePtr = new Derived(200, 100); ClassData = GetData(*BasePtr);

cout << "Derived class data " << ClassData << endl; delete BasePtr;

}

370 12 DATA ACQUISITION WITH OPERATOR OVERLOADING

Note that for both GetClassData() functions in program Listing 12-1, the keyword const is added at the end of the function heading as shown below:

int GetClassData() const

{

return DerivedClassData;

}

Such functions are named constant functions. These functions are not allowed to modify any of the data members of their class.

Now consider the function GetData():

int GetData(const Base baseObject) //Pass by value

{

return baseObject.GetClassData();

}

The parameter baseObject passed to the non-member function GetData(), is passed by value as a const object. As such, the object passed must not be changed by any statements within the body of the GetData() function. Therefore, the statement baseObject.GetClassData() must not make any changes to baseObject. This is ensured since the GetClassData() function has been specified as a constant function. In the next program, when we pass parameters by reference, we will pass them as const objects to prevent the function from changing them. This allows us to keep both programs as similar as possible and to focus on the behaviour of the two programs in terms of pass by reference and pass by value.

The GetData() function is intended to extract the value of the data member that belongs to a particular class. The value of data member BaseClassData will be returned if baseObject is of type Base, and the value of data member

DerivedClassData is returned if baseObject is of type Derived.

In this program we call the GetData() function under two different circumstances. Consider the first case:

Base* BasePtr = new Base(100);

int ClassData = GetData(*BasePtr);

We would expect the function GetData()to call, from within its body, the member function GetClassData() belonging to the Base class. This should and does retrieve the value of member data BaseClassData and assign its value of 100 to variable ClassData. Now consider the second case:

Base* BasePtr = new Derived(200, 100); int ClassData = GetData(*BasePtr);

Once again, we would expect the GetData() function to call, from within its body, the member function GetClassData() belonging to the Derived class.

12 DATA ACQUISITION WITH OPERATOR OVERLOADING 371

This should set the value of ClassData to 200. However, this will not happen in this case. Instead, the program produces an unexpected (error) result by setting ClassData to 100. Note: the parameter is passed by de-referencing a pointer. Recall that base class pointers can point to derived class objects. Had a pointer not been used, we could not pass an object of type Derived as an actual argument for a parameter of type Base. If we simply attempted to pass a derived class object to take the place of a base class parameter, the compiler would report a type mismatch error.

In the second case described above, since the function GetData() is programmed to receive its parameter by value, the function will be compiled to get a copy of a Base class object rather than a copy of a Derived class object. Thus, the entire derived class object is not visible to the GetData() function – only the base class portion (inherited by derivation) is visible. This is a typical situation where the object type of the parameter is different from the object type of the actual argument. Note that the compiler cannot detect this situation since it occurs at run-time. The program in Listing 12-1 demonstrates this faulty behaviour producing the following result when it executes:

Base class data 100

Derived class data 100

The data from the Derived class is certainly not 100. The program should have stored and then retrieved the data as 200. This problem can be rectified by passing the baseObject parameter by reference to the GetData() function. If passed by reference, no copy of a base class object will be made. The entire Derived class object will be accessible to the function GetData(), and the correct result of 200 will be produced. The corrected program is shown in Listing 12-2.

Listing 12-2 Corrected version of Listing 12-1.

//This program produces correct results. #include <iostream.h>

class Base

{

private:

int BaseClassData; public:

Base(int baseclassdata)

{

BaseClassData = baseclassdata;

}

virtual int GetClassData() const

{

372 12 DATA ACQUISITION WITH OPERATOR OVERLOADING

return BaseClassData;

}

};

class Derived : public Base

{

private:

int DerivedClassData;

public:

Derived(int derivedclassdata,

int baseclassdata): Base(baseclassdata)

{

DerivedClassData = derivedclassdata;

}

int GetClassData() const

{

return DerivedClassData;

}

};

int GetData(const Base& baseObject) //Pass by reference

{

return baseObject.GetClassData();

}

void main()

{

Base* BasePtr; int ClassData;

BasePtr = new Base(100); ClassData = GetData(*BasePtr);

cout << "Base class data " << ClassData << endl; BasePtr = new Derived(200, 100);

ClassData = GetData(*BasePtr);

cout << "Derived class data " << ClassData << endl;

}

You will see the following result when this program executes:

Base class data 100 Derived class data 200

12 DATA ACQUISITION WITH OPERATOR OVERLOADING 373

There is a lesson to be learned from this exercise - whenever class objects are passed to a function it is prudent to pass them by reference. As shown in Listing 12-2, the keyword const can be prefixed to the parameter passed by reference to protect the parameter from any inadvertent changes within the function.

12.2.4 The Copy Constructor

In previous chapters we used default constructors and standard constructors to instantiate objects. The copy constructor is a special constructor that is called when a copy of an object is created. If the developer does not provide a copy constructor, the compiler will generate one by default. This constructor makes a copy of an object by copying member-by-member from one object to the other for three situations:

(i)When passing parameters to a function by value, a copy of the object must be created.

(ii)When parameters are returned by value, a copy of the object to be returned must be made. Again, the same copy constructor will be called to make the copy.

(iii)If an object is declared and initialised using another object passed as a parameter, the copy constructor must be called.

The assignment operator can also be used to initialise an object that was created previously using the default constructor. In principle, the assignment operator (=) must carry out the same actions as the copy constructor. If the developer does not overload the assignment operator the compiler will do so to suit the class. We will defer discussing overloading the assignment operator until operator overloading concepts have been described.

A few examples of object instantiation using various constructors are:

DCMotor Motor1;

// default

constructor used

DCMotor Motor2(Motor1);

// copy constructor used

DCMotor Motor3;

//

default

constructor

used

Motor3 = Motor1;

//

assignment operator

used

We will develop an example program that operates with arrays using the IntArray object to improve your understanding of the copy constructor. Consider the definition of the IntArray class given in Listing 12-3.

Listing 12-3 Header file intarray.h shows a class definition for an array of integers.

#ifndef IntarrayH #define IntarrayH

class IntArray

{

private:

374 12 DATA ACQUISITION WITH OPERATOR OVERLOADING

int NumInts;

int* ArrayPointer;

public:

// Default constructor

IntArray();

IntArray(int numints);

// Constructor

~IntArray();

// Destructor

void EnterArray();

// Other member functions

void PrintArray();

 

};

 

#endif

 

The IntArray class will instantiate an array of integers having a specified number of elements. The data member NumInts will store the total number of elements in the array and the pointer ArrayPointer will point to the dynamically allocated portion of memory containing the array of integers. The destructor ~IntArray() will release the dynamically allocated memory. The function EnterArray() will prompt the user for array values, receive user input via the keyboard, and fill the array. The final function PrintArray() is used to print the contents of the array on the screen.

The IntArray class’s constructor and its default constructor initialise the member data NumInts and ArrayPointer. If a parameter is passed to the constructor, memory for the array will be dynamically allocated as shown in the following constructor definitions:

IntArray::IntArray() // Default constructor

{

NumInts = 0; ArrayPointer = NULL;

}

 

IntArray::IntArray(int numints)

// Constructor

{

 

if(numints <=0)

 

{

 

NumInts = 0;

 

ArrayPointer = NULL;

 

}

 

else

 

{

 

NumInts = numints;

ArrayPointer = new int[NumInts];

}

}

12 DATA ACQUISITION WITH OPERATOR OVERLOADING 375

Note that if the value of numints is 0 or negative, NumInts is initialised to 0 and ArrayPointer is initialised to the predefined constant NULL to indicate that the pointer is not pointing anywhere.

Suppose we use the object class IntArray in a main() function to instantiate two IntArray objects named A and B. We will then pass the objects A and B by value to a function named AddArrays() to add the IntArray object A to the IntArray object B as shown in the fragment of code below:

void AddArrays(IntArray a, IntArray b) // Pass by value

{

// print the result of summation on-screen

}

void main()

{

IntArray A(5); IntArray B(5);

AddArrays(A,B);

.

.

.

}

Object A

 

Object B

NumInts

 

NumInts

ArrayPointer

 

 

 

 

 

 

 

ArrayPointer

 

 

 

 

 

Copy of A

 

 

 

 

 

Copy of B

 

 

 

 

 

 

 

 

 

NumInts

 

 

 

 

 

NumInts

 

 

 

 

 

ArrayPointer

 

 

 

 

 

ArrayPointer

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Memory dynamically allocated by the constructors

Figure 12-1 Objects A and B copied by the compiler-generated copy constructor.

When the actual arguments A and B are passed by value to the function AddArrays(), copies of A and B must be made. The compiler-generated copy

376 12 DATA ACQUISITION WITH OPERATOR OVERLOADING

constructor will be called to make these copies; this copy-making process is shown graphically in Figure 12-1.

The compiler-generated copy constructor has created a copy of each data member of the original objects. However, it did not make copies of the dynamically allocated memory. As a result, the pointer to the original object and the pointer to the copied object point to the same portion of memory.

The destructor ~IntArray() is called after the function AddArrays() executes. The destructor will free the memory used for the temporary copies of A and B together with the original data objects that were dynamically allocated. This outcome is shown in Figure 12-2.

Object A

 

 

Object B

 

 

NumInts

 

 

NumInts

 

 

ArrayPointer

 

?

ArrayPointer

 

?

 

 

Figure 12-2 Result of discarding the copies of A and B.

Object A

 

Object B

 

NumInts

 

 

 

 

 

NumInts

 

 

 

 

 

 

 

ArrayPointer

 

 

 

 

 

 

ArrayPointer

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Memory dynamically allocated

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Copy of A

 

 

by the constructors

 

 

 

 

 

 

Copy of B

 

 

 

 

NumInts

 

 

 

 

 

NumInts

 

 

 

 

 

 

ArrayPointer

 

 

 

 

 

 

ArrayPointer

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Memory dynamically allocated

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

by the copy constructors

 

 

 

 

 

 

 

 

 

 

Figure 12-3 Copies of objects A and B made by a user written copy constructor.

Now the main() function has lost the original data it had created at the time of instantiating the objects A and B. We can overcome the problem created by the compiler-generated copy constructor if we write our own copy constructor that not