Previous Lecture Lecture 10 Next Lecture

Lecture 10, Tue 05/05

Inheritance and Polymorphism (cont.)

Polymorphism (Dynamic Binding) - Read PS 15.3

Example: Adding a toString function to both Person and Student

class Person {
public:
...
	std::string toString();
...
};

string Person::toString() {
	return "Person - Name: " + name + ", age: " + to_string(age);
}

class Student {
public:
...
	std::string toString();
...
};

string Student::toString() {
    return "Student - name: " + getName() + ", age: " +
      to_string(getAge()) + ", " + to_string(studentId);
}

// main.cpp
Person p1("Jacob", 30);
cout << p1.toString() << endl;

Student s1("John Doe", 21, 12345678);
cout << s1.toString() << endl;

Person* p2 = new Person("Someone", 18);
cout << p2->toString() << endl;

Student* s2 = new Student("Jane Doe", 19, 23456789);
cout << s2->toString() << endl;

Person* p3 = new Student("Person Student", 21, 11111111);
cout << p3->toString() << endl;//uses Person::toString(). Not polymorphic

The above example uses Person’s toString function instead of Student’s even though p3 points to a Student object.

// add the keyword "virtual" to the toString functions
class Person {
public:
	virtual std::string toString();
...
};

class Student {
public:
	virtual std::string toString();
...
};

Notes:

Example of Object slicing

Person p4 = Student("Person Student 2", 21, 1000000);
cout << p4.toString() << endl; // which toString is called?

Example of Polymorphism with Objects on the Stack

void f1(Person& p) {
    cout << p.toString() << endl;
}
int main () {
    Student r = Student("Jacob", 21, 1234567);
    cout << r.toString() << endl; // Calls Student::toString

    Person s = Student("T", 12, 1111112);
    cout << s.toString() << endl; // calls Person::toString() (object sliced)

    Person* t = new Student("R", 10, 654321);
    cout << t->toString() << endl; // calls Student::toString (polymorphic)

    f1(r); // calls Student::toString()

    f1(s); // calls Person::toString() since a person is constructed in memory

    f1(*t); // calls Student::toString() since a student is constructed on heap
}

Inheritance and Memory Layout Example

class X {
    public:
    X() { a = 10; b = 20; }
    short a;    // change this to int
    int b;      // change this to short
                // observe how class Y is memory padded
};

class Y : public X {
    public:
    Y() : X() { c = 30; d = 40; }
    short c;
    short d;
};

int main() {
    X x;
    Y y;
    X z = y;

    cout << "&x = " << &x << endl;
    cout << "&x.a = " << &x.a << endl;
    cout << "&x.b = " << &x.b << endl;
    cout << "---" << endl;
    cout << "&y = " << &y << endl;
    cout << "&y.a = " << &y.a << endl;
    cout << "&y.b = " << &y.b << endl;
    cout << "&y.c = " << &y.c << endl;
    cout << "&y.d = " << &y.d << endl;
    cout << "---" << endl;
    cout << "&z = " << &z << endl;
    cout << "&z.a = " << &z.a << endl;
    cout << "&z.b = " << &z.b << endl;

    return 0;
}

Pure Virtual Functions and Abstract Classes

There are times where it doesn’t make sense for a base class to have a virtual function.

Example

class Shape {
public:
	virtual double area();
};

Example (defining a pure virtual function)

class Shape {
public:
	virtual double area() = 0; // pure virtual function
};

You can have references or pointers to abstract classes (i.e. Shape* s is legal and can point to objects of any child class).

class Rectangle : public Shape {
public:
	Rectangle() {}
	Rectangle(double length, double width);
	virtual double area();
private:
	double length;
	double width;
};

Rectangle::Rectangle(double length, double width) {
	this->length = length;
	this->width = width;
}

double Rectangle::area() {
	cout << "In Rectangle::area()" << endl;
	return length * width;
}

class Square : public Shape {
public:
	Square(double side);
	virtual double area();
private:
	double side;
};

Square::Square(double side) {
	this->side = side;
}

double Square::area() {
	cout << "In Square::area()" << endl;
	return side * side;
}

Array of Pointers

Shape shapes[2]; // ERROR, tries to construct a shape object.
Shape* shapes[2]; // OK

shapes[0] = new Square(10);
shapes[1] = new Rectangle(3, 4);

cout << shapes[0]->area() << endl;
cout << shapes[1]->area() << endl;

The correct area() function will be called on a sub class object if that object is pointed to by Shape* even if Shape does not have an area() definition.

Example with various scenarios

	Rectangle* r = new Rectangle[100]; // Rectangle array on the heap
	//r[0] = new Rectangle(2,2); // ERROR, expects objects, not pointers
	r[0] = Rectangle(2,2); // OK, r[0] is an object

	//Rectangle* r1 = new Rectangle*[100]; // ERROR
	Rectangle** r1 = new Rectangle*[100]; // OK, r1 is a pointer to an array of pointers
	r1[0] = new Rectangle(2,3); // r1[0] is a pointer
	cout << r1[0]->area() << endl;

	Shape* s = new Rectangle(5,5);
	cout << s->area() << endl; // Works as expected.

	Shape* s1[100]; // Array declared on the stack
	s1[0] = new Rectangle(6,6);
	cout << s1[0]->area() << endl; // Works as expected.

	delete [] r;	// OK, deletes the array of Rectangles on heap
	delete [] r1;
	// OK, deletes array of Rectangle pointers on heap
	// Be careful, Rectangle objects that elements in r1
	// point to may still be on the heap.
	// You may need to manually delete these as well.

	delete s1[0];
	// OK, deleting a Rectangle object on the heap
	// may receive a warning if Shape does not have a virtual destructor

	//delete [] s1;	// ERROR. Trying to delete something on the stack.