C++知识点总结(三)

19.C++内存分区

C++中的内存分区,分别是堆、栈、全局/静态存储区、常量存储区和代码区,如下图所示:

每个区域的含义如下:

  • 栈:存储函数参数和局部变量,函数执行结束时自动释放,效率较高。

  • 堆:堆是程序中预留的一块内存空间,由程序员自己管理,需要手动申请和释放,空间较大,但是操作不当会出现内存泄露和内存碎片。

  • 全局/静态存储区:分为.bss和.data两个区域,其中.bss存储未初始化的全局变量和静态变量,.data存储已经初始化的全局变量和静态变量。

  • 常量存储区:存储常量,不允许修改。

  • 代码区:存放程序的二进制代码。

 

20.内存对齐

20.1 内存对齐的概念

1.内存对齐规则

    内存对齐:不同类型的数据在内存中按照一定的规则排列,而不一定是顺序的一个接一个的排列。

    内存对齐规则:

    结构体中,每个变量相对于起始位置的偏移量必须是该变量类型大小的整数倍,不是整数倍空出内存,直到偏移量是整数倍为止。第一个成员起始于0偏移处,结构体总长度必须为里面变量类型最大值的整数倍

    #pragma pack能够改变编译器的默认内存对齐方式,方式为:#pragma pack(n)(n为数字)

    添加了#pragma pack(n)后规则就变成了下面这样:

  • 偏移量要是n和当前变量大小中较小值的整数倍。

  • 整体大小要是n和最大变量大小中较小值的整数倍。

  • n值必须为1,2,4,8…,为其他值时就按照默认的分配规则

    例如:

//32位系统
#pragma pack(2)
struct 
{
    char c1;
    int i;
    char c2;
}s;
​
int main()
{
    printf("%d\n", sizeof(s));  // 不加#pragma pack(2)输出12,加上#pragma pack(2)输出8
​
    return 0;
}
​

按照默认规则,

    1)sizeof(c1) = 1 <= 4(有效对齐位),按照1字节对齐,占用第0单元;

    2)sizeof(i) = 4 <= 4(有效对齐位),相对于结构体首地址的偏移要为4的倍数,占用第4,5,6,7单元;

    3)sizeof(c2) = 1 <= 4(有效对齐位),相对于结构体首地址的偏移要为1的倍数,占用第8单元;

    4)此时结构体s占9个字节,但总长度必须是4的倍数,所以结构体s占用12个字节;

按照#pragma pack(2)规则

    1)sizeof(c1) = 1 按照1字节对齐,占用第0单元;

    2)sizeof(i) = 4,相对于结构体首地址的偏移要为2的倍数,占用第2,3,4,5单元;

    3)sizeof(c2) = 1,相对于结构体首地址的偏移要为1的倍数,占用第6单元;

    4)此时结构体s占7个字节,但总长度必须是4的倍数,所以结构体s占用8个字节;

2.为什么要内存对齐

    因为CPU的读取不是连续的,而是分成块读取的,块的大小只能是1、2、4、8、16字节,读取的数据未对齐,性能会大大折扣。

20.2 C++11 alignas与 alignof关键字

    C++11 引入了aligna和alignof关键字,其中alignof可以计算出类型的对齐方式,alignas可以指定结构体的对齐方式(不能小于默认对齐方式)。使用示例如下:

struct s
{
    char c1;
    int i;
    char c2;
};
​
struct alignas(8) s2
{
    char c1;
    int i;
    char c2;
};
​
int main()
{
    cout << alignof(s) << endl;    //4
    cout << sizeof(s) << endl;     //12
    cout << alignof(s2) << endl;   //8
    cout << sizeof(s2) << endl;    //16
​
    return 0;
}    

    上述示例中,s是按照默认规则对齐的,所以 alignof(s) = 4,使用 alignas 将s2按照8字节对齐,所以alignof(s) = 8

 

21.内存泄露和内存溢出

    内存泄露:内存泄露是指程序中已动态分配的堆内存没有释放和回收,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果,内存泄漏多了会导致可用内存空间变小,进一步造成内存溢出。

    内存溢出:内存溢出往往是没有足够的内存空间供程序使用,原因可能如下:

    1)内存中加载的数据过于庞大;

    2)代码中存在死循环;

    3)递归调用太深,导致堆栈溢出等;

    4)内存泄漏最终导致内存溢出;

 

22.this指针

1)this指针是类的指针,指向当前对象的首地址,要用—>来访问当前对象的所有成员变量或成员函数,所谓当前对象,是指正在使用的对象。

2)this 实际上是成员函数的一个形参,不过 this 这个形参是隐式的,它并不出现在代码中,而是在编译阶段由编译器默默地将它添加到参数列表中。在调用成员函数时将对象的地址作为实参传递给 this,实际上,成员函数默认第一个参数为T * const this

例如:

class Test
{
public: 
    int func(int a, int b){}   //在编译器里,func应该是:int func(Test* const this, int a, int b);
};
​
int main()
{
    Test t;
    t.func(1, 2);    //编译器会将对象的地址作为实参传给this,即:Test::func(&t, 1, 2);
}

3)this在成员函数的开始前构造,在成员函数的结束后清除,这个生命周期同任何一个函数的参数是一样的。

4)只有当对象被创建后 this 才有意义,所以this指针只能在成员函数内部使用,在全局函数、静态成员函数中都不能用this

5)通过 this 可以访问类的所有成员,包括 private、protected、public 属性的。

6)this 是 const 指针,它的值是不能被修改的,一切企图修改该指针的操作,如赋值、递增、递减等都是不允许的。

7)在类的非静态成员函数中返回类对象本身的时候,直接使用 return *this

8)如果出现成员函数的参数和成员变量重名,只能通过 this 区分。以成员函数setname(char *name)为例,它的形参是name,和成员变量name重名,如果写作name = name;这样的语句,就是给形参name赋值,而不是给成员变量name赋值。而写作this -> name = name;后,=左边的name就是成员变量,右边的name就是形参,一目了然。

9)在成员函数中调用delete this,则类对象的空间被释放,在delete this之后进行的其他任何函数调用,只要不涉及到this指针的内容,都能够正常运行。一旦涉及到this指针,如操作数据成员,调用虚函数等,就会出现不可预期的问题。

10)在析构函数中调用delete this,会导致堆栈溢出。因为delete的本质是为将被释放的内存调用一个或多个析构函数,然后,释放内存。显然,delete this会去调用本对象的析构函数,而析构函数中又调用delete this,形成无限递归,造成堆栈溢出,系统崩溃。

 

 

参考:

  1. 《C++ Primer 第5版》

  2. 《深入理解C++对象内存模型》

  3. 《Effective C++》

  4. http://c.biancheng.net/cplus/

posted @ 2021-11-03 23:05  烟消00云散  阅读(79)  评论(0编辑  收藏  举报