目标: 掌握C++核心特性,为嵌入式开发打基础
好的,我来为你详细梳理一下 继承与多态、虚函数 相关的知识点,包括单继承、多继承、虚函数表机制、纯虚函数与抽象类、动态绑定。以下内容适合中等难度层次的理解,便于考试复习或面试准备。
🌟 继承与多态,虚函数
1️⃣ 单继承和多继承
单继承
-
一个派生类只有一个基类。
-
结构简单,层次清晰。
-
示例:
class Base { public:void show() { std::cout << "Base" << std::endl; } };class Derived : public Base { public:void display() { std::cout << "Derived" << std::endl; } };
public 公开继承(最常用,基类 public 和 protected 成员在派生类中保持原有权限)
protected 保护继承(基类 public 和 protected 成员在派生类中都变成 protected)
private 私有继承(基类 public 和 protected 成员在派生类中都变成 private)
多继承
-
一个派生类可以同时继承多个基类。
-
可带来灵活性,但也可能引发二义性问题(如菱形继承问题)。
-
解决办法:虚继承(
virtual
关键字) -
示例:
class Base1 { public:void func1() { std::cout << "Base1" << std::endl; } };class Base2 { public:void func2() { std::cout << "Base2" << std::endl; } };class Derived : public Base1, public Base2 { };
菱形继承问题
class A { public: int x; };
class B : virtual public A { };
class C : virtual public A { };
class D : public B, public C { };
👉 如果 B
和 C
都虚继承自 A
,D
中只有一份 A
的成员。
🚀 构造函数后 :
的用途 —— 初始化列表
在 C++ 中,构造函数定义时可以用 :
引出一个 初始化列表,用于初始化成员变量和基类。
语法
class 类名 {
public:类型1 成员1;类型2 成员2;类名(参数列表) : 成员1(值1), 成员2(值2) {// 构造函数体}
};
🌟 初始化列表主要作用
✅ 初始化 const 成员(必须用初始化列表)
✅ 初始化 引用成员(必须用初始化列表)
✅ 调用基类构造函数(在继承中必用)
✅ 效率更高(成员在进入构造函数体之前就已初始化)
🌰 示例 1:普通成员初始化
class Point {int x;int y;
public:Point(int a, int b) : x(a), y(b) {// 构造函数体可以为空}
};
等价于:
Point p(1, 2);
这里 x
初始化为 1,y
初始化为 2。
🌰 示例 2:const 和引用成员
class Example {const int ci;int& ref;
public:Example(int i, int& r) : ci(i), ref(r) { }
};
👉 注意:const
和 &
成员必须在初始化列表里赋值,不能在构造函数体内赋值。
🌰 示例 3:继承情况下,调用基类构造函数
class Base {
public:Base(int a) { std::cout << "Base: " << a << std::endl; }
};class Derived : public Base {
public:Derived(int a, int b) : Base(a) {std::cout << "Derived: " << b << std::endl;}
};
当你写:
Derived d(10, 20);
输出:
Base: 10
Derived: 20
✅ 先调用基类构造函数。
用法 | 符号 : 位置 | 例子 |
---|---|---|
类继承声明 | class A : public B | class B : public A { }; |
初始化列表 | 构造函数头部后 | A(int x) : a(x) { } |
🌟 初始化列表主要作用
✅ 初始化 const 成员(必须用初始化列表)
✅ 初始化 引用成员(必须用初始化列表)
✅ 调用基类构造函数(在继承中必用)
✅ 效率更高(成员在进入构造函数体之前就已初始化)
普通 const 成员是属于对象的,每个对象的 const 成员值可能不同。
必须在构造对象时确定 const 成员值,所以需要初始化列表。
👉 引用必须在定义时绑定到对象(或变量)上,不能晚绑定。
一旦引用被初始化(绑定),它就永远指向这个对象或变量,不可改变。
特性 | const 成员变量 | static const 成员变量 |
---|---|---|
初始化方式 | 必须用初始化列表初始化 | 可以在类内初始化 |
属于 | 对象的每个实例 | 类的所有实例共享一份 |
例子 | A(int v) : x(v) {} | static const int x = 10; |
2️⃣ 虚函数表机制
- 虚函数表 (vtable):编译器为含有虚函数的类生成的一张函数地址表。
- 虚指针 (vptr):每个含虚函数的对象实例中包含一个指向虚函数表的指针。
- 派生类覆盖虚函数时,vtable 的相应入口会被派生类的函数地址替换。
- 调用虚函数时,根据 vptr 定位 vtable,再调用正确的函数地址,实现多态。
class Base {
public:virtual void func() { std::cout << "Base::func" << std::endl; }
};class Derived : public Base {
public:void func() override { std::cout << "Derived::func" << std::endl; }
};
📌 当你写 Base* p = new Derived(); p->func();
时,会通过 vptr 查找 vtable 中的 Derived::func
地址。
3️⃣ 纯虚函数和抽象类
纯虚函数
- 语法:
virtual void func() = 0;
- 没有实现,需要派生类重写。
抽象类
- 包含至少一个纯虚函数的类。
- 无法实例化对象,只能作为基类。
示例:
class Shape {
public:virtual void draw() = 0; // 纯虚函数
};class Circle : public Shape {
public:void draw() override { std::cout << "Draw Circle" << std::endl; }
};
👉 Shape s;
❌不允许
👉 Shape* ps = new Circle();
✅允许,用基类指针指向子类对象。
4️⃣ 动态绑定
-
动态绑定(又称 运行时多态):在运行时确定调用哪个函数。
-
前提:
- 函数是虚函数。
- 通过基类指针或引用调用。
-
如果不满足上面条件,编译时静态绑定。
示例:
Base* p = new Derived();
p->func(); // 动态绑定,调用 Derived::func
如果是 p->Base::func();
则会静态绑定,强制调用基类版本。
💡 总结
特性 | 单继承 | 多继承 | 虚函数 | 纯虚函数 | 动态绑定 |
---|---|---|---|---|---|
关系 | 一个基类 | 多个基类 | 实现多态 | 实现接口 | 运行时确定函数 |
优点 | 简单易维护 | 灵活 | 多态行为 | 强制派生类实现 | 多态支持 |
缺点 | 功能受限 | 易引发二义性 | 增加内存开销 | 不能实例化基类 | 性能略低于静态绑定 |
继承与 vptr
重新赋值的背景
- 每个对象的
vptr
用来指向当前对象所属类的虚函数表(vtable
)。 - 当你创建一个派生类对象时,这个对象其实包含了基类子对象部分。
- 在构造过程中,随着构造函数的调用,
vptr
会被设置为对应类的vtable
。
两个对象:
各自有一个 vptr
vptr 都指向同一张 vtable(Derived 的 vtable)vtable 是编译器为类生成的唯一一张表(每个带虚函数的类一张)
🌟 C++ 模板基础
1️⃣ 函数模板语法
👉 语法:
template <typename T>
T add(T a, T b) {return a + b;
}
或:
template <class T> // typename 和 class 都可以
T add(T a, T b) {return a + b;
}
👉 使用:
add(3, 4); // T 推导为 int
add(3.5, 4.2); // T 推导为 double
add<int>(3, 4); // 显式指定 T = int
2️⃣ 类模板实现
👉 语法:
template <typename T>
class MyClass {
public:T data;MyClass(T val) : data(val) {}void show() { std::cout << data << "\n"; }
};
👉 使用:
MyClass<int> obj1(10);
MyClass<std::string> obj2("hello");
👉 注意:
- 必须在使用类模板时提供模板参数(除非有默认参数)。
- 类模板成员函数定义可以在类外,但必须带模板头:
template <typename T>
void MyClass<T>::show() {std::cout << data << "\n";
}
3️⃣ 模板特化
👉 全特化:
template <typename T>
class Printer {
public:void print(T val) { std::cout << val << "\n"; }
};// 对 char* 的特化
template <>
class Printer<char*> {
public:void print(char* val) { std::cout << "char* : " << val << "\n"; }
};
👉 偏特化:
template <typename T, typename U>
class Pair { /* ... */ };template <typename T>
class Pair<T, int> { /* 针对第二个参数是 int 的特化 */ };
4️⃣ 模板参数推导
👉 函数模板支持参数推导:
template <typename T>
void func(T val) { /* ... */ }func(10); // 推导 T=int
func(3.14); // 推导 T=double
func("hello"); // 推导 T=const char*
👉 类模板 不支持自动推导(C++17 前),但 C++17 起支持 类模板参数推导引擎:
template <typename T>
class Wrapper {
public:T value;Wrapper(T v) : value(v) {}
};Wrapper w(10); // C++17 起推导出 Wrapper<int>
🌟 小结表
模板特性 | 作用 | 特点 |
---|---|---|
函数模板 | 让函数支持不同类型 | 支持参数推导,可显式指定 |
类模板 | 让类支持不同类型 | 使用时需指定参数(除非 C++17 推导) |
模板特化 | 针对特定类型提供不同实现 | 全特化或偏特化 |
参数推导 | 自动根据实参确定模板参数 | 类模板一般不推导,函数模板支持 |
🌟 C++ 异常处理基础
1️⃣ 异常处理机制
基本语法:
try {// 可能抛出异常的代码throw std::runtime_error("Error occurred");
}
catch (const std::runtime_error& e) {std::cout << "Caught: " << e.what() << "\n";
}
catch (...) {std::cout << "Caught unknown exception\n";
}
👉 流程
try
块中代码运行时遇到throw
,立即停止执行,跳转到对应catch
。- 匹配的
catch
语句被调用。 - 如果没有匹配的
catch
,程序调用std::terminate()
。
👉 注意
throw;
可以重新抛出当前捕获的异常。- 异常匹配是按照
catch
的顺序自上而下。
2️⃣ 自定义异常类
你可以自定义异常类型,通常继承自 std::exception
或其子类。
class MyException : public std::exception {
public:const char* what() const noexcept override {return "My custom exception";}
};
使用:
try {throw MyException();
}
catch (const MyException& e) {std::cout << e.what() << "\n";
}
3️⃣ RAII 与异常安全
RAII(资源获取即初始化)是 C++ 保证异常安全的重要手段。
例子:
class FileWrapper {FILE* fp;
public:FileWrapper(const char* filename) {fp = fopen(filename, "r");if (!fp) throw std::runtime_error("File open failed");}~FileWrapper() {if (fp) fclose(fp);}
};
✔ 如果在构造中抛出异常,已构造对象的析构函数会被自动调用,资源得到释放。
✔ 这就是 异常安全 的 RAII 精神。
4️⃣ 嵌入式系统中异常使用注意事项
👉 为什么嵌入式常禁用异常?
- 异常会增加代码体积(嵌入式对ROM/Flash大小敏感)
- 异常处理可能需要栈展开,增加运行时开销
- 嵌入式通常要求可控、确定性的错误处理
👉 替代方案
- 返回错误码
- 使用断言
assert
- 用状态机或专用错误处理函数
👉 嵌入式项目编译器一般禁用异常
g++ -fno-exceptions ...
🌟 小结表
特性 | 描述 | 注意事项 |
---|---|---|
try-catch-throw | 异常处理结构,用于捕获和处理异常 | 匹配顺序重要,throw 可重新抛出 |
自定义异常类 | 提供更清晰的异常类型 | 继承自 std::exception ,重写 what() |
RAII | 自动管理资源,防止泄漏 | 析构函数释放资源,保证异常安全 |
嵌入式异常 | 通常禁用,因开销大 | 推荐用错误码或断言代替 |
🌟 C++ 异常为什么会增大代码空间?
因为 编译器为了支持异常处理,需要生成额外的元数据、表和隐藏代码。主要包括以下几个方面:
1️⃣ 栈展开(stack unwinding)信息
👉 当你 throw
异常时,程序必须从抛出点开始,依次调用每个对象的析构函数,正确释放资源。
💡 为了做到这点,编译器会:
- 在二进制中生成一个“异常处理表”(也叫 栈展开表 或 unwind table)
- 记录每个函数的栈帧布局、哪些地方有局部对象、析构函数地址等
⚠ 这些表存在于可执行文件中(通常是 .eh_frame
段),直接占用代码空间。
2️⃣ 异常处理控制逻辑
👉 编译器生成隐藏代码:
- 检测抛出异常的地方
- 跳转到异常处理器(
catch
代码) - 调用析构函数做清理
这些代码虽然不显式写在源代码中,但会体现在最终的机器码中。
3️⃣ 多余的辅助代码 / 运行库支持
👉 异常处理需要运行库提供辅助函数,比如:
- 异常对象的创建、复制、销毁逻辑
- 抛出异常时调用的全局函数(例如
__cxa_throw
,__cxa_begin_catch
等,GCC/Clang 下)
👉 这部分库代码也会被链接进你的程序中,增加体积。
🌈 对比:C vs C++ 错误处理
特性 | C语言 | C++异常 |
---|---|---|
机制 | 返回值、errno 、setjmp/longjmp | try-catch-throw |
自动清理 | ❌ 无,必须手工清理 | ✅ 自动调用析构,RAII |
错误传播 | 必须层层传递或显式 longjmp | 自动沿调用栈寻找 catch |
可读性 | 易出错、代码繁琐 | 更简洁、可读性好 |
性能 | 开销小,简单高效 | 异常表增加代码体积,栈展开有开销 |
🌟 你问得很好!咱们一起把“C语言需要根据错误手动操作、层层处理”这个概念彻底搞清楚。
🔑 为什么说 C 需要“层层”处理错误?
在 C 语言里,函数出错后,它本身不会“跳回去”或者自动通知调用者发生了什么(不像 C++ 异常可以自动向上传递)。
👉 你得 显式返回错误码,并在每一层调用代码里手工检查。
🌰 例子:层层返回错误码
假设你写一个程序,调用很多函数,某一层出错了,需要上层知道:
#include <stdio.h>int lowLevel(int a) {if (a == 0) {return -1; // 出错:除 0}return 100 / a;
}int midLevel(int a) {int res = lowLevel(a);if (res == -1) {return -1; // 出错,继续向上报告}return res + 10;
}int highLevel(int a) {int res = midLevel(a);if (res == -1) {return -1; // 出错,继续向上报告}return res * 2;
}int main() {int result = highLevel(0);if (result == -1) {printf("Error happened!\n");} else {printf("Result = %d\n", result);}
}
✅ 这就是所谓 层层检查、层层返回:
每一层都必须写:
if (返回值 == 错误码) return 错误码;
否则,错误就会“漏掉”。
🔥 和 C++ 异常对比:自动向上传递
同样逻辑如果是 C++:
int lowLevel(int a) {if (a == 0) throw std::runtime_error("div by zero");return 100 / a;
}int midLevel(int a) {return lowLevel(a) + 10;
}int highLevel(int a) {return midLevel(a) * 2;
}int main() {try {int result = highLevel(0);std::cout << "Result = " << result << "\n";} catch (const std::exception& e) {std::cout << "Error: " << e.what() << "\n";}
}
✅ 你不用每层写“检查返回值、return -1”了,异常会自动穿过每一层,直到 catch
。
🌈 层层检查的含义总结
C 语言层层检查 | C++ 异常 |
---|---|
每层手动检查返回值或 errno | 异常自动向上传递到最近的 catch |
错误处理代码和主逻辑混在一起,容易混乱 | 错误处理代码集中在 catch ,主逻辑更清晰 |
易写漏检查,容易埋 bug | 不易漏掉错误(除非没有 catch ) |
⚠ C 的手工层层处理的风险
- 容易漏掉某层的检查,错误被默默吞掉
- 错误码设计混乱时,调试困难
- 错误处理代码重复多,维护麻烦
🚀 STL 容器基础 (vector, array, list)
1️⃣ 容器的选择原则
✅ vector
- 连续内存块(像动态数组)
- 随机访问快(支持
[]
、at()
) - 尾部插入/删除快(
push_back
/pop_back
O(1)) - 中间/开头插入删除慢(需移动元素)
✅ array
- 固定大小、栈分配(本质是封装了 C 风格数组)
- 编译期大小确定
- 非常轻量、效率高(零开销封装)
✅ list
- 双向链表
- 任意位置插入/删除 O(1)
- 不支持随机访问(无
[]
)
💡 选择思路:
需求 | 容器 |
---|---|
需要频繁随机访问 | vector 、array |
需要频繁插入删除(中间或两端) | list |
大小固定、性能极高 | array |
动态大小、插尾快 | vector |
2️⃣ 迭代器使用
所有 STL 容器都支持迭代器,用于统一遍历:
#include <vector>
#include <iostream>int main() {std::vector<int> v = {1, 2, 3, 4};// 普通迭代器for (std::vector<int>::iterator it = v.begin(); it != v.end(); ++it) {std::cout << *it << " ";}std::cout << "\n";// 范围 for(C++11)for (auto x : v) {std::cout << x << " ";}std::cout << "\n";// 反向迭代器for (auto it = v.rbegin(); it != v.rend(); ++it) {std::cout << *it << " ";}std::cout << "\n";
}
👉 注意:
vector
:随机访问迭代器list
:双向迭代器(不能做随机访问操作)
3️⃣ 算法库基础
STL 提供强大算法,与容器无关:
#include <algorithm>
#include <vector>
#include <iostream>int main() {std::vector<int> v = {4, 1, 3, 2};std::sort(v.begin(), v.end()); // 排序std::for_each(v.begin(), v.end(), [](int x) {std::cout << x << " ";});std::cout << "\n";auto it = std::find(v.begin(), v.end(), 3); // 查找if (it != v.end()) {std::cout << "Found: " << *it << "\n";}
}
💡 STL 算法特点:
- 和容器解耦(基于迭代器工作)
- 提供排序、查找、修改、统计等功能
- 支持自定义谓词(lambda、函数对象)
4️⃣ 嵌入式环境下 STL 使用注意事项
嵌入式开发中 STL 使用会遇到一些问题:
⚠ 动态分配
vector
、list
都依赖堆内存,嵌入式堆内存可能紧张或管理严格。
⚠ 代码膨胀
- 模板、泛型带来代码体积增大。
⚠ 实时性
- 有些操作(如
vector
扩容)可能会带来不可预测的耗时。
✅ 对策
- 优先选用
array
(栈上固定大小,零开销封装) - 或自定义 allocator 控制内存管理
- 或使用轻量级替代库(如:Embedded STL、ETL)
🌈 小结
容器 | 优势 | 适用场景 |
---|---|---|
vector | 动态大小、随机访问快 | 数组替代、需要动态增长 |
array | 固定大小、栈分配、零开销 | 大小固定、嵌入式友好 |
list | 插入删除快、稳定迭代器 | 频繁中间操作、元素数量不大 |
嵌入式建议 |
---|
尽量避免堆分配容器(vector、list) |
用 array 或静态分配的容器 |
控制代码体积(注意模板实例化膨胀) |
迭代器类自己重载了运算符
这些迭代器类型都会重载必要的运算符,比如:
*it
—— 重载了operator*()
,返回元素引用++it
—— 重载了operator++()
,让迭代器移动到下一个元素--it
—— 重载了operator--()
(如果支持)it + n
、it - n
—— 只有随机访问迭代器(如vector
)会重载it == it2
、it != it2
—— 重载了比较运算符
⚡ 不同容器的迭代器能力不同
容器 | 迭代器类型 | 支持运算符 |
---|---|---|
vector / array | 随机访问迭代器 | ++ , -- , + , - , [] |
list | 双向迭代器 | ++ , -- |
forward_list | 单向迭代器 | 只能 ++ |
🚀 智能指针 (unique_ptr, shared_ptr)
智能指针(std::unique_ptr, std::shared_ptr, std::weak_ptr)
→ 属于 C++ 标准库的一部分
→ 定义在 头文件中
→ 实现基于模板、RAII、引用计数等技术,但不归类在 STL 容器/算法里
1️⃣ RAII 原理
RAII 全称:
👉 Resource Acquisition Is Initialization
👉 资源获取即初始化
🚀 RAII 的核心思想
把资源(内存、文件句柄、锁、网络连接等)的管理交给对象的生命周期。
也就是说:
- 对象创建时(构造函数):获取资源
- 对象销毁时(析构函数):释放资源
💡 用对象的生命周期保证资源安全,不需要手工释放。
🌈 RAII 的应用场景
- 智能指针(unique_ptr, shared_ptr)管理内存
- fstream 管理文件句柄
- lock_guard / unique_lock 管理锁
- 各种容器管理内部数据
- 自定义资源类(比如管理数据库连接、网络 socket)
🌰 RAII 例子
裸指针(非 RAII)
void foo() {int* p = new int(10);// ... 使用 pdelete p; // 别忘了!否则内存泄漏
}
❌ 如果程序中途抛异常或 return,可能忘了 delete。
💡 智能指针就是 RAII 的经典应用:
- 构造时接管裸指针(
new
的结果) - 析构时自动
delete
,防止内存泄漏
{std::unique_ptr<int> p(new int(10)); // 自动管理内存,无需手动 delete
} // 作用域结束,自动 delete
✅ RAII 的实现逻辑
RAII 的实现 = 用类封装资源
- 构造函数:负责获取资源
- 析构函数:负责释放资源
资源对象的生命周期 = 资源的生命周期
对象被创建时 → 自动获得资源
对象被销毁时 → 自动释放资源
🌰 手写一个简单 RAII 类(以文件操作为例)
我们不用 std::ofstream
,自己实现一个简单 RAII 文件类:
#include <iostream>
#include <cstdio>class FileRAII {
private:FILE* file;public:// 构造函数:打开文件FileRAII(const char* filename, const char* mode) {file = std::fopen(filename, mode);if (!file) {throw std::runtime_error("Failed to open file");}std::cout << "File opened\n";}// 提供操作文件的方法void write(const char* text) {if (file) {std::fputs(text, file);}}// 析构函数:关闭文件~FileRAII() {if (file) {std::fclose(file);std::cout << "File closed\n";}}// 禁止拷贝,防止重复关闭FileRAII(const FileRAII&) = delete;FileRAII& operator=(const FileRAII&) = delete;
};
🌟 使用例子
int main() {try {FileRAII file("test.txt", "w");file.write("Hello RAII!");// 不需要手工 fclose,离开作用域自动关闭} catch (const std::exception& e) {std::cerr << e.what() << "\n";}return 0;
}
🌈 RAII 实现的关键点
部分 | 作用 |
---|---|
构造函数 | 获取资源(例如分配内存、打开文件、加锁) |
析构函数 | 释放资源(例如释放内存、关闭文件、解锁) |
禁止拷贝 | 防止多个对象共享同一资源导致重复释放 |
可选:支持移动语义 | 允许资源转移所有权(C++11 以后推荐) |
RAII 本质就是编写一个资源封装类,通过构造 + 析构管理资源,让对象生命周期决定资源管理,无需手工操作。
🚀 RAII + 异常处理:优雅管理资源
在 C++ 中,经常这样用:
try {SomeRAIIObject obj;// ... 可能抛异常的代码 ...
} catch (...) {// 处理异常
}
// 无论如何 obj 析构、资源释放
💡 这就是 现代 C++ 推荐风格:RAII 负责资源安全,异常处理负责逻辑控制。
2️⃣ unique_ptr 使用场景
✅ 特点
- 独占所有权(禁止拷贝,只允许移动)
- 不允许多个 unique_ptr 管同一块内存
✅ 使用场景
- 确保资源唯一所有权
- 避免手写
delete
,防止泄漏 - 用在工厂函数、返回局部对象时:
std::unique_ptr<Foo> createFoo() {return std::unique_ptr<Foo>(new Foo);
}
- 用于指向大对象、避免复制
✅ 移动所有权
std::unique_ptr<int> p1(new int(10));
std::unique_ptr<int> p2 = std::move(p1); // p1 放弃所有权
3️⃣ shared_ptr 引用计数
✅ 特点
- 多个
shared_ptr
可以共享一块内存的所有权 - 内部维护一个 引用计数
- 最后一个
shared_ptr
被销毁时,资源才被释放
✅ 引用计数机制
auto p1 = std::make_shared<int>(10);
auto p2 = p1; // 引用计数 +1
auto p3 = p2; // 引用计数 +1
// p1, p2, p3 全部销毁后,delete 内存
你可以通过 use_count()
查看计数:
std::cout << p1.use_count(); // 输出当前计数
4️⃣ 避免循环引用
✅ 循环引用问题
如果两个对象都用 shared_ptr
指向对方,会导致引用计数永远不为 0,内存无法释放。
💡 示例
struct B;
struct A {std::shared_ptr<B> bptr;
};
struct B {std::shared_ptr<A> aptr;
};
💥 这里 A 和 B 相互持有 shared_ptr
,会产生循环引用。
✅ 解决方案
用
weak_ptr
打破循环
struct B;
struct A {std::shared_ptr<B> bptr;
};
struct B {std::weak_ptr<A> aptr; // 不增加引用计数
};
weak_ptr
不会增加引用计数,只是观测对象是否还活着。- 用
lock()
可以临时获得一个shared_ptr
:
if (auto sp = aptr.lock()) {// 安全访问
}
🌈 小结对比
智能指针 | 特点 | 适用场景 |
---|---|---|
unique_ptr | 独占、禁止拷贝 | 独占资源,防止泄漏 |
shared_ptr | 引用计数、共享资源 | 多方共享、动态生命周期管理 |
weak_ptr | 弱引用、不增加计数 | 避免循环引用、观察 shared_ptr |
🌟 C++ 智能指针主要就是 unique_ptr, shared_ptr, weak_ptr 三种,每种针对不同的所有权管理需求,配合 RAII 自动管理内存,防止泄漏。
✅ RAII 类本身不需要配合智能指针
👉 RAII 类 = 自己封装了资源管理(构造获取资源 + 析构释放资源)
👉 它的资源管理已经是安全的,不依赖智能指针。
🌰 比如:
{FileRAII file("test.txt", "w");file.write("Hello RAII");// 离开作用域时自动关闭文件,不需要智能指针
}
➡ 这里 RAII 类的对象本身就在栈上,出作用域自动销毁,资源释放。根本不需要智能指针参与。
🌈 什么时候会配合智能指针使用?
你可能会 动态创建 RAII 对象,这时:
- 如果你用
new
创建 RAII 对象,为避免手工 delete,就用智能指针管理它。 - 特别是当 RAII 对象需要跨作用域、多处共享时,智能指针(如
unique_ptr
/shared_ptr
)就很方便。
🌰 示例:
#include <memory>
auto filePtr = std::make_unique<FileRAII>("test.txt", "w");
filePtr->write("Hello RAII");
// unique_ptr 离开作用域自动释放 FileRAII 对象,FileRAII 析构释放资源
👉 这里智能指针管理 RAII 对象本身的生命周期,RAII 对象内部管理资源的生命周期。
⚡ 智能指针和 RAII 类的关系总结
情况 | 是否需要智能指针 |
---|---|
RAII 类对象在栈上声明(局部变量) | ❌ 不需要智能指针,作用域退出时自动释放 |
RAII 类对象动态创建(new) | ✅ 推荐用智能指针管理(避免手工 delete) |
RAII 类对象需要跨多个作用域共享 | ✅ 用 shared_ptr |
RAII 类对象转移所有权 | ✅ 用 unique_ptr |
📝 核心思路
RAII 解决资源管理,智能指针解决对象管理。它们可以单独用,也可以组合用,取决于对象的使用方式。