见到一篇好博客:https://blog.csdn.net/qq_52670477/article/details/121419019
《深度探索C++对象模型》:
"一个pointer或一个reference之所以支持多态,是因为它们并不引发内存任何“与类型有关的内存委托操作; 会受到改变的。只有它们所指向内存的大小和解释方式 而已"
1、指针和引用并不涉及内存中对象的类型转换,只改变内存的地址和大小
2、直接调用赋值=会发生转型,
虚函数
虚函数重写
1 |
|
输出: 1
2
3
4
5
6
7
8
9
10
11
12A
B
C
D
A
B
C
D
A
B
C
D
若把fun函数参数中的引用去掉 1
2
3
4void fun(A p)
{
p.foo();
}1
2
3
4A
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
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
4A 0
B 0
C 0
D 0
虚函数重写的例外:
协变
虚函数要求返回值类型相同、函数名相同以及参数列表完全相同,但是协变是个例外,子类重写基类虚函数时,与基类虚函数返回值类型可以不同。但是返回值类型也必须满足父子关系。
father和son的foo函数返回值类型对调就会报错。
1 |
|
析构函数的重写(基类与派生类析构函数的名字不同)
如果基类的析构函数为虚函数,此时子类的析构函数只要定义,无论是否添加virtual关键字,都与基类的析构函数构成重写。
虽然基类与派生类析构函数名字不同,但是编译器对析构函数的名字进行了特殊的处理,基类和派生类的析构函数构成隐藏。编译后析构函数的名称同一处理成destructor(),其目的是为了实现析构函数的多态。
这也就导致了一个问题,基类与派生类的析构函数名字相同,那指向派生类的基类指针调用的就是基类的析构函数:
1 |
|
输出:
1 | A |
1 |
|
输出:
1 | 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
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;
}
虚函数表指针指向虚表首部地址,虚表存放虚函数,虚表本质是一个存虚函数指针的指针数组。
虚表指针在构造函数阶段(初始化列表)填入到对象中,虚表则是在编译时就生成好了。
虚表里面放的是虚函数地址,虚函数和普通函数一样,编译完成以后,都是放在代码段中。
一个类中所有的虚函数,都会放在虚表中。
子类会将父类的虚表拷贝一份,然后用重写的虚函数地址覆盖掉原来虚表中的函数地址,因此虚函数的重写,也叫虚函数的覆盖。