|
|
Summary
22) Operator Overloading
23) Assignment Operator
24) Example
25) this Pointer
26) Self Assignment
27) Returning this Pointer from a Function
28) Conversions
29) Sample Program (conversion by constructor)
Operator Overloading
As earlier discussed, overloading of operators is carried out in classes on some
occasions
to enable ourselves to write a code that looks simple and clean. Suppose, there
is a class
and its two objects, say a
and b,
have been defined. Addition of these objects in the class
by writing a + b
will mean that we are adding two different objects (objects are instance
of a class which is a user defined data type). We want our code to be simple and
elegant.
In object base programming, more effort is made in class definitions, as classes
are data
types that know how to manipulate themselves. These know how to add objects of their
own type together, how to display themselves and do many other manipulations. While
discussing date
class in the previous lecture, we referred to many examples. In an
example, we tried to increment the ‘date’. The best way is to encapsulate it in
the class
itself and not in the main program when we come around to use the
date class.
Assignment Operator
At first, we ascertain whether there is need of an assignment operator or not? It
is needed
when we are going to assign one object to the other, that means when we want to
have
Page 418
expression like a = b.
C++ provides a default assignment operator. This operator does a
member-wise assignment. Let’s say, we have in a structure of a class three integers
and
two floats as data members. Now we take two objects of this class
a and
b and write
a =
b. Here the first integer of
a will have the value of
first integer of b.
The second will have
the value of second integer and so on. This means that it is a member-wise copy.
The
default assignment operator does this. But what is to do if we want to do something
more,
in some special cases?
Now let’s define a String
class. We will define it our self, without taking the built in
String class of C. We know that a string is nothing but an array of characters.
So we
define our String
class, with a data member buffer, it is a pointer to character and is
written as *buf
i.e. a pointer to character (array). There are constructors and destructors of
the class. There is a member function
length that returns the
length of the string of the
calling object. Now we want an assignment operator for this class. Suppose we have
a
constructor that allows placing a string into the buffer. It can be written in the
main
program as under:
String s1 ( “This is a test” ) ;
Thus, an object of String
has been created and initialized. The string “This is a test” has
been placed in its buffer. Obviously, the buffer will be large enough to hold this
string as
defined in our constructor. We allocate the memory for the buffer by using
new operator.
What happens if we have another
String object, let’s say
s2, and want to write s2 =
s1 ;
Here we know that the buffer is nothing but a pointer to a memory location. If it
is an
array of characters, the name of the array is nothing but a pointer to the start
of the
memory location. If default assignment operator is used here, the value of one pointer
i.e.
buf of one object will be assigned to
buf of the other object.
It means there will be the
same address in the both objects. Suppose we delete the object
s1, the destructor of this
object will free the allocated memory while giving it back to the free store. Now
the buf
of s2 holds the
address of memory, which actually has gone to free store, (by the
destructor of s1). It is no longer allocated, and thus creates a problem. Such problems
are
faced often while using default assignment operator. To avoid such problems, we
have to
write our own assignment operator.
Before going on into the string assignment operator, let’s have a look on the addition
operator, which we have defined for strings. There is a point in it to discuss.
When we
defined addition operator for strings, we talked about that what we have to do if
we want
to add (concatenate) a string into the other string. There we had a simple structure
i.e.
there is a string defined char
buf with a space of 30 characters. If we have two string
objects and the strings are full in the both objects. Then how these will be added?
Now
suppose for the moment that we are not doing memory allocation. We have a fixed
string
buffer in the memory. It is important that the addition operator should perform
an error
check. It should take length of first string , then the length of second string,
before adding
them up, and check whether it is greater than the length defined in the String (i.e.
30 in
this case). If it is greater than that, we should provide it some logical behavior.
If it is not
Page 419
greater, then it should add the strings. Thus, it is important that we should do
proper error
checking.
Now in the assignment operator, the problem here is that the
buf, that we have defined,
should not point to the same memory location in two different objects of type
String.
Each object should have its own space and value in the memory for its string. So
when
we write a statement like s2
= s1, we want to make sure that at assignment time, the
addresses should not be assigned. But there should be proper space and the strings
should
be copied there. Let’s see how we can do this.
Now take a look on the code itself. It is quiet straight forward what we want to
do is that
we are defining an assignment operator (i.e. =) for the String class. Remember that
the
object on left side will call the = operator. If we write a statement like
s2 = s1;
s2 will be
the calling object and s1
will be passed to the = operator as an argument. So the data
structure of s2
i.e. buf is available
without specifying any prefix. We have to access the
buf of s1
by writing s1.buf.
Suppose s2 has already
a value. It means that if s2
has a value, its buffer has allocated
some space in the memory. Moreover, there is a character string in it. So to make
an
assignment first empty that memory of
s2 as we want to write
something in that memory.
We do not know whether the value, we are going to write, is less or greater than
the
already existing one. So the first statement is:
delete buf ;
Here we write buf
without any prefix as it is
buf of the calling object.
Now this buffer is
free and needs new space. This space should be large enough so that it can hold
the string
of s1. First we
find the length of the string of
s1 and then we use the
new operator and
give it a value that is one more than the length of the buffer of
s1. So we write it as:
buf = new char[length + 1] ;
where length is
the length of s1.
Here buf is without
a prefix so it is the buf
of object on
the left hand side i.e. s2.
Now, when the buf
of s2 has
a valid memory address, we copy
the buf of
s1 into the
buf of
s2 with the use of string copy function
(strcpy). We write it
as:
strcpy ( buf, s1.buf ) ;.
Now the buf of string
of s1 has been copied
in the buf of
s2 that are located
at different
spaces in the memory. The advantage of it is that now if we delete one object
s1 or
s2, it
will not affect the other one. The complete code of this example is given below.
Example
/*This program defines the assignment operator. We copy the string of one object
Page 420
into the string of other object using different spaces for both strings in the memory.
*/
#include <iostream.h>
#include <string.h>
#include <stdlib.h>
// class definition
class String
{
private :
char *buf ;
public:
// constructors
String();
String( const char *s )
{
buf = new char [ 30 ];
strcpy (buf,s);
}
// display the string
void display ( )
{
cout << buf << endl ;
}
// getting the length of the string
int length ()const
{
return strlen(buf);
}
// overloading assignment operator
void operator = ( const String &other );
};
// ----------- Assignment operator
void String::operator = ( const String &other )
{
int length ;
length = other.length();
delete buf;
buf = new char [length + 1];
strcpy( buf, other.buf );
}
//the main program that uses the new String class with its assignment operator:
main()
{
Page 421
String myString( "here's my string" );
cout << “My string is = ” ;
myString.display();
cout << '\n';
String yourString( "here's your string" );
cout << “Your string is = ” ;
yourString.display();
cout << '\n';
yourString = myString;
cout << “After assignment, your string is = ” ;
yourString.display();
cout << '\n';
system ("pause");
}
Following is the output of the program.
My string is = here's my string
Your string is = here's your string
After assignment, your string is = here's my string
The above example is regarding the strings. Yet in general, this example pertains
to all
classes in which we do memory manipulations. Whenever we use objects that allocate
memory, it is important that an assignment operator (=) should be defined for it.
Otherwise, the default operator will copy the values of addresses and pointers.
The actual
values and memory allocation will not be done by it.
Let’s go on and look what happens when we actually do this assignment? In the
assignment of integers, say we have three integers i, j and k with some values.
It is quiet
legal to write as i = j
; By this ,we assign the value of
j to
i. After this we write
k = i ; this
assigns the value of i
to k. In
C, we can write the above two assignment statements in one
line as follows
k = i = j ;
This line means first the value of
j is assigned to
i and that value of
i is assigned to
k.
The mechanism that makes this work is that in C or C++ every expression itself has
a
value. This value allows these chained assignment statements to work. For example,
when we write k = i = j
; then at first i =
j is executed. The value at the left hand side of
this assignment statement is the value of the expression that is returned to the
part ‘k =’
.This value is later assigned to
k. Now take another example.
Suppose we have the
following statement:
Page 422
k = i = ++j ;
In this statement, we use the pre-increment operator, as the increment operator
(++) is
written before j.
This pre-increment operator will increment the value of
j by 1 and this
new value will be assigned to
i. It is pertinent to note that this way
++j returns a value
that is used in the statement. After this, the value of
i is assigned to
k. For integers, it is
ok. Now, how can we make this mechanism work with our
String objects. We have
three
String objects s1,
s2 and
s3. Let’s say there is
a value (a string ) in the buffer of
s1. How
can we write s3 = s2 = s1;.
We have written s2 = s1
in the previous example in
assignment operator. We notice that there is no return statement in the code of
the
assignment operator. It means that operator actually returns nothing as it has a
void return
type. If this function is not returning any thing then
s3 = s2 = s1 ; cannot work.
We make
this work with the help of
this pointer.
this Pointer
Whenever an object calls a member function, the function implicitly gets a pointer
from
the calling object. That pointer is known as
this pointer. ‘this’
is a key word. We cannot
use it as a variable name. ‘this’
pointer is present in the function, referring to the calling
object. For example, if we have to refer a member, let’s say
buf, of our
String class, we
can write it simply as:
buf ;
That means the buf
of calling object is being considered. We can also write it as
this->buf ;
i.e. the data member of the object pointed by
this pointer is being called.
These ( buf and
this->buf ) are exactly the same. We can also write it in a third way
as:
(*this).buf ;.
So these three statements are exactly equivalent. Normally we do not use the statements
written with this
key word. We write simply
buf to refer to the calling
object.
In the statement (*this).buf
; The parentheses are necessary as we know that in
object.buf
the binding of dot operator is stronger than the *. Without parentheses the
object.buf is
resolved first and is dereferenced. So to dereference
this pointer to get the
object, we
enforced it by putting it in parentheses.
Self Assignment
Suppose, we have an integer ‘i.
In the program, somewhere, we write
i = i ; It’s a do
nothing line which does nothing. It is not an error too. Now think about the String
object,
we have a string s
that has initialized to a string, say, ‘This is a test’. And then we write
s
= s ; The behavior of equal operator that we have defined for the String
object is that it, at
first deletes the buffer of the calling object. While writing
s = s ; the assignment
operator
Page 423
frees the buffer of s.
Later, it tries to take the buffer of the object on right hand side,
which already has been deleted and trying to allocate space and assign it to
s. This is
known as self-assignment. Normally, self -assignment is not directly used in the
programs. But sometimes, it is needed. Suppose we have the address of an object
in a
pointer. We write:
String s, *sptr ;
Now sptr is a pointer
to a String while
s is a
String object. In the code
of a program we
can write
sptr = &s ;
This statement assigns the address of
s to the pointer
sptr. Now some where in
the
program we write s = *sptr
; that means we assign to
s the object being pointed
by sptr.
As sptr has the
address of s , earlier
assigned to it. This has the same effect as
s = s ;. The
buffer of s will
be deleted and the assignment will not be done. Thus, the program will
become unpredictable. So self-assignment is very dangerous especially at a time
when we
have memory manipulation in a class. The
String class is a classic
example of it in which
we do memory allocation. To avoid this, in the equal operator (operator=), we should
first
check whether the calling object (L.H.S.) and the object being gotten (R.H.S.) are
the
same or not. So we can write the equal operator (operator=) as follows
void String::operator=( const String &other )
{
if( this == &other )
return;
delete buf;
length = other.length;
buf = new char[length + 1];
strcpy( buf, other.buf );
}
Here above, the statement if
( this == &other) checks that if the calling object (which is
referred by this)
is the same as the object being called then do nothing and return as in
this case it is a self assignment. By doing this little change in our assignment
operator, it
has become safe to use. So it is the first usage of
this pointer that is a
check against self
assignment.
Page 424
Returning this Pointer From a Function
Now lets look at the second use of
this pointer. We want to
do the assignment as
s3 = s2 = s1 ;
In this statement the value of
s2 = s1 is assigned to
s3. So to do this
it is necessary that
the assignment operator should return a value. Thus, our assignment operator will
expand
so that it could return a value. The assignment operator, till by now copies the
string on
right hand side to the left hand side. This means
s2 = s1 ; can be done by
this. Now we
want that this s2
should be assigned to
s3, which can be done only if
s2 = s1 returns
s2
(an object). So we need to return a
String object. Here becomes
the use of this
pointer, we
will write as
return *this ;
Here, in the assignment operator code
this is referring to the
calling object (i.e. s2 in this
case). So when we write return
*this ; it means return the calling object ( object on
L.H.S.) as a value. Thus s3
gets the value of
s2 by executing
s3 = s2 where
s2 is the
value returned by the assignment operator by
s2 = s1; Thus the complete
assignment
operator (i.e. operator= function) that returns a reference to an object will be
written as
under
String &String::operator=( const String &other )
{
if( &other == this ) //if calling and passed objects are
return *this; // same then do nothing and
return
delete buf;
Page 425
length = other.length;
buf = new char[length + 1];
strcpy( buf, other.buf );
return *this;
}
Now, here the first line shows that this
operator= function returns
a reference to an
object of type String
and this function takes a reference as an argument. The above
version of operator=
function takes a reference as an argument. First of all it checks it
with the calling object to avoid self assignment. Then it deletes the buffer of
calling
object and creates a new buffer large enough to hold the argument object. And then
at the
last it returns the reference of the calling object by using
this pointer.
With this version of the assignment operator ( operator= function) we can chain
together
assignments of String
objects like s3 = s2
= s1 ;
Actually, we have been using
this pointer in chained statements of
cout. For example, we
write
cout << a << b << c ;
Where a,
b, and
c are any data type. It
works in the way that the stream of
cout i.e. << is
left associative. It means first
cout << a is executed and
to further execute it, the second
<< should have cout
on its left hand side. So here, in a way, in this operator overloading,
a reference to the calling object that is
cout is being returned
to the stream insertion <<.
Thus a reference of cout
is returned, and (as a reference to
cout is returned) the <<
sees a
cout on left hand side and thus the next
<< b is executed and it
returns a reference to cout
and with this reference the next
<< c works. This all work
is carried out by this
pointer.
We have seen that value can be returned from a function with
this pointer. We used it
with assignment operator. Lets consider our previous example of
Date class. In that class
we defined increment operator, plus operator, plus equal operator, minus operator
and
minus equal operator. Suppose we have
Date objects
d1,
d2 and
d3. When we write like
d2 = d1++ ; or d2
= d1 + 1 ; here we realize that the + operator and the ++ operator
should return a value. Similarly, other operators should also return a value. Now
let’s
consider the code of Date
class. Now we have rewritten these operators. The difference in
code of these is not more than that now it returns a reference to an object of type
Date.
There are two changes in the previous code. First is in the declaration line where
we now
use & sign for the reference and we write it like
Date& Date::operator+=(int days)
We write this for the all operators (i.e. +, ++, - and -=). Then in the function
definition we
return the reference of the left hand side
Date object ( i.e. calling
object) by writing
return *this ;
Now we can rewrite the operator+=
of the Date
class as follows.
The declaration line in the class definition will be as
Page 426
Date& operator+=(int days);
And the function definition will be rewritten as the following.
Date& Date::operator+=(int days) // return type reference to
object
{
for (int i=0; i < days; i++)
*this++;
return *this; // return reference to object
}
This concludes that whenever we are writing arithmetic operator and want that it
can be
used in chained statements (compound statements) then we have to return a value.
The
easiest and most convenient way of returning that value is by returning a reference
to the
calling object. So, by now we can easily write the statements of
date object, just like
we
write for integers or floats. We can write
date2 = date1 + 1 ;
Or date2 = date1++
;
and so on.
Conversions
Being in the C language, suppose we have an integer
i and a float
x. Now in the program
we write x = i ;
As we know that the operations of
int and
float are different in
the
memory. Therefore, we need to do some kind of conversion. The language automatically
converts i (int)
to a float (or to whatever type is on the L.H.S.) and then does the
assignment.
Both C and C++ have a set of rules for converting one type to another. These rules
are
used in the following situations
- When assigning a value. For example, if you assign an integer to a variable of
type long, the compiler converts the integer to a long.
- When performing an arithmetic operation. For example, if you add an integer and
a floating-point value, the compiler converts the integer to a float before it performs
the addition.
- When passing an argument to a function; for example, if you pass an integer to
a
function that expects a long.
- When returning a value from a function; for example, if you return a float from
a
function that has double as its return type.
In all of these situations, the compiler performs the conversion implicitly. We
can make
the conversion explicit by using a cast expression.
Page 427
Now the question arises that can we do conversion with objects of our own classes.
The
answer is yes. If we go to the basic definition of a class it is nothing but a user
defined
data type. As it is a user defined data type, we can also define conversion on it.
When we
define a class in C++, we can specify the conversions that the compiler can apply
when
we use instances of that class. We can define conversions between classes, or between
a
class and a built-in type
There is an example of it. Suppose we have stored date in a serial number form.
So now it
is not in the form of day, month and year but it is now a long integer. For example
if we
start from January 1, 1900 then 111900 will be the day one, second January will
be the
day 2, third January will be the day 3 and so on. Going on this way we reached in
year
2000. There will be a serial number in year 2000. This will be a single long integer
that
represents the number of days since a particular date. Now if we have a
Date object
called d and an
integer i. We can
write d = i ; which
means we want that i
should go in
the serial part of d.
This means convert the integer
i into
date object and then the
value of
i should go into the serial number and then that object should be assigned
to d. We can
make this conversion of Date
object. We do this in the constructor of the class. We pass
the integer to the constructor, here convert it into a date, and the constructor
then returns
a Date object. On
the other hand, if there is no constructor then we can write conversion
function. We have used the conversion operator with
cast. The way of casting
is that we
write the name of the cast (type to which we want to convert) before the name of
variable. Thus if we have to write
x = i ; where
x is a float and
i is an integer then we
write it with conversion function as
x = (float) i ; The (float)
will be the conversion
operator. Normally this conversion is done by default but sometimes we have to force
it.
Now we want to write a conversion operator, which converts an integer to a
Date object.
(here Date is not our old class, it’s a new one). We can write a conversion function.
This
function will be a member of the
Date class. This function
will return nothing, as it is a
conversion function. The syntax of this function will be
Date () that means now
this is a
conversion function.
In the body of this function we can write code of our own. Thus we can define the
operator, and it will work like that we write within the parentheses the name of
the
conversion operator. The conversion functions are quiet interesting. They allow
us to
manipulate objects of different classes. For example, we have two classes one is
a truck
and other is a car. We want to convert the car to a truck. Here we can write a conversion
function, which says take a car convert it into a truck and then assign it to an
object of
class truck.
Sample Program (conversion by constructor)
Lets take a look at an example in which the conversion functions are being used.
There is
a class Fraction.
Lets talk why we called it
Fraction class. Suppose we have an
assignment
double x = 1/3 ;
Page 428
When we store 1/3 in the computer memory, it will be stored as a double precision
number. There is a double precision division of 1 by 3 and the answer 0.33333… is
stored. Here the number of 3s depends upon the space in the memory for a double
precision number. That value is assigned to a
double variable. What happens
if we
multiply that double
variable by 3, that means we write
3 * x; where x was equal
to 1/3
(0.33333…). The answer will be 0.99999…, whatever the number of digits was. The
problem here is that the answer of 3 * 1 / 3 is 1 and not 0.99999. This problem
occurs,
when we want to represent the numbers exactly. The fraction class is the class in
which
we provide numerator and denominator to the object and it stores them separately.
It will
always keep them as an integer numerator and an integer denominator and we can do
all
kinds of arithmetic with it. Now if 1 and 3 are stored separately as numerator and
denominator, how we can add them to some other fraction. We have enough tools at
our
disposal. One of which is that we can overload the addition operator. We can add
1/3 and
2/5, the result of which is another fraction i.e. numerator and denominator. In
this way we
have no worries of round off errors, the truncation and conversions of int and floats.
Considering the Fraction
class we can think of a constructor which takes an integer and
converts it into a fraction. We do not want that as a fraction there should be some
value
divided by zero. So we define the default constructor that takes two integers, one
for
numerator and one for denominator. We provide a default value for the denominator
that
is 1. It means that now we can construct a fraction by passing it a single integer,
in which
case it will be represented as a fraction with the passed integer as a numerator
and the
default vale i.e. 1 as the denominator. If we have a fraction object
f and we write
f = 3;
Then automatically this constructor (i.e. we defined) will be called which takes
a single
integer as an argument. An object of type fraction will be created and the assignment
will
be carried out. In a way, this is nothing more than a conversion operation. That
is a
conversion of an integer into a fraction. Thus a constructor that takes only one
parameter
is considered a conversion function; it specifies a conversion from the type of
the
parameter to the type of the class.
So we can write a conversion function or we can use a constructor of single argument
for
conversion operation. We cannot use both, we have to write one or the other. Be
careful
about this. We don’t use conversion functions often but sometimes it is useful to
write
them. The conversion operators are useful for defining an implicit conversion from
the
class to a class whose source code we don't have access to. For example, if we want
a
conversion from our class to a class that resides within a , we cannot define
a
single-argument constructor for that class. Instead, we must use a conversion operator.
It makes our code easier and cleaner to maintain. It is important that pay more
attention
while defining a class. A well-defined class will make their use easy in the programming.
Following is the code of the example stated above.
/* This program defines a class Fraction which stores numerator and
denominator of a fractional number separately. It also overloads the
addition operator for adding the fractional numbers so that exact results
can be obtained.
Page 429
*/
#include <stdlib.h>
#include <math.h>
#include <iostream.h>
// class definition
class Fraction
{
public:
Fraction();
Fraction( long num, long den );
void display() const;
Fraction operator+( const Fraction &second ) const;
private:
static long gcf( long first, long second );
long numerator, denominator;
};
// ----------- Default constructor
Fraction::Fraction()
{
numerator = 0;
denominator = 1;
}
// ----------- Constructor
Fraction::Fraction( long num, long den )
{
int factor;
if( den == 0 )
den = 1;
numerator = num;
denominator = den;
if( den < 0 )
{
numerator = -numerator;
denominator = -denominator;
}
factor = gcf( num, den );
if( factor > 1 )
{
numerator /= factor;
denominator /= factor;
}
}
// ----------- Function to print a Fraction
Page 430
void Fraction::display() const
{
cout << numerator << '/' << denominator;
}
// ----------- Overloaded + operator
Fraction Fraction::operator+( const Fraction &second ) const
{
long factor, mult1, mult2;
factor = gcf( denominator, second.denominator );
mult1 = denominator / factor;
mult2 = second.denominator / factor;
return Fraction( numerator * mult2 + second.numerator * mult1,
denominator * mult2 );
}
// ----------- Greatest common factor
// computed using iterative version of Euclid's algorithm
long Fraction::gcf( long first, long second )
{
int temp;
first = labs( first );
second = labs( second );
while( second > 0 )
{
temp = first % second;
first = second;
second = temp;
}
return first;
}
//main program
void main()
{
Fraction a, b( 23, 11 ), c( 2, 3 );
a = b + c;
a.display();
cout << '\n';
system("pause");
}
The output of the program is as follows
91/33
Page 431
Here is an example from the real world. What happens if we are dealing with currency?
The banks deal with currency by using computer programs. These programs maintain
the
accounts by keeping the track of transactions and manipulating deposits and with
drawls
of money. Suppose the bank declares that this year the profit ratio is 3.76 %. Now
if the
program calculates the profit as 3.67 % of the balance, will it be exactly in rupees
and
paisas or in dollars and cents? Normally, all the currencies have two decimal digits
after
decimal point. Whenever we apply some kind of rates in percentage the result may
become in three or four decimal places. The banks cannot afford that the result
of 90
paisas added to 9 rupees and 10 paisas become 10 rupees and 1 paisa. They have to
accurate arithmetic. So they do not rely on programs that use something like
double
precisions to represent currencies. They would have rather written in the program
to treat
it as a string. Thus 9 is a string, 10 is a string and the program should define
string
addition such that the result of addition of the strings .10 and .90 should be 1.00.
Thus,
there are many things that happen in real world that force us as the programmer
to
program the things differently. So we do not use native data types.
The COBOL, COmmon Business Oriented Language has a facility that we can represent
the decimal numbers exactly. Internally it (the language) keeps them as strings
in the
memory. There is no artificial computer representation of numbers. Now a day, the
languages provide us the facility by which we can define a data type according to
a
specific requirement, as we defined
fraction to store numerator
and denominator
separately. By using this fraction
data type, we never loose precision in arithmetic. The
same thing applies to the object like currency, where we can store the whole number
part
and the fractional part both as separate integers and never loose accuracy. But
whenever
we get into these classes, it is our responsibility to start writing all of the
operators that
are required to make a complete class. |
|
|
|