Object Oriented Programming: Lecture Notes 10
Author: A. El-Gadi/Faculty of Computer Engineering/Tripoli University
We have seen how, that by default, the default constructor of the base class is used to construct the base part of the derived class this behavior can be overridden to force the base part of the derived class to be constructed using one of the overloaded versions of the base class constructors. This allows us to control how the initialization is to be carried out and in certain situations, such as when there the base class has private members it can be the only way to assign those values. To override the default behavior of the construction, initialization lists are used.
We have seen how, that by default, the default constructor of the base class is used to construct the base part of the derived class this behavior can be overridden to force the base part of the derived class to be constructed using one of the overloaded versions of the base class constructors. This allows us to control how the initialization is to be carried out and in certain situations, such as when there the base class has private members it can be the only way to assign those values. To override the default behavior of the construction, initialization lists are used.
Initialization lists
An initialization list is a syntax that got introduced in C++ and its task is to initialize members of the class in a way different from simple assignment during construction time, we will talk about the difference later, but for the time being let us see the syntax so that we can anchor our discussion in a concrete piece of code. The following class uses familiar assignment to set the values of the data.
class AA{public:
int a;
double b;
A(){a=0; b=2.34;}
A(int aa,double bb){a=aa; b=bb;}
};
The following class is identical in every respect except that it use initialization lists instead of simple assignment.
class AA{public:
int a;
double b;
A():a(0), b(2.34){};
A(int aa,double bb):a(aa),b(bb){};
};
In the code above the member variable to left will take whatever value there is between the parentheses. A different way of achieving the above is the following.
class AA{public:
int a;
double b;
A():A(0,2.34){};
A(int aa,double bb):a(aa),b(bb){};
};
We can list a constructor in the initialization list to make it construct the object using some specific values we wish the member variables to hold. This feature is what allows us to override the construction behavior of the base part of a derived class in the case of inheritance, because we can list one of the base class constructors in the initialization list of a derived class constructor.
class Base{public:
int a;
double b;
Base(){};
Base(int aa, double bb){a=aa; b=bb;}
};
class Derived:public Base{public:
int x;
double y;
Derived():Base(10,0.1){};
Derived(int xx, double yy):Base(2*xx,2*yy),x(xx){y=b*yy;}
};
The difference in behavior that we mentioned earlier lies in that assignment created the variable and then changes its value using the assignment operator =. In the case of initialization lists the values are set at the instant of creation which means that there is no assignment taking place. This has huge consequences, because, in the case of const member variables it is not allowed to change there values after creation. Therefore, the only opportunity to give them a meaningful value is at the moment of creation, and this can only be accomplished using initialization lists. The existence of private member variables of the base class is also another situation where an initialization list is the only possible way to give them meaningful values.
Polymorphism
Inheritance in OOP is the way to capture the type/subtype relationship. We have seen how inheritance makes public and protected member variables and member functions of a base class, available in its derived classes. This is sufficient enough in many situations except when the behavior of some member function of the base class is different when inherited in the derived class. Take for example the general behavior of walking. A human walks in a way that is different from an ostrich or a crocodile. Moreover, different subtypes of humans walk in different manners. For instance, a man walks differently from a woman. This fact can be captured in the OOP framework by overriding the base class function, as in the code below.
class Human{public:
double age;
double height;
void walk(){cout<<"Walk like a human.";}
};
class Man:public Human{public:
int xy;//Some man attribute.
void walk(){cout<<"Walk like a man.";}
};
class Woman:public Human{public:
int xx;//Some woman attribute.
void walk(){cout<<"Walk like a woman.";}
};
int main(){Human h1; Man m1; Woman w1;
h1.walk();//Output: Walk like a human.
m1.walk();//Output: Walk like a man.
w1.walk();//Output: Walk like a woman.
return 0;}
When we contemplate the reality of things, we realize that the attributes of a subtype include and often add to the attributes of its type. This means that an object of some type cannot fully represent an object of one of the subtypes of the type to which it belongs. This situation is called slicing in OOP, and corresponds to assigning objects of a subclass to objects of its super-class. Object slicing is indeed undesirable. Below is an example of object slicing.
class Human{public:
double age;
double height;
void walk(){cout<<"Walk like a human.";}
};
class Man:public Human{public:
int xy;//Some man attribute.
void walk(){cout<<"Walk like a man.";}
};
class Woman:public Human{public:
int xx;//Some woman attribute.
void walk(){cout<<"Walk like a woman.";}
};
int main(){Human h1; Man m1;
h1=m1;//Object sliced. xy is lost.
return 0;
}
However, we can refer to a man or a woman instance as human without contradiction. In other words we can point to an instance of a subclass using a pointer of type super-class. This is logical because, the fact that all men are human does not entail them losing their manhood and the fact that women are human does not entail them losing their womanhood. This is the rationale behind allowing the following syntax in OOP.
class Human{public:
double age;
double height;
void walk(){cout<<"Walk like a human.";}
};
class Man:public Human{public:
int xy;//Some man attribute.
void walk(){cout<<"Walk like a man.";}
};
class Woman:public Human{public:
int xx;//Some woman attribute.
void walk(){cout<<"Walk like a woman.";}
};
int main(){Human *ph1; Man m1;
ph1=&m1;//No object copying, so no slicing
return 0;
}
In the code above, we can point to a man using a pointer of type human. However, the attributes specific to the class Man are inaccessible through this pointer, and this is all too natural; because referring to a man as a human should preclude us from addressing the respects in which that human is a man. But there is a small problem; when we make a man walk using the walk function, a man walks like a human rather than like a man. This is similar to changing specific behavior of an individual when referred to using a more general type to which it belongs. In other words, when we pick out a man on the street by pointing a finger at him and say "this human", he does not, magically, change his walking behavior into a more general human gate. This is unrealistic and therefore undesirable; because we know that the human pointer is in actuality pointing to a man, who walks in a manner specific to men.
class Human{public:
double age;
double height;
void walk(){cout<<"Walk like a human.";}
};
class Man:public Human{public:
int xy;//Some man attribute.
void walk(){cout<<"Walk like a man.";}
};
class Woman:public Human{public:
int xx;//Some woman attribute.
void walk(){cout<<"Walk like a woman.";}
};
int main(){Human *ph1; Human *ph2; Man m1; Woman w1;
ph1=&m1;
ph2=&w1;
ph1->walk();//Output: Walk like a human.
ph2->walk();//Output: Walk like a human.
/*Despite the existence of member functions for specific Man and Woman walking behavior, both pointers use the general walking behavior of class Human.*/
return 0;
}
The more logical behavior is for a man is to walk like a man and a woman to walk like a woman, even when pointed to by a pointer of a more general type. This effect can be achieved by virtualizing the functions whose behavior should change according to the type/subtype of the object a pointer is pointing to. The syntactical solution is to prepend the function by the keyword virtual.
class Human{public:
double age;
double height;
virtual void walk(){cout<<"Walk like a human.";}
};
class Man:public Human{public:
int xy;//Some man attribute.
void walk(){cout<<"Walk like a man.";}
};
class Woman:public Human{public:
int xx;//Some woman attribute.
void walk(){cout<<"Walk like a woman.";}
};
int main(){Human *ph1; Human *ph2; Man m1; Woman w1;
ph1=&m1;
ph2=&w1;
ph1->walk();//Output: Walk like a man.
ph2->walk();//Output: Walk like a woman.
/*Both pointers use the specific Man and Woman walking behavior.*/
return 0;
}
Now that we know how to make a class behave in the desired way, we will have to go back, and see if there is any change that needs to be done to our classes when we have inherited them, and we will find that destructors need to be declared virtual if they are to behave in the required way. If the destructor is not declared virtual, then when destroying an object through it a super-class pointer only the part of the object that comes from the super class will be destroyed. The following code illustrates the problem:
class Human{public:
double age;
double height;
virtual void walk(){cout<<"Walk like a human.";}
};
class Man:public Human{public:
int xy;//Some man attribute.
void walk(){cout<<"Walk like a man.";}
};
int main(){Human *ph1;
ph1=new Man;
delete ph1; //Only the part of the object that comes from Human will be destroyed, leaving the rest of the object dangling in memory.
return 0;
}
The solution is to make the destructor virtual, so that the type, to which the pointer points, is checked and its version of the destructor is called.
class Human{public:
double age;
double height;
virtual void walk(){cout<<"Walk like a human.";}
virtual ~Human(){};
};
class Man:public Human{public:
int xy;//Some man attribute.
void walk(){cout<<"Walk like a man.";}
};
int main(){Human *ph1;
ph1=new Man;
delete ph1; //Since the destructor is virtual the type pointed to will be checked and its destructor will be used. Here the destructor of Man will be called, which in turn will call the destructor of human. leaving nothing of the object in memory.
return 0;
}