[C++ Primer Note14] 面向对象程序设计

  1. 对于某些函数,基类希望它的派生类各自定义适合自身的版本,此时基类就将这些函数声明成虚函数(virtual function)
class Quote{
public:
    string isbn() const;
    virtual double net_price(size_t n) const;
}
  1. 派生类必须通过使用类派生列表明确指出它是从哪个基类继承而来的。类派生列表的形式是:首先是一个冒号,后面紧跟着以逗号分隔的基类列表,其中每个基类可以有访问说明符
  2. 派生类必须在其内部对所有重新定义的虚函数进行声明。派生类可以在这样的函数之前加上virtual关键字,但并不是非得这么做。
  3. 当我们使用基类的引用或指针调用一个虚函数时将发生动态绑定,即根据实际对象类型来选择函数版本。
  4. 基类通常都应该定义一个虚析构函数,即使该函数什么都不做
  5. 在C++中,基类必须把它的两种成员函数区分开来:一种是基类希望其派生类进行覆盖的函数;另一种是基类希望派生类直接继承而不要改变的函数。
  6. 关键字virtual智能出现在类内部的声明语句之前而不能用于类外部的函数定义。如果基类把一个函数声明成虚函数,则该函数在派生类中隐式地也是虚函数
  7. 派生类可以继承定义在基类中的成员,但是派生类的成员函数不一定有权访问从基类继承而来的成员。派生类能访问公有成员,而不能访问私有成员。不过某些时候有一种成员基类希望派生类有权访问,而其他用户禁止访问,我们用protected访问运算符说明这些成员。
  8. 类派生列表中用到的访问说明符可以是:publicprotected或者private中的一个。它的作用是控制派生类从基类继承而来的成员是否对派生类的用户可见
  9. 如果派生类没有覆盖基类中的某个虚函数,则该虚函数的行为类似于其他普通成员,派生类会直接继承其在基类中的版本。C++11标准允许派生类显式地注明它覆盖了虚函数,具体做法是在形参列表后(const,引用限定符后)添加一个关键字override
  10. 一个派生类对象包含多个组成部分:一个含有派生类自己定义的成员的子对象,以及一个与该派生类继承的基类对应的子对象,因为在派生类对象含有与其基类对应的组成部分,所以我们能把派生类对象当成基类对象来使用,而且我们也能将基类的指针或引用绑定到派生类对象中的基类部分:
Father father;
Son son;
Father *p=&father;
p=&son;
Father &r=son;

这种转换通常称为派生类到基类的(derived-to-base)类型转换,和其他类型转换一样,编译器会隐式地执行这种转换。
我们可以把派生类对象或者派生类对象的引用用在需要基类引用的地方;也可以把派生类对象的指针用在需要基类指针的地方。

  1. 尽管在派生类对象中含有从基类继承的成员,但是派生类并不能直接初始化成员,派生类必须使用基类的构造函数来初始化它的基类部分
  2. 派生类对象的基类部分和派生类对象自己的数据成员都是在构造函数的初始化阶段执行初始化操作的。派生类构造函数同样通过初始化列表来将实参传递给基类构造函数的,比如:
Bulk_quote(const string &book,double p,size_t pty,double disc):
                  Quote(book,p), min_qty(qty),discount(disc){ }

首先初始化基类的部分,然后按照声明的顺序依次初始化派生类的成员。

  1. 如果基类定义了一个静态成员,则在整个继承体系中只存在该成员的唯一定义
  2. 派生类的声明与其他类差别不大,声明中包含类名但是不包含派生列表。如果我们想将某个类用作基类,则该类必须已经定义而非仅仅声明。
  3. C++11标准提供了一种防止继承发生的方法,即在类名后跟一个关键字final
  4. 因为一个基类的对象可能是派生类对象的一部分,也可能不是,所以不存在从基类到派生类的自动类型转换,除此以外即使一个基类指针或引用绑定在一个派生类对象上,我们也不能执行基类到派生类的转换
Son son;
Father *p=&son;   //正确,动态类型是Son
Son *ps=p;   //错误,不能将基类转换成派生类
  1. 当我们用一个派生类对象为一个基类对象初始化或赋值时,只有该派生类对象的基类部分会被拷贝,移动或赋值,它的派生类部分将被忽略。
  2. 我们必须为每一个虚函数都提供定义,不管它是否被使用,因为编译器无法确定到底会使用哪个虚函数。
  3. 当且仅当对通过指针或引用调用虚函数时,才会在运行时解析该调用,也只有在这种情况下对象的动态类型才有可能与静态类型不同。
  4. 一个派生类的函数如果覆盖了某个继承而来的虚函数,则它的形参类型必须与被它覆盖的基类函数完全一致,同时返回类型也必须相匹配,但如果类的虚函数返回类型是类本身的指针或引用时,规则无效。
    如果D由B派生得到,则B的虚函数可以返回B*而派生类可以返回D*,只不过要求从D到B的类型转换时可访问的。
  5. 派生类如果定义了一个函数与基类中虚函数的名字相同但是形参列表不同,这仍然是合法的行为,但这有时候可能是一种错误。我们可以通过override关键字来让编译器为我们发现一些错误。
  6. 我们还能把某个函数指定为final的,这样之后任何尝试覆盖此函数的操作都将引发错误:
struct D2:B{
    void fi(int) const final;
};
  1. 和其他函数一样,虚函数也可以拥有默认实参,如果某次函数调用使用了默认实参,则该实参值由本次调用的静态类型决定,所以基类和派生类中定义的默认实参最好一致。
  2. 我们可以通过作用域运算符来让对虚函数的调用不要进行动态绑定,而是强迫其执行虚函数的某个特定版本:
double undiscounted=baseP->Quote::net_price(42);

这种机制一般用在派生类的虚函数体内调用基类虚函数版本时,如果没有使用作用域运算符,则会导致无限递归

  1. 我们可以定义纯虚函数告诉用户当前这个函数没有实际意义。一个纯虚函数无需定义,我们通过在函数体的位置(声明语句的分号前)书写=0就可以将一个虚函数说明为纯虚函数,其中=0只能出现在类内部的声明语句处。我们也可以为纯虚函数提供定义,不过函数体必须在类的外部。
  2. 含有(或者未经覆盖直接继承)纯虚函数的类是抽象基类(abstract base class),我们不能直接创建一个抽象基类的对象
  3. protected成员对于派生类的成员友元是可访问的,但只能通过派生类对象来访问,派生类对于一个基类对象中的protected成员没有任何访问特权
  4. 派生列表中的访问说明符对于派生类成员(友元)能否访问其直接基类的成员没什么影响。对基类成员的访问权限只与基类中的访问说明符有关。派生列表访问说明符的目的是控制派生类用户(包括派生类的派生类)对于基类成员的访问权限
  5. 只有当公有继承时,用户代码才能使用派生类向基类的转换
  6. 友元关系不能继承
  7. 有时我们需要改变派生类继承的某个名字的访问级别,通过使用using声明可以达到这一目的:
class Base{
public:
    size_t size() const { return n;}
protected:
    size_t n;
};
class Derived: private Base{
public:
    using Base::size;
protected:
    using Base::n;
};

using声明语句中名字的访问权限由之前的访问说明符决定

  1. 我们曾经介绍过structclass具有不同的默认访问说明符。类似的,默认派生运算符也由定义派生类所用的关键字来决定。默认情况下,使用class定义的派生类是私有继承的,struct则是公有继承的。实际上,这两点也是class和struct的唯二区别了。
  2. 当存在继承关系时,派生类的作用域嵌套在其基类的作用域之内。
  3. 一个对象,引用或指针的静态类型决定了该对象的哪些成员是可见的,我们能使用哪些成员是由静态类型决定的。比如我们不能用基类引用调用派生类独有的函数。
  4. 派生类的成员将隐藏同名的基类成员。我们可以通过作用域运算符来使用被隐藏的基类成员。
  5. 声明在内层作用域的函数并不会重载声明在外层作用域的函数。因此,定义在派生类的函数也不会重载基类的同名成员,而只会隐藏
  6. 继承关系对基类拷贝控制最直接的影响是基类通常应该定义一个虚析构函数,这样我们就能动态分配继承体系中的对象了。因为这样我们确保delete基类指针时能运行正确的析构函数版本,如果没有定义虚析构函数,将产生未定义的行为。
  7. 如果一个类定义了析构函数,即使通过=default的形式使用了合成的版本,编译器也不会为这个类合成移动操作
  8. 基类或派生类的合成拷贝控制成员与其他合成的构造函数,赋值运算符或析构函数类似:它们对类本身的成员一次进行初始化,赋值或销毁。此外,还负责使用直接基类中对应的操作对一个对象的直接基类部分进行相应的操作
  9. 默认情况下,基类默认构造函数初始化派生类对象的基类部分。如果我们想拷贝(或移动)基类部分,则必须在派生类的构造函数初始值列表中显式地使用基类的拷贝(或移动)构造函数。同样的,派生类的赋值运算符也必须显式地为基类部分赋值
  10. 如果构造函数或析构函数调用了某个虚函数,则我们应该执行与构造函数或析构函数所属类型相对应的虚函数版本。
  11. 当派生类对象被赋值给基类对象时,其中的派生类部分将被切掉,因此容器和存在继承关系的类型无法兼容。当我们希望在容器中存放具有继承关系的对象时,我们实际上存放的通常是基类指针(更好的选择是智能指针)。

推荐阅读更多精彩内容