❀多态
Nevermore Lang
# 1. 多态的实现
静态的多态:编译时实现。如
template<class T>
定义的模板函数传入不同的变量,编译时处理成不同的函数,然后根据传入变量的类型进行调用,这是由函数重载实现的。静态的多态有:函数重载、重定义、运算符重载。动态的多态:运行时实现。假设父类有许多派生类,那么对于父类和派生类中的某个function可以有多种实现方式。在外部调用时,可以设计一个函数,当传递入不同的对象可以调用不同类中的function。这种通过父类的指针或引用保存子类空间的地址,在运行时传递不同的对象时调用不同的函数的方式称为多态。实现动态的多态通过虚函数。虚成员函数有时称为方法。实现动态的多态需具有三个条件:
- 有对父类继承的子类
- 子类重写父类的虚函数
- 通过父类指针(或引用)指向子类空间调用虚函数。
# 1.1 虚函数
在父类中某非静态成员函数前加
virtual
关键字修饰的成员函数称为虚函数。子类中该函数的virtual
可加可不加,因为继承保留了父类虚函数的属性(父类的访问限定符也会被继承,虚函数表是可以公开访问的,即若父类中函数是公有的、子类是私有,但是仍可被外部访问——通过虚表里的函数指针)。被虚函数修饰的函数会产生虚函数指针(在构造函数初始化列表初始化),该指针指向虚函数表,该表中记录的是当前类的虚函数的入口地址。子类完成函数重写(覆盖)需满足三个条件:子类中的虚函数与父类虚函数(函数名、参数、返回值)相同。若任意一个条件不满足就构成隐藏。对于返回值不同的,有一个特殊情况也构成重写——协变(返回值是父子关系的指针或引用)。
class Base
{
public:
virtual void greeting()
{
std::cout << "hello, I am Base!" << std::endl;
}
};
class DerivedA : public Base
{
public:
virtual void greeting() //子类重写父类的虚函数:函数名、参数、返回值相同
{
std::cout << "hello, I am DerivedA!" << std::endl;
}
};
class DerivedB : public Base
{
public:
virtual void greeting(int a) //不满足构成多态的条件,为隐藏
{
std::cout << "hello, I am DerivedB!" << std::endl;
}
};
void Func(Base& b) //若不构成多态,最终的调用结果和形参b的类型有关;考虑参数传参(拷贝构造)
{
b.greeting();
}
//----------- 协变 - 返回值是父子关系的指针或引用-----------
class A {};
class B :public A {};
class BaseTest {
public:
virtual A* test()
{
std::cout << "virtual A* test()" << std::endl;
return nullptr;
}
};
class DerivedTest : public BaseTest {
public:
virtual B* test() //返回值与父类不同,但是构成重写
{
std::cout << "virtual B* test()" << std::endl;
return nullptr;
}
};
//----------------------------------------------------------
int main()
{
Base* pB = new Base;
Base* pDA = new DerivedA;
pB->greeting();
pDA->greeting();
DerivedB pDB;
Func(pDB);
BaseTest* pBT = new DerivedTest;
pBT->test();
return 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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
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
运行结果:
hello, I am Base!
hello, I am DerivedA!
hello, I am Base!
virtual B* test()
1
2
3
4
2
3
4
# 1.2 纯虚函数
- 虚函数后加
= 0
构成纯虚函数。含有纯虚函数的类称为抽象类,抽象类不能单独实例化对象。抽象类必须继承且被子类重写,否则子类也是抽象类。纯虚函数使用前提:①不会单独调用父类;②父类一定会派生出子类,且子类一定会重写虚函数(此时父类的虚函数是没有意义的)。
class A
{
public:
virtual void f() = 0; //纯虚函数只声明不实现——实现没有价值,虚函数放在虚函数表中,而不是静态存储区
};
class B: public A
{
public: //派生类继承纯虚函数也不能实例化对象,只有重写纯虚函数才可
virtual void f()
{
std::cout << "hello world" << std::endl;
}
};
int main()
{
A* p = new B;
p->f();
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 1.3 虚析构
- 对于抽象类的析构需要使用虚析构函数(析构函数前加
virtual
),否则根据父类指针维护的子类对象在释放时,只会析构父类的空间而不会调用子类的析构。使用虚析构可以通过父类指针释放子类的所有空间。(先析构子类,再析构父类) - 若父类的析构函数是虚函数,则子类的析构构成重写——虽然析构的名字不同,但是编译器会将析构函数类名特殊处理成destructor这个统一的单词。
class Base {
public:
virtual ~Base()
{
std::cout << "~Base()" << std::endl;
}
};
class DerivedA : public Base {
public:
virtual ~DerivedA() // 重写父类的析构函数
{
std::cout << "~DerivedA()" << std::endl;
}
};
int main()
{
//DerivedA s; //无论是否是虚函数,都能正确调用其析构函数
Base* p1 = new DerivedA; // operator new + 构造
delete p1; // 构造 + operator delete 若不写虚析构,无法调用子类析构函数。
return 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 结果
~DerivedA()
~Base()
1
2
3
2
3
# 1.4 纯虚析构
- 纯虚析构:必须在类外实现。
#include <iostream>
#include <string>
class A
{
public:
virtual ~A() = 0;
};
A::~A() //必须在类外实现
{
std::cout << "virtual ~A() = 0" << std::endl;
}
class B :public A
{
~B()
{
std::cout << "~B()" << std::endl;
}
};
int main()
{
A* p = new B;
delete p;
return 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
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
# 2. 多态实现的原理
首先看一段代码:
#include <iostream>
#include <string>
class Base
{
public:
virtual void greeting()
{
std::cout << "hello, I am Base!" << std::endl;
}
virtual void hello()
{
std::cout << "hello world!" << std::endl;
}
};
class DerivedA : public Base
{
public:
virtual void greeting() //子类重写父类的虚函数
{
std::cout << "hello, I am DerivedA!" << std::endl;
}
};
int main()
{
Base b;
std::cout << sizeof(b) << std::endl;
Base *pb = new Base;
Base* pDa1 = new DerivedA;
Base* pDa2 = new DerivedA;
return 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
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
输出结果:
4 # 包含了一个虚函数表指针
1

可以看到:
- 父类和子类的虚函数表指针指向的是不同的虚函数表,虚函数表中指向的是各自类中的虚函数;若重写了,就指向重写的新函数地址。

- 这里可以解释为什么需要父类指针(或引用)指向子类空间调用虚函数才可以实现多态:指针和引用可以获取对象的虚表指针,而使用
Base b = DerivedA
调用拷贝构造,无法拷贝子类虚函数表指针(否则父类对象的虚函数表指针会出现指向子类虚表的情况)。 - 同一个类构造的不同对象共有一份虚函数表,虚函数指针表指向同一个地址。
- 没有重写的虚函数使用的是同一地址。
- 补充:普通函数和虚函数(非纯虚)存储位置一样,都在代码段。虚函数表存放在常量区。
- 对于多继承的子类对象,其中可能含有多份来自父类的虚表指针:
#include <iostream>
#include <string>
class Base1
{
public:
virtual void Fun1()
{
std::cout << "Base1::Fun1()!" << std::endl;
}
virtual void Fun2()
{
std::cout << "Base1::Fun2()!" << std::endl;
}
};
class Base2
{
public:
virtual void Fun1()
{
std::cout << "Base2::Fun1()!" << std::endl;
}
virtual void Fun2()
{
std::cout << "Base2::Fun2()!" << std::endl;
}
};
class DerivedA : public Base1, public Base2
{
public:
virtual void Fun1() //重写继承的函数
{
std::cout << "DerivedA::Fun1()!" << std::endl;
}
private:
virtual void Fun3() //私有的函数
{
std::cout << "DerivedA::Fun3()!" << std::endl;
}
void Fun4() // 普通函数不存在虚表里
{
std::cout << "DerivedA::Fun4()!" << std::endl;
}
};
typedef void (*vfptr)(); //定义一个虚函数指针类型
void PrintVFTable(vfptr* pf) //传入虚函数表指针,打印虚表函数
{
for (int i = 0; pf[i]; i++) //虚函数表以nullptr结尾
{
printf("table[%d] -> %p ", i,pf[i]);
pf[i]();
}
}
int main()
{
DerivedA da;
PrintVFTable((vfptr*)*((void**)&da));
std::cout << std::endl;
PrintVFTable((vfptr*)*((void**)( (char *)&da + sizeof(Base1) ) ) );
Base2* pda = &da;
std::cout << std::endl << "切片的结果一样,Base2* pda = &da->:" << std::endl;
PrintVFTable((vfptr*)*((void**)(pda)));
return 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
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
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
输出结果:
table[0] -> 00401262 DerivedA::Fun1()!
table[1] -> 0040129E Base1::Fun2()!
table[2] -> 0040114A DerivedA::Fun3()!
table[0] -> 004013CA DerivedA::Fun1()!
table[1] -> 00401087 Base2::Fun2()!
切片的结果一样,Base2* pda = &da->:
table[0] -> 004013CA DerivedA::Fun1()!
table[1] -> 00401087 Base2::Fun2()!
1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10

- 在vs编译器里,虚表里真实存放的是jmp指令的地址,通过jmp跳转到对应的函数地址。最终都指向同一个函数
# 不能被继承的类
- 方式一:将父类构造函数私有,子类就无法调用父类构造实例化对象。
- 方式二:C++11中,对象 + final