Classes continued


The following topics are covered in this section:


Scope Resolution Operator (::)

In the earlier section on classes, we have defined the member functions within the class itself. Hence there was no need to declare the function. But is it possible to define the member function of a class outside the class?

We have already discussed inline functions. Even in classes you can explicitly declare functions to be inline using the ‘inline’ keyword.

Remember: When you declare and define a function within the class it is made an inline function. If you define a function outside a class (using scope resolution operator), it will be treated like a normal function (not inline).

Usually when using classes, any function that is just one or two lines long will be defined within the class itself. Longer functions are defined outside the class.

Member functions can be defined outside the class by using the scope resolution operator. Using this we can declare a function within the class and define it outside the class.

Let’s take a look at the syntax:

return-data-type name-of-class-to-which-it-belongs :: function-name (parameters)

Consider the example below:

class Timer
{

private:
    int count;

public:
void display( ); //declared inside the class
void increment( );
Timer( );
};

//Function defined outside the class using scope resolution operator

void Timer::display( )
{
    cout<<"remaining:"<<count;
}

void Timer::increment( )
{
    count=count+100;
}

Timer::Timer( )
{
    cout<<"timer!";
    count=0;
}

int main( )
{
Timer t1;
t1.display( );
t1.increment( );
t1.display( );
return 0;
}

Everything in the above program is same except that we have defined all the member functions outside the class. For this purpose we use the scope resolution operator to tell the compiler to which class the function belongs to. The statement

void Timer::display( )

tells the compiler that the function ‘display’ belongs to the class ‘Timer’ and has no return data type (void). Similarly we have used the scope resolution operator to define the function increment( ). In this example we’ve also defined the constructor outside the class:

Timer::Timer( )
{
cout<<"timer!";
count=0;
}

Since a constructor cannot return any value we don’t specify any return data type (not even void should be used as return data type).

Remember: Though the functions are defined outside the class, they still belong to the class. The scope resolution operator is used to say that this function belongs to this class.

Programmers will usually define functions outside the class because they want to separate the implementation from the interface. When a programmer creates a class and supplies it for others to use he may not give away the implementation details to the public. Instead he’ll only provide the users with the interface and declaring functions outside a class aids a programmer in achieving this. We’ll discuss how this can be achieved later.


Destructor:

When an object is created the constructor is invoked (or called). What happens when an object is no longer needed? The complement of the constructor is called. As the name implies, the destructor will destroy the object that was created. For a constructor we use the same name as the class name and for a destructor also we will use the same class name. But to differentiate between the constructor and destructor, C++ makes use of the tilde (~) symbol for the destructor.

class class-name
{

class-name( ) //constructor
{ }

~class-name ( ) //destructor
{ }

};

The important features of a destructor are:

  • A class can have only one destructor function.
  • Destructor cannot have any arguments.
  • It cannot return anything.
  • If a destructor is not explicitly specified by the programmer, there is no problem. In some cases a destructor needs to be specified.

  • So, what are the cases where we need to bother about the destructor? The most common example is when you have allocated some memory using the ‘new’ operator. Memory allocated using ‘new’ should always be freed using ‘delete’ before the termination of the program. Thus in this case we will specify the appropriate ‘delete’ statement in the destructor to ensure that the memory is freed up.

    Let’s see a simple program to understand a destructor:

    #include <iostream.h>

    class car
    {
    private:
        int speed;
        char color[20];

    public:
    car( ) //Constructor
    {
    cout<<"\nCreating a Car";
    }

    ~car( ) //Destructor
    {
    cout<<"\nDestroying the Car";
    }

    };        //End of class declaration

    int main( )
    {
    car mine;
    return 0;
    }

    The output would be:

    Creating a Car
    Destroying the Car


    Dynamically creating objects:

    In the chapter on pointers we discussed how we can allocate memory dynamically. This concept can be extended to allocate memory for user-defined data types also (like objects).

    C programmers would be familiar with the dynamic allocation operators ‘malloc’ and ‘free’ (the C++ equivalent are ‘new’ and ‘delete’). One of the most significant features of ‘new’ and ‘delete’ is that these operators are aware of constructors and destructors. Let’s try out a simple example:

    class car
    {

    private:
        int speed;
        char color[20];

    public:
    car( ) //Constructor
    {
    cout<<"a Car";
    }

    ~car( ) //Destructor
    {
    cout<<"the Car";
    }

    };

    int main( )
    {
    car *mycar=new car;
    delete mycar;
    return 0;
    }

    The output will be:

    Creating a Car
    Destroying the Car

    As you can deduce from the output, creating an object using ‘new’ ensures that the constructor for that object is called. Similarly delete calls the destructor of the object. This won’t happen if we were to use ‘malloc’ and ‘free’.


    Objects in Functions:

    Objects can be used in functions just like other data types are used. Objects can be passed as argument to a function and an object can be returned from the function. These two topics will be dealt below separately.

    Passing Objects to Functions:

    An object can be passed to a function as shown in the program below:

    #include <iostream.h>

    class rect
    {
    private:
        int length, breadth;

    public:
    rect(int x, int y)
    {
    cout<<endl<<"New rectangle created";
    length=x;
    breadth=y;
    }

    void area( )
    {
    cout<<endl<<"The area of the rectangle is : "<<length*breadth;
    }

    ~rect( )
    {
    cout<<endl<<"Rectangle destroyed";
    }

    };

    void calculate(rect r) //object used as parameter
    {
    r.area( );
    }

    int main( )
    {
    rect ob(5,4);
    calculate(ob);
    return 0;
    }

    The output will be:

    New rectangle created
    The area of the rectangle is : 20
    Rectangle destroyed
    Rectangle destroyed

    ‘ob’ is an object of type ‘rect’. It is initialized using the parameterized constructor. The function calculate can accept arguments of type ‘rect’ (i.e. it can be passed ‘rect’ objects). We have created only one object (ob) but the destructor has been executed twice; why?

    When we say:

    calculate(ob);

    ‘ob’ will be passed by value to the function ‘calculate’ (i.e. the function will not directly work on the object ‘ob’. It will only work on a copy of the object ‘ob’). Since it works on a copy of the object, a temporary object of type ‘rect’ will be created when calculate ( ) function is executed. When the program exits the calculate ( ) function, the temporary object has to be destroyed and this is why the destructor has been invoked twice in the above program (once for destroying the temporary object and once for destroying the object ‘ob’).

    You might wonder why the constructor is also not executed twice? When a temporary object is created the constructor is not invoked. When a temporary object is created the program wants to maintain the state of the temporary object exactly the same as the original object (i.e. the member data values of the original and temporary object should be the same). Constructors are usually used for initializing and if the temporary object gets initialized then the values in the temporary object will be different from the original. So the constructor is not executed for a temporary object creation.

    This topic is discussed again in the “Copy Constructors” section.

    Returning Objects from Functions:

    What happens while passing an object to a function will happen even when you return an object from a function (i.e. a temporary object is created while returning an object also).

    class rect
    {
    private:
        int length, breadth;

    public:
    rect(int x, int y)
    {
    cout<<endl<<"New rectangle created";
    length=x;
    breadth=y;
    }

    void area( )
    {
    cout<<endl<<"The area of the rectangle is : "<<length*breadth;
    }

    ~rect( )
    {
    cout<<endl<<"Rectangle destroyed";
    }

    };

    rect dummy_func( )
    {
    rect r(5,5);
    return r;
    }

    int main( )
    {
    rect ob(9,9);
    ob=dummy_func( );
    ob.area( );
    return 0;
    }

    The output is:

    New rectangle created
    New rectangle created
    Rectangle destroyed
    Rectangle destroyed
    The area of the rectangle is : 25
    Rectangle destroyed

    The header of dummy_func( ) is:

    rect dummy_func( )

    This signifies that dummy_func( ) is a function which has no parameters and returns an object of type ‘rect’.

    Based on the output we can say that the destructor is invoked one extra time (just as it was done when we passed an object to a function). In this case the destructor is invoked due to the statement:

    ob=dummy_func( );

    While returning an object the program will create a temporary object (which gets destroyed at the end). This extra temporary object can create problems if you make use of dynamic memory allocation.

    We’ll revisit this topic after we learn copy constructors.


    Initializing an object using an existing object:

    In fundamental data types, the following is possible:

    int age1 = 10;
    int age2 = age1;

    We’ve used one variable to initialize another. The same is possible with objects also but care has to be taken. Consider the following example:

    #include <iostream.h>

    class rect
    {

    private:
        int length, breadth;

    public:
    rect(int x, int y);
    void area( )
    {
        cout<<endl<<"The area of the rectangle is : "<<length*breadth;
    }

    ~rect( )
    {
        cout<<endl<<"Rectangle destroyed";
    }

    };

    //Constructor can be defined outside the class using scope resolution operator

    rect::rect(int x, int y)
    {
    cout<<endl<<"New rectangle created";
    length=x;
    breadth=y;
    }

    int main( )
    {
    rect ob1(10,9);
    rect ob2=ob1;
    rect ob3(ob1);
    ob1.area( );
    ob2.area( );
    ob3.area( );
    return 0;
    }

    The output will be:

    New rectangle created
    The area of the rectangle is : 90
    The area of the rectangle is : 90
    The area of the rectangle is : 90
    Rectangle destroyed
    Rectangle destroyed
    Rectangle destroyed

    Quite interesting! We’ve created 3 objects and we have 3 destructors but only one constructor! The code:

    rect ob1(10,9);

    works normally. This would create an object called ‘ob1’ and it would invoke the constructor (and finally a destructor). The problem lies in the following 2 statements:

    rect ob2=ob1;
    rect ob3(ob1);

    Something unexpected happens here. When trying to initialize an object using another object, C++ uses its own assignment operator. You might ask, “We’ve only used one constructor in our code. Where is this copy constructor?”. The compiler provides a default copy constructor when we don’t provide one. In the above example this doesn’t create much of a problem because our destructor doesn’t do anything useful (and if we never defined a destructor we would never have realized the existence of the copy constructor). But there are cases where the destructor is defined to perform some important clean up work and in these situations the default copy constructor can create havoc in your code. We’ll discuss this later in this chapter.


    Go back to the Contents Page


    Copyright © 2005 Sethu Subramanian All rights reserved.