为什么多态要求是基类的指针或者引用
多态情况 根据不同对象执行相应代码 普通情况 根据类型执行相应代码
1.多态回顾
首先派生类继承基类的成员(拷贝过来) 虚表也跟着拷贝过来了
然后虚函数重写 把派生类基类部分中 的的虚表(函数体地址)重写/覆盖
2.为什么不能是派生类的指针
如果是派生类指针
派生类指针只能指向派生类 无法指向基类 就不存在多态
3.为什么不能是基类对象
如果是对象 那就涉及切片和拷贝 切片的时候 派生类已经继承了基类 虚表中的地址也是重写/覆盖 后的地址 这个时候如果切片 那就是把派生类中父类的部分切出来 再拷贝过去
这个时候如果你拷贝过去 的是派生类的虚表 拷贝过去 就会导致基类原本的虚表变成派生类的虚表了
这个时候如果你基类对象去访问基类的虚函数 这个时候调用的就是派生类的虚函数
与原本期望的基类的虚函数不符!
所以对象切片的时候就不会把虚表拷贝过去! 但是拷贝不过去 我怎么是实现多态
Person是基类 Student是派生类
假设这地方对象可以实现多态 且实现了虚函数重写和三同
那么 ps调用虚函数应该去调用派生类的函数
但是这个地方我切片的时候没有把派生类的虚表拷贝过去 所以我去调用函数调用的还是基类的
就没有实现多态的效果
1.虚表所在位置
多态里面派生类里面存的是虚表的指针!
确定虚表所在位置 最简单的方式就是通过实验去验证
int main()
{
Person ps;
Student st;
int a = 0;
printf("栈:%p\n", &a);
static int b = 0;
printf("静态区:%p\n", &b);
int* p = new int;
printf("堆:%p\n", p);
const char* str = "hello world";
printf("常量区:%p\n", str);
printf("虚表1:%p\n", *((int*)&ps));
printf("虚表2:%p\n", *((int*)&st));
return 0;
}
最后我们发现打印出来的虚表离 常量区最近 离其他位置都很远 所以虚表存放在常量区
2.单继承多继承中的多态
(1)单继承
首先我们要明白一件事 就是虚函数不管有没有被重写 都是被放在虚表里面的
多态不过是对派生类中 基类部分的原本虚表 进行覆盖
派生类中 没有实现多态的虚函数的地址 也被放在派生类中 基类部分的虚表里面(区别是没有被覆盖)
派生类里面基类的部分存放的是指向虚表的指针 虚表本质上是一个函数指针数组
我们可以通过代码来验证
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
virtual void Func1()
{
cout << "Person::Func1()" << endl;
}
virtual void Func2()
{
cout << "Person::Func2()" << endl;
}
//protected:
int _a = 0;
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
private:
virtual void Func3()
{
cout << "Student::Func3()" << endl;
}
protected:
int _b = 1;
};
typedef void(*FUNC_PTR) ();
void PrintVFT(FUNC_PTR* table)
{
for (size_t i = 0; table[i] != nullptr; i++)
{
printf("[%d]:%p->", i, table[i]);
FUNC_PTR f = table[i];
f();
}
printf("\n");
}
int main()
{
Person ps;
Student st;
int vft1 = *((int*)&ps);
PrintVFT((FUNC_PTR*)vft1);
int vft2 = *((int*)&st);
PrintVFT((FUNC_PTR*)vft2);
return 0;
}
注:由于vs上虚表的末尾都是00才可以这样写 gcc就不行 这个地方是以x86机器为例
运行前 记得重新生成解决方案 不然可能会出错
这个func3是虚函数 但是没有实现多态
且func3是派生类特有的 所以只有在派生类的基类部分的虚表里面才有
单基类的虚表里面是没有的!
那如果func3是在基类里面的 派生类没有呢?
答案就是 无论是单基类的虚表里面 还是派生类基类部分的虚表里面都有func3的地址
因为多态本质上 是把基类的虚表拷贝过来 再对实现多态的虚函数 进行重写/覆盖
派生类自己独有的虚函数会被添加到派生类虚表的末尾
(2)多继承
多继承
我们看这串代码 首先 这个地方虚表有几份?
答案是两份 Base1继承过来一份
Base2继承过来一份
由前面学过的先继承在上面可以画出图来
由Base1继承来的虚表有func1()和func2()
由Base2继承来的虚表有func1()和func2()
那么派生类自己的虚函数放到哪个虚表里面的?
答案是Base1 这是编译器的一个机制 存在先继承的那个虚表里面
而且我们同时会发现 这个地方的Base1继承过来的虚表的funcfunc2 和Base2继承过来的虚表func1 func2地址不同
但是调用的是同一个函数?
首先我们要明确一个点就是 派生类调用 实现多态的虚函数 的时候 传的是派生类的this指针
因为函数内可能会使用到 派生类特有的成员变量 所以我们要传的this指针是派生类的地址
派生类的指针指向的是派生类的最前面 和数组指针指向数组的第一个元素同理
由上面图可知 ptr1的地址是和ptr3的地址的值是相同的(ptr1和Derive的指针相同,ptr3就是Derive的指针) 只是类型不同 (汇编的时候不看类型) 所以编译器会把ptr1和ptr3都看成是Derive的指针
但是ptr2的地址却不同 所以Derive中Base2部分中虚表里面的func1和func2的地址 和Derive中Base1部分中虚表里面的不同
原因是 Derive中Base2部分中虚表里面的func1和func2的地址 是要经历减8(为什么要减8 减8 之后的地址和 ptr1 ptr3一样都是指向Derive的头了 这样编译器才会认为其是Derive指针!) 后再调用
编译器把减8和调用封装在一块了 所以 地址不同
但是本质上ptr 1 ptr2 ptr3调用的是同一个func1函数 只不过步骤不一样
多态派生类重写的虚函数调用的时候 用的是基类函数的函数头 重写的是派生类的虚函数的实现
但是其this指针用的还是派生类的
(3)菱形继承
菱形继承和多继承一样
class A
{
public:
virtual void func1()
{
cout << "A::func1" << endl;
}
public:
int _a;
};
class B : public A
{
public:
virtual void func1()
{
cout << "B::func1" << endl;
}
virtual void func2()
{
cout << "B::func2" << endl;
}
public:
int _b;
};
class C : public A
{
public:
virtual void func1()
{
cout << "C::func1" << endl;
}
virtual void func2()
{
cout << "C::func2" << endl;
}
public:
int _c;
};
class D : public B, public C
{
public:
virtual void func1()
{
cout << "D::func1" << endl;
}
virtual void func3()
{
cout << "D::func3" << endl;
}
public:
int _d;
};
64 9b ab 00 和 10 9b ab 00都是虚表指针!
本质上就是一个多继承 D类继承过来的 B类和C类 B类的A类部分 存着虚表指针 这个指针指向虚表 虚表里面存着派生类(包含基类部分)的所有虚函数的地址!
C对象和B对象同理
如果你问D对象自己特有的虚函数地址存在哪个虚表里面 请看上面多继承的Base3
(4)菱形虚拟继承
正常情况下也没什么问题 和多继承一样
只不过A类会被单独存放
但是如果B类和C类函数重写A类的同一个函数就完蛋了!
为什么? 因为B类虚表和C类虚表都是通过其A类虚表指针去访问的
如果我要重写 原本它们两个各有一个A的部分 重写自己的A的部分没问题
但是现在它们两个公用一个A 重写本质是覆盖 我如果用B类的虚函数覆盖了
那C类的虚函数就不能实现重写了啊! 我就实现不了多态的问题啊! 到底是B重写还是C重写呢?
所以编译器会报错 报错就是重写不明确 到底是重写B还是C编译器也不知道
怎么解决这个问题呢?
我们把D重写一下就行了!
这个时候我们由于菱形虚拟继承拿出来单独放的A 的指针指向的虚表存的就是D的虚函数(重写)地址了
那这个时候B和C重写的是不是就没有意义了?
当然不是 我们也有可能会单独创建B对象和C对象
如果这个时候 B C不仅同时重写A的虚函数 B C 还有单独的虚函数呢? 那会被放到哪?
会不会放到公共的A的虚表 不能 重写都不能
B C 会各自再建立一张虚表
D有一个单独的虚函数 会不会自己建立一个虚表 不会 会直接放到B指针指向的虚表的里面
3.抽象类
(1)抽象类概念
什么是抽象类呢? 抽象的反义词是具体 像我们创建对象的时候都是具体的对象 比如说int a
抽象类本质上是一个接口 这个接口可以被派生类实现多态 抽象类的实现需要纯虚函数
// 定义抽象类 Shape
class Shape {
public:
// 纯虚函数,计算面积
virtual double area() const = 0;
// 虚函数,打印信息
virtual void printInfo() const {
std::cout << "This is a shape." << std::endl;
}
};
// 定义 Circle 类,继承自 Shape
class Circle : public Shape {
private:
double radius;
public:
Circle(double r) : radius(r) {}
// 实现纯虚函数 area()
virtual double area() const override {
return 3.14 * radius * radius;
}
// 重写虚函数 printInfo()
virtual void printInfo() const override {
std::cout << "This is a circle." << std::endl;
}
};
int main() {
Circle circle(5.0);
// 使用基类指针指向派生类对象
Shape* shapePtr = &circle;
// 调用派生类实现的 area() 方法
std::cout << "Area: " << shapePtr->area() << std::endl;
// 调用派生类重写的 printInfo() 方法
shapePtr->printInfo();
return 0;
}
纯虚函数等于0就是虚函数头
virtual double area() const = 0;//纯虚函数
(2)抽象类和纯虚函数的关系
有纯虚函数的类就是抽象类(派生类继承过来的也算) 抽象类是无法实例化的! 除非你重写才能用
比如说上面我定义了Shape类 这是一个抽象类 写了两个虚函数 一个计算面积 一个打印信息
但是这个类是不能使用对象的 因为它是一个抽象的类 表示形状 表示啥形状?
是一个抽象的 要具体 怎么具体 通过派生类具体 比如派生类圆形 就让这个形状具体起来了
我们可以定义多个这样的派生类 比如像Square 正方形
虽然抽象类 不能实例化对象 但是当作函数传参的类型是可以的!
(3)抽象类的虚表
抽象类是有虚表的 只要有虚函数(包括纯虚函数) 就存在虚表
但是抽象类是无法直接拥有虚表指针
因为虚表指针存在对象里面的 抽象类都实例不出对象
(我有虚表的前提是对象要用虚函数才有 虚表里存的是指向虚函数的指针 不管有没有虚表 虚函数都是存在公共区域!)
但是派生类是可以实例化对象的 存在虚表指针(还会存在重写!)
(4)纯虚函数的作用
纯虚函数作用是 强制实现了重写 因为你如果没有重写 就不能实例化对象 就无法实例化
(5)抽象类的成员
抽象类可以有自己的变量和默认成员函数和普通成员函数 因为派生类中基类那部分实例化和销毁的时候调用的都是基类的成员函数!
(6)纯虚函数和override的区别
override是强制检查有没有实现重写 没有实现会报错
纯虚函数:间接强制实现虚函数重写