特性三:多态和虚函数

作者:jicanmeng

时间:2014年12月05日


先看一个程序:

#include <iostream>
#include <string>

class Animal
{
protected:
    std::string m_strName;

    // We're making this constructor protected because
    // we don't want people creating Animal objects directly,
    // but we still want derived classes to be able to use it.
    Animal(std::string strName)
        : m_strName(strName)
    {
    }

public:
    std::string GetName() { return m_strName; }
    const char* Speak() { return "???"; }
};

class Cat: public Animal
{
public:
    Cat(std::string strName)
        : Animal(strName)
    {
    }

    const char* Speak() { return "Miao"; }
};

int main()
{
    using namespace std;

    Cat cCat("Fred");
    cout << "cCat is named " << cCat.GetName() << ", and it says " <<
	cCat.Speak() << endl;

    Animal *pAnimal = &cCat;
    cout << "pAnimal is named " << pAnimal->GetName() << ", and it says " <<
	pAnimal->Speak() << endl;

    Animal &rAnimal = cCat;
    cout << "rAnimal is named " << rAnimal.GetName() << ", and it says " <<
	rAnimal.Speak() << endl;
    return 0;
}

以下是运行结果:

[jicanmeng@andy tmp]$ ./a.out
				cCat is named Fred, and it says Miao
				pAnimal is named Fred, and it says ???
				rAnimal is named Fred, and it says ???
			[jicanmeng@andy tmp]$

对结果的分析,learncpp.com上面解释的非常清楚(learncpp.com上面举了一个例子,父类名称为Base,子类名称为Derived,所以下面的说明中,注意Base对应于Animal,pBase对应于pAnimal,rBase对应于rAnimal):

It turns out that because rBase and pBase are a Base reference and pointer, they can only see members of Base (or any classes that Base inherited). So even though Derived::GetName() is an override of Base::GetName(), the Base pointer/reference can not see Derived::GetName(). Consequently, they call Base::GetName(), which is why rBase and pBase report that they are a Base rather than a Derived.

Note that this also means it is not possible to call Derived::GetValue() using rBase or pBase. They are unable to see anything in Derived.

假设现在除了定义了一个Cat子类,还定义了cDog,Wolf,Horse,Cow等等子类。我们需要实现一个函数,打印每个动物的名字和声音。对于Cat,函数如下:

void Report(Cat &cCat)
{
    cout << cCat.GetName() << " says " << cCat.Speak() << endl;
}

对于cDog,函数如下:

void Report(Dog &cDog)
{
    cout << cDog.GetName() << " says " << cDog.Speak() << endl;
}

如果对于每一个子类,我们都实现一个类似的函数,那么工作量就很大。

为了解决这样的问题,C++中引入了虚函数的概念。我们将上面的程序修改为虚函数的形式。

#include <iostream>
#include <string>

class Animal
{
protected:
    std::string m_strName;

    // We're making this constructor protected because
    // we don't want people creating Animal objects directly,
    // but we still want derived classes to be able to use it.
    Animal(std::string strName)
        : m_strName(strName)
    {
    }

public:
    std::string GetName() { return m_strName; }
    virtual const char* Speak() { return "???"; }
};

class Cat: public Animal
{
public:
    Cat(std::string strName)
        : Animal(strName)
    {
    }

    virtual const char* Speak() { return "Miao"; }
};

int main()
{
    using namespace std;

    Cat cCat("Fred");
    cout << "cCat is named " << cCat.GetName() << ", and it says " <<
	cCat.Speak() << endl;

    Animal *pAnimal = &cCat;
    cout << "pAnimal is named " << pAnimal->GetName() << ", and it says " <<
	pAnimal->Speak() << endl;

    Animal &rAnimal = cCat;
    cout << "rAnimal is named " << rAnimal.GetName() << ", and it says " <<
	rAnimal.Speak() << endl;
    return 0;
}

以下是运行结果:

[jicanmeng@andy tmp]$ ./a.out
				cCat is named Fred, and it says Miao
				pAnimal is named Fred, and it says Miao
				rAnimal is named Fred, and it says Miao
			[jicanmeng@andy tmp]$

可以看出来,定义一个指针*pAnimal指向一个派生类对象,但是指针本身是指向基类类型的,基类和派生类中都有虚函数Speak(),那么pAnimal->Speak()调用的不是基类的Speak()函数,而是派生类的Speak()函数。

使用虚函数,前面提到的问题可以得到解决。我们将Speak()定义为虚函数,然后定义一个函数:

void Report(Animal &cAnimal)
{
    cout << cAnimal.GetName() << " says " << cAnimal.Speak() << endl;
}

这样,传进来的是Cat类型的对象,就调用Cat类的Speak()函数。传进来的是Dog类型的对象,就调用Dog类的Speak()函数。这种通过基类指针或引用,调用函数时根据指针指向的对象的类,来决定调用哪个函数的行为,就称为多态。多态是通过虚函数实现的。

learncpp.com上面是这样描述的:

"A virtual function is a special type of function that resolves to the most-derived version of the function with the same signature. To make a function virtual, simply place the “virtual” keyword before the function declaration."

对于virtual function,有四个需要注意的地方:

  1. 注意上面的加粗字体:resolves to the most-derived version: 看下面的例子:
    #include <iostream>
    
    class A
    {
    public:
        virtual const char* getName() { return "A"; }
    };
    
    class B: public A
    {
    public:
        virtual const char* getName() { return "B"; }
    };
    
    class C: public B
    {
    public:
        virtual const char* getName() { return "C"; }
    };
    
    class D: public C
    {
    public:
        virtual const char* getName() { return "D"; }
    };
    
    int main()
    {
        C c;
        A &rBase = c;
        std::cout << "rBase is a " << rBase.getName() << '\n';
    
        return 0;
    }
    输出结果为"rBase is a C"。为什么是C而不是D呢?因为我们创建的就是一个C object,这是最终派生的对象。
  2. virtual关键字在base class中的函数前面写上即可,在derived class中的函数前面写不写都可以。对于上面的例子,我们这样写也是完全可以的:
    #include <iostream>
    
    class A
    {
    public:
        virtual const char* getName() { return "A"; }
    };
    
    class B: public A
    {
    public:
        const char* getName() { return "B"; }
    };
    
    class C: public B
    {
    public:
        const char* getName() { return "C"; }
    };
    
    class D: public C
    {
    public:
        const char* getName() { return "D"; }
    };
    
    int main()
    {
        C c;
        A &rBase = c;
        std::cout << "rBase is a " << rBase.getName() << '\n';
    
        return 0;
    }
    但是为了清楚明了,建议写上。
  3. derived class中的函数如果要作为base class中的某个virtual function的对应函数, 那么返回值类型, 函数个数和参数类型, 是否是const都要保持一致。把上面的例子稍微修改一下:
    #include <iostream>
    
    class A
    {
    public:
        virtual const char* getName() const { return "A"; }
    };
    
    class B: public A
    {
    public:
        const char* getName() { return "B"; }
    };
    
    class C: public B
    {
    public:
        const char* getName() { return "C"; }
    };
    
    class D: public C
    {
    public:
        const char* getName() { return "D"; }
    };
    
    int main()
    {
        C c;
        A &rBase = c;
        std::cout << "rBase is a " << rBase.getName() << '\n';
    
        return 0;
    }
    现在的输出结果就为"rBase is a A"。因为在base class中有一个const,而derived class中没有。
  4. 构造函数和析构函数中不要调用虚函数。learncpp.com上面是这样说的:
    Remember that when a Derived class is created, the Base portion is constructed first. If you were to call a virtual function from the Base constructor, and Derived portion of the class hadn’t even been created yet, it would be unable to call the Derived version of the function because there’s no Derived object for the Derived function to work on. In C++, it will call the Base version instead.
    A similar issue exists for destructors. If you call a virtual function in a Base class destructor, it will always resolve to the Base class version of the function, because the Derived portion of the class will already have been destroyed.

参考资料

  1. <<C++实用教程>> 电子工业出版社 郑阿奇 主编 丁有和 编著
  2. The C++ Tutorial:
    http://www.learncpp.com/cpp-tutorial/121-pointers-and-references-to-the-base-class-of-derived-objects/
    http://www.learncpp.com/cpp-tutorial/122-virtual-functions/