❀多态
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
 
