添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接
相关文章推荐
苦恼的闹钟  ·  传百年匠心雅韵 ...·  4 月前    · 
急躁的番茄  ·  C# ...·  5 月前    · 
完美的双杠  ·  An Unfortunate ...·  6 月前    · 

描述 C++ 的内存分配方式以及它们之间的区别?

在 C++ 中, 内存区分为5个区, 分别是: 堆, 栈, 自由存储区, 全局/静态存储区, 常量存储区.

  • 栈: 函数内的局部变量的存储单元都会在栈上创建, 函数执行结束时这些存储单元会被自动释放
  • 堆: 也称为动态内存. 程序在运行的时候用 malloc 申请任意大小的内存, 程序员自己负责在何时使用和释放这些内存. 动态内存的生存期由程序员自己决定, 使用非常灵活, 但相关的内存泄漏问题也尝尝发生.
  • 自由存储区域: 程序在运行时候利用 new 申请的内存空间.
  • 全局/静态存储区域: 存储全局变量和静态变量, 处于该内存的变量在程序的整个运行期间都一直存在.
  • 常量存储区: 存储常量字符串, 程序结束后由系统释放.
  • 自由存储区和堆的区别

    从技术上来说, 堆(heap)是 C 语言和操作系统的术语, 它是操作系统所维护的一块特殊内存, 提供了动态分配的功能, 当运行程序调用 malloc() 时就会从堆中分配内存, 之后会调用 free() 将内存归还. 而自由存储区是 C++ 中通过 new delete 动态分配和释放对象的 抽象概念 , 通过 new 来申请的内存区域成为自由存储区. 实际上, 基本所有的 C++ 编译器都默认使用堆来实现自由存储区 , 也就是说, 缺省的 全局运算符 new delete 大多会通过 malloc free 来实现, 这时候我们说 new 申请的内存在自由存储区上或者在堆中都是正确的. 但是程序员可以通过重载 new delete 运算符, 来改用其他内存实现自由存储区, 这时候自由存储区就区别于堆了. 我们所需要记住的是: 堆是操作系统维护的一块特殊内存, 而自由存储区是 C++ 中通过 new delete 动态分配和释放对象的抽象概念. 二者并不完全等价.

    new 和 malloc 的区别

    (1). 申请的内存位置
    new 操作符从 自由存储区 上为对象动态分配内存空间, 而 malloc 函数从 上动态分配内存. 自由存储区是 C++ 基于 new 操作符的一个抽象概念, 凡是通过 new 操作符进行内存申请, 那么该内存就是自由存储区. 而堆是操作系统中的术语, 是操作系统所维护的一块特殊内存, 用于程序的内存动态分配, C 语言使用 malloc 从堆上分配内存, 使用 free 释放已经分配的对应内存.
    自由存储区是否在堆上(问题等价于 new 申请的内存是否在堆上), 这取决于 new 运算符的实现细节. 自由存储区不仅可以是堆, 还可以是静态存储区, 这主要看 new 实现时在哪里为对象分配内存.

    (2). 返回类型安全性
    new 运算符内存分配成功时, 返回的是对象类型的指针, 类型严格与对象匹配, 无序进行类型转换, 故 new 是类型安全的运算符. 而 malloc 内存分配成功时返回的是 void * , 需要通过强制类型转换将 void * 指针转换成需要的类型.

    (3). 内存分配失败时的返回值
    new 内存分配失败时, 会抛出 bad_alloc 异常;
    malloc 分配内存失败时会返回 NULL .
    因此, 二者在判断是否分配成功时的代码逻辑不太, 如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // malloc
    int* a = (int*)malloc(sizeof(int));
    if (a == NULL) {
    ...
    } else {
    ...
    }

    // new
    try {
    int* a = new int();
    } catch (bad_alloc) {
    ...
    }

    (4). 是否调用构造函数/析构函数
    new delete 会调用对象的构造函数/析构函数以完成对象的构造/析构. 而 malloc 则不会.

    (5). 对数组的处理
    C++ 提供了 new[] delete[] 来专门处理数组类型. 但是对于 malloc free 来说, 它并不关心分配的内存是否为数组, 需要程序员自行决定参数的合理设置.

    delete 会调用对象的析构函数, 和new对应

    free 只会释放内存, 和malloc.

    malloc/free是C++/C 语言的标准库函数, new/deletec是C++语言的运算符, 因此, 我们可以通过重载 new delete 运算符, 来完成自定义的功能.

    它们都可用于申请动态内存和释放内存, 对于非内部数据类型的对象而言, 光用 malloc/free 无法满足动态对象的要求, 对象在创建的同时要自动执行构造函数, 在消亡之时要自动执行析构函数, 由于malloc/free是库函数而不是运算符, 因此不在编译器控制权限之内, 不能够把执行构造函数和析构函数的任务强加于malloc/free. 因此C++语言需要一个能完成动态内存分配和初始化工作的运算符new, 以及一个能完成清理与释放内存工作的运算符delete.

    new 运算符, operator new, new 表达式

    运算符 new new[] 实际上是分别调用了如下函数:

    1
    2
    void * operator new(std::size_t);
    void * operator new[](std::size_t);

    当我们在使用一个 new 的时候, 它就变成了一个表达式的形式, 如下所示:

    1
    Test* test = new Test;

    new 表达式的行为主要有两步:

  • 执行 operator new() 函数, 在堆空间中搜索合适的内存并进行分配
  • 调用相应的构造函数构造对象, 初始化这块内存空间.
  • delete 和 delete []的区别

    delete只会调用一次析构函数, 而delete[]会调用数组内每一个成员的析构函数. 在More Effective C++中有更为详细的解释: 当delete操作符用于数组时, 它为每个数组元素调用析构函数, 然后调用operator delete来释放内存

    在对 内建数据类型 使用时, delete和delete[]是等价的, 因此delete[]会调用数组元素的析构函数, 但是内部数据类型没有析构函数, 所以可以直接使用.

    如何限制一个类对象只在栈(堆)上分配空间?

    在C++中,类的对象建立分为两种,一种是静态建立,如 A a ;另一种是动态建立,如 A* ptr=new A ;这两种方式是有区别的。

  • 静态建立类对象:是由编译器为对象在栈空间中分配内存,是通过直接移动栈顶指针,挪出适当的空间,然后在这片内存空间上调用构造函数形成一个栈对象。使用这种方法,直接调用类的构造函数。
  • 动态建立类对象:是使用new运算符将对象建立在堆空间中。这个过程分为两步,第一步是执行operator new()函数,在堆空间中搜索合适的内存并进行分配;第二步是调用构造函数构造对象,初始化这片内存空间。这种方法,间接调用类的构造函数。
  • (1). 只在对上分配类对象
    就是不能静态建立类对象, 即不能直接调用类的构造函数.
    首先要知道, 当对象建立在栈上面时, 是由编译器分配内存空间的, 当对象使用完以后, 编译器会调用析构函数来释放对象所占的空间. 实际上, 编译器在为类对象分配栈空间时, 会检查类的析构函数的访问性(其他非静态函数也会检查), 如果类的析构函数是私有的, 则编程器不会在栈空间上为类对象分配内存 . 因此, 我们只需要将析构函数设为私有, 类对象就无法建立在栈上了, 如下所示:

    1
    2
    3
    4
    5
    6
    7
    class A{
    public:
    A(){}
    void destroy(){delete this;}
    private:
    ~A(){}
    }

    注意 , 由于 new 表达式会在分配内存以后调用构造函数, 因此构造函数必须是公有的, 同时, 由于 delete 此时无法访问私有的析构函数, 因此必须提供一个 destroy 函数, 来进行内存空间的释放.

    存在问题:

  • 无法解决继承问题: 为了实现多态, 析构函数通常要设为 virtual , 因此析构函数不能设为 private , 此时我们可以使用 protected , 这样, 子类可以访问析构函数, 而外部无法访问.
  • new destroy 的对应关系容易引起误解, 解决办法是将构造函数也设置为 protected , 然后提供一个 create 函数和 destroy 对应.
  • (2). 只在栈上分配类对象
    只有使用new运算符,对象才会建立在堆上,因此,只要禁用new运算符就可以实现类对象只能建立在栈上。
    虽然你不能影响new operator的能力(因为那是C++语言内建的),但是你可以利用一个事实:new operator 总是先调用 operator new,而后者我们是可以自行声明重写的。

    因此,将operator new()设为私有即可禁止对象被new在堆上。

    代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    class A  
    {
    private:
    void* operator new(size_t t){} // 注意函数的第一个参数和返回值都是固定的
    void operator delete(void* ptr){} // 重载了new就需要重载delete
    public:
    A(){}
    ~A(){}
    };