C++标准特性2(垃圾回收机制、智能指针、nullptr/constexpr、互斥量)
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.互斥量的作用:“加锁-操作-解锁”等原子流程
- 加锁(Lock):线程要操作共享内存前,先尝试获取互斥量的锁。
- 如果锁未被占用,线程获取锁,继续执行(此时其他线程无法获取该锁);
- 如果锁已被占用,线程会阻塞(等待),直到锁被释放。
- 操作共享内存:获取锁的线程安全地读写共享内存(此时无其他线程干扰)。
- 解锁(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)。
缩小临界区范围:“临界区” 是加锁后操作共享内存的代码段,应尽量缩短(只包含必要的读写操作)。
不要滥用互斥量:如果线程操作的是 “线程私有数据”(如局部变量),无需加锁,滥用会导致性能损耗(线程频繁阻塞等待)。
更多推荐


所有评论(0)