1. 多态的概念
// 静态的多态:函数重载,看起来调用同一个函数有不同行为。静态:编译时实现的
int main()
{
int i = 1;
double d = 3.14;
cout << i;//cou.operator<<(int);
cout << d;//cou.operator<<(double);
//c++的流插入和流提取可以实现自动识别类型
return 0;
}
// 动态的多态:一个父类的对象的引用或指针去调用同一个函数,如果传递不同的对象,会调用不同的函数
// 动态:运行时实现的
// 本质:不同人去做同一件事情,结果不同
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
// 子类中满足三同(函数名,参数,返回值)虚函数,叫做重写(覆盖)
virtual void BuyTicket() { cout << "买票-半价" << endl; }
//二者不写virtual,不满足三同,就会构成隐藏
};
void Func(Person& p)
{
p.BuyTicket();//多态,传的是父类就会调用父类。传的是子类的就会调用子类
}
int main()
{
Person ps;
Student st;
Func(ps);
Func(st);
//买票 - 全价
//买票 - 半价
return 0;
}
多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。
2. 多态的定义及实现.
2.1多态的构成条件
多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如Student继承了Person。Person对象买票全价,Student对象买票半价。
那么在继承中要构成多态还有两个条件:
- 必须通过基类的指针或者引用调用虚函数(才可以接受子类和父类的对象)
- 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
//传对象有问题
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
// 构成多态,跟p的类型没有关系,传的那个类型的对象,调用的就是这个类型的虚函数--跟对象有关
// 不构成多态,调用就是p类型的虚函数--跟类型有关
//void Func(Person& p)//引用
void Func(Person p)//对象
{
p.BuyTicket();
}
int main()
{
Person ps;
Student st;
Func(ps);
Func(st);
//买票 - 全价
//买票 - 全价
//没有实现多态
return 0;
}
//传指针--没有问题
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
void Func(Person* p)//对象
{
p->BuyTicket();
}
int main()
{
Person ps;
Student st;
Func(&ps);
Func(&st);
//买票 - 全价
//买票 - 半价
return 0;
}
2.2虚函数
虚函数:即被virtual修饰的类的非成员函数称为虚函数,其他函数不能称为虚函数
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl;}
};
问题:静态成员是否可以加virtual?
不能,
普通函数是否可以是虚函数?
不能
只能是类的非静态函数才能是虚函数!
2.3虚函数的重写
虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数
2.3.1协变(基类与派生类虚函数返回值类型不同)
派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引
用,派生类虚函数返回派生类对象的指针或者引用时,称为协变
class A {};
class B : public A {};
class Person {
public:
virtual A* BuyTicket() { cout << "买票-全价" << endl; return nullptr; }
};
class Student : public Person {
public:
virtual B* BuyTicket() { cout << "买票-半价" << endl; return nullptr; }
};
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person ps;
Student st;
Func(ps);
Func(st);
return 0;
}
//重写要求返回值相同有一个例外:协变--要求返回值是父子关系的指针或者引用
class A {};
class B : public A {};
class Person {
public:
virtual A* BuyTicket() { cout << "买票-全价" << endl; return nullptr; }
};
class Student : public Person {
public:
virtual B* BuyTicket() { cout << "买票-半价" << endl; return nullptr; }
};
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person ps;
Student st;
Func(ps);
Func(st);
return 0;
}
2.3.2. 析构函数的重写(基类与派生类析构函数的名字不同)
如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor
//析构函数是虚函数,是否构成重写?构成
//析构函数名被特殊处理,处理成了destructor
class Person {
public:
virtual ~Person() { cout << "~Person()" << endl; }
};
class Student : public Person {
public:
virtual ~Student() { cout << "~Student()" << endl; }
};
int main()
{
//普通对象,无论是否是虚函数,是否完成重写,都正确调用了
//Person p;
//Student s;
// 输出:
//~Student()
//~Person()
//~Person()
//动态申请的对象,如果给了父类指针管理,那么需要析构函数是虚函数
Person* p1 = new Person;//new是由:operator new+构造函数 构成
Person* p2 = new Student;
//delete是由:析构函数+operator delete
// //p1->destructor()
delete p1;
delete p2;
//输出:
//~Person()
//~Student()
//~Person()
//延伸:这样也可以正确调用(不适用虚函数)
Person* p1 = new Person;
Student* p2 = new Student;
return 0;
}
//为什么析构函数全部不搞成虚函数?
//答:不是所有的情况下都需要,
// 动态申请的父子对象,如果给了父类指针管理,那么需要析构函数是虚函数,完成重写,构成多态,那么才能正确调用析构函数
// 其他场景,析构函数是不是虚函数,都可以正确调用析构函数
2.3.3父类写了virtual 子类没写
// 虚函数的重写允许:两个都是虚函数或者父类是虚函数,再满足三同,就构成重写。
// 其实这个是C++不是很规范的地方,当然我们建议两个都写上virtual
// 本质上,子类重写的虚函数,可以不加virtual是因为析构函数,
// 大佬设计初衷:父类析构函数加上virtual,那么就不存在不构成多态,没调用子类析构函数,内存泄漏场景
// 建议,我们自己写的时候,都加上virtual,肯定没毛病
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
virtual ~Person() { cout << "~Person()" << endl; }
};
class Student : public Person {
// 虽然子类没写virtual,但是他是先继承了父类的虚函数的属性,再完成重写。那么他也算是虚函数
void BuyTicket() { cout << "买票-半价" << endl; }
public:
~Student() { cout << "~Student()" << endl; }
};
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person* p1 = new Person;
Person* p2 = new Student;
delete p1;
delete p2;
Person ps;
Student st;
Func(ps);
Func(st);
return 0;
}
2.4 C++11 override 和 final
引入:
C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失,因此:C++11提供了override和final两个关键字,可以帮助用户检测是否重写
final
// 设计一个不能被继承的类?
// 将构造函数搞成私有(C++98)
// 为什么?
// 因为B去继承A是有要求的:B
//class A
//{
//private:
// A(int a = 0)
// :_a(a)
// {}
//protected:
// int _a;
//};
(间接限制)子类构成函数无法调用父类构造函数初始化成员,没办法实例化对象
//class B :public A
//{
//
//};
//
//int main()
//{
// B bb;
// //子类要初始化,要调用父类的构造函数,但是父类的构造函数是私有的,在子类是不可见的,即不能用的,语法上强制限制了你不能用
// //那么子类就没有办法构造了
//
// return 0;
//}
class A
{
private:
A(int a = 0)
:_a(a)
{}
public:
static A Creatobj(int a = 0)
//为什么是静态的?
//不是静态的要调用这个成员函数,成员函数必须要对象能调用,而你需要这个来获取对象
{
//new A;二者皆可
return A(a);
}
protected:
int _a;
};
int main()
{
//A aa;
//延伸问题:A也构造不了了
A aa = A::Creatobj(10);
return 0;
}
//C++11的方法 直接限制
class A final//将这个类设计成为最终类
{
protected:
int _a;
};
//直接限制
class B :public A
{
};
class C
{
public:
virtual void F() final//限制重写
{
cout << "C::F()" << endl;
}
};
class D :public C
{
public:
virtual void F()
{
cout << "D::F()" << endl;
}
};
int main()
{
return 0;
}
final:修饰虚函数,限制他不能被子类中的虚函数重写
override
//override : 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
class Car {
public:
virtual void Drive() {}// void Drive() {}报错
};
class Benz :public Car {
public:
virtual void Drive() override { cout << "Benz-Drive" << endl; }
};
int main()
{
return 0;
}
2.5 重载、覆盖(重写)、隐藏(重定义)的对比
3 抽象类
3.1概念:
在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承
//class Car
//{
//public:
// //纯虚函数一般声明,不实现--》实现没有价值
// //virtual void Drive() = 0;
// //也是可以实现的
// virtual void Drive() = 0
// {
// cout << "virtual void Drive() = 0" << endl;
// }
// //为什么实现之后没有价值?因为包含纯虚函数的类不能实例化出对象
// //实现了也没有人去调用。
//
// void f()
// {
// cout << "void f()" << endl;
// }
//};
//int main()
//{
// //Car c;//不能实例化出对象
// //Car* p;//可以
//
// Car* p = nullptr;
// p->Drive();//可以调到
// return 0;
//}
class Car
{
public:
virtual void Drive() = 0;
};
class Benz :public Car
{
public:
virtual void Drive()
{
cout << "Benz-drive" << endl;
}
};
int main()
{
Car* p = new Benz;
p->Drive();//可以调到
return 0;
}
//包含纯虚函数的类是 抽象类?
//什么样的类需要将其设计成抽象类(包含纯虚函数)
//什么是抽象?在现实世界中没有对应的实物
// 一个类型如果一般在世界中没有具体的对应实物就定义成抽象类比较好
// 例如:图书馆系统,学校里的每个角色,人可以定义成抽象类
// 学校没有“人”这个角色,学校里面的应该是 学生,老师,工作人员,
// 每个角色都有自己的职能,人是可以抽象的
// 设计类是为了区分这些角色,
// 抽象类干了什么?
// 强制子类去完成重写
// override只是在与发生检查是否完成重写。
3.2 接口继承和实现继承
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数
4. 多态的原理
4.1虚函数表
虚函数表指针:(简称虚表指针)本质上是一个函数指针数组
这个虚表是用来干什么的?
答:多态
多态的原理:
积累的指针/引用,指向谁?就去谁的虚表中找到对应位置的虚函数进行调用
延伸问题:为什么对象不行?对象也可以父类和子类切片呀?
对象切片和引用切片的区别是:
对象不能确定自己指向的是父类的虚函数还是子类的虚函数
同类型的指针,他的虚表指向是否是一样的?
一样
普通函数和虚函数存储的位置是否相同?
他们是一样的,都在公共的代码段。只是虚函数要把地址存一份到虚表中,方便实现多态
延伸总结:
多态调用,在编译时,不能确定调用的是那个函数
运行时,去p指向对象的需表中找到虚函数的地址
延伸问题:子类的虚函数是私有为什么也能实现多态?
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
protected:
int _p = 1;
};
class Student : public Person {
private://子类的虚函数是私有为什么也能实现多态?
virtual void BuyTicket() { cout << "买票-半价" << endl; }
protected:
int _s = 2;
};
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person p1;
Func(p1);
Student S1;
Func(S1);
return 0;
}
//因为从语法上来说,重写是一种接口继承,重写了你的实现
// 在语法的角度上检查不出来
// 编译器在检查时,父类对象去调用p.BuyTicket();
// 我只是在虚表里面去找,只要能找到就能够使用
// 所以私有的限制不起作用
另外一层理解:
C++的访问限定符,是否是绝对的安全?私有的一定是否一定是调不到的
虚函数放到虚表中,我们可以从虚表中去找
我们只要有函数地址就能够去调用
补充:强制去调用虚表中的成员
确认虚表在哪?
int main()
{
int* p = (int*)malloc(4);
printf("堆:%p\n", p);
int a = 0;
printf("栈:%p\n", &a);
static int b = 0;
printf("数据段:%p\n", &b);//静态区(静态区大归类上面属于栈数据段)
const char* str = "aaaaaaa";
printf("常量区:%p\n", str);
printf("代码段:%p\n", &Base::Func1);//函数地址
Base bs;
printf("虚函数表:%p\n", *((int*)&bs));
//虚函数表,需要看地址,地址在三十二位下头4个字节,64位下头八个字节
//如何取一个对象的头四个字节或者八个字节?
return 0;
}
总结:
- 派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,虚表指针也就是存在部分的另一部分是自己的成员。
- 基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
- 另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函数,所以不会放进虚表。
- 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。
- 总结一下派生类的虚表生成:a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
- 这里还有一个童鞋们很容易混淆的问题:虚函数存在哪的?虚表存在哪的? 答:虚函数存在虚表,虚表存在对象中。注意上面的回答的错的。但是很多童鞋都是这样深以为然的。注意虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。那么虚表存在哪的呢?实际我们去验证一下会发现vs下是存在代码段的
5. 单继承和多继承关系中的虚函数表
5.1引入多继承虚表,并从内存中查看虚表
fun3 和 fun4看不见
有两种方式可以找到:
1.打印虚表
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;
};
typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{
cout << " 虚表地址>" << vTable << endl;
for (int i = 0; vTable[i] != nullptr; ++i)
{
printf("第%d个虚函数地址 :0X%x,->", i, vTable[i]);
VFPTR f = vTable[i];
f();
}
cout << endl;
}
int main()
{
Derive d;
return 0;
}
多继承时,子类重写了Base1和Base2虚函数func1,但是虚表中重写的func1的地址却不一样
但是没关系,他们最终调用还是同一个函数
为什么要设置这几个跳跃,才能到最终的函数?
主要是为了修正ecx,即修饰传给this指针的值
问题:虚表的指针是在什么时候初始化的?
虚表的指针是在构造函数初始化列表阶段进行初始化的
5.2单继承打印虚表
//打印虚表
//虚函数表(虚表):
//本质从类型角度:函数指针 数组
class Base {
public:
virtual void func1() { cout << "Base::func1" << endl; }
virtual void func2() { cout << "Base::func2" << endl; }
private:
int a;
};
class Derive :public Base {
public:
virtual void func1() { cout << "Derive::func1" << endl; }
virtual void func3() { cout << "Derive::func3" << endl; }
void func4() { cout << "Derive::func4" << endl; }
private:
int b;
};
//VE_PTR:类型定义
typedef void(*VF_PTR)();
//void PrintfVFTable(VF_PTR table[])
void PrintVFTable(VF_PTR* table)
{
for (int i = 0; table[i] != nullptr; ++i)
{
printf("vft[%d]:%p\n", i, table[i]);
VF_PTR f = table[i];//方法二:查看我们是否打印正确与否
f();
}
}
int main()
{
Base b;
//PrintVFTable((VF_PTR*)(*(int*)&b));32位下用这个
//PrintVFTable((VF_PTR*)(*(long long*)&b));64位下用这个
PrintVFTable((VF_PTR*)(*(void**)&b));//这个是最标准的32位下和64位下均可使用
PrintVFTable((VF_PTR*)(*(char**)&b));//这个是最标准的32位下和64位下均可使用
//
//vft[0]:0014161
//Dvft[1]:00141613
Derive d;
return 0;
}
//会出现的问题:vs下默认以nullptr为结尾,但是中间一旦发生什么改变就会输出错误
//所以需要清理
//在生成中选择“清理解决方案”
//如何知道只有两个函数,即:我们是否打印的正确与否?
//方法一:监视窗口
//方法二:见代码
//延伸:函数指针
//typedef void(*)() VF_PTR;
//这是我们正常理解的typedef写法,但是这样是错误的
//正确写法
//typedef void(*VF_PTR)();
//PrintVFTable((VF_PTR*)(*(long long*)&b));
//问题:这个在32位下也是可以运行的?
// 在32位下long long 将其进行强转成八个字节,VF_PTR又将其截断四个字节
//这个属于巧合
//改进int main()
//int main()
//{
// Base b;
//#ifdef _WIN64
// PrintVFTable((VF_PTR*)(*(long long*)&b));//32位下用这个
//#else
// PrintVFTable((VF_PTR*)(*(int*)&b)); //64位下用这个
//#endif
// return 0;
//}
5.3多继承打印虚表
Drive的对象模型:
代码:
//多继承打印虚表
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;
};
typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{
cout << " 虚表地址>" << vTable << endl;
for (int i = 0; vTable[i] != nullptr; ++i)
{
printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);
VFPTR f = vTable[i];
f();
}
cout << endl;
}
int main()
{
Derive d;
VFPTR* vTableb1 = (VFPTR*)(*(void**)&d);
PrintVTable(vTableb1);
VFPTR* vTableb2 = (VFPTR*)(*(void**)((char*)&d + sizeof(Base1)));
PrintVTable(vTableb2);
return 0;
}
6. 继承和多态常见的面试问题
#include<iostream>
using namespace std;
class A {
public:
A(char* s) { cout << s << endl; }
~A() {}
};
class B :virtual public A//当BC的虚继承去掉后,会直接报错
{
public:
B(char* s1, char* s2) :A(s1) { cout << s2 << endl; }
};
class C :virtual public A
{
public:
C(char* s1, char* s2) :A(s1) { cout << s2 << endl; }
};
class D :public B, public C//这里先继承的会先在前面
{
public:
D(char* s1, char* s2, char* s3, char* s4) :B(s1, s2), C(s1, s3), A(s1)
//因为BC中的A是虚继承,所以无论这里的A顺序如何,都是A先初始化
//然后根据声明的BC先后顺序来确定BC的顺序
{
cout << s4 << endl;
}
};
int main() {
D* p = new D("class A", "class B", "class C", "class D");
delete p;
return 0;
}
//输出:ABCD
多继承中指针偏移问题?下面说法正确的是(C )
class Base1 { public: int _b1; };
class Base2 { public: int _b2; };
class Derive : public Base1, public Base2 { public: int _d; };
int main() {
Derive d;
Base1* p1 = &d;
Base2* p2 = &d;
Derive* p3 = &d;
return 0;
}
A:p1 == p2 == p3 B:p1 < p2 < p3 C:p1 == p3 != p2 D:p1 != p2 != p3
以下程序输出结果是什么()
class A
{
public:
virtual void func(int val = 1) { std::cout << "A->" << val << std::endl; }
virtual void test() { func(); }
};
class B : public A
{
public:
void func(int val = 0) { std::cout << "B->" << val << std::endl; }
};
int main(int argc, char* argv[])
{
B* p = new B;
p->test();
return 0;
}
A: A->0 B: B->1 C: A->1 D: B->0 E: 编译出错 F: 以上都不正确
inline函数可以是虚函数吗?
答:可以,不过编译器就忽略inline属性,这个函数就不再是inline,因为虚函数要放到虚表中去。
内联函数是没有地址的,只要符合定义的内联函数调用的地方会被替换掉,不需要地址的
内联是一个建议,当编译器不采用的时候,他也不是真的内联函数,他也不起作用
所以他就不可能同时是虚函数和内联函数
调用时,如果不构成多态,这个函数保持inline属性
如果构成多态,这个函数就没有inline属性了,因为调用是到对象的虚函数表中找到虚函数地址,实现调用,无法使用内敛属性
静态成员可以是虚函数吗?
答:不能,因为静态成员函数没有this指针,
使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。
static成员函数定义成虚函数没有价值
构造函数可以是虚函数吗?
答:不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。
构造函数称为虚函数没有价值
虚函数的意义是构成多态调用
多态调用要去虚函数表中查找虚函数
对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的
析构函数可以是虚函数吗?什么场景下析构函数是虚函数?
答:可以,并且最好把基类的析构函数定义成虚函数。
对象访问普通函数快还是虚函数更快?
答:首先如果是普通对象,是一样快的。如果是指针对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找。
虚函数的调用是否一定是在虚函数表中查找的吗?
不是,
如果不构成多态,都是编译器确定调用函数的地址
那么他们一样快
如果构成多态,那么是虚函数时运行时,去虚函数表中确定函数地址
普通函数编译时直接确定地址,那么普通函数快
虚函数表是在什么阶段生成的,存在哪的?
答:虚函数表是在编译阶段就生成的,一般情况下存在代码段(常量区)的。