C++设计范式
C++程序设计模型支持三种programming paradigms:
- procedursal model
过程式编程模型,想必各位已经很熟悉了。 - abstract data type model
该模型所谓的“抽象”是和一组表达式(public接口)一起提供,而其运算定义符隐而未明:1
2
3
4String girl = "Anna";
String daughter;
//String::operator=();
daughter = girl; - object-oriented model
在此模型中有一些彼此相关的类型,通过一个abstract base class(提供通用接口)被封装起来。
多范式混用带来的危害
纯粹以某一种paradigm编程有助于保证整体行为的稳定性与正确性,如果混合使用了多种paradigm,可能会造成意料之外的后果。
sliced
假设当前有Library_material与其派生类book,继承体系如下:
考虑如下代码:1
2
3
4
5
6
7
8//ADT paradigm
Library_material thing1;
Book book;
thing1 = book;//本意是重绑定
thing1.checkin();//本意是调用虚函数
//OO paradigm
Library_material thing2 = &book;
thing2.checkin();//book::checkin();
多态性是OO paradigm的重要特性,ADT paradigm不具备此特性,如果在使用时混用二者,极易造成sliced并且产生意外后果。
OO paradigm 与 ADT paradigm
在OO中,程序员需要处理一个个未知实体,它的类型虽然有所界定,却具备无穷可能。实体的类型受限于继承体系,但继承体系的深度与广度没有限制。原则上,被指定的Object的真实类型在每一个特定动作执行前无法确认,只有通过pointer或reference才能够完成这一系列操作。但在ADT中,程序员处理的是一个拥有固定且单一形态的实体,其所有特性在编译时期就已被完全定义:1
2
3
4
5//无法确定px rx的动态类型
Library_material *px = sth;
Libraay_material &rx = *px;
//静态类型已知
Library_material dx = *px;
多态的实现要求此object必须经由pointer或reference存取,但pointer与reference的操作并不总是构成多态(例如其指向对象不在继承体系中)。
多态
C++以下列方法支持多态:
- 一组隐式转换操作,例如把一个derived class指针转为指向base class的指针。
- virtual function机制
- 经由dynamic_cast与typeid运算符
多态的主要用途是经由一个共同的接口来影响类型的封装,该接口通常被定义在一个abstract base class中,通过多态性,我们得以保证当类型发生修改时,程序代码无需改变。
object的大小
究竟一个object需要占用多少内存?一般而言,object的内存占用由三部分组成:
- 所有non-static data member所占大小
- 为了支持virtual而产生的额外开销(vptr)
- 齐位所带来的额外占用
指针类型差异
1
2
3ZooAnimal *px;
int *pi;
Array<String> *pta;
这三种指针有什么不同吗?
以内存需求的角度而言,这三种指针并无不同,都需要足够的内存来放置一个机器地址。“指向不同类型的指针的差异”(ZooAnimal*、int*等等),既不在于指针表示法,也不在于内容(地址)不同,而在于其寻址得到的object类型不同。“指针类型”的主要功能是:教导编译器如何解释某个特定地址中的内存内容及其大小:
- 一个指向地址1000的int*,在地址空间上将涵盖1000~1003(32位及其上整数是4-bytes)
- 如果String基于传统实现,那么地址空间上将涵盖1000~1015(4+8+4)
那如果现存在一个地址为1000但类型为void*的指针呢?我们无法了解其指向的具体对象,也无法判断它具体如何覆盖地址空间。
所以说,cast只是一种编译器指令,它并不改变真正的地址,只是影响编译器如何去解读地址。
引入多态之后的指针
现有继承体系如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14class Bear::public ZooAnimal{
public:
Bear();
~Bear();
void rotate();//继承得到的virtual
virtual void dance();//特有
protected:
enum Dances {...};
Dances dances_known;
int cell_block;
};
Bear b("Yogi");
Bear *pb = &b;
Bear &rb = *pb;
b、pb、rb的内存需求何如?
pointer与refernce只需要一个字长的大小,Bear object 需要24 bytes(ZooAnimal的16 bytes + Bear自有的8 Bytes):
pointer to base 与 pointer to derived
1 | Bear b; |
一个Bear*与一个ZooAnimal*有何区别?
它们都指向了b的第一个byte,其差别在于pz只涵盖了b中的ZooAnimal subobject部分,pb则是涵盖了整个Bear object。除了Base部分之外,你无法调用任何Bear的专属member,唯一例外是virtual机制。当我们写下:1
pz->rotate();
pz的类型将在编译期检查以下两点:(显式接口)
- 该接口是否存在于base内
- 该接口的access level是否为public
在运行期,pz所指向的object决定了rotate的调用实体。类型信息的封装并不存在于pz中,而是存在于vbtl的首元素。
考虑如下情况:1
2
3Bear b;
ZooAnimal za =b;//sliced
za.rotate();//ZooAnimal::rotate()
为什么调用的并不是Bear的函数实体?此外,如果初始化函数复制了object,为什么vptr没有发生变化指向新的vbtl?
关于第二个问题,编译器给出的答案是:如果某个object含有一个或以上一个以上的vptrs,这些vptrs的内容不会在初始化或赋值时发生改变。
对于第一个问题,答案是:za并不也不可能是一个Bear,它只能是一个ZooAnimal。有这么一种似是而非的观念:OO程序设计并不支持对object的直接处理。例如在如下继承体系中:1
2
3
4
5ZooAnimal za;
ZooAnimal *pza;
Bear b;
Panda *pp = new Panda;
pza =&b;
其内存布局可能如下:
我们只需要认定,pointer与reference的类型决定了该段内存被如何解析。但如果你试图改变object za的大小,比如将整个Bear Object赋给它,那就直接引发了sliced,于是多态便不再呈现。
OO与OB
C++通过pointer与reference来实现多态,这种程序设计风格被称为面向对象。
C++也支持ADT程序风格,现如今它们被称为OB(object-based)。OB提供封装的非多态形式,也不支持类型扩充,其典型实例就是STL中的各类容器。OB比OO更具备空间紧凑性,且提供更快的速度,但其缺点在于缺乏设计弹性。
在OO与OB之间存在着取舍,在我们使用之前必须分析当前应用领域的实际需求。