Home | Gaming | Programming | Play Online | Contact | Keyword Query
Games++ Games & Game Programming

GAMES++
Games++ Home
Games++ Gaming
Games++ Programming
Beta Testing Games
Free Online Games
Hints & Cheats

BROWSER UTILITIES
E-mail This Page
Add to Favorites

SITE SEARCH

Web Games++

AFFILIATES
Cheat Codes
Trickster Wiki
Game Ratings
Gameboy Cheats
PlayStation Cheats
BlackBerry Games
Photoshop Tutorials
Illustrator Tutorials
ImageReady Tutorials

ADVERTISEMENT

ADVERTISEMENT

Ch 9: Classes and Operator Overloading - C++ by Metrowerks

Introduction to C++ by Metrowerks

Passing and Returning Objects

We will begin this lesson by studying the issues related to passing objects to, and returning objects from, functions. But first, let's look at assigning objects.

Assigning Objects

If two objects are of the same class or type, the attributes of one object can be assigned to the other. When one object is assigned to another, a copy of the first object's data is transferred to the other object. The statement used to assign the attributes of one object, obj1, to a second object, obj2, would look like this:

obj2 = obj1

If both objects are of the same type, all the data in obj1 will be copied to obj2. Simple!

Passing Objects to Functions

Just like any other data type, an object can be passed to a function. This passing is achieved by the standard "call-by-value" method, so the object that is passed cannot be changed by the function.

Constructors, Destructors, and Passing Objects

Since a copy of an object, rather than the original object itself, is passed to a function, how will we know when its constructor and destructor functions will be called? Remember, a constructor function is called each time a new object of a certain class or type is created, and a destructor function is called to destroy that object.

So, as we pass an object, exactly when are the constructor and destructor functions called? A constructor function is called to create an object. When a copy of the object is created (so it can be passed to a function), your constructor function is not called again. However, (if you don't have one) a special constructor called a copy constructor is automatically created for you by the compiler, and that is used. Then, when the function is finished, you call the destructor function, and that copy of the object is destroyed. Once the original object is destroyed, the destructor function is again called to take care of the copy. In summary, if an object is passed to a function, your constructor function is only called once. The destructor function, however, is called for both the copy constructor built object and the original object.

Potential Problem When Passing Objects

If dynamic memory is allocated to an object (using the new operator), and that object is then passed to a function, problems can arise when the copy of the object is destroyed. If both the original destructor function and the copy of the object deallocate the same chunk of dynamic memory, the original object will be rendered useless.

Therefore it is extremely important that if you delete memory in your destructor that you also write your own copy constructor and either re-allocate the memory so that the pointer points to a different memory location or assign any pointers to NULL to prevent runtime errors.

You can also avoid this problem by having a reference passed to the function. Using the "call-by-reference" method of object passing does not create a copy of the object -- it uses the existing object. Therefore, the destructor is not called when the function is exited.

Returning Objects

Just as an object can be passed to a function, an object can be returned by a function. Simply declare the function with a specific return class, and use the return statement as usual.

However, the same potential problem exists when returning an object as exists when passing an object. A temporary copy of the object is created during the execution of the function. When that temporary copy is destroyed as the function ends, the object is often rendered useless as its dynamic memory is deallocated. (This is only a risk when pointers have been created using the new operator.) Sometimes it is possible to return a pointer or a reference to prevent this problem. However, this tactic doesn't always work. We will examine the full solution to this problem later in this lesson.

Copy Constructors

One way of dealing with the problems mentioned in the previous section is by creating a copy constructor. The main problem, as we saw in the previous section, is that a bitwise copy of an object is made each time it is passed to, or returned by, a function by the implicit copy constructor. This means that the new object is an exact duplicate of the original, including the memory addresses pointed to by any member pointers. Now, when an object is used as an argument, the same memory is allocated, when constructed, for both the original and copied object, and subsequently freed when the object is destroyed. When this happens, the local copy of the object inside the function will free the same memory when its destructor is called. This will leave the original object damaged and effectively useless. We'll now work on solving this recurring problem.

Creating and Using a Copy Constructor

There are two situations that allow the value of one object to be given to another assignment and initialization. The two problem situations presented above can occur with the initialization process. Value transfer can also occur in the declaration statement if an object is initialized by another object. The copy constructor applies only to these initialization situations, not the actual assignment. You should write your own overloaded assignment operator each time you write a copy constructor, since, like the copy constructor, one will automatically be generated for you if you don't do it yourself. Later in this lesson, you will learn how to write overloaded operators. A copy constructor looks like this:

classname (const classname &objectname) {
    // . . .
}

The objectname is the reference to the object that is being used to initialize a second object. Within this function, you will write code to safely create a new instance of the object. New memory must be allocated, variables copied, and any initialization done. Keep in mind that this function will be called instead of the normal constructor or the compiler's native copy functions, so everything must be done manually.

Copy Constructors and Parameters

The following sample program uses a copy constructor and an object as the parameter for another object. When the object is passed to the function, the copy constructor is used to make a copy of the object. The copy constructor copies -- to another location in the memory -- the information that is referenced by the original object. Therefore, when the copy is destroyed and the memory is deallocated, the safety of the original object is not compromised:

#include 
#include 
using namespace std;
class samp_class {
    int *p;
public:
    samp_class(int j);
    samp_class(const samp_class &ob1);
    ~samp_class();
    int returnval() {return *p;}
};
// display() prototype
void display(samp_class ob1);
// Here is the normal constructor.
samp_class::samp_class(int j)
{
    cout << "Normal constructor called.\n";
    p = new int;
    if (!p) {
        cout << "Out of memory. Allocation failed.\n";
        exit(1);
    }
    *p = j;
}
// This is the copy constructor.
samp_class::samp_class(const samp_class &ob2)
{
    p = new int;
    if (!p) {
        cout << "Out of memory. Allocation failed.\n";
        exit(1);
    }
// copying the VALUE of the foreign pointer into the new pointer
    *p = *ob2.p;
    cout << "Copy constructor called.\n";
}
// This is the destructor
samp_class::~samp_class()
{
    cout << "Destructor called.\n";
    delete p;
}
void display(samp_class ob1)
{
    cout << ob1.returnval() << '\n';
}
int main()
{
    samp_class x(55);
    display(x);
    return 0;
}

The output from this program looks like this:

Normal constructor called.
Copy constructor called.
55
Destructor called.
Destructor called.

Again, go through this program and make sure you see how the output is derived.

Copy Constructors and Initializations

We will now look at a program that shows how one object is used to initialize another.

#include 
#include 
using namespace std;
class samp_class {
    int *p;
public:
    samp_class(int j);
    samp_class(const samp_class &ob1);
    ~samp_class();
    int returnval() {return *p;}
};
// Here is the normal constructor.
samp_class::samp_class(int j)
{
    cout << "Normal constructor called.\n";
    p = new int;
    if (!p) {
        cout << "Out of memory. Allocation failed.\n";
        exit(1);
    }
    *p = j;
}
// This is the copy constructor.
samp_class::samp_class(const samp_class &ob2)
{
    p = new int;
    if (!p) {
        cout << "Out of memory. Allocation failed.\n";
        exit(1);
    }
    *p = *ob2.p;
    cout << "Copy constructor called.\n";
}
samp_class::~samp_class()
{
    cout << "Destructor called.\n";
    delete p; [online text has an extra char here]
}
int main()
{
    samp_class x(55);
    samp_class y = x;
    return 0;
}

The output from this program looks like this:

Normal constructor called.
Copy constructor called.
Destructor called.
Destructor called.

Using Copy Constructors When an Object is Returned

The third type of initialization we discussed is that of a function returning an object:

#include 
using namespace std;
class samp_class {
public:
    samp_class() {cout << "Normal constructor called.\n";}
    samp_class(const samp_class &ob1) {cout << "Copy constructor called.\n";}
};
samp_class funct()
{
    samp_class ob2;
    return ob2;
}
int main()
{
samp_class x;
x = funct();
return 0;
}

The output from this program looks like this:

Normal constructor called.
Normal constructor called.
Copy constructor called.

The three sample programs seen in this section make the copy constructor seem like a waste of time. Although its use is not technically required here, as we continue, its use will become necessary. Be sure you understand its uses in each of the situations above.

Operator Overloading Using Member Functions

An operator (for example: +, ++, -, --) can be overloaded, meaning that it can perform special operations relative to the classes that you create. More specifically, when you overload either a function or an operator, you are forcing expanded capability, or functionality, to the same function or operator. For example, the operand "+" is usually thought of as the operator that adds two numbers together, right? In order for this to happen, the C++ compiler looks at the variable before and after the "+" operator. If both variables are a numeric type (int, float, double), then the compiler knows to perform mathematical addition on the two variables. However, the "+" operator can also be used to add two strings of text together (called concatenation). How does the compiler know to use the "+" operator to concatenate two variables instead of perform mathematical addition? Because the two variables involved are of type char. Therefore, the compiler recognizes that two char variables precede and follow the "+" operator, so it knows to perform a concatenation operation instead of mathematical operation. The compiler is able to make the distinction because of the number and type of the data involved!

You overload operators by creating operator functions. An operation function defines the operations that the overloaded operator will perform relative to the class upon which it will work. Operator functions can be either member or nonmembers of a class. However, they are usually always member functions of the class they operate on. When an operator function is a nonmember of a class, they are almost always friend functions of the class (we'll talk about friend functions in the next section). In order to specify which operator you want to overload, you would perform the following declaration:

ret-type class-name::operator#(arg-list)
{
    //operations
}
  • ret-type can be any valid type; however, ret-type usually returns an object of the class they are operating on.
  • class-name is the name of the class the operator function is operating on.
  • When you are creating an operator function, substitute the operator for the #. For example, if you are overloading the + operator, then use operator+.

Let's look at the following sample program to see how this works:

#include 
using namespace std;
class threenums {
    int a, b, c;
public:
    threenums() {a=b=c=0;}
    threenums(int x, int y, int z) {a=x; b=y; c=z;}
    threenums operator+(threenums o2);
    threenums operator=(threenums o2);
    void display();
};
threenums threenums::operator+(threenums o2)
{
    threenums num;
    num.a = a + o2.a;
    num.b = b + o2.b;
    num.c = c + o2.c;
    return num;
}
threenums threenums::operator=(threenums o2)
{
    a = o2.a;
    b = o2.b;
    c = o2.c;
    return *this;
}
void threenums::display()
{
    cout << a << ", ";
    cout << b << ", ";
    cout << c << '\n';
}
int main()
{
    threenums x(5, 10, 15), y(10, 15, 20), z;
    x.display();
    y.display();
    z = x + y;
    z.display();
    z = x + y + z;
    z.display();
    z = x = y;
    x.display();
    y.display();
    z.display();
    return 0;
}

Let's take a look at the output from this program:

5, 10, 15
10, 15, 20
15, 25, 35
30, 50, 70
10, 15, 20
10, 15, 20
10, 15, 20

Again, study this program and make sure you follow what is happening. The + and = operators are overloaded; they perform multiple tasks when used in conjunction with multiple members of a class.

Unary Operator Overloading Using Member Functions

Let's look at a variation of the above program that overloads the unary operator ++.

#include 
using namespace std;
class threenums {
    int a, b, c;
public:
    threenums() {a=b=c=0;}
    threenums(int x, int y, int z) {a=x; b=y; c=z;}
    threenums operator+(threenums o2);
    threenums operator=(threenums o2);
    threenums operator++();
    void display();
};
threenums threenums::operator+(threenums o2)
{
    threenums num;
    num.a = a + o2.a;
    num.b = b + o2.b;
    num.c = c + o2.c;
    return num;
}
threenums threenums::operator=(threenums o2)
{
    a = o2.a;
    b = o2.b;
    c = o2.c;
    return *this;
}
threenums threenums::operator++()
{
    a++;
    b++;
    c++;
    return *this;
}
void threenums::display()
{
    cout << a << ", ";
    cout << b << ", ";
    cout << c << '\n';
}
int main()
{
    threenums x(5, 10, 15), y(10, 15, 20), z;
    x.display();
    y.display();
    z = x + y;
    z.display();
    z = x + y + z;
    z.display();
    z = x = y;
    x.display();
    y.display();
    z.display();
    ++z;
    z.display();
    return 0;
}

Let's take a look at the output from this program:

5, 10, 15
10, 15, 20
15, 25, 35
30, 50, 70
10, 15, 20
10, 15, 20
10, 15, 20
11, 16, 21

Operator Overloading Restrictions

When using overloaded operators, do not deviate much from their intended function. As you can see in the examples above, the +, =, ++, and -- do what you would expect them to do. Overloaded functions and operators can do anything, but it's smart to avoid confusing what these functions do.

The only operators that cannot be overloaded are: . ? * ::

When overloading an operator, the operator's precedence cannot be altered. Also, the number of operands required by the operator cannot be changed. Most operator functions cannot have default arguments. (We will look at the one exception later in this lesson.)

Friend Operator Functions

An operator function can be either a "friend" function or a "member" function of a class. A friend function has access to all private and protected members of a class for which it is a friend. A friend function is declared using a prototype within the class, preceded by the keyword friend. The following operators cannot be overloaded using friend functions: = () [] -

Another Way to Overload

We will now revisit the threenum from the previous section and overload the + operator using a friend function.

#include 
using namespace std;
class threenums {
    int a, b, c;
public:
    threenums() {a=b=c=0;}
    threenums(int x, int y, int z) {a=x; b=y; c=z;}
    friend threenums operator+(threenums o1, threenums o2);
    threenums operator=(threenums o2);
    void display();
};
threenums operator+(threenums o1, threenums o2)
{
    threenums num;
    num.a = o1.a + o2.a;
    num.b = o1.b + o2.b;
    num.c = o1.c + o2.c;
    return num;
}
threenums threenums::operator=(threenums o2)
{
    a = o2.a;
    b = o2.b;
    c = o2.c;
    return *this;
}
void threenums::display()
{
    cout << a << ", ";
    cout << b << ", ";
    cout << c << '\n';
}
int main()
{
    threenums x(5, 10, 15), y(10, 15, 20), z;
    x.display();
    y.display();
    z = x + y;
    z.display();
    z = x + y + z;
    z.display();
    z = x = y;
    x.display();
    y.display();
    z.display();
    return 0;
}

The output from this program will be identical to the output from our original program with the threenums class. The difference here is that both operands are now passed to the + operator function. The left operand is passed in o1 and the right operator in o2.

Using a Friend Function to Overload a Unary Operator

Overloading a unary operator, such as the ++ operator, with a friend function is no easy task. The problem is that friend functions do not receive a this pointer, and therefore cannot access the object that invoked the function.

For a friend function to overload the increment or decrement operator, an object must be passed to the function as a reference parameter. The prefix and postfix forms of these operators are different; the postfix form takes two parameters, one being an integer that is not used. This is illustrated in the following program:

#include 
using namespace std;
class threenums {
    int a, b, c;
public:
    threenums() {a=b=c=0;}
    threenums(int x, int y, int z) {a=x; b=y; c=z;}
    friend threenums operator++(threenums &o1);
    friend threenums operator++(threenums &o1, int useless);
    void display();
};
threenums operator++(threenums &o1)
{
    o1.a++;
    o1.b++;
    o1.c++;
    return o1;
}
threenums operator++(threenums &o1, int useless)
{
    useless = 0;
    threenums temp = o1;
    o1.a++;
    o1.b++;
    o1.c++;
    return temp;
}
void threenums::display()
{
    cout << a << ", ";
    cout << b << ", ";
    cout << c << '\n';
}
int main()
{
    threenums x(5, 10, 15), y(10, 15, 20), z(15, 20, 25);
    x.display();
    y.display();
    z.display();
    x++;
    x.display();
    ++y;
    y.display();
    x = ++z;
    x.display();
    z.display();
    x = ++z;
    x.display();
    z.display();
    return 0;
}

Let's take a look at the output from this program:

5, 10, 15
10, 15, 20
15, 20, 25
6, 11, 16
11, 16, 21
16, 21, 26
16, 21, 26
17, 22, 27
17, 22, 27

Friend Classes

Occasionally, you will come upon a circumstance where one class needs access to the members of another class. You can allow this by specifying, in the definition of one class, that other classes are its friends. The syntax for this is:

friend class class_name;

This code can be included in any section of the class declaration. No special modification needs to be made to the friend class -- it will be given permission to access all the private members it needs. Consult your textbook for more information on friend classes.

Other Overloading Considerations

We will now look at other operators and how they can be overloaded. We will begin with the [ ] operator, continue with the ( ) operator, and then discuss general operator overloading.

Overloading the Array Subscripting Operator

When overloaded, the [ ] operator is treated as a binary operator -- that is, it requires one argument. It can only be overloaded relative to a class and can be overloaded only by a member function of that class. Also, and most importantly, an overloaded array scripting operator cannot be a nonstatic member function: it cannot be a friend function. Look at the sample program:

#include 
using namespace std;
const int arrsz = 4;
class samp_class {
    int x[arrsz];
public:
    samp_class() {
        register int j;
        for(j=0; j
int main()
{
    samp_class o1;
    cout << o1[3];
    return 0;
}

This program will output the number 3.

Overloading the Function Call Operator

Let's take a look at a program with a twonums class that overloads the () operator:

#include 
using namespace std;
class twonums {
    int a, b;
public:
    twonums() {a=b=0;}
    twonums(int x, int y) {a=x; b=y;}
    twonums operator()(int x, int y);
    void display();
};
twonums twonums::operator()(int x, int y)
{
    twonums num;
    num.a = a * x;
    num.b = b * y;
    return num;
}
void twonums::display()
{
    cout << a << ", ";
    cout << b << '\n';
}
int main()
{
    twonums o1(5, 10), o2;
    o2 = o1(2, 3);
    o1.display();
    o2.display();
    return 0;
}

Let's take a look at the output from this program:

5, 10
10, 30

When overloading a function call operator, any parameters and any return type may be used to satisfy the needs of your program.

Pretty Easy, Huh?

Part of your assignment for this lesson will be to create a program that overloads an operator not discussed in this lesson.

There was a lot here today! Are you still with me? Make sure you understand where we've been before we move into the next lessons! Next time, we'll discuss the concept of inheritance and explore virtual functions. Keep on truckin'!

Next Lesson

Copyright © 1998-2007, Games++ All rights reserved. | Privacy Policy