每日一个C++知识点|底层内存管理
本文深入解析C++内存管理的核心机制,重点探讨内存分区、分配/释放方法及常见内存问题。C++内存分为栈区、堆区、全局/静态区、常量区和代码区五大区域,其中堆区需程序员手动管理。通过对比malloc/free和new/delete的差异,指出后者能自动调用构造/析构函数的优势。文章还详细分析了内存泄漏和野指针的产生原因及危害,并提出解决方案:及时释放内存、RAII原则、智能指针等。这些知识对编写安全
C++的手动内存管理机制赋予了程序员极高的灵活性,但也带来了内存泄漏、野指针等风险。本文从内存区分开始,逐步从深入了解C++内存的核心知识~
内存分区
在C++程序运行时,内存会被划分为五个区域,分别是栈区、堆区、全局/静态区、常量区和代码区,如图所示:
栈区是程序运行中一块连续的内存区域,主要用来存储局部变量,由编译器自动分配和释放,空间小但速度快
堆区是程序运行时由程序员手动分配和释放的非连续内存区域,主要用于存储动态数组、对象等数据
全局/静态区是专门存储全局变量和静态变量的区域,程序启动时分配,退出时释放
常量区是存储字符串常量、const常量的区域,都是只读不可修改
代码区是存储程序的二进制执行代码的区域
下面通过代码进行举例,通过注释标出以上分区对应的代码段,使我们有一个直观的感受:
#include <iostream>
#include <string>
using namespace std;
// 全局/静态区:全局变量
int global_num = 100;
// 简单的学生类(用于演示对象存储)
class Student {
public:
string name;
int age;
Student(string n, int a) : name(n), age(a) {}
};
int main() {
// 栈区:局部变量和局部对象
int local_num = 200;
Student stu1("张三", 18);
// 堆区:动态分配的对象
Student* stu2 = new Student("李四", 19);
// 静态区:静态变量
static int static_num = 300;
// 常量区:只读字符串常量
const char* greeting = "Hello C++";
// 输出数据验证
cout << "全局变量:" << global_num << endl;
cout << "局部变量:" << local_num << endl;
cout << "栈区对象:" << stu1.name << "," << stu1.age << endl;
cout << "堆区对象:" << stu2->name << "," << stu2->age << endl;
cout << "静态变量:" << static_num << endl;
cout << "常量字符串:" << greeting << endl;
// 手动释放堆区内存
delete stu2;
stu2 = nullptr;
return 0;
}
上述代码中为什么没有代码区呢?代码区存储的是程序的二进制执行指令,而非可直接操作的变量和数据,因而代码区无法在上述代码中展示,以上就是五大内存分区的主要内容~
内存的分配和释放
我们初步了解五大内存分区的基本情况之后,接下来便要了解内存分配和释放的原理和方法
作为程序员,这里我们主要讲的是程序员手动分配和释放内存的区域,就是堆区
在堆区,我们创建和销毁内存时可以使用 C 语言的malloc/free函数库,也可以使用 C++ 特有的new/delete操作符,下面我们通过具体的代码示例对比这两种方法:
C语言:malloc+free
int* c_int = (int*)malloc(sizeof(int)); // 仅分配内存,未初始化
if (c_int == nullptr) { // 需手动检查分配是否成功
cerr << "malloc失败" << endl;
return 1;
}
*c_int = 10; // 手动赋值
cout << "malloc分配的int值:" << *c_int << endl;
free(c_int); // 仅释放内存,无其他操作
c_int = nullptr; // 避免野指针
C++:new+delete
int* cpp_int = new int(20); // 分配内存 + 直接初始化(值为20)
// new失败会抛异常(默认),无需手动检查(除非用nothrow版本:new (nothrow) int)
cout << "new分配的int值:" << *cpp_int << endl;
delete cpp_int; // 仅释放内存,无析构(基本类型无析构)
cpp_int = nullptr;
运行结果如下:
两种方法都实现了内存创建和释放的功能
但如果时涉及构造函数和析构函数的时候,new会自动调构造函数,malloc不会;delete会自动调用析构函数,free不会;举例如下:
// 定义一个测试类(用于体现new/delete的构造/析构特性)
class Test {
public:
// 构造函数(new会自动调用,malloc不会)
Test(int val = 0) : num(val) {
cout << "Test构造函数:num = " << num << endl;
}
// 析构函数(delete会自动调用,free不会)
~Test() {
cout << "Test析构函数:num = " << num << endl;
}
int num; // 成员变量
};
测试如下:
// C语言:malloc+free(无法调用构造/析构)
Test* c_test = (Test*)malloc(sizeof(Test)); // 仅分配内存,构造函数未执行
if (c_test == nullptr) {
cerr << "malloc失败" << endl;
return 1;
}
// 手动调用构造函数(需用placement new,非常规操作)
new (c_test) Test(30); // 仅演示,实际极少用
cout << "malloc+placement new的Test值:" << c_test->num << endl;
c_test->~Test(); // 手动调用析构函数
free(c_test); // 释放内存
c_test = nullptr;
// C++:new+delete(自动调用构造/析构)
Test* cpp_test = new Test(40); // 分配内存 + 自动调用构造函数
cout << "new分配的Test值:" << cpp_test->num << endl;
delete cpp_test; // 自动调用析构函数 + 释放内存
cpp_test = nullptr;
运行结果如下:
由此可知:malloc仅分配内存,不会自动调用构造函数;free仅释放内存,不会自动调用析构函数;new/delete既可以手动分配/释放内存,又可以自动调用构造函数和析构函数来分配和释放内存
内存泄漏
上面我们已经了解内存的分配和释放的过程和方法了,但是如果已分配的堆内存不再使用,但未被释放,就会导致系统内存被持续占用,最终可能使程序崩溃,这就是内存泄漏
例如,创建了一个动态对象后,不小心覆盖了指向它的指针,导致内存无法释放:
#include <iostream>
#include <string>
using namespace std;
// 简单的字符串包装类
class MyString {
public:
MyString(const string& s) {
cout << "创建字符串:" << s << endl;
// 模拟分配堆内存
data = new char[s.size() + 1];
copy(s.begin(), s.end(), data);
data[s.size()] = '\0';
}
~MyString() {
cout << "释放字符串:" << data << endl;
delete[] data;
}
private:
char* data;
};
int main() {
// 内存泄漏场景1:覆盖指针,失去对堆内存的引用
MyString* str1 = new MyString("hello");
str1 = new MyString("world"); // 原str1的内存无法释放,泄漏!
// 内存泄漏场景2:函数中分配内存,未返回也未释放
auto create_string = []() {
MyString* str = new MyString("test");
// 忘记return或delete,内存泄漏!
};
create_string();
return 0;
}
正确做法:及时释放,避免覆盖指针
MyString* str2 = new MyString("correct");
delete str2;
str2 = nullptr; // 置空,避免野指针
避免内存泄漏的方法除了及时释放外,还可以使用RAII原则的方法,利用对象的构造/析构函数自动管理资源,或者可以使用智能指针
野指针
除了内存泄漏会导致程序崩溃之外,野指针也会使程序崩溃
野指针是指向已释放内存或非法内存的指针,使用野指针会导致程序崩溃、数据损坏,甚至比内存泄漏更危险
为什么会产生野指针呢?主要有一下三种原因:
- 指针未初始化
- 指针指向的内存被释放后未置空
- 指针越界
针对野指针产生的问题,有以下四种方法解决:
- 初始化指针:声明时直接置空
int* p = nullptr; - 释放内存后置空
delete p; p = nullptr; - 避免指针越界
使用vector代替原生数组 - 使用智能指针
以下通过代码进行展示:
#include <iostream>
using namespace std;
// 简单的整数包装类
class MyInt {
public:
MyInt(int v) : val(v) {
cout << "创建MyInt:" << val << endl;
}
~MyInt() {
cout << "销毁MyInt:" << val << endl;
}
int val;
};
int main() {
// 野指针场景1:指针未初始化
MyInt* p1;
// p1->val = 10; // 未定义行为,程序可能崩溃!
// 野指针场景2:释放后未置空
MyInt* p2 = new MyInt(20);
delete p2;
// p2->val = 30; // 野指针,访问已释放内存!
p2 = nullptr; // 置空后,访问会直接崩溃(便于调试)
// 正确做法:初始化+释放后置空
MyInt* p3 = nullptr;
p3 = new MyInt(40);
if (p3 != nullptr) { // 判空后使用
cout << "MyInt的值:" << p3->val << endl;
delete p3;
p3 = nullptr;
}
return 0;
}
智能指针
C++开发中内存管理是一件大事,经常会出现内存泄漏或野指针这种问题导致程序崩溃,而且手动管理内存容易出错,有什么办法可以一劳永逸呢?
C++11引入了智能指针,基于RAII原则,将指针封装成类,在构造函数中分配内存,析构函数中自动释放内存,从根源上解决内存泄漏和野指针问题,这就是智能指针
智能指针有三种,分别是unique_ptr,shared_ptr,weak_ptr
unique_ptr
unique_ptr是独占式智能指针,独占所管理的内存,不允许拷贝和赋值,只能移动
其适用场景是单一对象的独占管理,比如单个动态对象、动态数组等
shared_ptr
shared_ptr是共享式智能指针,通过引用计数实现多个指针共享同一块内存,引用计数为0时释放内存
适用场景是多个对象共享同一个资源
weak_ptr
weak_ptr是弱引用智能指针,配合shared_ptr使用,不增加引用计数,解决循环引用问题
主要适用场景是打破shared_ptr的循环引用,如双向链表的节点相互引用
下面通过代码示例来体现智能指针的用法
#include <iostream>
#include <memory>
using namespace std;
// 简单的日志类(作为共享资源)
class Logger {
public:
Logger(const string& n) : name(n) {
cout << "创建日志器:" << name << endl;
}
~Logger() {
cout << "销毁日志器:" << name << endl;
}
void log(const string& msg) {
cout << "[" << name << "] " << msg << endl;
}
string name;
};
// 业务类(使用日志器)
class Business {
public:
Business(shared_ptr<Logger> l) : logger(l) {
cout << "创建业务对象,使用日志器:" << l->name << endl;
}
~Business() {
cout << "销毁业务对象" << endl;
}
void do_work() {
logger->log("执行业务逻辑");
}
private:
shared_ptr<Logger> logger;
};
int main() {
// 1. unique_ptr:独占资源
unique_ptr<Logger> log1(new Logger("独占日志器"));
// unique_ptr<Logger> log2 = log1; // 报错:不能拷贝
unique_ptr<Logger> log2 = move(log1); // 移动语义,log1变为空
// 2. shared_ptr:共享资源(多个业务对象共享同一个日志器)
shared_ptr<Logger> shared_log(new Logger("共享日志器"));
Business b1(shared_log);
Business b2(shared_log);
cout << "日志器的引用计数:" << shared_log.use_count() << endl; // 输出:3(shared_log + b1 + b2)
b1.do_work();
b2.do_work();
return 0;
}
shared_ptr 的循环引用
shared_ptr的引用计数机制看似完美,但当两个shared_ptr互相指向对方时,会产生循环引用,导致引用计数永远不为 0,内存无法释放。
那么应该如何解决循环引用问题呢?将其中一个shared_ptr改为weak_ptr,因为weak_ptr不增加引用计数,仅作为弱引用
#include <iostream>
#include <memory>
using namespace std;
// 简单的节点类(用于演示循环引用)
class Node {
public:
string name;
// 子节点:shared_ptr
shared_ptr<Node> child;
// 父节点:weak_ptr(解决循环引用)
weak_ptr<Node> parent; // 若改为shared_ptr<Node> parent,则产生循环引用
Node(string name_) : name(name_) {
cout << "创建节点:" << name << endl;
}
~Node() {
cout << "销毁节点:" << name << endl;
}
};
int main() {
// 创建父节点和子节点
shared_ptr<Node> parent(new Node("父节点"));
shared_ptr<Node> child(new Node("子节点"));
// 建立引用关系
parent->child = child;
child->parent = parent;
cout << "父节点引用计数:" << parent.use_count() << endl; // 输出:1
cout << "子节点引用计数:" << child.use_count() << endl; // 输出:2(child + parent->child)
// weak_ptr的使用:lock()转换为shared_ptr
shared_ptr<Node> p = child->parent.lock();
if (p) {
cout << "子节点的父节点:" << p->name << endl;
}
return 0;
}
如果将weak_ptr<Node> parent改为shared_ptr<Node> parent,则会产生循环引用,节点的析构函数不会被调用,导致内存泄漏
以上就是内存管理的基本内容~
总结
C++底层内存管理主要说了以下几方面的内容:
- 在哪里分配内存(五大内存分区)
- 怎么分配内存(new/delete)
- 要注意内存泄漏
- 解决内存泄漏(智能指针)
本文写到这里就结束了,如果这文章对你有帮助的话,欢迎点赞+关注哦~
更多推荐

所有评论(0)