cpp多态如何实现

多态的实现

需要先了解c++对象模型机制

多态(Polymorphisn)在C++中是通过虚函数实现的。通过前面的模型【参见“有重写的单继承”】知道,如果类中有虚函数,编译器就会自动生成一个虚函数表,对象中包含一个指向虚函数表的指针。能够实现多态的关键在于:虚函数是允许被派生类重写的,在虚函数表中,派生类函数对覆盖(override)基类函数。除此之外,还必须通过指针或引用调用方法才行,将派生类对象赋给基类对象。

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
#include <iostream>
using namespace std;

class BaseClass
{
public:
BaseClass()
{
cout << "base::base()" << endl;
}
virtual ~BaseClass()
{
cout << "base::~base()" << endl;
}
void print()
{
cout << "base::print()" << endl;
}
virtual void print_virtual()
{
cout << "base::print_virtual()" << endl;
}
};

class DerivedClass : public BaseClass
{
public:
DerivedClass() : BaseClass()
{
cout << "Derived::Derived()" << endl;
}
virtual ~DerivedClass()
{
cout << "Derived::~Derived()" << endl;
}
void print()
{
cout << "Derived::print()" << endl;
}
virtual void print_virtual()
{
cout << "Derived::print_virtual()" << endl;
}
};

int main()
{
BaseClass b;
DerivedClass d;

b = d;
b.print();
b.print_virtual();

BaseClass* p;

p = &d;
p->print();
p->print_virtual();

return 0;
}

上面2个类,基类Base、派生类Derived中都包含下面2个方法:

void print() const;

virtual void print_virtual() const;

这个2个方法的区别就在于一个是普通成员函数,一个是虚函数

根据模型推测只有p->print_virtual()才实现了动态,其他3调用都是调用基类的方法。原因如下:

b.print();b.print_virtual();不能实现多态是因为通过基类对象调用,而非指针或引用所以不能实现多态。
p->print();不能实现多态是因为,print函数没有声明为虚函数(virtual),派生类中也定义了print函数只是隐藏了基类的print函数。

结果输出:

1
2
3
4
5
6
7
8
9
10
base::base()
base::base()
Derived::Derived()
base::print()
base::print_virtual()
base::print()
Derived::print_virtual()
Derived::~Derived()
base::~base()
base::~base()

为什么析构函数设为虚函数是必要的

析构函数应当都是虚函数,除非明确该类不做基类(不被其他类继承)。基类的析构函数声明为虚函数,这样做是为了确保释放派生对象时,按照正确的顺序调用析构函数。

从前面介绍的C++对象模型可以知道,如果析构函数不定义为虚函数,那么派生类就不会重写基类的析构函数,在有多态行为的时候,派生类的析构函数不会被调用到(有内存泄漏的风险!)。

C++封装带来的布局成本是多大

在C语言中,“数据”和“处理数据的操作(函数)”是分开来声明的,也就是说,语言本身并没有支持“数据和函数”之间的关联性。
在C++中,我们通过类来将属性与操作绑定在一起,称为ADT,抽象数据结构。

C语言中使用struct(结构体)来封装数据,使用函数来处理数据。举个例子,如果我们定义了一个struct Point3如下:

1
2
3
4
5
6
7
typedef struct Point3
{
float x;
float y;
float z;
} Point3;

为了打印这个Point3d,我们可以定义一个函数:

1
2
3
4
void Point3d_print(const Point3d *pd)
{
printf("(%f,%f,%f)",pd->x,pd->y,pd_z);
}

而在C++中,我们更倾向于定义一个Point3d类,以ADT来实现上面的操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Point3d
{
public:
point3d (float x = 0.0,float y = 0.0,float z = 0.0)
: _x(x), _y(y), _z(z){}

float x() const {return _x;}
float y() const {return _y;}
float z() const {return _z;}

private:
float _x;
float _y;
float _z;
};

inline ostream&
operator<<(ostream &os, const Point3d &pt)
{
os<<"("<<pr.x()<<","
<<pt.y()<<","<<pt.z()<<")";
}

看到这段代码,很多人第一个疑问可能是:加上了封装,布局成本增加了多少?答案是class Point3d并没有增加成本。学过了C++对象模型,我们知道,Point3d类对象的内存中,只有三个数据成员。

上面的类声明中,三个数据成员直接内含在每一个Point3d对象中,而成员函数虽然在类中声明,却不出现在类对象(object)之中,这些函数(non-inline)属于类而不属于类对象,只会为类产生唯一的函数实例。

所以,Point3d的封装并没有带来任何空间或执行期的效率影响。而在下面这种情况下,C++的封装额外成本才会显示出来:

虚函数机制(virtual function) , 用以支持执行期绑定,实现多态。
虚基类 (virtual base class) ,虚继承关系产生虚基类,用于在多重继承下保证基类在子类中拥有唯一实例。

不仅如此,Point3d类数据成员的内存布局与c语言的结构体Point3d成员内存布局是相同的。C++中处在同一个访问标识符(指public、private、protected)下的声明的数据成员,在内存中必定保证以其声明顺序出现。而处于不同访问标识符声明下的成员则无此规定。对于Point3类来说,它的三个数据成员都处于private下,在内存中一起声明顺序出现。我们可以做下实验:

1
2
3
4
5
6
7
8
9
10
11
12
void TestPoint3Member(const Point3d& p)
{

cout << "推测_x的地址是:" << (float *) (&p) << endl;
cout << "推测_y的地址是:" << (float *) (&p) + 1 << endl;
cout << "推测_z的地址是:" << (float *) (&p) + 2 << endl;

cout << "根据推测出的地址输出_x的值:" << *((float *)(&p)) << endl;
cout << "根据推测出的地址输出_y的值:" << *((float *)(&p)+1) << endl;
cout << "根据推测出的地址输出_z的值:" << *((float *)(&p)+2) << endl;

}
//测试代码
Point3d a(1,2,3);
TestPoint3Member(a);

运行结果:

图 1

从结果可以看到,_x,_y,_z三个数据成员在内存中紧挨着。

总结一下:

不考虑虚函数与虚继承,当数据都在同一个访问标识符下,C++的类与C语言的结构体在对象大小和内存布局上是一致的,C++的封装并没有带来空间时间上的影响。

下面这个空类构成的继承层次中,每个类的大小是多少?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class B{};
class B1 :public virtual B{};
class B2 :public virtual B{};
class D : public B1, public B2{};

int main()
{
B b;
B1 b1;
B2 b2;
D d;
cout << "sizeof(b)=" << sizeof(b)<<endl;
cout << "sizeof(b1)=" << sizeof(b1) << endl;
cout << "sizeof(b2)=" << sizeof(b2) << endl;
cout << "sizeof(d)=" << sizeof(d) << endl;
getchar();
}

输出结果是:
图 2

解析:

  • 编译器为空类安插1字节的char,以使该类对象在内存得以配置一个地址。
  • b1虚继承于b,编译器为其安插一个4字节的虚基类表指针(32为机器),此时b1已不为空,编译器不再为其安插1字节的char(优化)。
  • b2同理。
  • d含有来自b1与b2两个父类的两个虚基类表指针。大小为8字节。

参考:

https://www.cnblogs.com/qg-whz/p/4909359.html#!comments