作者前言
🎂 ✨✨✨✨✨✨🍧🍧🍧🍧🍧🍧🍧🎂
🎂 作者介绍: 🎂🎂
🎂 🎉🎉🎉🎉🎉🎉🎉 🎂
🎂作者id:老秦包你会, 🎂
简单介绍:🎂🎂🎂🎂🎂🎂🎂🎂🎂🎂🎂🎂🎂🎂🎂
喜欢学习C语言、C++和python等编程语言,是一位爱分享的博主,有兴趣的小可爱可以来互讨 🎂🎂🎂🎂🎂🎂🎂🎂
🎂个人主页::小小页面🎂
🎂gitee页面:秦大大🎂
🎂🎂🎂🎂🎂🎂🎂🎂
🎂 一个爱分享的小博主 欢迎小可爱们前来借鉴🎂
多态
- **作者前言**
- 概念
- 介绍
- 虚函数
- 虚函数重写
- 虚函数重写的协变
- 析构函数的重写(基类与派生类析构函数的名字不同)
- 重载、覆盖(重写)、隐藏(重定义)的对比
- c++ 11 增加的override 和 final
- 多态原理
- 动态绑定和静态绑定
- 抽象类
- 接口继承和实现继承
- 虚函数表存在区域
- 多继承的多态
概念
多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。
介绍
多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如Student继承了Person。Person对象买票全价,Student对象买票半价。简单理解就是,不同的对象,调用相同的函数,会产生不一样的结果。
前面我们知道,继承后,不管基类还是派生类调用相同的函数都会产生相同的效果,从这里就可以猜想出,多态和继承的差别了
所以说要想写出多态,有以下条件:
- 使用virtual修饰函数,(虚函数重写)
简单的理解就是, 基类和派生类的两个虚函数 要有三同(函数名、参数以及返回值)
只能修饰函数 - 必须通过基类的指针或者引用去调用虚函数
这里我们可以理解一下, 前面我们知道派生类的对象可以赋值给基类的指针、引用以及基类对象, 这里就可以很好的理解为,传入基类对象,调用基类的虚函数,传入派生类的对象,会进行切片,然后调用调用切片后的虚函数
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
namespace bit
{
class Person
{
public:
virtual void BuyTicket()
{
cout << "我是普通人===》 全票" << endl;
}
};
struct Student : Person
{
virtual void BuyTicket()
{
cout << "我是学生===》 半票" << endl;
}
};
void BuyT1(Person* People)
{
People->BuyTicket();
}
void BuyT2(Person& People)
{
People.BuyTicket();
}
}
int main()
{
bit::Person People1;
bit::Student stud;
bit::BuyT1(&People1);
bit::BuyT1(&stud);
bit::BuyT2(People1);
bit::BuyT2(stud);
return 0;
}
结果:
虚函数
我们以上面的代码为例子
虚函数:即被virtual修饰的类成员函数称为虚函数
virtual void BuyTicket()
{
cout << "我是学生===》 半票" << endl;
}
虚函数重写
虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数
class Person
{
public:
virtual void BuyTicket()
{
cout << "我是普通人===》 全票" << endl;
}
};
struct Student : Person
{
virtual void BuyTicket()
{
cout << "我是学生===》 半票" << endl;
}
};
可以看出,派生类对基类进行了虚函数的重写
虚函数重写的协变
虽然虚函数很好理解,但是也会出问题,其中一个问题就叫协变
- 协变(基类与派生类虚函数返回值类型不同)
派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。(了解)
简单的理解就是我们写出来的虚函数的返回值是基类的地址或者引用,可是我们返回的这个基类的地址或者引用可能是原本基类的,也可能是派生类切片的
再通俗的说就是,这个返回值必须是基类或者是派生类。否则不构成协变就会报错 ,如果都返回基类不是协变,而是 虚函数重写,
namespace bit
{
class A
{
public:
virtual A* funtion()
{
cout << "A()" << endl;
return this;
}
};
struct B : A
{
virtual B* funtion()
{
cout << "B()" << endl;
return this;
}
};
}
int main()
{
bit::A a;
bit::B b;
a.funtion();
b.funtion();
return 0;
}
析构函数的重写(基类与派生类析构函数的名字不同)
普通调用
namespace bit
{
class A
{
public:
virtual A* funtion()
{
cout << "A()" << endl;
return this;
}
~A()
{
cout << "~A()" << endl;
}
};
struct B : A
{
public:
virtual B* funtion()
{
cout << "B()" << endl;
return this;
}
~B()
{
cout << "~B()" << endl;
}
};
int main()
{
bit::A *a = new bit::A;
bit::A *b = new bit::B;
delete a;
delete b;
return 0;
}
结果:
可以看到,这是是普通的调用。这里是有内存泄漏的情况,进而可以得出: 普通调用,看的是指针、引用或者对象的类型,所以说图中的a和b释放的话只能调用基类的析构,调用不了派生类的析构,要想各自调用各自的,可以进行虚函数重写
前面我们知道,多态的调用主要是看基类的指针或者引用,基类的话调用基类的,派生类切片的就调用派生类的,在这里的话,多态还要满足一个条件,就是两个虚函数要三同,可是析构函数的函数名是不同的,还是可以进行虚函数 ,
原因:
编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。
namespace bit
{
class A
{
public:
virtual A* funtion()
{
cout << "A()" << endl;
return this;
}
virtual ~A()
{
cout << "~A()" << endl;
}
};
struct B : A
{
public:
virtual B* funtion()
{
cout << "B()" << endl;
return this;
}
virtual ~B()
{
cout << "~B()" << endl;
}
};
}
结果:
还有一些情况:
基类去掉virtual,派生类不去掉,不构成虚函数重写,但是构成隐藏
class A
{
public:
A* funtion()
{
cout << "A()" << endl;
return this;
}
virtual ~A()
{
cout << "~A()" << endl;
}
};
struct B : A
{
public:
virtual B* funtion()
{
cout << "B()" << endl;
return this;
}
virtual ~B()
{
cout << "~B()" << endl;
}
};
int main()
{
bit::A *a = new bit::A;
bit::A *b = new bit::B;
a->funtion();
b->funtion();
(new bit::B)->funtion();
delete a;
delete b;
return 0;
}
这种情况的话基类的funtion()和派生类的funtion()构成隐藏关系,不是虚函数重写
派生类去掉virtual,基类不去掉,构成虚函数重写
class A
{
public:
virtual A* funtion()
{
cout << "A()" << endl;
return this;
}
virtual ~A()
{
cout << "~A()" << endl;
}
};
struct B : A
{
public:
B* funtion()
{
cout << "B()" << endl;
return this;
}
virtual ~B()
{
cout << "~B()" << endl;
}
};
int main()
{
bit::A *a = new bit::A;
bit::A *b = new bit::B;
a->funtion();
b->funtion();
(new bit::B)->funtion();
delete a;
delete b;
return 0;
}
一般建议这样写,因为怕别人看不懂,
如果这些难记的话,就简单的记得,只要基类使用了virtual修饰函数,派生类写一样的函数就是虚函数(无论是否加了virtual修饰)
还
重载、覆盖(重写)、隐藏(重定义)的对比
还有一些需要注意, 派生类调用父类的成员,使用的是父类对象进行调用(过程有切片),而不是派生类对象,
还要多态调用的重写,是派生类对基类虚函数的实现重写(基类虚函数的函数体拷贝一份到派生类的虚函数中,内容自己重写来覆盖掉,也就是为啥可以不加virtual也可以构成虚函数)
c++ 11 增加的override 和 final
实现一个类不能被继承的话有几种方法
-
父类构造函数私有化,派生类实例不了对象
-
final修饰的类为最终类,不能被继承
final:修饰虚函数,表示该虚函数不能再被重写
class A
{
public:
virtual A* funtion() final
{
cout << "A()" << endl;
return this;
}
virtual ~A()
{
cout << "~A()" << endl;
}
};
struct B : A
{
public:
virtual B* funtion()//会报错
{
cout << "B()" << endl;
return this;
}
virtual ~B()
{
cout << "~B()" << endl;
}
};
检查虚函数是否被派生类重写的话, 可以增加override进行修饰
override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
class A
{
public:
virtual A* funtion()
{
cout << "A()" << endl;
return this;
}
virtual ~A()
{
cout << "~A()" << endl;
}
};
struct B : A
{
public:
virtual B* funtion() override
{
cout << "B()" << endl;
return this;
}
virtual ~B() override
{
cout << "~B()" << endl;
}
};
多态原理
在讲这个我们先做一道题
class A
{
public:
void funtion()
{
cout << "我爱中国" << endl;
}
virtual void funtion1()
{
cout << "我爱中国" << endl;
}
virtual void funtion2()
{
cout << "我爱中国" << endl;
}
virtual void funtion3()
{
cout << "我爱中国" << endl;
}
protected:
int _b;
};
int main()
{
A a;
cout << sizeof(a) << endl;
return 0;
}
我们看看sizeof(A)的大小是(64位下的)
这个结果看过去不太符合内存对齐,有这个效果是因为,拥有虚函数的类会增加一个成员_vfptr
这个_vfptr是一个指针,这个指针我们叫做虚函数表指针(v代表virtual,f代表function),一个含有虚函数的类中都至少都有一个虚函数表指针,虚函数指针要存放在虚函数表里面,虚函数表也简称虚表 ,所以说_vfprt也叫虚表指针,
然后我们看看派生类的虚表
写一个派生类
struct B : A
{
virtual void funtion1()
{
cout << "我非常的爱中国" << endl;
}
protected:
int _bb;
};
int main()
{
A a;
B b;
cout << sizeof(a) << endl;
return 0;
}
可以看到b对象的里面的成员
如果细心的发现可以看出基类的虚表和派生类的虚表不一样,我们大致可以想到,派生类的虚表是拷贝基类的过来,然后如果发现派生类对基类的虚函数进行了重写,就会把重写后的派生类虚函数指针覆盖基类的虚函数指针,进而达到了多态效果
多态调用的原理: 运行时,去虚函数表里面找虚函数指针,进行调用,指针指向基类虚函数就调用基类的虚函数,否则反之(运行时找到地址))
普通调用的原理: 编译时,通过类型去确定函数地址(编译时找到地址))
总结:
虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr(不同的编译器有不同的做法-)。对象里面存放的是虚表指针,不是虚表
a.先将基类中的虚表内容拷贝一份到派生类虚表中
b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数
c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。需要注意的是在VS中的监视窗口是不会显示出来的,需要看内存区才能看到派生类的虚函数地址
虚函数以及普通函数编译完后,就是一段指令, 所以说虚函数和普通函数都存放在代码段中
动态绑定和静态绑定
静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,
比如:函数重载
动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。
抽象类
概念:
在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
//抽象类
class A
{
public:
virtual void funtion() = 0;//纯虚函数
};
struct B : A
{
public:
virtual void funtion()
{
cout << "我喜欢中国" << endl;
}
};
struct c : A
{
}
int main()
{
bit::A a;//不能创建
bit::B b;//可以创建
bit::C c;//不能创建
b.funtion();
return 0;
}
接口继承和实现继承
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。
虚函数表存在区域
前面我们知道,指针类型决定解引用访问多大的字节, 比如 int* 解引用访问四个字节,char* 访问一个,地址类型进行强转(类型相近才可以强转,指针类型可以随意), 所以可以通过这个方法来判断内存的大小端存储,如果我们要获取到虚函数的地址, 我们可以根据这个方法来, 先获取到对象的地址,然后强转成int* ,然后解引用,就可以获取到四个字节的内容,也就是虚函数的地址,
我们还是利用上面的代码来
namespace bit
{
class A
{
public:
void funtion()
{
cout << "我爱中国" << endl;
}
virtual void funtion1()
{
cout << "我爱中国" << endl;
}
virtual void funtion2()
{
cout << "我爱中国" << endl;
}
virtual void funtion3()
{
cout << "我爱中国" << endl;
}
protected:
int _b;
};
struct B : A
{
public:
virtual void funtion1()
{
cout << "我非常爱中国" << endl;
}
protected:
int _bb;
};
}
int main()
{
bit::A a;
bit::A* p = &a;
int* pa = (int*)p;//获取到虚函数表的地址
const char* ch = "ssssss";// 存放在常量区
static int digth = 0;//存放在静态区
printf("bit::A :%p\n", *pa);
printf("const char* :%p\n", ch);
printf("static int* :%p\n", &digth);
return 0;
}
可以看出前两个的地址相近不远,所以可以判断虚函数表存放在常量区中
我们也可以根据地址来调用函数
int main()
{
bit::A a;
bit::A* p = &a;
int* pa = (int*)p;
for (int i = 0; (void(*)())(((int*)(*pa))[i]) != nullptr; i++)
{
((void(*)())(((int*)(*pa))[i]))();
}
return 0;
}
c/c++的内存分配
- 栈(stack):又称堆栈,栈是由编译器自动分配释放,存放函数的参数值,局部变量的值等(但不包括static声明的变量,static意味着在数据段中存放变量)。除此之外,在函数被调用时,栈用来传递参数和返回值。由于栈的先进后出的特点,所以栈特别方便用来保存/恢复调用数据。其操作方式类似于数据结构中的栈。
- 堆(heap):堆是用于存放进程运行中被动态分配的内存段,它的大小,并不固定,可动态扩张或缩放。当进程调用malloc/free等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张)/释放的内存从堆中被提出(堆被缩减)。堆一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。
- 全局数据区(静态区)(static):全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和静态变量在相邻的另一块区域。
- 文字常量区:常量字符串就是放在这里,程序结束后由系统释放。
- 程序代码区:存放函数体的二进制代码
需要注意的是, 多个相同的对象,使用的虚函数表是一样的,
总结:
虚函数表位于只读数据段(.rodata),即:C++内存模型中的常量区;
虚函数代码则位于代码段(.text),也就是C++内存模型中的代码区
多继承的多态
前面我们写的都是单继承,现在我们来讲一讲多继承的多态
namespace bit
{
class A
{
public:
void funtion()
{
cout << "我爱中国" << endl;
}
virtual void funtion1()
{
cout << "我爱中国" << endl;
}
virtual void funtion2()
{
cout << "我爱中国" << endl;
}
virtual void funtion3()
{
cout << "我爱中国" << endl;
}
protected:
int _a;
};
struct B
{
public:
void funtion4()
{
cout << "我非常爱中国" << endl;
}
virtual void funtion5()
{
cout << "我非常爱中国" << endl;
}
virtual void funtion6()
{
cout << "我非常爱中国" << endl;
}
protected:
int _bb;
};
struct C : A, B // 先继承的先初始化
{
public:
virtual void funtion1()
{
cout << "我超喜欢中国" << endl;
}
virtual void funtion5()
{
cout << "我超喜欢中国" << endl;
}
virtual void funtion9()
{
cout << "我超喜欢中国" << endl;
}
protected:
int _c;
};
}
int main()
{
bit::A a;
bit::B b;
bit::C c;
cout << sizeof(c);
return 0;
}
结果:
可以看到c里面有两个虚表指针,派生类和基类的虚表不一样,这点不难,
我们需要思考的是,funtion9函数的指针放在哪里呢,
代码如下:
int main()
{
bit::C c;
bit::A* a = &c;
bit::B* b = &c;
for (int i = 0; (int*)(((int*)(*(int*)a))[i]) != nullptr; i++)
{
printf("[%d]:%p->",i, (((int*)(*(int*)a))[i]));
((void(*)())(((int*)(*(int*)a))[i]))();
}
cout << endl;
for (int i = 0; (int*)(((int*)(*(int*)b))[i]) != nullptr; i++)
{
printf("[%d]:%p", i, (((int*)(*(int*)b))[i]));
((void(*)())(((int*)(*(int*)b))[i]))();
}
cout << sizeof(c);
return 0;
}
结果:
可以看出,派生类没有的虚函数的指针存放在第一张虚函数表里面,而不是第二张
依次也可以说明,一个类可以有多个虚表。