细节跳过:
- P 535
细节发现 - P540 虚基类
由于不考, 所以这里跳过 - P542~ 544
跳过, 感觉和笔记的内容重复
需要进行校验 - P545 友元后的内容
暂时先跳过 - P551
覆盖重载函数的using解决方案 - P552合成拷贝控制与继承
后头都没有看, 待拓展
类继承:
派生类的作用:
派生类继承了基类除构造函数、析构函数以外所有的数据成员和成员函数,实现了代码重用
派生类必然具有某些和基类不同的属性和行为,需要对基类进行扩充和改造。
扩充:
在派生类中增加新的成员函数和数据成员。
改造:
当继承而来的成员不能满足需要时,可以进行覆盖。覆盖是在派生类中定义与基类同名的函数,覆盖也可针对数据成员。慎用!
区分重载:
在同一个类中定义同名函数但参数不同。
定义:
前提:
作为派生类的基类, 必须在派生类定义时已经给出定义, 而不仅仅是声明
派生类的基类的定义:
class baseClassName{
public:
...;
virtual ~baseClassName() =default;
protected:
...;
private:
...;
}
基类的定义与普通类相同
但是通常需要添加一个virtual的虚析构函数用于后头的动态绑定
即使该函数不执行任何操作
派生类的定义:
使用类派生列表定义基类的派生类, 标准格式:
class className : 继承方式 baseClassName{
public:
...;
protected:
...;
private:
...;
}
几种继承方式的区别:
公有继承public:
基类的公有成员在派生类中仍然是公有成员;派生类中可以访问;通过派生类对象可以访问。
基类的保护成员在派生类中仍然是保护成员;派生类中可以访问;通过派生类对象不可以访问。
基类的私有的成员在派生类中不可访问,即在派生类成员函数中,不能访问从基类继承过来的私有成员,虽然它们存在于内存中;通过派生类对象更不能访问。
看图说话:
保护继承protected:
基类的私有成员在派生类中是不可访问的,而公有和保护成员成为派生类的保护成员。
通过派生类的对象不能访问基类的任何成员,需要在派生类中定义公有接口。
通过保护继承,基类中的保护和公有成员变成派生类的保护成员,将该派生类作为基类再继续派生时,只要不使用私有继承,它们在派生类中可以继续访问。
看图说话:
私有继承private:
private继承与protected继承相似, 唯一不同的是:
基类的公用成员和保护成员都以私有身份出现在派生类中
而protected继承中是:
基类的公用成员和保护成员都以保护身份出现在派生类中
基类中的static成员:
- 静态成员也可以被继承, 但仍然受到访问控制
(与其他成员的访问控制规则相同) - 父类和子类中的同名static数据成员共用空间
即在父类和子类中定义多个同名的static数据成员实际上在内存中只有一个 - 对于static成员函数, 没有虚函数
因为static成员函数相当于加上了访问控制的全局函数 - 子类中的static函数会覆盖父类中同名的static函数
声明:
声明的作用一直都是使编译器知道一个名字的存在以及名字对应的类型, 对于类的声明也是如此
几个注意点:
-
作为派生类的基类, 必须在派生类定义时已经给出定义, 而不仅仅是声明
-
声明派生类时, 无需加上其派生列表:
class Bulk_quote : xxxxxx; //ERROR class Bulk_quote; //RIGHT
友元与继承:
和之前的友元关系不能传递一样, 友元关系同样不能被继承
一个类的友元只能访问此类的对象, 而不能访问他的派生类和基类的对象
防止继承:
在类的名字后加一个final即可防止此类作为基类被继承:
//如果此时NoDerived被继承, 编译器会报错
class NoDerived final{
...
}
继承与覆盖:
派生类可以使用直接基类&间接基类中的成员, 也可以在派生类中重新定义他们, 这将发生覆盖
如:
class Point_1D{
public:
int output(int a=111, int b=111){
return a+b;
}
};
class Point_2D:public Point_1D{
public:
//覆盖了Point_1D中的output
int output(int a, int b){
return a*b;
}
//output的另一个重载版本
int output (){
return 2;
}
};
class Point_3D:public Point_2D{
public:
//覆盖了Point_2D中的output的两个重载版本
int output (){
return 3;
}
};
注意编译器名字查找的过程, 实际上找的只是函数名, 并非函数的签名(包括函数的返回值&参数列表&名字)
所以只要找到与调用处同名的函数, 就会结束查找:
但是, 除了下头的虚函数, 派生类最好不要覆盖基类中的其他函数
都TM是挖坑给自己跳
编译时的名字查找&覆盖技术:
其实覆盖的本质上还是编译器的名字查找的问题, 编译器沿着从内向外的作用域范围查找匹配的函数名字
注意: 派生类的作用域在基类的作用域之内
大致的查找流程:
所以当派生类中定义了与基类中同名的函数, 编译器直接在派生类的作用域中找到了匹配对象, 就直接退出查找了, 相当于基类中的同名函数被覆盖了
关于名字查找与类型匹配:
类型匹配在名字查找之后
即在退出名字查找后才开始将函数调用处的类型与找到的类型进行匹配
对于基类中的重载函数:
由于重载函数具有相同的函数名, 仅仅是函数的参数列表不同
所以, 如果派生类中定义的同名的函数, 会覆盖基类中所有版本的同名重载函数
如:
//接上头的类定义:
int main(){
class Point_1D p1;
class Point_2D p2;
class Point_3D p3;
//在Point_3D中找到匹配的函数, 结束名字查找, 使用Point_3D的output
printf("a+b=%d\n",p3.output ());
//报错, Point_3D中只有一个output(void)函数,没有找到output(int,int)的类型
printf("a+b=%d\n",p3.output (555,555));
return 0;
}
访问被覆盖的成员:
通过作用域运算符, 直接使编译器到指定的作用域中查找相应的名字, 可以访问到被覆盖的对象
构造&析构函数:
核心:
每个类控制它自己的成员的初始化&析构过程
在派生类中, 派生类的构造函数通过初始化列表将实参传递给基类的构造函数
//Circle为Point的派生类
Circle(double a1,double b1, double a2, double b2):
Point(a,b), ... {} //直接Point()
注意:
- 构造函数和析构函数不能被继承
- 不能在派生类中显式调用基类构造函数
在创建派生类对象时,按顺序由系统自动调用基类和派生类的构造函数 - 不能在派生类中显式调用基类的析构函数
- 最好定义基类的虚析构函数
详情见下头
派生类中构造&析构函数的调用顺序:
构造函数的调用顺序:
- 原则1:父子类之间,先基类构造,再派生类构造;
- 原则2:同一个类中,先对象成员,再本类构造函数体
析构函数的调用顺序与之相反!
类型转换&继承:
每一个派生类对象都可以看做是有基类对象加上派生类中的额外内容, 所以:
- 派生类的对象可以赋值给基类对象
- 派生类的对象可以初始化基类的引用
- 指向基类的指针也可以指向派生类
- 通过基类对象名、指针只能使用从基类继承的成员
但是如上使用时, 对于基类中没有的东西会被切掉
关于指针和引用的理解:
因为指针只是一个地址, 访问时根据指针的类型解析内存内容
而派生类和基类中存有相同部分的东西, 所以基类的指针可以指向派生类对象, 但是只能通过该指针访问基类部分的内容
几个转换的注意点:
- 不存在从基类向派生类的隐式类型转换
- 对象之间无法类型转换, 只能转换对应的指针或是引用
指向派生类/基类的指针&引用的类型转换:
通常:
只允许将派生类的指针或引用转换为指向基类的指针&引用, 反过来时通常出错
<前置概念>静态类型&动态类型:
-
当一个表达式不是指针也不是引用时, 其的静态类型和动态类型 相同
-
但是, 当其为指针或引用时, 编译期绑定的类型和程序实际运行期的类型可能不同
例如:
double print_total (ostream &os, const Quote &item, size_t n){
double ret=item.net_price(n); //此时item的类型为Quote&
......
}
//但是, 如果调用时传一个Bulk_quote对象给item
//则item的静态类型和动态类型就不相同了
dynamic_cast显式类型转换:
-
dynamic_castの主要用途:
将基类的指针或引用安全地转换成派生类的指针或引用,并用派生类的指针或引用调用非虚函数
如果是基类指针或引用调用的是虚函数, 无需转换就能在运行时调用派生类的虚函数。 -
dynamic_castの使用条件:
使用dynamic_cast进行转化时, 只有该类型含有虚函数时,才能进行这种转
否则,编译器会报错。 -
dynamic_cast的使用方法:
dynamic_cast<type*>(e) //e是指针 dynamic_cast<type&>(e) //e是左值 dynamic_cast<type&&>(e) //e是右值
其中, e能成功转换为type*类型的情况有三种:
-
派生类到基类的转换:
派生类向基类转换一定会成功 -
基类到派生类的转换:
通常不允许基类到派生列的转换, 但是有以下两种情况会成功:
- e是指向派生类对象的指针
- e是基类的引用, 但引用的是派生类对象
-
自身到自身的转换
type到type的转换一定会成功
-
-
dynamic_cast失败:
若转换失败,返回空指针或抛出异常(引用)
所以使用dynamic_cast还是比较安全的
其他显式类型转化:
基本不安全的, 头铁的可以试一下, 即使编译通过也会有问题
但是如果确定转化是安全的, 可以使用static_cast进行转化
虚函数:
virtual为C++多态实现的核心
当基类中的某个函数希望派生的类对其覆盖(重新定义)时, 通常将其定义为虚函数
当使用基类的指针或引用时, 会触发动态绑定:
静态&动态绑定:
静态: 编译时已经确定, 在运行时无法修改(效率较高, 但是灵活性较差, 穷举有限)
动态: 编译时无法确定, 当程序实际运行, 到达指令执行时才能确定, (具有跟高的灵活性和扩展性, 但效率较低)
根据之前指针和引用的动态类型与静态类型
可得, 当使用基类的指针或引用调用virtual函数时(包括引用&指针), 会根据动态绑定选择匹配的版本, 提升程序运行时的灵活性
虚函数的定义&继承:
//基类中的定义
virtual void shout() {
cout<<“I’m a mammal.\n”;
}
//子类中的覆盖:
virtual void shout() { //与基类中的函数签名完全相同
cout<<“meow.\n”;
}
//定义了className类的一个虚析构函数
virtual ~className() =default;
关于虚函数的继承
- 当子类中函数的签名和父类中的一个虚函数相同时, 发生覆盖
- 当其不相同时, 仅仅相当于添加了一个函数而已
- 如果在派生类中没有对基类中的虚函数进行覆盖, 派生类中会直接继承在基类中的版本
特别提醒:
子类在继承并覆盖基类的虚函数时, 最好加上virtual
虽然一旦一个函数被声明为virtual, 它在整个继承体系中就都是virtual
但是在子类中再加一个virtual可以突出其虚函数的特性, 方便阅读
override强制覆盖:
为了防止因为手滑而写错的存在
override用于子类继承并覆盖基类的虚函数(只能是虚函数)时的声明:
virtual void method(int) override;
显式的说明了此函数是对基类中某一虚函数(只能是虚函数)的覆盖
方便阅读的同时, 令编译器强制检查基类中有无签名相同的函数, 如果没有则报错
函数签名: (函数名& 参数列表& 返回类型相同)
虚函数&默认实参:
虚函数的允许使用默认实参
注意点:
-
当调用含有默认实参的虚函数时, 实参的值由其静态类型决定
即, 在一个多重继承体系中, 如果使用第二层派生类的指针或引用调用虚函数, 则最后传入的实参是第二层派生类中给定的实参, 而不是第二层派生类再派生出的类中的虚函数
如:
//myClass.h class Point_1D{ public: virtual int output(int a=111, int b=111){ return a+b; } }; class Point_2D:public Point_1D{ public: virtual int output(int a=222, int b=222){ return a+b; } }; class Point_3D:public Point_2D{ public: virtual int output(int a=333, int b=333){ return a+b; } };
//main.cpp void testFun(Point_2D &p){ printf("a+b=%d\n", p.output ()); return; } int main(){ class Point_1D p1; class Point_2D p2; class Point_3D p3; testFun(p3); testFun(p2); // testFun(p1); //报错, 子类引用不可绑定基类成员 return 0; }
输出结果:
a+b=444
a+b=444 -
基类和派生类中的虚函数的默认实参最好相同
正是因为上头的原因, 当子类中的默认参数和基类中不同时, 可能会输出和我们意料之外的结果
但是如果就是要求子类的默认实参和基类不一样, 则可以这样做
指定虚函数的版本:
指定了虚函数的版本相当于回避了虚函数的多态机制
通过作用域运算符可以指定使用哪个派生类或是基类的虚函数
如:
//将上头的testFun改成如下版本:
void testFun(Point_2D &p){
printf("a+b=%d\n", p.Point_1D::output ());
return;
}
输出结果:
a+b=222
a+b=222
虚析构函数:
存在の意义:
之前写到, 当一个指针指向继承体系中的类对象时, 可能出现静态类型与动态类型不匹配的状态
并且, 当delete指针时, 会调用对应类类型的析构函数
所以, 当delete静态类型和动态类型不匹配的指针时, 如基类指针指向的是派生类, 但是调用的却是基类的析构函数, 此时delete指针会发生内存泄漏, 并且会产生UB未定义行为
解决的方案, 就是将基类的析构函数定义为virtual, 使得调用的析构函数与动态类型相同
当基类定义的虚析构函数后, 销毁对象时, 调用子类的析构函数, 进而再调用基类的析构函数
定义方法:
virtual ~Quote() =default;
virtual ~Quote() =0; //纯虚函数定义方法
纯虚函数&抽象基类:
纯虚函数是一种特殊的虚函数,在基类中声明为虚函数,但不提供实现部分,而要求各派生类提供该虚函数的不同版本实现
纯虚函数的意义:
相当于是描述类的一个抽象概念, 如会飞
并且要求在由此类派生时补全不同的定义
定义方法:
//没有定义, 只有声明, 并在最后加一个=0
virtual double getArea() =0;
注意:
-
纯虚函数的 =0 只能出现在类内部的虚函数声明语句处
-
纯虚函数可以定义函数体, 但是没有意义
因为纯虚函数必定会被子类的同名函数覆盖-
定义必须放在类的外部
-
给出定义后的纯虚函数可以被直接调用, 但是对应的类仍然是抽象基类, 不可实例化
-
抽象基类:
任何含有纯虚函数的类, 或是没有完全覆盖基类中抽象函数的类被称为抽象基类
而只含有纯虚函数的类又被称作接口类
注意:
-
抽象基类不可被实例化
同时也防止了抽象基类被实例化即不可直接使用抽象基类创建对象, 因为抽象基类中的纯虚函数只是一个抽象概念, 没有定义, 无法被实例化
-
只有在子类中将纯虚函数全部定义覆盖后才能定义对象
(可实例化) -
可以定义抽象基类的指针或引用
使用方法&特性和上头相同
纯虚函数是一种特殊的虚函数,在基类中声明为虚函数,但不提供实现部分,而要求各派生类提供该虚函数的不同版本实现
纯虚函数的意义:
相当于是描述类的一个抽象概念, 如会飞
并且要求在由此类派生时补全不同的定义
定义方法:
//没有定义, 只有声明, 并在最后加一个=0
virtual double getArea() =0;
注意:
-
纯虚函数的 =0 只能出现在类内部的虚函数声明语句处
-
纯虚函数可以定义函数体, 但是没有意义
因为纯虚函数必定会被子类的同名函数覆盖-
定义必须放在类的外部
-
给出定义后的纯虚函数可以被直接调用, 但是对应的类仍然是抽象基类, 不可实例化
-
抽象基类:
任何含有纯虚函数的类, 或是没有完全覆盖基类中抽象函数的类被称为抽象基类
而只含有纯虚函数的类又被称作接口类
注意:
-
抽象基类不可被实例化
同时也防止了抽象基类被实例化即不可直接使用抽象基类创建对象, 因为抽象基类中的纯虚函数只是一个抽象概念, 没有定义, 无法被实例化
-
只有在子类中将纯虚函数全部定义覆盖后才能定义对象
(可实例化) -
可以定义抽象基类的指针或引用
使用方法&特性和上头相同