目录
什么是C++对象模型
第一章 关于对象
1.1 C++对象模式
1.2 关键词所带来的差异
1.3 对象的差异
多态
指针的类型
第二章 构造函数语意学
2.1 Default Constructor的构造操作
2.2 拷贝构造器的构造操作
2.3 程序转化语意学
2.4 成员们的初始化队伍
第三章 Data语意学
3.1 Data Member的绑定
3.2 Data Member的布局
3.3 Data Member的存取
Static Data Member
Nonstatic Data Members
3.4 “继承”与Data Member
只要继承不要多态
加上多态
多重继承
虚拟继承
3.5 对象成员的效率
3.6 指向Data Member的指针
“指向Member的指针”的效率问题
第4章 Function语意学
4.1 Member的各种调用方式
非静态成员函数
虚拟成员函数
静态成员函数
4.2 虚拟成员函数
多重继承下的虚函数
虚继承下的虚函数
4.4 指向成员函数的指针
第5章 构造、析构、拷贝语意学
1.纯虚函数
5.1 “无继承”情况下的对象构造
5.2 继承体系下的对象构造
虚拟继承
vptr初始化语意学
5.3 对象复制语意学
5.4 对象的效能
5.5 析构语意学
第6章 执行期语意学
6.1 对象的构造和析构
全局对象
局部静态对象
对象数组
Default Constructor和数组
6.2 new和delete运算符
针对数组的new语意
Placement Operator new的语意
6.3 临时性对象
第7章 站在对象模型的尖端
7.1 Template
7.2 Exception handling
7.3 执行期类型识别
1.语言中直接支持面向对象程序设计的部分。
2.对于各种支持的底层实现机制。
在C语言中,“数据”和“处理数据的操作”是分开来声明的,也就是说,语言本身并没有支持“数据和函数”之间的关联性。我们把这种程序方法称为程序性的,由一组“分布在各个以功能为导向的函数中”的算法所驱动,它们处理的是共同的外部数据。
而在C++中,Point3d有可能采用独立的“抽象数据类型(ADT)”来实现
加上封装后的布局成本:主要是由virtual引起的,包括,virtual function;virtual base class。然而,一般而言,并没有什么天生的理由说C++程序一定比其C兄弟庞大或迟缓。
在C++中有两种类数据成员:static 和 nonstatic。以及三种类成员函数:static、nonstatic和virtual。
简单对象模型
一个对象是一系列的slots,每一个slot指向一个成员。成员按其声明顺序,各被指定一个slot。
表格驱动对象模型
为了统一,另一种对象模型是将与成员相关的信息抽出来,一个放入数据成员表中,一个放入成员函数表中,类对象本身则内含指向这两个表格的指针即可。然后二者分别都是一系列的slots,成员函数表中每一个slot指向一个成员函数,而数据成员表则直接持有数据本身。
C++对象模型
非静态数据成员被配置于每一个类对象之内,而静态数据成员、静态和非静态成员函数都被放在个别的类对象之外。虚函数则以两个步骤支持之:每个类产生出一个虚表;每个类对象被安插一个指针指向虚表。(每一个类所关联的type_info object也经由虚表被指出,通常放在表格的第一个slot。)
该模型的优点在于空间和存取时间的效率,主要缺点在于非静态数据成员修改时必须重新编译。
对象模型如何影响程序?
暂时不太懂,总之就是编译器构造和析构的时候,内部会发生一系列的转化,而采用不同的对象模型,转化的效率应该是不一样的。
观念上,在C语言中,struct代表的是一个数据集合体,因此它没有private data,member function;而在C++中,二者均是代表类,所不同的仅在默认访问权限和默认继承类型不同罢了。
因为template与C不兼容,因此以下代码不合法:
template
struct mumble{...};
C struct 在C++中的一个合理的用途,是当你要传递“一个复杂的class object的全部或部分”到某个C函数去时,struct声明可以将数据封装起来,并保证拥有与C兼容的空间布局。
C++程序设计模型直接支持三种程序设计范式:
1.程序模型:即来自C语言的部分;
2.抽象数据类型模型:即封装与抽象。此模型所谓的“抽象”是和一组表达式(public接口)一起提供的。
3.面向对象模型:定义基类并派生出子类。
只有通过指针和引用的间接处理,才支持OO程序设计所需的多态性质。
C++以下列方法支持多态:
1.隐含的转化操作:把派生类指针转化为一个指向其基类的指针:shape *ps=new circle();
2.经由虚函数机制:ps->rotate();动态绑定
3.dynamic_cast和typeid运算符,大致算作强制类型转换吧。
那么,需要多少内存才能表现一个类对象呢?
这里最重要的是想说,指针类型所带来的影响在于这个指针涵盖的范围是不一样的!!!
一个指向地址1000而类型为void*的指针,由于我们不知道它将涵盖怎样的地址空间,因此不能通过它操作所指之object。
所以cast其实是一种编译器指令。大部分情况下它并不改变一个指针所含的真正地址,它只影响“被指出之内存的大小和其内容”的解释方式。
一个基类指针和一个派生类指针都指向派生类对象的第一个byte,但它们的差别是,派生类指针涵盖整个派生类对象,而基类指针只包含派生类对象的基类部分。
一个指针或一个引用之所以支持多态,是因为它们并不引发内存中任何“与类型有关的内存委托操作”:会受到改变的,只有它们所指向的内存的“大小和内容解释方式”而已。
(自己的理解:比如基类指针和派生类指针都指向的是派生类对象,只不过涵盖范围不同,并没有涉及到类型的转换。)
default constructor在需要的时候被编译器产生出来,而不是程序需要,差别在于是程序需要还是编译器需要。
//默认构造函数测试
#include
using namespace std;
class Foo
{
public:
int val;
Foo *ptr;
};
int main()
{
Foo foo;
if(foo.val || foo.ptr)
cout<<"参数没有初始化"<
1.带有“Default Constructor”的Member Class Object(一个类的成员变量为一个对象类型且这个成员对象含有默认的构造函数)
一个类没有构造器,但是它的类对象含有默认构造器,那么这个类的隐式默认构造器就是有用的。不过合成的操作只有真正在需要被调用时才会发生。
当default constructor已经被显式定义出来了,编译器没办法合成第二个,但是编译器还需要完成它的部分,那它会怎么办呢?
即:如果class A内含一个或一个以上的member class objects,那么class A的每个构造器必须调用每一个member class的default constructor。编译器会扩张已存在的constructors,在其中安插一些代码,使得用户代码被执行之前,先调用必要的default constructor。
比如:
//程序员定义的default constructor
Bar::Bar(){str=0;}
编译器还要初始化它内含的那个类对象的部分,因此将其拓展为:
//C++伪码
Bar::Bar(){
foo.Foo.Foo();//附加上的comiler code
str=0; //显式的用户代码
}
2.一个类的基类带有默认的构造函数。
3.如果一个class它自己或者是他继承串链中的其中一个父类含有virtual函数
一个虚函数表vtbl会被编译器产生出来,内放class的虚函数地址;
在每一个类对象中,一个额外的pointer member(vptr)会被编译器合成出来,内含相关之类的vtbl的地址。
4.带有一个虚基类的类
共同点在于必须使虚基类在其每一个派生类中的位置能于执行期准备妥当。需要编译器为这个类的对象合成一个虚基类指针,执行它的虚基类。
具体参见:http://blog.csdn.net/bluedog/article/details/4711169
总结:上面的4种情况,会使得编译器为没有声明构造函数的类合成一个默认的构造函数。在c++标准中把这些合成物称之为隐式有效的默认构造函数。但是被合成的默认构造函数只满足编译器的需要,并不能满足程序员的需要。至于除了这4中情况之外而又没有声明任何构造函数的class,在本书中说它们拥有的是一个隐式的无效的默认构造函数,实际上编译器不会去合成它们。
三种情况会使用到拷贝构造器,以一个object的内容作为另一个类对象的初值:对一个object用另一个对象初始化;作为参数传递给某个函数;函数返回一个类对象时。
当类没有提供一个显式的拷贝构造器时,拷贝类时内部会以对其内建或派生的data member逐一拷贝,而不拷贝其中的成员类对象。
什么时候一个class不展现出“bitwise copy semantics”呢?就需要编译器合成默认拷贝构造器了,有4种情况:
1.当class内含一个类对象而后者的类声明中有一个拷贝构造器。
2.当class继承自一个基类而后者存在一个拷贝构造器时。
3.当class声明了一个或多个虚函数时。
4.当class派生自一个继承串链,其中有一个或多个虚基类时。
编译期间的两个扩张操作(只要有一个类声明了一个或多个virtual functions就会如此):
1.增加一个虚函数表(vtbl),内含每一个有作用的虚函数。
2.一个指向虚函数表的指针(vptr),安插在每一个类对象内。
当一基类对象用一个派生类对象初始化时,其vptr操作也必须保证安全:
不可以直接将派生类对象的vptr拷贝给基类对象,这样它将指向派生类的虚函数表,而事实上它在初始化时已经将派生类的部分切割掉了。也就是说,合成出来的基类拷贝构造器会显式设定对象的vptr指向基类的虚函数表,而不是直接拷贝派生类的vptr。
1.重写每一个定义,其中的初始化操作会被剥除。
2.class的拷贝构造器调用操作会被安插进去。
函数参数是一个对象时,编译器实现技术上一般采用两个策略
1.导入所谓的临时性对象,并调用拷贝构造器将它初始化,然后将此临时性对象交给函数。但这样没有释放这个临时对象,因此将函数形参从原先的一个对象改变为一个类引用。
2,拷贝建构方式把实际参数直接建构在其应该的位置上。
首先加入一个额外参数,然后在return指令之前安插一个拷贝构造器调用操作,以便将欲传回之对象的内容当作上述新增参数的初值。
定义另一个“计算用”的构造器,可以直接计算出xx的值。
NRV优化,以result参数取代named return value。直接处理_result
copy constructor的应用,迫使编译器多多少少对程序代码做部分转化,尤其是当一个函数以传值的方式返回一个类对象,而该类有一个copy constructor时,这将导致深奥的程序转化——不论是在定义上的还是使用上。
在下列情况为了使程序能够顺利编译,必须使用member initialization list:
1.当初始化一个reference member时;
2.当初始化一个const member时;
3.当调用一个基类的constructor,而它拥有一组参数时;
4.当调用一个member class的constructor,而它拥有一组参数时。
member initialization list中并不是一组函数调用。而是一 一操作初始化列表,以适当顺序在构造器之内安插初始化操作,并在任何显式用户代码之前。
注意:list的项目顺序是由class中的members声明顺序决定的,而不是由初始化列表中的排列顺序决定。
如果二者顺序出现外观错乱,可能会导致一些错误,比如i(j)比j(val)更早执行,但因为j一开始未有初值,所以i(j)执行结果导致i无法预知。
引例:
#include
class X {};
class Y :public virtual X {};
class Z :public virtual X {};
class A :public Z, public Y {};
using namespace std;
void main() {
cout << sizeof(X);
cout << sizeof(Y);
cout << sizeof(Z);
cout << sizeof(A);
system("pause");
}
结果输出:1448。Y和Z的大小受三个因素影响:
1.语言本身所造成的额外负担(overhead)。
当语言支持虚基类时,就会导致一些额外负担。在派生类中,这个额外负担反映在某种形式的指针上,它或者指向虚基类子对象,或指向一个表格。
2.编译器对于特殊情况所提供的优化处理。
对empty virtual base class提供的特殊处理:比如讲空虚基类视为派生类最开头的一部分,也就是说它并没与花费任何的额外空间。(因为既然有了成员,就不需要空基类中安插的一个char,也就不需要allignment了)
3.Allignment的限制
对inline函数本体的分析,会直到整个类声明都出现了才开始。然而这对于成员函数的参数列表并不为真。参数列表中的名称还是会在他们第一次遭遇时被适当地决议完成。因此在extern和nested type names之间的非直觉绑定操作还是会发生。
理解:相当于在说内部数据成员和全局变量的分辨吧,编译器还是会先决议出定义的变量是哪个?成员函数中的变量可能会先被决议为全局变量(可能这就是非直觉绑定操作?),就会与之后类中自己又重新定义数据成员发生冲突,因此建议nested type声明放在类的起始处。详情见P91。
非静态数据成员在类对象中的排列顺序将和其被声明的顺序一样,而任何中间介入的静态成员变量都不会放进对象布局当中。
每个static data member只有一个实例,存放在程序的data segment之中。因为static data member并不在class object中,因此存取静态成员并不需要通过class object。
若取一个static data member的地址,会得到一个指向其数据类型的指针,而不是一个指向其class member的指针,因为static member并不内含在一个class object之中。
非静态成员直接存放在每一个类对象中。除非经由显式的或隐式的,否则没有办法直接存取它们。
注意:C++语言保证“出现在派生类中的base class subobject有其完整原样性”,比如concrete1->concrete2->concrete3的对象布局如下:
如果不这样的话,当发生Concrete1 subobjcet的复制操作时,就会破坏Concrete2 members了,比如
P109面注意形参是基类对象,否则当p3d+=p2d时,就会出现没有找到接受Point2d类型的右操作数的运算符。
vptr 放在对象的前端,不然必须在执行期就备妥对象的offset。
对一个多重派生对象,将其地址指定给“最左端基类的指针”,情况和单一继承相同。至于第二个或后继的基类地址指定操作,则需要将地址修改过。
class如果内含一个或多个虚基类子对象,像istream那样,将被分割成两个部分:一个不变区域和一个共享区域。共享区域所表现的就是虚基类子对象。这一部分的数据,其位置将因为每次的派生操作而有变化,所以它们只可以被间接存取。各家编译器实现技术之间的差异就在于间接存取的方法不同。
比如在C++对象模型中,虚继承而来的子类会生成一个隐藏的虚基类指针(vbptr),在Microsoft Visual C++中,虚基类表指针总是在虚函数表指针之后,因而,对某个类实例来说,如果它有虚基类指针,那么虚基类指针可能在实例的0字节偏移处(该类没有vptr时,vbptr就处于类实例内存布局的最前面,否则vptr处于类实例内存布局的最前面),也可能在类实例的4字节偏移处。
#include
#include
using namespace std;
class Point3d
{
public:
Point3d(float x = 0.0, float y = 0.0, float z = 0.0)
: _x(x), _y(y), _z(z) {}
float x() const { return _x; }
float y() const { return _y; }
float z() const { return _z; }
private:
float _x;
float _y;
float _z;
};
inline ostream&
operator<<(ostream &os, const Point3d &pt)
{
os << "(" << pt.x() << ","<< pt.y() << "," << pt.z() << ")";
return os;
}
void main() {
Point3d x=Point3d(1, 2, 3.2);
cout << x;
system("pause");
}
为了区分一个“没有指向任何data member”的指针,和一个指向第一个data member的指针,每一个真正的member offset值都被加上1.
为每一个“member存取操作”加上一层间接性,会使未优化的执行时间多出一倍不止,优化后时间一致。
单一继承不会影响,虚拟继承会妨碍优化的有效性。
C++支持三种类型的成员函数:static、nonstatic和virtual,每种类型被调用的方式都不相同。
静态成员函数不可能直接存取非静态成员和被声明为const;
C++编译器内部将Nonstatic member function转换为非成员函数,转换步骤如下:
1.改写函数的 signature(函数原型)以添加一个 this 指针;
2.通过 this 指针存取操作 nonstatic data member;
3.将成员函数重写成为一个外部函数,经过名称“ mangling ”处理转换为独一无二的非成员函数(加上 class 名称与参数类型)。两个实例如果拥有独一无二的 name mangling ,那么任何不正确的调用操作在链接时期就因无法决议 resolved 而失败;
名称的特殊处理(Name Mangling)
成员的名称后加上类名,形成独一无二的命名,函数可以加上它们的参数列表。
经由一个类对象调用一个虚函数,这种操作总是被编译器像对待一般非静态成员函数一样地加以决议。(因为这种情况并不支持多态,所以没必要复杂)
1.静态成员函数类似于静态成员变量都属于类而不是对象。
2.静态成员函数仅可以调用类的静态成员变量,不可以调用普通成员变量。
3.不具有this指针,因而自然不能声明为const。
程序方法上的解决之道是很奇特地把0强制转换为一个class指针,因而提供出一个this指针实例。
静态成员函数的主要特性:没有this指针。次要特性:
Static member function 由于缺乏this指针,因此差不多等同于nonmember function。它提供了一个意想不到的好处——可以做 callback 函数。
在独立的类中可以通过 指针调用 与 对象调用 两种方式使用 虚拟成员函数。
在必须支持某种形式之“执行期多态”的时候,需要必要的信息。RTTI(runtime typy identification)
额外的信息又有哪些呢?ptr所指对象的真实类型。z()实例的位置,以便我能够调用它。
一个类只会有一个虚表。每一个表内含其对应类对象中所有active virtual functions函数实例的地址。这些active 虚函数包括:
需要以适当的offset来调整this指针;跳到虚函数去
有两个虚表被编译器产生出来:一个主要实例,与Base1共享,一个次要实例,与Base2有关。
建议不要在一个虚基类中声明nonstatic data members。
在虚继承下,对给定虚基类,无论该类在派生层次中作为虚基类出现多少次,只继承一个共享的基类子对象。共享的基类子对象称为虚基类
指向成员函数的指针声明语法,以及指向成员选择运算符的指针,其作用是作为this指针的空间保留者,这也就是为什么静态成员函数没有this指针的原因了。
inline函数
如果函数因其复杂度,或因其建构问题,被判断不可成为inline,它会被转为一个static函数,并在“被编译模块”内产生对应的函数定义。
如果一个类被声明为抽象基类(其中有 pure virtual function),则抽象基类不能实例化,但它仍需要一个显示的构造函数以初始化其成员变量。如果没有这个初始化操作,其 derived class 的局部对象 _mumble 将无法决定初值。
类设计者一定要定义纯虚析构器;关于虚析构函数,不要将 virtual destructor 声明为 pure。因为每一个 derived class destructor 会被编译器加以扩张,以静态的方式调用其每一个 virtual base class 以及上一层 base class 的 destructor,只要缺乏任何一个基类析构函数的定义就会导致链接失败。而此时如果此时是纯虚函数,则不可能有析构函数来调用。
在C++中,全局对象都是被以“初始化过的数据”来对待,因此置于data segment。
1.如果有 virtual base class,虚基类的构造函数必须被调用,由浅到深,从左往右:
- 如果class位于成员初值列,有任何显示指定的参数都应该传递过去。
- 若没有位于初值列,而class含有一个默认构造(拷贝)函数,也应该调用。
- 如果class是多重继承下的第二或者后继的base class,那么this指针应该有所调整。
类中的每个虚基类子对象的偏移位置必须在执行期可被存取。
2.如果有base class,基类的构造函数必须被调用;
3. 如果有虚函数,必须设定vptr指向适当的虚表;
4. 如果一个member没有出现在成员初值列表中,但是该成员有一个默认构造函数,那么这个默认构造函数必须被调用;
5. 成员初值列表中的member初始化操作放在constructor的函数体内,且顺序和声明顺序一致。
即继承情况下的对象构造顺序为:虚基类 -> 基类 -> vptr与虚表 -> 类成员初始化 -> 自定义的代码。
在此状态下,“virtual base class constructor的被调用”有着明确的定义:只有当一个完整的类对象被定义出来时,它才会被调用;如果对象只是某个完整对象的子对象,它就不会被调用。
当我们定义一个PVertex对象时,constructor的调用顺序是:(Point3d和Vertex对Point构造器的调用不发生)(由根源到末端,由内而外)
Point(x,y);
Point3d(x,y,z);
Vertex(x,y,z);
Vertex3d(x,y,z);
PVertex(x,y,z);
在一个类的构造器中,经由构造中的对象来调用一个虚函数,其函数实例应该是在此类中有作用的那个。比如在Point3d constructor中调用的size()函数,必须被决议为Point3d::size()而不是PVertex::size()。
什么是决定一个类的虚函数名单的关键?答案是虚表。虚表如何被处理?通过vptr。所以为了控制一个类中有所作用的函数,编译系统只要简单地控制住vptr初始化和设定操作即可。
vptr的初始化操作:在base class constructor调用操作之后,但是在程序员供应的代码或”成员初值列中所列出的成员初始化操作”之前。
构造器的执行算法如下:
1.在派生类构造器中,“所有虚基类”及“上一层基类”的构造器会被调用。
2.上述操作完成后,对象的vptr(s)被初始化,指向相关的虚表。
3.如果有成员初始化列表的话,将在构造器体内扩展开来 。这必须在vptr设定之后才做,以免有一个虚成员函数被调用。
4.最后,执行程序员所提供的代码。
在不涉及虚拟继承只有一个子对象的情况下,编译器合成的派生类的赋值运算符函数会调用所有即时基类的 operator = 函数。
编译器如何能够在Point3d和Vertex的copy assignment operator抑制Point的copy assignment operator呢?(两种解决方法)不然会多次调用虚基类的拷贝赋值运算符。
1.constructor中的解决办法是添加一个额外的参数_most_derived。
2.编译器可能为opy assignment operator产生分化函数以支持这个类成为most-derived class或者成为中间的基类。
建议:尽可能不要允许一个virtual base class的拷贝操作,更进一步,不要在任何virtual base class中声明数据。
抽象数据类型,单一继承,多重继承等都支持bitwise copy语意,然而导入虚拟继承语意,情况就改变了。
析构函数也是根据编译器的需要才会合成出来,两种情况:
1. class中内含的某个object拥有析构函数;
2. 继承自某个base class,该base class含有析构函数。
在继承体系中,由我们定义的destructor的扩展方式和constructor类似,只是顺序相反,顺序如下:
1. destructor的函数体首先执行。
2. 如果class拥有member class object,且该class含有destructor,那么它们会以声明顺序相反的顺序依次被调用。
3. 如果object内含一个vptr,重新被设定指向适当的base class的virtual table(即对象在析构的过程中,依次蜕变为其基类)。
4. 如果有任何上层的nonvirtual base classes拥有destructor,那么它们会以声明顺序相反的顺序依次被调用。
5. 如果有任何virtual base classes拥有destructor,那么它们会以原来构造顺序相反的顺序依次被调用。
执行期语义主要从一下两个方面展开:
静态初始化的原因:在c语言中一个全局对象只能被一个常量表达式(可在编译时期求其值的那种)设定初值。而constructor并不是常量表达式。因此,虽然class object在编译时期可以放置在data segment并且内容为0(c++会这样做,而c这不处理),但constructor一直到程序激活(startup)时才会实施,必须对一个“放置在program data segment 中的object的初始化表达式”做评估。
我们有如下程序片段:
const Matrix & identity(){
static Matrix mat_identity;
return mat_identity;
}
局部静态对象保证了如下语意:
解决方法:1.无条件的在起始时构造出对象来。
2.导入一个临时性对象以保护mat_identity的初始化操作。第一次处理时该对象评估为false,constructor会被调用,然后被改为true,之后不再调用。同样地,析构器也需要有条件的施行于mat_identity,只有在mat_identity已被构造出来才算数。
1.vec_new()取一个default constructor的地址,激活constructor,然而这样将无法(不能允许)存取default argument values。
2.默认的参数如何支持vec_new?
cfront所采用的方法是产生一个内部的sub construtor,没有参数。在其函数内调用由程序员提供的constructor,并将default 参数值明确地指定过去。
int *pi=new int(10);
事实上,它是由两个步骤完成的:1.通过适当地new运算符函数实例配置所需内存。2.将配置来的对象设立初值。
1.int *p_array=new int[5];
vec_new()不会调用,因为它的主要功能是把default constructor施行于class object所组成数组的每一个元素身上。不过new运算符函数会被调用。
2. //struct simple{int i1,i2;};
simple *p_aggr=new simple_aggr[5];
vec_new也不会被调用。因为:simple并没有定义一个constructor和destructor,所以配置数组以及清除p_aggr数组的操作,只是单纯地获取内存和释放内存而已。
3. 如果class定义有一个default constructor,某些版本的vec_new()就会被调用,配置并构造class objectes所组成的数组。
作用就是在指定内存实例化一个对象。
一个预先定义好的重载的 new 运算符,称为 Placement Operator new。它需要一个 void* 指针作为参数,指示出要生成的对象的存放的内存地址。调用如下:
Point2w *ptw = new (arena) Point2w;
那它等价于下列语句么?
Point2w *ptw=(Point2w*) arena;
并不,上述语句只完成了一半功能,placement Operator new所扩充的另一半将Point2w constructor自动实施于arena所指的地址上:
//C++伪码
Point2w *ptw =(Point2w*)arena; //指定地址
if(ptw!=0)
ptw->Point2w::Point2w(); //调用构造函数
析构后希望再次使用arena的正确方法:
p2w->~Point2w;
p2w=new(arena)Point2w;
如果直接使用 delete ,对象被析构的同时指向的内存也会被释放,之后不能再使用了。
一般而言,placement new operator 并不支持多态,被交给 new的指针,应当适当的指向一块预先配置好的内存。
1.T c=a+b;
加法定义为:T operator+(const T &,const T &); 或T T::operator(const T&);
实现根本不会产生一个临时对象。
注:1> 直接以拷贝构造的方式,将a+b的值放到c中。
2> 视operator的定义而定,NRV优化也可能实施起来,这将导致直接在上述c对象中求表达式结果,避免执行copy constructor和具名对象的构造和析构。
2.然而,意义相当的赋值语句:c=a+b;
不能忽略临时对象,相反,他会导致下面的结果:
//c++伪码
T temp;
temp.operator+(a,b);
c.operator=(temp);
temp.T::~T();
3.没有出现目标对象:a+b;
这时有必要产生一个临时对象,以放置运算后的结果。然后其析构有点复杂:
C++标准上这么规定:
临时性对象的被摧毁,应该是对完整表达式求值过程中的最后一个步骤。该完整表达式造成临时性对象的产生。完整表达式就是被涵括的表达式最外围那。比如:
((objA > 1024) && (objB > 1024))
? objA + objB : foo(objA ,objB)
一共有五个子算式,内含在一个“?:完整表达式”中。任何一个子式所产生的任何一个临时对象,都应该在完整表达式被求值完成后,才可以销毁。
临时性对象的生命规则有两个例外:
主要有Template、Exception handling和Runtime type identification。
虽然enum Status的真正类型在所有的Point instantations中都一样,其enumerators也是,但它们每一个都只能够通过template Point class的某个实例来存取或操作。
只有在成员函数被使用的时候,C++standard才要求它们被实例化。
对Exception handling的支持:
当一个Exception发生的时候,编译系统必须完成以下的事情:
1.检验发生throw操作的函数。
2.决定throw操作是否发生在try区段中。
3.若是,编译系统必须把exception type拿来和每个catch子句进行比较。
4.如果比较后吻合,流程控制应该交给catch子句。
5.如果throw的发生并不在try区段中,或没有一个catch子句吻合,那么系统必须(a)摧毁所有active local objects。(b)从堆栈中将目前的函数“unwind”掉。(c)进行到程序堆栈中的下一个函数中去,然后重复2-5。
类型描述器时必要的,因为真正的exception是在执行期被处理的,其object必须有自己的类型信息。
欲支持type-safe downcast,在对象空间(type information)和执行时间(决议执行期的类型runtime type)上都需要一些额外负担。
Dynamic cast:安全则传回被适当转换过的指针,不安全,则传回0。
dynamic cast运算符也适用于reference身上。然而对于一个non-type-safe cast,其结果不会与施行于指针的情况相同。为什么?
因为一个引用不可以像指针那样“把自己设为0便代表了'no object' ”;若将一个引用设为0,会引起一个临时性对象被产生出来,该临时性对象初值为0,这个引用然后被设定为该临时对象的一个别名。
因为如果构造函数为虚函数的话,它将在执行期间被构造,而执行期则需要对象已经建立,构造函数所完成的工作就是为了建立合适的对象,因此在没有构建好的对象上不可能执行多态(虚函数的目的就在于实现多态性)的工作。在继承体系中,构造的顺序就是从基类到派生类,其目的就在于确保对象能够成功地构建。构造函数同时承担着虚函数表的建立,如果它本身都是虚函数的话,如何确保V-Table的构建成功呢?
注意:当基类的构造函数内部有虚函数时,会出现什么情况呢?结果是在构造函数中,虚函数机制不起作用了,调用虚函数如同调用一般的成员函数一样。
当基类的析构函数内部有虚函数时,又如何工作呢?与构造函数相同,只有“局部”的版本被调用。但是,行为相同,原因是不一样的。构造函数只能调用“局部”版本,是因为调用时还没有派生类版本的信息。析构函数则是因为派生类版本的信息已经不可靠了。我们知道,析构函数的调用顺序与构造函数相反,是从派生类的析构函数到基类的析构函数。当某个类的析构函数被调用时,其派生类的析构函数已经被调用了,相应的数据也已被丢失,如果再调用虚函数的派生类的版本,就相当于对一些不可靠的数据进行操作,这是非常危险的。因此,在析构函数中,虚函数机制也是不起作用的。