@zhengyuhong
2014-10-09T02:52:02.000000Z
字数 6950
阅读 1182
读书笔记 C++
答案是Point3d并没有增加成本。三个data members直接内含在每一个class object之中,就像C struct的情况一样。而member function虽然含在class的声明之内,却不出现在object之中。每一个非内联函数只会产生一个函数实例。至于内联函数则会在每一个对象产生一个函数实例。Point3d支持封装性质,这一点并未带给它任何空间或者执行器的不良后果。C++在布局以及存取时间上主要的额外负担是由virtual引起,包括:
虚函数机制
虚继承
C++对象模型中,非静态成员变量被放置于每一个对象之内,静态成员变量则存放在对象之外。静态成员函数与非静态成员函数均放置在对象之外。虚函数则是用以下两个步骤来支持:
每一个类产生一堆指向虚函数的指针,这一堆虚函数指针则放置在一个称为虚函数表(virtual table)当中。
每一个对象则被安插一个指针vptr,指向虚函数表。vptr的设定(setting)与重置(resetting)都有每一个类的构造函数、析构函数、赋值运算符自动完成。每一个类都有一个关于的类型信息(type_info),用来支持RTTI,用一个指针指向,这个指针通常放置在虚函数表的第一个slot。
C++支持多继承
class iostream:public istream,public ostream{...};
甚至,继承也可以指定为虚拟(virtual,也就是共享的意思)
class istream : virtual public ios{...};class ostream : virtual public ios{...};
在虚继承的情况下,共享基类不管在继承链中被派生多少次,永远只存在一个实例。
那么虚继承中,派生类是如何在本质上构造共享基类呢?
简单对象模型:
每一个派生类对象(离共享基类最远的派生类)内部有一个slot指向共享基类对象,这个体制的缺点是,间接性而导致空间与存取时间上的额外负担,优点是派生类对象的大小不会因为共享基类的改变而受到影响,共享基类改变时,派生类无需重新编译。
表格驱动对象模型
就好像每一个虚函数表中内含每一个虚函数地址一样,每一个派生类对象都有一个bptr指向它所继承的基类表格(一个或者多个)。主要缺点是由于间接性而导致空间和存取时间上的额外负担,优点则是每一个派生类(包括中间的派生类)都有一致的表现形式:每一个派生类对象都在某一个位置安插一个bptr,与基类的大小、个数均为关系。第二个是基类表格改变时(增减基类)时,无需改变派生类,派生类无需重新编译。
什么时候一个程序员用struct取代class? 当它让一个人感觉比较好的时候。
本人对于这一节看得不多,因为我没有在选择struct与class的困扰,我一直是使用class,我只知道它们两者有一些区别
成员变量在内存中的顺序
class中处于同一个访问级(public,protected,private)的数据必定保证以其声明的顺序出现在内存布局当中。然而被放置在多个访问级的各笔数据,排列顺序就不一定了。
同理,基类与派生类的成员变量的布局没有谁先谁后的强制规定,这个看各个厂商的编译器如何支持。
struct在C++中的一个合理用途,是当要传递一个复杂的对象全部或者部分到某一个C函数时,struct声明可以将数据封装起来,并保证拥有与C兼容的空间布局。
对象的多态操作要求此对象必须可以经由一个指针或者引用来存取,不过指针与引用并不是多态的必要结果。如:
const int maxSize = 100;int * vec = new int[maxSize];...delete[] vec;
C++以一下方法支持多态
shape *ps = new circle();
ps->rotate();
if(circle *pc = dynamic_cast<circle* >(ps))...
多态的主要用途是由共同的基类指针操作不同的派生类,或者使共同接口操作派生自同一个基类的派生类的对象。根据执行期中对象的真正类型来解析出调用哪一个函数。
需要多少内存才能能表现一个对象?一般而言有:
一个指针,不管它指向哪一种类型,指针本身所需的内存大小是固定的。举一个例
class ZooAnimal{public:ZooAnimal();virtual ~ZooAnimal();//...virtual void rotate();protected:int loc;String name;}
一个指向ZooAnimal的指针是如何地与一个指向整数的指针有所不同?
ZooAnimal *px;int* pi;
以内存需求来说,没有什么不同!它们三个都需要足够的内存来放置一个机器地址。指向不同类型的指针间的差异既不在其指针表示法的不同,也不在其内容的不同,而是在其所寻址出来的对象类型的不同。也就是说指针类型是教导编译器如何解释指针指向的地址中的大小及其内容:
class Bear:public ZooAnimal{public:Bear();~Bear();void rotate();virtual void dance();protected:enum Dances{...};int cell_block();};Bear b;//假设b放在地址1000处
一个Bear指针与ZooAnimal指针有什么不同?
Bear b;ZooAnimal *pz = &b;Bear *pb = &b;
它们都指向b的首地址,根据前面指针类型,它们的区别是pb所涵盖的地址包含整个b对象,而pz则所涵盖的地址只包含b对象中的ZooAnimal部分。
除了ZooAnimal部分中出现的members,你不能使用pz来直接处理Bear的任何members,唯一例外就是虚函数机制,我pz调用rotate()时
pz->rotate();
pz的类型将在编译时期决定以下两点:
该接口的访问级
在执行器,pz所指的对象类型可以决定rotate()所调用的实例,(就是rotate的函数实现),类型信息的封装并不是维护在pz中,而是维护在虚函数表中,通常第一个slot是type_info,保存类型信息)。
现在看如下情况:
Bear b;ZooAnimal za = b;//引起切割slicedza.roate();
前面1.1、C++对象模型中说到,vptr会在构造函数、析构函数、赋值运算符中会将**vptr**setting与resetting,当上述za = b;会发生切割,并且会重置za的vptr(如果有的话)
一个指针或者引用之所以支持多态,是因为它们并不引发内存中任何“与类型有关的内存委托操作(type-dependent commitment)”会受到改变的,只是使得它们指向的内存的“大小与内容解释”发生变化。
如果一个类没有定义任何构造函数,但有一个拥有默认构造函数的成员变量m,那么这时编译器会自动合成一个有用的默认构造函数,用以初始化m。不过这个操作也是在默认构造函数真正需要时才合成,如此整个源码都没有定义一个这个类的变量,那么就没有使用到默认构造函数,所以就没有必要合成。而且合成的构造函数、拷贝函数、赋值运算符与析构函数都是以inline方式完成。初始化拥有默认构造函数的成员变量时编译器的责任。
如果一个类没有定义任何构造函数,但有一个成员变量str,它没有默认构造函数,这时候编译器不会合成一个默认构造函数,初始化str是程序员的责任了。
总的来说,初始化拥有默认构造函数的成员变量m时编译器的责任,在定义的构造函数中的初始化列表没有初始化m,编译器就自动插入至初始化列表当中并调用m默认构造函数,如果没有定义默认构造函数,就合成一个,然后插入初始化列表当中。
这里的基类base与上述的成员变量m一样,初始化拥有默认构造函数的基类的责任是编译器的责任。
如下两种情况也需要合成默认构造函数
m的情况,即便是有构造函数,编译器也会在其中安插代码设置虚函数表指针、虚基类指针memberwise 深层拷贝
bitwise 浅层拷贝
并不是所有未定义有拷贝构造函数的类编译器都会为其合成拷贝构造函数,编译 器只有在必要的时候才会为其合成拷贝构造函数。所谓必要的时刻是指编译器在 普通手段无法完成解决“当一个类对象以另一个同类实体作为初值”时,才会合成 拷贝构造函数。也就是说,当常规武器能解决问题的时候,就没必要动用非常规 武器。
如果一个类没有定义拷贝构造函数,通常按照“成员逐一初始化(Default Memberwise Initialization)”的手法来解决“一个类对象以另一个同类实体作为 初值”——也就是说把内建或派生的数据成员从某一个对象拷贝到另一个对象身上, 如果数据成员是一个对象,则递归使用“成员逐一初始化(Default Memberwise Initialization)”的手法。
成员逐一初始化(Default Memberwise Initialization)具体的实现方式则是位 逐次拷贝(Bitwise copy semantics)1。也就是说在能使用这种常规方式 来解决“一个类对象以另一个同类实体作为初值”的时候,编译器是不需要合成拷 贝构造函数的。但有些时候常规武器不那么管用,我们就得祭出非常规武器了 ——拷贝构造函数。有以下几种情况之一,位逐次拷贝将不能胜任或者不适合来完 成“一个类对象以另一个同类实体作为初值”的工作。此时,如果类没有定义拷贝 构造函数,那么编译器将必须为类合成一个拷贝构造函数。
1. 当类内含一个成员对象,而后者的类声明有一个拷贝构造函数时(不论是设 计者定义的还是编译器合成的)。
2. 当类继承自一个声明有拷贝构造函数的类时(同样,不论这个拷贝构造函数 是被显示声明还是由编译器合成的)。
3. 类中声明有虚函数。
4. 当类的派生串链中包含有一个或多个虚基类。
写到这里,我感觉我明白了很多什么时候是编译器需要了,通过都跟上面四个有关了,其实默认构造函数、拷贝构造函数、赋值运算符与析构函数四个需要合成的时候都是要处理这上面四个问题。
显式的初始化操作
已知有定义
X x0;
下面的三个定义,每一个都是以x0来初始化其对象
void foo(){X x1(x0);X x2 = x0;X x3 = X(x0);}
必要的程序转换有两个阶段:
void foo(){X x1;X x2;X x3;x1.X::X(x0);//是一个bitwise过程x2.X::X(x0);x3.X::X(x0);}
按值传递的参数初始化,有:
void foo(X x0);//下面这样子调用方式X xx;foo(xx);
会被编译器转码为:
X _temp;_temp.X::X(xx);//是一个bitwise过程foo(_temp);
返回值的初始化
已知下面这个函数定义:
X bar(){X xx;//处理xxreturn xx;}
NVI的做法:
1. 首先加上一个额外的参数,类型是一个返回值类型的引用
2. 在return指令之前安插一个拷贝构造函数调用操作,以传入返回值
上述函数会被改写为:
void bar(X &_result){X xx;xx.X::X();...处理x;_result.X::X(xx);return ;}
X xx = bar();
转换为
X xx;bar(xx);<div class="md-section-divider"></div>
在下列情况下,为了让你的程序能够被顺利编译,你必须使用member initialization list:
每一个对象必须有足够的大小容纳它所有非静态成员变量,以及:
1. 由于编译器自动加上的额外成员变量,例如虚函数表指针、虚基类指针
2. 边界调整
非静态成员变量在对象中的排列顺序如其声明顺序一样,至于静态成员变量则是放在程序的data segment中,与个别对象无关。C++标准规定,在同一个访问级别区域中,成员变量的排列只需要合乎晚出现在较高地址上即可,并不一定得连续排列,中间可以需要边界调整。
至于内部使用的成员变量,如虚函数表指针与虚基类指针等,这个位置允许各厂商自行放置,可以在对象最前、最后甚至在成员变量中间
对于静态成员变量,存放在类之外的全局变量区
对于非静态成员变量,每一个非静态成员变量的偏移位置在编译时期即可获知,因此存取非静态成员变量的效率与C语言的结构体一样的。
但是涉及虚继承以及多态时存取速度就会降低了。
origin.x = 0;p->x = 0;<div class="md-section-divider"></div>
这两个的存取速度会因多态性而已,当有多态时,会涉及执行期的经过额外的索引才能解决,而origin就不会了,因为已经肯定知道它的类型了
在不涉及虚继承的时候,继承而来的成员变量与派生类的成员变量的数据布局应该与非继承的差不多。继承来的成员变量也没有规定是否在前或者后。
非静态成员函数应与普通函数的效率一样
实际上每一个非静态成员函数均被转换为普通函数,转换步骤:
floatPoint3d::magnitude()改写为:flaot magnitude(Point3d *const this);floatPoint3d::magnitude()const改写为:flaot magnitude(const Point3d *const this);<div class="md-section-divider"></div>
floatPoint3d::magnitude(){x *= 2;y *= 2;z *= 2;}改写为:flaot magnitude(Point3d *const this){this->x *= 2;this->y *= 2;this->z *= 2;}
静态成员函数没有this指针,所以就有以下特性
为了调用正确的虚函数,一个指针应有如下:
接下来,还没看懂,这本书不容易看懂啊