文章目录x86中涉及的指针是4bytes,有些平台下比如x64就要考虑指针是8bytes的问题
多态
1.多态的概念
1.1概念 2.多态的定义及实现
2.1多态的构成条件2.2虚函数2.3虚函数的重写2.4虚函数重写的两个例外
2.4.1协变(基类与派生类虚函数返回值类型不同)2.4.2析构函数的重写(基类和派生类的名字不同) 2.5c++11的override和final2.6重载、覆盖(重写)、隐藏(重定义)的对比 3.抽象类
3.1概念3.2接口继承和实现继承 4.多态的原理
4.1原理的解释4.2虚表的深入4.3动态绑定和静态绑定(动态多态和静态多态) 5.单继承和多继承关系的虚函数表
5.1单继承+打印虚表内容5.2多继承5.菱形继承与菱形虚拟继承 面试题
1.多态的概念 1.1概念多态就是多种形态.具体就是说,完成某个行为,当不同的对象去完成时会产生出不同的状态
比如说买票,普通人全票,学生半票,军人优先买票.
比如支付宝新用户和老用户的红包都是不一样的.
2.多态的定义及实现 2.1多态的构成条件多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为.比如Student继承了Person.而Person对象买票全家,Student对象买票半价.
那么在继承中要构成多态还有两个条件(先记住)
- 必须通过基类的指针或者引用来调用虚函数被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写.
满足多态的条件:跟对象有关,指向哪个对象就调用它的虚函数
不满足多态的条件:跟类型有关,调用的类型是谁,调用就是谁的.
另外:派生类的虚函数可以不写virtual,也能构成多态,但是不规范。
class Person{
public:
virtual void BuyTicket()
{
cout<<"买票全价"<
注意:virtual在虚继承和虚函数没有任何关系,只是用了同一个关键字
可以修饰成员函数,为了完成虚函数的重写,满足多态的条件之一可以在菱形继承中,完成虚继承,解决数据冗余和二义性
重写(覆盖)的要求
函数名相同,参数相同,返回值相同,并且都是虚函数
撤掉虚函数重写的条件
class Person{
public:
void BuyTicket()
{
cout<<"买票全价"<
撤掉基类的引用或指针
class Person{
public:
virtual void BuyTicket()
{
cout<<"买票全价"<
2.2虚函数
定义:非静态成员函数前加virtual关键字的函数
虚函数的内存
会多一个虚函数表,一个指针.
class Person{
public:
virtual void BuyTicket(){
cout<<"半价"<
2.3虚函数的重写
虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即三同),称子类的虚函数重写了基类的虚函数
另外:派生类的虚函数可以不写virtual,也能构成多态,但是不规范。(因为继承后基类的虚函数被继承下来在派生类依然保持虚函数属性)
class Person{
public:
virtual void BuyTicket()
{
cout<<"买票全价"<
class Person{
public:
virtual void BuyTicket()
{
cout<<"买票全价"<BuyTicket();//都是全价
}
int main(){
Person p;
Student s;
Func(&p);
Func(&s);
}
不构成多态的条件:
class Person{
public:
virtual void BuyTicket()
{
cout<<"买票全价"<
2.4虚函数重写的两个例外
2.4.1协变(基类与派生类虚函数返回值类型不同)
派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或引用,派生类虚函数返回派生类对象的指针或引用,称为协变。
其结果仍然满足多态。
class Person{
public:
virtual Person* BuyTicket()
{
cout<<"买票全价"<
只要两者的返回值的指针或者引用构成继承关系
class A{}
class B:public A{};
class Person{
public:
virtual A* f(){return new A;}
};
class Student :public Person{
public:
virtual B* f() {return new B;}
};
2.4.2析构函数的重写(基类和派生类的名字不同)
如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写。虽然此时基类与派生类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称进行了特殊处理,编译后析构函数的名称统一处理成destructor。
class Person{
public:
virtual ~Person(){cout<<"~Person()"<
要记住的特殊场景:
不构成多态,调用的类型是谁的,就调用谁的。
class Person{
public:
~Person(){cout<<"~Person()"<
这里和前面继承部分派生类对象默认调用的时候会调用基类析构函数不矛盾。自动调用需要定义的是派生类对象,而这里使用的是基类指针来指向派生类对象,这个角度上来说,delete去调用类的析构函数只能是基类的。
只有派生类Student的析构函数重写了Person的析构函数,下面的delete对象调用析构函数,才能构成多态,才能保证p1和p2指向的而对象正确地调用析构函数
构成多态,调用的指针指向谁就调用谁的析构函数
class Person{
public:
virtual ~Person(){cout<<"~Person()"<
2.5c++11的override和final
c++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重载,而这种错误在编译期间是不会爆出的。只有在程序运行时没有得到预期结果才debug。
因此c++11提供了两个关键字帮助用户检测是否重写。
final:修饰虚函数,表示该虚函数不能被继承
也可以放类使其类不能被继承。(c++11玩法)
把基类的构造函数放为private(c++98玩法)
class A{
public:
virtual void func(int val=1) final {
std::cout<<"A->"<"<test();
return 0;
}
override:检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
class Car{
public:
virtual void Drive(){}
};
class Benz:publc Car{
public:
//注意函数名不同,不会报错
virtual void Dirve() {cout<<"Benz"<
2.6重载、覆盖(重写)、隐藏(重定义)的对比
重载
两个函数在一个作用域函数名相同、参数类型不同 重写(覆盖)
重写是为了重写函数的实现
两个函数分别在基类和派生类的作用域函数名/参数/返回值都必须相同(协变除外)两个函数必须是虚函数(子类不写virtual也是虚函数) 重定义(隐藏)
两个函数分别在基类和派生类的作用域函数名相同两个基类和派生类的同名函数不构成重写就是重定义
3.抽象类
3.1概念
在虚函数的后面写上=0,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写。另外纯虚函数更体现出了接口继承。
纯虚函数的作用:
强制派生类完成重写表示抽象的类型
抽象就是在现实中没有对应的实体的。比如车,植物。车都能开,定义成纯虚函数,其他类都去继承他。
///抽象类
class Car{
public:
virtual void Drive() = 0;//不需要实现,纯虚函数
};
int main(){
Car car;//error:抽象类不能实例化出对象
}
这里的理解是这样的。继承的虽然是父类该函数的使用权,但是当派生类调用到该函数时,需要去基类中找,而基类的是纯虚函数尚未实现。因此派生类若不实现纯虚函数仍旧不可以实例化。
///抽象类
class Car{
public:
virtual void Drive() = 0;//不需要实现,纯虚函数
};
class Benz:public Car{
public:
};
int main(){
Benz bb;//error:不能实例化出对象
}
///抽象类
class Car{
public:
virtual void Drive() =0;//不需要实现,纯虚函数
};
class Benz:public Car{
public:
virtual void Drive(){
}
};
int main(){
Benz bb;//重写后可以
}
3.2接口继承和实现继承
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的重写,目的是为了重写(也就是说实际上是不想要原来函数的实现的),达成多态。继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。
4.多态的原理
4.1原理的解释
class base{
public:
virtual void Func1(){
cout<<"Func1()"<
Student类继承了Person类的虚函数指针。
如果是父类,就调用父类的虚表指针去找函数。
如果是子类,就把子类切片,然后调用子类的虚表指针的函数。切片就是父类的大小。
也就是说指向谁就到谁的虚表里去找对应的虚函数,而虚函数是提前已经写好的。重写就是虚表里面会变成子类的。所以覆盖就是把父类的虚表里的函数copy下来然后子类重新写里面的实现。
若没有完成重写。子类和父类的虚表里的函数是同一个地址(子类直接copy下来的)。而重写了子类就会把自身虚表里面的这个函数给覆盖了。
多态是在运行时到指向的对象的虚表中查找要调用的虚函数的地址来进行调用。
4.2虚表的深入
对于虚函数特意建虚表放进去,普通函数就不必要。派生类对于重写了的虚函数进行了覆盖。覆盖是原理上的叫法,重写是语法上的说法。
虚表本质上是函数指针数组。一般规定以0(nullptr)结束
总结
派生类对象中也有一个虚表指针,该对象由两部分组成,一部分是父类继承下来的成员,虚表指针也就是存在部分的另一部分是自己的成员。
基类对象b和派生类对象d虚表是不一样的。Fun1完成了重写,所以d的虚表中存的是重写的Derive::Func1*,所以虚函数的重写也叫做覆盖。覆盖是原理上的叫法,重写是语法上的说法。
另外Func2继承下来后是虚函数,所以放进了虚表。但Func3不是虚函数,所以不放进虚表
虚表本质是一个存虚函数指针的指针数组,这个数组的最后面放了一个nullptr
总结一下派生类的虚表生成:
先将基类中的虚表内容拷贝一份放到派生类的虚表中如果派生类重写了基类的某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数派生类自己增加的虚函数按其在派生给中的声明次序增加到派生类虚表的最后
还有一个易混淆的问题:虚函数存在哪里?虚表存在哪里?
虚表中存的是虚函数指针,不是虚函数。虚函数和普通函数一样存在代码段,只是它的指针又存到了虚表中。
对象中存的是虚表指针,不是虚表。
那么虚表存在哪里?
vs下存在代码段
说明多个对象指的是一个公共区域,而公共区域有堆,数据段,代码段。堆是动态开辟,数据段主要存全局数据和静态数据。只有代码段相对合适一些。(代码段的只读相对用户,编译的时候写好,进程加载进来运行的时候不能修改,覆盖只是一个形象的概念,其实编译器都算好了)
4.3动态绑定和静态绑定(动态多态和静态多态)
#include
using namespace std;
class base {
public:
virtual void Func1() {
cout << "base:Func1()" << endl;
}
virtual void Func2() {
cout << "base:Func2()" << endl;
}
void Func3() {
cout << "base:Func3()" << endl;
}
private:
int _b = 1;
};
class Derive :public base {
virtual void Func1() {
cout << "Derive::Func1()" << endl;
}
private:
int _d = 2;
};
void f1(int i)
{ }
void f1(double d)
{ }
int main() {
int i = 0;
double d = 1.1;
//静态绑定 静态的多态 (静态:编译时确定函数地址,生成是一个.exe)
f1(i);
f1(d);
//动态绑定 动态的多态 (动态:运行时到虚表中找虚函数地址 父进程创建一个子进程 用exec()换掉)
base* p = new base;
p->Func1();
p = new Derive();
p->Func1();
};
动态绑定
5.单继承和多继承关系的虚函数表
监视窗口中看不到fun3()和fun4(),是因为监视窗口隐藏了(它觉得你不需要)
5.1单继承+打印虚表内容
#include
using namespace std;
class base {
public:
virtual void func1() { cout << "base:func1" << endl; }
virtual void func2() { cout << "base:func2" << endl; }
private:
int a;
};
class Derive :public base {
virtual void func1() { cout << "Derive::func1" << endl; }
virtual void func3() { cout << "Derive::func3" << endl; }
virtual void func4() { cout << "Derive::func4" << endl; }
private:
int b;
};
//打印虚表
///void (*p) () //定义一个函数指针变量
typedef void (*VF_PTR)();//函数指针类型typedef
void PrintVFTable(VF_PTR* ptable) {
for (size_t i = 0;ptable[i] != 0; i++) {
printf("vfTable[%d]:%p->", i, ptable[i]);
VF_PTR tmp=ptable[i];
tmp();
}printf("n");
}
int main() {
base b;
PrintVFTable( (VF_PTR*) (*(int*)(&b)) );
Derive d;
PrintVFTable( (VF_PTR*) (*(int*)(&d)));
};
5.2多继承
class base1 {
public:
virtual void func1() { cout << "base1:func1" << endl; }
virtual void func2() { cout << "base1:func2" << endl; }
private:
int b1;
};
class base2 {
public:
virtual void func1() { cout << "base2:func1" << endl; }
virtual void func2() { cout << "base2:func2" << endl; }
private:
int b2;
};
class Derive :public base1, public base2 {
public:
virtual void func1() { cout << "Derive:func1" << endl; }
virtual void func3() { cout << "Derive:func3" << endl; }
private:
int d1;
};
int main() {
Derive d;
//函数指针数组首元素地址此时以"int"形式表示,需要再强转
PrintVFTable( (VF_PTR*)(*(int*)&d) );
PrintVFTable( (VF_PTR*)(*(int*) ( (char*)&d+sizeof(base1) ) ) );
cout<
子类继承父类的虚表并覆盖,所以不用自己额外创表,而且这里要两个虚表指针,一个是base1的,一个是base2的。假如两个虚表合并了,派生类的func1就不知道是重写哪个了。这里派生类的重写func1重写了两个基类的func1。
但是现在看不到派生类的func3放在哪个虚表。—打印虚表
Derive::func3放在第一个虚表。
5.菱形继承与菱形虚拟继承
https://coolshell.cn/articles/12176.html
https://coolshell.cn/articles/12165.html#%E5%A4%9A%E9%87%8D%E7%BB%A7%E6%89%BF%EF%BC%88%E6%9C%89%E8%99%9A%E5%87%BD%E6%95%B0%E8%A6%86%E7%9B%96%EF%BC%89
面试题
析构函数要不要定义成虚函数呢?
答案是一定要,如果student析构函数中有资源释放而这里没有调用到,就会发生内存泄漏。
以下程序输出的结果是什么?
class A{
public:
virtual void func(int val=1){
std::cout<<"A->"<"<test();
return 0;
}
A.A->0
B.B->1
C.A->1
D.B->0
E.编译出错
F.以上都不对
答案是B。
p->test();///p->test(p); virtual void test()//A* this.相当于这个this指向了p,也就是父类指针指向了子类对象
//此时父类指针调用func虚函数,而这个虚函数完成了重写。满足多态条件。
//A* this->func(); ---父类的指针或者引用去调用
//重写的过程是先继承下来,继承下来继承的而是接口(函数名,参数,返回值),唯独不继承实现{},这部分是重写。因此此时用的缺省参数是val=1.
6.1概念查考
1.下面哪种面向对象的方法可以让你变得复用()
A.继承B:封装C:多态D:抽象
2.()是面向对象程序设计语言中的一种机制。这种机制实现了方法的定义与具体的对象无关,而对方法的调用则可以关联于具体的对象。
A:继承 B:模板 C:对象的自身引用 D:动态绑定
3.面向对象设计中的继承和组合,下面说法错误的是?()
A:继承允许我们覆盖里写父类的实现细节,父类的实现对于子类是可见的,是一种静态复用,也称为白盒复用
B:组合的对象不需要关心各自的实现细节,之间的关系是在运行时候才确定的,是一种动态复用,也称为黑盒复用
C:优先使用继承,而不是组合,是面向对象设计的第二原则
D:继承可以使子类能自动继承父类的接口,但在设计模式中认为这是一种破坏了父类的封装性的表现
4.以下关于纯虚函数的说法,正确的是()
A:声明纯虚函数的类不能实例化
B:声明纯虚函数的类成虚基类
C:子类必须实现基类的纯虚函数
D:纯虚函数必须是空函数
5.关于虚函数的描述正确的是()
A:派生类的虚函数与基类的虚函数具有不同的参数个数和类型
B:内联函数不能是虚函数
C:派生类必须重新定义基类的虚函数
D:虚函数可以是一个static型的函数
A D C A B
//2.虚函数定义在类里面,和具体对象无关。
//4.子类可以不实现,大不了不实例化
//5.内联函数是展开的,没有地址,没法放虚表。不能是static,和虚表有关系,static没有this指针,没法放虚表里面去。虚表必须放对象里面。
6.关于虚表说法正确的是()
A:一个类只能有一张虚表
B:基类中有虚函数,如果子类中没有重写基类的虚函数,此时子类
与基类共用同一张虚表
C:虚表是在运行期间动态生成的
D:一个类的不同对象共享该类的虚表
7.假设A类中有虚函数,B继承自A,B重写A中的虚函数,也没有定义任何虚函数,则()
A:A类对象的前4个字节存储虚表地址,B类对象前4个字节不是虚表地址
B:A类对象和B类对象前4个字节存储的都是虚基表的地址
C:A类对象和B类对象前4个字节存储的虚表地址相同
D:A类和B类中的内容完全一样,但是A类和B类使用的不是同一张虚表
6. D (A.多重继承。B.重写了也是共用一张虚表
7. D (B.是虚基表,存的是偏移量,解决菱形继承的数据冗余和二义性4)
sizeof(base)是多少
class base{
public:
virtual void Func1(){
cout<<"Func1()"<
虚函数存在哪里?
代码段
虚表(虚函数表)存在哪里?
代码段(常量区)



