C++ primer plus读书笔记——第13章 类继承
第13章 类继承
1. 如果购买厂商的C库,除非厂商提供库函数的源代码,否则您将无法根据自己的需求,对函数进行扩展或修改。但如果是类库,只要其提供了类方法的头文件和编译后的代码,仍可以使用库中的类派生出新的类。而且可以在不公开实现的情况下将自己的类分发给其他人,同时允许他们在类中添加新特性。
2. 派生类构造函数首先创建基类对象,如果不调用基类构造函数,程序将使用默认的基类构造函数。
3. 创建派生类对象时,程序首先调用基类构造函数,然后再调用派生类构造函数。派生类对象过期时,程序将首先调用派生类析构函数,然后再调用基类析构函数。
4. 派生类对象可以使用基类的方法,条件是方法不是私有的。
5. 基类指针可以在不进行显式类型转换的情况下指向派生类对象;基类引用可以在不进行显式类型转换的情况下引用派生类对象。然而,基类指针或引用只能用于调用基类方法,不能调用派生类方法。
这种兼容性使得可以用派生类对象来初始化基类对象,也可以将派生类对象赋给基类对象。
6. 通常,C++要求引用和指针类型与赋给的类型匹配,但这一规则对继承来说是个例外。
7. 如果要在派生类中重新定义基类的方法,通常应将基类方法声明为虚的virtual,程序将根据引用或指针指向的对象的类型来选择方法。如果没有使用关键字virtual,程序将根据引用类型或指针类型来选择方法。
8. 方法在基类中声明为虚的后,它在派生类中将自动成为虚方法。然而,在派生类声明中使用关键字virtual来指出哪些函数是虚函数也不失为一个好办法。
9. 基类声明了一个虚析构函数。这样做是为了确保释放派生类对象时,按正确的顺序调用析构函数。
10. 关键字virtual只用于类声明中,而没有用于方法定义中。
11. 为何需要虚析构函数?
如果析构函数不是虚的,则将只调用对应于指针类型的析构函数,即使指针指向派生类对象。如果析构函数是虚的,如果基类指针指向派生类对象,将调用派生类对象的析构函数,然后自动调用基类的析构函数。因此,使用虚析构函数可以保证正确的析构函数序列被调用。
12. 将源代码中的函数调用解释为执行特定的函数代码块称为函数名联编(binding)。在C语言中,这非常简单,因为每个函数名都对应一个不同的函数。在C++中,由于函数重载的缘故,这些任务更复杂。在编译过程进行联编被称为静态联编(static binding),又称为早期联编(early binding)。编译器必须生成能够在程序运行时选择正确的虚方法的代码,这称为动态联编(dynamic binding),又称为晚期联编(late binding)。
在C++中,动态联编与通过指针和引用调用方法相关,从某种程度上说,这是由继承控制的。
13. 编译器对非虚方法使用静态联编,对虚方法使用动态联编。
14. 动态联编让您能够重新定义类方法,为什么不将它设置为默认的?原因有两个——效率和概念模型。
为使程序能够在运行阶段决策,必须采取一些方法来跟踪基类指针或引用指向的对象类型,这增加了额外的处理开销。由于静态联编的效率更高,因此被设置为C++的默认选择。
在设计类时,可能包含一些不在派生类重新定义的成员函数。不将这些函数设置为虚函数,有两方面的好处。首先,效率更高;其次,指出不要重新定义该函数。这表明,如果要在派生类中重新定义基类的方法,则将它们设置为虚方法;否则,设置为非虚方法。
15. 虚函数的工作原理P504
编译器处理虚函数的方法是:给每个对象添加一个隐藏成员。隐藏成员中保存了一个指向函数地址数组的指针。这种数组称为虚函数表。编译器为每个类创建1个虚函数表,为该类的所有对象共享。无论类中包含的虚函数是1个还是10个,都只需要在对象中添加1个地址成员,只是表的大小不同而已。
调用虚函数时,程序将查看存储在对象中的虚函数表的地址,然后转向相应的函数地址表。如果使用类声明中定义的第一个虚函数,则程序将使用数组中的第一个函数地址,并执行具有该地址的函数。如果使用声明中的第三个虚函数,程序将使用地址为数组中第三个元素的函数。
总之,使用虚函数,在内存和执行速度方面有一定的成本,包括:
1) 每个对象都将增大,增大量为存储虚函数表的地址的空间;
2) 对于每个类,编译器都创建一个虚函数地址表(数组);
3) 对于每个函数调用,都需要执行一项额外的操作,即到表中查找地址。
16. 构造函数不能是虚函数。
基类应提供一个虚析构函数,即使它不需要析构函数。
17. 如果派生类没有重新定义虚函数,则将使用该函数的基类版本。
18. 重新定义继承的方法并不是重载,将隐藏方法。P506
19. 如果基类中函数被重载了,则应在派生类中重新定义所有的基类版本。否则没有重定义的版本将被隐藏,派生类对象无法使用它们。
- 20. private和protected的区别只有在基类派生的类中才会表现出来。派生类的成员可以直接访问基类的保护成员,但不能直接访问基类的私有成员。因此,对于外部世界来说,保护成员的行为和私有成员相似;但对于派生类来说,保护成员的行为与公有成员相似。
21. 最好对类数据成员采用私有访问控制,不要使用保护访问控制;同时通过基类方法使派生类能够访问基类数据。
然而,对于成员函数来说,包含访问控制很有用,它让派生类能够访问公众不能使用的内部函数。
22. 至少包含一个纯虚函数的类称为抽象基类。
C++通过使用纯虚函数提供未实现的函数,纯虚函数声明的结尾处为=0,参见Area()方法。
当类声明中包含纯虚函数时,则不能创建该类的对象。
23. 总之,在原型中使用=0指出类是一个抽象基类,在类中可以不定义该函数,也可以定义。
24. 可以将ABC看作是一种必须实施的接口。ABC要求具体派生类覆盖其纯虚函数——迫使派生类遵循ABC设置的借口规则。
25. 如果基类使用动态内存分配,并重新定义析构函数、赋值函数、复制构造函数,而派生类不使用动态内存分配,则派生类不需要定义显示析构函数、复制构造函数和赋值运算符。
首先,来看是否需要析构函数。如果没有定义析构函数,编译器将定义一个不执行任何操作的默认析构函数。该派生类析构函数执行自身代码后,调用基类析构函数。
接着看复制构造函数。默认复制构造函数执行成员复制,但复制类成员或继承的类组件时,则是使用该类的复制构造函数完成的。
对于赋值来说,也是如此。类的默认赋值运算符将自动使用基类的赋值运算符来对基类组件进行赋值。因此,默认赋值运算符也是合适的。
26. 当基类和派生类都采用动态内存分配时,派生类的析构函数、复制构造函数、赋值运算符函数都必须显式提供,而且都必须使用相应的基类方法来处理基类元素。这种要求是通过三种不同的方式来满足的。对于析构函数,这是自动完成的;对于构造函数,这是通过在初始化列表中调用基类的复制构造函数来完成的;如果不这样做,将自动调用基类的默认构造函数。对于赋值运算符,这是通过使用作用域解析运算符显式地调用基类的赋值运算符来完成的。P518
27. 对于基类,即使它不需要析构函数,也应提供一个虚析构函数。
28. 构造函数、析构函数、赋值运算符是不能被继承的。
29. 可以将派生类对象赋给基类对象,但此时赋值运算符只负责基类成员。
30. 问题“是否可以将基类对象赋给派生类对象”的答案是“也许”。如果派生类对象包含了这样的构造函数,即对将基类对象转换为派生类对象进行了定义(转换构造函数),则可以将基类对象赋给派生类对象。如果派生类定义了用于将基类赋给派生类对象的赋值运算符,则也可以这样做。如果上述条件都不满足,则不能这样做,除非使用显式强制类型转换。
31. 由于友元函数并非类成员,因此不能继承。然而,您可能希望派生类的友元函数能够使用基类的友元函数。为此,可以通过强制类型转换符,将派生类引用或指针转换为基类引用或指针,然后使用转换后的指针或引用来调用基类的友元函数。
32. 派生类方法可以使用作用域解析运算符类调用公有的和受保护的基类方法。