0%

C++多态

见到一篇好博客:https://blog.csdn.net/qq_52670477/article/details/121419019

《深度探索C++对象模型》:

"一个pointer或一个reference之所以支持多态,是因为它们并不引发内存任何“与类型有关的内存委托操作; 会受到改变的。只有它们所指向内存的大小和解释方式 而已"

1、指针和引用并不涉及内存中对象的类型转换,只改变内存的地址和大小

2、直接调用赋值=会发生转型,

虚函数

虚函数重写

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
#include <bits/stdc++.h>

class A
{
public:
virtual void foo() { std::cout << "A" << std::endl; }
};

class B : public A
{
public:
virtual void foo() { std::cout << "B" << std::endl; }
};

class C : public B
{
public:
virtual void foo() { std::cout << "C" << std::endl; }
};

class D : public A
{
public:
virtual void foo() { std::cout << "D" << std::endl; }
};

void fun(A &p)
{
p.foo();
}

int main()
{
std::ios::sync_with_stdio(false);
std::cin.tie(0);
A a;
B b;
C c;
D d;

A *pa = &a;
B *pb = &b;
C *pc = &c;
D *pd = &d;

pa->foo();
pb->foo();
pc->foo();
pd->foo();

pa->foo();
pa = pb;
pa->foo();
pa = pc;
pa->foo();
pa = pd;
pa->foo();

fun(a);
fun(b);
fun(c);
fun(d);

return 0;
}

输出:

1
2
3
4
5
6
7
8
9
10
11
12
A
B
C
D
A
B
C
D
A
B
C
D

若把fun函数参数中的引用去掉

1
2
3
4
void fun(A p)
{
p.foo();
}
调用fun(a),fun(b),fun(c),fun(d)的输出会变成
1
2
3
4
A
A
A
A

虚函数重写只是重写函数的实现,继承的是父类的接口定义(声明),不会重写函数的缺省参数。

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
#include <bits/stdc++.h>

class A
{
public:
virtual void foo(int val = 0) { std::cout << "A" << " " << val << std::endl; }
};

class B : public A
{
public:
virtual void foo(int val = 1) { std::cout << "B" << " " << val << std::endl; }
};

class C : public B
{
public:
virtual void foo(int val = 2) { std::cout << "C" << " " << val << std::endl; }
};

class D : public A
{
public:
virtual void foo(int val = 3) { std::cout << "D" << " " << val << std::endl; }
};

void fun(A &p)
{
p.foo();
}

int main()
{
std::ios::sync_with_stdio(false);
std::cin.tie(0);
A a;
B b;
C c;
D d;

fun(a);
fun(b);
fun(c);
fun(d);

return 0;
}

输出:

1
2
3
4
A 0
B 0
C 0
D 0

虚函数重写的例外:

协变

虚函数要求返回值类型相同、函数名相同以及参数列表完全相同,但是协变是个例外,子类重写基类虚函数时,与基类虚函数返回值类型可以不同。但是返回值类型也必须满足父子关系。

father和son的foo函数返回值类型对调就会报错。

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
#include <bits/stdc++.h>

class A
{
};

class B : public A
{
};

class father
{
public:
virtual A *foo()
{
std::cout << "father" << std::endl;
return new A;
}
};

class son:public father
{
public:
virtual B *foo()
{
std::cout << "son" << std::endl;
return new B;
}
};

void fun(father &p)
{
p.foo();
}

int main()
{
std::ios::sync_with_stdio(false);
std::cin.tie(0);
father p;
son q;

fun(p);
fun(q);

return 0;
}
析构函数的重写(基类与派生类析构函数的名字不同)

如果基类的析构函数为虚函数,此时子类的析构函数只要定义,无论是否添加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
#include <bits/stdc++.h>

class A
{
public:
~A() { std::cout << "A" << std::endl; }
};

class B : public A
{
public:
~B() { std::cout << "B" << std::endl; }
};

int main()
{
std::ios::sync_with_stdio(false);
std::cin.tie(0);

A *pa = new A;
A *pb = new B;

delete pa;
delete pb;

return 0;
}

输出:

1
2
A
A
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
#include <bits/stdc++.h>

class A
{
public:
virtual ~A() { std::cout << "A" << std::endl; }
};

class B : public A
{
public:
~B() override { std::cout << "B" << std::endl; }
};

int main()
{
std::ios::sync_with_stdio(false);
std::cin.tie(0);

A *pa = new A;
A *pb = new B;

delete pa;
delete pb;

return 0;
}

输出:

1
2
3
A
B
A

所以,为了不出现内存泄漏的问题,基类的析构函数要加virtual,由于派生类的虚函数不加virtual关键字也可以构成重写,这样在delete就能够实现多态的正确调用析构函数。

C++11 override 和 final

override:检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错

final:修饰虚函数,表示该虚函数不能再被重写

抽象类

在虚函数的后面写上=0,则这个函数为纯虚函数,包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。

派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。

实现继承: 普通函数的继承是一种实现继承,派生类继承了基类函数的实现,可以使用该函数。

接口继承: 虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态。

建议: 所以如果不实现多态,就不要把函数定义成虚函数。

虚函数表

虚函数表指针

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
#include <iostream>

class A
{
public:
long long n;

virtual void foo() const {};

virtual void fun() {};

virtual void fun1() {};

virtual void fun2() {};

virtual void fun3() {};
};

class B
{
public:
int n;

B()
{

};

void print() {};
};

inline void fun(const A& a)
{
a.foo();
}

int main()
{
std::ios::sync_with_stdio(false);
std::cin.tie(0);
A a;
B b;
std::cout << sizeof(a) << " " << sizeof(b) << std::endl;
return 0;
}

虚函数表指针指向虚表首部地址,虚表存放虚函数,虚表本质是一个存虚函数指针的指针数组。

虚表指针在构造函数阶段(初始化列表)填入到对象中,虚表则是在编译时就生成好了。

虚表里面放的是虚函数地址,虚函数和普通函数一样,编译完成以后,都是放在代码段中。

一个类中所有的虚函数,都会放在虚表中。

子类会将父类的虚表拷贝一份,然后用重写的虚函数地址覆盖掉原来虚表中的函数地址,因此虚函数的重写,也叫虚函数的覆盖。