子类的构造
任何一个子类对象中都包含着它的"基类子对象".
基类子对象: 在子类对象中包含一个基类的对象
任何一个子类对象都可以被视为它的基类对象 --- is a
任何时候,一个子类对象的引用或者指针,都可以被隐式转换为它的基类类型的引用或者指针
基类类型的引用,如果引用子类对象, 它其实是引用的基类子对象(只引用了子类对象的一部分)
如果一个类有多个父类,通过不同的父类,引用同一个子类对象,引用的不是同一部分
通过不同父类的指针,指向同一个子类对象,指针的值可能不一样的
虽然子类对象可以隐式转换为基类类型的引用 和 指针类型
但是反过来却不可以隐式转换 必须显示类型转换 static_cast<>
什么时候必须用初始化列表?
1.常属性和引用属性
2.显示调用父类或者成员属性的有参构造函数
如果把一个父类类型对象 通过 强制类型转换 或者 static_cast 或者 reinterpret_cast
转变了子类对象, 如果父类类型引用的对象本质不是子类对象的类型,则能够转换成功
但是后续可能出来未知的错误
如果父类类型的引用 引用子类对象,或者父类类型的指针指向子类对象
通过强制类型转换和静态类型转换,成功地把父类的指针和引用 指向/引用正确的子类对象
通过reinterpret_cast把父类对象的指针强制转换为子类对象的指针
能够成功,但是可能引发问题
子类对象可以直接赋值给父类对象 调用父类的拷贝赋值函数
子类对象可以构造新的父类对象 调用父类的拷贝构造函数
子类中的构造函数,会默认按照继承顺序调用父类的无参构造函数进行构造基类子对象
构造函数的执行顺序:
1.按照继承顺序依次调父基类的构造函数(如果一个基类它还有基类,则会一直去调用基类的基类构造函数)
2.按照成员属性的定义顺序,依次调用各个类类型属性的构造函数
3.执行本类的构造函数体
析构函数的执行顺序和构造函数正好相反
如果用一个父类类型的指针 指向一个new的子类对象
在delete这个父类类型指针时,只会调用父类析构函数函数,内存泄露
在构造函数(拷贝构造除外)的初始化列表中,会依次调用父类的无参构造函数和成员类型的无参构造
如果父类中没有无参构造,子类将必须在初始化列表中显示调用父类的有参构造函数
子类默认的拷贝构造函数,会依次调用父类的拷贝构造函数 和 成员的拷贝构造
如果子类实现拷贝构造函数,默认在初始化列表中调用其类 和 类类型成员的无参构造
一般需要在初始化列表中调用基类和类类型成员的拷贝构造函数
多继承与多重继承
多继承语法和语义上与单继承没有本质的区别,只是子类对象中包含了更多的基类子对象
这些基类子对象在内存中按照继承表的先后顺序从低地址到高地址依次排列
子类对象可以用任意类型的基类引用变量来引用
子类对象的指针可以隐式转换为任意基类类型的指针
无论是隐式转换,还是静态转换,编译器都能保证特定类型的基类指针指向相应的基类子对象
通过父类类型指针指向子类对象,通过强制类型转换和静态类型转换,
编译器能够保证转换之后指针指向的是子类对象的地址
但是,重解释类型转换,无法保证上面两种情况
重解释类型转换之后 地址值不会发生变化
强制类型转换 和 静态类型转换 隐式类型转换 地址值可能发生变化
成员属性和方法尽量避免重名
如果要访问重名的基类属性时: 基类名::属性名
子类对象.基类名::属性名
钻石继承 和 虚继承
A
/ \
B C
\ /
D
class A{};
class B:public A{};
class C:public A{};
class D:public B,public C{};
钻石继承: 如果一个类有多个基类,而这多个基类又有公共的基类
沿着不同的继承路径,公共的基类子对象会在最终子类对象中有多份实例
通过子类对象去访问时,可能造成数据不一致的问题
虚继承virtual
在继承表中通过virtual关键字指定从公共基类中虚继承,这样就可以保证
公共的基类和最终的子类对象中,仅存在一份公共基类子对象实例,
避免了沿着不同的继承路径访问公共基类子对象成员时,所引发不一致问题
只有当所创建对象的类型回溯中存在钻石结构,虚继承才起作用,
否则编译器会自动忽略virtual关键字
用父类指针指向子类对象,只能调用父类中的方法
用父类的指针指向new子类对象,delete只调用父类的析构函数,造成内存泄漏
到目前为止,调用哪个类的方法和析构函数,取决于引用或指针的类型,而非目标类型
虚函数 与 多态
如果将基类中的成员函数声明为虚函数,那么子类中可以重写基类中该虚函数
(子类无论是否有virtual关键字都是虚函数)这样对基类中的虚函数形成覆盖。
这时,通过一个基类类型的指针指向子类对象时,
或者通过一个基类类型的引用引用子类对象时,
调用该虚函数时,实际被调用的函数不由指针或者引用本身的类型决定,
而是由它们的目标对象来决定,最终导致子类覆盖版本的函数被调用。
这种现象称为多态。
如果一个类中有虚函数(个数只要大于0),
那么该类或者对象在用sizeof求字节数时比没有虚函数时大4byte/8byte
多出的4/8byte 指针
多态: 虚函数/覆盖 + 指针/引用
class CLA_NAME{
virtual RET_TYPE func_name(arglist...){
}
};
重载
在同一个作用域下,函数名相同,参数列表不同即构成重载
条件:
必须在同一个作用域(类)下
函数名相同
参数列表不同: 参数类型和参数的个数不一样 对于指针和引用常属性不一样也构造重载
与返回值类型无关
与函数是否是虚函数无关
在编译时,根据调用时传递的实参类型和个数来绑定调用的函数,静态绑定
覆盖
子类重写父类同型的虚函数
条件:
分布在父子类中
函数名相同
参数列表必须相同:参数类型和个数必须相同 函数的常属性必须相同 参数是指针和引用 常属性也必须相同
与返回值类型有关 如果不是类类型指针或者类类型引用,必须完全一致,否则可以有父子关系
基类必须是虚函数 重写的版本也一定是虚函数
在运行时,根据指针所指向的目标类型 或者 根据引用所引用的目标类型,来决定调用哪个版本的方法
动态绑定 在运行时才决定调用哪个函数
隐藏
子类隐藏父类同名的标识符(只讨论函数)
条件:
分布在父子类中
函数名相同
如果参数不同,不管是否有virtual关键字,都构成隐藏
如果参数一致,基类函数没有virtual关键字,构成隐藏,否则构成覆盖
虚析构函数
将基类的析构函数声明为virtual虚函数,delete一个指向子类对象的基类指针时
实际被执行的将是子类的析构函数,而子类的析构函数可以自动调用基类的析构函数
进而保证子类特有的资源和基类子对象中的资源都能够得到释放,从而防止内存泄漏
如果一个类存在虚函数,那么就有必要为其定义一个虚析构,即使析构函数中啥也不干
虚析构的作用:
防止内存泄漏
虚函数可以是声明为内联函数吗? 不可以
指针/引用 调用虚函数时 只有到运行时才能确定调用哪个函数
内联函数 在编译时 用函数的二进制代码替换掉调用指令
因为内联函数没有地址,而虚表里面存放的就是虚函数的地址
一个构造函数可以被定义为虚函数吗? 不可以的 因为虚函数是存放在对象的虚表里面,如果将构造函数定义为虚函数,
则构造函数也必须存放在虚表里面,但是此时对象都还没有创建也就没有所谓的虚表。
一个类的静态成员函数可以被定义为虚函数吗? 不可以
因为虚函数是放在对象的虚表里面的,同一个类中的所有对象虽然共用同一张虚表,但是类名无法找到虚表
一个全局函数可以被定义为虚函数吗? 不可以
一个类的成员操作符函数可以被定义为虚函数吗? 可以的
纯虚函数
有的时候基类的虚函数实现没有任何意义,只是为了提供一个接口(函数的声明)让子类去重写
就可以把虚函数不实现函数体,从而定义为纯虚函数
virtual RET_TYPE func_name(arg list,...) [const] = 0;
纯虚函数:没有函数体(函数体用 = 0替换掉)的虚函数
一个拥有纯虚函数的类,称为抽象类
抽象类:拥有纯虚函数的类
抽象类也称为不完全类
抽象类不能实例化对象
抽象类可以允许拥有自己的成员属性和构造函数,但就是不允许实例化对象
如果一个类继承自抽象类,但是没有为抽象基类中的全部纯虚函数提供覆盖,
那么该类也是一个抽象类
纯抽象类:
除了构造函数和析构函数以外,所有的成员函数都是纯虚函数的类称为纯抽象类
抽象类既然不能实例化对象,那么抽象类的意义在哪:
1.为子类提供一个公共的基类类型 可以定义抽象类的引用 和 指针 引用子类对象 指向子类对象
2.定义统一的接口(虚函数) 为子类重写提供一个模型
3.封装子类共同拥有的属性和方法 提高代码的复用
4.虽然不能实例化对象 但依然能够实现多态的效果
虚函数表 与 虚表指针
虚函数表:
如果一个类中存在虚函数,那么在编译该类时,编译器会为该类生成一个虚函数表
所谓虚函数表,即存储虚函数地址的数组 (函数指针数组) environ环境列表
数组中的每一个项都存储一个虚函数的地址(指针)
如果存储继承关系,那么在生成子类的虚函数表时,会把父类的虚函数表拷贝过来
然后,子类如果提供了覆盖版本的函数,则修改相对应函数的地址
如果子类没有提供覆盖版本,则是父类函数对应的地址
而且在虚函数表中添加子类自己定义的虚函数地址
虚表指针:
拥有虚函数类的对象都会有4/8byte一个指针,该指针指向该类的虚函数表
一个类只有一个虚函数表 一个类所有对象的虚表指针的值都是一样的
如果是多继承(基类都是有虚函数),则会有多个虚表指针,
从不同的基类继承拷贝一份进行覆盖
多态 动态绑定
当编译器看到通过指向子类对象的基类指针或者引用子类对象的基类引用,
调用基类中的虚函数时,并不会直接生成函数的调用代码,
相反,会在该函数调用地方生成若干指令,这些指令在程序运行时被执行,依次完成如下功能:
(1)根据指针或引用的目标对象找到对应的虚函数表的指针(虚表指针)
(2)根据虚函数表指针,找到该类对应虚函数的地址
(3)根据虚函数的地址,指向对应虚函数的代码,
(4)执行虚函数的代码指令
故,多态是运行时绑定,动态绑定,指的是只有当运行阶段才能确定调用的函数
动态绑定对程序的性能会造成一定的影响,(让程序的运行效率变慢),如果没有必要实现多态时
就不要用虚函数
C++的异常处理
1.通过返回值表达错误
( 如果是调用系统函数出错 设置全局的errno )
层层判断返回值,流程变得繁琐
错误处理和正常逻辑 代码是混在一起
局部对象能够正常析构
2.通过setjmp/longjmp远程跳转
可以一步到位进行错误处理,流程简单
局部对象将无法得到析构 内存泄露
3.异常处理
可以一步到位进行错误处理 流程简单
局部对象能够得到释放
C++中错误可以通过抛出异常来表示
当C++执行代码过程中遇到异常,就会生成一个异常对象,然后进行抛出
(从产生错误的代码行直接跳出),去最紧密的catch分支进行比对,
只要在catch中有捕获对应类型的分支,从到此catch分支进行处理
处理之后,代码能够正常往下执行
当所有的catch分支都没有捕获该异常类型时,该异常继续外抛出
异常捕获的语法规则:
try{
}catch(类型1& e){
if(e == 1){
}else if(e==2){
}
}catch(类型2& e){
}catch(类型3& e){
}....
catch(...){
}
一般用不同的异常类型来表示不同的错误
同一种错误可以用不同的值来表示不同地主出现的错误
异常的抛出语法:
throw 异常对象;
异常对象可以是基础数据类型的变量,也可以是类类型的对象
一般用专门的异常类型对象
捕获异常是根据类型来捕获的
子类的异常捕获要在父类异常捕获之前
函数的异常声明
在一个函数的形参表后面,添加函数的异常说明
RET_TYPE func_name(arglist,...)throw(异常类型1,异常类型2,...){}
异常说明表示这个函数可以被捕获的异常类型
如果在该函数中抛出了异常说明之外的异常类型,即使try_catch也无法捕获
throw () 这个函数所抛出的异常均无法捕获
没有异常说明,表示该函数抛出的任何异常都可以捕获
重写对于函数异常说明的限制:
子类不能声明比父类更多类型的异常说明
特别注意:
throw (A);
throw (B,C,D);
|