第十七章 使用多态和派生类

17.1使用虚成员函数实现多态

为实现多态,使用Mammal指针来调用成员函数,不用知道也不用关心该指针指向的是哪种对象以及该对象的成员函数是如何实现的。

要声明虚成员函数,可使用关键字virtual

程序清单17.1 Mammal8.cpp

#include <iostream>

class Mammal
{
private:
    int age;

public:
    Mammal() : age(1) { std::cout << "Mammal constructor ...\n"; }
    ~Mammal() { std::cout << "Mammal destructor ...\n"; }
    void move() const { std::cout << "Mammal, move one step\n"; }
    virtual void sepak() const { std::cout << "Mammal speak!\n"; }
};

class Dog : public Mammal
{
public:
    Dog() { std::cout << "Dog constructor ...\n"; }
    ~Dog() { std::cout << "Dog destructor ...\n"; }
    void move() const { std::cout << "Dog move 5 steps ...\n"; }
    void sepak() const { std::cout << "Woof!\n"; }
    void wagTail() { std::cout << "Wagging tail ...\n"; }
};

int main()
{
    Mammal *pDog = new Dog;
    pDog->move();
    pDog->sepak();
    return 0;
}

image-20200723134735386

你要观察到创建的是父类的指针,但是却将一个子类对象的地址赋值给它,所以这是父类指针指向子类对象。所以按照子类对象的构造函数顺序是先调用父类构造,然后调用子类构造。因为编译器知道这是一个父类指针,所以未使用virtual关键字的函数都先在父类中去找。如果是调用虚成员函数,编译器则会去子类中找。

程序清单17.2 Mammal9.cpp

#include <iostream>

class Mammal
{
protected:
    int age;

public:
    Mammal() : age(1) {}
    ~Mammal() {}
    virtual void sepak() const { std::cout << "Mammal speak!\n"; }
};

class Dog : public Mammal
{
public:
    void sepak() const { std::cout << "Woof!\n"; }
};

class Cat : public Mammal
{
public:
    void sepak() const { std::cout << "Meow!\n"; }
};

class Horse : public Mammal
{
public:
    void sepak() const { std::cout << "Whinny!\n"; }
};

class Pig : public Mammal
{
public:
    void sepak() const { std::cout << "Oink!\n"; }
};

int main()
{
    Mammal *array[5];
    Mammal *ptr;
    int choice, i;
    for (i = 0; i < 5; i++)
    {
        std::cout << "(1) dog (2) cat (3) horse (4) pig: ";
        std::cin >> choice;
        switch (choice)
        {
        case 1:
            ptr = new Dog;
            break;
        case 2:
            ptr = new Cat;
            break;
        case 3:
            ptr = new Horse;
            break;
        case 4:
            ptr = new Pig;
            break;
        default:
            ptr = new Mammal;
            break;
        }
        array[i] = ptr;
    }
    for (i = 0; i < 5; i++)
    {
        array[i]->sepak();
    }
    for (i = 0; i < 5; i++)
    {
        delete array[i];
    }
    return 0;
}

image-20200723141757197

17.2虚成员函数的工作原理

创建派生对象时,首先调用基类(父类)的构造函数,然后调用派生类(子类)的构造函数。说明整个派生对象包括了父类构造函数与子类构造函数所创造对象两部分在内存中是相邻的。

在类中创建虚成员函数之后,这个类的对象必须跟踪它。很多编译器会创建虚成员函数表(v-table),每个类都有一个虚成员函数表,而每个对象都有一个指向虚成员函数表的指针(vptr或v-pointer)

创建Dog的Mammal部分时,vptr被初始化为指向Mammal的虚成员函数。

调用Dog的构造函数以添加对象的Dog部分时,将调整vptr指针,使其指向Dog类重写的虚成员函数。

使用Mammal指针时,vptr将根据Mammal指针所指向对象的实际类型指向正确的函数。

子类有而父类没有的函数不能通过父类指针来进行访问,除非将其强制转换为子类指针。

仅当通过指针和引用进行调用时,才能发挥成员函数的魔力。

程序清单17.3 Mammal10.cpp

#include <iostream>

class Mammal
{
protected:
    int age;

public:
    Mammal() : age(1) {}
    ~Mammal() {}
    virtual void sepak() const { std::cout << "Mammal speak!\n"; }
};

class Dog : public Mammal
{
public:
    void sepak() const { std::cout << "Woof!\n"; }
};

class Cat : public Mammal
{
public:
    void sepak() const { std::cout << "Meow!\n"; }
};

void valueFunction(Mammal);
void ptrFunction(Mammal *);
void refFunction(Mammal &);

int main()
{
    Mammal *ptr = 0;
    int choice;
    while (1)
    {
        bool fQuit = false;
        std::cout << "(1) dog (2) cat (0) quit: ";
        std::cin >> choice;
        switch (choice)
        {
        case 0:
            fQuit = true;
            break;
        case 1:
            ptr = new Dog;
            break;
        case 2:
            ptr = new Cat;
            break;
        default:
            ptr = new Mammal;
            break;
        }
        if (fQuit)
        {
            break;
        }
        ptrFunction(ptr);
        refFunction(*ptr);
        valueFunction(*ptr);
    }
    return 0;
}

void valueFunction(Mammal mammalValue) { mammalValue.sepak(); }
void ptrFunction(Mammal *pMammal) { pMammal->sepak(); }
void refFunction(Mammal &rMammal) { rMammal.sepak(); }

image-20200723145028263

虚析构函数:

在c++中,在需要基类指针的地方使用指向派生对象的指针是一种合法且常见的做法。

当指向派生对象的指针被删除时将发生什么情况呢?如果析构函数是虚成员函数,将执行正确的操作:调用派生类的析构函数。由于派生类的析构函数会自动地调用基类的析构函数,因此整个对象将被正确地销毁。

经验规则是:如果类中的任何一个函数是虚成员函数,那么析构函数也应该是虚成员函数。

虚复制构造函数

构造函数不能是虚成员函数,然而,有时候程序非常需要通过传递一个指向基类对象的指针,创建派生类对象的拷贝。对于这种问题,一种常见的解决方法是,在基类中创建一个clone()成员函数,并将其设置为虚成员函数。clone()函数创建当前对象的拷贝,并返回该对象。

由于每个派生类对象都重写函数clone(),因此使其创建派生类对象的拷贝。

程序清单17.4 Mammal11.cpp