2. Class and Object-oriented Methodology

TODO: THIS WHOLE CHAPTER NEEDS OVERHAUL

../_images/road-works-sign.gif

class is a C++ keyword to define a class type, which is an extension to C’s struct. Similiar to struct type, after defining a class type, we can define a variable of that class type. This variable is called an instance or object of that class. From this point on, a whole bunch of theories and practices are developed under the name of object-oriented methodology. Encapsulation, inheritance, and polymorphism are the three most significant characteristics of object-oriented methodology. We will introduce these as well as related C++ syntax.

2.1. Encapsulation

When programming in C, we often encapsulate related variables and functions in a .c source file and call that a module. We declare those variables and functions interior to the module as static, which are invisible outside the module. We export public variables and functions, i.e. those declared without static, by putting their declarations in a .h header file. By including the header file, those public interfaces can be used from outside.

Take a look at the FILE * pointer from stdio.h, this reflects another way of modularization. The FILE * pointer is an opaque handler, which means FILE struct encapsulates some data members that the caller should never know about. All that can be done by the caller is to get the FILE * pointer from fopen and pass it to other library functions, letting those library functions maintain private data within FILE struct. The library functions know about private data in FILE struct because they all come from the same module. We will mimic this method in the following C code:

person.h

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#ifndef PERSON_H
#define PERSON_H

typedef void *HPERSON;

HPERSON person_create(const char *name, int age);
void person_display(const HPERSON p);
void person_delete(HPERSON p);

#endif /* PERSON_H */

person.c

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include "person.h"

typedef struct
{
     char *name;
     int age;
} person_t;

HPERSON person_create(const char *name, int age)
{
     printf("create person\n");
     person_t *p = malloc(sizeof(person_t));
     p->name = malloc(strlen(name)+1);

     strcpy(p->name, name);
     p->age = age;
     return p;
}

void person_display(const HPERSON p)
{
     printf("<<<< display person information >>>>\n");
     printf(" Name:%s\n Age:%d\n", ((person_t *)p)->name, ((person_t *)p)->age);
}

void person_delete(HPERSON p)
{
     printf("delete person\n");
     free(((person_t *)p)->name);
     free(p);
}

main.c

1
2
3
4
5
6
7
8
9
#include "person.h"

int main(void)
{
     HPERSON person = person_create("XiaoMing", 12);
     person_display(person);
     person_delete(person);
     return 0;
}

person.c is a module. It encapsulates some private data in person_t struct. All functions provided in this module have access to private members in person_t struct. They can initialize a person_t object, display it in a user-friendly way, or delete it. However, codes from outside can only keep and pass pointers to person_t objects, but can’t access private members. The trick is to expose the pointer to person_t as a void pointer type HPERSON, which cannot be dereferenced.

The above C code achieve the effect of encapsulation by modularization and some tricks, whereas C++ supports encapsulation by its syntax. Now we rewrite the code in C++. First, define a Person class in person.h:

person.h

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#ifndef PERSON_H
#define PERSON_H

class Person
{
public:
     Person(const char *name, int age);
     ~Person();
     void display() const;
private:
     char *name;
     int age;
};

#endif //PERSON_H

C++’s class extends C’s struct in two ways. First, C++’s class can have not only data members but also member functions. Second, class memebers can have access specifiers: private member can only be accessed by member functions of the same class, and public members can be accessed from outside. In the above example, name and age are two private members, they can only be accessed by Person(), ~Person(), and display(), but those member functions in turn are public members and can be accessed from main function or member functions of other classes. In other words, main function can only access data members in Person class through public member functions provided by it. This is essentially the same way we did in the above C code.

Note the semicolon ; at the end of the class definition, which is often omitted by C++ newbies. This follows the same syntax as C’s struct. In fact, class and struct have the same syntax in C++. C++’s struct can also have member functions and access specifiers. There’s only one little difference: if the access specifier is not explicitly specified, the default specifier for a class member is private, but for a struct member it is public. This syntax keeps compatible with C. If we compile C code with a C++ compiler, C’s struct is taken as C++’s struct with no member functions and with all data members public.

Now look at the implementation of Person class:

person.cpp

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>
#include <cstring>
#include "person.h"
using namespace std;

Person::Person(const char *name, int age)
  :name(NULL), age(age)
{
  cout<<"create person"<<endl;
  this->name = new char[strlen(name)+1];
  strcpy(this->name, name);
}

void Person::display() const
{
  cout<<"<<<< display person information >>>>"<<endl
      <<" Name:"<<name<<endl<<" Age:"<<age<<endl;
}

Person::~Person()
{
  cout<<"delete person"<<endl;
  delete[] name;
}

Member function names are under the namespace of their class. Therefore, the full name of member function display is Person::display, similar to person_display in the above C code. We can see from nm output what constitutes a member function name:

$ g++ -c person.cpp
$ nm person.o
......
00000200 T _ZNK6Person7displayEv
......

Member function names should not be prefixed by their class namespace when declared within class {...} declaration, but must be prefixed when declared outside. Person::Person and Person::~Person are two special member functions called constructor and destructor respectively. A constructor has the same name as its class. A destructor’s name is composed of a tilde ~ and its class name. These two functions have no return values. Note they do not return void, they have no return values or return types at all.

We will take a look at the main function first, and then see how it interacts with Person class.

main.cpp

1
2
3
4
5
6
7
8
9
#include "person.h"

int main()
{
  Person *person = new Person("XiaoMing", 12);
  person->display();
  delete person;
  return 0;
}

person points to an object allocated by new operator and initialized by () operator. We have seen this syntax before. But now we deal with a custom type instead of built-in type, and we pass two arguments. Actually we are calling a constructor called Person.

The caller:

1
Person *person = new Person("XiaoMing", 12);

The callee:

1
2
3
4
5
6
7
Person::Person(const char *name, int age)
  :name(NULL), age(age)
{
  cout<<"create person"<<endl;
  this->name = new char[strlen(name)+1];
  strcpy(this->name, name);
}

The :name(NULL), age(age) part in constructor is called initializer list. Member name is initialized to NULL, and member age is initialized to parameter age, which is 12. Then in the body of constructor a string is allocated to member name, and parameter name, which values "XiaoMing", is copied into the string pointed to by member name. After construction, the object looks like:

../_images/cppoo.personobject.png

Why didn’t I show any member functions in the object? In fact, each Person object has its own data members, but the three member functions are shared among all Person objects. Then how does a member function such as Person::display differentiate between object A and object B? It has to know who’s calling it so as to display the caller’s information. The C code above solves this problem by providing a parameter identifying the caller.

1
void person_display(const HPERSON p);

Person::display does have an implied parameter for identifying the caller. It is called this pointer. When we invoke a member function like person->display(), we actually pass person to Person::display as the implied parameter this. We can think of this invocation like Person::display(person). this pointer can be used in member functions to refer to the caller object. For example, in constructor we refer to member name by this->name, and refer to parameter name by name alone. In an unambiguous context, such as Person::display, this-> can be omitted, so we can refer to member name by name alone.

Note the keyword const at the end of the prototype of Person::display. That means the object pointed to by this pointer is readonly, thus inhibiting Person::display from modifying its data members.

The delete person; statement at the end of main function calls the destructor Person::~Person. We have allocated a string in the constructor, so the destructor is responsible for freeing it. If we do not do this in the destructor, and we new and delete many objects, the memory will be clumped with many strings with no pointer referencing them. This is called memory leak. A class can have many overloaded constructors, each with different parameter list, but can have only one destructor, because the destructor cannnot take any parameter.

2.2. const Members

Data members of a class can be qualified with const. const data members can be initialized with a constant in the initializer list of a constructor. From then on they cannot be modified. For example,

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class A
{
public:
        A(int size):SIZE(size){};
        const int SIZE;
};

int main()
{
        A a(100);
        A b(200);
        return 0;
}

Two objects a and b are allocated on the stack of main function. They will be automatically destructed when their lifetimes are over, i.e. when main function returns. a has a member SIZE initialized to 100, and b has a member SIZE initialized to 200. Once initialized, those const data members cannot be modified, even in the body of the constructor. Thus, we cannot write the constructor as

1
A(int size){ SIZE=size; };

Initialization and assignment are different. Remember the same principle holds when we initialize a const variable in C. We can write

1
const int i=10;

but not

1
2
const int i;
i=10;

Note in this example we implement the constructor within the class {...} declaration, rather than merely declare it first and implement later. Member functions directly implemented in class {...} declaration are taken as inline functions.

2.3. static Members

We know that each object of a class has its own copy of data members, but data members qualified with static are exceptional. static data members are shared by all objects of one class. They don’t belong to any specific object. There won’t be multiple copies of static data members. For each class there’s only one copy. For example,

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
using namespace std;

class A
{
public:
        static int i;
};

int A::i;

int main()
{
  A a, b;
  a.i = 10;
  b.i = 20;
  cout<<a.i<<endl<<b.i<<endl;
  A::i = 30;
  cout<<a.i<<endl<<b.i<<endl;
  return 0;
}

i is a static data member. Although there are two instances of class A, there’s only one instance of i. It can be accessed through either a.i or b.i, or even through A::i. i is not allocated with object a or b, it’s allocated and initialized when the program starts to run, in the same way as global variables. Note in the class {...} declaration we can only declare a static member, but not define it. We should define it in the global scope, just like we define a global variable.

static can also be used to qualify member functions. We know a member function is special in that it has an implied this parameter. But a static member function doesn’t have this parameter. Therefore it cannot access non-static data members through this pointer. In other words, static member functions can only access static data members, they are just another form of global functions and variables with a namespace prefix.

2.4. Overloaded Member Operators

We have seen that an overloaded operator is a special form of function. The same is true with an overloaded member operator. Then which operator should be implemented within a class as a member function, and which should be implemented outside? By convention, asymmetric operators such as += and -= should be implemented as member functions, while symmetric operators such as + and - should not. That’s only a convention, not a syntax restriction.

Let’s continue our example of Complex type. We overload += operator as a member function:

1
Complex& Complex::operator+=(const Complex&);

In an earlier section we overloaded + operator as:

1
Complex operator+(const Complex&, const Complex&);

Naturally it has two parameters, since it’s a binary operator. But the member operator += has only one parameter, where is the other? Remember a member function has an implied this parameter. That’s the left operand, while the only parameter is the right operand. There’s another difference. The + operator has a return value of type Complex, whereas the += operator has a return value of type Complex&. Think about it for a while.

Here is the full code.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <iostream>
using namespace std;

class Complex
{
        friend ostream& operator<<(ostream&, const Complex&);
public:
        Complex(double real, double img):real(real),img(img){}
        Complex& operator+=(const Complex& b)
        {
                real += b.real;
                img += b.img;
                return *this;
        }
private:
        double real,img;
};

ostream& operator<<(ostream& o, const Complex& a)
{
        o<<a.real<<'+'<<a.img<<'i';
        return o;
}

int main(void)
{
        Complex a(1,2), b(3,4);
        a += b;
        cout<<a<<endl;
        return 0;
}

We declare real and img as private members to protect them from being accessed outside. Then how to implement << operator? To print a Complex object, we must access its two private members. Besides, << operator cannot be implemented as a member of Complex class, because its left operand is an ostream object, not a Complex object. We introduce a new keyword friend, which can declare a function or class as a “friend” of another class and allow access to its private members.

2.5. The Big Three

Constructor, destructor and assignment operator are the most important functions of a class. They are usually referred to as the big three. The big three are special because if they are absent the compiler will automatically generate a synthesized version for the class.

For a class A, if there isn’t any constructor, the compiler will synthesize a default constructor with no parameter.

1
A(void);

If we define an object a of class A,

1
A a;

then the default constructor is called and all data members get allocated. Note if a has a data member of pointer type, only the pointer itself is allocated. To allocate the space pointed to by the pointer we have to implement our own constructor. As long as we provide at least one constructor for the class, whether it has any parameter or not, the compiler will not synthesize a default constructor. Note we can’t call a constructor with no parameter as:

1
A a();

This statement has ambiguity. We can also take it as a function declaration which is named a, has no parameter, and returns an object of type A. Therefore C++ syntax forbids this usage, although it makes good sense.

Copy constructor is a special constructor with the following form:

1
A(const A &a);

If a copy constructor is absent, the compiler will also provide a synthesized version for the class. A copy constructor is invoked in the following case.

1
2
3
A a;
A b = a;
A c(a);

Here the = isn’t an assignment operator, but an initializor. A b = a; is identical to A b(a);. In both cases the copy constructor is called with a parameter a. Therefore b and c are constructed with a as the role model. If the synthesized copy constructor is called, all data members of a will be copied to b and c. If A has a data member of pointer type, only the pointer itself is copied to b and c, rendering three pointers pointing to the same space. This behavior is called shallow copy. If we want the behavior of deep copy, that is, both the pointer and the space being pointed to are copied, we should implement our own version of copy constructor.

If a destructor is absent, the compiler will provide a synthesized version:

1
~A(void);

The destructor will be called when an object allocated by new operator is delete``ed, or when an object allocated on stack runs out of its duration. Global and ``static objects are constructed when the program starts and destructed when the program terminates. If the synthesized destructor is called, all data members are deallocated. If A has a data member of pointer type, only the pointer itself is deallocated, the space pointed to will be left unchanged, and probably with no pointer referencing it. This is called memory leak. We have to implement our own version of destructor to avoid this behavior.

Assignment operator has the form:

1
A &operator=(const A &a);

If an assignment operator is absent, the compiler will provide a synthesized version for the class. An assignment operator is invoked in the following case.

1
2
A a, b;
b = a;

b = a; is an assignment, not initialization. Note the difference between b = a; and A b = a;. If the synthesized assignment operator is called, all data members of a will be copied to b. If A has a data member of pointer type, only the pointer itself is copied to b, rendering two pointers pointing to the same space. If we want the behavior of deep copy, we should implement our own version of assignment operator.

To summarize, if a class has data members of pointer type, the behavior of compiler synthesized functions usually isn’t what we want. Therefore, to define a class with pointer members, we must implement the big three ourselves. Even if you define a class without pointer members, you’d better implement the big three to make sure everything works as you expect.

There’s no string type in C, strings can only be represented by char pointer or array. Now we implement a C++ string class, supporting operations such as getting length, concatenating, and printing. Once the class is done, codes using this class can be very tidy.

mystring.h

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#include <iostream>
using namespace std;

class MyString
{
friend ostream &operator<<(ostream &, const MyString &);
friend MyString operator+(const MyString &, const MyString &);
public:
        MyString(const char *);
        MyString(const MyString &);
        ~MyString(void);
        MyString &operator=(const MyString &);
        int length(void) const;
        bool isEmpty(void) const;
private:
        char *data;
        int len;
};

Because MyString class encapsulates a pointer member data, we must implement the big three ourselves. We overload + operator for string concatenation.

mystring.cpp

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
#include <iostream>
#include "mystring.h"
using namespace std;

MyString::MyString(const char *str)
{
  cout<<"ctor"<<endl;

  if(str==NULL) {
    len = 0;
    data = new char[1];
    *data = '\0';
  } else {
    len = strlen(str);
    data = new char[len+1];
    strcpy(data, str);
  }
}

MyString::MyString(const MyString &other)
{
  cout<<"copy ctor"<<endl;

  len = other.len;
  data = new char[len+1];
  strcpy(data, other.data);
}

MyString::~MyString(void)
{
  cout<<"dtor"<<endl;

  delete[] data;
}

MyString &MyString::operator=(const MyString &other)
{
  cout<<"assignment"<<endl;

  if(this==&other)
    return *this;

  delete[] data;
  len = other.len;
  data = new char[len+1];
  strcpy(data, other.data);
  return *this;
}

int MyString::length(void) const
{
  return len;
}

bool MyString::isEmpty(void) const
{
  return len==0;
}

ostream &operator<<(ostream &out, const MyString &str)
{
  out << str.data;
  return out;
}

MyString operator+(const MyString &a, const MyString &b)
{
  cout<<"concatenate"<<endl;

  MyString temp("");

  delete[] temp.data;
  temp.len = a.len + b.len;
  temp.data = new char[temp.len + 1];
  strcpy(temp.data, a.data);
  strcat(temp.data, b.data);

  return temp;
}

Note we do sanity check at the beginning of the assignment operator:

1
2
if (this == &other)
    return *this;

Think about it: What if we didn’t check it?

main.cpp

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>
#include "mystring.h"
using namespace std;

int main()
{
        MyString str("hello");
        MyString str1 = str;
        cout<<"Length of str1: "<<str1.length()<<endl;
        cout<<"Value of str1: "<<str1<<endl;

        MyString str2 = "";
        cout<<"Is str2 empty? "<<(str2.isEmpty()?"true":"false")<<endl;

        str2 = " world";
        cout<<"Now value of str2: "<<str2<<endl;

        cout<<"Now value of str: "<<str1+str2<<endl;
        cout<<"Value of another expression: "<<str1 + "_world"<<endl;
        cout<<"Value of yet another expression: "<<"Hello," + str2<<endl;

        MyString str3 = str1 + str2;
        cout<<"Value of str3: "<<str3<<endl;

        return 0;
}

The user code is really clean. No malloc, free, strlen, strcat any more. Perhaps much to your surprise, you can make such assignment:

1
str2 = " world";

The right operand of our assignment operator has type MyString&, not const char *. How can that be done? The compiler does an implicit conversion here, converting from const char * to MyString, based on our constructor. The steps are:

  1. Taking "world" as an argument, construct a temporary object.
  2. Taking the temporary object as the right operand, invoke the assigment operator.
  3. Destruct the temporary object.

Here’s a segment taken from the output, which verifies these steps:

...
ctor
assignment
dtor
Now value of str2:  world
...

This rule also applies to the code str1 + "_world". In a word, defining a constructor taking one parameter also implies an implicit conversion rule.

The statement MyString str3 = str1 + str2; also involves complicated steps:

  1. Taking str1 and str2 as operands, invoking overloaded operator +.
  2. Within the operator +, construct a local variable temp on stack.
  3. Concatenate str1 and str2 and save the result in temp.
  4. When it arrives return temp;, first copy construct a temporary object from temp, then return from operator + and destruct temp.
  5. Copy construct str3 from the temporary object constructed in the last step, then destruct the temporary object.

Although in theory we should take all five steps, g++ actually makes proper optimizations. Please analyze the output and figure out the actual steps.

2.6. Inheritance

A class can inherit from another class to get its data members and member functions. They are called derived class and base class respectively. The main purpose for inventing class inheritance is to reuse existing code.

The Person class has two members, name and age. Now we need a Student class, which conceptually is a kind of Person. It has another member score besides name and age. If we write the new class from scratch, Person and Student each has its own name and age, duplicated code is produced. Duplicated code has a bad smell. Since Student is a kind of Person, it should have what Person has and behave like Person. Each time we update Person class, adding new members or changing behaviors, we should update Student class as well and produce even more duplicated code. If we find a bug in Person class, we should fix it in both Person and Student class. The resulting code is both hard to maintain and error-prone, until class inheritance comes into play.

Let’s begin with our familiar C code first, then we’ll rewrite it in C++.

student.h

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#ifndef STUDENT_H
#define STUDENT_H

typedef void *HPERSON;

HPERSON person_create(const char *name, int age);
void person_display(const HPERSON p);
void person_delete(HPERSON p);

typedef void *HSTUDENT;

HSTUDENT student_create(const char *name, int age, int score);
void student_display(const HSTUDENT s);
void student_delete(HSTUDENT s);

#endif /* STUDENT_H */

student.c

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "student.h"

typedef struct
{
     char *name;
     int age;
} person_t;

HPERSON person_create(const char *name, int age)
{
     printf("create person\n");
     person_t *p = malloc(sizeof(person_t));
     p->name = malloc(strlen(name)+1);

     strcpy(p->name, name);
     p->age = age;
     return p;
}

void person_display(const HPERSON p)
{
     printf("<<<< display person information >>>>\n");
     printf(" Name:%s\n Age:%d\n", ((person_t *)p)->name, ((person_t *)p)->age);
}

void person_delete(HPERSON p)
{
     printf("delete person\n");
     free(((person_t *)p)->name);
     free(p);
}

typedef struct
{
     person_t person;
     int score;
} student_t;

HSTUDENT student_create(const char *name, int age, int score)
{
     printf("create student\n");
     student_t *student = malloc(sizeof(student_t));
     student->person.name = malloc(strlen(name)+1);

     strcpy(student->person.name, name);
     student->person.age = age;
     student->score = score;
     return student;
}

void student_display(const HSTUDENT s)
{
     printf("<<<< display student information >>>>\n");
     printf(" Name:%s\n Age:%d\n Score:%d\n",
            ((person_t *)s)->name, ((person_t *)s)->age, ((student_t *)s)->score);
}

void student_delete(HSTUDENT s)
{
     printf("delete student\n");
     free(((person_t *)s)->name);
     free(s);
}

main.c

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#include "student.h"

int main(void)
{

     HSTUDENT student = student_create("XiaoMing", 12, 99);
     student_display(student);
     person_display(student);
     student_delete(student);
     return 0;
}

A person_t object is embedded at the beginning of a student_t object.

../_images/cppoo.studentobject.png

Therefore a pointer pointing to a student_t object can also be taken as a pointer pointing to a person_t object. Although we specifically define a student_display function for a student_t object, the original person_display function can also apply to a student_t object.

Now let’s see the equivalent C++ code.

student.h

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#ifndef STUDENT_H
#define STUDENT_H

class Person
{
public:
     Person(const char *name, int age);
     ~Person();
     void display() const;
protected:
     char *name;
     int age;
};

class Student: public Person
{
public:
     Student(const char *name, int age, int score);
     ~Student();
     void display() const;
private:
     int score;
};

#endif //STUDENT_H

The protected access specifier allows corresponding members to be accessed within derived class, while still protecting them from being accessed outside. The private members of a class are inaccessible to its derived class, so name and age should be declared as protected instead of private. Note the first line of class Student declaration:

1
class Student: public Person

Here the public keyword indicates how Student class inherits from Person class. It determines the access specifier of inherited members in derived class. If the inheritance is public:

access specifier of base class members access specifier of inherited members
public public
protected protected
private inaccessible

If the inheritance is protected:

access specifier of base class members access specifier of inherited members
public protected
protected protected
private inaccessible

If the inheritance is private:

access specifier of base class members access specifier of inherited members
public private
protected private
private inaccessible

Just like person_t and student_t in the prior C code, a Person object is embedded at the beginning of a Student object. Note if the embedded Person object has private members, they are accessible to member functions inherited from Person class but inaccessible to other member functions of Student class.

Here is the implementation:

student.cpp

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <iostream>
#include <cstring>
#include "student.h"
using namespace std;

Person::Person(const char *name, int age)
  :name(NULL), age(age)
{
  cout<<"create person"<<endl;
  this->name = new char[strlen(name)+1];
  strcpy(this->name, name);
}

void Person::display() const
{
  cout<<"<<<< display person information >>>>"<<endl
      <<" Name:"<<name<<endl<<" Age:"<<age<<endl;
}

Person::~Person()
{
  cout<<"delete person"<<endl;
  delete[] name;
}

Student::Student(const char *name, int age, int score)
  :Person(name, age), score(score)
{
  cout<<"create student"<<endl;
}

void Student::display() const
{
  cout<<"<<<< display student information >>>>"<<endl
      <<" Name:"<<name<<endl<<" Age:"<<age<<endl
      <<" Score:"<<score<<endl;
}

Student::~Student()
{
  cout<<"delete student"<<endl;
}

Note the initializer list in the constructor of Student class:

1
2
Student::Student(const char *name, int age, int score)
    :Person(name, age), score(score)

When we construct an object from a class, the constructor of its base class is always invoked before other initialization steps. If class C inherits class B, which in turn inherits class A, then when we construct an object from class C, A‘s constructor is invoked first to initialize members of part A, B‘s constructor is invoked next to initialize members of part B, C‘s constructor is invoked last to initialize the remaining part of the object.

If we do not call Person(name, age) in the initializer list, the default Person() constructor with no parameter will be called. Because we do not define such a constructor, and because we do provide a constructor so that compiler’s synthesized version is suppressed, the code will end up with a compile error. Please try it yourself.

Here is the user code:

main.cpp

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#include "student.h"

int main()
{
  Student *student = new Student("XiaoMing", 12, 99);
  student->display();

  Person *p = student;
  p->display();

  delete student;
  return 0;
}

As we can see, p->display() is invoking Person::display() because p is of type Person *, while student->display() is invoking Student::display() becase student is of type Student *. If the derived class inherits a member function from the base class and implements another function with the same name, it’s own version overrrides the inherited version. In this case, if we don’t implement Student::display(), then student->desplay() is also invoking Person::display(), because that function is inherited and not overridden.

Think about this: we already see a pointer of base class type points to an object of derived class, then can we make a pointer of derived class type point to an object of base class? Why?

2.7. Polymorphism

In the example of the last section, p->display() is invoking Person::display(). If p points to a Student object, only name and age will be displayed, and Person::display() knows nothing about score. Sometimes what we really want is that the most verbose information should be displayed. If p points to a Person object, name and age should be displayed by p->display(). If p points to a Student object, name, age and score should be displayed by p->display(). This is called polymorphism. How to achieve this? Again we will start from C code.

student.h

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#ifndef STUDENT_H
#define STUDENT_H

typedef void *HPERSON;

HPERSON person_create(const char *name, int age);
void person_delete(HPERSON p);

typedef void* HSTUDENT;

HSTUDENT student_create(const char *name, int age, int score);
void student_delete(HSTUDENT s);

void display(HPERSON);

#endif /* STUDENT_H */

student.c

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "student.h"

typedef struct
{
     char *name;
     int age;
     struct operations *ops;
} person_t;

struct operations
{
     void (*display)(const HPERSON);
};

static void p_display(const HPERSON p)
{
     printf("<<<< display person information >>>>\n");
     printf(" Name:%s\n Age:%d\n", ((person_t *)p)->name, ((person_t *)p)->age);
}

static struct operations person_ops = { p_display };

HPERSON person_create(const char *name, int age)
{
     printf("create person\n");
     person_t *p = malloc(sizeof(person_t));
     p->name = malloc(strlen(name)+1);
     p->ops = &person_ops;

     strcpy(p->name, name);
     p->age = age;
     return p;
}

void person_delete(HPERSON p)
{
     printf("delete person\n");
     free(((person_t *)p)->name);
     free(p);
}

typedef struct
{
     person_t person;
     int score;
} student_t;

static void s_display(const HSTUDENT s)
{
     printf("<<<< display student information >>>>\n");
     printf(" Name:%s\n Age:%d\n Score:%d\n",
            ((person_t *)s)->name, ((person_t *)s)->age, ((student_t *)s)->score);
}

static struct operations student_ops = { s_display };

HSTUDENT student_create(const char *name, int age, int score)
{
     printf("create student\n");
     student_t *s = malloc(sizeof(student_t));
     ((person_t *)s)->name = malloc(strlen(name)+1);
     ((person_t *)s)->ops = &student_ops;

     strcpy(((person_t *)s)->name, name);
     ((person_t *)s)->age = age;
     s->score = score;
     return s;
}

void student_delete(HSTUDENT s)
{
     printf("delete student\n");
     free(((person_t *)s)->name);
     free(s);
}

void display(HPERSON p)
{
     ((person_t *)p)->ops->display(p);
}

main.c

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#include "student.h"

int main()
{
     HSTUDENT student = student_create("XiaoMing", 12, 99);
     display(student);
     student_delete(student);

     HPERSON person = person_create("XiaoHua", 11);
     display(person);
     person_delete(person);

     return 0;
}

person_t has a pointer ops pointing to an operations structure. Since person_t is embedded in student_t, student_t also has the ops pointer. person_ops and student_ops are two operations structures, which consists of various function pointers. In this case only one function pointer display is contained. person_ops‘s display pointer points to p_display function, while student_ops‘s display pointer points to s_display function. A student_t object’s ops member will be initialized to student_ops while a person_t object’s ops member will be initialized to person_ops. That way a person_t object and a student_t object will invoke different functions through ops->display pointer.

../_images/cppoo.studentobject2.png

We have to access the corresponding operations structure through an ops pointer embedded in persion_t. Then why not embed the operations structure directly into person_t? Please think about it.

Now let’s see how to achieve polymorphism in C++.

student.h

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#ifndef STUDENT_H
#define STUDENT_H

class Person
{
public:
     Person(const char *name, int age);
     ~Person();
     virtual void display() const;
protected:
     char *name;
     int age;
};

class Student: public Person
{
public:
     Student(const char *name, int age, int score);
     ~Student();
     virtual void display() const;
private:
     int score;
};

#endif //STUDENT_H

This header file differs from the previous version in only one place: The display function is declared virtual. A class with virtual functions has a hidden pointer member vptr, which points to a virtual function table. vptr is similar to the ops pointer and virtual function table is similar to the operations structure in the above C code. When constructing a Person object, vptr is initialized to a virtual function table similar to person_ops in the above C code. When construct a Student object, vptr is initialized to a virtual function table similar to student_ops in the above C code.

Once a member function is declared virtual in a base class, all derived classes also inherit this function as virtual, even if we do not explicitly write virtual when declaring this function in derived classes. Keeping virtual in derived class function declarations will make the code more explicit. If a derived class doesn’t provide an implementation for a virtual function, the base class version is inherited. If a derived class does provide an implementation for a virtual function, this function’s prototype must comply with the base class version. This is different from the case of overriding. If a derived class overrides a base class function, this new function’s prototype need not comply with the inherited version.

student.cpp is exactly the same as in the last section. So we’ll omit it. Here is the user code:

main.cpp

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#include "student.h"

int main(void)
{
  Student *student = new Student("XiaoMing", 12, 99);
  student->display();
  Person *p = student;
  p->display();
  delete student;

  p = new Person("XiaoHua", 11);
  p->display();
  delete p;

  return 0;
}

Note the same p->display() appears twice in the code and results in two different functions being called. When p points to a Student object, p->display() is invoking Student::display(). When p points to a Person object, p->display() is invoking Person::display(). How can the same code be compiled to different binary instructions? In fact the binary instructions make no difference, but the invocation is through a function pointer which points to different functions in these two cases. That is, which function to call is not determined by the code itself, but by the actual object involved. In another word, it’s determined at runtime rather than compile time. This is called dynamic binding.

Here is another example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <iostream>
using namespace std;

class Animal
{
public:
        virtual const char *saywhat(void)=0;
};

class Cow: public Animal
{
public:
        virtual const char *saywhat(void) { return "Moo"; }
};

class Dog: public Animal
{
public:
        virtual const char *saywhat(void) { return "Bark"; }
};

void whosay(const char *who, Animal *a)
{
  cout<<who<<" says '"<<a->saywhat()<<"'"<<endl;
}

int main(void)
{
        Cow debby;
        Dog puppy;
        whosay("Debby", &debby);
        whosay("Puppy", &puppy);
        cout<<"sizeof(Cow)="<<sizeof(Cow)<<endl;
        return 0;
}

Note this declaration:

1
virtual const char *saywhat(void)=0;

If a virtual function declaration is appended with =0, it is called pure virtual function. And the corresponding class is called abstract class. An abstract class cannot be instantiated, that is, we can’t create an object of an abstract class. The sole purpose for defining an abstract class is to derive from it. The function whosay is quite versatile. It takes a paramter of type Animal *, which can point to either a Cow object or a Dog object and call the corresponding saywhat function. Although the abstract class Animal cannot be instantiated, defining a pointer to Animal is perfectly legal. The abstract class defines a set of interfaces (saywhat in this case) that derived classes must implement. Besides, the abstract class itself often appears in function parameters. For these reasons, the abstract class is often called an interface. Deriving from an abstract class is often called implementing an interface.

If a class has no data members, the object of this class should have zero byte. However, C++ requires one byte be allocated for such an object. This can avoid two or more objects occupying the same address. The example above outputs sizeof(Cow). Is it one? Try and think about it.