Файл: Interfacing with C plus plus-programing communication with microcontrolers (K. Bentley, 2006).pdf

ВУЗ: Не указан

Категория: Не указан

Дисциплина: Не указана

Добавлен: 14.06.2025

Просмотров: 3591

Скачиваний: 1

ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.

12.1 Introduction

Some of the programs written in this book can be improved to require less memory when executing, and also operate faster by using pass by reference and return by reference mechanisms. They can also be changed to take advantage of simpler statements by using operator overloading and gain access to private member data through friend functions.

In this chapter, we will develop a data acquisition program to demonstrate how operator overloading can be used to write elegant programs that have the advantages outlined above. During data acquisition, signals are converted to data using a device such as an analog-to-digital converter. The data is then directly processed or written to a data file on a mass storage device such as a hard disk, or in some cases, sent to a standard output device such as a screen or printer.

12.2 Operator Overloading

When an operator is overloaded, the action carried out by the operator depends on the arguments the operator is associated with. For example, the results will be different if the division operator ( / ) is used in the following two contexts. One operation produces integer division and the other floating-point division:

5/2; // the result is 2 5.0/2.0; // the result is 2.5

Similarly, the double left arrow operator behaves in two different ways in the following two cases:

int y =

200;

output.

cout

<<

y; // 200 is sent to the standard

y <<

1;

// Shifts bits of y to left by

1 bit-position.

The action of an operator depends on the type of object it is used with. In the expression cout << y, cout is a class object of type ostream and y is an object of type int. The << operator takes appropriate action to print the value of y on the screen. However, both operands are of type integer in y << 1, and the action taken by the operator is to shift bits of y by 1 bit-position to the left.

The operators shown in Table 12-1 cannot be overloaded.

Table 12-1 Operators that cannot be overloaded.

. ?: :: .* sizeof

12 DATA ACQUISITION WITH OPERATOR OVERLOADING 365

We will demonstrate operator overloading by developing program segments that overload the double right arrow (>>) and the double left arrow (<<) operators to perform the following tasks:

1.Carry out an analog-to-digital conversion using an Adc object of type ADC, and store the result in the variable named value of type unsigned char. We want to be able to use a statement of the following form to accomplish this.

Adc >> value;

2.Carry out an analog-to-digital conversion and send the data directly to the standard output device (computer screen) using a statement of the following form. The object cout is of type ostream and Adc is again an object of type ADC.

cout << Adc;

Using a statement like this would simplify programming of a data acquisition system with an analog-to-digital converter where the results are to be viewed onscreen or stored in a file.

Operators can be overloaded in two different ways by writing a function using syntax that is specific to operator overloading:

1.As a member function of a class.

2.As a non-member function.

These two ways of overloading an operator will be discussed in the sections ahead. Operators can also operate as unary operators (such as ++ in the case of ++i) or as binary operators (such as + in the expression x+y). The unary operators shown in Table 12-2 operate on an object of type ObjectX. The binary operators operate on two objects; one of type ObjectX, and the other of type ObjectY. In this example the operator being overloaded is the @ symbol.

Table 12-2 Function headings for operator overloading.

Unary operator as a member function

ObjectX::operator@()

Unary operator as a non-member function

operator@(ObjectX x)

Binary operator as a member function

ObjectX::operator@(ObjectY y)

Binary operator as a non-member function

operator@(ObjectX x, ObjectY y)

The operators overloaded as shown above are used as follows. In the case of a unary operator, the operand must be to the right of the operator. For example, if x is an object of type ObjectX, the usage is:

@x;


366 12 DATA ACQUISITION WITH OPERATOR OVERLOADING

In the case of a binary operator, the first operand must be to the left of the operator and the second operand must be to the right of the operator. If x is an object of type ObjectX and y is an object of type ObjectY, then the usage is:

x @ y;

The syntax used with the operator is the same if the operator is overloaded as a member function or a non-member function. C++ concepts such as pass by value, pass by reference and copying objects with the copy constructor need to be understood before being able to understand how operators can be overloaded. These concepts are explained in the sections ahead.

12.2.1 Passing Parameters to a Function by Value

Our previous programs have often employed functions that used parameters. At the time of calling the function, these parameters are replaced by copies of the actual arguments (the real values used in the calling function). These copies of the arguments passed to the function are created as temporary values, used by the function, and destroyed when the function exits. As a result, the actual argument used when calling the function (in the calling environment) will not be affected by any changes the function makes to its copy.

The passing of parameters to functions can be better understood by considering the following example that attempts to add up n integers that start from the number 0:

#include <iostream.h>

// NOTE: the result from this function cannot be used! void FindSum(int sum, int n)

{

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

}

void main()

{

int Sum = 0; int n = 10;

FindSum(Sum,n); // FindSum() is called here

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

}

This program will print the following text on the screen:

The sum of 10 integers is 0


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