|

| |
"It
is appallingly obvious that our technology exceeds our
humanity."
- Albert Einstein
"No,
no, you're not thinking, you're just being logical."
- Niels Bohr |
C++'s built in types
(int, real, char, etc.) and built in operators (multiplication, addition, etc.) can be added to your own
classes.
Operator overloading - Example: A "Counter" object will be created and used in loops, incrementing, decrementing, and tracking. The counter object can not be incremented or manipulated unless you use operator overloading to restore its functionality. Example:
//Overloading the increment operator
to increment objects typedef
unsigned short USHORT; #include <iostream.h>
//-------------------------------------------------------------------------------------------------------------------
class Counter {
public: Counter(); ~Counter(){}
USHORT GetItsVal()const { return
itsVal; }
void SetItsVal(USHORT x) {itsVal = x; }
void Increment() { ++itsVal; }
const Counter &operator++();
//overloads the
increment operator
private: USHORT itsVal; };
//Defines
constructor outside of class Counter::Counter(): itsVal(0) {};
//Overloaded increment operator - we teach it to
increment objects. It returns a reference
//to a const Counter object.
REFERENCE used to avoid creation of extra temporary
object.
const Counter &Counter::operator++() { ++itsVal;
return *this; }
//-------------------------------------------------------------------------------------------------------------------
int main() { Counter
SomeCounterObject; cout << "The value of
SomeCounterObject is "
<< SomeCounterObject.GetItsVal() << endl;
SomeCounterObject.Increment();
cout << "The value of
SomeCounterObject is "
<< SomeCounterObject.GetItsVal() << endl;
++SomeCounterObject;
cout << "The value of
SomeCounterObject is "
<< SomeCounterObject.GetItsVal() << endl;
Counter AnotherCounterObject = ++SomeCounterObject;
cout << "The value of
AnotherCounterObject: "
<< AnotherCounterObject.GetItsVal();
cout << " and
SomeCounterObject: "
<< SomeCounterObject.GetItsVal() << endl;
return 0; } |
Note: The value returned is a Counter reference, avoiding the creation of an extra temporary object. It is a const reference because the value should not be changed by the function using Counter. If the Counter allocated memory, it would be important to override the copy constructor, but in this case the default copy constructor works fine.
When overloading these operators, remember the subtleties
between postfix vs. prefix:
Prefix - increment and then fetch
the value. Postfix - fetch and then increment the
value (returns value that existed BEFORE it was incremented). You must create a temporary object to hold the old value while you increment it to the new value. You return the temporary, old value because the postfix operator asks for the original value, not the incremented one. Example:
a = x++;
If x was 5 then a would be 5 and x 6. If x is an object,
and we seek to overload the postfix increment operator, we must
teach the compiler how to do this. We would need to code
our function stash away the original value "5" in a temporary object, increment x's value to "6", and
then return the temporary object to assign its value to a. Because the temporary
object is being returned, it must be returned by value and not reference.
This is because the temporary will go out of scope as soon as the function returns
and if there were a reference, it would be left aliasing nothing. Example:
//Returning the dereferenced this pointer
#include <iostream.h>
typedef
unsigned short USHORT;
//-------------------------------------------------------------------------------------------------------------------
class Counter {
public: Counter(); ~Counter(){}
USHORT GetItsVal()const {
return itsVal; }
void SetItsVal(USHORT x) {itsVal = x; }
const Counter
&operator++();
//prefix
const Counter
operator++(int);
//postfix
private: USHORT itsVal; };
Counter::Counter():itsVal(0) {}
//Overloaded prefix operator
const Counter &Counter::operator++() { ++itsVal;
return
*this; }
//Overloaded
postfix operator
const Counter Counter::operator++(int) {
//Create a Counter object that takes a pointer to
the object itself calling this function Counter temp(*this); ++itsVal;
return temp; }
//-------------------------------------------------------------------------------------------------------------------
int main() { Counter
ACounterObject;
cout << "The value of
ACounterObject is "
<< ACounterObject.GetItsVal() << endl;
ACounterObject++;
//calls overloaded
postfix increment operator
cout << "The value of
ACounterObject is "
<< ACounterObject.GetItsVal() << endl;
++ACounterObject;
//calls overloaded
prefix increment operator
cout << "The value of ACounterObject is "
<< ACounterObject.GetItsVal() << endl;
Counter AnotherCounterObject = ++ACounterObject;
cout << "The value of
AnotherCounterObject: "
<< AnotherCounterObject.GetItsVal();
cout << " and ACounterObject: "
<< ACounterObject.GetItsVal() << endl;
AnotherCounterObject = ACounterObject++;
cout << "The value of
AnotherCounterObject: "
<< AnotherCounterObject.GetItsVal();
cout << " and ACounterObject: "
<< ACounterObject.GetItsVal() << endl;
return 0; } |
Output:
The value of
ACounterObject is 0 The value of
ACounterObject is -1 The value of
ACounterObject is 2 The value of
AnotherCounterObject: 3 and
ACounterObject: 3 The value of
AnotherCounterObject: 4 and
ACounterObject: 4
The call to the overloaded prefix increment operator does not include the flag integer (x), but
it is used with normal syntax. The overloaded postfix operator uses a flag value (x)
represented by its sole parameter "int" to signal that it is the
overloaded postfix function, rather than the prefix function. The flag value (x) is never
actually used to pass values, however, it is merely a detector.
unary operator - Operates on one object only. The increment
and decrement operators are urnary. binary operator -
Operates on two objects. The addition (+) operator is binary.
When overloading binary operators, we would declare two Counter
objects as though they were Counter variables and then add them:
Counter varOne, varTwo, varThree; VarThree = VarOne + VarTwo;
Start by writing the function Add().
It takes a Counter as its argument, adds the values, then returns a Counter
object with the result. Example:
//The Add() function
#include <iostream.h>
typedef
unsigned short USHORT;
//-------------------------------------------------------------------------------------------------------------------
class Counter {
public: Counter(); Counter(USHORT initialValue); ~Counter(){}
USHORT GetItsVal()const { return itsVal; }
void SetItsVal(USHORT x) {itsVal = x; }
//Takes
a reference to a constant Counter object
Counter Add(const Counter
&);
private: USHORT itsVal; };
//Counter
constructor can take an argument Counter::Counter(USHORT initialValue):itsVal(initialValue) { }
Counter::Counter():itsVal(0) { }
//Define the Add fucntion
Counter Counter::Add(const Counter
& rhs) {
return Counter(itsVal+
rhs.GetItsVal()); }
//-------------------------------------------------------------------------------------------------------------------
int main() { Counter varOne(2), varTwo(4), varThree;
varThree = varOne.Add(varTwo);
cout << "varOne: " << varOne.GetItsVal()<< endl; cout << "varTwo: " << varTwo.GetItsVal() << endl; cout << "varThree: " << varThree.GetItsVal() << endl;
return
0; } |
Output:
varOne: 2 varTwo: 4 varThree: 6
We might provide the default value 0 to the constructor declared here.
The Add() function is declared. It takes a const Counter reference which is the number to add to the current object.
Since varOne and varTwo need to be initialized to a non-zero value, another constructor is created. The default constructor initializes itsVal to 0.
The Add() function returns a Counter object which is the result to be assigned to the left side of the
assignment statement. At present, varOne is the object, varTwo is the parameter to the Add() function,
and the result is assigned to varThree. To create varThree without having to initialize a value for it, the
default constructor is required.
Operator + - The Add() function shown previously works, but its use seems unnatural. Overloading the operator + would make for a more natural use of the Counter class. Example:
// Overloading operator plus (+)
to add complete objects
#include <iostream.h>
typedef unsigned long ULONG;
//-------------------------------------------------------------------------------------------------------------------
class Counter { public: Counter(); Counter(ULONG initialValue); ~Counter(){}
ULONG GetItsVal()const { return itsVal; }
void SetItsVal(ULONG x) {itsVal = x; }
Counter operator+ (const Counter
&);
private: ULONG itsVal; };
Counter::Counter(ULONG initialValue): itsVal(initialValue) {}
Counter::Counter(): itsVal(0) {}
//Define overloaded addition operator
//Adds the
"itsVal" data member of the object passed in to the
other "itsVal" data member Counter Counter::operator+ (const Counter
& rhs) {
return Counter(itsVal + rhs.GetItsVal()); }
//-------------------------------------------------------------------------------------------------------------------
int main() { Counter varOne(2), varTwo(4), varThree;
//Add 2 objects
together and assign it to the third object varThree = varOne
+ varTwo;
//looks like plain
math but really operator overloading!
cout << "varOne: " << varOne.GetItsVal()<< endl; cout << "varTwo: " << varTwo.GetItsVal() << endl; cout << "varThree: " << varThree.GetItsVal() << endl;
return 0; } |
Output:
varOne: 2 varTwo: 4 varThree: 6
In this example, the overloaded operator + is declared. The syntax is different
in this case - it is more natural to say varThree = varOne + varTwo; than it is to say
varThree = varOne.Add(varTwo);.
By overloading the "+" operator, we can add objects together as
well as integers and floats.
Operators on built-in types, like int, cannot be overloaded.
Only operators on user-defined types. The precedence and the "arity" of the operator cannot be changed. The "arity" refers to whether it is "unary" or "binary".
Unfortunately, you cannot make new operators, so you cannot declare ** to be the "power of" operator. Operator overloading is very abused and misused in C++ programming. It can be dangerous to use "+" to concatenate a series of letters, or to use / to split a string.
Operator overloading is a powerful tool, but it should be used
cautiously.
It is also possible to overload operator = . The compiler provides a
default constructor, destructor, copy constructor and an
assignment operator "=", if one is not declared. The
default assignment operator is called whenever you assign to an object.
Example: CAT catTwo(3,4) - catTwo is created and assigned values 3 and 4.
A quick review:
Shallow copy - Just copies members, both objects point to same area on free store.
Two pointers end up pointing to same memory location. Deep copy -
Allocates necessary memory
to NEW locations. Pointers pointers point to different
locations.
In the expression ,CAT catTwo(3,4),
the object, catTwo, already exists and already has memory allocated.
That memory must be deleted to avoid a memory leak. The first thing to do is delete memory assigned to its pointers. But what if you assign catTwo to itself, like
catTwo = catTwo; ? It is possible for this to happen by accident when referenced and dereferenced
pointers hide the fact that the object's assignment is to
itself. The object catTwo could delete its memory allocation, and when it tried to copy memory from the right-hand assignment, the memory would be gone. To protect against this, the assignment operator
should check to see if the right-hand side of the assignment operator is the object itself by examining the "this" pointer
to reveal whether or not they have the same address. Example:
//Copy constructors #include <iostream.h>
//----------------------------------------------------------------------------------------------------------------
class Platypus {
public:
Platypus();
//default constructor
//Accessor methods
int GetAge()
const {
return *itsAge; }
int GetWeight()
const {
return *itsWeight; }
void SetAge(int age) {
*itsAge = age; }
//Overloaded assignment operator
Platypus
operator=(const
Platypus
&);
private:
int *itsAge;
int *itsWeight; };
//Platypus
constructor assigns default values
Platypus::Platypus() { itsAge = new int; itsWeight =
new int;
*itsAge = 5;
*itsWeight = 9; }
//By overloading the assignment operator, we can use
it to copy one object's data member to another
//Overloaded assignment operator
checks to make sure address of object being passed
in is not the same as object itself
Platypus Platypus::operator=(const
Platypus & rhs) {
if (this ==
&rhs)
return *this;
delete itsAge;
delete itsWeight;
itsAge = new int; itsWeight =
new int;
*itsAge = rhs.GetAge();
*itsWeight = rhs.GetWeight();
return
*this; }
//----------------------------------------------------------------------------------------------------------------
int main() {
Platypus Joe;
cout << "Joe's age: " <<
Joe.GetAge() << endl; cout << "Setting
Joe to 6...\n";
Joe.SetAge(6);
Platypus Bill;
cout << "Bill's age: " << Bill.GetAge() << endl; cout
<< "copying Joe to Bill...\n";
Bill = Joe;
//calls the
overloaded assignment operator
cout << "Bill' age: " << Bill.GetAge() << endl;
return 0; } |
Output:
Joe's age: 5 Setting Joe to 6 Bill's age: 5 copying
Joe to Bill... Bill's age: 6
In this example, the current Platypus object being assigned to is tested to see whether it is the same as the
Platypus being assigned. This is done by checking whether the address of "rhs" is the same as the address stored in the "this" pointer.
Remember it is common to refer to the parameter of a copy constructor as "rhs", which stands for "right hand side",
signifying that which is on the right side of the assignment
operator.
Another way is to check this is to dereference the "this" pointer and see if the 2 objects are the same,
possessing the same address. Example:
if (*this == rhs). The equality operator (==) can be overloaded, allowing you to determine
for yourself what it means for your objects to be equal.
Now that is power and freedom! Let's pose a question: What happens if you try to assign a variable of a built in type (such as
int or unsigned short) to an object of a user defined class?
// This code won't compile!
#include <iostream.h>
typedef unsigned short USHORT;
//----------------------------------------------------------------------------------------------------------------
class Counter {
public: Counter(); ~Counter(){}
//Accessor methods
USHORT GetItsVal()const {
return itsVal; }
void SetItsVal(USHORT x) {itsVal = x; }
private: USHORT itsVal;
};
//Constructor implemented outside of class, assigns
a default value Counter::Counter(): itsVal(0) {}
//----------------------------------------------------------------------------------------------------------------
int main() { USHORT
ShortInteger = 5;
Counter ACounter = ShortInteger;
cout << "ACounter: " << ACounter.GetItsVal() << endl;
return 0; } |
In the example above,
the Counter class declared has only a default constructor. It declares no particular method for turning an
int into a Counter object, so
trying to compile this will cause cause a compile time error. The compiler can not figure out, unless
we tell it, that given an
int, it should assign that value to the
Counter class data member variable, "itsVal". We can correct this by creating a conversion operator. In
this case, we will implement this in our constructor.
Conversion operator - a constructor that takes an
int and produces a Counter object.
Conversion operators are implemented using constructor or
operator overloading. They allow us to take an object, and
using a basic operator, teach the compiler how to manipulate the
object's data members.
// Constructor as conversion operator
#include <iostream.h>
typedef unsigned short USHORT;
//----------------------------------------------------------------------------------------------------------------
class Counter {
public:
//2 overloaded
constructors. The second is our conversion
operator.
Counter();
Counter(USHORT val); ~Counter(){}
//Accessor methods
USHORT GetItsVal()const {
return itsVal; }
void SetItsVal(USHORT x) {itsVal = x; }
private: USHORT itsVal;
};
Counter::Counter():itsVal(0) { }
Counter::Counter(USHORT val):itsVal(val) { }
//----------------------------------------------------------------------------------------------------------------
int main() { USHORT
AShortInteger = 5;
Counter ACounterObject = AShortInteger;
cout << "ACounterObject: " << ACounterObject.GetItsVal() << endl;
return 0; } |
Output:
ACounterObject: 5
This implements an important change, the constructor is overloaded to take an
int. This effectively creates a Counter
object out of an int. The compiler is able to call the constructor that takes an
int as its argument. If you try to reverse it, however, you will get a compile time error. The compiler knows how to create a Counter out of an
int, but it does not know how to reverse the process
and create an int out of a Counter.
For this, C++ provides conversion operators that can be added to your class. They allow your class to specify how to do implicit conversions to built-in types.
Conversion operators do not specify a return value, even though they do return a "converted" value. Example:
// Overload operator plus (+)
#include <iostream.h>
typedef unsigned long ULONG;
//----------------------------------------------------------------------------------------------------------------
class Counter {
public:
//2 overloaded
constructors. The second is our conversion
operator.
Counter();
Counter(ULONG initialValue); ~Counter(){}
//Accessor methods
ULONG GetItsVal()const { return itsVal; }
void SetItsVal(ULONG x) {itsVal = x; }
//Overload operator + - takes a reference to a const
Counter object
Counter operator+ (const Counter
&);
private: ULONG itsVal; };
Counter::Counter():itsVal(0) { }
Counter::Counter(ULONG initialValue):itsVal(initialValue) { }
//Overload
operator + to add "itsVal" data member when adding
objects Counter Counter::operator+ (const Counter
& rhs) {
return Counter(itsVal
+ rhs.GetItsVal()); }
//----------------------------------------------------------------------------------------------------------------
int main() { Counter
CounterOne(2), CounterTwo(4), CounterThree;
//The statement below uses both our overloaded
operator + and our conversion operator.
//The overloaded operator + allows us to add the right and
left objects together by adding their data members.
//The conversion operator allows us to assign that integer
result to the third object's int data member. CounterThree = CounterOne
+ CounterTwo;
cout << "CounterOne: " << CounterOne.GetItsVal()<< endl; cout << "CounterTwo: " <<
CounterTwo.GetItsVal() << endl; cout << "CounterThree: " <<
CounterThree.GetItsVal() << endl;
return 0; } |
Output:
CounterOne: 2
CounterTwo: 4
CounterThree: 6
In this example, a conversion operator is declared by
overloading the second constructor - It has no return value.
The compiler has no idea how to add the user-defined data type
"Counter" to other Counter objects. So, operator "+" is
overloaded, so that when it is used to add Counter objects, it
will in fact add the values of their "itsVal" data members
together. We may now return the value of itsVal, a data
member of a Counter object, converted to an int.
Now, relatively speaking, the compiler can turn
int's into Counter objects and
Counter objects into int's. They can be assigned to each other freely.
This is because the overloaded operator +
allows us to add the right and left objects together by adding
their integer data members together. The conversion
operator then allows us to assign the integer result of the
addition to the third object's int
data member.
©2004 C. Germany
|