文章目录
第一章 关于对象
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
)指向相应的vtblclass 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继承:类似于vptr
和vtbl
那样 => 在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++语言扩充性质:template
、exception handling(EH)
、runtime type identification(RTTI)