【C++ Primer 5th】Chapter 15
摘要:
1. 面向对象程序设计的核心思想是数据抽象、继承和动态绑定。数据抽象将类的接口和实现分离;继承定义相似的类型并对齐相似关系建模;动态绑定,在一定程度上忽略相似类型的区别,而以统一的方式使用它们的对象。
2. 派生类列表中每个基类前面可以有访问说明符;派生类必须在其内部对所有重新定义的虚函数进行声明;派生类可以在这样的函数前加上virtual关键字,但不是必须,因为在C++11中,允许派生类通过在函数形参列表之后增加overside关键字,以此来显式注明它将使用该成员函数改写基类的虚函数。
3. 基类通常都应该定义一个虚析构函数,即使该函数不执行任何实际操作也是如此。《Effective C++》(P41)写到:当派生类对象经由一个基类指针被删除,而该基类带着一个non-virtual析构函数,其结果未定义——实际执行时通常发生的时派生类对象的derived(继承)成分未被销毁,即“局部销毁”现象。消除这个问题的做法就是给基类一个virtual析构函数。如果一个class不含virtual函数通常表示它不打算被用作一个基类,因此当class不企图被当作基类时,我们就不应令其析构函数为virtual。通常的经验是:只有当class内至少含有一个virtual函数时我们才为它声明virtual析构函数。
4. 任何构造函数之外的非静态函数都可以是虚函数,virtual关键字只能出现在类内部的声明语句之前而不能用于类外部的函数定义。如果基类把一个函数声明成虚函数,则该函数在派生类中隐式地也是虚函数。
5. 派生类必须将其继承来的成员函数中需要覆盖的那些重新声明。访问说明符的作用是控制派生类从基类继承而来的成员是否对派生类的用户可见。
6. 一个派生类对象包含多个组成部分:一个含有派生类自己定义的(非静态)成员的子对象,以及一个与该派生类继承的基类对应的子对象(如果有多个基类,那么这样的子对象也将有多个)。C++没有明确规定派生类的对象在内存中如何分布。
7. 派生类必须使用基类的构造函数来初始化它的基类部分,即每个类控制自己的成员初始化过程。派生类构造函数通过构造函数初始化列表来将实参传递给基类构造函数(即调用基类的构造函数来初始化那些从基类继承而来的成员),否则派生类对象的基类部分将会像数据成员一样执行默认初始化。首先初始化基类的部分,然后按照声明的顺序依次初始化派生类的成员。派生类构造函数只初始化它的直接基类。
8. 每个类负责定义自己的接口。要想与类的对象交互必须使用该类的接口,即使这个对象是派生类的基类部分。
9 派生类的作用域嵌套在基类的作用域之内。如果基类定义了一个静态成员,则在整个继承体系中只存在该成员的唯一定义。
10. 如果想用某个类用作基类,则该类必须已经定义而非仅仅声明,一个类不能派生它本身。
11. C++11提供一种防止继承发生的方法:在该类名后加关键字final
12. 通常情况下,如果想把引用或指针绑定到一个对象上,则引用或指针的类型应与对象的类型一致,或者对象的类型含有一个可接受的const类型转换规则。但我们可以把基类的指针或引用绑定到派生类对象上(即派生类到基类的类型转换)。原因在于每个派生类对象都包含一个基类部分,而基类的指针或引用可以绑定到该基类部分,也就是说,不存在从基类到派生类的隐式类型转换,因为一个基类对象可能是派生类对象的一部分,也可能不是,即使一个基类指针或引用绑定在一个派生类对象上,也不能执行基类向派生类的隐式转换。派生类向基类的自动类型转换只对指针或引用类型有效。
D derived;
B *based = &derived; // 正确:动态类型是D
D *derivedP = based; // 错误:不能将基类转换为派生类
这是由编译器的特性决定的,编译器无法确定某个转换在运行时是否安全,只能通过检查指针或引用的静态类型来推断该转换是否合法。解决方法是:a) 如果基类含有一个或多个虚函数,可以使用dynamic_cast来请求类型转换,该转换的安全检查在运行时执行;b) 使用static_cast来强制覆盖掉编译器的检查工作。
13. 表达式的静态类型在编译时总是已知的,它是变量声明时的类型或表达式生成的类型;动态类型则是变量或表达式表示的内存中的对象的类型,在运行时才可知。如果表达式既不是指针也不是引用,那么它的动态类型永远与静态类型一致。对象的静态类型就是它在程序中被声明时所采用的类型。
14. 派生类与基类对象之间不存在自动类型转换,自动类型转换只对指针或引用类型有效。我们能够将一个派生类对象拷贝、移动或赋值给一个基类对象或者是用一个派生类对象初始化一个基类对象,不过只能处理派生类对象的基类部分,派生类部分被忽略掉了。
15. 所有虚函数都必须有定义。被调用的函数是与绑定到指针或引用上的对象的动态类型相匹配的那一个。需要强调的是动态绑定只有当我们通过指针或引用调用虚函数时才会发生;当我们通过一个具有普通类型(非引用非指针)的表达式调用虚函数时,在编译时就会将调用的版本确定下来。指针或引用的静态类型与动态类型不同正式C++语言支持多态性的根本所在。
16. 一旦某个函数被声明成虚函数,则在所有派生类中都是虚函数。如果派生类想定义自己的函数来覆盖掉某个继承来的虚函数,则它的形参类型必须与被它覆盖的基类函数完全一致;同时返回类型也必须与基类函数匹配(有一个例外:当类的虚函数返回类型是类本身的指针或引用时,该规则无效)。如果D由B派生得来,那么B的虚函数可以返回B*而D对应的虚函数可以返回D*,这就要求从D到B的类型转换是可以访问的。
17. 如果派生类定义了一个函数与基类虚函数名字相同但是形参列表不同,这是合法的,编译器会认为新定义的函数与基类原有的函数时相互独立的,也就是说派生类函数并未覆盖掉基类版本。这就有可能出现误操作(比如形参写错)而导致未能覆盖掉基类虚函数,解决方法是:C++11 允许使用override关键字来指明派生类中的虚函数,好处是若指明的函数没有覆盖掉基类版本,那么编译器将报错。同时,如果将函数定义成final,则之后任何尝试覆盖该函数的操作都将引发错误。override和final关键字位于形参列表(包括任何const或引用修饰符)以及尾置返回类型之后
18. 虚函数可以有默认实参。如果某次函数调用使用了默认实参,则该实参值由本次调用的静态类型决定。virtual函数是动态绑定的,但是缺省参数值缺失静态绑定的(《Effective C++》条款37)。这就可能会在“调用一个定义于派生类内的virtual函数”的同时,却使用了基类为它指定的缺省参数值。假如一个基类指针B *based指向了派生类对象D derived,当based指针调用虚函数时发生动态绑定,那么此时based指针动态类型时D*,但是静态类型是B*,所以被调用的虚函数是来自基类B的版本,那么缺省参数值也就是基类版本的了。关于C++允许这种现象出现的原因可以阅读《Effective C++》P182。所以为了不必要的麻烦,绝对不要重新定义一个继承而来的缺省参数值,这样基类和派生类中定义的默认参数就一致了。
19. 我们可以通过使用作用域运算符强制执行虚函数的某个特定版本,那么该调用将在编译期完成解析,不会再发生动态绑定。回避虚函数机制通常发生在特定情况下:一个派生类需要调用它覆盖的基类的虚函数版本。通常只有成员函数(或友元)中的代码才需要使用作用域运算符来回避虚函数机制
20. 一个纯虚函数无需定义,通过在函数体的位置(即在声明语句的分号之前)加上“=0”就可以将一个虚函数说明为纯虚函数,且“=0”只能出现在类内部的虚函数声明语句处。但是我们也可以为纯虚函数提供定义,不过该定义的函数体必须定义在类的外部。即不能类内为纯虚函数提供函数体。
21. 含有(或者未经覆盖直接继承)纯虚函数的类是抽象基类。注意是含有!不能(直接)创建一个抽象基类的对象。派生类必须对继承来的纯虚函数给出定义,否则该派生类也将是抽象基类。
22. public、protected和private 成员访问权限:
public: 类成员函数、类用户(即实例化后的对象)、子类成员及用户皆可访问,即整个程序内都可访问
protected: 类成员函数、子类成员可访问,类用户、子类用户不可访问
private: 类成员函数可访问,类用户、子类不可访问
#include <iostream> using namespace std; class B{
friend void printFriendB1(B &);
public:
void printB1(){
cout << "printB1" << endl;
}
protected:
void printB2(){
cout << "printB2" << endl;
}
private:
void printB3(){
cout << "printB3" << endl;
}
}; class D : public B{
friend void printFriendD1(B &);
friend void printFriendD2(D &);
public:
void printD1(){
cout << "printD1" << endl;
}
void printD11(B& based){
based.printB1();
based.printB2(); // 错误,类用户不可访问其protected成员
based.printB3(); // 错误,类用户不可访问基类对象的private成员
this->printB1();
this->printB2(); // 正确,派生类成员可通过派生类对象(this)来访问派生类对象中的基类部分的protected成员
this->printB3(); // 错误,派生类成员不可访问基类对象的private成员
}
protected:
void printD2(){
cout << "printD2" << endl;
basedTmp.printB1();
basedTmp.printB2(); // 错误,类用户不可访问其protected成员
basedTmp.printB3(); // 错误,类用户不可访问其private成员
}
private:
void printD3(){
cout << "printD3" << endl;
} B basedTmp;
};
void printFriendB1(B &based){
based.printB1();
based.printB2();
based.printB3();
}
void printFriendD1(B &based){
based.printB1();
based.printB2(); // 错误,类用户不可访问其protected成员
based.printB3(); // 错误,类用户不可访问其private成员
}
void printFriendD2(D &derived){
derived.printB1();
derived.printB2(); // 正确,派生类友元可通过派生类对象来访问派生类对象中的基类部分的protected成员
derived.printB3(); // 错误,派生类不可访问基类private成员
derived.printD1();
derived.printB2();
derived.printD3();
}
int main(){
B based;
D derived;
based.printB1();
based.printB2(); // 错误 类用户不可访问protected成员
based.printB3(); // 错误 类用户不可访问private成员
derived.printB1();
derived.printB2(); // 错误 派生类用户不可访问基类部分的protected成员
derived.printB3(); // 错误 派生类用户不可访问基类部分的private成员
derived.printD1();
derived.printD2(); // 错误 类用户不可访问protected成员
derived.printD3(); // 错误 类用户不可访问private成员 return 0;
}
派生类的成员和友元只能访问派生类对象中的基类部分的受保护成员,对于普通的基类对象中的成员不具有特殊的访问权限。
23. 某个类对其继承来的成员的访问权限受到两个因素影响:a) 在基类中该成员的访问说明符;b) 在派生类的派生列表中的访问说明符
public、protected和private 继承访问权限:(基类中的访问权限:继承方式:派生类中的访问权限)
public protected继承 protected
public public继承 public
public private继承 private
protected public继承 protected
protected protected继承 protected
protected private继承 private
private public继承 无访问权限
private protected继承 无访问权限
private private继承 无访问权限
派生访问说明符对于派生类的成员(及友元)能否访问其直接基类的成员没什么影响(注意是直接基类)。派生访问说明符的目的是控制派生类用户(包括派生类的派生类在内)对于基类成员的访问权限。
24. 派生类向基类的转换是否可访问由使用该转换的代码决定,同时派生类的派生访问说明符也会有影响。
a) 只有当D公有继承B时,用户代码才能使用派生类向基类的转换;受保护和私有继承都不能使用该转换
b) 不论D以何种方式继承B,D的成员函数和友元都能使用派生类到基类的转换;派生类向其直接基类的类型转换对于派生类的成员和友元来说永远时可访问的。
c) 如果D公有或受保护继承B,那么D的派生类的成员和友元可以使用D向B的类型转换,私有则不可。
25. 友元关系既不能传递也不能继承。基类的友元可以访问基类对象的成员,包括基类对象内嵌在其派生类对象中的情况。
26. 通过在类内使用using声明语句,可以将该类的直接或间接基类中的任何可访问成员标记出来。using声明语句中的名字的访问权限由该using声明语句前的访问说明符来决定。需要注意的是派生类只能为它可以访问的名字提供using声明。
27. 当存在继承关系时,派生类的作用域嵌套在其基类的作用域之内。如果一个名字在派生类的作用域内无法正确解析,则编译器将继续在外层的基类作用域中寻找该名字的定义。派生类的成员将隐藏同名的基类成员,不过可以使用作用域运算符来使用一个被隐藏的基类成员。
28. 名字查找先于类型查找。声明在内层作用域的函数并不会重载声明在外层作用域的函数(重载发生在同一作用域内)。因此定义在派生类中的函数不会重载基类的成员,而是隐藏,即使形参列表不一致。所以除了覆盖继承而来的虚函数之外,派生类不要重用其他定义在基类中的名字。
29. 基类中虚函数假如有重载版本,那么派生类要么全部覆盖要么一个也不覆盖,因为部分覆盖会因名字相同而隐藏掉未覆盖的基类同名虚函数。而如果只想覆盖重载集合的一部分,可以使用using声明语句将所有重载实例添加到派生类作用域中(using声明语句指定一个名字而不指定形参列表),此时只需要定义需要覆盖的函数即可,免去了繁琐的覆盖操作。
30.
练习:
15.2.3
练习15.8:
给出静态类型和动态类型的定义:
静态类型在编译时总是已知的,它是变量声明时的类型或表达式生成的类型;
动态类型直到运行时才知道,它是变量或表达式表示的内存中的对象的类型;
另,如果表达式既不是引用也不是指针,则它的动态类型和静态类型永远一致。
练习15.9:
在什么情况下表达式的静态类型可能与动态类型不同?请给出三个静态类型与动态类型不同的例子
如果表达式既不是引用也不是指针,则它的动态类型和静态类型永远一致。
基类的指针或引用的静态类型可能与其动态类型不一致
练习15.18:
只有当派生类公有地继承基类时,用户代码才能使用派生类向基类的转换;也就是说,如果派生类继承基类的方式是受保护的或者私有的,则用户代码不能使用该转换。
只有d1和dd1赋值是合法的,因为只有d1和dd1类是公有地继承基类。
练习15.24:
作为基类使用的类应该具有虚析构函数,以保证在删除指向动态分配的对象的基类指针时根据指针实际指向的对象所属的类型运行适当的析构函数。
虚析构函数可以为空,即不执行任何操作。一般而言,析构函数的主要作用是清除本类中定义的数据成员。如果该类没有定义指针类成员,则使用合成版本即可;如果该类定义了指针成员,则一般需要自定义析构函数以对指针成员进行适当的清除。因此,如果有虚析构函数必须执行的操作,则就是清除本类中定义的数据成员的操作。
【C++ Primer 5th】Chapter 15的更多相关文章
- 【C++ Primer 5th】Chapter 1
1. 每个C++都包含至少一个函数,其中一个必须为main函数,且 main 函数的返回类型必须为 int. 2. 函数定义包括:返回类型,函数名,形参列表,函数体 3. main 函数返回值用来指示 ...
- 【c++ primer, 5e】参数传递
p187~p199: [传值参数] [传引用参数] 使用引用形参返回额外信息:传入一个(或者多个)额外的引用实参,令其保存字符出现的次数. 6.11 #include <iostream> ...
- 【c++ primer, 5e】设计Sales_data类 & 定义改进的Sales_data类
[设计Sales_data类] 1.考虑Sales_data类应该有那些接口. isbn.combine.add.read.print... 2.考虑如何使用这些接口. Sales_data tota ...
- 【c++ primer, 5e】函数重载
[函数重载] Java中的重载一般是指重载构造器,或是子类覆写父类的方法:C++中的重载稍微复杂一些. 定义重载函数 典型的数据库应用. Record lookup(const Account& ...
- 【c++ primer, 5e】返回类型和return语句
[无返回值函数] 1.在c++的void函数中,可以显式地使用return;语句来提前结束函数的调用. [有返回值函数] 1.值是如何被返回的:返回一个值的方式和初始化一个变量或者形参的方式完全一样. ...
- 【c++ primer, 5e】类的其他特性(卒)
1 - Class Members Revisited 2 - Functions That Return *this 3 - Class Types 4 - Friendship Revisited ...
- 【互动问答分享】第15期决胜云计算大数据时代Spark亚太研究院公益大讲堂
"决胜云计算大数据时代" Spark亚太研究院100期公益大讲堂 [第15期互动问答分享] Q1:AppClient和worker.master之间的关系是什么? AppClien ...
- 【c++ primer, 5e】构造函数 & 拷贝、赋值和析构
[构造函数] 1.构造器就是创建对象时被调用的代码. 2.如果没有自定义构造器,那么编译器将自动合成一个默认的无参构造器. 3.自定义的构造器不允许加const,所创建const的对象只有在构造器代码 ...
- 【网络流24题】 No.15 汽车加油行驶问题 (分层图最短路i)
[题意] 问题描述:给定一个 N*N 的方形网格,设其左上角为起点◎, 坐标为( 1, 1), X 轴向右为正, Y轴向下为正, 每个方格边长为 1, 如图所示. 一辆汽车从起点◎出发驶向右下角终点▲ ...
随机推荐
- Chrome自带恐龙小游戏的源码研究(三)
在上一篇<Chrome自带恐龙小游戏的源码研究(二)>中实现了云朵的绘制和移动,这一篇主要研究如何让游戏实现昼夜交替. 昼夜交替的效果主要是通过样式来完成,但改变样式的时机则由脚本控制. ...
- 创业神人&当时钢铁侠:Elon Musk
Steve Jobs的光环已经随着他的离去而淡褪,短期内,世上恐怕再难有人像他这样惊世骇俗般的改变了世界.但是如果你了解到一个人,一个来自南非年仅40岁的企业家,在短短的20年里,在全世界最酷的三个领 ...
- sealed,new,virtual,abstract与override关键字的区别?
1. sealed——“断子绝孙” 密封类不能被继承.密封方法可以重写基类中的方法,但其本身不能在任何派生类中进一步重写.当应用于方法或属性时,sealed修饰符必须始终与override一起使用. ...
- 小明同学喜欢体育锻炼,他常常去操场上跑步。跑道是一个圆形,在本题中,我们认为跑道是一个半径为R的圆形,设圆心的坐标原点(0,0)。小明跑步的起点坐标为(R,0),他沿着圆形跑道跑步,而且一直沿着一个方向跑步。回到家后,他查看了自己的计步器,计步器显示他跑步的总路程为L。小明想知道自己结束跑步时的坐标,但是他忘记自己是沿着顺时针方向还是逆时针方向跑的了。他想知道在这两种情况下的答案分别是多少。
include "stdafx.h" #include<iostream> #include<vector> #include<string> ...
- 九度OJ 1019:简单计算器 (基础题、DP)
时间限制:1 秒 内存限制:32 兆 特殊判题:否 提交:6725 解决:2454 题目描述: 读入一个只包含 +, -, *, / 的非负整数计算表达式,计算该表达式的值. 输入: ...
- nohup COMMAND > FILE
nohup --help nohup(1) - Linux man page https://linux.die.net/man/1/nohup
- python 搜集参数的共有项和所有项
搜集共性项和所有项 ###搜集共有参数值 def intersect(*args): res=[] for x in args[0]: for other in args[1:]: if x not ...
- 微信小程序配置详解
在之前已经通过微信公众平台的官方网站https://mp.weixin.qq.com/debug/wxadoc/dev/devtools/devtools.html,注册好小程序并且登录成功后(这里主 ...
- shell执行lua脚本传参数
#lua test.lua 2 5arg[0]= test.lua arg[1]= 2arg[2]= 5 if arg[1] and arg[1] == "2" then prin ...
- wamp server 安装后 Apache80端口占用
提示:Your port 80 is actually used by :Server: Microsoft-HTTPAPI/2.0 解决方案:计算机->右键管理->服务和应用程序-> ...