《C++ Primer》 Part IV(Object-Oriented and Generic Programming)
1、在 C++ 中,基类必须指出希望派生类重写哪些函数,定义为 virtual 的函数是基类期待派生类重新定义的,基类希望派生类继承的函数不能定义为虚函数。
2、要触发动态绑定,满足两个条件:第一,必须指定为虚函数;第二必须通过基类类型的引用或指针进行函数调用。除了构造函数之外,任意非 static 成员函数都可以是虚函数。保留字只在类内部的成员函数声明中出现,不能用在类定义体外部出现的函数定义上。一般而言,派生类只(重)定义那些与基类不同或扩展基类行为的方面。
#include <iostream> using namespace std; class Father { public: Father():i(10),j(20){} virtual void print1() { cout<<"Fahter1"<<endl;} //只有virtual关键字的才会启动动态绑定 void print2() { cout<<"Fahter2"<<endl;}
virtual void test() { cout<<"test"<<endl;} protected: int i; int j; private: int k; }; class Child : public Father { public: Child(){i = 3; } //Child():i(3){} void print1(){ cout<<"Child1"<<endl;} void print2() { cout<<"Child2"<<endl;} void print3() { cout<<i<<endl;} void print4() { cout<<j<<endl;} private: int j; //如果这里重定义,会覆盖掉父类的j。不建议这样做!与基类成员同名的派生类成员将屏蔽对基类成员的直接访问。
}; int main() { Father *f = new Child; Child c; f->print1(); //Child1
//f->print3(); //Error!!!! 基类类型的指针(引用或对象)只能访问对象的基类部分。
f->Father::print1(); //Father1 当要调用基类的虚函数时,必须显式使用作用域操作符。 f->print2(); //Father2 c.print3(); //3 c.print4(); //由于子类覆盖了父类的j,且没有初始化,所以值不确定
f->test(); //test 如果virtual函数没有被子类重定义,则使用基类中定义的版本
delete f;
}
3、非虚函数总是在编译时根据调用该函数的对象、引用或指针的类型而确定。无论在运行时 item 引用的实际对象是什么类型,调用该对象的非虚函数都将会调用基类中定义的版本。
4、在同一虚函数的基类版本和派生类版本中使用不同的默认实参几乎一定会引起麻烦。。如果通过基类的引用或指针调用虚函数,但实际执行的是派生类中定义的版本,这时就可能会出现问题。
5、派生列表中使用的访问标号决定该成员在派生类中的访问级别(派生类的用户对继承而来的成员的访问控制):
如果是公用继承,基类成员保持自己的访问级别:基类的 public 成员为派生类的 public 成员,基类的 protected 成员为派生类的 protected成员。
如果是受保护继承,基类的 public 和 protected 成员在派生类中为protected 成员。
如果是私有继承,基类的的所有成员在派生类中为 private 成员。
6、接口继承与实现继承承对派生类的用户具有重要含义。
7、默认的继承访问级别:使用 class 的是 private,使用 struct 的是 public。所以在使用 class 继承时,除非有意,否则请不要忘记使用 public。
class 和 struct 定义的类唯一的不同,就是默认的成员保护级别和默认的派生保护级别不同,没有其它不同。
8、派生类可以恢复继承成员的访问级别,但不能使访问级别比基类中原来指定的更宽松:
class Child : private Father { public:
using Father::size; //最多只能恢复到Father中指定的访问级别
protected: using Father::name; //最多只能恢复到Father中指定的访问级别
}
9、如果基类被授予友元关系,则只有基类具有特殊访问权限,该基类的派生类不能访问授予友元关系的类。
10、如果基类定义 static 成员,则整个继承层次中只有一个这样的成员。无论从基类派生出多少个派生类,每个 static 成员只有一个实例。
11、派生类到基类的转换:
1)、使用派生类对象的地址,对基类类型的指针进行赋值或初始化。
2)、使用派生类的引用或对象初始化基类类型的引用。
但一般不能使用派生类对象,对基类对象进行直接赋值。
Father *f = new Child; //OK Child c; Father &f = c; //OK
Father *f = &c; //OK Child &rc = c; Father &f = rc; //OK Father f = c; //ERROR!!!!这个对象还是Father的对象。切记切记!!!!!!!!
一个是派生类对象转换为基类类型引用,一个是用派生类对象对基类对象进行初始化或赋值,理解它们之间的区别很重要。
从基类到派生类的自动转换是不存在的。
12、每个派生类对象由派生类中定义的(非 static)成员加上一个或多个基类子对象构成。构造函数和复制控制成员不能继承,每个类定义自己的构造函数和复制控制成员。像任何类一样,如果类不定义自己的默认构造函数和复制控制成员,就将使用合成版本。
派生类构造函数的初始化列表只能初始化派生类的成员,不能直接初始化继承成员。相反派生类构造函数通过将基类包含在构造函数初始化列表中来间接初始化继承成员。
13、一个类只能初始化自己的直接基类。这一限制的原因是,类B 的作者已经指定了怎样构造和初始化 B 类型的对象。像类 B 的任何用户一样,类 C 的作者无权改变这个规约。
14、尊重基类接口:
派生类构造函数不能初始化基类的成员且不应该对基类成员赋值。如果那些成员为 public 或 protected,派生构造函数可以在构造函数函数体中给基类成员赋值,但是,这样做会违反基类的接口。派生类应通过使用基类构造函数尊重基类的初始化意图,而不是在派生类构造函数函数体中对这些成员赋值。
15、如果派生类定义了自己的复制构造函数,该复制构造函数一般应显式使用基类复制构造函数初始化对象的基类部分。赋值操作符通常与复制构造函数类似:如果派生类定义了自己的赋值操作符,则该操作符必须对基类部分进行显式赋值。
16、析构函数的工作与复制构造函数和赋值操作符不同:派生类析构函数不负责撤销基类对象的成员。编译器总是显式调用派生类对象基类部分的析构函数。每个析构函数只负责清除自己的成员。对象的撤销顺序与构造顺序相反:首先运行派生析构函数,然后按继承层次依次向上调用各基类析构函数。如果析构函数为虚函数,那么通过指针调用时,运行哪个析构函数将因指针所指对象类型的不同而不同。像其他虚函数一样,析构函数的虚函数性质都将继承。因此,如果层次中根类的析构函数为虚函数,则派生类析构函数也将是虚函数,无论派生类显式定义析构函数还是使用合成析构函数,派生类析构函数都是虚函数。
17、基类析构函数是三法则的一个重要例外。三法则指出,如果类需要析构函数,则类几乎也确实需要其他复制控制成员。基类几乎总是需要构造函数,从而可以将析构函数设为虚函数。如果基类为了将析构函数设为虚函数,则具有空析构函数,那么,类具有析构函数并不表示也需要赋值操作符或复制构造函数。即使析构函数没有工作要做,继承层次的根类也应该定义一个虚析构函数。
18、在复制控制成员中,只有析构函数应定义为虚函数,构造函数不能定义为虚函数。构造函数是在对象完全构造之前运行的,在构造函数运行的时候,对象的动态类型还不完整。虽然可以在基类中将成员函数 operator= 定义为虚函数,但这样做并不影响派生类中使用的赋值操作符。每个类有自己的赋值操作符,派生类中的赋值操作符有一个与类本身类型相同的形参,该类型必须不同于继承层次中任意其他类的赋值操作符的形参类型。将赋值操作符设为虚函数可能会令人混淆,因为虚函数必须在基类和派生类中具有同样的形参。基类赋值操作符有一个形参是自身类类型的引用,如果该操作符为虚函数,则每个类都将得到一个虚函数成员,该成员定义了参数为一个基类对象的 operator=。但是,对派生类而言,这个操作符与赋值操作符是不同的。将类的赋值操作符设为虚函数很可能会令人混淆,而且不会有什么用处。
19、构造派生类对象时首先运行基类构造函数初始化对象的基类部分。在执行基类构造函数时,对象的派生类部分是未初始化的。实际上,此时对象还不是一个派生类对象。
撤销派生类对象时,首先撤销它的派生类部分,然后按照与构造顺序的逆序撤销它的基类部分。
在这两种情况下,运行构造函数或析构函数的时候,对象都是不完整的。为了适应这种不完整,编译器将对象的类型视为在构造或析构期间发生了变化。在基类构造函数或析构函数中,将派生类对象当作基类类型对象对待。
20、继承情况下的类作用域:
在继承情况下,派生类的作用域嵌套在基类作用域中。如果不能在派生类作用域中找到名字,就在外围基类作用域中查找该名字的定义。正是这种类作用域的层次嵌套使我们能够直接访问基类的成员,就好象这些成员是派生类成员一样。
21、与基类成员同名的派生类成员将屏蔽对基类成员的直接访问,但使用作用域操作符访问被屏蔽成员。设计派生类时,只要可能,最好避免与基类成员的名字冲突。在基类和派生类中使用同一名字的成员函数,其行为与数据成员一样:在派生类作用域中派生类成员将屏蔽基类成员。即使函数原型不同,基类成员也会被屏蔽。
回忆一下,局部作用域中声明的函数不会重载全局作用域中定义的函数,同样,派生类中定义的函数也不重载基类中定义的成员。通过派生类对象调用函数时,实参必须与派生类中定义的版本相匹配,只有在派生类根本没有定义该函数时,才考虑基类函数。
22、如果派生类想通过自身类型使用的重载版本,则派生类必须要么重定义基类所有重载版本,要么一个也不重定义。派生类不用重定义所继承的每一个基类版本,它可以为重载成员提供 using声明。一个 using 声明只能指定一个名字,不能指定形参表,因此,为基类成员函数名称而作的 using 声明将该函数的所有重载实例加到派生类的作用域。将所有名字加入作用域之后,派生类只需要重定义本类型确实必须定义的那些函数,对其他版本可以使用继承的定义。
23、理解 C++ 中继承层次的关键在于理解如何确定函数调用。确定函数调用遵循以下四个步骤:
1). 首先确定进行函数调用的对象、引用或指针的静态类型。
2). 在该类中查找函数,如果找不到,就在直接基类中查找,如此循着类的继承链往上找,直到找到该函数或者查找完最后一个类。如果不能在类或其相关基类中找到该名字,则调用是错误的。
3). 一旦找到了该名字,就进行常规类型检查,查看如果给定找到的定义,该函数调用是否合法。
4). 假定函数调用合法,编译器就生成代码。如果函数是虚函数且通过引用或指针调用,则编译器生成代码以确定根据对象的动态类型运行哪个函数版本,否则,编译器生成代码直接调用函数。
24、含有(或继承)一个或多个纯虚函数的类是抽象基类。除了作为抽象基类的派生类的对象的组成部分,不能创建抽象类型的对象。