多态的实现
需要先了解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);
运行结果:
从结果可以看到,_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(); }
|
输出结果是:
解析:
- 编译器为空类安插1字节的char,以使该类对象在内存得以配置一个地址。
- b1虚继承于b,编译器为其安插一个4字节的虚基类表指针(32为机器),此时b1已不为空,编译器不再为其安插1字节的char(优化)。
- b2同理。
- d含有来自b1与b2两个父类的两个虚基类表指针。大小为8字节。
参考:
https://www.cnblogs.com/qg-whz/p/4909359.html#!comments