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

本文深入探讨了C/C++编程语言中关于整数符号的陷阱、浮点数运算的不精确性、堆栈内存管理的细节、虚函数的实现原理等关键主题。通过具体的代码示例揭示了这些概念背后的底层机制。
Code:

  1.   
    C/C++内存与运行时深入研究 [作者Jean.Love]  
  2. -----------------------------------------------------------------------------------
    • (一)整数符号的陷阱  
    • (二)浮点数的本质  
    • (三)堆栈的内存管理结构  
    • (四)符号解析  
    • (五)对齐和总线错误  
    • (六)函数指针  
    • (七)虚函数的实现机理  
    • (八)引用的实现机理  
    • (九)虚拟继承对象的内存结构  
    • (十)混合编程时的初始化顺序  
    • (十一)数组和指针的异同  
    • (十二)const限定的传递性  
    • (十三)数据类型的限定性检查  
    • (十四)使用STL时的类型限制  
    • (十五)迭代器自身的类型  
    • (十六)运行时的类型信息  
    • (十七)new/delete重载  
    • (十八)如何拷贝一个文件  
    •   
    • (一)整数符号的陷阱  
    •         x<y和x-y<0的结果是一样的吗??看看下面这个简单的程序。  
    • #include<stdio.h>  
    • int main(void){  
    • int x=1;  
    • unsigned int y=2;  
    • int b=x<y;  
    • int b2=(x-y<0);  
    • printf("%d,%d/n",b,b2);  
    • return 0;  
    • }  
    • 它输出什么呢?  
    • 1,0  
    •   
    •     令人震惊,不是吗,x<y和x-y<0不是一样的!  
    • (1)x<y的时候,由于y是无符号数字,所以x也被提升为无符号数字,所有x<y是成立的,返回1  
    • (2)x-y的结果计算的时候,返回一个0xfffffffe,它被当成无符号数字理解并和0比较,显然<0不成立,返回0。  
    •   
    • 总结一下,整数的运算,加减乘的时候,根本不管是否声明为是否有符号,在2进制cpu上面的计算是相同的,但是比较的时候(<,>,==)会根据类型,调用不同的比较指令,也就是以不同的方式来理解这个2进制结果。当signed和unsigned混用的时候,全部自动提升为无符号整数。  
    • #include<stdio.h>  
    • int main(void){  
    •   int i=-2;  
    •   unsigned j=1;  
    •   if(j+i>1) //提升为两个uint相加  
    •     printf("sum=%d/n",j+i);//打印的结果根据%d制定,j+i的内存值永远不变。  
    •   return 0;  
    • }  
    • 输出  
    • > ./a.out  
    • sum=-1  
    •    
    • 再举一个例子  
    • #include<stdio.h>  
    • int main(void){  
    •  int i=-4;  
    •  unsigned int j=1;  
    •  int ii=i+j;  
    •  unsigned int jj=i+j;  
    •  printf("%d,%ud/n",ii,jj);  
    •  if(ii>1){printf("100000");}  
    •  if(jj>1){printf("100001");}  
    •  return 0;  
    • }  
    • 用gcc -S得到汇编,会发现if(ii>1)和if(jj>1)对应两个不同的跳转指令jle和jbe。  
    •   
    • 总结: int和unit在做比较操作和除法的时候不同,其他情况相同。  
    • 返回页首  
    •   
    • (二)浮点数的本质  
    •         用一个程序来说明浮点数的IEEE表示。注意Linux没有atoi,ltoi,itoa这样的函数,那几个函数是VC独家提供的,不是ANSI C标准,所以*nix要用到sprintf函数来打印整数的内容到字符串里面。IEEE浮点数对于32位的float来说,从高位到低位分别是1bit符号位,8bit指数位,23bit浮点数位。当然由于内存地址是从低到高排列的,所以要把这4个字节的内容反过来,作为整数,转换为字符串打印出来的内容才是正确的。在x86机器上,同样是低位字节在前高位字节在>后,这样做得好处就是可以把浮点数作为有符号整数来排序。  
    •         例如浮点书-0.875,符号为1(复数),二进制表示为-0.111,表示为1-2之间的小鼠就是-1.11 x 2^-1,指数项-1,加上128得到1111111(127),因为指数项的8个bit必须保证是无符号数,所以有了这样的表示。而23bit的整数项则是11000000000000000000,也就是取了-1.11在小数点后面的内容,没有的后端补0。  
    • 所以,-0.875f的2进制表示就是10111111011000000000000000000000。写一个小程序来验证  
    • #include<stdio.h>  
    • #include<stdlib.h>  
    • void pfloat(float f){  
    •   int i,j;  
    •   char buf[4][9];  
    •   char* p=(char*)&f;  
    •   printf("before loop/n");  
    •   for(i=0;i<4;++i){  
    •    for(j=0;j<8;++j){  
    •     buf[i][j]=(p[i]&(0x80>>j))>0?'1':'0';  
    •    }  
    •    buf[i][8]='/0';  
    •   }  
    •   for(i=3;i>=0;i--){  
    •    printf("%s",buf[i]);  
    •   }  
    •   printf("/n");  
    •   printf("end loop/n");  
    • }  
    • int main(void){  
    •   float d1=-0.875;  
    •   pfloat(d1);  
    •   return 0;  
    • }  
    •         看看输出和我们预期的一致。浮点数的计算总是充满了陷阱。首先,因为浮点数的精度有限,所以在做四则运算的时候,低位很可能在过程中被舍弃。因此,浮点运算不存在严格的运>算的结合律。在32位系统上面,浮点数float为4字节长,其中整数位23位,表示范围转换为10位数的话有9个有效数字。所以  
    •   float f1=3.14;  
    •   float f2=1e20;  
    •   float f3=-1e20;  
    •   printf("%d,%f/n",i,f);  
    •   printf("%f/n",f1+f2+f3);  
    •   printf("%f/n",f2+f3+f1);  
    •   
    •     上面两个printf的结果是不一样的,第一个结果是0,第二个结果是3.14。再举一个例子  
    •   float k=1.3456789;  
    •   float k2=k;  
    •   k-=1000000.0;  
    •   printf("%f/n",k);  
    •   k+=1000000.0;  
    •   printf("%f/n",k);  
    •   int b=(k==k2);  
    •   printf("%d/n",b);  
    •     结果是什么呢? b=0,因为k的值在之前的运算中,小数点后面已经有5为被舍入了,所以k不再等于k2。要使得k==k2成立,必须提高京都,使用double--52位整数域,相当于10进制有效数字16位,可以克服上面这个运算的不精确性。  
    •   double d1,d2;  
    •   printf("%f/n",d1);  
    •   d1=d2=1.3456789;  
    •   d2+=1000000.0;  
    •   printf("%f/n",d2);  
    •   d2-=1000000.0;  
    •   printf("%f/n",d2);  
    •         现在d==d2的返回值就是真了。为了使得运算结果有可以比较的意义,通常定义一个门限值。#define fequals(a,b) fabs(a-b)<0.01f  
    •         如果浮点数计算溢出,printf能够输出适当的表示  
    •   float nan=3.0f/0.0f;  
    •   printf("%f/n",nan);  
    • 打印inf,如果结果是负无穷大,打印-inf。  
    • 返回页首  
    •   
    • (三)堆栈的内存管理结构  
    •         堆和栈的内存管理(x86机器)与分布是什么样子的?用一个程序来说明问题。看看堆和栈的空间是怎么增长的。  
    • $ cat stk.c  
    • #include<stdio.h>  
    • #include<stdlib.h>  
    • int main(void){  
    •  int x=0;  
    •  int y=0;  
    •  int z=0;  
    •  int *p=&y;  
    •  *(p+1)=2;//这条语句究竟是设置了x还是设置了z?和机器的cpu体系结构有关  
    •  int* px=(int*)malloc(sizeof(int));  
    •  int* py=(int*)malloc(sizeof(int));  
    •  int* pz=(int*)malloc(sizeof(int));  
    •  *px=1;  
    •  *py=1;  
    •  *pz=1;  
    •  *(py+1)=3;  
    •  printf("%d,%d,%d/n",x,y,z);  
    •  printf("%p,%p,%p/n",px,py,pz);  
    •  printf("%d,%d,%d/n",*px,*py,*pz);  
    •  free(px);  
    •  free(py);  
    •  free(pz);  
    •  return 0;  
    • }  
    • 编译和运行的结果  
    • $ gcc stk.c && ./a.out  
    • 2,0,0  
    • 0x9e8b008,0x9e8b018,0x9e8b028  
    • 1,1,1  
    •   
    • (1)如果把上面的分配内存的代码改成  
    • int* px=(int*)malloc(sizeof(int)*3);  
    • int* py=(int*)malloc(sizeof(int)*3);  
    • int* pz=(int*)malloc(sizeof(int)*3);  
    • 第三个printf的输出仍然是  
    • 0x9e8b008,0x9e8b018,0x9e8b028  
    •         说明什么呢? malloc分配的时候,分配的大小总是会比需要的大一些,也就是稍微有一些不大的内存越界并不会引起程序崩溃。当然这种情况可能导致得不到正确的结果。  
    •   
    •         我们看看堆和栈的内存分布吧,在一台安装了Linux的x86机器上  
    • ---------------------  
    • 0xffffffff  
    • ->OS内核代码,占据1/4的内存地址空间  
    • 0xc000000  
    • ->stack是运行时的用户栈,地址从高往低增长  
    • |     x  
    • |     y           ->int*(&y)+1指向的就是x  
    • |     z  
    •   
    • ->共享库的存储器映射区域  
    • 0x40000000  
    • ->运行时堆,往上增长  
    • |    pz  
    • 。。。。。。  
    • |    py           ->由于py分配的内存大于实际想要的, *(py+1)=3;不对程序结果有影响  
    • 。。。。。。  
    • |    px           ->malloc分配的内存从低往高分配  
    • 。。。。。。  
    • ->可读写数据区(全局变量等)  
    • ->只读代的代码和数据(可执行文件,字面常量等)  
    • 0x08048000        ->是的,代码总是从同一地址空间开始的  
    • ->未使用  
    • 0x00000000  
    • ---------------------  
    •   
    • 如果把程序改为 *(py+4)=3;  
    • 那么程序最好一行的输出就是  
    • 1,1,3  
    • 也就是pz的内容被写入。验证了理论。  
    • 返回页首  
    •   
    • (四)符号解析  
    •         符号是怎么被解析的?什么时候会有符号解析的冲突?假设两个模块里面都有全局变量  
    • $ cat f.c  
    • #include<stdio.h>  
    • int i=0;  
    • void f(){  
    •   printf("%d/n",i);  
    • }  
    • $ cat m.c  
    • int i=3;  
    • extern void f();  
    • int main(void){  
    •  f();  
    •  return 0;  
    • }  
    • 这样的话,编译和链接会有错误:  
    • $ gcc -o main m.o f.o  
    • f.o:(.bss+0x0): multiple definition of `i'  
    • m.o:(.data+0x0): first defined here  
    • collect2: ld 返回 1  
    • 也就是说,我们定义了重名的全局变量i,那么链接器就不知道应该用哪个i了,用nm可以看到符号表:  
    • $ nm m.o f.o  
    •   
    • m.o:  
    •          U f  
    • 00000000 D i  
    • 00000000 T main  
    •   
    • f.o:  
    • 00000000 T f  
    • 00000000 B i  
    •          U printf  
    •   
    •   
    • 解决方法有两种:  
    • 1. 在m.c里面把int i=3变成main内部的局部变量,这样的话:  
    • $ cat mcp.c  
    • extern void f();  
    • int main(void){  
    •  int i=3;  
    •  f();  
    •  return 0;  
    • }  
    • [zhang@localhost kg]$ nm mcp.o  
    •          U f  
    • 00000000 T main  
    •   
    • 在文件m.o中没有了全局符号i,链接就没有了错误。  
    •   
    • 2.在f.c中把int i从全局变量变成static静态变量,使得它只在当前文件中可见  
    • $ cat fcp.c  
    • #include<stdio.h>  
    • static int i=0;  
    • void f(){  
    •   printf("%d/n",i);  
    • }  
    • [zhang@localhost kg]$ nm fcp.o  
    • 00000000 T f  
    • 00000000 b i     ->这里i的类型从以前的B变成了b  
    •          U printf  
    •   
    • main的执行结果是0,也就是f里面的i就是当前文件的i,不会使用m.c中定义的全局i。这两个i由于不冲突,就被定义在不同的地址上面了。  
    • 返回页首  
    •   
    • (五)对齐和总线错误  
    •         什么是Bus error? 一般是总线寻址造成的,由于指针类型和long有相同大小,cpu总是找到%4/%8的地址作为指针的起始地址,例如:  
      • #include<stdio.h>  
      • int main(void){  
      • char buf[8]={'a','b','c','d','e','f'};  
      • char *pb=&(buf[1]);     //这里pb的地址不是4bytes或8bytes对齐的,而是从一个奇数地址开始  
      • int *pi=(int*)pb;  
      • printf("%d/n",*pi);  
      • return 0;  
      • }  
      •    
      •        这类问题的结果和CPU的体系结构有关,取决于CPU寻址的时候能否自动处理不对齐的情况。下面这个小程序是一个例子。分别在 Sparc(solaris+CC)和x86(vc6.0)上面测试: Sparc上面就会崩溃(Bus error (core dumped)),x86就没有问题。  
      •        Plus: 在hp的pa-risc(aCC),itanium(aCC),IBM(xlC)的power上面测试  
      • power不会core dump, pa-risc和Itanium也均core dump.  
      • 返回页首  
      •   
      • (六)函数指针  
      •        要控制函数的行为,可以为函数传入一个回调函数作为参数。C++的STL使用的是functional算子对象,C语言可以传递一个函数或者一个函数指针。  
      • #include <stdio.h>  
      • #include <stdlib.h>  
      • typedef void callback(int i);  
      • void p(int i){printf("function p/n");}  
      • void f(int i,callback c){c(i);}  
      • int main(void)  
      • {  
      •  f(20,p);  
      •  return 0;  
      • }  
      • > ./a.out  
      • function p  
      •     既然可以把函数直接作为回调参数传给另一个主函数,为什么还要用函数指针呢? 相像一下f函数运行在一个后台线程里面,这个线程是个服务器不能被停止,那么我们想要动态改变f的行为就不可能了,除非f的第二个参数是 callback* 而传入的这个变量我们去另一个线程里面改变。这样就实现了灵活性。  
      • 返回页首  
      •   
      • (七)虚函数的实现机理  
      •     因为C++里面有指针,所以所谓的publicprivate在强类型转换面前没有意义。我们总是可以拿到私有的成员变量。 winXP+gcc3.4.2得到的虚函数表最后一项是0,是个结束符。注意,这是严重依赖编译器的,C++标准甚至都没要求是要用虚函数表来实现虚函数机制。  
      • /*----------------------------------------------------------------------------*/  
      • #include<stdio.h>  
      • class B{  
      •       int x;  
      •       virtual void f(){printf("f/n");}  
      •       virtual void g(){printf("g/n");}  
      •       virtual void h(){printf("h/n");}  
      • public:  
      •       explicit B(int i) {x=i;}  
      • };  
      • typedef void (*pf)();  
      • int main(void){  
      •   B b(20);  
      •   int * pb=(int*)&b;  
      •   printf("private x=%d/n",pb[1]);  
      •   pf *pvt=(pf*)pb[0];//虚函数表指针  
      •   pf f1=(pf)pvt[0];  
      •   pf f2=(pf)pvt[1];  
      •   pf f3=(pf)pvt[2];  
      •   (*f1)();  
      •   (*f2)();  
      •   (*f3)();  
      •   printf("pvt[3]=%d/n",pvt[3]);//虚函数表结束符号   
      •   return 0;  
      • }  
      •   
      • 程序输出  
      • private x=20  
      • f  
      • g  
      • h  
      • pvt[3]=0  
      •    
      •     理解的关键是,b的第一个dword,里面保存了一个指针,指向虚函数表。我们用两次强制转型,一次得到b的第一个dword,在把这个dword转为  
      •     当然,上面的这个结果是和编译器类型以及版本有关系的,gcc2.95.2版本对象的结构就不同,它把虚函数表指针放到了对象的后面,也就是pvt= ((int*)(&b))[1]才是指针域,而且pvt[0]=0是结束符,pvt[1]才是第一个虚函数的起始地址。所以这样写出来的程序是不通用的。同一台机器上,不同的编译器来编上面那个程序,有的能工作,有的coredump。因为C++对象的内存模型不是C++标准的一部分,可以有不同的实现,不同实现编出来的结果(和虚函数有关的)互相之间没有任何通用性。  
      •    
      •     如果有访问对象的成员呢? 情况更复杂。  
      • #include<string>  
      • using namespace std;  
      • struct a{  
      •  int x;  
      •  virtual void f(){printf("f(),%d/n",x);}  
      •  explicit a(int xx){x=xx;}  
      • };  
      • int main(void){  
      •   a a1(2);  
      •   a a2(3);  
      •   int* pi=(int*)&a1;  
      •   int* pvt=(int*)pi[0];  
      •   typedef void(*pf)();  
      •   pf p=(pf)pvt[0];  
      •   (*p)();  
      •   int *p2=(int*)&a2;  
      •   int *pv2=(int*)p2[0];  
      •   pf px=(pf)pv2[0];  
      •   (*px)();  
      •   return 0;  
      • }  
      • 输出是什么呢?  
      • $ g++ r.cpp &&./a.out  
      • f(),3  
      • f(),3  
      •    为什么会有这样的错误? 因为成员函数在传递参数的时候默认含有一个this指针,但是我这里的简单调用并没有去指定this指针,所以程序没有挂掉就已经很幸运了。怎么才能得到正确的结果呢? 像下面这样增加一个this类型的调用参数:  
      • #include<stdio.h>  
      • struct a{  
      • int x;  
      • virtual void f(){printf("f(),%d/n",x);}//............  
      • explicit a(int xx){x=xx;}  
      • };  
      • int main(void){  
      •   a a1(2);  
      •   a a2(3);  
      •   int* pi=(int*)&a1;  
      •   int* pvt=(int*)pi[0];  
      •   typedef void(*pf)(a*);  
      •   pf p=(pf)pvt[0];  
      •   (*p)(&a1);  
      •   int *p2=(int*)&a2;  
      •   int *pv2=(int*)p2[0];  
      •   pf px=(pf)pv2[0];  
      •   (*px)(&a2);  
      •   return 0;  
      • }  
      • > g++ p.cpp && ./a.out  
      • f(),2  
      • f(),3  
      • 现在结果就正确了。  
      •    
      •    
      •     再次说明,this指针的传递方法在C++标准里面并没有说明,而是各家编译器各自实现。这里引用OwnWaterloo的一段解释性代码,说明问题。  
      •    
      • (1)gcc3.4.x 是通过给参数列表增添一个隐藏参数, 来传递this的, 代码 :  
      •   
      • /*----------------------------------------------------------------------------*/  
      • class C {  
      •     int i_;  
      • public:  
      •     explicit C(int i) :i_(i) {}  
      •     virtual ~C() {}  
      •     virtual void f() { printf("C::f(%d)/n",i_); }  
      • };  
        • #if defined(__GNUC__)  
        • #if __GNUC__!=3  
        • #error not test on other gcc version except gcc3.4  
        • #endif  
        • #include <assert.h>  
        • #include <string.h>  
        • #include <stdio.h>  
        • #define intprt_t int*  
        • int main()  
        • {  
        •     C c1(1212);  
        •     C c2(326);  
        •   
        •     typedef void (* virtual_function)(C*);  
        •     // gcc 通过一个增加一个额外参数, 传递this  
        •     // virtual_function 即是C的虚函数签名  
        •   
        •     struct  
        •     {  
        •         virtual_function* vptr;  
        •         // 虚函数表指针  
        •         // 当然,它指向的表不全是函数, 还有RTTI信息  
        •         // 总之, 它就是这个类的标识, 唯一的“类型域”  
        •   
        •         int i;  
        •         // data member  
        •     } caster;  
        •     // 我们猜想, gcc将虚函数表指针安排在对象的最前面。  
        •   
        •     memcpy(&caster,&c1,sizeof(caster));  
        •     printf("c1.i_ = %d/n",caster.i); // 1212  
        •     printf("c1.vptr_ = %p/n"  
        •         ,reinterpret_cast<void*>(reinterpret_cast<intptr_t>(caster.vptr)) );  
        •     virtual_function* vptr1 = caster.vptr;  
        •   
        •     memcpy(&caster,&c2,sizeof(caster));  
        •     printf("c2.i_ = %d/n",caster.i);  
        •     printf("c2.vptr_ = %p/n",(void*)caster.vptr);  
        •     virtual_function* vptr2 = caster.vptr;  
        •      
        •     assert(vptr1==vptr2);  
        •     // 显然, 它们都是C, 所以vptr指向相同的地址  
        •   
        •     vptr1[2](&c1); // C::f(1212)  
        •     vptr2[2](&c2); // C::f(326)  
        •     /* 我们再猜想 f在虚函数表中的第2项。这里的~C是虚函数表第1项。*/  
        •     /* 在存在有虚析构函数的时候,虚表的第0项似乎只是个导引。如果把~C去掉改为别的虚函数,那么f就是虚表的第1项。*/  
        • }  
        • (2)MSVC使用另一种实现  
        • int main()  
        • {  
        •     C c1(1212);  
        •     C c2(326);  
        •     typedef void (__stdcall* virtual_function)(void);  
        •     // msvc 通过ecx传递this, 所以参数列表和虚函数相同  
        •     // 同时, msvc生成的虚函数, 会平衡堆栈  
        •     // 所以这里使用 __stdcall  让调用者不做堆栈的平衡工作  
        •   
        •     struct {  
        •         virtual_function* vptr;  
        •         int i;  
        •     } caster;  
        •     // 这同样是对编译器生成代码的一种假设和依赖  
        •   
        •     memcpy(&caster,&c1,sizeof(caster));  
        •     printf("c1.i_ = %d/n",caster.i);    // 1212  
        •     virtual_function* vptr1 = caster.vptr;  
        •     printf("c1.vptr_ = %p/n"  
        •         ,reinterpret_cast<void*>(reinterpret_cast<ptrdiff_t>(vptr1)) );  
        •   
        •     memcpy(&caster,&c2,sizeof(caster));  
        •     printf("c2.i_ = %d/n",caster.i);    // 326  
        •     virtual_function* vptr2 = caster.vptr;  
        •     printf("c2.vptr_ = %p/n"  
        •         ,reinterpret_cast<void*>(reinterpret_cast<ptrdiff_t>(vptr2)) );  
        •     assert(vptr1==vptr2);  
        •     // 显然 c1 c2 都是 C,它们的虚指针是相同的  
        •       
        •     // 但是, 直接调用是不行的, 因为没传递this  
        •     //vptr1[2]();  
        •   
        •     // 这样也不行  
        •     //_asm { lea ecx, c1 }  
        •     // 因为下面这行代码, 修改了 ecx  
        •     // vptr1[2]();  
        •   
        •     // 所以要如下进行直接调用  
        •     virtual_function f1 = vptr1[1];  
        •     _asm {  
        •         lea ecx,c1  
        •         call f1  
        •     }  
        •     virtual_function f2 = vptr2[1];  
        •     _asm {  
        •         lea ecx,c2  
        •         call f2  
        •     }  
        •     // 分别打印出 C::f(1212),C::f(326)  
        •     // 同时, C::f在虚表的第1项, vs的watch窗口说的 ……  
        • }  
        • 返回页首  
        •   
        • (八)引用的实现机理  
        •         引用的工作方式是什么呢 不纠缠于语法的解释,看代码和汇编结果最直接。举下面这个小例子程序:(gcc -masm=hello -S main.cpp可以得到汇编代码)  
        • #include<stdio.h>  
        • int x=3;  
        • int f1(){return x;}  
        • int& f2(){return x;}  
        • int main(){  
        • int a=f1();  
        • int y=f2();  
        • y=4;//仍然有x=3  
        • int&z=f2();  
        • z=5;  
        • printf("x=%d,y=%d",x,y);//z改变了x  
        • return 0;  
        • }  
        • 输出是什么呢? x=5,y=4  
        • 分析:  
        • f2是个返回引用的函数,当且仅当int&z =f2()的时候才是真的返回引用,int y=f2()返回的仍然是一个值的拷贝。汇编代码如下(部分)  
        •   
        • -----------------------------------------------------------------------------------  
        • f1和f2的定义:  
        • .globl __Z2f1v  
        •  .def __Z2f1v; .scl 2; .type 32; .endef  
        • __Z2f1v:  
        •  push ebp  
        •  mov ebp, esp  
        •  mov eax, DWORD PTR _x              f1()返回一个值的拷贝  
        •  pop ebp  
        •  ret  
        •  .align 2  
        • .globl __Z2f2v  
        •  .def __Z2f2v; .scl 2; .type 32; .endef  
        • __Z2f2v:  
        •  push ebp  
        •  mov ebp, esp  
        •  mov eax, OFFSET FLAT:_x             f2()返回的就是一个地址,不是值  
        •  pop ebp  
        •  ret  
        •  .def ___main; .scl 2; .type 32; .endef  
        •  .section .rdata,"dr"  
        • 我们看一下main函数  
        • _main:  
        •  push ebp  
        •  mov ebp, esp  
        •  sub esp, 40  
        •  and esp, -16  
        •  mov eax, 0  
        •  add eax, 15  
        •  add eax, 15  
        •  shr eax, 4  
        •  sal eax, 4  
        •  mov DWORD PTR [ebp-16], eax  
        •  mov eax, DWORD PTR [ebp-16]  
        •  call __alloca  
        •  call ___main  
        •  call __Z2f1v                                 -> 调用f1(), 返回值放在eax  
        •  mov DWORD PTR [ebp-4], eax      -> eax赋值给a  
        •  call __Z2f2v  
        •  mov eax, DWORD PTR [eax]         -> 调用f2(), 返回x的值拷贝放在eax  
        •  mov DWORD PTR [ebp-8], eax     ->  eax赋值给y  
        •  mov DWORD PTR [ebp-8], 4         ->  立即数"4"赋值给y. y的改变不会改变x!!!!!!  
        •  call __Z2f2v  
        •  mov DWORD PTR [ebp-12], eax   -> 调用f2(), 返回x的地址给z  
        •  mov eax, DWORD PTR [ebp-12]   -> x的地址放入eax  
        •  mov DWORD PTR [eax], 5            -> 赋值5给eax指向的地址x  
        •  mov eax, DWORD PTR [ebp-8]    //以下是printf的调用  
        •  mov DWORD PTR [esp+8], eax  
        •  mov eax, DWORD PTR _x  
        •  mov DWORD PTR [esp+4], eax  
        •  mov DWORD PTR [esp], OFFSET FLAT:LC0  
        •  call _printf  
        •  mov eax, 0  
        •  leave  
        •  ret  
        •  .def _printf; .scl 2; .type 32; .endef  
        • 返回页首  
        •   
        • (九)虚拟继承有什么样子的内存模型  
        •         研究了一下虚拟继承时,对象的内存分布模型,写了下面这个小程序  
        • #include<stdio.h>  
        • struct A {int x;int y; };  
        • struct B : virtual public A {  
        • int a;  
        • B(){x=1;y=2;a=55;}  
        • };  
        • struct C : virtual public A {  
        • int b;  
        • C(){x=3;y=4;b=66;}  
        • };  
        • struct D : public B, public C { };  
        • int main(void) {  
        • A a;  
        • B b;  
        • C c;  
        • D d;  
        • D *pd = &d;  
        • C *pd_c =(C*)(&d);  
        • B *pd_b =(B*)(&d);  
        • A *pd_a =(A*)(&d);  
        • printf("%d,%d,%d,%d/n",sizeof(a),sizeof(b),sizeof(c),sizeof(d));  
        • printf("%p,%p,%p,%p/n",pd,pd_c,pd_b,pd_a);  
        • int *pd2=(int*)pd;  
        • printf("%p,%d,%p,%d,%d,%d/n",**((int**)(pd2)),*(pd2+1),**((int**)(pd2+2)),*(pd2+3),*(pd2+4),*(pd2+5));  
        • return 0;  
        • }  
        • 输出  
        • 8,16,16,24  
        • 0022FF20,0022FF28,0022FF20,0022FF30  
        • 00000008,55,00000000,66,3,4  
        •   
        •         结论:D的内存分布像是这样(堆栈从高到低),vbptr表示虚基类量偏移指针  
        • |A.y|  
        • |A.x|  
        • |C.b|  
        • |C.vbptr|  
        • |B.a|  
        • |B.vbptr|  
        • 其中bvptr是virtual public类型的对象中,虚基类的偏移量。这里C.vbptr=0,B.vbptr=8.对于d来说,C::C()在B::B()之后调用,所以(x,y)=(3,4)  
        • 因此按顺序输出D的内存内容就得到(8,55,0,66,3,4)  
        • 返回页首  
        •   
        • (十)混合编程时的初始化顺序  
        • (1)ctor,dtor和atexit的调用顺序  
        • #include<stdio.h>  
        • #include<stdlib.h>  
        • class a{  
        •   int ii;  
        • public:  
        •   explicit a(int i){  
        •     ++count;  
        •     ii=i;  
        •     printf("ctor i=%d/n",ii);  
        •     atexit(f);  
        •   }  
        •   ~a(){printf("dtor i=%d/n",ii);}  
        •   static void f(){printf("f() count=%d/n",count);}  
        •   static int count;  
        • };  
        • int a::count=0;  
        • void g(){  
        •   a a2(2);//注意,如果a对象声明在一个循环中,那么循环执行N次a的构造函数就会调用N次!!  
        •   printf("after g() a ctor/n");  
        • }  
        • a a3(3);//最外层的对象  
        • int main(void){  
        •   a a1(1);//次外层的对象  
        •   atexit(g);  
        •   return 0;  
        • }  
        • 运行输出  
        • ./a.out  
        • ctor i=3  
        • ctor i=1  
        • dtor i=1  
        • ctor i=2  
        • after g() a ctor  
        • dtor i=2  
        • f() count=3  
        • f() count=3  
        • dtor i=3  
        • f() count=3  
        •   
        • (2)一个程序本质上都是由 bss段、data段、text段三个组成的。这样的概念,不知道最初来源于哪里的规定,但在当前的计算机程序设计中是很重要的一个基本概念。而且在嵌入式系统的设计中也非常重要,牵涉到嵌入式系统运行时的内存大小分配,存储单元占用空间大小的问题。  
        •   
        •     在采用段式内存管理的架构中(比如intel的80x86系统),bss段(Block Started by Symbol segment)通常是指用来存放程序中未初始化的全局变量的一块内存区域,一般在初始化时bss 段部分将会清零。bss段属于静态内存分配,即程序一开始就将其清零了。  
        •   
        •     比如,在C语言之类的程序编译完成之后,已初始化的全局变量保存在.data 段中,未初始化的全局变量保存在.bss 段中。  
        •     在《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.  
        •     text和data段都在可执行文件中(在嵌入式系统里一般是固化在镜像文件中),由系统从可执行文件中加载;而bss段不在可执行文件中,由系统初始化。  
        •    
        • 例子: (windows+cl)  
        • 程序1:  
        • int ar[30000];  
        • void main()    ......  
        •   
        • 程序2:  
        • int ar[300000] =  {1, 2, 3, 4, 5, 6 };  
        • void main()    ......  
        •   
        • 发现程序2编译之后所得的.exe文件比程序1的要大得多。发现在程序1.asm中ar的定义如下:  
        • _BSS SEGMENT  
        •      ?ar@@3PAHA DD 0493e0H DUP (?)    ; ar  
        • _BSS ENDS  
        • 而在程序2.asm中,ar被定义为:  
        • _DATA SEGMENT  
        •      ?ar@@3PAHA DD 01H     ; ar  
        •                 DD 02H  
        •                 DD 03H  
        •                 ORG $+1199988  
        • _DATA ENDS  
        •   
        • 区别很明显,一个位于.bss段,而另一个位于.data段,两者的区别在于:全局的未初始化变量存在于.bss段中,具体体现为一个占位符;全局的已初始化变量存于.data段中;而函数内的自动变量(每个编译器都不同,cl是0xCCCCCCCC)都在栈上分配空间。.bss是不占用.exe文件空间的,其内容由操作系统初始化(清零);而.data却需要占用,其内容由程序初始化,因此造成了上述情况。  
        •    
        • (3)例子:一个很特殊的strcpy例子,可以让程序崩溃的:  
        • #include<stdio.h>  
        • #include<string.h>  
        • #include<ctype.h>  
        • void f(char* s){  
        •     int len=strlen(s);  
        •     char buf[len+1];  
        •     strcpy(buf,s);  
        •     printf("s=%s,buf=%s/n",s,buf);  
        •     strcpy(s,buf);  
        •     printf("after strncpy/n");  
        • }  
        • int main(void){  
        •         f("abc");  
        •         return 0;  
        • }  
        • ./a.out  
        • s=abc,buf=abc  
        • 段错误  
        • 如果我把main函数的内容改为  
        •         char b[]="abc";//堆栈分配  
        •         f(b);  
        • 运行就没有问题。  
        •    
        • 原因: “abc”是存在只读属性数据区,不能做strcpy的目的地  
        • 数组内存分配在栈上,可作修改,所以数组名可以做strcpy的第一个参数。  
        •    
        • (4)如果循环里面要用到某个类对象(默认构造函数),最好把对象的声明移动到循环外面,否则这个对象被初始化的次数就是循环的次数  
          • #include<stdio.h>  
          • class c{  
          • public:  
          •   c(){printf("ctor/n");}  
          • };  
          • int main(void){  
          •   int i=10;  
          •   while(i--){  
          •     c c1;  
          •   }  
          •   return 0;  
          • }  
          • 运行结果就是"ctor"被打印10次  
          • 返回页首  
          •   
          • (十一)数组和指针的异同  
          •     这个是C/C++中最容易混淆,最容易头晕的一个话题。  
          •    
          •     我们先从一个简单的例子看起(一维数组)  
          • void f(char* buf);    |         void f(char* buf);  
          • int main(...){        |         int main(...){  
          • char buf[]="abc";       |         char* pbuf="abc";  
          • f(buf);               |         f(pbuf);            ->相同的生成代码  
          • buf[2]='x';           |         pbuf[2]='x'         ->不同的生成代码  
          •    
          • 上面这两个程序有区别吗? 答案是:  
          • (1)对于一维数组的处理,传递参数的地时候统统作为指针来看待,也就是f(buf)的调用被编译器等效成了  
          • char* pbuf="abc",f(pbuf)这样的调用。  
          • (2)对于寻址和赋值:  
          • buf[2] 是编译器计算(buf的地址+2),放入x  
          • pbuf[2]是编译器计算pbuf的地址,得到pbuf中的值,再以这个值为基地址,+2,放入x  
          • 也就是说,pbuf的赋值语句是2次跳转的,效率比不上buf[2]这样的语句。  
          • --------------------------------------------------------------  
          •    考虑复杂一点的情况,**数组怎么办?  
          • int main(...){  
          •    int buf[2][3];//这个buf数组在内存中仍然是1维连续内存!  
          •    
          • 那么buf[10][10]=6;这样的语句是如何计算的呢? buf的结构被看成一个矩阵被拉直为行向量的表示,10行10列,buf[1][2]的地址就是:  
          •    第二行的起始(1*10)+第3个元素的偏移(2),等效于((int*)(buf))[12]。  
          •    这样说很清楚了吧,如果我们要把buf传递给一个函数作为参数,怎么办呢? 只需要保证编译器能看出,这个被拉直的,2维数组,每一行多少个元素:  
          •    void f(int buf[][10]){  
          •      buf[1][2]=6;//编译器能够通过f的形式参数声明来决定buf[1][2]是从buf偏移多少。  
          •    ...  
          • 上面这个声明和void f(int buf[10][10])甚至void f(int buf[20][10])是等效的。因为我们只需要知道每行包括多少个元素,至于有多少行,(数组多大),不是编译器控制的,是程序元的责任。  
          •    
          • --------------------------------------------------------------  
          •     如果f的声明是f(int buf[][])呢? 它等效于f(int *buf[])或者f(int ** ppbuf)这样的声明,传入参数必须是一个真正的2维数组。像下面这样  
          •     int** buf=new int*[10];  
          •     for(int i=0;i<10;++i)buf[i]=new int[10];  
          •     f(buf);  
          •     buf数组本身必须是一个指针数组,buf[1][2]这样的计算是:  
          • (a)计算buf[1]的值  
          • (b)这个值是一个地址,指向一个数组,取这个数组的偏移量2中的值。  
          •    
          • 如果我混用f(int buf[][10])和f(int buf[][]),我就会得到一个编译警告或者错误:  
          • void f2(int ppi[][2]){}  
          • void f3(int *ppi[2]){}  
          • int p2[3][2]={ {1,2},{3,4}, };  
          • f2(p2);正确的用法  
          • f3(p2);警告:传递参数 1 (属于 ‘f3’)时在不兼容的指针类型间转换。  
          •    
          •    由于f3的生成代码是2次跳转,因此传入p2作为参数的时候,会把一个真正的数组元素的值作为地址看待,再次计算一个内存地址偏移量中的值,可能导致程序崩溃。  
          •    再看一个程序,看看运行的结果是什么。  
          • int main(void)  
          • {  
          •     int arr[2][3] = {  
          •         {0,1,3},  
          •         {4,5,6}  
          •     };  
          •     int i1=(int)arr;  
          •     int i2=(int)(arr+1);  
          •     printf("i2-i1=%d/n",i2-i1);  
          •     printf("%x/n",arr+1);  
          •     printf("%x/n",*(arr+1));  
          •     printf("%d/n",**(arr+1)));  
          •     return 0;  
          • }  
          •         关于这个话题,最好的相关参考文献:《C专家编程》  
          • 返回页首  
          •   
          • (十二)const限定的传递性  
          • (1)如何理解复杂const的指针类型定义?  
          • char  * const cp; ( * 读成 pointer to ) 等效于const char* p  
          • cp is a const pointer to char  
          •   
          • const char * p;  
          • is a pointer to const char;  
          • 先向右看, 再向左看, thinking in C++ 说的很清楚了  
          •   
          • (2)const对于函数声明:  
          • 是个很严格的概念,const对象被调的过程必须保证其使用了带const的函数。例如:  
          • > cat t.cpp  
          • struct a{  
          •        int x;  
          •        bool operator==(const a& ia){return x==ia.x;}//这里是编译不过的!!!!!!!!  
          • };  
          • bool f(const a& ia, const a& ib){  
          •      return ia==ib;//因为这里的==操作了const a&,而operator==没有被定义为const函数  
          • }  
          • int main(int argc, char *argv[]){  
          •     return 0;  
          • }  
          • 问题解决的方案:  
          • bool operator==(const a& ia) const {return x==ia.x;}  
          • f中被比较的a类对象是const的,传递给operator==函数,函数不能改变它,因此==必须也是const的。  
          • 返回页首  
          •   
          • (十三)数据类型的限定性检查  
          • (1)使用C风格的初始化  
          • > cat t.cpp  
          • #include<stdio.h>  
          • struct e{//结构体有3个成员  
          •    int x;  
          •    int y;  
          •    e& operator=(const e& ie){*this=ie;}  
          •    ~e(){}  
          • };  
          • int main(void){  
          •    e buf[]={//用两个成员的{}来初始化  
          •       {1,2},  
          •       {1,3},  
          •       {1,4}  
          •    };  
          •    printf("%d %d %d/n",buf[0].y,buf[1].y,buf[2].y);  
          •    return 0;  
          • }  
          • 编译没有问题,但是如果增加了e的构造函数,编译就出错。  
          • 原因:只有那些没有定义构造函数且所有数据成员全部为public的类,才可以应用C风格的初始化方式(大括号方式),这是为了与C兼容  
          •    
          • (2)成员函数中的static变量,作用和类的static变量相同  
          • #include<stdio.h>  
          • struct B{  
          •   void inc(){  
          •     static int i=0;  
          •     printf("%d/n",++i);  
          •   }  
          • };  
          • int main(void){  
          •   B b1,b2;  
          •   b1.inc();  
          •   b2.inc();  
          • }  
          • > ./a.out  
          • 1  
          • 2  
          •    
          • (3)explicit的作用域  
          • class i{  
          • public:  
          •       int* a;int b;   
          •       i(int* x){ printf("ctor/n"); a=x; }  
          •       i(const i& ii){printf("copy ctor/n");a=ii.a;}   
          •       explicit i(){printf("ctor default/n");}  
          •       i& operator=(const i& ii){printf("operator/n"); a=ii.a;}  
          • };     
          • int main(int argc, char *argv[]){  
          •     i i1;  
          •     int x=20;  
          •     int *b=&x;  
          •     i1=b;  
          •     printf("i1.a=%d,p=%d/n",*(i1.a),i1.a);  
          •     return 0;  
          • }  
          • 程序像的输出是  
          • ctor default  
          • ctor  
          • operator  
          • i1.a=20,p=2293596  
          • 这里int* b=&x被隐式转换成了i的对象i1=b,但是我的无参数构造函数  
          • explicit i()...是加了explicit关键字的,为什么仍然编译通过并正确执行呢?  
          •   
          • 解释: explicit 只对有一个参数(或者有多个参数,但除了第一个,其他参数都有默认值)的构造函数起作用  
          •    
          • (4)dynamic_cast的有效性:  
          • 只要dynamic_cast输入的参数是一个内容正确的左值,哪怕它是其他类型的指针或者引用转型过来的,只要它本身内容正确(指向了正确的虚函数表),RTTI就能成功。  
          • #include <cstdlib>  
          • #include <cstdio>  
          • #include <typeinfo>  
          • using namespace std;  
          • class A{};  
          • class C{  
          • public:  
          •     virtual void g(){}  
          • };  
          • class D:public C{};  
          • int main(int argc, char *argv[])  
          • {  
          •     D d ;  
          •     A *a=(A*)&d;  
          •     C* pa=(C*)a;  
          •     C& pc=*pa;  
          •     try{  
          •         C& pc2=dynamic_cast<C&>(pc);  
          •         D& pd=dynamic_cast<D&>(pc);  
          •     }catch(bad_cast){  
          •         printf("bad_cast/n");  
          •     }catch(...){  
          •         printf("other exception/n");  
          •     }  
          •     return EXIT_SUCCESS;  
          • }  
          • 程序不会抛出任何异常。如果我把"D d"的声明改为"C d"的声明,"D& pd=dynamic_cast<D&>(pc)"就会抛出std::bad_cast异常  
          •    
          • Plus:  
          • dynamic_cast的输入参数如果无效,是指针是返回NULL,是引用时抛出bad_cast异常  
          •    
          • (5)union里面的struct必须是plain old data,不能含有ctor,dtor,operator=函数  
          • (6)#define宏定义的变量,在编译之后消失了,不利于理解程序合调试,因为没有符号存在。C++为了解决这个问题引入了enum类型,这个类型信息在编译时作为const常量存在,编译后仍然存在符号表信息,利于调试。  
          • #define MONDAY 1  
          • class b{  
          • public:  
          •   const static int i=0;//if not const, compile error  
          •   enum{friday=5};//equal to const int  
          • };  
          • const char* buf="abc";  
          • int main(void){  
          • buf="xyz";  
          • int x=b::friday;  
          • int y=MONDAY;  
          • return 0;  
          • }  
          • Plus: 注意类当中的static变量,如果加const可以在声明时初始化,不加const必须在类声明之外初始化。  
          • 返回页首  
          •   
          • (十四)使用STL时的类型限制  
          • (1)自定义迭代器需要注意的问题  
          • 下面这个这个程序的目的是,自定义一个迭代器的类型,模仿STL的访问方式,打印数组的全部内容。  
          • #include<cstdio>  
          • #include<cstdlib>  
          • #include<algorithm>  
          • #include<iterator>  
          • #include<iostream>  
          • using namespace std;  
          • class array{  
          •    int * pi;  
          • public:  
          •    array(){  
          •            pi=new int[5];  
          •            pi[0]=3;  
          •            pi[1]=44;  
          •            pi[2]=5;  
          •            pi[3]=1;  
          •            pi[4]=26;  
          •    }  
          •    virtual ~array(){  if(pi){delete[] pi;pi=0;}  }  
          •    class Iter{//自己实现的一个迭代器  
          •          int* data;  
          •       public:  
          •          Iter(int* i){data=i;}  
          •          Iter(){data=0;}  
          •          Iter& operator=(const Iter& i){data=i.data;}  
          •          bool operator!=(const Iter& i){return data!=i.data;}  
          •          int operator*(){return *data;}  
          •          void operator++(){++data;}  
          •          void operator--(){--data;}  
          •    };  
          •    Iter begin(){return Iter(π[0]);}  
          •    Iter end(){return Iter(π[5]);}  
          • };  
          • int main(int argc, char *argv[])  
          • {  
          •     array l;  
          •     array::Iter it;  
          •     for(it=l.begin();it!=l.end();++it){cout<<*it<<' ';}  
          •     cout<<'/n';  
          •     //copy(l.begin(),l.end(),ostream_iterator<int>(cout, "  ")); //不加这一句,运行没有问题  
          •     return 0;  
          • }  
          •   
          • ->问题:  
          • 我把上面那行注释了的"copy(l.begin(),l.end(),ostream_iterator<int>(cout, "  ")); "变成有效,编译就过不去了  
          • ->原因的解释:  
          • 因为,用于标准库算法的迭代器内需要定义有五个公有的类型:iterator_category, value_type, difference_type, distance_type, pointer, reference,其方法有两种:(1)手动定义这五个类型(2)从std::iterator继承  
          •     引用《C++程序设计(特别版)》里的一句话:  
          • “内部类型int *就是int[]的一个迭代器,类型list<int>::iterator是list类的一个迭代器”不是对内部类型没有要求,而是对内部类型的迭代器有一个默认的解释。iterator_traits 有关于指针类型的偏特化版本.  
          •    
          • (2)ostream_iterator, 传给cout的对象必须能强制转化为基本类型,或者重载<<  
          • #include<algorithm>  
          • #include<iterator>  
          • #include<iostream>  
          • using namespace std;  
          • struct e{    float v;char c;     
          • operator float()const {return v;}     
          • //operator char() const {return c;}  
          • //should conflict with operator float() };  
          • int main(int argc, char *argv[])  
          • {    int l=4;    e p[]={ {1.5,'x'}, {0.5,'a'}, {1.2,'b'}, {0.7,'y'}    };     
          • copy(p,p+l,ostream_iterator<e>(cout," "));     
          • cout<<'/n';     
          • return 0;}  
          • 上面的operator float()和operator char()只能用一个,因为互相冲突  
          •    
          • (3)friend的一个使用场景  
          • 例如,要设计一个单线程的简单singleton,我把 ctor,dtor,copyctor,"="重载ctor都声明为private, 用一个静态函数来创建instance。然后由于我只有创建函数没有销毁函数,我使用auto_ptr来声明这个对象,让编译器来完成对象的释放。  
          • class s{  
          •   static auto_ptr<s> pInst;  
          •   s(){}  
          •   ~s(){}  
          •   s(const s& os){printf("s.copy ctor/n");}  
          •   s& operator = (const s& os){printf("s.operator= called/n");}  
          • public:  
          •   static s& getInst(){  
          •      if(pInst.get()==0)  
          •          pInst.reset(new s());  
          •      return *pInst;  
          •   }  
          • };  
          • auto_ptr<s> s::pInst;  
          •   
          •     上面这个程序是编译不通过的,因为auto_ptr的析构函数去delete <s>,而s的析构函数是私有的,因此在s类的最后面我们还需要加上  
          •   friend class auto_ptr<s>;  
          • 这样的语句才能编译通过。一个替代的解决方案是不使用auto_ptr,而去使用atexit这样的函数注册一个销毁函数,让程序退出时系统自动调用。  
          •    
          • (4) class Iter:public std::iterator<bidirectional_iterator_tag, int>  
          • 这样的话就能  
          • copy(l.begin(),l.end(),ostream_iterator<int>(cout, "  "));来打印到标准输出  
          • 因为:  
          •     用于标准库算法的迭代器内需要定义有五个公有的类型:iterator_category, value_type, difference_type, distance_type, pointer, reference,其方法有两种:  
          • 1.手动定义这五个类型  
          • 2.从std::iterator继承  
          • 返回页首  
          •   
          • (十五)迭代器自身的类型  
          •         在用STL编写庞大程序的时候,如何才能知道一个迭代器指向的对象的真正类型呢? 能否把编译时确定的信息(特化的类型)保存下来以后可以用? 我们的法宝是使用一个iterator_traits对象,它是iterator的内置对象,保留了特化的类型。(通过typedef一个通用的名字来做到的)  
          •    
          • 对于stl::iterator_traits的一个非常好的解释: 它就是得到一系列的typedef来指示iterator指向对象的类型,原文来自http://msdn.microsoft.com/en-us/library/zdxb97eh(VS.80).aspx  
          • template<class Iterator>  
          •    struct iterator_traits {  
          •    typedef typename Iterator::iterator_category iterator_category;  
          •    typedef typename Iterator::value_type value_type;  
          •    typedef typename Iterator::difference_type difference_type;  
          •    typedef typename Iterator::pointer pointer;  
          •    typedef typename Iterator::reference reference;  
          •    };  
          • template<class Type>  
          •    struct iterator_traits<Type*> {  
          •    typedef random_access_iterator_tag iterator_category;//那种类型的迭代器  
          •    typedef Type value_type;//--------------->最关键的地方!!!!保存类型信息!!!!  
          •    typedef ptrdiff_t difference_type;  
          •    typedef Type *pointer;  
          •    typedef Type& reference;  
          •    };  
          • template<class Type>  
          •    struct iterator_traits<const Type*> {  
          •    typedef random_access_iterator_tag iterator_category;  
          •    typedef Type value_type;  
          •    typedef ptrdiff_t difference_type;  
          •    typedef const Type *pointer;  
          •    typedef const Type& reference;  
          •    };  
          • 例子程序  
          • // iterator_traits.cpp  
          • // compile with: /EHsc(该选项仅对于VC编译器)  
          • #include <iostream>  
          • #include <iterator>  
          • #include <vector>  
          • #include <list>  
          • using namespace std;  
          • template< class it >  
          • void  
          • function( it i1, it i2 )  
          • {  
          •    iterator_traits<it>::iterator_category cat;  
          •    cout << typeid( cat ).name( ) << endl;  
          •    while ( i1 != i2 )  
          •    {  
          •       iterator_traits<it>::value_type x;  
          •       x = *i1;  
          •       cout << x << " ";  
          •       i1++;  
          •    };    
          •    cout << endl;  
          • };  
          • int main( )  
          • {  
          •    vector<char> vc( 10,'a' );  
          •    list<int> li( 10 );  
          •    function( vc.begin( ), vc.end( ) );  
          •    function( li.begin( ), li.end( ) );  
          • }  
          • Output:  
          • struct std::random_access_iterator_tag  
          • a a a a a a a a a a  
          • struct std::bidirectional_iterator_tag  
          • 0 0 0 0 0 0 0 0 0 0  
          •   
          • Plus:  
          •    iterator不但可以用来访问元素,也可以用于赋值  
          • typedef vector<int> vi;  
          • vi v(3);  
          • vi::iterator it=v.begin();  
          • for(it;it!=v.end();++it)*it=9;  
          • copy(v.begin(),v.end(),ostream_iterator<int>(cout,"_"));  
          • 返回页首  
          •   
          • (十六)运行时的类型信息  
          • (1)typeid的作用,可以得到动态运行时的信息(对于多态类)  
          • >cat type.cpp  
          • #include<iostream>  
          • using namespace std;  
          • class Base {  
          • public:  
          •    virtual void vvfunc() {}  
          • };  
          • class Derived : public Base {};  
          • using namespace std;  
          • int main() {  
          •    Derived* pd = new Derived;  
          •    Base* pb = pd;  
          •    cout << typeid( pb ).name() << endl;   //prints "class Base *"   静态信息  
          •    cout << typeid( *pb ).name() << endl;   //prints "class Derived" 动态信息  
          •    cout << typeid( pd ).name() << endl;   //prints "class Derived *"静态信息  
          •    cout << typeid( *pd ).name() << endl;   //prints "class Derived" 动态信息  
          •    delete pd;  
          • }  
          • 在solaris上面CC的输出结果是  
          • > ./a.out  
          • Base*  
          • Derived  
          • Derived*  
          • Derived  
          •   
          • typeid 将返回一个派生类的type_info引用。但是expression必须指向一个多态类,否则返回的将是静态类信息。此外,指针必须被提领,以便使用它所指向的对象,没有提领指针,结果将是指针的type_info(这是一个静态信息),而不是它所指向的对象的type_info  
          •   
          • (2)static_cast能够处理类型运算符重载并解析  
          • 一个类,重载(char*)强制类型转换运算符,当我使用static_cast<char*>()的时候,该重载仍然是有效的。  
          • #include <cstdio>  
          • #include <cstdlib>  
          • #include <iostream>  
          • using namespace std;  
          • struct s{  
          •       char buf[4];  
          •       s(){strcpy(buf,"abc");}  
          •       operator char*(){return "kkk";}  
          • };  
          • struct c{  
          •       char *buf;  
          •       c(){buf="xyz";}  
          • };  
          • int main(void){  
          •     s s1;  
          •     printf("string1 =%s/n",&s1);//打印字符串,效果同s.buf   
          •     c c1;  
          •     printf("string2 =%s/n",*((char**)&c1));//打印字符串  
          •     printf("string3 =%s/n",(char*)s1);  
          •     cout<<static_cast<char*>(s1)<<'/n';//这里,重载的(char*)起了作用   
          •     return 0;  
          • }  
          •    
          • (3)虚函数表的存储结构研究:  
          • #include<stdio.h>  
          • class B{//对于含有虚函数的类,内存结构中的首元素是指向虚表的指针。  
          •       int x;  
          •       virtual void f(){printf("f/n");}  
          •       virtual void g(){printf("g/n");}  
          •       virtual void h(){printf("h/n");}  
          • public:  
          •       explicit B(int i) {x=i;}  
          • };  
          • typedef void (*pf)();  
          • int main(void){  
          •   B b(20);  
          •   int * pb=(int*)&b;  
          •   printf("private x=%d/n",pb[1]);  
          •   pf *pvt=(pf*)pb[0];//虚函数表指针是b的第一个元素,它指向一个保存指针的表  
          •   pf f1=(pf)pvt[0];  
          •   pf f2=(pf)pvt[1];  
          •   pf f3=(pf)pvt[2];  
          •   (*f1)();  
          •   (*f2)();  
          •   (*f3)();  
          •   printf("pvt[3]=%d/n",pvt[3]);//虚函数表结束符号,gcc是0  
          •   return 0;  
          • }  
          • 程序输出  
          • private x=20  
          • f  
          • g  
          • h  
          • pvt[3]=0  
          • 返回页首  
          •   
          • (十七)new/delete重载  
          • (1)new和delete运算符的重载,可以用来跟踪代码中内存申请和释放的过程。  
          • 下面的例子是重载类中的new和delete  
          • class a{  
          • public:  
          •        voidoperator new(size_t){  
          •              printf("a::new/n");  
          •              return ::new a;  
          •        }  
          •        voidoperator new[](size_t l){  
          •              printf("a::new[%d]/n",l);  
          •              return ::new a[l];  
          •        }  
          •        void operator delete(void* p){  
          •              printf("a::delete/n");  
          •              ::delete (a*)p;  
          •        }  
          •        void operator delete[](void* p){  
          •              printf("a::delete[]/n");  
          •              ::delete[] (a*)p;  
          •        }  
          • };  
          • int main(void){  
          •    a* pa=new a;  
          •    delete pa;  
          •    pa=new a[2];  
          •    delete[] pa;   
          •    return 0;  
          • }  
          • 输出  
          • > CC t.C && ./a.out  
          • a::new  
          • a::delete  
          • a::new[2]  
          • a::delete[]  
          •   
          • (2)replacement new需要注意的地方。例如  
          • class c{  
          •    int x;  
          • public:  
          •    explicit c(int ix){x=ix;printf("ctor/n");}  
          •    ~c(){printf("dtor/n");}  
          • };  
          • int main(void){  
          •    try{  
          •       char mem[sizeof(c)*2];  
          •       c* pc1=new(mem) c(2);  
          •       c c3(4); //加上这句以后,delete pc1就是非法退出, 不加这句就没事...........................................  
          •       delete pc1;//不能去delete内存池中的东西,否则出错 ????????  
          •       //pc1->~c();//用显示调用析构函数而不用delete总是安全的。  
          •    }catch(...){  
          •       printf("get exception/n");  
          •    }  
          •    return 0;  
          • }  
          • 上面的c* pc1=new(mem) c(2);  
          • 和delete pc1;//不能去delete内存池中的东西,否则出错 ????????  
          • 这种方式就是错误的,因为你用的是new的放置语法,而放置语法要求显式的调用析构函数,同时不用的内存需要自己释放时可以free掉,但是在堆栈上的自己费心。更多详细资料可以问问《C++程序设计语言(特别版)第2版》  
          •    
          • (3)在很多实现中,不考虑构造和析构的话,new/malloc,delete/free是等效的,举VC的例子  
          • #if !_VC6SP2 || _DLL  
          • void *__CRTDECL operator new[](size_t count) _THROW1(std::bad_alloc)  
          •         {        // try to allocate count bytes for an array  
          •         return (operator new(count));  
          •         }  
          • #endif /* !_VC6SP2 || _DLL */  
          •   
          • _C_LIB_DECL  
          • int __cdecl _callnewh(size_t size) _THROW1(_STD bad_alloc);  
          • _END_C_LIB_DECL  
          •   
          • void *__CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)  
          •         {       // try to allocate size bytes  
          •         void *p;  
          •         while ((p = malloc(size)) == 0)  
          •                 if (_callnewh(size) == 0)  
          •                 {       // report no memory  
          •                 static const std::bad_alloc nomem;  
          •                 _RAISE(nomem);  
          •                 }  
          •   
          •         return (p);  
          •         }  
          •    
          • (4)在重载的operator new调用的时候,如果分配的是有析构函数的对象数组,那么传进来的size_t会多出一个整数字节的大小,用于记录数组大小(delete[] 需要循环调用各对象的析构函数)  
          • 下面这个小程序是重载全局的new/delete操作符来实现对象的分配和释放:  
          • #include <new>  
          • #include <cstdio>  
          • #include <cstdlib>  
          • using namespace std;  
          • voidoperator new(size_t size) throw(bad_alloc)  
          • {  
          • printf("operator new:%d Byte/n",size);  
          • void* m=  malloc(size);  
          • if(!m) puts("out of memory");  
          • return m;  
          • }  
          • void operator delete(void* m)throw(){  
          • puts("operator delete");  
          • free(m);}  
          • class B{  
          • int s;  
          • public:  
          • B(){/*puts("B::B()");*/}  
          • ~B(){/*puts("B::~B()");*/}  
          • };  
          • int main(int argc, char* argv[]){  
          • int* p = new int(4);  
          • delete p;  
          • B* s = new B;  
          • delete s;  
          • B* sa = new B[10];  
          • delete []sa;  
          • int* pi=new int[3];  
          • delete []pi;  
          • return 0;  
          • }  
          •   
          • 程序的输出是  
          • > gcc n.C && ./a.out  
          • operator new:4 Byte  
          • operator delete  
          • operator new:4 Byte  
          • operator delete  
          • operator new:48 Byte         ->问题出在这里,new为类指针数组分配的时候,4x10应该是10个字节,多出来的8个字节是做什么的?  
          • operator new:12 Byte  
          • 回答:  
          • 是编译的时候就做到了.  
          •   
          • 如:  
          •     class B xxxxxxxxxxxxx;  
          •     p=new B[num];  
          • 那么编译器会处理成(注意:不同的编译器会有所不同):  
          •   +--------------------------------------------------------------+  
          •    |num|var[0]|var[1]|var[2]|var[3]|........|var[num-1]|  
          •   +--------------------------------------------------------------+  
          •   
          •         push        n               ;n=num*var_size+4  
          •         call           我重载的new         
          •         ....................................  
          •         push        B::~B()的地址  
          •         push        B::B()的地址  
          •         *((int*)p)=num;  
          •         ((int*)p)++;  
          •         push        num  
          •         push        var_size       
          •         push    p  
          •         call          vector_constructor_iterator  ;这里会循环调用B::B(),次数是num  
          •   +--------------------------------------------------------------+  
          • 对类类型,delete一个数组时(比如,delete []sa;),要为每一个数组元素调用析构函数。但对于delete表达式(比如,这里的delete []sa),它并不知道数组的元素个数(只有new函数和delete函数知道)。因此,必须有一种手段来告诉delete表达式的数组大小是多少。那么一种可行的方式就是,多分配一个大小为4字节的空间来记录数组大小,并可以约定前四字节来记录大小。那么,由new函数分配的地址与new表达式返回的地址应该相差4个字节(这可以写程序来验证)。对于非类类型数组和不需要调用析构函数的类类型数组,这多于的四字节就不需要了。  
          •    
          • (5)同理,可以重载全局的new/delete,形如  
          • voidoperator new( size_t size ){  
          •     if( 0 == size ) // 注意!!!!  
          •         size = 1;  
          •     while(1){  
          •         分配size字节内存;  
          •         if(分配成功)  
          •             return 指向内存的指针;  
          •         new_handler g= set_new_handler(0);  
          •         set_new_handler(g);  
          •         if( g)(*g)();  
          •         else throw std::bad_alloc();  
          •     }  
          • }  
          • void operator delete( void* p){  
          •     if( 0 == p) // 须要注意  
          •         return;  
          •     ...  
          • }  
          • 上面的new_handler是用户自定义的全局set_new_handler处理函数,newhandler形式是:  
          • void mynewhandler(){  
          •         if( 使得operator new成功 )  
          •         {  
          •             例如等待一段时间,再次分配内存  
          •             return;  
          •         }  
          •         // 主动退出  
          •         或 abort/exit 直接退出程序  
          •         或 set_new_handler(其他newhandler或者0);  
          •         或 set_new_handler(0)  
          •         或 throw bad_alloc()//比较好   
          • }  
          • 返回页首  
          •   
          • (十八)如何拷贝一个文件----标准C/C++运行库里面没有拷贝文件的函数,必须自己完成  
          • (1)标准c的逐字节拷贝  
          • #include<stdio.h>  
          • int main(void){  
          •   FILE* pin=fopen("in.data","rb");  
          •   FILE* pout=fopen("out.data","wb");  
          •   int c;  
          •   while((c=fgetc(pin))!=EOF){  
          •     fputc(c,pout);}  
          •   fclose(pin);  
          •   fclose(pout);  
          •   return 0;  
          • }  
          • (2)Iostream的多字节拷贝  
          • #include<iostream>  
          • #include<fstream>  
          • using namespace std;  
          • int main(void){  
          •   ifstream fi;  
          •   fi.open("in.data",ios::binary);  
          •   ofstream fo;  
          •   fo.open("out.data",ios::binary);  
          •   char buf[1024];  
          •   do{  
          •     fi.read(buf,sizeof(buf));  
          •     fo.write(buf,fi.gcount());  
          •   }while(fi.good());  
          •   fi.close();  
          •   fo.close();  
          •   return 0;  
          • }  
          • 可以把do-while的循环用一句话代替: fo<<fi.rdbuf()  
          • (3)STL算法拷贝,逐字节进行  
          • #include<fstream>  
          • #include<iterator>  
          • #include<algorithm>  
          • using namespace std;  
          • int main(void){  
          •   ifstream fi;  
          •   fi.open("in.data",ios::binary);  
          •   ofstream fo;  
          •   fo.open("out.data",ios::binary);  
          •   copy(istreambuf_iterator<char>(fi),istreambuf_iterator<char>(),ostreambuf_iterator<char>(fo));  
          •   fi.close();  
          •   fo.close();  
          •   return 0;  

}  

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值