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原则的方法,利用对象的构造/析构函数自动管理资源,或者可以使用智能指针

野指针

除了内存泄漏会导致程序崩溃之外,野指针也会使程序崩溃

野指针是指向已释放内存或非法内存的指针,使用野指针会导致程序崩溃、数据损坏,甚至比内存泄漏更危险

为什么会产生野指针呢?主要有一下三种原因:

  1. 指针未初始化
  2. 指针指向的内存被释放后未置空
  3. 指针越界

针对野指针产生的问题,有以下四种方法解决:

  1. 初始化指针:声明时直接置空
    int* p = nullptr;
  2. 释放内存后置空
    delete p; p = nullptr;
  3. 避免指针越界
    使用vector代替原生数组
  4. 使用智能指针

以下通过代码进行展示:

#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++底层内存管理主要说了以下几方面的内容:

  1. 在哪里分配内存(五大内存分区)
  2. 怎么分配内存(new/delete)
  3. 要注意内存泄漏
  4. 解决内存泄漏(智能指针)

本文写到这里就结束了,如果这文章对你有帮助的话,欢迎点赞+关注哦~

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐