《深度探索C++对象模型》

第一章 关于对象

1.1 C++对象模式

  • 两种class data member
    static、nonstatic

  • 三种class function member
    static、virtual、nonvirtual

  • C++对象模型
    1.nonstatic data放置在每一个class object内
    2.static data:放置在个别的class object之外;
    3.static function:放置在个别的class object之外;
    4.nonvirtual function:放置在个别的class object之外;
    5.virtual function:1.每个class(类)有一个virtual table(vtbl)存放指向各虚函数及type_info的指针;2.每个class object(对象)有一个指针(vptr)指向相应的vtbl

    class Point{
    	public:
    		Point(float xval);
    		virtual ~Point();
    		
    		float x() const;
    		static int PointCount();
    	protected:
    		virtual ostream& print(ostream& os) const;
    		float _x;
    		static int _point_count;
    };
    

    则Point的布局如图:
    在这里插入图片描述
    疑问:类是如何找到其static成员(data、function)的呢?

  • 带有继承时的对象模型
    对于非virtual继承base class的数据直接放置在derived class对象中,且通常在最前面
    对于virtual继承:类似于vptrvtbl那样 => 在class object(对象)中增加一个指针bptr指向base class table,而这个表中存放所有到父类的指针……
    在这里插入图片描述

1.2 关键词所带来的差异

  • 概述
    这一小节主要讲述struct和class的关联,但是讲得并不清晰,也没有多少重点……
  • access sections中数据的存放顺序
    1.所谓access section就是class中public、protected、private三种访问权限下的数据;
    2.处于同一access section下的数据,必定按声明顺序出现在内存布局中
    3.不同access section下的数据,顺序就不是那么确定了……

1.3 对象的差异

  • C++支持多态的方式
    1.经一组隐式的转换操作:Shape* ps=new Circle();
    2.经由virtual function机制:ps->rotate();
    3.经过dynamic_cast和typid运算符:if(circle* pc=dynamic_cast<circle*>(ps)) ....)

  • class object占用的内存大小
    主要包括以下部分:
    1. nonstatic data members的总和大小;
    2. 由于对齐(alignment)而浪费的空间;
    3. 为了支持virtual而产生的额外负担(比如vptr);
    :这里只是说class object的大小,没有算static成员、函数,以及nonstatic函数的空间,因为它们不属于对象!!!

  • 指针的类型
    指针的类型用于指导编译器如何解释某个特定地址中的内存内容及其大小
    对于void*指针,由于没有类型,编译器只知道地址,但是不知道大小,所以无法通过它操作对象,因而通常需要将其转换为其他类型指针;
    => 类型转换本质上就是编译器指令,告诉编译器对应地址处的内存大小和内容……

  • 普通继承下的对象布局
    在这里插入图片描述

  • 切割(sliced)

    Bear b;
    ZooAnimal za=b;		// 这会引起切割
    za.rotate();		// 调用的将会是ZooAnimal::rotate, 而不是Bear::rotate
    

    调用的将会是ZooAnimal::rotate, 而不是Bear::rotate =>
    因为多态并不能实际发挥在“直接存取objects”这件事情上(而应该通过virtual和指针), 上面的代码会造成切割,za获得的只是b对象中父类的那部分……

第二章 构造函数语义学

2.1 Default Constructor的构造操作(重点)

  • 概述
    什么是default constructor:默认构造函数,亦即无参构造函数(对比copy constructor)
    分类:trival 和 nontrival; nontrival则会被合成/扩张;trival不会自动合成/扩张
    什么时候自动合成default constructor当编译器需要它的时候,而不是用户程序需要它的时候;被合成出来的constructor只执行编译器所需行动,用户需要的初始化应该自己进行……
    nontrival default constructor => 四类情况:
    1.有默认构造函数的Member Class Object
    2.有默认构造函数的Base Class
    3.带有一个虚函数的Class
    4.带有一个虚基类的Class

  • 1.带有Default Constructor的Member Class Object
    A.若带有成员对象的类没有显式定义构造函数 => 编译器默认创建……

    class Foo{
    	public:
    		Foo();
    		Foo(int);
    	......
    };
    class Bar{
    	public:
    		Foo foo;
    		char* str;
    	......
    };
    
    // 客户代码..
    void foo_bar(){
    	Bar bar;	// Bar::foo必须在此初始化
    	if(str){....}
    }
    

    上述情况中,类Bar的成员对象(member class object)Foo有默认构造函数,且类Bar没有显式定义构造函数,此时编译器会自动为类Bar创建默认构造函数,并调用其成员对象的默认构造函数,可能情况如下:

    Bar::Bar(){
    	// c++ 伪码
    	foo.Foo::Foo();
    }
    

    注意:此时合成的Bar的构造函数只初始化了foo,并没有初始化str!!!
    B.若带有成员对象的类定义了构造函数 => 编译器自动扩展已有的……
    如果上面的Bar定义了构造函数:

    	class Bar{
    		public:
    			Foo foo;
    			char* str;
    			
    			Bar(){
    				str=0;
    			}
    		......
    	};
    

    这种情况下,类Bar的成员对象(member class object)Foo有默认构造函数,且类Bar显式定义了构造函数,此时编译器会自动扩展类Bar已有的构造函数,并调用其成员对象的默认构造函数,可能情况如下:

    Bar::Bar(){
    	foo.Foo::Foo();		// 附加上的complier code
    	str=0;				// explicit user code
    }
    
  • 2.带有Default Constructor的Base Class
    原理同上……
    A.若子类没有显式定义构造函数 => 编译器合成默认构造函数并调用父类的默认构造函数
    B.若子类显式定义了构造函数 => 编译器自动扩展已有的构造函数并调用父类的默认构造函数

  • 3.带有一个Virtual Function的Class
    同上,若未显式定义则创建;若显式定义了则扩展……

    class Widget{
    	public:
    		virtual void flip()=0;
    	// ......
    };
    
    class Bell:public Widget{
    	// ......
    }
    
    void flip(const Widget& widget){
    	widget.flip();
    }
    
    // 客户代码
    void foo(){
    	Bell b;
    	flip(b);
    }
    

    上述情况,类Bell未显式定义构造函数,编译器为其自动创建,并需要完成以下工作(扩展也是):
    1.生成一个vtbl,用于存放virtual functions的地址
    2.在object内生成一个vptr,指向vtbl
    此外,widget.flip()的虚拟调用操作会被改写(创建构造函数时),以使用widget的vptr和vptl中的flip()条目,可能情况如下:

    void flip(const Widget& widget){
    	//widget.flip();
    	(*widget.vptr[1])(&widget);
    	
    	// 1 表示flip()实例在virtual table中的索引
    	// &widget表示要交给 "被调用的某个flip函数实例" 的this指针(每一个实例函数其实都需要将this作为参数传递!!!)
    }
    
  • 4.带有一个Virtual Base Class的Class
    同上,若未显式定义则创建;若显式定义了则扩展……

    class X { public: int i; };
    class A:public virtual X { public: int j; };
    class B:public virtual X { public: double d; };
    class C:public A,B {public: int k; };
    
    void foo(const A* pa){
    	pa->i=1024;
    }
    
    main(){
    	foo(new A);
    	foo(new B);
    }
    

    在上面这种虚拟继承的情况下,编译器无法得出foo()中pa->i的实际偏移,因为pa的真正类型可变(它可以是A的指针,也可以是C的指针);上面的A没有构造函数(B也是),编译器在创建默认构造函数时(同虚函数的情况),会自动修改foo()函数,可能情况如下:

    void foo(const A* pa){
    	// __vbcX表示:指向virtual base class X的指针
    	// 改写是在A的对象构造期间完成的!!!
    	pa->__vbcX->i=1024;
    }
    
  • 总结
    a. 在上述4种情况下,会自动创建/扩展构造函数;否则都是属于无用的构造函数trival,编译器不会自动创建
    b. 在合成default constructor时,只有base class subobjects和member class objects会被初始化;所有其他nonstatic data member都不会被初始化!!!
    c. 不是说没有默认构造函数就总是会合成;
    d. 不是说合成的构造函数会初始化所有成员;

2.2 Copy Constructor的构造函数(对比2.1)

  • 概述
    什么是copy constructor:拷贝构造函数,亦即带有当且仅有一个参数,类型为同类对象的构造函数 => 注意:前面说法不准确,即使参数类型不是此类,也是拷贝构造函数!!!
    分类:trival 和 nontrival; nontrival则会被合成;trival不会自动合成函数(而是直接拷贝成员)
    什么时候自动合成copy constructor当编译器需要它的时候,而不是用户程序需要它的时候;被合成出来的constructor只执行编译器所需行动,用户需要的初始化应该自己进行……
    nontrival copy constructor => 四类情况:
    1.有拷贝构造函数(不论其copy constructor是显式声明的还是编译器合成的)的Member Class Ojbect;
    2.有拷贝构造函数(不论其copy constructor是显式声明的还是编译器合成的)的Base Class;
    3.带有虚函数的Class;
    4.带有虚基类的Class

  • bitwise copy semantics与tirval
    当class展现出bitwise copy semantics时,它的拷贝构造函数就是trival的,这种情况下编译器不会合成拷贝构造函数……

    class Word{					// 此类体现了bitwise copy semantics
    	public:
    		Word(const char*);
    		~Word(){delete[] str}
    	private:
    		int cnt;
    		char* str;
    };
    
    Word noun("book");
    void foo(){
    	Word verb=noun;
    	
    	// 在bitwise copy semantics下,verb的初始化并非经过拷贝构造函数
    	// 而是:
    	// verb.cnt=noun.cnt;
    	// verb.str=noun.str;
    	......
    }
    

    上述的Word展现出了bitwise copy semantics,所以编译器不会合成拷贝构造函数,此时的verb的初始化直接复制了noun的成员,而不是通过拷贝构造函数!
    但是对于下面这种情况:

    class Word{					// 此类未体现bitwise copy semantics
    	public:
    		Word(const char*);
    		~Word(){delete[] str}
    	private:
    		int cnt;
    		String str;
    };
    
    Word noun("book");
    void foo(){
    	Word verb=noun;
    	
    	// 在非bitwise copy semantics下,verb的初始化经过拷贝构造函数
    	// 可能合成如下:
    	// inline Word::Word(const Word& wd){
    	// 		str=String::String(wd.str);
    	// 		cnt=wd.cnt;
    	// }
    	......
    }
    

    上面Word没有体现出bitwise cpoy semantics,为nontrival,所以编译器为其合成拷贝构造函数,verb的初始化是通过拷贝构造函数

  • 1.当class内含member object且后者的class有copy constructor
    这种情况下是nontrival的,编译器需要合成拷贝构造函数,且需要将member object的copy conctructor调用操作安插到合成的copy constructor中;基本类型的数据则在合成的copy constructor中直接复制(如上面代码)……

  • 2.当class继承自一个base class且后者存在copy constructor
    这种情况下是nontrival的,编译器需要合成拷贝构造函数,且需要将base class的copy conctructor调用操作安插到合成的copy constructor中

  • 3.当class 声明了一个或多个virtual functions
    这种情况下是nontrival的,编译器需要合成拷贝构造函数。合成出来的copy constructor会显式设定new object的vptr,而不是直接复制old object的vptr => 因为二者可能不同,比如下面的这种情况:

    class ZooAnimal{
    	public:
    		ZooAnimal();
    		virtual ~ZooAnimal();
    		
    		virtual void animate();
    		virtaul void draw();
    };
    
    class Bear:public ZooAnimal{
    	Bear();
    	void animate();			// 仍然是virtual的,因为继承的ZooAnimal
    	void draw();
    	virtual void dance();
    };
    
    // 客户代码
    Bear yogi;
    ZooAnimal franny=yogi;		// 此时有切割(slicing)行为
    

    由于Bear对象赋给ZooAnimal对象,会发生切割行为。franny是一个Annimal对象,yogi是一个Bear对象;虽然Bear继承自Annimal,但是二者的虚函数是不同的(如下图),所以franny和yogi的vptr不同!!!
    在这里插入图片描述

  • 4.当class的继承链中有一个或多个virtual base classes
    这种情况下是nontrival的,编译器需要合成拷贝构造函数。类似于3要决定怎么设定虚函数表指针 => 这种情况下合成的拷贝构造函数要决定怎么设定虚基类的指针和偏移量
    详见p57……

2.3 程序转化语义学(TODO)

2.4 成员初始化列表(TODO)

第三章 Data语义学

3.0 引子

  • class的大小
    class X {};
    class Y: public virtual X {};
    class Z: public virtual X {};
    class A: public Y,public Z {};
    
    判断上述各类的大小?
    sizeof(X) => 1 byte
    sizeof(Y) => 8 bytes
    sizeof(Z) => 8 bytes
    sizeof(A) => 12 bytes
    
  • 原因
    在这里插入图片描述
    1. 对于empty class X而言,编译器会自动插入一个char,从而让它的objects在内存中独一无二;
    2. 对于 Y、Z而言,编译器需要插入指针指向虚基类(32bit下占4bytes),然后是虚拟类X的char(1byte),然后是alignment(填充3bytes),所以最终有8bytes;
    3. 对于A而言,虚拟类X占1byte、父类Y、Z各占4bytes(此时Y/Z不算X大小,因为虚基类是公共的),alignment占3bytes,所以最终12bytes!

3.1 Data Member的绑定

  • 内联成员函数中数据成员的绑定时机
    一个inline函数实体,在整个class声明未被完全看见前,是不会被评估求值的。如果inline在函数体内部,那么对函数的分析直至class声明的右大括号出现才开始。所以在inline member function躯体之内的一个data member绑定操作,会在整个class声明后才发生

    class Point3d{
    	private:
    		float x;
    	public:
    		// 对这个inline函数本体的分析将延迟
    		// 直到class声明的右大括号出现才开始!!!
    		float X(){
    			return x;
    		}
    };
    
  • 成员函数argument list的绑定时机
    argument list中的名称还是会在它们第一次遭遇时被适当地决议(resolve)(而不是像inline member function那样等类定义完成之后……)

    typedef int length;
    class Point3d{
    	public:
    		void mumble(length val){
    			_val=val;
    		}
    		length mumble(){
    			return _val;
    		}
    	private:
    		// length必须在 "本class对它的第一个参考操作" 之前被看见
    		// 这样声明将时先前的参考操作不合法
    		typedef float length;
    		length _val;
    };
    

    上述情况中, mumble(length val);中的length将会被决议(resolve)为int,而不是期望的float => 应该将类中typedef float length;放到类声明的最前面,从而让之前的参考操作不合法,length才能决议到期望的类型……

  • 早期防御性的风格
    1. 把data members放在class声明开始处 => 让类之前的全局变量在类中失效
    2. 把inline functions放在class声明之外 => 与内联成员函数被resolve的时机呼应
    3. 把nested type(typedef)声明放在class的起始处 => 让类之前的typedef在类中失效
    ……
    上述风格现在虽已无必要,但是个人仍然应该这样做

3.2 Data Member的布局

  • data member的布局
    1. nonstatic data member在objects中的排列顺序和被声明的顺序一样
    2. 任何static data member都不会被放进objects的布局之中
    3. 编译器还可能合成一些内部使用的data member,比如vptr等

3.3 Data Member的存取

第四章 Function语义学

第六章 执行期语意学

第七章 站在对象模型的尖端

  • 前言
    本章探讨三个著名的c++语言扩充性质:templateexception handling(EH)runtime type identification(RTTI)

7.1 Template

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
【内容简介】 Inside The C++ Object Model专注于C++对象导向程序设计的底层机制,包括结构式语意、暂时性对象的生成、封装、继承,以及虚拟——虚拟函数和虚拟继承。这本书让你知道:一旦你能够了解底层实现模型,你的程序代码将获得多么大的效率。Lippman澄清了那些关于C++额外负荷与复杂度的各种错误信息和迷思,但也指出其中某些成本和利益交换确实存在。他阐述了各式各样的实现模型,指出它们的进化之道及其本质因素。本书涵盖了C++对象模型的语意暗示,并指出这个模型是如何影响你的程序的。 本书重点:探索对象导向程序所支持的C++对象模型”下的程序行为。对于“对象导向性质之基础实现技术”以及“各种性质背后的隐含利益交换”提供一个清楚的认识。检验由程序变形所带来的效率冲击。提供丰富的程序范例、图片,以及对象导向观念和底层对象模型之间的效率测量。 C++成山似海的书籍堆中,这一本不是婴幼儿奶粉,也不是较大婴儿奶粉,它是成人专用的低脂高钙特殊奶粉。 对于C++底层机制感兴趣的读者,这本书会给你“漫卷诗书喜欲狂”的感觉。 了解C++ Object Model,是学习Component Object Model的最短路线。 如果你是一位C++程序员,渴望对于底层知识获得一个完整的了解,那么Inside The C++ Object Model正适合你。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值