虚函数及静态联编与动态联编---学习笔记

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/wangell/article/details/38777023

C++ Primer Plus 第13章 类继承


    由于虚函数与静动态联编关系紧密,所以放在一处详细说明,令请见另一篇文章 virtual属性对类继承的影响---学习笔记,此文章包含了下面1中第二点的详细描述及一个示例。


1、提到静态联编与动态联编,我们首先需要来讲讲virtual函数,虚函数的一些要点:

    在基类方法的声明中使用关键字virtual可使该方法在基类以及所有的派生类(包括从派生类派生出来的类)中是虚拟的。

    如果使用指向对象的引用或指针来调用虚方法,程序将使用为对象类型定义的方法,而不使用为引用或指针类型定义的方法。(详见virtual属性对类继承的影响---学习笔记)这称为动态联编或晚期联编。这种行为非常重要,因为这样积累指针或引用可以指向派生类对象。

    如果定义的类将被用作基类,则应将那些要在派生类中重新定义的类方法声明为虚拟的。


2、虚方法的一些其它知识

    a、构造函数

        构造函数不能是虚函数。创建派生类对象时,将调用派生类的构造函数,而不是基类的构造函数,然后,派生类的构造函数将使用基类的一个构造函数,这种顺序不同于继承机制。因此派生类不继承基类的构造函数,所以将基类构造函数声明为虚拟的没有什么意义。

    b、析构函数

        析构函数应当是虚函数,除非类不用做基类。例如,假设Employee是基类,Singer是派生类,并添加一个 char* 成员,该成员指向由 new 分配的内存。当 Singer 对象过期时,必须调用 ~Singer() 析构函数来释放内存。

    // 请看下面的代码:
    Employee *pe = new Singer; // legal because Employee is base for Singer
    // ...
    delete pe; // ~Employee() or ~Singer();
        如果使用默认的静态联编(即不使用 virtual ),delete 语句将调用 ~Employee() 析构函数。这将释放由 Singer 对象中的 Employee 部分指向的内存,但不会释放新的类成员指向的内存。但如果析构函数是虚拟的,则上述代码将先调用 ~Singer() 析构函数释放由 Singer 组件指向的内存,然后调用 ~Employee() 析构函数来释放由 Employee 组件指向的内存。(此处解释参考1中的第二点,同样也可以参考 virtual属性对类继承的影响---学习笔记)

        这意味着,即使基类不需要显示析构函数提供服务,也不应依赖于默认构造函数,而应提供虚拟析构函数,即使它不执行任何操作: virtual ~BaseClass()

        顺便说一句,给类定义一个虚拟析构函数并非错误,即使这个类不用做基类,这只是一个效率方面的问题。

    c、友元

        友元不能是虚函数,因为友元不是类成员,而只有成员才能是虚函数。如果由于这个原因引起了设计问题,可以通过让友元函数使用虚拟成员函数来解决。

    d、没有重新定义(即在继承类中没有实现该虚函数)

        如果派生类没有重新定义函数,将使用该函数的基类版本。如果派生类位于派生链中,则将使用最新的虚函数版本,例外的情况是基类版本是隐藏的。

    e、重新定义隐藏方法(函数隐藏)

    // 假设创建了如下所示的代码:
    class Dwelling
    {
    public:
        virtual void showperks(int a) const;
    // ...
    };

    class Hovel : public Dwelling
    {
    public:
        virtual void showperks() const;
    // ...
    };
    这个可能不会出现警告(取决与编译器),也可能会出现类似于这样的编译器警告:Warning:  Hovel::showperks(void) hides Dewlling::showperks(int)

    代码将具有如下含义:

        Hovel trump;

        trump.showperks();    // valid

        trump.showperks(5); // invalid

        新定义将 showperks() 定义为一个不接受任何参数的函数。重新定义不会生成函数的两个重载版本,而是隐藏了接收一个 int 参数的基类版本。简而言之,重新定义继承的方法并不是重载。如果在派生类中重新定义函数,将不是使用相同的函数特征覆盖基类声明,而是隐藏同名的基类方法,不管参数特征标如何。

        这引出了两条经验规则:第一,如果重新定义继承的方法,应确保与原理啊的原型完全相同,但如果返回类型是基类引用或指针,则可以修改为指向派生类的引用或指针(这种例外是新出现的)。这种特征被称为返回类型协变(covariance of return type),因为允许返回类型随类的类型变化而变化。

    class Dewlling
    {
     public:
        // a base method
        virtual Dwelling &build(int n);
        // ...
    };

    class Hovel : public Dwelling
    {
    public:
        // a derived method with a covariance return type
        virtual Hovel &build(int n); // same function signature
    };

        注意,这种例外只适用于返回值,而不适用于参数。

        第二,如果基类声明被重载了,则应在派生类中重新定义所有的基类版本。

class Dwelling
{
public:
    // three overloaded showperks
    virtual void showperks(int a) const;
    virtual void showperks(double x) const;
    virtual void showperks() const;
    // ...
};

class Hovel : public Dwelling
{
public:
    // three redefined showperks
    virtual void showperks(int a) const;
    virtual void showperks(double x) const;
    virtual void showperks() const;
    // ...
};
        如果只重新定义一个版本,则另外两个版本将被隐藏,派生类对象将无法使用它们。注意,如果不需要修改,则新定义可只调用基类版本。


猜你喜欢

转载自blog.csdn.net/wangell/article/details/38777023