1.垃圾回收机制

垃圾回收机制是C++自动管理内存的技术,核心作用是识别并释放不再被程序使用的内存空间(即垃圾内存),避免内存泄漏,减轻开发者手动管理内存的负担。

在没有GC的语言(如C/C++)中,开发者需要手动malloc/free、new/delete申请和释放内存,容易出现两个问题。

   内存泄漏:忘记释放不再使用的内存,导致内存被持续占用,最终可能耗尽系统内存,使程序崩溃。

   野指针/重复释放:释放已释放的内存,或访问已释放的内存(野指针),导致程序运行异常(如崩溃、数据错乱)。

而GC的出现就是为了自动解决这些问题--让程序自己判断哪些内存没用了,并自动回收,开发者无需关注内存释放。

1.C++ 中 “替代 GC” 的内存管理方案

虽然没有内置GC,但C++提供了多种“安全且自动化”的内存管理方案,避免手动管理的风险(如内存泄漏、野指针),核心思路是通过对象生命周期自动管内存。

1.智能指针(最常用的“准GC”方案)

C++11引用的智能指针(定义于<memory>头文件)通过“RAll(资源获取即初始化)”机制,让指针拥有“自动释放内存”的能力--当智能指针的生命周期结束(如离开作用域),会自动调用delete释放指向的内存,无需手动操作。

智能指针类型 核心特性 适用场景
unique_ptr 独占所有权(同一时间只有一个指针指向内存) 管理单个对象的独占内存(如局部动态对象)
shared_ptr 共享所有权(多个指针指向同一内存,引用计数为0时释放) 多模块共享对象(如跨函数传递动态对象)
weak_ptr 若引用(不增加shared_ptr的引用计数,避免循环引用) 配合shared_ptr解决循环引用问题
#include<memory>
#include<iostram>

int main(){
  //unique_ptr自动管理内存,离开作用域后自动释放
  unique_ptr<int>ptr1=make_unique<int>(10);
  cout<<*ptr1<<endl;//输出10

  //shared_ptr共享内存,最后一个指针销毁时释放
  shared_ptr<int>ptr2=make_shared<int>(20);
  shared_ptr<int>ptr3=ptr2;//引用计数变为2
  //prt2和ptr3离开作用域时,引用计数变为0,内存自动释放

  return  0;//无需手动delete,智能指针自动处理
}

2.容器与标准库组件

C++标准库的容器(如vector、string、map)内部已封装内存管理,无需手动调用malloc/free字符缓存区。

3.Rall自定义资源管理

对于非内存资源(如文件句柄、网络连接)或复杂内存场景,可通过 RAII 类 自定义 “自动释放逻辑”—— 在类的构造函数中申请资源,析构函数中释放资源,利用对象生命周期自动管理资源。

#include <iostream>

class ArrayRAII {
private:
    int* arr;
    int size;
public:
    // 构造函数:申请内存
    ArrayRAII(int s) : size(s) {
        arr = new int[size];
        std::cout << "内存申请成功" << std::endl;
    }

    // 析构函数:自动释放内存(对象销毁时调用)
    ~ArrayRAII() {
        delete[] arr;
        std::cout << "内存自动释放" << std::endl;
    }

    // 提供访问接口
    int& operator[](int idx) { return arr[idx]; }
};

int main() {
    {
        ArrayRAII arr(5); // 申请 5 个 int 的内存
        arr[0] = 10;
        std::cout << arr[0] << std::endl;
    } // 离开作用域,arr 对象销毁,析构函数自动释放内存

    return 0;
}

4.C++ 内存管理的核心逻辑

C++不依赖GC,而是通过“开发者控制+自动化工具辅助”实现内存安全。

  基础原则:谁申请谁释放,通过智能指针、RAll等工具,将手动释放转化为自动释放,避免内存泄漏。

  核心优势:无GC带来的性能和内存开销,适合高性能、实时性要求高的场景。

  注意点:避免使用裸指针(int *p=new int),优先使用智能指针;避免循环引用(shared_ptr相互引用会导致内存泄漏,需用weak_ptr解决)。

简单说,C++ 的 “内存管理哲学” 是 “让开发者拥有控制权,同时提供工具降低手动管理的风险”,而非像 GC 那样完全交由语言自动处理。

2.智能指针

在C++中,智能指针是一种封装了裸指针(如int*、object*)的模板类,核心功能是自动管理动态内存的释放,避免手动调用delete导致的内存泄漏、重复释放或野指针等问题,智能指针的本质是 “用对象的生命周期来管理内存的生命周期”—— 当智能指针对象被销毁(如离开作用域)时,会自动调用析构函数释放它所指向的动态内存,无需开发者手动干预。

上一章已讲过,C++提供了三种核心智能指针。

使用原则:优先使用make_unique和make_shared创建智能指针,而非直接用new(更安全,避免异常风险)。

3.nullptr/constepr

1.nullptr

是C++引入的关键字,专门用于表示空指针,替代了传统的NULL(本质是0的宏定义),解决了null可能导致的类型歧义问题。

类型明确:nullptr的类型是nullptr_t,专门用于指针语境,编译器能明确区分它是指针空值,而非整数0。

避免歧义:当函数重载同时接收指针和整数参数时,nullptr会被正确匹配到指针版本,而NULL可能被误判成整数。

#include <iostream>

// 重载函数:一个接收指针,一个接收整数
void func(int x) {
    std::cout << "调用 int 版本:" << x << std::endl;
}
void func(int* p) {
    std::cout << "调用指针版本" << std::endl;
}

int main() {
    func(NULL);    // 歧义!C++ 中可能调用 int 版本(因为 NULL 本质是 0)
    func(nullptr); // 明确调用指针版本(nullptr 是指针空值)
    return 0;
}

2.constexpr:编译器常量/表达式

constexpr是C++引入的关键字,用于声明编译器可计算的常量或函数,让代码在编译阶段就能确定值,提升程序效率(避免运行时计算)。

constexpr变量的值必须在编译器确定,constexpr函数可以在编译期被调用并返回结果。constexpr可用于常量表达式语境,如数组大小、模板参数等必须在编译器确定的值,可以使用constexpr。

#include <iostream>

// constexpr 变量:编译期确定值
constexpr int MAX_SIZE = 100; 
int arr[MAX_SIZE]; // 正确:MAX_SIZE 是编译期常量

// constexpr 函数:编译期可计算
constexpr int add(int a, int b) {
    return a + b;
}

int main() {
    // 编译期计算结果(相当于直接写 3)
    constexpr int result = add(1, 2); 
    std::cout << result << std::endl; // 输出:3

    return 0;
}

constexpr函数的逻辑必须足够简单,无循环、无复杂分支,才能在编译期计算。

4.互斥量

互斥量,是多线程编程中用于保护共享内存的(或共享资源)不被同时访问的同步工具,核心作用是确保“同一时间只有一个线程能操作共享内存”,避免数据竞争导致的错误。

1.为什么需要互斥量

多线程程序中,多个线程可能同时读写同一块共享内存(如全局变量、动态分配的内存、容器等)。如果没有保护,会出现 “数据竞争”,导致结果错乱或程序崩溃。

举例子(无互斥量的错误场景)

#include<thread>
#include<iostream>

int shred_num=0;//共享内存(全局变量)

//线程函数:给shared_num加1000次
void add(){
  for(int i=0;i<1000;++i){
    shared_num++;//看似简单的自增,实际是"读-写-写"三步操作
  }
 int mian(){
   thread t1(add);
   thread t2(add);
   t1.join();
   t2.join();
   cout<<shread_num<<endl;//预期2000,可能是1500、1800等随机值。
   return 0;
}

问题原因shared_num++ 不是原子操作(无法一步完成)。当 t1 读取 shared_num=100 后,t2 也读取到 100,两者分别加 1 后写回,最终 shared_num=101(而非预期的 102),导致结果错误。

2.互斥量的作用:“加锁-操作-解锁”等原子流程

  1. 加锁(Lock):线程要操作共享内存前,先尝试获取互斥量的锁。
    • 如果锁未被占用,线程获取锁,继续执行(此时其他线程无法获取该锁);
    • 如果锁已被占用,线程会阻塞(等待),直到锁被释放。
  2. 操作共享内存:获取锁的线程安全地读写共享内存(此时无其他线程干扰)。
  3. 解锁(Unlock):线程操作完成后,释放互斥量的锁,让其他等待的线程可以获取锁。

加锁-操作-解锁的流程是原子的(要么完整执行,要么不执行),确保共享内存始终被“独占访问”。

3.C+中互斥量使用

C++ 标准库(<mutex> 头文件)提供 mutex 类实现互斥量,配合 lock_guard(自动加锁 / 解锁,避免手动解锁遗漏)使用更安全。

#include <thread>
#include <iostream>
#include <mutex> // 包含互斥量头文件

std::mutex mtx; // 定义互斥量(全局,确保所有线程可见)
int shared_num = 0;

void add() {
    for (int i = 0; i < 1000; ++i) {
        // std::lock_guard 构造时自动加锁,析构时自动解锁(离开作用域)
        std::lock_guard<std::mutex> lock(mtx); 
        shared_num++; // 临界区:仅当前线程能执行
    }
}

int main() {
    std::thread t1(add);
    std::thread t2(add);
    t1.join();
    t2.join();
    std::cout << shared_num << std::endl; // 必然输出 2000,结果正确
    return 0;
}

构造时调用 mtx.lock(),析构时调用 mtx.unlock(),即使函数中途抛出异常,也能确保锁被释放,避免 “死锁”(锁永远不释放)。

4.互斥量的核心注意事项

  避免死锁:死锁是多线程的致命问题,通常由 “多个线程互相等待对方的锁” 导致(如 t1 持有锁 A 等锁 B,t2 持有锁 B 等锁 A)。

  缩小临界区范围:“临界区” 是加锁后操作共享内存的代码段,应尽量缩短(只包含必要的读写操作)。

  不要滥用互斥量:如果线程操作的是 “线程私有数据”(如局部变量),无需加锁,滥用会导致性能损耗(线程频繁阻塞等待)。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐