对象概述

这是《Inside the C++ Object Model (深度探索C++对象模型) 》的第一章内容,本书正如译者推荐的一样——它是一本卓越的书。大多数的书籍停滞于“编程语言中支持的面向对象程序设计”,很少探究“对象模型的底层实现机制”。在对照原版阅读本书的过程中发现,译者在少量地方的翻译中,使用了带有自我口语化的翻译风格(或者说本书原文有一些随笔的风格),所以阅读过程中不必太细究翻译出的文字,读不通就去查询原版吧;但是译者增加了很多插图和说明帮助理解,这一点非常棒。

本书的组织在开始有必要提一下,

  • 1 关于对象的概述,可以在浅层面看到C++对象模型的基本实现;
  • 2 讲述constructor如何工作;
  • 3 C++对象模型中关于data members的处理;
  • 4 C++对象模型中关于function members(包括virtual function)的处理;
  • 5 C++对象模型中关于construction, destruction, copy的处理;
  • 6 程序执行过程中对象模型的行为;
  • 7 On the Cusp(顶端) of the Object Model. 
本篇博文为第一章,按照惯例,第一章都是对后面所有内容的一个简单综述,所以看的过程中难免磕磕绊绊,当读完这本书时,希望面对曾经的很多疑惑,引用书中的一句话来表述感受:“Ideally, you'll look back, snap your fingers, and say, "Oh, yeah, sure", wondering why you were ever puzzled.(到时候你会回头看,一边玩弄你的手指,一边说:“喔,是的,当然”,同时奇怪当初为什么会迷惘)”。


1. Procedrual vs Object-Oriented Programming
学过C和C++的同学对这两个概念的区别肯定有困惑,我们就从这个区别入手来阐述第一章的内容[1][2]。
“程序化程序设计”与“面向对象程序设计”区别如下图:

  • procedural programming:主要的思想是函数与其操作的数据没有关联性,也就是说,只要给定了correct number和type of arguments作为input,function就会工作并返回相应的output;modules之间的通信主要靠存储在shared data structure的read和write来进行的;
  • object-oriented programming:data和function被封装在object中,data的访问和改动只能通过object内相应的函数来进行;modules之间的通信主要靠发送消息来进行;好的OO设计是没有shared data和global data的。
相对于C++而言,C的吸引力在于它的精简,加上class的类封装后,C++的内存布局成本增加了多少呢?封装本身并没有多少成本,C++在布局以及时间上的成本主要是由virtual机制引起的(virtual function & virtual base class),这在后面会讨论。
下面的讨论分三个部分,1 C++对象模型,2 struct和class的差别以及3 不同程序设计范式的差异。


2. C++对象模型(the C++ object model)
C++ class中包含data members(数据成员)和member functions(成员函数),其中data members分为static和non-static两种,member functions分为static, non-static, virtual三种。[Static Data Members (C++)] [Static Member Function (C++)] [static function不能使用non-static data](class内部和外部的定义)
下面几种object model就是一个object的表现形式,当然我们主要关注 C++ Object Model:

  • simple object model是把一个object中的每个member(data members与member functions)的指针放入一个slot,编译复杂度低,而空间和运行代价高;
  • table-driven object model把data和function用两个table分开存放,并没有真正使用过;
  • C++ object model借鉴table的结构,但table只用于存放virtual functions,即virtual table;vtbl的第一个是type_info,用于支持runtime type identification(RTTI);主要优点在于它的空间和存储效率,缺点在于class object中的non-static data member需要修改时(如float _x是non-static变量),代码需要重新编译,而table-driven object model由于只存放指针,所以不需要。
上面的前两种模型都是为C++ object model的出现做铺垫的,那么,当模型加上继承关系之后,会发生怎样的变化?比如继承关系如下:

假如 class istream : virtual public ios {}; class ostream : virtual public ios {};
如果继承关系被制定为virtual(i.e. shared),那么不管派生多少次,永远只会存在一个subobject(即iostream中只有virtual ios base class的一个instance),后续会说明更多关于继承与虚基类的东西,这里就不细讨论了。

我们上面说了那么多关于object model的,但是object model对程序有什么样的影响呢?
Support for the object model result in both modification of the existing program code and the insertion of additional code,即不同的对象模型会导致C++原始程序在编译前,转化出的代码产生了改动,详见书中P13.


3. An keyword distinction(struct与class的差别 )
如果不是为了努力维护与C之间的兼容性,C++远可以比现在更简单些。那么,“在C++中什么时候应该以struct取代class?”从前Lippman的回答是:“Never!”后来的回答是:“Whenever it makes one feel better”。
我们完全可以用相同的方式理解它们,所以C中的struct和C++中的class之间重要的一点是:“关键词本身并不提供这种差异”;如果说struct关键字实现了C的ADT观念,那么class实现了C++的ADT观念,这两者之间只是风格上的不一致,尽管struct不能有private data,也没有member functions。
有时候,C程序员使用的一些小技巧,在C++中并不适用。比如如下示例,
C:
struct mumble {
/* stuff */
char pc[ 1 ];
};
// grab a string from file or standard input
// allocate memory both for struct & string
struct mumble *pmumb1 = ( struct mumble* )
malloc(sizeof(struct mumble)+strlen(string)+1);
strcpy( &mumble.pc, string );

C++:
class stumble {
public:
// operations ...
protected:
// protected stuff
private:
/* private stuff */
char pc[ 1 ];
};
如上,C程序员可能将可变大小的数组放在struct尾端,而class中变量和函数的布局顺序却是不确定的,这一点我们在后面的章节会说明,所以,C中的程序设计技巧并不适用。
C struct在C++中的一个合理用途是 Composition(组合):
当要传递一个“复杂class object的部分或全部”到某个C函数中时,struct可以将数据封装起来,并保证拥有与C兼容的空间布局。


4. An object distinction(不同programming paradigms在多态下的表现)
首先,C++支持的三种programming paradigms(程序设计范式,可以理解为一种思想的组织方式,程序设计的架构;The basic structuring of thought underlying the programming activity)[3]:
  • Procedural model(程序化模型)
  • Abstract data type model (ADT,抽象数据类型模型),也叫Object-based model(OB model)
  • Object-oriented model(OO model)

(1)Object-based(OB,基于对象) vs Object-oriented(OO,面向对象)
前面已经说过procedural与object-oriented的区别,现在看OB与OO的区别;
Stack Overflow上有个人的回答很精彩:“The language which itself contains objects is called Object-Based language, and the language with follows object oriented concepts is known as Object-Oriented language.”[4]
那么,object oriented concepts,即面向对象的特点,有哪些呢?
Encapsulation, Inherience, Polymorphism(i.e. 封装,继承,多态),OB只有encapsulation而没有其它两个特点
  • encapsulation:objects提供一系列API,通过API操纵object;
  • inherience:object可以被视为其它一些objects的特殊实现;
  • polymorphism:代码使用object的时候不用关心它具体是什么type;
在OO paradigm中,原则上object的类型到达运行点时才确定,而ADT paradigm中在编译时期便已经确定下来。

(2)C++中的多态
C++中的多态通过pointer和references(i.e. * and &)来实现,这里关于reference的用法做一下说明:
  • 1. take the address of a value;
  • 2. declare a reference to a type(reference is an alias for an object. [5][6] 指向同一地址)
C++通过以下方法支持多态:
  • 1. 经由一组隐式的转化操作: shape *ps = new circle();  //shape是base class,circle是derived class;
  • 2. 经由virtual function机制: ps->rotate();
  • 3. 经由dynamic_cast和typeid运算符: circle *pc = dynamic_cast< circle * > (ps);
其中,dynamic_cast是动态转换,它提供了错误检查,所以提倡在多态中使用:
T1 obj;

T2* pObj = dynamic_cast<T2*>(&obj);//转换为T2指针,失败返回NULL

T2& refObj = dynamic_cast<T2&>(obj);//转换为T2引用,失败抛出bad_cast异常
关于多态之间的up cast(向上转型),down cast(向下转型),cross cast(横向转型),请参考[7]。

(3)class object的内存布局
一般而言需要以下三部分:
  • 1. nonstatic data members的总和大小;
  • 2. alignment,对齐的填补空间;
  • 3. 支持virtual机制产生的负担;
无论是Zooanimal *px, int *pi, 还是Array<String> *pta,它们在内存需求的观点来说都是相同的,因为只是一个机器地址(一个word的大小,比如32 bit机器上为4 byte),只是在编译过程中,“指针类型”会指导编译器解释指针指向地址处的内容及大小。
所以说,
  • 1)上面谈论的cast其实只是一种编译器指令,大部分情况下它并不改变指针指向的地址,只是影响了编译器解析其地址内容的方式而已;
  • 2)void *只能持有一个地址而无法操作object的原因也正是如此。
那么,加上多态之后,内存上会产生什么变化呢?我们看下面代码:
class Bear : public ZooAnimal {
	public:
		Bear();
		~Bear();
		// ...
		void rotate();
		virtual void dance();
		// ...
	protected:
		enum Dances { ... };
		Dances dances_known;
		int cell_block;
};

Bear b( "Yogi" );
Bear *pb = &b;
Bear &rb = *pb;
它可能的内存布局如下:


如上图,Bear继承了ZooAnimal,pb是Bear *类型,rb是Bear&类型,都指向1000,__vptr__ZooAnimal指向虚地址表;我们接着有如下代码:
Bear b;
ZooAnimal *pz = &b;
Bear *pb = &b;
那么此时,虽然pz和pb都指向1000,但涵盖的地址范围不一样,pz只涵盖ZooAnimal subobject的部分。
关于pz类型,在编译时期决定了其
  • 1)固定的可用接口:pz只能调用ZooAnimal的public接口(public继承?);
  • 2)该接口的access level;
关于以上两点及细节问题,我们会在后面依次详细讨论,这里便不赘述。

(4)inline function和macro
关于内联函数和宏的联系与区别,这里只做补充论述:
宏是在预处理的时候进行简单替换,而内联函数是通过编译器的控制来实现的。内联函数是真正的函数,但调用时不需要进行压栈和出栈的操作,而是像宏一样展开,所以调用时的开销比较小。详见[宏与内联函数

以上便为第一章《Object Lessons》的内容。

References:

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值