51CTO技术论坛_中国领先的IT技术社区's Archiver

ttt110 发表于 2007-8-17 13:34

深度探索C++对象模型(9)

介绍 dO%N6E)is5G ~
当编译一个C++程序时,计算机的内存被分成了4个区域,一个包括程序的代码,一个包括所有的全局变量,一个是堆栈,还有一个是堆(heap),我们称堆是自由的内存区域,我们可以通过new和delete把对象放在这个区域。你可以在任何地方分配和释放自由存储区。但是要注意因为分配在堆中的对象没有作用域的限制,因此一旦new了它,必须delete它,否则程序将崩溃,这便是内存泄漏。(C#已经通过内存托管解决了这一令人头疼的问题)。C++通过new来分配内存,new的参数是一个表达式,该表达式返回需要分配的内存字节数,这是我以前掌握的关于new的知识,下面看看通过这本书,使我们能够更进一步的了解到些什么。
4v,n!e8L6qJSC k]jq']A
正文 ]cwkEF1o8J P
QG(L9U]C3| \\
这一章主要是说Runtime Semantics执行期语义学。
!r(m:@$I"q0e
D H(z~ o:WE&JL 这是我们平时写的程序片段:
WU LD6o:c R.u Matrix identity; //一个全局对象
9t7w0Xft Main() PM*j@Y%u4u1T.Y
{ R"tam/BaEM
  Matrix m1=identity;
C3H0K ` Em1k1H   …… B`XZS#X
  return 0; o B.@$hL0R.\e}kl
}
EQ Vw&zy4{ 很常见的一个代码片段,雷神从来没有考虑过identity如何被构造,或者如何被销毁。因为它肯定在Matrix m1=identity之前就被构造出来了,并且在main函数结束前被销毁了。我们不用考虑这些问题,好象C++就应该这样。但这本书是研究C++底层机制的。既然我们在看这本书,说明我们希望了解C++的编译器又做了那些大量的工作,使得我们可以这样使用对象。
3l I!@j2t/l$N 7D'II C5{h$T8@
在C++程序中所有的全局对象都被放在data segment中,如果明确赋值,则对象以该值为初值,否则所配置到内存内容为0。也就是说,如果我们有以下定义
(T*AQT6Q ?+K Int v1=1024; 5zZg7Pb,V_t:`
Int v2; 3CP)Bm6[ m[;Y
则v1和v2都被配置于data segment,v1值为1024,v2值为0。(雷神在VC6环境用MFC编程时中发现如果int v2;v2的值不为0,而是-8,不知为什么?编译器造成的?)。
"X+bbcvKu9g0XY 'B4w5k:mj,u
如果有一个全局对象,并且这个对象有构造函数和析构函数的话,它需要静态的初始化操作和内存释放工作,C++是一种跨平台的编程语言,因此它的编译器需要一种可以移植的静态初始化和内存释放的方法。下面便是它的策略。 }tE5b5T'P1yl/b
1、  为每一个需要静态初始化的档案产生一个_sit()函数,内带构造函数或内联的扩展。 hgFH0f3d
2、  为每一个需要静态的内存释放操作的文件中,产生一个_std()函数,内带析构函数或内联的扩展。 *uyF~\v^#jk4C!f8a
3、  提供一个_main()函数,用来调用所有的_sti()函数,还有一个exit()函数调用所有的_std()函数。 0Xai$\6`U
侯先生说:
W5}ZZ| c#f Sit可以理解成static initialization的缩写。 Y5Q l3[}i?T
Std可以理解成static deallocation的缩写。
+RKy#_5O PK l8d 那么main函数会被编译器变成这样:
-B"lFom!Rt)^4n1Tq Matrix identity; //一个全局对象
9qO$Pr}(@(Fjz z Main() u`Y0n)zQ/P.U
{ WTvcx+NtY
  _main();//对所有的全局对象做static initialization动作。 mY\%T-oU.Q"a
  Matrix m1=identity; H9~D4up/sy ~5h
  …… rAt?1~|*O3Q R(U
  exit();//对所有的全局对象做static deallocation动作。
Y VQdE Y } !NXj1]*C*h
其中_main()会有一个对identity对象的静态初始化的_sti函数,象下面伪码这样: /m^"}exC
// matrix_c是文件名编码_identity表示静态对象,这样能够保证向执行文件提供唯一的识别符号 +l6Q&?+V2V\`
_sti__matrix_c_identity() 8\bv)Y Q$_ kd,sqj
{
/^ [ u;ld&E   identity.Matrix:: Matrix(); //这就是静态初始化
)Z)qI)\.FAcf9y }
Lw\BH0Cz])V1_ 相应的在exit()函数也会有一个_std_matrix_c_identity(),来进行static deallocation动作。
^ Z!n,f*zt*x Pq 但是被静态初始化的对象有一些缺点,在使用异常时,对象不能被放置在try区段内。还有对象的相依顺序引出的复杂度,因此不建议使用需要静态初始化的全局对象。 Ym Cm G"l0F0S
;K&`*OXUl0PA+\
局部静态对象在C++底层机制是如何构造和在内存中销毁的呢? o f;G8R v&p0k4{lR
1、  导入一个临时对象用来保护局部静态对象的初始化操作。
?jdjY.o"iS 2、  第一次处理时,临时对象为false,于是构造函数被调用,然后临时对象被改为true.
2ZtG7Fe:U8n+pY3t 3、  临时对象的true或者false便成为了判断对象是否被构造的标准。 Rcf;td@
4、  根据判断的结果决定对象的析构函数是否执行。
U&E.C { g:i d
DFR? Z fw%E \ 如果一个类定义了构造函数或者析构函数,则当你定义了一个对象数组时,编译器会通过运行库将你的定义进行加工,例如:
x0]:r5G,w'{~h point knots[10]; //我们的定义
8o$Q*E_TU vec_new(&knots,sizeof(point),10,&point::point,0); //编译器调用vec_new()操作。 l|VR+R

z/z7C&Fu9zIT 下面给出vec_new()原型,不同的编译器会有差别。 trw;ZD
void * vec_new(
#E$P F8HE-k*A~ void *array, //数组的起始地址
^&DN%]!Mv'_x size_t elem_size,  //每个对象的大小
3f2oIU,~diR/C#Q int elem_count,  //数组元素个数
Z0Hb-a d cY9Y P void(*constructor)(void*), @\1R9xxz
void(*destructor)(void* ,char) %Qs&q*s)p+|~j[
)
'UH#~1K*j:`5s 对于明显获得初值的元素,vec_new()不再有必要,例如: 3EwZ_d-M A#_s:Y
point knots[10]={ k)zx Y Sn
Point(),    //knots[0]
V$J1l(`.J3Ju Point(1.0,1.0,0.5), //knots[1] *Lq F~h)s{
-1.0 //knots[2] C:Q5gM7i4a![#y+V
}; Sm#@4Nr7K
会被编译器转换成: ~5Jj;RXR
//C++伪码
(z Y-W+H po Point::Point(&knots[0]);
F kNn:b\8nS Point::Point(&knots[1],1.0,1.0,0.5);
xh-Np8F.^/y| Point::Point(&knots[2],-1.0,0.0,0.0);
^b"J3uRr-p p*bre vec_new(&knots,sizeof(point),10,&point::point,0); //剩下的元素,编译器调用vec_new()操作。 ^UD4fC[1F
怎么样,很神奇吧。 E!|-E2o HyK"z4A
LV\9Tr9}t|
当编译一个C++程序时,计算机的内存被分成了4个区域,一个包括程序的代码,一个包括所有的全局变量,一个是堆栈,还有一个是堆(heap),我们称堆是自由的内存区域,我们可以通过new和delete把对象放在这个区域。你可以在任何地方分配和释放自由存储区。但是要注意因为分配在堆中的对象没有作用域的限制,因此一旦new了它,必须delete它,否则程序将崩溃,这便是内存泄漏。(C#已经通过内存托管解决了这一令人头疼的问题)。C++通过new来分配内存,new的参数是一个表达式,该表达式返回需要分配的内存字节数,这是我以前掌握的关于new的知识,下面看看通过这本书,使我们能够更进一步的了解到些什么。 p Fs7R \V
Point3d *origin=new Point3d; //我们new 了一个Point3d对象
i i;J {gD@0n \ Z 编译器开始工作,上面的一行代码被转换成为下面的伪码:
R#Kp4t'b Point3d * origin; ]C0_4idP-W
If(origin=_new(sizeof(Point3d))) Y _5Zk3X1_p-A#p0Y
{
s^{B6K@B {   try{ n!vK c j]G S
    origin=Point3d::Point3d(origin); paN_ms
  } }Z2m6Q?;Y0FE vl5p
  catch(…){ .VNz0b.|;iRS&p
    _delete(origin); H,i_/q _
throw; 6{!Z i)|M |L(~
} 4i3~+Y-{:^
}
1S] k C$E(C 而delete origin;
+e#J?eU 会被转换成(雷神将书上的代码改为exception handling情况):
(}^I3Oi%R if(origin!=0){ {hI:irHha?!jvg
  try{ T.Y _ RV"K+m
    Point3d::~Point3d(origin);
C$z5omoD(P${c%Ws     _delete(origin); a8|B6dNRU6@xC
  catch(…){ kR#n u/E
    _delete(origin); //不知对否?
X ^T"Iz[!\c     throw; !igO6C][]%k2`:w&ZS
  }
yOKA0\g;A }
*L-ET!p YO(hok 一般来说对于new的操作都直截了当,但语言要求每一次对new的调用都必须传回一个唯一的指针,解决这个问题的办法是,传回一个指针指向一个默认为size=1的内存区块,实际上是以标准的C的malloc()来完成。同样delete也是由标准C的free()来完成。原来如此。 Nea@$RKRp\8y
r,l3m9r R4R
最后这篇笔记再说说临时对象的问题。 N(?"k:WN
T operator+(const T&,const T&); //如果我们有一个函数
.J'Ww \)B T a,b,c; //以及三个对象:
i$c \F%Xh c=a+b; K`+@:e6_ ?#Nhfac
//可能会导致临时对象产生。用来放置a+b的返回值。然后再由  T的copy constructor把临时对象当作c的初值。也有可能直接由拷贝构造将a+b的值放到c中,这时便不需要临时对象。另外还有一种可能通过操作符的重载定义,经named return value优化也可以获得c对象。这三种方法结果一样,区别在于初始化的成本。对临时对象书上有很好的总结:
}R|Ly 在某些环境下,有processor产生的临时对象是有必要的,也是比较方便的,这样的临时对象由编译器决定。 H!@(|9m{"Ey
临时对象的销毁应该是对完整表达式求值过程的最后一个步骤。 "Y ~(xNl
因为临时对象是根据执行期语义有条件的产生,因此它的生命规则就显得很复杂。C++标准要求凡含有表达式执行结果的临时对象,应该保留到对象的初始化操作完成为止。当然这样也会有例外,当一个临时对象被一个引用绑定时,对象将残留,直到被初始化的引用的生命结束,或者超出临时对象的作用域。

页: [1]

Powered by Discuz! Archiver 6.1.0  © 2001-2007 Comsenz Inc.