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; // ... };如果只重新定义一个版本,则另外两个版本将被隐藏,派生类对象将无法使用它们。注意,如果不需要修改,则新定义可只调用基类版本。