文章首发
【重学 C ++】01| C++ 如何进行内存资源管理?
前言
大家好,我是只讲技术干货的会玩 code,明天是【重学 C ++】的第一讲,咱们来学习下 C ++ 的内存治理。
与 java、golang 等自带垃圾回收机制的语言不同,C++ 并不会主动回收内存。咱们必须手动治理堆上内存调配和开释,这往往会导致内存透露和内存溢出等问题。而且,这些问题可能不会立刻呈现,而是运行一段时间后,才会裸露呈现,排查也很艰难。因而,理解和把握 C ++ 中的内存治理技巧和工具是十分重要的,能够进步程序性能、缩小谬误和减少安全性。
内存分区
在 C ++ 中,将操作系统调配给程序的内存空间依照用处划分了 代码段、数据段、栈、堆 几个不同的区域,每个区域都有其独特的内存管理机制。
代码区
代码区是用于存储程序代码的区域,代码段在程序真正执行前就被加载到内存中,在程序执行期间,代码区内存不会被批改和开释。
因为代码区是只读的,所以会被多个过程共享。在多个过程同时执行同一个程序时,操作系统只须要将代码段加载到内存中一次,而后让多个过程共享这个内存区域即可。
数据段
数据段用于存储动态全局变量、动态局部变量和动态常量等静态数据。在程序运行期间,数据段的大小固定不变,但其内容能够被批改。依照变量是否被初始化。数据段可分为已初始化数据段和未初始化数据段。
栈
C++ 中函数调用以及函数内的局部变量的应用,都是通过栈这个内存分区实现的。栈分区由操作系统主动调配和开释,是一种 ” 后进先出 ” 的一种内存分区。每个栈的大小是固定的,个别只有几 MB,所以如果栈变量太大,或者函数调用嵌套太深,容易产生栈溢出(stack overflow)。
先来一段示例代码,看看 C ++ 是如何应用栈进行应用栈来进行函数调用的。
#include <iostream>
void inner(int a) {std::cout << a << std::endl;}
void outer(int n) {
int a = n + 1;
inner(a);
}
int main() {outer(4);
}
下面这段代码运行过程中的栈变动如下图
每当程序调用一个函数时,该函数的参数、局部变量和返回地址等信息会被压入栈中。当函数执行结束,再将这些信息从栈中弹出。依据之前压入的外层调用者压入栈的返回地址,返回到外层调用者未执行的代码继续执行。
本地变量是间接存储在栈上的,当函数执行实现后,这些变量占用的内存就会被开释掉了。后面例子中的本地变量是简略类型,在 C ++ 中称为 POD 类型。对于带有结构和析构函数的非 POD 类型变量,栈上的内存调配同样无效。编译器会在适合的机会,插入对构造函数和析构函数的调用。
这里有个问题,当函数执行产生异样时,析构函数还会被调用吗?
答案是会的,C++ 对于产生异样时对析构函数的调用称为 ” 栈开展 ”。通过上面这段代码演示栈开展。
#include <iostream>
#include <string>
class Obj {
public:
std::string name_;
Obj(const std::string& name):name_(name){std::cout << "Obj()" << name_ << std::endl;};
~Obj() {std::cout << "~Obj()" << name_ << std::endl;};
};
void bar() {auto o = Obj{"bar"};
throw "bar exception";
}
int main() {
try {bar();
} catch (const char* e) {std::cout << "catch Exception:" << e << std::endl;}
}
执行代码的后果是:
Obj() bar
~Obj() bar
catch Exception: bar exception
能够发现,产生异样时,bar
函数中的本地变量 o
还是能被失常析构。
栈开展的过程实际上是异样产生时,匹配 catch 子句的过程。
- 程序抛出异样,进行以后执行的调用链,开始寻找与异样匹配的 catch 子句。
- 如果异样产生在 try 中,则会首先查看与该 try 块匹配的 catch 子句。如果异样所在函数体没有 try 捕捉异样。则会间接进入下一步。
- 如果第二步未找到匹配的 catch,则会在外层的 try 块中查找,直到找到为止。
- 如果到了最外层还没有找到匹配的 catch,也就是说异样得不到解决,程序会调用规范库函数 terminate 终止函数的执行。
在这期间,栈上所有的对象都会被主动析构。
堆
堆是 C ++ 中用来存储动静分配内存的内存分区,堆内存的调配和开释须要手动治理,能够通过 new/delete 或 malloc/free 等函数进行调配和开释。堆内存的大小通常是不固定的,当咱们须要动静分配内存时,就能够应用堆内存。
堆内存由程序员手动调配和开释,因而应用堆内存须要留神内存透露和内存溢出等问题。当程序员遗记开释已调配的内存时,会导致内存透露问题。而当申请的堆内存超过了操作系统所调配给过程的内存限度时,会导致内存溢出问题。
C++ 程序绝大多数的内存泄露,都是因为遗记调用 delete/free 来开释堆上的资源。
还是上代码
#include <iostream>
#include <string>
class Obj {
public:
std::string name_;
Obj(const std::string& name):name_(name){std::cout << "Obj()" << name_ << std::endl;};
~Obj() {std::cout << "~Obj()" << name_ << std::endl;};
};
Obj* makeObj() {
Obj* obj = nullptr;
try {obj = new Obj{"makeObj"};
...
} catch(...) {
delete obj;
throw;
}
return obj;
}
Obj* foo() {
Obj* obj = nullptr;
try {obj = makeObj();
...
} catch(...) {delete obj;}
return obj;
}
int main() {Obj* obj = foo();
...
delete obj;
}
能够看到,由 makeObj
函数创立的堆变量obj
, 在每个获取该变量的下层调用中,都须要关怀对该变量的解决。这无疑极大得减少了开发者的心智累赘。
RAII
想在堆上创建对象,又不想解决这么简单的内存开释操作。C++ 没有像 java、golang 其余语言创立一套垃圾回收机制,而是采纳了一种特有的资源管理形式 — RAII(Resource Acquisition Is Initialization,资源获取即初始化)。
RAII 利用栈对象在作用域完结后会主动调用析构函数的特点,通过创立栈对象来治理资源。在栈对象构造函数中获取资源,在栈对象析构函数中负责开释资源,以此保障资源的获取和开释。
上面给出一个通过 RAII 来主动开释堆内存的例子
#include <iostream>
class AutoIntPtr {
public:
AutoIntPtr(int* p = nullptr) : ptr(p) {}
~AutoIntPtr() { delete ptr;}
int& operator*() const { return *ptr;}
int* operator->() const { return ptr;}
private:
int* ptr;
};
void foo() {AutoIntPtr p(new int(5));
std::cout << *p << std::endl; // 5
}
int main() {foo();
}
下面例子中,AutoIntPtr
类封装了一个动态分配的 int
类型的指针,它的构造函数用于获取资源 (ptr = p),析构函数用于开释资源(delete ptr)。当AutoIntPtr
超出作用域时,主动调用析构函数来开释所蕴含的资源。
基于 RAII,C++11 引入了 std::unique_ptr
和std::shared_ptr
等智能指针用于内存治理类,使得内存治理变得更加不便和平安。这些内存治理类能够主动进行内存开释,防止了手动开释内存的繁琐工作。值得一提的是,下面的 AutoIntPtr
就是一个简化版的智能指针了。
在理论开发中,RAII 的利用很广。不仅仅用于主动开释内存。还能够用来敞开文件、开释数据库连贯、开释同步锁等。
总结
本文介绍了 C ++ 中的内存管理机制,包含内存分区、栈、堆和 RAII 技术等内容。通过学习本文,咱们能够更好地把握 C ++ 的内存治理技巧,防止内存透露和内存溢出等问题。