c++多态

文章目录

多态感觉真挺难的,这篇就水一水了

引言

多态:多种形态
静态的多态:函数重载,同一个函数看起来调用同一个函数有不同的行为
动态的多态:
静态:是在编译的时候实现的

动态的多态

一个父类的引用或指针去调用同一个函数,如果传递不同的对象,会调用不同的函数
动态:运行时实现的

举个例子:

买票这个行为,对于不同的人价格不同,普通人去买价格低,而有钱人买价格高

扫红包:也是一种多态行为,根据行为去分类,鼓励新用户用,给的红包就多,

虚函数

子类中满足三同
函数名,参数,返回值相同的虚函数,就叫做重写(覆盖),只有虚函数
本质就是不同的人去做同一件事情,结果不同
virtual(只能是类的非静态的成员函数才能是虚函数):其他函数不能称为虚函数
:在最前面加一个virtual

静态成员函数不能加virtual

否则就是隐藏的关系

多态的构成条件(少一个都不行)

  1. 必须通过基类指针或者引用调用虚函数
  2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

重写要求返回值相同,有一个例外:协变:要求返回值是父子关系的指针或者引用(基本不会见到)

class A
class B

1
2
3
4
5
6
7
8


void func(Person p)//我们没有用父类的指针和引用,而是用对象,就不能实现多态
{
p.BuyTicket();
}
func(st);//正常来说因为我们是父类的引用,所以的对父类使用,一定是没有问题的,但是这个对子类也是可以使用的
func(ps);//

析构函数的重写(析构函数)

析构函数也可以是虚函数,构成重写(函数名,返回值,参数,virtual)都相同
把析构函数都被特殊处理成destructor,
完成重写,构成多态,那么才能正确调用析构函数
只有动态申请的对象给了父类指针管理才可以,那么就需要定义成虚函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

class Person
{
public:
virtual ~Person()//析构的名字都被特殊处理过了,差个虚函数的条件,这样就能够实现多态了
{
cout << "person" << endl;
}
};
class Student : public Person
{
public:
virtual ~Student() //

{
cout << "student" << endl;
}
};

//析构函数是否是虚函数都正常调用了
int main()
{
/*
Person p;
Student s;//person也会被析构,但是没加virtual也没有关系,
*/
//
Person *p1 = new Person; // new 是开空间+构造函数
Person *p2 = new Student;//这里是基类的指针,这里是父类的指针,就需要多态了
Student *p3 = new Student;//这里是基类的指针,z这里不需要多态


//p1->destructor()
//p2->destructor()



//多态申请的对象如果给了父类指针管理,那么需要析构函数是虚函数(我们直接全部都)
delete p1;//析构函数+释放空间
//我期望p1这里调用父类的析构函数
delete p2;//指向student就掉student,又会掉person
delete p3;
//这里调用子类的析构函数
//这里不构成多态,都指向person,因为p2也是person
//构成多态才可以
return 0;
}

虚函数的重写允许
:两个都是虚函数,或者父类是虚函数,再满足三同就构成重写
虽然子类没有写virtual
是因为他继承了父类的virtual的属性,也完成了重写(是因为析构函数,父类析构函数加上virtual,就不存在不构成多态的了,没钓到子类析构函数,内存泄漏的)
:在public:private也是可以的

一般来说父类是别人写的,子类是我们写的,只要父类加了virtual,子类就不用加virtual
我们建议都写上

C++11 override 和final

final

设计一个不能被继承的类
c++ 98的方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
//设计一个无法被继承的基类,只能让基类的构造函数变成私有,因为初始化子类对象的时候要去调用其父类的构造函数,但是没法使用,间接限制
//c++ 98的方式
class A
{
private:
A(int a = 0)
: _a(a)
{
}

public:
//因为如果是一个成员函数,返回A 他必须是一个对象,但是他没有对象,加了一个静态就可以使用了
static A createobj(int a = 0) //调用这个函数就可以使用了,获得父类的构造函数,如果是成员函数
//静态的成员函数,没有this指针,只能访问静态的成员变量,和成员函数,不能访问_a 这些成员变量
{

return A(a);
}

protected:
int _a;
};
class B:public A
{

};
//子类继承了父类,但是构造函数被变成私有的了
int main()
{
//因为我们把A 类型的构造函数变成一个私有的构造函数,所以他没有办法被使用,但是我们写了一个静态的成员函数就可以获得这个东西
A aa=A::createobj(10);//就可以这样把对象创建出来了

return 0;
}

c++ 11

  1. 使用final 这个类就无法被继承
    
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class A final//不希望A 被继承

{
protected:
int _a;
};

class B:public A
{

};

int main()
{
return 0;
}
  1. 使用final这个函数就无法被重写,放在父类中
    
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

class C
{
public:
virtual void f() final// 我不想他被重写
{
cout<<"hello "<<endl;
}
};
class D :public C
{
public:
virtual void f()//重写
{
cout<<"world"<<endl;
}
};

int main()
{
return 0;
}

override

同样也是放在类的后面
放在子类重写的虚函数的后面,检查是否完成重写,如果没有完成重写就会报错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class car 
{
public:
virtual void drive()
{

}
};


class benz: public car
{
public:
virtual void drive() override
{

}
};

int main()
{
return 0;
}

在这里插入图片描述
没加之后的报错

重载,覆盖,隐藏的对比

重载:

  1. 两个函数在同一个作用域
  2. 函数名相同,参数不同,返回值没有要求

覆盖:
3. 两个函数分别在基类和派生类中的作用域中
4. 函数名/参数/返回值,都相同(协变除外)
5. 两个函数必须是虚函数

重定义(隐藏)
6. 一个在基类一个在派生类
7. 函数名相同
8. 不构成重写就是隐藏

抽象类

在虚函数的后面加上=0,这个函数就叫做纯虚函数,
包含纯虚函数的类就叫做抽象类,不能实例化出对象,
派生类继承以后,也不能实例化出对象,只有子类重写这个虚函数,才能实例化出对象

底层剖析

在这里插入图片描述
我们会发现子类和父类是不一样的虚表,下面继承的是重写的虚函数(覆盖)
父类对象是父类对象的虚函数,子类对象是子类对象的虚函数
重写是语法层面,覆盖是原理上
覆盖成我重写的虚函数,

多态为什么必须是指针和引用呢?
指针和引用,虚表就是各自的,而用对象,那么虚表都是父类的,无法调用子类的,切片没有办法把虚表指针拷过去

多态的原理:
基类的指针/引用,指向谁就去谁的虚函数表中找到对应的虚函数进行调用,在父类里面找到父类的虚函数,在子类里面找到子类的虚函数,

同类型的对象,他的虚表指针是一样的,指向同一张虚表,都是指向它的
在这里插入图片描述
普通函数和虚函数都存储在代码段,只不过虚函数要把地址存一份到虚表,方便实现多态,

多态就算是私有的也可以调用,因为它是从虚表里面去找的,所以没有影响,有了虚函数这一个,私有也都是可以调用的,

总结:

  1. 派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,虚表指针也就是存在部分的另一部分是自己的成员
  2. 基类b对象,和派生类d对象虚表是不一样的,这里我们发现func完成了重写,覆盖是指虚表中虚函数的覆盖,重写是语法上的 叫法,覆盖是原理层的叫法
  3. 子类继承下来没有重写的一部分,地址还是和父类的虚函数一样
  4. 子类也写了虚函数,这个虚函数也会在需表里面,只不过我们不好看

虚函数表在代码端(OS)/常量区(c语言)里面
虚表里面存的地址不是函数真正的地址,而是要跳几次才可以跳到目的函数

多继承中的虚表

打印虚表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
class Base
{
public:
virtual void func1() { cout << "base1::func1" << endl; }
virtual void func2() { cout << "base1::func2" << endl; }

private:
int a;
};


class driver : public Base
{
public:
virtual void func1()
{
cout << "driver func1" << endl;
}
virtual void func3()
{
cout << "deriver func3" << endl;
}
void func4()
{
cout << "derive func4" << endl;
}

private:
int b;
};


typedef void(*vfptr)() ;//函数指针的typedef方式不一样,把名字定义在中间,重命名为vfptr
//对函数指针进行重命名


void printvftable(vfptr table[])//函数指针数组
{
for(int i=0;table[i]!=nullptr;i++)//在windows下虚表最后一个元素是空
{
printf("vft[%d] :%p\n",i,table[i]);
//我们有了一个函数地址,那么就可以调用它
vfptr f=table[i];//f指向了函数指针的地址
f();//调用了这个函数
//我们发现这个是可以调用的
//就算是私有的我们也可以调用到,因为我们这里不是通过直接调用,而是通过取出来了虚函数的地址,
//这里不是通过对象调的
//有了虚表就会有一些安全隐患

}
cout<<endl;
}
int main()
{
person mike;
student jason;
func(mike); //到mike里面找到虚函数指针,在虚函数指针里面找到对应的虚函数,然后调用它
func(jason); //如果是子类,就会发生切割(切片),
person &r1 = jason;
person r2 = jason;
student s1;
student s2;

derive d; //多继承之后,就有两个虚表,
//子类里面有两张虚表,它自己的虚函数只放一个虚表,放在第一个继承的虚表里面
// base1和base2里面都有func1,但是发现他们的地址不一样,只是jump指令,最后会jump到一个地方去了

Base b;//虚表的指针在初始化列表(构造函数)里面处理,内置类型不处理,自定义类型调用自己的构造函数处理
//vfptr(virtual function table pointer )

printvftable((vfptr*)(*(void**)&b));//取虚表数组的首元素地址,在这个地方的头4个字节, 先转化成int* 再解引用就变成int了可以获得头4个字节了,但是类型不匹配, 所以还得再强转一下
//取一个对象里面的虚表(虚函数指针数组)
//这样就弄出来了虚表里面的两个函数
//不能直接转化的,不是所有类型都可以强制类型转化的(不相关的类型是不可以直接转化的,相关类型之间才可以直接转),要先取出来对应的地址
driver s;
printvftable((vfptr*)(*(void**)&s));//取虚表数组的首元素地址,在这个地方的头4个字节, 先转化成int* 再解引用就变成int了可以获得头4个字节了,但是类型不匹配, 所以还得再强转一下

//重写的func1,拷贝下来的func2,还有自己的func3

return 0;
}

在这里插入图片描述
打印成功
在这里插入图片描述


c++多态
http://example.com/2022/05/18/c++多态/
作者
Zevin
发布于
2022年5月18日
许可协议