当前位置: 首页 > news >正文

C++多态

深入剖析C++多态、VPTR指针、虚函数表

C++虚函数表分析

C++继承中重载、重写、重定义的区别:

C++虚函数表剖析

C++获取虚表地址和虚函数地址时的两个强制类型转换

类型兼容性原则

  类型兼容规则是指在需要基类对象的任何地方,都可以使用公有派生类的对象来替代。通过公有继承,派生类得到了基类中除构造函数、析构函数之外的所有成员。这样,公有派生类实际就具备了基类的所有功能,凡是基类能解决的问题,公有派生类都可以解决。

  类型兼容规则中所指的替代包括以下情况:(1)子类对象可以当作父类对象;(2)使用子类对象可以直接赋值给父类对象;(3)子类对象可以直接初始化父类对象;父类指针可以直接指向子类对象;父类引用可以直接引用子类对象。

  在替代之后,派生类对象就可以作为基类的对象使用,但是只能使用从基类继承的成员。

 1 class Animal{
 2 public:
 3     Animal(){
 4         cout << "Animal默认构造函数" << endl; 
 5     }
 6 
 7     void aniSpeak(){
 8         cout << "Animal speak" << endl;
 9     }
10     Animal(int m){
11         cout << "Animal有参构造函数" << endl;
12         m_Age = m;
13     }
14     Animal(const Animal&p){
15         cout << "Animal拷贝构造函数" << endl;
16     }
17 
18     int m_Age;
19 };
20 
21 class Sheep:public Animal{
22 public:
23     void sheSpeak(){
24         cout << "Sheep speak" << endl;
25     }
26 
27     Sheep(){ cout << "Sheep默认构造函数" << endl; }
28 
29     Sheep(int m){
30         cout << "Sheep有参构造函数" << endl;
31         m_sheAge = m;
32     }
33 
34     Sheep(const Sheep& sp){
35         cout << "Sheep拷贝构造函数" << endl;
36     }
37 
38     int m_sheAge;
39 };
40 
41 //父类指针和父类引用,只能执行父类函数
42 void doSpeak(Animal &animal){
43     animal.aniSpeak();
44 }
45 
46 void doSpeak2(Animal *animal){
47     animal->aniSpeak();
48 }
49 
50 void test01(){
51     Animal animal;
52     animal.aniSpeak(); //animal speak
53     
54     //复习继承中创建子类对象时的构造函数
55     //-----------------------
56     //子类调用某种构造函数时,先调用父类中对应的构造函数
57     //-----------------------
58     cout << "------sheep默认------" << endl;
59     Sheep sheep; 
60     cout << "--------------------" << endl;
61 
62     cout << "------sheep有参------" << endl;
63     Sheep sheep0(0);
64     cout << "--------------------" << endl;
65 
66     cout << "-----sheep拷贝-------" << endl;
67     Sheep sheep1 = sheep;
68     cout << "--------------------" << endl;
69 
70     //子类对象初始化父类对象
71     //-----------------
72     //空指针也可以访问成员函数,但要注意this指针
73     //Animal *animalPtr = NULL;
74     //animalPtr->aniSpeak();
75     //-----------------
76 
77     //父类对象通过new在堆上初始化
78     Animal *animalequ0 = new Sheep; 
79     Animal *animalequ1 = new Sheep();
80     Animal *animalequ2 = new Sheep(1);
81 
82     //父类对象在栈上初始化
83     Animal *animalPtr = NULL;
84     animalPtr = &sheep;
85     animalPtr->aniSpeak(); //animal speak
86 
87     //子类对象直接赋值给父类对象
88     Animal animalequ3 = sheep;
89     Animal animalequ4(sheep);
90 
91     //子类对象直接当父类对象
92     sheep.aniSpeak(); //animal speak
93 
94     //父类对象或指针指向子类对象
95     doSpeak(sheep); //animal speak
96     doSpeak2(&sheep); //animal speak
97 }
类型兼容规则示例

多态引出

  上述代码中,由于类型兼容性原则不管传入子类还是父类对象,都是调用的父类函数,但我们想传入子类对象调用子类函数,传入父类对象调用父类函数,即同样的调用语句有多种不同的表现形态。

  为此,引入多态来解决该问题,多态实现的基础:(1)要有继承;(2)要有虚函数重写;(3)父类指针(引用)指向子类对象

1. 虚函数

  虚函数机制是实现多态的关键技术,对于父类与子类的同名函数,父类同名函数必须声明为virtual函数,子类的的同名函数前面的virtual可写可不写。

2. 重写/重定义/重载

  重载overload:是函数名相同,参数列表不同。重载只是在同一个类的内部存在,但是不能靠返回类型来判断

  重写override:也叫做覆盖。子类重新定义父类中有相同名称和参数的虚函数。两者的函数特征相同。但是具体实现不同,主要是在继承关系中出现的 。

  重写需要注意:

    (1)被重写的函数不能是static的。必须是virtual的

    (2)重写函数必须有相同的类型,名称和参数列表

    (3)重写函数的访问权限可以不同。尽管virtual是private的,子类中重写改写为public,protected也是可以的

  重定义redefining:也叫做隐藏。子类重新定义父类中有相同名称的非虚函数 ( 参数列表可以不同 ) 。如果一个类,存在和父类相同的函数,那么,这个类将会覆盖其父类的方法,除非你在调用的时候,强制转换为父类类型或加上父类作用域,否则试图对子类和父类做类似重载的调用是不能成功的(详见:C++继承:继承中的同名处理)。 

 1 class Parent0
 2 {
 3 public:
 4     //以下三个函数在同一个类中表现为重载
 5     virtual void fun(){
 6         cout << "func1()" << endl;
 7     }
 8     virtual void fun(int i){
 9         cout << "func2()" << endl;
10     }
11     virtual void fun(int i, int j){
12         cout << "func3()" << endl;
13     }
14     int abc(){
15         cout << "abc" << endl;
16     }
17     virtual void fun(int i, int j, int k, int r){
18         cout << "fun(i,j,k,r)" << endl;
19     }
20 };
21 
22 class Parent1
23 {
24 public:
25     int abc(){
26         return 0;
27     }
28 };
29 
30 class Son :public Parent1
31 {
32 public:
33     int abc(){//非虚函数重写--->重定义
34         cout << "abc" << endl;
35         return 1;
36     }
37 };
38 
39 class Parent2
40 {
41 public:
42     virtual void fun(int i, int j){
43         cout << "func3()" << endl;
44     }
45 };
46 
47 class Son :public Parent2
48 {
49 public:
50     void fun(int i, int j){//与父类相同,這是虚函数重写
51         cout << "fun(int i,int j)" << endl;
52     }
53 };
重写、重载、重定义示例

多态

  多态,就是根据实际的对象类型决定函数调用语句的具体调用目标。总结一句话就是,同样的调用语句有多种不同的表现形态。

  多态分为静态多态(运算符重载、函数重载)和动态多态(派生类、虚函数),两者主要的区别:函数地址是早绑定(静态联编)还是晚绑定(动态联编)。即,在编译阶段确定好地址还是在运行时才确定地址。

 1 class Animal{
 2 public:
 3     virtual void speak(){ //在父类中声明虚函数,可以实现多态,动态联编
 4         cout << "Animal speak" << endl;
 5     }
 6 };
 7 
 8 class Sheep :public Animal{
 9 public:
10     void speak(){ //发生多态时,子类对父类中的成员函数进行重写,virtual可写可不写
11         cout << "Sheep speak" << endl;
12     }
13 };
14 
15 void doSpeak(Animal &animal){
16     animal.speak();
17 }
18 
19 //想通过父类引用指向子类对象,即输出sheep speak
20 void test01(){
21     Sheep sheep;
22     doSpeak(sheep); //sheep speak;
23 
24     Animal amimal0 = sheep;
25     animal0.speak(); //sheep speak;
26 
27     Animal *animal = new Sheep();
28     animal->speak(); //sheep speak;
29 }
多态简单示例

多态原理剖析

  类似于虚继承中的虚基类指针和虚基类表,多态中的虚函数会有虚函数指针和虚函数表,通过开发人员工具可以清楚的看到虚函数指针和虚函数表的内部结构和存储位置。

1. 不同继承状态下的结构分析

  (1)一般继承(无虚函数覆盖)

 1 class Animal{
 2 public:
 3     virtual void aniSpeak(){
 4         cout << "Animal speak" << endl;
 5     }
 6 
 7 };
 8 
 9 class Sheep:public Animal{
10 public:
11     virtual void sheSpeak(){
12         cout << "Sheep speak" << endl;
13     }
14 };
一般继承(无虚函数覆盖)

  图中所示:虚函数按照其声明顺序放于表中;父类的虚函数在子类的虚函数前面。

  (2)一般继承(有虚函数覆盖)

 1 class Animal{
 2 public:
 3     virtual void aniSpeak(){
 4         cout << "Animal speak" << endl;
 5     }
 6 
 7 };
 8 
 9 class Sheep:public Animal{
10 public:
11     virtual void sheSpeak(){
12         cout << "Sheep speak" << endl;
13     }
14     virtual void aniSpeak(){
15         cout << "Sheep not speak" << endl;
16     }
17 };
一般继承(有虚函数覆盖)

  图中所示:子类中重写的aniSpeak函数在虚函数表中覆盖了之前的父类函数;未重写的函数,并没有发生改变。

  (3)多继承(无虚函数覆盖)

 1 class Animal{
 2 public:
 3     virtual void aniSpeak(){
 4         cout << "Animal speak" << endl;
 5     }
 6 
 7 };
 8 
 9 class Sheep{
10 public:
11     virtual void sheSpeak(){
12         cout << "Sheep speak" << endl;
13     }
14 };
15 
16 class Tuo :public Animal, public Sheep{
17 public:
18     virtual void TuoSpeak(){
19         cout << "Tuo speak" << endl;
20     }
21 };
多继承(无虚函数覆盖)

  图中所示:(1)每个父类都有自己的虚表;(2)子类的成员函数被放到了第一个父类的表中。(所谓的第一个父类是按照声明顺序来判断的),这样做就是为了解决不同的父类类型的指针指向同一个子类实例,而能够调用到实际的函数。

  (4)多继承(有虚函数覆盖)

 1 class Animal{
 2 public:
 3     virtual void aniSpeak(){
 4         cout << "Animal speak" << endl;
 5     }
 6 
 7 };
 8 
 9 class Sheep{
10 public:
11     virtual void sheSpeak(){
12         cout << "Sheep speak" << endl;
13     }
14 };
15 
16 class Tuo :public Animal, public Sheep{
17 public:
18     virtual void aniSpeak(){
19         cout << "Tuo not speak" << endl;
20     }
21     virtual void sheSpeak(){
22         cout << "Sheep not speak" << endl;
23     }
24     virtual void TuoSpeak(){
25         cout << "Tuo speak" << endl;
26     }
27 };
多继承(有虚函数覆盖)

  图中所示:子类重写了aniSpeak()函数、sheSpeak()函数,在虚函数表中覆盖了之前的父类函数。

2. 虚函数指针和虚函数表

  实现多态的流程:虚函数指针->虚函数表->函数指针->入口地址,虚函数表(vftable)属于类,或者说这个类的所有对象共享一个虚函数表;虚函数指针(vfptr)属于单个对象。

  在程序调用时,先创建对象,编译器在对象的内存结构头部添加一个虚函数指针,进行动态绑定,虚函数指针指向对象所属类的虚函数表。

  虚函数表是一个指针数组,其元素是虚函数的指针,每个元素对应一个函数的指针。如果子类对父类中的一个或多个虚函数进行重写,子类的虚函数表中的元素顺序,会按照父类中的虚函数顺序存储,之后才是自己类的函数顺序。

 1 class Animal{
 2 public:
 3     virtual void AniSpeak(){
 4         cout << "Animal speak" << endl;
 5     }
 6     virtual void aniSpeak(){
 7         cout << "Animal speak" << endl;
 8     }
 9     
10 
11 };
12 
13 class Tuo :public Animal{
14 public:
15     
16     virtual void TuoSpeak(){
17         cout << "Tuo speak" << endl;
18     }
19     virtual void aniSpeak(){
20         cout << "Tuo not speak" << endl;
21     }
22     virtual void AniSpeak(){
23         cout << "Sheep not speak" << endl;
24     }
25     int m_Age;
26 };
子类对父类两个虚函数重

  编译器根本不会去区分,传进来的是子类对象还是父类对象,而是关心print()是否为虚函数。如果是虚函数,就根据不同对象的vptr指针找属于自己的函数。父类对象和子类对象都有vfptr指针,传入对象不同,编译器会根据vfptr指针,到属于自己虚函数表中找自己的函数。即:vptr--->虚函数表------>函数的入口地址,从而实现了迟绑定(在运行的时候,才会去判断)。

  如果不是虚函数,那么这种绑定关系在编译的时候就已经确定的,也就是静态联编!

  虚函数表(vftable)存储着所有虚函数的位置,由于其动态绑定特性,在重写(override)后在子类中存储的虚函数位置与父类中不相同,这样才会实现同样的调用语句传入不同类型的参数有多种不同的表现形态,详见如下示例。

 1 class A {
 2 public:
 3     virtual void vfunc1();
 4     virtual void vfunc2();
 5     void func1();
 6     void func2();
 7 private:
 8     int m_data1, m_data2;
 9 };
10 
11 class B : public A {
12 public:
13     virtual void vfunc1();
14     void func1();
15 private:
16     int m_data3;
17 };
18 
19 class C: public B {
20 public:
21     virtual void vfunc2();
22     void func2();
23 private:
24     int m_data1, m_data4;
25 };
动态绑定示例

  从上图中可看出, 实例化一个对象,可以想象为每个对象为一个结构体,首元素为指针,其余为自身数据(只包括非静态数据,不包括静态成员和非虚函数)。对象的地址,是数组中首元素的地址,即虚函数指针,虚函数指针指向虚函数表(虚函数指针的值为虚函数表的地址),可以通过地址偏移访问到每个成员属性和成员函数,具体见下一小节。

3. 深入剖析虚函数表

  (1)确定虚函数指针的存在

 1 class Parent1{
 2 public:
 3         Parent1(int a=0){
 4             this->a = a;}
 5         void print(){ 
 6             cout<<"parent"<<endl;}  
 7 private:
 8         int a;
 9 };
10 class Parent2{
11 public:
12         Parent2(int a=0){
13             this->a = a;}
14         virtual void print(){ 
15             cout<<"parent"<<endl;}  
16 private:
17         int a;
18 };
19 
20 void main(int argc, char const *argv[]){
21         cout<<"Parent1"<<sizeof(Parent1)<<endl; //4
22         cout<<"Parent2"<<sizeof(Parent2)<<endl; //8
23         return 0;
24 }
确定虚函数指针存在

  通过sizeof运算符我们可以查看一个对象所占的空间,对于一个只有函数(无虚函数)没有属性的空类对象,它的空间长度为1,而对于有虚函数、没有属性的空类对象,无论虚函数的数量为多少,空间长度都为4,这是因为它包含一个指向虚表的指针,指针内存放的是虚表的地址。

  (2)虚函数指针的个数

  子类中的虚函数指针个数和继承的类个数相关,继承的每个类仅产生一个虚函数指针。

  (3)编译器寻找虚函数地址

  对象本身地址类型为class *,但指针内的值为整型,因此需要涉及到C++指针类型间强制转换(详见C++指针类型间强制转换)。对象包含一个指向虚表的指针,指针内存放的是虚表的地址,指针的长度为4个字节。

  ①tuo 对象相当于一个结构体,首元素是虚指针,后面是继承的数据及自身的其他数据

  ②&tuo 对象的地址,结构体的地址,首元素的地址,也是虚函数指针vfptr的地址,空间长度为四个字节

  ③(int *)(&tuo) &tuo的类型为class Tuo*,需要强转为int *,因为int*和指针都是4个字节,可以互相转换,为了后面用int进行解释

  ④*(int*)(&tuo) 用int类型解释&tuo指向的数据,取到了虚函数表(同样相当于指针数组)的地址值,也是指针数组的首元素的地址值

  ⑤(int*)*(int *)(&tuo) 地址值为整型,需将其转化为合法地址,虚函数表里面存储的是函数指针(函数的入口地址),函数指针为4个字节,强转为int *,对虚函数表的地址取前四个字节

  ⑥*(int*)*(int *)(&tuo) 对地址解引用,得到第一个函数指针

  ⑦((void(*)()) (*(int_*)*(int_ *)(&tuo)))() 通过函数指针进行函数调用(详见C语言函数指针和回调函数)

 1 class Animal{
 2 public:
 3     virtual void AniSpeak(){
 4         cout << "Animal speak" << endl;
 5     }
 6     virtual void aniSpeak(){
 7         cout << "Animal speak" << endl;
 8     }
 9 };
10 
11 class Tuo :public Animal{
12 public:
13     Tuo(){ m_Age = 10; }
14     virtual void TuoSpeak(){
15         cout << "Tuo speak" << endl;
16     }
17     virtual void aniSpeak(){
18         cout << "Tuo not speak" << endl;
19     }
20     virtual void AniSpeak(){
21         cout << "Sheep not speak" << endl;
22     }
23     int m_Age;
24 };
25 
26 void test03(){
27     cout << "----------------class Tuo---------------" << endl;
28     //虽然地址没有类型,但是地址的值(8位十六进制数字),为整型值,将地址解释为其他类型(float或char)没有意义
29     //由于虚函数指针和函数指针均为4个字节,强转时必须也为4个字节,否则在取数据时会出现错误
30     typedef long int_;
31 
32     Tuo tuo;
33     //cout << (float*)*(float*)(&tuo) << endl; //*(float*)(&tuo)将对象地址强转为float*,将其指向的数据按float进行解释为浮点型,但浮点型无法作为合法地址
34     cout << *(int*)*(int*)(&tuo) << endl; //*(int*)(&tuo)将对象地址强转为int*,将其指向的数据按int进行解释为整型,整型可以强转作为合法地址
35 
36     //它包含一个指向虚表的指针,指针内存放的是虚表的地址,指针的长度为4个字节
37     //tuo 对象相当于一个指针数组,首元素是虚指针,后面是继承的数据及自身的其他数据
38     //&tuo 对象的地址,指针数组的地址,首元素的地址,也是虚函数指针vfptr的地址,空间长度为四个字节
39     //(int *)(&tuo) &tuo的类型为class Tuo*,需要强转为int *,因为int*和指针都是4个字节,可以互相转换,为了后面用int进行解释
40     //*(int*)(&tuo) 用int类型解释&tuo指向的数据,取到了虚函数表(同样相当于指针数组)的地址值,也是指针数组的首元素的地址值
41     //(int*)*(int *)(&tuo) 地址值为整型,需将其转化为合法地址,虚函数表里面存储的是函数指针(函数的入口地址),函数指针为4个字节,强转为int *,对虚函数表的地址取前四个字节
42     //*(int*)*(int *)(&tuo) 对地址解引用,得到第一个函数指针
43     //((void(*)()) (*(int_*)*(int_ *)(&tuo)))() 通过函数指针进行函数调用
44 
45     int_ p0 = (*((int_ *)(&tuo) + 1));
46     cout << p0 << endl;
47 
48     ((void(*)()) (*(int_*)*(int_ *)(&tuo)))();
49 
50     typedef void(*func)();
51     func p = (func)(*(int_*)*(int_*)(&tuo));
52     p();
53 }
54     
编译器寻找函数地址示例

  在取虚表地址的代码中,我们可以看到这个虚表指针是没有名字的,因此就无法像我们平常使用指针那样直接调用变量来引用内容,只能通过*(int*)&tuo的方式获取指针的内容。

  (4)通过对象指针寻找成员地址

 1 //--------------------------
 2 //通过对象指针查找对象成员
 3 //--------------------------
 4 class Person{
 5 public:
 6     Person(){ m_A = 10; m_B = 20; }
 7 
 8     int m_A;
 9     float m_B;
10 };
11 
12 void test01(){
13     Person person;
14     //通过对象地址查找对象成员
15     int m_a = 0;
16     m_a = *(int *)(&person);
17     cout << m_a << endl;
18 }
对象指针寻找成员地址

  具体寻址原理与编译器寻找虚函数地址同。

  (5)内存中对象和虚函数表的数据结构

 1 class Animal{
 2 public:
 3     virtual void AniSpeak(){
 4         cout << "Animal speak" << endl;
 5     }
 6     virtual void aniSpeak(){
 7         cout << "Animal speak" << endl;
 8     }
 9 };
10 
11 class Tuo :public Animal{
12 public:
13     Tuo(){ m_Age = 10.1; }
14     virtual void TuoSpeak(){
15         cout << "Tuo speak" << endl;
16     }
17     virtual void aniSpeak(){
18         cout << "Tuo not speak" << endl;
19     }
20     virtual void AniSpeak(){
21         cout << "Sheep not speak" << endl;
22     }
23     float m_Age;
24 };
25 
26 void test08(){
27     
28     Tuo tuo;
29 
30     //对象内存的数据结构类似结构体,首元素为指针,其余为自身数据
31     int* mptr = NULL;
32     mptr = (int*)(&tuo);
33     cout << mptr[0] << endl;
34     //cout << mptr[1] << endl;
35     cout << *((float*)(&tuo)+1) << endl;
36 
37     //虚函数表为指针数组
38     typedef void(*func)();
39     func* mtab = NULL;
40     mtab = (func*)((*(int*)(&tuo)));
41     func f = mtab[0];
42     f();
43 }
虚函数指针&&虚函数表数据结构

  内存中对象的数据结构类似结构体,首元素为指针,其余为自身数据,虚函数表的数据结构为指针数组。

4. 构造函数中能否实现多态 

  该问题等价于虚函数指针什么时候初始化?

 1 class Parent{
 2 public:
 3     Parent(int a=0){
 4             this->a = a;
 5             print();
 6     }
 7     virtual void print(){
 8     cout<<"Parent"<<endl;
 9     }
10 private:
11     int a;
12 };
13 class Son:public Parent{
14     Son(int a=0,int b=0):Parent(a){ //初始化列表
15         this->b = b;
16         print();
17     }
18     virtual void print(){
19     cout<<"Son"<<endl;
20     }
21 };
22 void main(){
23         Son s;
24         return 0;
25 }
构造函数实现多态

  代码中构造子类对象,但调用的是父类的print,因此构造函数中不能实现多态。

  ①对象在创建的时,由编译器对VPTR指针进行初始化 

  ②只有当对象的构造完全结束后VPTR的指向才最终确定,即子类对象创建过程中虚函数指针指向会发生变化 

  ③父类对象的VPTR指向父类虚函数表 

  ④子类对象的VPTR指向子类虚函数表

  当定义一个子类对象的时候比较麻烦,因为构造子类对象的时候会首先调用父类的构造函数然后再调用子类的构造函数。当调用父类的构造函数的时候,此时会创建Vptr指针(也可以认为Vptr指针是属于父类的成员,所以在子类中重写虚函数的时候virtual关键字可以省略,因为编译器会识别父类有虚函数,然后就会生成Vptr指针变量),该指针会指向父类的虚函数表;然后再调用子类的构造函数,此时Vptr又被赋值指向子类的虚函数表。 

  (执行父类的构造函数的时候Vptr指针指向的是父类的虚函数表,所以只能执行父类的虚函数)
5. 非虚函数

  (1)虚函数才有虚函数表,非虚函数没有虚函数表,直接编译时静态联编访问;(2)非虚函数不能通过对象指针进行访问,因为非虚函数和静态成员不属于对象。

抽象类和纯虚函数

   在程序设计中,如果仅仅为了设计一些虚函数接口,打算在子类中对其进行重写,那么不需要在父类中对虚函数的函数体提供无意义的代码,可以通过纯虚函数满足需求。

  纯虚函数的语法格式:  1 virtual 返回值类型 函数名 () = 0;   只需要将函数体完全替换为 = 0即可,纯虚函数必须在子类中进行实现,在子类外实现是无效的。

注意:

  (1)如果父类中出现了一个纯虚函数,则这个类变为了抽象类,抽象类不可实例对象;

  (2)如果父类为抽象类,子类继承父类后,必须实现父类所有的纯虚函数,否则子类也为抽象类,也无法实例对象

 1 class Base1{
 2 public:
 3     Base1(){
 4         cout << "默认构造函数" << endl;
 5     }
 6     ~Base1(){
 7         cout << "默认析构函数" << endl;
 8     }
 9 
10     //virtual int getresult(){ return 0; }
11     virtual int getresult() = 0;
12     virtual int getresult0() = 0;
13 };
14 
15 //这里声明是无效的,子类仍为抽象类
16 //int Base1::getresult(){
17 //    cout << "get" << endl;
18 //    return 0;
19 //}
20 
21 class Son1 :public Base1{
22 public:
23     Son1(){
24         cout << "子类默认构造函数" << endl;
25     }
26 
27     ~Son1(){
28         cout << "子类默认析构函数" << endl;
29     }
30 
31     int getresult0(){
32         cout << "get0" << endl;
33         return 0;
34     }
35     int getresult(){
36         cout << "get" << endl;
37         return 0;
38     }
39 
40 };
抽象类&纯虚函数示例

虚析构、纯虚析构

1. 虚析构

  我们知道,仅仅发生继承时,创建子类对象后销毁,函数调用流程为:父类构造函数->子类构造函数->子类析构函数->父类析构函数;当发生多态时(父类指针指向子类对象),通过父类指针在堆上创建子类对象,然后销毁,调用流程为:父类构造函数->子类构造函数->父类析构函数,不会调用子类析构函数,因此子类中会出现内存泄漏问题。

  解决方法:将父类中的析构函数定义为虚函数,语法格式为:  virtual ~类名(){ //Something }  

2. 纯虚析构

  与纯虚函数写法一致,语法格式为: virtual ~类名() = 0; 

   注意(1)纯虚析构需要类内声明,类外实现;(2)纯虚析构也是虚函数,该类也为抽象类;(3)子类不会继承父类的析构函数,当父类纯虚析构没有实现时,子类不是抽象类,可以创建创建对象。

 1 class Base1{
 2 public:
 3     Base1(){
 4         cout << "默认构造函数" << endl;
 5     }
 6 
 7     //发生多态时,普通析构函数不会调用子类析构函数,会导致子类内存泄漏
 8     //~Base1(){
 9     //    cout << "默认析构函数" << endl;
10     //}
11 
12     //改为虚析构即可解决该问题
13     //virtual ~Base1(){
14     //    cout << "默认析构函数" << endl;
15     //}
16 
17     //纯虚析构需要类内声明,类外实现
18     //子类无法继承父类的构造和析构函数,因此需要在子类外实现
19     //纯虚析构也是纯虚函数,需要实现,否则抽象类无法实例对象
20     virtual ~Base1() = 0;
21 };
22 
23 //父类、子类外实现纯虚析构函数
24 Base1::~Base1(){
25     cout << "默认析构函数" << endl;
26 }
27 
28 class Son1 :public Base1{
29 public:
30     Son1(char* name){
31         cout << "子类默认构造函数" << endl;
32         this->m_Name = new char[strlen(name)+1];
33         strcpy(m_Name, name);
34     }
35 
36     ~Son1(){
37         cout << "子类默认析构函数" << endl;
38         if (this->m_Name != NULL){
39             delete[] this->m_Name;
40             this->m_Name = NULL;
41         }
42     }
43 
44     char *m_Name;
45 
46 };
47 
48 void test02(){
49     Base1 *base = new Son1 ("Tom");
50     delete base;
51 
52     //当父类的纯虚析构没有实现时,子类可以实例对象,也可以说明子类没有继承父类的析构函数
53     //Son1 son("tom");
54 }
虚析构&纯虚析构示例

  建议:尽量将析构函数都写为虚析构。

向上&向下类型转换

  派生类继承于基类,除了继承基类的数据,派生类还有自身的数据,因此,基类大小<=派生类大小。

1. 向下类型转换

  基类强转到派生类,称为向下类型转换,这种转换是不安全的,即操作范围容易越界。因为基类创建对象后的空间比派生类空间小,转为派生类后,操作易越界。

2. 向上类型转换

  派生类强转到基类,称为向上类型转换,这种操作是安全的,因为派生类创建的空间比基类的空间大。

3. 多态

  发生多态,及父类引用或指针指向子类对象,此时直接开辟的为派生类的空间,无论怎么强转,总是安全的。

 1 class Base1{
 2 public:
 3     Base1(){
 4         cout << "默认构造函数" << endl;
 5     }
 6     ~Base1(){
 7         cout << "默认析构函数" << endl;
 8     }
 9 };
10 
11 class Son1 :public Base1{
12 public:
13     Son1(){
14         cout << "子类默认构造函数" << endl;
15     }
16 
17     ~Son1(){
18         cout << "子类默认析构函数" << endl;
19     }
20 };
21 
22 void test02(){
23     //向下类型转换
24     Base1 *base0 = new Base1;
25     Son1 *son0 = (Son1*)base0;
26 
27     //向上类型转换
28     Son1 *son1 = new Son1;
29     Base1 *base1 = (Base1*)son1;
30 
31     //多态
32     Base1 *base2 = new Son1;
33 }
类型转换示例

 

转载于:https://www.cnblogs.com/qinguoyi/p/10283214.html

相关文章:

  • MariaDB 数据库
  • 应用调试(三)oops
  • 谷歌是 CNCF 开源项目最大贡献者,红帽次之
  • 海南“多规合一”改革促行政审批提速城乡面貌提质
  • jmap命令 Java Memory Map
  • 服务器从安装到部署全过程(二)
  • 对APP单例的统一封装(常规式)
  • 优化关键渲染路径
  • TiDB 3.0 Beta Release Notes
  • 台湾屏东县一肉鸭场检验出禽流感 扑杀6510只肉鸭
  • 山西球迷大范围辱骂裁判被CBA公司罚款2万元
  • 从前后端分离到GraphQL,携程如何用Node实现?\n
  • Python数据结构和算法学习笔记1
  • 北京因地制宜编制村庄规划 着重体现京韵农味
  • TCPIP网络协议层对应的RFC文档
  • HomeBrew常规使用教程
  • HTTP--网络协议分层,http历史(二)
  • Intervention/image 图片处理扩展包的安装和使用
  • Java多线程(4):使用线程池执行定时任务
  • Java精华积累:初学者都应该搞懂的问题
  • Java面向对象及其三大特征
  • SQLServer之创建数据库快照
  • use Google search engine
  • Vue.js源码(2):初探List Rendering
  • 从重复到重用
  • 代理模式
  • 后端_ThinkPHP5
  • 技术攻略】php设计模式(一):简介及创建型模式
  • 将回调地狱按在地上摩擦的Promise
  • 那些年我们用过的显示性能指标
  • 深入浅出webpack学习(1)--核心概念
  • 一个项目push到多个远程Git仓库
  • 用简单代码看卷积组块发展
  • ​Base64转换成图片,android studio build乱码,找不到okio.ByteString接腾讯人脸识别
  • ​比特币大跌的 2 个原因
  • ​用户画像从0到100的构建思路
  • # 深度解析 Socket 与 WebSocket:原理、区别与应用
  • #[Composer学习笔记]Part1:安装composer并通过composer创建一个项目
  • #前后端分离# 头条发布系统
  • #我与Java虚拟机的故事#连载15:完整阅读的第一本技术书籍
  • (2.2w字)前端单元测试之Jest详解篇
  • (M)unity2D敌人的创建、人物属性设置,遇敌掉血
  • (超详细)2-YOLOV5改进-添加SimAM注意力机制
  • (附源码)springboot宠物管理系统 毕业设计 121654
  • (论文阅读31/100)Stacked hourglass networks for human pose estimation
  • (三维重建学习)已有位姿放入colmap和3D Gaussian Splatting训练
  • (一)Thymeleaf用法——Thymeleaf简介
  • (一)搭建springboot+vue前后端分离项目--前端vue搭建
  • (转)项目管理杂谈-我所期望的新人
  • .bat文件调用java类的main方法
  • .net core webapi 部署iis_一键部署VS插件:让.NET开发者更幸福
  • .NET Framework .NET Core与 .NET 的区别
  • .NET Reactor简单使用教程
  • .Net(C#)常用转换byte转uint32、byte转float等
  • .NET:自动将请求参数绑定到ASPX、ASHX和MVC(菜鸟必看)