CSDN的文章像寄生虫,不管怎么搜都是重复的、低质量的、互相抄的、只有标题的、还有一堆点进去是跳转链接的、还全他娘标的“原创的”、“原创”的文章内容告诉你如下图,图都他娘的没有,抄都抄不全,最关键的是传播错误知识,让包括我在内的众多用户消化了很多错误知识,多走了很多歪路。因此,希望这篇文章能对CSDN的质量有一丝丝的提升,也包括了些自己的看法和经验,权当抛砖引玉。
理解什么时候用堆或栈,我们先回顾下基础知识,在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,很多博客将其视为少用堆,然而,这并不是同一件事,因为
辟谣:并非少用堆,而是少用容易导致内存泄漏的写法,我们不需要强行规避动态内存分配。
在 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的好处是:
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 实例将拥有自己的字符串实例,每个实例都有自己的内存,并且都将在程序结束时释放。