zoukankan      html  css  js  c++  java
  • C/C++内存与运行时深入研究(1)

    C/C++内存与运行时深入研究
    -----------------------------------------------------------------------------------
    (一)整数符号的陷阱
    (二)浮点数的本质
    (三)堆栈的内存管理结构
    (四)符号解析
    (五)对齐和总线错误
    (六)函数指针
    (七)虚函数的实现机理
    (八)引用的实现机理
    (九)虚拟继承对象的内存结构
    (十)混合编程时的初始化顺序
    (十一)数组和指针的异同
    (十二)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++里面有指针,所以所谓的public,private在强类型转换面前没有意义。我们总是可以拿到私有的成员变量。 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() ......

    作者:BuildNewApp
    出处:http://syxchina.cnblogs.comBuildNewApp.com
    本文版权归作者、博客园和百度空间共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则作者会诅咒你的。
    如果您阅读了我的文章并觉得有价值请点击此处,谢谢您的肯定1。
  • 相关阅读:
    windows 按时自动化任务
    Linux libusb 安装及简单使用
    Linux 交换eth0和eth1
    I.MX6 GPS JNI HAL register init hacking
    I.MX6 Android mmm convenient to use
    I.MX6 GPS Android HAL Framework 调试
    Android GPS GPSBasics project hacking
    Python windows serial
    【JAVA】别特注意,POI中getLastRowNum() 和getLastCellNum()的区别
    freemarker跳出循环
  • 原文地址:https://www.cnblogs.com/syxchina/p/2197404.html
Copyright © 2011-2022 走看看