C/C++内存与运行时深入研究

  1. C/C++内存与运行时深入研究 [作者Jean.Love]    
  2. -----------------------------------------------------------------------------------  
  3. (一)整数符号的陷阱    
  4. (二)浮点数的本质    
  5. (三)堆栈的内存管理结构    
  6. (四)符号解析    
  7. (五)对齐和总线错误    
  8. (六)函数指针    
  9. (七)虚函数的实现机理    
  10. (八)引用的实现机理    
  11. (九)虚拟继承对象的内存结构    
  12. (十)混合编程时的初始化顺序    
  13. (十一)数组和指针的异同    
  14. (十二)const限定的传递性    
  15. (十三)数据类型的限定性检查    
  16. (十四)使用STL时的类型限制    
  17. (十五)迭代器自身的类型    
  18. (十六)运行时的类型信息    
  19. (十七)new/delete重载    
  20. (十八)如何拷贝一个文件    
  21.     
  22. (一)整数符号的陷阱    
  23.         x<y和x-y<0的结果是一样的吗??看看下面这个简单的程序。    
  24. #include<stdio.h>    
  25.     int main(void){    
  26.      int x=1;    
  27.      unsigned int y=2;    
  28.      int b=x<y;    
  29.      int b2=(x-y<0);    
  30.      printf("%d,%d/n",b,b2);    
  31.      return 0;    
  32. }    
  33. 它输出什么呢?    
  34. 1,0    
  35.     
  36.     令人震惊,不是吗,x<y和x-y<0不是一样的!    
  37. (1)x<y的时候,由于y是无符号数字,所以x也被提升为无符号数字,所有x<y是成立的,返回1    
  38. (2)x-y的结果计算的时候,返回一个0xfffffffe,它被当成无符号数字理解并和0比较,显然<0不成立,返回0。    
  39.     
  40. 总结一下,整数的运算,加减乘的时候,根本不管是否声明为是否有符号,在2进制cpu上面的计算是相同的,但是比较的时候(<,>,==)会根据类型,调用不同的比较指令,也就是以不同的方式来理解这个2进制结果。当signed和unsigned混用的时候,全部自动提升为无符号整数。    
  41. #include<stdio.h>    
  42.      int main(void){    
  43.      int i=-2;    
  44.      unsigned j=1;    
  45.      if(j+i>1) //提升为两个uint相加    
  46.           printf("sum=%d/n",j+i);//打印的结果根据%d制定,j+i的内存值永远不变。    
  47.      return 0;    
  48. }    
  49. 输出    
  50. > ./a.out    
  51. sum=-1    
  52.      
  53. 再举一个例子    
  54. #include<stdio.h>    
  55.      int main(void){    
  56.      int i=-4;    
  57.      unsigned int j=1;    
  58.      int ii=i+j;    
  59.      unsigned int jj=i+j;    
  60.      printf("%d,%ud/n",ii,jj);    
  61.      if(ii>1){printf("100000");}    
  62.      if(jj>1){printf("100001");}    
  63.      return 0;    
  64. }    
  65. 用gcc -S得到汇编,会发现if(ii>1)和if(jj>1)对应两个不同的跳转指令jle和jbe。    
  66.     
  67. 总结: int和unit在做比较操作和除法的时候不同,其他情况相同。    
  68. 返回页首    
  69.     
  70. (二)浮点数的本质    
  71.         用一个程序来说明浮点数的IEEE表示。注意Linux没有atoi,ltoi,itoa这样的函数,那几个函数是VC独家提供的,不是ANSI C标准,所以*nix要用到sprintf函数来打印整数的内容到字符串里面。IEEE浮点数对于32位的float来说,从高位到低位分别是1bit符号位,8bit指数位,23bit浮点数位。当然由于内存地址是从低到高排列的,所以要把这4个字节的内容反过来,作为整数,转换为字符串打印出来的内容才是正确的。在x86机器上,同样是低位字节在前高位字节在>后,这样做得好处就是可以把浮点数作为有符号整数来排序。    
  72.         例如浮点书-0.875,符号为1(复数),二进制表示为-0.111,表示为1-2之间的小鼠就是-1.11 x 2^-1,指数项-1,加上128得到1111111(127),因为指数项的8个bit必须保证是无符号数,所以有了这样的表示。而23bit的整数项则是11000000000000000000,也就是取了-1.11在小数点后面的内容,没有的后端补0。    
  73. 所以,-0.875f的2进制表示就是10111111011000000000000000000000。写一个小程序来验证    
  74. #include<stdio.h>    
  75. #include<stdlib.h>    
  76.      void pfloat(float f){    
  77.      int i,j;    
  78.      char buf[4][9];    
  79.      char* p=(char*)&f;    
  80.      printf("before loop/n");    
  81.      for(i=0;i<4;++i){    
  82.           for(j=0;j<8;++j){    
  83.                buf[i][j]=(p[i]&(0x80>>j))>0?'1':'0';    
  84.           }    
  85.           buf[i][8]='/0';    
  86.      }    
  87.      for(i=3;i>=0;i--){    
  88.           printf("%s",buf[i]);    
  89.      }    
  90.      printf("/n");    
  91.      printf("end loop/n");    
  92. }    
  93. int main(void){    
  94.      float d1=-0.875;    
  95.      pfloat(d1);    
  96.      return 0;    
  97. }    
  98.         看看输出和我们预期的一致。浮点数的计算总是充满了陷阱。首先,因为浮点数的精度有限,所以在做四则运算的时候,低位很可能在过程中被舍弃。因此,浮点运算不存在严格的运>算的结合律。在32位系统上面,浮点数float为4字节长,其中整数位23位,表示范围转换为10位数的话有9个有效数字。所以    
  99.   float f1=3.14;    
  100.   float f2=1e20;    
  101.   float f3=-1e20;    
  102.   printf("%d,%f/n",i,f);    
  103.   printf("%f/n",f1+f2+f3);    
  104.   printf("%f/n",f2+f3+f1);    
  105.     
  106.     上面两个printf的结果是不一样的,第一个结果是0,第二个结果是3.14。再举一个例子    
  107.   float k=1.3456789;    
  108.   float k2=k;    
  109.   k-=1000000.0;    
  110.   printf("%f/n",k);    
  111.   k+=1000000.0;    
  112.   printf("%f/n",k);    
  113.   int b=(k==k2);    
  114.   printf("%d/n",b);    
  115.     结果是什么呢? b=0,因为k的值在之前的运算中,小数点后面已经有5为被舍入了,所以k不再等于k2。要使得k==k2成立,必须提高京都,使用double--52位整数域,相当于10进制有效数字16位,可以克服上面这个运算的不精确性。    
  116.   double d1,d2;    
  117.   printf("%f/n",d1);    
  118.   d1=d2=1.3456789;    
  119.   d2+=1000000.0;    
  120.   printf("%f/n",d2);    
  121.   d2-=1000000.0;    
  122.   printf("%f/n",d2);    
  123.         现在d==d2的返回值就是真了。为了使得运算结果有可以比较的意义,通常定义一个门限值。#define fequals(a,b) fabs(a-b)<0.01f    
  124.         如果浮点数计算溢出,printf能够输出适当的表示    
  125.   float nan=3.0f/0.0f;    
  126.   printf("%f/n",nan);    
  127. 打印inf,如果结果是负无穷大,打印-inf。    
  128. 返回页首    
  129.     
  130. (三)堆栈的内存管理结构    
  131.         堆和栈的内存管理(x86机器)与分布是什么样子的?用一个程序来说明问题。看看堆和栈的空间是怎么增长的。    
  132. $ cat stk.c    
  133. #include<stdio.h>    
  134. #include<stdlib.h>    
  135. int main(void){    
  136.      int x=0;    
  137.      int y=0;    
  138.      int z=0;    
  139.      int *p=&y;    
  140.      *(p+1)=2;//这条语句究竟是设置了x还是设置了z?和机器的cpu体系结构有关    
  141.      int* px=(int*)malloc(sizeof(int));    
  142.      int* py=(int*)malloc(sizeof(int));    
  143.      int* pz=(int*)malloc(sizeof(int));    
  144.      *px=1;    
  145.      *py=1;    
  146.      *pz=1;    
  147.      *(py+1)=3;    
  148.      printf("%d,%d,%d/n",x,y,z);    
  149.      printf("%p,%p,%p/n",px,py,pz);    
  150.      printf("%d,%d,%d/n",*px,*py,*pz);    
  151.      free(px);    
  152.      free(py);    
  153.      free(pz);    
  154.      return 0;    
  155. }    
  156. 编译和运行的结果    
  157. $ gcc stk.c && ./a.out    
  158. 2,0,0    
  159. 0x9e8b008,0x9e8b018,0x9e8b028    
  160. 1,1,1    
  161.     
  162. (1)如果把上面的分配内存的代码改成    
  163. int* px=(int*)malloc(sizeof(int)*3);    
  164. int* py=(int*)malloc(sizeof(int)*3);    
  165. int* pz=(int*)malloc(sizeof(int)*3);    
  166. 第三个printf的输出仍然是    
  167. 0x9e8b008,0x9e8b018,0x9e8b028    
  168.         说明什么呢? malloc分配的时候,分配的大小总是会比需要的大一些,也就是稍微有一些不大的内存越界并不会引起程序崩溃。当然这种情况可能导致得不到正确的结果。    
  169.     
  170.         我们看看堆和栈的内存分布吧,在一台安装了Linux的x86机器上    
  171. ---------------------    
  172. 0xffffffff    
  173. ->OS内核代码,占据1/4的内存地址空间    
  174. 0xc000000    
  175. ->stack是运行时的用户栈,地址从高往低增长    
  176. |     x    
  177. |     y           ->int*(&y)+1指向的就是x    
  178. |     z    
  179.     
  180. ->共享库的存储器映射区域    
  181. 0x40000000    
  182. ->运行时堆,往上增长    
  183. |    pz    
  184. 。。。。。。    
  185. |    py           ->由于py分配的内存大于实际想要的, *(py+1)=3;不对程序结果有影响    
  186. 。。。。。。    
  187. |    px           ->malloc分配的内存从低往高分配    
  188. 。。。。。。    
  189. ->可读写数据区(全局变量等)    
  190. ->只读代的代码和数据(可执行文件,字面常量等)    
  191. 0x08048000        ->是的,代码总是从同一地址空间开始的    
  192. ->未使用    
  193. 0x00000000    
  194. ---------------------    
  195.     
  196. 如果把程序改为 *(py+4)=3;    
  197. 那么程序最好一行的输出就是    
  198. 1,1,3    
  199. 也就是pz的内容被写入。验证了理论。    
  200. 返回页首    
  201.     
  202. (四)符号解析    
  203.         符号是怎么被解析的?什么时候会有符号解析的冲突?假设两个模块里面都有全局变量    
  204. $ cat f.c    
  205. #include<stdio.h>    
  206. int i=0;    
  207. void f()  
  208. {    
  209.      printf("%d/n",i);    
  210. }    
  211. $ cat m.c    
  212. int i=3;    
  213. extern void f();    
  214. int main(void){    
  215.      f();    
  216.      return 0;    
  217. }    
  218. 这样的话,编译和链接会有错误:    
  219. $ gcc -o main m.o f.o    
  220. f.o:(.bss+0x0): multiple definition of 'i'  
  221. m.o:(.data+0x0): first defined here    
  222. collect2: ld 返回 1    
  223. 也就是说,我们定义了重名的全局变量i,那么链接器就不知道应该用哪个i了,用nm可以看到符号表:    
  224. $ nm m.o f.o    
  225.     
  226. m.o:    
  227.          U f    
  228. 00000000 D i    
  229. 00000000 T main    
  230.     
  231. f.o:    
  232. 00000000 T f    
  233. 00000000 B i    
  234.          U printf    
  235.     
  236.     
  237. 解决方法有两种:    
  238. 1. 在m.c里面把int i=3变成main内部的局部变量,这样的话:    
  239. $ cat mcp.c    
  240. extern void f();    
  241. int main(void){    
  242.  int i=3;    
  243.  f();    
  244.  return 0;    
  245. }    
  246. [zhang@localhost kg]$ nm mcp.o    
  247.          U f    
  248. 00000000 T main    
  249.     
  250. 在文件m.o中没有了全局符号i,链接就没有了错误。    
  251.     
  252. 2.在f.c中把int i从全局变量变成static静态变量,使得它只在当前文件中可见    
  253. $ cat fcp.c    
  254. #include<stdio.h>    
  255. static int i=0;    
  256. void f(){    
  257.   printf("%d/n",i);    
  258. }    
  259. [zhang@localhost kg]$ nm fcp.o    
  260. 00000000 T f    
  261. 00000000 b i     ->这里i的类型从以前的B变成了b    
  262.          U printf    
  263.     
  264. main的执行结果是0,也就是f里面的i就是当前文件的i,不会使用m.c中定义的全局i。这两个i由于不冲突,就被定义在不同的地址上面了。    
  265. 返回页首    
  266.     
  267. (五)对齐和总线错误    
  268.         什么是Bus error? 一般是总线寻址造成的,由于指针类型和long有相同大小,cpu总是找到%4/%8的地址作为指针的起始地址,例如:    
  269.    
  270. #include<stdio.h>    
  271. int main(void){    
  272. char buf[8]={'a','b','c','d','e','f'};    
  273. char *pb=&(buf[1]);     //这里pb的地址不是4bytes或8bytes对齐的,而是从一个奇数地址开始    
  274. int *pi=(int*)pb;    
  275. printf("%d/n",*pi);    
  276. return 0;    
  277. }    
  278.      
  279.        这类问题的结果和CPU的体系结构有关,取决于CPU寻址的时候能否自动处理不对齐的情况。下面这个小程序是一个例子。分别在 Sparc(solaris+CC)和x86(vc6.0)上面测试: Sparc上面就会崩溃(Bus error (core dumped)),x86就没有问题。    
  280.        Plus: 在hp的pa-risc(aCC),itanium(aCC),IBM(xlC)的power上面测试    
  281. power不会core dump, pa-risc和Itanium也均core dump.    
  282. 返回页首    
  283.     
  284. (六)函数指针    
  285.        要控制函数的行为,可以为函数传入一个回调函数作为参数。C++的STL使用的是functional算子对象,C语言可以传递一个函数或者一个函数指针。    
  286. #include <stdio.h>    
  287. #include <stdlib.h>    
  288. typedef void callback(int i);    
  289. void p(int i){printf("function p/n");}    
  290. void f(int i,callback c){c(i);}    
  291. int main(void)    
  292. {    
  293.  f(20,p);    
  294.  return 0;    
  295. }    
  296. > ./a.out    
  297. function p    
  298.     既然可以把函数直接作为回调参数传给另一个主函数,为什么还要用函数指针呢? 相像一下f函数运行在一个后台线程里面,这个线程是个服务器不能被停止,那么我们想要动态改变f的行为就不可能了,除非f的第二个参数是 callback* 而传入的这个变量我们去另一个线程里面改变。这样就实现了灵活性。    
  299. 返回页首    
  300.     
  301. (七)虚函数的实现机理    
  302.     因为C++里面有指针,所以所谓的publicprivate在强类型转换面前没有意义。我们总是可以拿到私有的成员变量。 winXP+gcc3.4.2得到的虚函数表最后一项是0,是个结束符。注意,这是严重依赖编译器的,C++标准甚至都没要求是要用虚函数表来实现虚函数机制。    
  303. /*----------------------------------------------------------------------------*/    
  304. #include<stdio.h>    
  305. class B{    
  306.       int x;    
  307.       virtual void f(){printf("f/n");}    
  308.       virtual void g(){printf("g/n");}    
  309.       virtual void h(){printf("h/n");}    
  310. public:    
  311.       explicit B(int i) {x=i;}    
  312. };    
  313. typedef void (*pf)();    
  314. int main(void){    
  315.   B b(20);    
  316.   int * pb=(int*)&b;    
  317.   printf("private x=%d/n",pb[1]);    
  318.   pf *pvt=(pf*)pb[0];//虚函数表指针    
  319.   pf f1=(pf)pvt[0];    
  320.   pf f2=(pf)pvt[1];    
  321.   pf f3=(pf)pvt[2];    
  322.   (*f1)();    
  323.   (*f2)();    
  324.   (*f3)();    
  325.   printf("pvt[3]=%d/n",pvt[3]);//虚函数表结束符号     
  326.   return 0;    
  327. }    
  328.     
  329. 程序输出    
  330. private x=20    
  331. f    
  332. g    
  333. h    
  334. pvt[3]=0    
  335.      
  336.     理解的关键是,b的第一个dword,里面保存了一个指针,指向虚函数表。我们用两次强制转型,一次得到b的第一个dword,在把这个dword转为    
  337.     当然,上面的这个结果是和编译器类型以及版本有关系的,gcc2.95.2版本对象的结构就不同,它把虚函数表指针放到了对象的后面,也就是pvt= ((int*)(&b))[1]才是指针域,而且pvt[0]=0是结束符,pvt[1]才是第一个虚函数的起始地址。所以这样写出来的程序是不通用的。同一台机器上,不同的编译器来编上面那个程序,有的能工作,有的coredump。因为C++对象的内存模型不是C++标准的一部分,可以有不同的实现,不同实现编出来的结果(和虚函数有关的)互相之间没有任何通用性。    
  338.      
  339.     如果有访问对象的成员呢? 情况更复杂。    
  340. #include<string>    
  341. using namespace std;    
  342. struct a{    
  343.  int x;    
  344.  virtual void f(){printf("f(),%d/n",x);}    
  345.  explicit a(int xx){x=xx;}    
  346. };    
  347. int main(void){    
  348.   a a1(2);    
  349.   a a2(3);    
  350.   int* pi=(int*)&a1;    
  351.   int* pvt=(int*)pi[0];    
  352.   typedef void(*pf)();    
  353.   pf p=(pf)pvt[0];    
  354.   (*p)();    
  355.   int *p2=(int*)&a2;    
  356.   int *pv2=(int*)p2[0];    
  357.   pf px=(pf)pv2[0];    
  358.   (*px)();    
  359.   return 0;    
  360. }    
  361. 输出是什么呢?    
  362. $ g++ r.cpp &&./a.out    
  363. f(),3    
  364. f(),3    
  365.    为什么会有这样的错误? 因为成员函数在传递参数的时候默认含有一个this指针,但是我这里的简单调用并没有去指定this指针,所以程序没有挂掉就已经很幸运了。怎么才能得到正确的结果呢? 像下面这样增加一个this类型的调用参数:    
  366. #include<stdio.h>    
  367. struct a{    
  368. int x;    
  369. virtual void f(){printf("f(),%d/n",x);}//............    
  370. explicit a(int xx){x=xx;}    
  371. };    
  372. int main(void){    
  373.   a a1(2);    
  374.   a a2(3);    
  375.   int* pi=(int*)&a1;    
  376.   int* pvt=(int*)pi[0];    
  377.   typedef void(*pf)(a*);    
  378.   pf p=(pf)pvt[0];    
  379.   (*p)(&a1);    
  380.   int *p2=(int*)&a2;    
  381.   int *pv2=(int*)p2[0];    
  382.   pf px=(pf)pv2[0];    
  383.   (*px)(&a2);    
  384.   return 0;    
  385. }    
  386. > g++ p.cpp && ./a.out    
  387. f(),2    
  388. f(),3    
  389. 现在结果就正确了。    
  390.      
  391.      
  392.     再次说明,this指针的传递方法在C++标准里面并没有说明,而是各家编译器各自实现。这里引用OwnWaterloo的一段解释性代码,说明问题。    
  393.      
  394. (1)gcc3.4.x 是通过给参数列表增添一个隐藏参数, 来传递this的, 代码 :    
  395.     
  396. /*----------------------------------------------------------------------------*/    
  397. class C {    
  398.     int i_;    
  399. public:    
  400.     explicit C(int i) :i_(i) {}    
  401.     virtual ~C() {}    
  402.     virtual void f() { printf("C::f(%d)/n",i_); }    
  403. };    
  404.    
  405. #if defined(__GNUC__)    
  406. #if __GNUC__!=3    
  407. #error not test on other gcc version except gcc3.4    
  408. #endif    
  409. #include <assert.h>    
  410. #include <string.h>    
  411. #include <stdio.h>    
  412. #define intprt_t int*    
  413. int main()    
  414. {    
  415.     C c1(1212);    
  416.     C c2(326);    
  417.     
  418.     typedef void (* virtual_function)(C*);    
  419.     // gcc 通过一个增加一个额外参数, 传递this    
  420.     // virtual_function 即是C的虚函数签名    
  421.     
  422.     struct    
  423.     {    
  424.         virtual_function* vptr;    
  425.         // 虚函数表指针    
  426.         // 当然,它指向的表不全是函数, 还有RTTI信息    
  427.         // 总之, 它就是这个类的标识, 唯一的“类型域”    
  428.     
  429.         int i;    
  430.         // data member    
  431.     } caster;    
  432.     // 我们猜想, gcc将虚函数表指针安排在对象的最前面。    
  433.     
  434.     memcpy(&caster,&c1,sizeof(caster));    
  435.     printf("c1.i_ = %d/n",caster.i); // 1212    
  436.     printf("c1.vptr_ = %p/n"    
  437.         ,reinterpret_cast<void*>(reinterpret_cast<intptr_t>(caster.vptr)) );    
  438.     virtual_function* vptr1 = caster.vptr;    
  439.     
  440.     memcpy(&caster,&c2,sizeof(caster));    
  441.     printf("c2.i_ = %d/n",caster.i);    
  442.     printf("c2.vptr_ = %p/n",(void*)caster.vptr);    
  443.     virtual_function* vptr2 = caster.vptr;    
  444.        
  445.     assert(vptr1==vptr2);    
  446.     // 显然, 它们都是C, 所以vptr指向相同的地址    
  447.     
  448.     vptr1[2](&c1); // C::f(1212)    
  449.     vptr2[2](&c2); // C::f(326)    
  450.     /* 我们再猜想 f在虚函数表中的第2项。这里的~C是虚函数表第1项。*/    
  451.     /* 在存在有虚析构函数的时候,虚表的第0项似乎只是个导引。如果把~C去掉改为别的虚函数,那么f就是虚表的第1项。*/    
  452. }    
  453. (2)MSVC使用另一种实现    
  454. int main()    
  455. {    
  456.     C c1(1212);    
  457.     C c2(326);    
  458.     typedef void (__stdcall* virtual_function)(void);    
  459.     // msvc 通过ecx传递this, 所以参数列表和虚函数相同    
  460.     // 同时, msvc生成的虚函数, 会平衡堆栈    
  461.     // 所以这里使用 __stdcall  让调用者不做堆栈的平衡工作    
  462.     
  463.     struct {    
  464.         virtual_function* vptr;    
  465.         int i;    
  466.     } caster;    
  467.     // 这同样是对编译器生成代码的一种假设和依赖    
  468.     
  469.     memcpy(&caster,&c1,sizeof(caster));    
  470.     printf("c1.i_ = %d/n",caster.i);    // 1212    
  471.     virtual_function* vptr1 = caster.vptr;    
  472.     printf("c1.vptr_ = %p/n"    
  473.         ,reinterpret_cast<void*>(reinterpret_cast<ptrdiff_t>(vptr1)) );    
  474.     
  475.     memcpy(&caster,&c2,sizeof(caster));    
  476.     printf("c2.i_ = %d/n",caster.i);    // 326    
  477.     virtual_function* vptr2 = caster.vptr;    
  478.     printf("c2.vptr_ = %p/n"    
  479.         ,reinterpret_cast<void*>(reinterpret_cast<ptrdiff_t>(vptr2)) );    
  480.     assert(vptr1==vptr2);    
  481.     // 显然 c1 c2 都是 C,它们的虚指针是相同的    
  482.         
  483.     // 但是, 直接调用是不行的, 因为没传递this    
  484.     //vptr1[2]();    
  485.     
  486.     // 这样也不行    
  487.     //_asm { lea ecx, c1 }    
  488.     // 因为下面这行代码, 修改了 ecx    
  489.     // vptr1[2]();    
  490.     
  491.     // 所以要如下进行直接调用    
  492.     virtual_function f1 = vptr1[1];    
  493.     _asm {    
  494.         lea ecx,c1    
  495.         call f1    
  496.     }    
  497.     virtual_function f2 = vptr2[1];    
  498.     _asm {    
  499.         lea ecx,c2    
  500.         call f2    
  501.     }    
  502.     // 分别打印出 C::f(1212),C::f(326)    
  503.     // 同时, C::f在虚表的第1项, vs的watch窗口说的 ……    
  504. }    
  505. 返回页首    
  506.     
  507. (八)引用的实现机理    
  508.         引用的工作方式是什么呢 不纠缠于语法的解释,看代码和汇编结果最直接。举下面这个小例子程序:(gcc -masm=hello -S main.cpp可以得到汇编代码)    
  509. #include<stdio.h>    
  510. int x=3;    
  511. int f1(){return x;}    
  512. int& f2(){return x;}    
  513. int main(){    
  514. int a=f1();    
  515. int y=f2();    
  516. y=4;//仍然有x=3    
  517. int&z=f2();    
  518. z=5;    
  519. printf("x=%d,y=%d",x,y);//z改变了x    
  520. return 0;    
  521. }    
  522. 输出是什么呢? x=5,y=4    
  523. 分析:    
  524. f2是个返回引用的函数,当且仅当int&z =f2()的时候才是真的返回引用,int y=f2()返回的仍然是一个值的拷贝。汇编代码如下(部分)    
  525.     
  526. -----------------------------------------------------------------------------------    
  527. f1和f2的定义:    
  528. .globl __Z2f1v    
  529.  .def __Z2f1v; .scl 2; .type 32; .endef    
  530. __Z2f1v:    
  531.  push ebp    
  532.  mov ebp, esp    
  533.  mov eax, DWORD PTR _x              f1()返回一个值的拷贝    
  534.  pop ebp    
  535.  ret    
  536.  .align 2    
  537. .globl __Z2f2v    
  538.  .def __Z2f2v; .scl 2; .type 32; .endef    
  539. __Z2f2v:    
  540.  push ebp    
  541.  mov ebp, esp    
  542.  mov eax, OFFSET FLAT:_x             f2()返回的就是一个地址,不是值    
  543.  pop ebp    
  544.  ret    
  545.  .def ___main; .scl 2; .type 32; .endef    
  546.  .section .rdata,"dr"    
  547. 我们看一下main函数    
  548. _main:    
  549.  push ebp    
  550.  mov ebp, esp    
  551.  sub esp, 40    
  552.  and esp, -16    
  553.  mov eax, 0    
  554.  add eax, 15    
  555.  add eax, 15    
  556.  shr eax, 4    
  557.  sal eax, 4    
  558.  mov DWORD PTR [ebp-16], eax    
  559.  mov eax, DWORD PTR [ebp-16]    
  560.  call __alloca    
  561.  call ___main    
  562.  call __Z2f1v                                 -> 调用f1(), 返回值放在eax    
  563.  mov DWORD PTR [ebp-4], eax      -> eax赋值给a    
  564.  call __Z2f2v    
  565.  mov eax, DWORD PTR [eax]         -> 调用f2(), 返回x的值拷贝放在eax    
  566.  mov DWORD PTR [ebp-8], eax     ->  eax赋值给y    
  567.  mov DWORD PTR [ebp-8], 4         ->  立即数"4"赋值给y. y的改变不会改变x!!!!!!    
  568.  call __Z2f2v    
  569.  mov DWORD PTR [ebp-12], eax   -> 调用f2(), 返回x的地址给z    
  570.  mov eax, DWORD PTR [ebp-12]   -> x的地址放入eax    
  571.  mov DWORD PTR [eax], 5            -> 赋值5给eax指向的地址x    
  572.  mov eax, DWORD PTR [ebp-8]    //以下是printf的调用    
  573.  mov DWORD PTR [esp+8], eax    
  574.  mov eax, DWORD PTR _x    
  575.  mov DWORD PTR [esp+4], eax    
  576.  mov DWORD PTR [esp], OFFSET FLAT:LC0    
  577.  call _printf    
  578.  mov eax, 0    
  579.  leave    
  580.  ret    
  581.  .def _printf; .scl 2; .type 32; .endef    
  582. 返回页首    
  583.     
  584. (九)虚拟继承有什么样子的内存模型    
  585.         研究了一下虚拟继承时,对象的内存分布模型,写了下面这个小程序    
  586. #include<stdio.h>    
  587. struct A {int x;int y; };    
  588. struct B : virtual public A {    
  589. int a;    
  590. B(){x=1;y=2;a=55;}    
  591. };    
  592. struct C : virtual public A {    
  593. int b;    
  594. C(){x=3;y=4;b=66;}    
  595. };    
  596. struct D : public B, public C { };    
  597. int main(void) {    
  598. A a;    
  599. B b;    
  600. C c;    
  601. D d;    
  602. D *pd = &d;    
  603. C *pd_c =(C*)(&d);    
  604. B *pd_b =(B*)(&d);    
  605. A *pd_a =(A*)(&d);    
  606. printf("%d,%d,%d,%d/n",sizeof(a),sizeof(b),sizeof(c),sizeof(d));    
  607. printf("%p,%p,%p,%p/n",pd,pd_c,pd_b,pd_a);    
  608. int *pd2=(int*)pd;    
  609. printf("%p,%d,%p,%d,%d,%d/n",**((int**)(pd2)),*(pd2+1),**((int**)(pd2+2)),*(pd2+3),*(pd2+4),*(pd2+5));    
  610. return 0;    
  611. }    
  612. 输出    
  613. 8,16,16,24    
  614. 0022FF20,0022FF28,0022FF20,0022FF30    
  615. 00000008,55,00000000,66,3,4    
  616.     
  617.         结论:D的内存分布像是这样(堆栈从高到低),vbptr表示虚基类量偏移指针    
  618. |A.y|    
  619. |A.x|    
  620. |C.b|    
  621. |C.vbptr|    
  622. |B.a|    
  623. |B.vbptr|    
  624. 其中bvptr是virtual public类型的对象中,虚基类的偏移量。这里C.vbptr=0,B.vbptr=8.对于d来说,C::C()在B::B()之后调用,所以(x,y)=(3,4)    
  625. 因此按顺序输出D的内存内容就得到(8,55,0,66,3,4)    
  626. 返回页首    
  627.     
  628. (十)混合编程时的初始化顺序    
  629. (1)ctor,dtor和atexit的调用顺序    
  630. #include<stdio.h>    
  631. #include<stdlib.h>    
  632. class a{    
  633.   int ii;    
  634. public:    
  635.   explicit a(int i){    
  636.     ++count;    
  637.     ii=i;    
  638.     printf("ctor i=%d/n",ii);    
  639.     atexit(f);    
  640.   }    
  641.   ~a(){printf("dtor i=%d/n",ii);}    
  642.   static void f(){printf("f() count=%d/n",count);}    
  643.   static int count;    
  644. };    
  645. int a::count=0;    
  646. void g(){    
  647.   a a2(2);//注意,如果a对象声明在一个循环中,那么循环执行N次a的构造函数就会调用N次!!    
  648.   printf("after g() a ctor/n");    
  649. }    
  650. a a3(3);//最外层的对象    
  651. int main(void){    
  652.   a a1(1);//次外层的对象    
  653.   atexit(g);    
  654.   return 0;    
  655. }    
  656. 运行输出    
  657. ./a.out    
  658. ctor i=3    
  659. ctor i=1    
  660. dtor i=1    
  661. ctor i=2    
  662. after g() a ctor    
  663. dtor i=2    
  664. f() count=3    
  665. f() count=3    
  666. dtor i=3    
  667. f() count=3    
  668.     
  669. (2)一个程序本质上都是由 bss段、data段、text段三个组成的。这样的概念,不知道最初来源于哪里的规定,但在当前的计算机程序设计中是很重要的一个基本概念。而且在嵌入式系统的设计中也非常重要,牵涉到嵌入式系统运行时的内存大小分配,存储单元占用空间大小的问题。    
  670.     
  671.     在采用段式内存管理的架构中(比如intel的80x86系统),bss段(Block Started by Symbol segment)通常是指用来存放程序中未初始化的全局变量的一块内存区域,一般在初始化时bss 段部分将会清零。bss段属于静态内存分配,即程序一开始就将其清零了。    
  672.     
  673.     比如,在C语言之类的程序编译完成之后,已初始化的全局变量保存在.data 段中,未初始化的全局变量保存在.bss 段中。    
  674.     在《Programming ground up》里对.bss的解释为:There is another section called the .bss. This section is like the data section, except that it doesn’t take up space in the executable.    
  675.     text和data段都在可执行文件中(在嵌入式系统里一般是固化在镜像文件中),由系统从可执行文件中加载;而bss段不在可执行文件中,由系统初始化。    
  676.      
  677. 例子: (windows+cl)    
  678. 程序1:    
  679. int ar[30000];    
  680. void main()    ......    
  681.     
  682. 程序2:    
  683. int ar[300000] =  {1, 2, 3, 4, 5, 6 };    
  684. void main()    ......    
  685.     
  686. 发现程序2编译之后所得的.exe文件比程序1的要大得多。发现在程序1.asm中ar的定义如下:    
  687. _BSS SEGMENT    
  688.      ?ar@@3PAHA DD 0493e0H DUP (?)    ; ar    
  689. _BSS ENDS    
  690. 而在程序2.asm中,ar被定义为:    
  691. _DATA SEGMENT    
  692.      ?ar@@3PAHA DD 01H     ; ar    
  693.                 DD 02H    
  694.                 DD 03H    
  695.                 ORG $+1199988    
  696. _DATA ENDS    
  697.     
  698. 区别很明显,一个位于.bss段,而另一个位于.data段,两者的区别在于:全局的未初始化变量存在于.bss段中,具体体现为一个占位符;全局的已初始化变量存于.data段中;而函数内的自动变量(每个编译器都不同,cl是0xCCCCCCCC)都在栈上分配空间。.bss是不占用.exe文件空间的,其内容由操作系统初始化(清零);而.data却需要占用,其内容由程序初始化,因此造成了上述情况。    
  699.      
  700. (3)例子:一个很特殊的strcpy例子,可以让程序崩溃的:    
  701. #include<stdio.h>    
  702. #include<string.h>    
  703. #include<ctype.h>    
  704. void f(char* s){    
  705.     int len=strlen(s);    
  706.     char buf[len+1];    
  707.     strcpy(buf,s);    
  708.     printf("s=%s,buf=%s/n",s,buf);    
  709.     strcpy(s,buf);    
  710.     printf("after strncpy/n");    
  711. }    
  712. int main(void){    
  713.         f("abc");    
  714.         return 0;    
  715. }    
  716. ./a.out    
  717. s=abc,buf=abc    
  718. 段错误    
  719. 如果我把main函数的内容改为    
  720.         char b[]="abc";//堆栈分配    
  721.         f(b);    
  722. 运行就没有问题。    
  723.      
  724. 原因: “abc”是存在只读属性数据区,不能做strcpy的目的地    
  725. 数组内存分配在栈上,可作修改,所以数组名可以做strcpy的第一个参数。    
  726.      
  727. (4)如果循环里面要用到某个类对象(默认构造函数),最好把对象的声明移动到循环外面,否则这个对象被初始化的次数就是循环的次数    
  728.    
  729. #include<stdio.h>    
  730. class c{    
  731. public:    
  732.   c(){printf("ctor/n");}    
  733. };    
  734. int main(void){    
  735.   int i=10;    
  736.   while(i--){    
  737.     c c1;    
  738.   }    
  739.   return 0;    
  740. }    
  741. 运行结果就是"ctor"被打印10次    
  742. 返回页首    
  743.     
  744. (十一)数组和指针的异同    
  745.     这个是C/C++中最容易混淆,最容易头晕的一个话题。    
  746.      
  747.     我们先从一个简单的例子看起(一维数组)    
  748. void f(char* buf);    |         void f(char* buf);    
  749. int main(...){        |         int main(...){    
  750. char buf[]="abc";       |         char* pbuf="abc";    
  751. f(buf);               |         f(pbuf);            ->相同的生成代码    
  752. buf[2]='x';           |         pbuf[2]='x'         ->不同的生成代码    
  753.      
  754. 上面这两个程序有区别吗? 答案是:    
  755. (1)对于一维数组的处理,传递参数的地时候统统作为指针来看待,也就是f(buf)的调用被编译器等效成了    
  756. char* pbuf="abc",f(pbuf)这样的调用。    
  757. (2)对于寻址和赋值:    
  758. buf[2] 是编译器计算(buf的地址+2),放入x    
  759. pbuf[2]是编译器计算pbuf的地址,得到pbuf中的值,再以这个值为基地址,+2,放入x    
  760. 也就是说,pbuf的赋值语句是2次跳转的,效率比不上buf[2]这样的语句。    
  761. --------------------------------------------------------------    
  762.    考虑复杂一点的情况,**数组怎么办?    
  763. int main(...){    
  764.    int buf[2][3];//这个buf数组在内存中仍然是1维连续内存!    
  765.      
  766. 那么buf[10][10]=6;这样的语句是如何计算的呢? buf的结构被看成一个矩阵被拉直为行向量的表示,10行10列,buf[1][2]的地址就是:    
  767.    第二行的起始(1*10)+第3个元素的偏移(2),等效于((int*)(buf))[12]。    
  768.    这样说很清楚了吧,如果我们要把buf传递给一个函数作为参数,怎么办呢? 只需要保证编译器能看出,这个被拉直的,2维数组,每一行多少个元素:    
  769.    void f(int buf[][10]){    
  770.      buf[1][2]=6;//编译器能够通过f的形式参数声明来决定buf[1][2]是从buf偏移多少。    
  771.    ...    
  772. 上面这个声明和void f(int buf[10][10])甚至void f(int buf[20][10])是等效的。因为我们只需要知道每行包括多少个元素,至于有多少行,(数组多大),不是编译器控制的,是程序元的责任。    
  773.      
  774. --------------------------------------------------------------    
  775.     如果f的声明是f(int buf[][])呢? 它等效于f(int *buf[])或者f(int ** ppbuf)这样的声明,传入参数必须是一个真正的2维数组。像下面这样    
  776.     int** buf=new int*[10];    
  777.     for(int i=0;i<10;++i)buf[i]=new int[10];    
  778.     f(buf);    
  779.     buf数组本身必须是一个指针数组,buf[1][2]这样的计算是:    
  780. (a)计算buf[1]的值    
  781. (b)这个值是一个地址,指向一个数组,取这个数组的偏移量2中的值。    
  782.      
  783. 如果我混用f(int buf[][10])和f(int buf[][]),我就会得到一个编译警告或者错误:    
  784. void f2(int ppi[][2]){}    
  785. void f3(int *ppi[2]){}    
  786. int p2[3][2]={ {1,2},{3,4}, };    
  787. f2(p2);正确的用法    
  788. f3(p2);警告:传递参数 1 (属于 ‘f3’)时在不兼容的指针类型间转换。    
  789.      
  790.    由于f3的生成代码是2次跳转,因此传入p2作为参数的时候,会把一个真正的数组元素的值作为地址看待,再次计算一个内存地址偏移量中的值,可能导致程序崩溃。    
  791.    再看一个程序,看看运行的结果是什么。    
  792. int main(void)    
  793. {    
  794.     int arr[2][3] = {    
  795.         {0,1,3},    
  796.         {4,5,6}    
  797.     };    
  798.     int i1=(int)arr;    
  799.     int i2=(int)(arr+1);    
  800.     printf("i2-i1=%d/n",i2-i1);    
  801.     printf("%x/n",arr+1);    
  802.     printf("%x/n",*(arr+1));    
  803.     printf("%d/n",**(arr+1)));    
  804.     return 0;    
  805. }    
  806.         关于这个话题,最好的相关参考文献:《C专家编程》    
  807. 返回页首    
  808.     
  809. (十二)const限定的传递性    
  810. (1)如何理解复杂const的指针类型定义?    
  811. char  * const cp; ( * 读成 pointer to ) 等效于const char* p    
  812. cp is a const pointer to char    
  813.     
  814. const char * p;    
  815. p is a pointer to const char;    
  816. 先向右看, 再向左看, thinking in C++ 说的很清楚了    
  817.     
  818. (2)const对于函数声明:    
  819. 是个很严格的概念,const对象被调的过程必须保证其使用了带const的函数。例如:    
  820. > cat t.cpp    
  821. struct a{    
  822.        int x;    
  823.        bool operator==(const a& ia){return x==ia.x;}//这里是编译不过的!!!!!!!!    
  824. };    
  825. bool f(const a& ia, const a& ib){    
  826.      return ia==ib;//因为这里的==操作了const a&,而operator==没有被定义为const函数    
  827. }    
  828. int main(int argc, char *argv[]){    
  829.     return 0;    
  830. }    
  831. 问题解决的方案:    
  832. bool operator==(const a& ia) const {return x==ia.x;}    
  833. f中被比较的a类对象是const的,传递给operator==函数,函数不能改变它,因此==必须也是const的。    
  834. 返回页首    
  835.     
  836. (十三)数据类型的限定性检查    
  837. (1)使用C风格的初始化    
  838. > cat t.cpp    
  839. #include<stdio.h>    
  840. struct e{//结构体有3个成员    
  841.    int x;    
  842.    int y;    
  843.    e& operator=(const e& ie){*this=ie;}    
  844.    ~e(){}    
  845. };    
  846. int main(void){    
  847.    e buf[]={//用两个成员的{}来初始化    
  848.       {1,2},    
  849.       {1,3},    
  850.       {1,4}    
  851.    };    
  852.    printf("%d %d %d/n",buf[0].y,buf[1].y,buf[2].y);    
  853.    return 0;    
  854. }    
  855. 编译没有问题,但是如果增加了e的构造函数,编译就出错。    
  856. 原因:只有那些没有定义构造函数且所有数据成员全部为public的类,才可以应用C风格的初始化方式(大括号方式),这是为了与C兼容    
  857.      
  858. (2)成员函数中的static变量,作用和类的static变量相同    
  859. #include<stdio.h>    
  860.    struct B{    
  861.      void inc(){    
  862.           static int i=0;    
  863.           printf("%d/n",++i);    
  864.      }    
  865. };    
  866.    int main(void){    
  867.         B b1,b2;    
  868.         b1.inc();    
  869.         b2.inc();    
  870.    }    
  871. > ./a.out    
  872. 1    
  873. 2    
  874.      
  875. (3)explicit的作用域    
  876. class i{    
  877. public:    
  878.       int* a;int b;     
  879.       i(int* x){ printf("ctor/n"); a=x; }    
  880.       i(const i& ii){printf("copy ctor/n");a=ii.a;}     
  881.       explicit i(){printf("ctor default/n");}    
  882.       i& operator=(const i& ii){printf("operator/n"); a=ii.a;}    
  883. };       
  884. int main(int argc, char *argv[]){    
  885.     i i1;    
  886.     int x=20;    
  887.     int *b=&x;    
  888.     i1=b;    
  889.     printf("i1.a=%d,p=%d/n",*(i1.a),i1.a);    
  890.     return 0;    
  891. }    
  892. 程序像的输出是    
  893. ctor default    
  894. ctor    
  895. operator    
  896. i1.a=20,p=2293596    
  897. 这里int* b=&x被隐式转换成了i的对象i1=b,但是我的无参数构造函数    
  898. explicit i()...是加了explicit关键字的,为什么仍然编译通过并正确执行呢?    
  899.     
  900. 解释: explicit 只对有一个参数(或者有多个参数,但除了第一个,其他参数都有默认值)的构造函数起作用    
  901.      
  902. (4)dynamic_cast的有效性:    
  903. 只要dynamic_cast输入的参数是一个内容正确的左值,哪怕它是其他类型的指针或者引用转型过来的,只要它本身内容正确(指向了正确的虚函数表),RTTI就能成功。    
  904. #include <cstdlib>    
  905. #include <cstdio>    
  906. #include <typeinfo>    
  907. using namespace std;    
  908. class A{};    
  909. class C{    
  910. public:    
  911.     virtual void g(){}    
  912. };    
  913. class D:public C{};    
  914. int main(int argc, char *argv[])    
  915. {    
  916.     D d ;    
  917.     A *a=(A*)&d;    
  918.     C* pa=(C*)a;    
  919.     C& pc=*pa;    
  920.     try{    
  921.         C& pc2=dynamic_cast<C&>(pc);    
  922.         D& pd=dynamic_cast<D&>(pc);    
  923.     }catch(bad_cast){    
  924.         printf("bad_cast/n");    
  925.     }catch(...){    
  926.         printf("other exception/n");    
  927.     }    
  928.     return EXIT_SUCCESS;    
  929. }    
  930. 程序不会抛出任何异常。如果我把"D d"的声明改为"C d"的声明,"D& pd=dynamic_cast<D&>(pc)"就会抛出std::bad_cast异常    
  931.      
  932. Plus:    
  933. dynamic_cast的输入参数如果无效,是指针是返回NULL,是引用时抛出bad_cast异常    
  934.      
  935. (5)union里面的struct必须是plain old data,不能含有ctor,dtor,operator=函数    
  936. (6)#define宏定义的变量,在编译之后消失了,不利于理解程序合调试,因为没有符号存在。C++为了解决这个问题引入了enum类型,这个类型信息在编译时作为const常量存在,编译后仍然存在符号表信息,利于调试。    
  937. #define MONDAY 1    
  938. class b{    
  939.     public:    
  940.     const static int i=0;//if not const, compile error    
  941.     enum{friday=5};//equal to const int    
  942. };    
  943. const char* buf="abc";    
  944. int main(void){    
  945.      buf="xyz";    
  946.      int x=b::friday;    
  947.      int y=MONDAY;    
  948.      return 0;    
  949. }    
  950. Plus: 注意类当中的static变量,如果加const可以在声明时初始化,不加const必须在类声明之外初始化。    
  951. 返回页首    
  952.     
  953. (十四)使用STL时的类型限制    
  954. (1)自定义迭代器需要注意的问题    
  955. 下面这个这个程序的目的是,自定义一个迭代器的类型,模仿STL的访问方式,打印数组的全部内容。    
  956. #include<cstdio>    
  957. #include<cstdlib>    
  958. #include<algorithm>    
  959. #include<iterator>    
  960. #include<iostream>    
  961. using namespace std;    
  962. class array{    
  963.    int * pi;    
  964. public:    
  965.    array(){    
  966.            pi=new int[5];    
  967.            pi[0]=3;    
  968.            pi[1]=44;    
  969.            pi[2]=5;    
  970.            pi[3]=1;    
  971.            pi[4]=26;    
  972.    }    
  973.    virtual ~array(){  if(pi){delete[] pi;pi=0;}  }    
  974.    class Iter{//自己实现的一个迭代器    
  975.          int* data;    
  976.       public:    
  977.          Iter(int* i){data=i;}    
  978.          Iter(){data=0;}    
  979.          Iter& operator=(const Iter& i){data=i.data;}    
  980.          bool operator!=(const Iter& i){return data!=i.data;}    
  981.          int operator*(){return *data;}    
  982.          void operator++(){++data;}    
  983.          void operator--(){--data;}    
  984.    };    
  985.    Iter begin(){return Iter(π[0]);}    
  986.    Iter end(){return Iter(π[5]);}    
  987. };    
  988. int main(int argc, char *argv[])    
  989. {    
  990.     array l;    
  991.     array::Iter it;    
  992.     for(it=l.begin();it!=l.end();++it){cout<<*it<<' ';}    
  993.     cout<<'/n';    
  994.     //copy(l.begin(),l.end(),ostream_iterator<int>(cout, "  ")); //不加这一句,运行没有问题    
  995.     return 0;    
  996. }    
  997.     
  998. ->问题:    
  999. 我把上面那行注释了的"copy(l.begin(),l.end(),ostream_iterator<int>(cout, "  ")); "变成有效,编译就过不去了    
  1000. ->原因的解释:    
  1001. 因为,用于标准库算法的迭代器内需要定义有五个公有的类型:iterator_category, value_type, difference_type, distance_type, pointer, reference,其方法有两种:(1)手动定义这五个类型(2)从std::iterator继承    
  1002.     引用《C++程序设计(特别版)》里的一句话:    
  1003. “内部类型int *就是int[]的一个迭代器,类型list<int>::iterator是list类的一个迭代器”不是对内部类型没有要求,而是对内部类型的迭代器有一个默认的解释。iterator_traits 有关于指针类型的偏特化版本.    
  1004.      
  1005. (2)ostream_iterator, 传给cout的对象必须能强制转化为基本类型,或者重载<<    
  1006. #include<algorithm>    
  1007. #include<iterator>    
  1008. #include<iostream>    
  1009.      using namespace std;    
  1010. struct e {  
  1011.      float v;char c;       
  1012.      operator float()const {return v;}       
  1013. //operator char() const {return c;}    
  1014. //should conflict with operator float()  
  1015. };    
  1016. int main(int argc, char *argv[])    
  1017. {  
  1018.      int l=4;    e p[]={ {1.5,'x'}, {0.5,'a'}, {1.2,'b'}, {0.7,'y'}    };       
  1019.      copy(p,p+l,ostream_iterator<e>(cout," "));       
  1020.      cout<<'/n';       
  1021.      return 0;  
  1022. }    
  1023. 上面的operator float()和operator char()只能用一个,因为互相冲突    
  1024.      
  1025. (3)friend的一个使用场景    
  1026. 例如,要设计一个单线程的简单singleton,我把 ctor,dtor,copyctor,"="重载ctor都声明为private, 用一个静态函数来创建instance。然后由于我只有创建函数没有销毁函数,我使用auto_ptr来声明这个对象,让编译器来完成对象的释放。    
  1027.      class s{    
  1028.      static auto_ptr<s> pInst;    
  1029.      s(){}    
  1030.      ~s(){}    
  1031.      s(const s& os){printf("s.copy ctor/n");}    
  1032.      s& operator = (const s& os){printf("s.operator= called/n");}    
  1033. public:    
  1034.      static s& getInst(){    
  1035.           if(pInst.get()==0)    
  1036.                pInst.reset(new s());    
  1037.           return *pInst;    
  1038.      }    
  1039. };    
  1040. auto_ptr<s> s::pInst;    
  1041.     
  1042.     上面这个程序是编译不通过的,因为auto_ptr的析构函数去delete <s>,而s的析构函数是私有的,因此在s类的最后面我们还需要加上    
  1043.   friend class auto_ptr<s>;    
  1044. 这样的语句才能编译通过。一个替代的解决方案是不使用auto_ptr,而去使用atexit这样的函数注册一个销毁函数,让程序退出时系统自动调用。    
  1045.      
  1046. (4) class Iter:public std::iterator<bidirectional_iterator_tag, int>    
  1047. 这样的话就能    
  1048. copy(l.begin(),l.end(),ostream_iterator<int>(cout, "  "));来打印到标准输出    
  1049. 因为:    
  1050.     用于标准库算法的迭代器内需要定义有五个公有的类型:iterator_category, value_type, difference_type, distance_type, pointer, reference,其方法有两种:    
  1051. 1.手动定义这五个类型    
  1052. 2.从std::iterator继承    
  1053. 返回页首    
  1054.     
  1055. (十五)迭代器自身的类型    
  1056.         在用STL编写庞大程序的时候,如何才能知道一个迭代器指向的对象的真正类型呢? 能否把编译时确定的信息(特化的类型)保存下来以后可以用? 我们的法宝是使用一个iterator_traits对象,它是iterator的内置对象,保留了特化的类型。(通过typedef一个通用的名字来做到的)    
  1057.      
  1058. 对于stl::iterator_traits的一个非常好的解释: 它就是得到一系列的typedef来指示iterator指向对象的类型,原文来自http://msdn.microsoft.com/en-us/library/zdxb97eh(VS.80).aspx    
  1059.      template<class Iterator>    
  1060.      struct iterator_traits {    
  1061.           typedef typename Iterator::iterator_category iterator_category;    
  1062.           typedef typename Iterator::value_type value_type;    
  1063.           typedef typename Iterator::difference_type difference_type;    
  1064.           typedef typename Iterator::pointer pointer;    
  1065.           typedef typename Iterator::reference reference;    
  1066.      };    
  1067.      template<class Type>    
  1068.      struct iterator_traits<Type*> {    
  1069.           typedef random_access_iterator_tag iterator_category;//那种类型的迭代器    
  1070.           typedef Type value_type;//--------------->最关键的地方!!!!保存类型信息!!!!    
  1071.           typedef ptrdiff_t difference_type;    
  1072.           typedef Type *pointer;    
  1073.           typedef Type& reference;    
  1074.      };    
  1075.      template<class Type>    
  1076.      struct iterator_traits<const Type*> {    
  1077.           typedef random_access_iterator_tag iterator_category;    
  1078.           typedef Type value_type;    
  1079.           typedef ptrdiff_t difference_type;    
  1080.           typedef const Type *pointer;    
  1081.           typedef const Type& reference;    
  1082.      };    
  1083. 例子程序    
  1084. // iterator_traits.cpp    
  1085. // compile with: /EHsc(该选项仅对于VC编译器)    
  1086. #include <iostream>    
  1087. #include <iterator>    
  1088. #include <vector>    
  1089. #include <list>    
  1090. using namespace std;    
  1091.      templateclass it >    
  1092.      void    
  1093.      function( it i1, it i2 )    
  1094.           {    
  1095.                iterator_traits<it>::iterator_category cat;    
  1096.                cout << typeid( cat ).name( ) << endl;    
  1097.                while ( i1 != i2 )    
  1098.                {    
  1099.                     iterator_traits<it>::value_type x;    
  1100.                     x = *i1;    
  1101.                     cout << x << " ";    
  1102.                     i1++;    
  1103.                };      
  1104.                cout << endl;    
  1105.           };    
  1106.      int main( )    
  1107.           {    
  1108.                vector<char> vc( 10,'a' );    
  1109.                list<int> li( 10 );    
  1110.                function( vc.begin( ), vc.end( ) );    
  1111.                function( li.begin( ), li.end( ) );    
  1112.           }    
  1113. Output:    
  1114. struct std::random_access_iterator_tag    
  1115. a a a a a a a a a a    
  1116. struct std::bidirectional_iterator_tag    
  1117. 0 0 0 0 0 0 0 0 0 0    
  1118.     
  1119. Plus:    
  1120.    iterator不但可以用来访问元素,也可以用于赋值    
  1121. typedef vector<int> vi;    
  1122. vi v(3);    
  1123. vi::iterator it=v.begin();    
  1124. for(it;it!=v.end();++it)*it=9;    
  1125. copy(v.begin(),v.end(),ostream_iterator<int>(cout,"_"));    
  1126. 返回页首    
  1127.     
  1128. (十六)运行时的类型信息    
  1129. (1)typeid的作用,可以得到动态运行时的信息(对于多态类)    
  1130. >cat type.cpp    
  1131. #include<iostream>    
  1132. using namespace std;    
  1133. class Base {    
  1134. public:    
  1135.      virtual void vvfunc() {}    
  1136. };    
  1137. class Derived : public Base {};    
  1138. using namespace std;    
  1139. int main() {    
  1140.      Derived* pd = new Derived;    
  1141.      Base* pb = pd;    
  1142.      cout << typeid( pb ).name() << endl;   //prints "class Base *"   静态信息    
  1143.      cout << typeid( *pb ).name() << endl;   //prints "class Derived" 动态信息    
  1144.      cout << typeid( pd ).name() << endl;   //prints "class Derived *"静态信息    
  1145.      cout << typeid( *pd ).name() << endl;   //prints "class Derived" 动态信息    
  1146.      delete pd;    
  1147. }    
  1148. 在solaris上面CC的输出结果是    
  1149. > ./a.out    
  1150. Base*    
  1151. Derived    
  1152. Derived*    
  1153. Derived    
  1154.     
  1155. typeid 将返回一个派生类的type_info引用。但是expression必须指向一个多态类,否则返回的将是静态类信息。此外,指针必须被提领,以便使用它所指向的对象,没有提领指针,结果将是指针的type_info(这是一个静态信息),而不是它所指向的对象的type_info    
  1156.     
  1157. (2)static_cast能够处理类型运算符重载并解析    
  1158. 一个类,重载(char*)强制类型转换运算符,当我使用static_cast<char*>()的时候,该重载仍然是有效的。    
  1159. #include <cstdio>    
  1160. #include <cstdlib>    
  1161. #include <iostream>    
  1162. using namespace std;    
  1163. struct s{    
  1164.      char buf[4];    
  1165.      s(){strcpy(buf,"abc");}    
  1166.      operator char*(){return "kkk";}    
  1167. };    
  1168. struct c{    
  1169.      char *buf;    
  1170.      c(){buf="xyz";}    
  1171. };    
  1172. int main(void){    
  1173.      s s1;    
  1174.      printf("string1 =%s/n",&s1);//打印字符串,效果同s.buf     
  1175.      c c1;    
  1176.      printf("string2 =%s/n",*((char**)&c1));//打印字符串    
  1177.      printf("string3 =%s/n",(char*)s1);    
  1178.      cout<<static_cast<char*>(s1)<<'/n';//这里,重载的(char*)起了作用     
  1179.      return 0;    
  1180. }    
  1181.      
  1182. (3)虚函数表的存储结构研究:    
  1183. #include<stdio.h>    
  1184. class B{//对于含有虚函数的类,内存结构中的首元素是指向虚表的指针。    
  1185.      int x;    
  1186.      virtual void f(){printf("f/n");}    
  1187.      virtual void g(){printf("g/n");}    
  1188.      virtual void h(){printf("h/n");}    
  1189. public:    
  1190.      explicit B(int i) {x=i;}    
  1191. };    
  1192. typedef void (*pf)();    
  1193. int main(void){    
  1194.      B b(20);    
  1195.      int * pb=(int*)&b;    
  1196.      printf("private x=%d/n",pb[1]);    
  1197.      pf *pvt=(pf*)pb[0];//虚函数表指针是b的第一个元素,它指向一个保存指针的表    
  1198.      pf f1=(pf)pvt[0];    
  1199.      pf f2=(pf)pvt[1];    
  1200.      pf f3=(pf)pvt[2];    
  1201.      (*f1)();    
  1202.      (*f2)();    
  1203.      (*f3)();    
  1204.      printf("pvt[3]=%d/n",pvt[3]);//虚函数表结束符号,gcc是0    
  1205.      return 0;    
  1206. }    
  1207. 程序输出    
  1208. private x=20    
  1209. f    
  1210. g    
  1211. h    
  1212. pvt[3]=0    
  1213. 返回页首    
  1214.     
  1215. (十七)new/delete重载    
  1216. (1)newdelete运算符的重载,可以用来跟踪代码中内存申请和释放的过程。    
  1217. 下面的例子是重载类中的newdelete    
  1218. class a{    
  1219. public:    
  1220. void* operator new(size_t){    
  1221.      printf("a::new/n");    
  1222.      return ::new a;    
  1223. }    
  1224. void* operator new[](size_t l){    
  1225.      printf("a::new[%d]/n",l);    
  1226.      return ::new a[l];    
  1227. }    
  1228. void operator delete(void* p){    
  1229.      printf("a::delete/n");    
  1230.      ::delete (a*)p;    
  1231. }    
  1232. void operator delete[](void* p){    
  1233.      printf("a::delete[]/n");    
  1234.      ::delete[] (a*)p;    
  1235. }    
  1236. };    
  1237. int main(void){    
  1238.      a* pa=new a;    
  1239.      delete pa;    
  1240.      pa=new a[2];    
  1241.      delete[] pa;     
  1242.      return 0;    
  1243. }    
  1244. 输出    
  1245. > CC t.C && ./a.out    
  1246. a::new    
  1247. a::delete    
  1248. a::new[2]    
  1249. a::delete[]    
  1250.     
  1251. (2)replacement new需要注意的地方。例如    
  1252. class c{    
  1253.      int x;    
  1254. public:    
  1255.      explicit c(int ix){x=ix;printf("ctor/n");}    
  1256.      ~c(){printf("dtor/n");}    
  1257. };    
  1258. int main(void){    
  1259.      try{    
  1260.           char mem[sizeof(c)*2];    
  1261.           c* pc1=new(mem) c(2);    
  1262.           c c3(4); //加上这句以后,delete pc1就是非法退出, 不加这句就没事...........................................    
  1263.           delete pc1;//不能去delete内存池中的东西,否则出错 ????????    
  1264.           //pc1->~c();//用显示调用析构函数而不用delete总是安全的。    
  1265.      }catch(...){    
  1266.           printf("get exception/n");    
  1267.      }    
  1268.      return 0;    
  1269. }    
  1270. 上面的c* pc1=new(mem) c(2);    
  1271. delete pc1;//不能去delete内存池中的东西,否则出错 ????????    
  1272. 这种方式就是错误的,因为你用的是new的放置语法,而放置语法要求显式的调用析构函数,同时不用的内存需要自己释放时可以free掉,但是在堆栈上的自己费心。更多详细资料可以问问《C++程序设计语言(特别版)第2版》    
  1273.      
  1274. (3)在很多实现中,不考虑构造和析构的话,new/malloc,delete/free是等效的,举VC的例子    
  1275. #if !_VC6SP2 || _DLL    
  1276.      void *__CRTDECL operator new[](size_t count) _THROW1(std::bad_alloc)    
  1277. {        // try to allocate count bytes for an array    
  1278.      return (operator new(count));    
  1279. }    
  1280. #endif /* !_VC6SP2 || _DLL */    
  1281.     
  1282. _C_LIB_DECL    
  1283. int __cdecl _callnewh(size_t size) _THROW1(_STD bad_alloc);    
  1284. _END_C_LIB_DECL    
  1285.     
  1286. void *__CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)    
  1287. {       // try to allocate size bytes    
  1288.      void *p;    
  1289.      while ((p = malloc(size)) == 0)    
  1290.           if (_callnewh(size) == 0)    
  1291.           {       // report no memory    
  1292.                static const std::bad_alloc nomem;    
  1293.                _RAISE(nomem);    
  1294.           }    
  1295.     
  1296.      return (p);    
  1297. }    
  1298.      
  1299. (4)在重载的operator new调用的时候,如果分配的是有析构函数的对象数组,那么传进来的size_t会多出一个整数字节的大小,用于记录数组大小(delete[] 需要循环调用各对象的析构函数)    
  1300. 下面这个小程序是重载全局的new/delete操作符来实现对象的分配和释放:    
  1301. #include <new>    
  1302. #include <cstdio>    
  1303. #include <cstdlib>    
  1304. using namespace std;    
  1305. void* operator new(size_t size) throw(bad_alloc)    
  1306. {    
  1307.      printf("operator new:%d Byte/n",size);    
  1308.      void* m=  malloc(size);    
  1309.      if(!m) puts("out of memory");    
  1310.      return m;    
  1311. }    
  1312. void operator delete(void* m)throw(){    
  1313.      puts("operator delete");    
  1314.      free(m);}    
  1315. class B{    
  1316.      int s;    
  1317. public:    
  1318.      B(){/*puts("B::B()");*/}    
  1319.      ~B(){/*puts("B::~B()");*/}    
  1320. };    
  1321. int main(int argc, char* argv[]){    
  1322.      int* p = new int(4);    
  1323.      delete p;    
  1324.      B* s = new B;    
  1325.      delete s;    
  1326.      B* sa = new B[10];    
  1327.      delete []sa;    
  1328.      int* pi=new int[3];    
  1329.      delete []pi;    
  1330.      return 0;    
  1331. }    
  1332.     
  1333. 程序的输出是    
  1334. > gcc n.C && ./a.out    
  1335. operator new:4 Byte    
  1336. operator delete    
  1337. operator new:4 Byte    
  1338. operator delete    
  1339. operator new:48 Byte         ->问题出在这里,new为类指针数组分配的时候,4x10应该是10个字节,多出来的8个字节是做什么的?    
  1340. operator new:12 Byte    
  1341. 回答:    
  1342. 是编译的时候就做到了.    
  1343.     
  1344. 如:    
  1345.     class B xxxxxxxxxxxxx;    
  1346.     p=new B[num];    
  1347. 那么编译器会处理成(注意:不同的编译器会有所不同):    
  1348.   +--------------------------------------------------------------+    
  1349.    |num|var[0]|var[1]|var[2]|var[3]|........|var[num-1]|    
  1350.   +--------------------------------------------------------------+    
  1351.     
  1352.         push        n               ;n=num*var_size+4    
  1353.         call           我重载的new           
  1354.         ....................................    
  1355.         push        B::~B()的地址    
  1356.         push        B::B()的地址    
  1357.         *((int*)p)=num;    
  1358.         ((int*)p)++;    
  1359.         push        num    
  1360.         push        var_size         
  1361.         push    p    
  1362.         call          vector_constructor_iterator  ;这里会循环调用B::B(),次数是num    
  1363.   +--------------------------------------------------------------+    
  1364. 对类类型,delete一个数组时(比如,delete []sa;),要为每一个数组元素调用析构函数。但对于delete表达式(比如,这里的delete []sa),它并不知道数组的元素个数(只有new函数和delete函数知道)。因此,必须有一种手段来告诉delete表达式的数组大小是多少。那么一种可行的方式就是,多分配一个大小为4字节的空间来记录数组大小,并可以约定前四字节来记录大小。那么,由new函数分配的地址与new表达式返回的地址应该相差4个字节(这可以写程序来验证)。对于非类类型数组和不需要调用析构函数的类类型数组,这多于的四字节就不需要了。    
  1365.      
  1366. (5)同理,可以重载全局的new/delete,形如    
  1367. void* operator newsize_t size ){    
  1368.     if( 0 == size ) // 注意!!!!    
  1369.         size = 1;    
  1370.     while(1){    
  1371.         分配size字节内存;    
  1372.         if(分配成功)    
  1373.             return 指向内存的指针;    
  1374.         new_handler g= set_new_handler(0);    
  1375.         set_new_handler(g);    
  1376.         if( g)(*g)();    
  1377.         else throw std::bad_alloc();    
  1378.     }    
  1379. }    
  1380. void operator deletevoid* p){    
  1381.     if( 0 == p) // 须要注意    
  1382.         return;    
  1383.     ...    
  1384. }    
  1385. 上面的new_handler是用户自定义的全局set_new_handler处理函数,newhandler形式是:    
  1386. void mynewhandler(){    
  1387.         if( 使得operator new成功 )    
  1388.         {    
  1389.             例如等待一段时间,再次分配内存    
  1390.             return;    
  1391.         }    
  1392.         // 主动退出    
  1393.         或 abort/exit 直接退出程序    
  1394.         或 set_new_handler(其他newhandler或者0);    
  1395.         或 set_new_handler(0)    
  1396.         或 throw bad_alloc()//比较好     
  1397. }    
  1398. 返回页首    
  1399.     
  1400. (十八)如何拷贝一个文件----标准C/C++运行库里面没有拷贝文件的函数,必须自己完成    
  1401. (1)标准c的逐字节拷贝    
  1402. #include<stdio.h>    
  1403.       int main(void){    
  1404.      FILE* pin=fopen("in.data","rb");    
  1405.      FILE* pout=fopen("out.data","wb");    
  1406.      int c;    
  1407.      while((c=fgetc(pin))!=EOF){    
  1408.           fputc(c,pout);}    
  1409.      fclose(pin);    
  1410.      fclose(pout);    
  1411.      return 0;    
  1412. }    
  1413. (2)Iostream的多字节拷贝    
  1414. #include<iostream>    
  1415. #include<fstream>    
  1416. using namespace std;    
  1417. int main(void){    
  1418.      ifstream fi;    
  1419.      fi.open("in.data",ios::binary);    
  1420.      ofstream fo;    
  1421.      fo.open("out.data",ios::binary);    
  1422.      char buf[1024];    
  1423.      do{    
  1424.           fi.read(buf,sizeof(buf));    
  1425.           fo.write(buf,fi.gcount());    
  1426.      }while(fi.good());    
  1427.      fi.close();    
  1428.      fo.close();    
  1429.      return 0;    
  1430. }    
  1431. 可以把do-while的循环用一句话代替: fo<<fi.rdbuf()    
  1432. (3)STL算法拷贝,逐字节进行    
  1433. #include<fstream>    
  1434. #include<iterator>    
  1435. #include<algorithm>    
  1436. using namespace std;    
  1437. int main(void){    
  1438.      ifstream fi;    
  1439.      fi.open("in.data",ios::binary);    
  1440.      ofstream fo;    
  1441.      fo.open("out.data",ios::binary);    
  1442.      copy(istreambuf_iterator<char>(fi),istreambuf_iterator<char>(),ostreambuf_iterator<char>(fo));    
  1443.      fi.close();    
  1444.      fo.close();    
  1445.      return 0;    
  1446. }   

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值