6.1 Operator Overloading:
Operator overloading is another example of C ++
polymorphism. We know that C++ enables you to define several functions having
the same name as long as they have different signatures (argument lists). That
was function overloading, or functional polymorphism. Its purpose is to let you
use the same function name for the same basic operation even though you apply
the operation to different data type.
Operator overloading
extends the overloading concept to operators, letting you assign multiple
meanings to C++ operators. Actually many C++ (and C) operators already are
overloaded. For example, the * operator, when applied to an address, yields the
value stored at that address. But applying * to two numbers yields the product
of the values. C++ uses the number and type of operands to decide which action
to taken.
C++
lets you extend operator overloading to user-defined types, permitting you,
say, to use the + symbol to add two objects. Again, the compiler will use the
number and type of operands to determine which definition of addition to use.
Overloaded operators often can make code look more natural.
For instance C++
permits us to add two variables of user-defined types with the same syntax that
is applied to the basic types. This means that C++ has the ability to provide
the operators with a special meaning for a data type. The mechanism of giving
such special meanings to an operator is known as operator overloading.
Operator
overloading provides a flexible option for the creation of new definitions for
most of the C++ operators. We can almost create a new language of our own by
the creative use of the function and operator overloading techniques. We can
overload (give additional meaning to) all the C++ operators except the
following:
·
Class member access operators ( ., .*).
·
Scope resolution operator (::).
·
Size operator (sizeof)
·
Conditional operator (?:).
The excluded
operators are very few when compared to the large number of operators, which
qualify for the operator overloading definition.
Although the semantics of an operator can be
extended, we cannot change its syntax, the
grammatical rules that govern its use such as the number of operands,
precedence and associativity. For example, the multiplication operator will
enjoy higher precedence than the addition operator. Remember, when an operator
is overloaded, its original meaning is not lost.
6.2 Defining Operator Overloading:
To define an additional task to
an operator, we must specify what it means in relation to the class to which
the operator is applied. This is done with the help of a special function,
called operator function, which
describes the task. The general form of an operator function is:
returntype classname :: operator op ( arg-Iist )
{
Function
body //task defined
}
where returntype is the type of value returned
by the specified operation and op is
the operator being overloaded. The op is
preceded by the keyword operator.
operator op is the function name.
Functions must be
either member functions or friend functions. A basic difference between them is
that friend function will have only one argument for unary operators
and two
binary operators, while member function has no arguments for unary operators
and only one for binary operators. This is because the object used to invoke
the member function is passed implicitly and therefore is available for the
member function. This is not the case with friend functions. Arguments may be
passed either by value or by reference.
Operator functions
are declared in the class using prototypes as follows:
vector
operator + (vector); //vector
addition
vector
operator -( ); //unary
minus
friend
vector operator + (vector, vector); //vector
addition
friend
vector operator -(vector); //unary
minus
vector
operator -(vector & a); //subtraction
int
operator == (vector); //comparison
friend
int operator == (vector, vector) //comparison
vector is a data type
of class and may represent both magnitude and direction (as in physics and
engineering) or a series of points called elements (as in mathematics).
The process of
overloading involves the following steps:
- First, create a class that defines the data type that is to be used in the overloading operation.
- Declare the operator function operator op ( ) in the public part of the class. It may be either a member function or a friend function.
- Define the operator function to implement the required operations.
Overloaded operator
functions can be invoked by expressions such as
op x or x op
for unary operators
and
x op y
for binary operators.
op x (OF x op) would
be interpreted as operator op (x)
In case of member
functions,
operator op (x, y)
Consider the
program below which overloads unary operators:
#
include<iostream.h>
class Counter {
private:
unsigned
int count;
public:
Counter( ) {
count = 0; }
int get-count( )
{ return count; }
void operator ++
( ) { count++; }
};
void main( )
{
Counter c1, c2;
cout <<
"\n c1=" << c1.get-count( );
cout <<
" \n c2=" << c2 .get-count ( ) ;
c1+t;
c2++;
++c2;
cout
<<"\n c1="<<c1.get-count( );
cout <<
"\n c2="<< c2.get-count( );
}
In this program
two object of class Counter are
created. The counts in the objects are initially 0. Using the overloaded ++
operator, we increment c1 and c2 twice.
6.3 operator Keyword:
The keyword operator is used to overload the ++
operator in this declaration:
void
operator ++ ( )
The return type
(void in this case) comes first, followed by the keyword operator, followed by the operator itself ( ++ ), and finally the
argument list enclosed in parentheses (which are empty here). This declaration
syntax tells the compiler to call this member function whenever the ++ operator
is encountered, provided the operand (the variable operated on by the ++) is of
type Counter. The only way the
compiler can distinguish between overloaded functions is by looking at the data
type of their arguments. In the same way, it can distinguish between overloaded
operators by looking at the data type of their operands. If the operand is a
basic type like an int, as
intvar ++;
then the
compiler will use its built-in routine to increment an int. But if the operand is a Counter
variable, then the compile will know to use our user-written operator++().
6.4 Operator Return Values:
The operator++ ( ) function has a subtle defect. You will discover it
if you use a statement like this in main
( ) :
c1 = c2++;
The compiler
will give an error. Because we have defined the ++ operator to have a return
type of void in the operator++ ( ) function, while in the
assignment statement it is being asked to return a variable of type Counter. That is, the compiler is being
asked to return whatever value c2
has after being operated on by the ++ operator, and assign this value to c1. So we can't use ++ to increment Counter objects in expression; it must
always stand alone with its operand. To make it possible to use our user
defined operator+ + ( ) in
expressions, we must provide a way for it to return a value. The program below
does this technique.
# include
<iostream.h>
class Counter {
private:
unsigned
int count;
public:
Counter( )
{ count = 0; }
int
get-count( ) { return count; }
Counter
operator ++ ( )
{
count++;
Counter temp;
temp.count
= count;
return
temp;
}
};
void main( ) {
Counter c1, c2;
cout <<
"\n c1=" << c1.get_count( );
cout <<
"\n c2=" << c2.get_count( );
c1++;
c2 = c1++;
cout <<
"\n c1=" << c1.get_count( );
cout <<
"\n c2=" << c2++.get-count( );
}
In this program
the operator++ ( ) function creates
a new object of type Counter, called
temp to use as a return value. It
increments the count data in its own object as before, then creates the new, temp object and assigns count in the new object the same value
as in its own object. Finally it returns the temp object. This has the desired effect. Expressions like
c1++
now returns a
value, so they can be used in other expressions, such as
c2 = c1++;
and
c2++.get_count(
)
In the first of
these statements the value returned from c1
++ will be assigned to c2, and in
the second it will be used to display itself using the get_count( ) member function.
6.5 Multiple Overloading:
We can have same
operator function called for different operand. Consider that you use three
different programs of the + operator to add distances, to add polar
coordinates, and to concatenate strings. You could put all these classes
together in the same program, and C++ would still know how to interpret the +
operator. It selects the correct function to carry out the "addition"
based on the type of operand.
In our first example
we'll overload the less than operator
< in the Distance class, so that we can compare two distances.
# include
<iostream.h>
enum boolean { false,
true };
class Distance
{
private:
int feet;
float
inches;
public:
Distance(
) { feet =
0; inches = 0.0; }
Distance(int
ft, float in) { feet = ft; inches
= in; }
void
getdist( )
{
cout
<< "\nEnter feet: "; cin
>> feet;
cout
<< "Enter inches: "; cin
>> inches;
}
void
showdist( )
{
cout
<< feet << "\,-" << inches << '\"';
}
boolean
operator < (Distance);
};
boolean
Distance::operator < (Distance d2)
{
float bf1 = feet + inches / 12;
float bf2 = d2.feet + d2.inches /12;
return (bf1 < bf2) ? true: false;
}
void main( )
{
Distance
dist1;
dist1.getdist(
);
Distance
dist2(6, 2.5);
cout
<< "\ndist1 = "; dist1.showdist( );
cout
<< "\ndist2 = "; dist2.showdist(
);
if( dist1
< dist2 )
cout << "\ndist1 is less
than dist2";
else
cout <<
"\ndist1 is greater than dist2";
}
This program compares
a distance entered by the user with a distance, 6'-2.5", initialized by
the program. Depending on the result, it then prints one of two possible
sentences. Here the operator<( ) function has a return
type of boolean (defined in the e n
u m statement at the beginning of the program). The return value is false
or true, depending on the comparison of the two distances. The comparison is
made by converting both distances to floating-point feet, and comparing them
using the normal < operator. Similarly you can overload other operators.
6.6 Data conversion:
Normally,
when the value of one object is assigned to another, of the same type, the
value, or the member data items are simply copied into the new object. The
compiler, doesn’t need any special instructions to use = for the assignment of
user-defined objects such as Distance
objects. Thus assignment between type, whether, they are basic types or user
defined types, are handled by the compile, with no effort on our part provided
that the same data type is used on both sides of the equals sign. But what happens
when the variables on different sides of the = are of different types? The
answer to this is Data conversion.
Conversions Between Basic Types:
When we write a statement like
intvar =
floatvar;
where intvar is of type int and floatvar is of
type float, we are assuming that the
compiler will call a special routine to convert the value of floatvar, which is expressed in
floating-point format, to an integer format so that it can be assigned to intvar.
There are of course many such conversions: from float to double, char to float,
and so on. Each such conversion has its own routine, built into the compiler
and called up when the data types on different sides of the = sign so dictate.
We say such conversions are implicit, because
they aren't apparent in the listing. Sometimes we want to force the compiler to
convert one type to another. To do this we use the cast operator.
For instance; to
convert float to int we could say
intvar =
int(floatvar);
Casting provides explicit conversion: It's obvious in the
listing that the int( ) conversion
function will convert from float to int. However, such explicit conversions
use the same built-in routines as implicit conversion.
Basic to Class Type: The conversion from basic type to class type is easy to accomplish. It may be recalled that the use of constructors was illustrated in a number of examples to initialize objects. For example, a constructor was used to build a vector object from an int type array. Similarly, we used another constructor to build a string type object from a char* type variable. Consider the following constructor:
string ::
string(char *a)
{
length =
strlen(a);
p = new
char[length+ 1];
strcpy
(P, a);
}
This constructor
builds a string type object from a char * type variable a. The variables length and p are data members of the
class string. Once this constructor
has been defined in the string class,
it can be used for conversion from char *
type to string type. Example:
string
S1, S2;
char *
name1 = "IBM PC";
char *
name2 = "Apple Computers";
S1
=5tring(name1);
S2 =
name2;
The statement
S1 =
Strlng(name1);
first converts name1 from char * type to string type and then assigns the string type values
to the object s1. The statement
S2 = name2;
also does the same
job by invoking the constructor implicitly.
Let us consider
another example of converting an int type
to a class type
class time {
int hrs;
int mins;
public :
……
…….
time(int
t) // constructor
{
hours = t
/ 60; //
t in minutes
mins = t%
60;
}
};
The following
conversion statements can be used in a function:
time = T1
; I lobject T1 created
int
duration = 85;
T1 =
duration; Il int to class type
After this
conversion, the hrs member of TI will contain a value of 1 and mins member a value of 25, denoting 1
hour and 25 minutes.
Note that the
constructors used for the type conversion take a single argument whose type is to be converted. In both the
examples, the left-hand operand of = operator is always a class object.
Therefore, we can also accomplish this conversion using an overloaded =
operator.
Class to Basic Type: The constructors did a fine job
in type conversion from a basic to Class type. What about the conversion from a
class to basic type? The constructor functions do not support this operation.
Luckily, C++ allows us to define an overloaded casting operator that could be used to convert a class type data to
a basic type. The general form of an overloaded casting operator function,
usually referred to as a conversion
function is:
operator typename(
)
{
……………...
(Function statements)
……………...
……………...
}
This function
converts a class type data to typename. For
example, the operator double( ) converts a class object to type double, the
oðerator int( ) converts a class type object to type int, and so on. Consider
the following conversion function:
vector ::
operator double( ) {
double
sum = 0;
for(int i
= 0; i < size; i++)
sum = sum
+ v [i] * v [i];
return
sqrt(sum);
}
This function
converts a vector to the corresponding scalar magnitude. Recall that the
magnitude of a vector is given by the square root of the sum of the squares of
its ãomponents. The operator double( ) can be used as follows:
double
length = double(V1);
or
double
length = V1 ;
where
V1 is an object of type vector. Both the statements have exactly the same
effect. When the compiler encounters a statement that requires the conversion
of a class type to a basic type, it quietly calls the casting operator function
to do the job.
The casting operator
function should satisfy the following conditions:
- It must be a class member.
- It must not specify a return type. .
- It must not have any arguments.
Since
it is a member function, the object invokes it and therefore, the values used
for conversion inside the function belong to the object that invoked the
function. This means that the function does not need an argument.
In the string example
described in the previous section, we can do the conversion from string to char
* as follows:
string ::
operator char*( )
{
return(p);
}
One Class to Another Class Type: We have just seen data conversion techniques from a basic to class type and a class to basic type. But there are situations where we would like to convert one class type data to another class type. Example:
objX =
objY; // objects of
different types
objX is an object of
class X and objY is an object of class Y.
The class Y type data is converted
to the class X type data and the
converted value is assigned to the objX.
Since the conversion takes place from class Y to class X, Y is known
as the source class and X is known as the destination class.
Either a constructor
or a conversion function can carry out such conversions between objects of
different classes. The compiler treats them the same way. Then, how do we
decide which - form to use? It depends upon where we want the type-conversion
function to be located, in the source class or in the destination class. We
know that the casting operator function
operator typename( )
converts the class
object of which it is a member to typename. The typename may be a built-in type or a user-defined one (another class
type). In the case of conversions between objects, typename refers to the destination class. Therefore, when a class
needs to be converted, a casting operator function can be used (i.e. source
class). The conversion takes place on the source class and the result is given
to the destination class object.
Now consider a
single-argument constructor function, which serves as an instruction for
converting the argument's type to the
class type of which it is a member. This implies that the argument
belongs to the source class and is
class for conversion. This makes it necessary that the conversion constructor
be placed in the destination class.
Table provides a
summary of all the three conversions. It shows that the conversion from a class
to any other type (or any other class) should make use of a casting operator in
the source class. On the other hand, to perform the conversion from any other
type/class to a class type, a constructor should be used in the destination
class.
Table: Type
conversions
Conversion
Required
|
Conversion takes place in |
|
source class
|
Destination class
|
|
Basic -> class
|
Not applicable
|
Constructor
|
Class -> basic
|
Casting operator
|
Not applicable
|
Class -> class
|
Casting operator
|
Constructor
|
When a conversion
using a constructor is performed in the destination class, we must be able to
access the data members of the object sent (by the source class) as an
argument. Since data members of the source class are private, we must use
special access functions in the source class to facilitate its data flow to the
destination class. For example:
# include <iostream.h>
class inventl Il source class
{
int code; //
item code
int items; // no of items
float price; Il cost of each item
public:
inventl(int a, int b,
float c)
{
code- a;
items =
b;
price =
c;
}
void putdata( )
{
cout
<< "Code: " << coda << "\n";
cout
<< “Items: " << items << "\n”;
cout
<< "Value:” << price
<< "\n”;
I }
int getcode( ) {
return code; }
int getitems( ) {
return items;}
int getprice( ) { return price; }
operator float( )
{return(items * price) ;}
}; 11 End of source class
class invent2 //
destination class
{
int code;
f1oat
value;
public: invent2( ) // constructor 1
{ code = 0; value = 0;
}
invent2(int x, float
y) // constructor 2
{ code =
X; value = y; }
void putdata( )
{
cout
<< “ Code: “ << code << "\n";
cout
<< "Value: “ << value << “\n\n" ;
}
invent2{invent1 p) //conversion constructor
{
code = p.getcode ( ) ;
value = p.getitems( ) * p.getprice( ) ;
}
}; II End of destination class
main( )
{
inventl
s1(100, 5, 140.0) ;
invent2
dl;
float
total value;
/*
inventl To float */
totalvalue
= sl;
/*
inventl To invent2 */
dl = s1;
cout
<< "Product details inventl type" << "\n”;
sl.putdata(
) ;
cout
<< "\nStock value" << "\n";
cout
<< "Value = " << total-value << "\n\n";
cout
<< "Product details-invent2 type" << "\n";
d1.putdata(
) ;
}
Exercise:
- Define a class String. Use overloaded = = operator to compare two string.
- Write a program that substitutes an overloaded + = operator. The operator should allow statements like: s1 + = s2 where s2 is added (concatenated) to s1 and the result in s1.
- Write a program to overload the - - operator.
- Define two classes Polar and Rectangle to represent points in the polar and rectangle systems. Use conversion routines to convert from one system to the other.