【C++】[C++ 自由存储区与动态内存分配] C++ 如何判断什么时候使用堆,什么时候用栈,什么时候用new

前言

CSDN的文章像寄生虫,不管怎么搜都是重复的、低质量的、互相抄的、只有标题的、还有一堆点进去是跳转链接的、还全他娘标的“原创的”、“原创”的文章内容告诉你如下图,图都他娘的没有,抄都抄不全,最关键的是传播错误知识,让包括我在内的众多用户消化了很多错误知识,多走了很多歪路。因此,希望这篇文章能对CSDN的质量有一丝丝的提升,也包括了些自己的看法和经验,权当抛砖引玉。

C++的内存管理(五区)

理解什么时候用堆或栈,我们先回顾下基础知识,在C++中,内存分成5个区,他们分别是堆、栈、自由存储区、全局/静态存储区和常量存储区。

在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
注:静态局部变量位于全局静态区(腾讯一面)

由malloc分配的内存块,动态分配资源,由我们的应用程序去控制,一个malloc就要对应一个free,如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。

这地方面试会考你内存泄漏的概念。引申出来C++11的智能指针或操作系统对内存碎片的处理都是常考内容。(大厂常考智能指针的底层原理)

自由存储区

C++的概念,new所申请的内存则是在自由存储区上,使用delete来释放。

你甚至能在CSDN搜到完全相反的定义(谣言:自由存储区是malloc出来的)。自由存储区在c++规范里说的很模糊,,具体的处理是靠编译器来的,大部分情况下能当成堆。这话可不是我瞎说的,我找到了一篇关于c++标准的文章
http://www.gotw.ca/gotw/009.htm
里面明确提到在这里插入图片描述
翻译过来即

自由存储是两个动态内存区域之一(另一个说的是堆),由 new/delete
分配/释放。对象生命周期可以小于分配存储的时间;也就是说,空闲存储对象可以在不立即初始化的情况下分配内存,并且可以在不立即释放内存的情况下销毁。

这里我们还能看到其和堆的区别:

堆是另一个动态内存区域,
由 malloc/free 及其
变体分配/释放(也就是说封装了malloc的操作符等)。请注意,虽然默认的全局
new 和 delete 可能
由特定编译器根据 malloc 和 free 实现,但
堆与空闲存储不同,
在一个区域中分配的内存不能在另一个区域中安全地
释放。从堆分配的内存可以
通过 placement-new 构造和显式销毁
如果这样使用,关于自由存储区生命周期的注释在这里同样适用。

也就是说,全靠编译器,基本上,大部分C++编译器默认使用堆来实现自由存储,这样的编译器编译出来的对象,说它在堆上也对,说它在自由存储区上也正确。但程序员也可以通过重载操作符,改用其他内存来实现自由存储,例如全局变量做的对象池,这时自由存储区就区别于堆了。(腾讯面试会问你如何改变分配对象的分配区,知识点就是这块)

全局/静态存储区

全局变量和静态变量被分配到同一块内存中,在以前的C语言中,全局变量又分为初始化的和未初始化的,在C++里面没有这个区分了,他们共同占用同一块内存区。

常量存储区

这是一块比较特殊的存储区,他们里面存放的是常量,不允许修改。

除此之外你还能看到分成其他区的,类似“代码区”、“BSS段”这类词,都不是C++层面的,别记。

堆与栈的使用

接下来的叙述,我们将自由存储区视为堆,或称这俩为动态内存分配。

为什么要使用动态内存分配

缺点:速度较慢并且可能导致内存泄漏或内存碎片。
优点:动态分配限制较少。

使用动态分配的两个主要原因:

  • 编译时不确定需要多少内存。

例如,在将文本文件读入字符串时,通常不知道文件的大小,因此在运行程序之前无法决定分配多少内存。
即编译器无法确定char file[文件大小],这句代码应该分配多少内存,因为文件大小在编译时无法给出确定的值。
然而,这里c++有个重要的特性,可以方便地使用动态内存分配, int n = 10; string text[n];这种写法没用到new,但却是在堆上分配内存,并会在离开当前块时,通过析构函数自动析构,使得代码看起来是在栈上创建的,实际上是在堆上创建的,我们下一节会详细提到。

  • 希望离开当前块后保留分配的内存。

例如,

int func(int num) {
  int ans = num;
  return ans;
}

在这种情况下,即使栈(string ans;)可以保存整个文件内容,您也无法从函数返回并保留分配的内存块,因为此时ans已经被操作系统回收了。(注,虽然你可以使用该函数的返回值,但实际上是进行了一次拷贝构造函数,即原来栈的对象已经被操作系统自动回收)
我们将地址打印,结果如下

#include "iostream"
long long count = 0;
using namespace std;
int func(int num) {
  int ans = num;
  cout << "stack ptr:" << &ans << endl;
  return ans;
}
int main() {
  int m = func(111);
  cout << "main ptr:" << &m << endl;
}
stack ptr:0x61fddc
main ptr:0x61fe1c

然而,内存泄漏是c++代码中,非常常见且致命的问题,记得delete是很简单,但又很难的一件事,为此,大部分大佬都会建议你少写new,很多博客将其视为少用堆,然而,这并不是同一件事,因为

  • 我们可以重载new,使new的内存并不在堆处分配
  • 我们可以使用C++的析构,使没用到new的变量也在堆上分配,并自动析构

辟谣:并非少用堆,而是少用容易导致内存泄漏的写法,我们不需要强行规避动态内存分配。

动态内存分配推荐的写法

在 C++ 中有一个称为析构函数的机制。此机制将资源“包装”到对象中,并在生命周期结束后自动调用析构函数进行内存回收。 如

int main ( int argc, char* argv[] )
{
    std::string program(argv[0]);
}

这种技术或思想称为RAII
https://zh.wikipedia.org/wiki/RAII
RAII实际上分配了可变数量的内存。对象使用std::string堆分配内存并在其析构函数中释放它。在这种情况下,您无需手动管理任何资源,仍然可以获得动态内存分配的好处。

特别是,这意味着另一种写法中:

int main ( int argc, char* argv[] )
{
    std::string * program = new std::string(argv[0]);  // Bad!
    delete program;
}

有不需要的动态内存分配。该程序需要更多的输入(!)并引入了忘记释放内存的风险。它这样做没有明显的好处。

栈或动态内存RAII的好处是:

  • 打字少,所以写代码快。(这确实是优点)
  • 运行快 (这个我不知道为什么)
  • 不需要手动delete ,降低内存泄漏风险。
另外一个例子告诉你为何需要避免非必要的new:
class Line {
public:
    Line();
    ~Line();
    std::string* mString;
};

Line::Line() {
    mString = new std::string("foo_bar");
}

Line::~Line() {
    delete mString;
}

这种写法的风险比下面这种大的多

class Line {
public:
    Line();
    std::string mString;
};

Line::Line() {
    mString = "foo_bar";
    // note: there is a cleaner way to write this.
}

原因出在拷贝构造函数上,当你没有显式定义拷贝构造函数的时候,类会定义一个默认的拷贝构造函数,然而这个拷贝构造函数是浅拷贝,并非深拷贝。让我们考虑以下代码。

int main ()
{
    Line l1;
    Line l2 = l1;
}

使用原始版本,该程序可能会崩溃,因为它对同一字符串使用了两次 delete。使用修改后的版本,每个 Line 实例将拥有自己的字符串实例,每个实例都有自己的内存,并且都将在程序结束时释放。

  • 12
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值