C++程序设计语言笔记——基本功能:指针、数组与引用
0 使用指针时越简单直接越好,不要对指针执行稀奇古怪的算术运算。
所有C++对象的尺寸都可以表示成char尺寸的整数倍,因此如果我们令char的尺寸为1,则使用sizeof运算符就能得到任意类型或对象的尺寸。下面是C++对于基本类型尺寸的一些规定:
- 1 = sizeof(char) ≤ sizeof(short) ≤ sizeof(int) ≤ sizeof(long) ≤ sizeof(long long)
- 1 ≤ sizeof(bool) ≤ sizeof(long)
- sizeof(char) ≤ sizeof(wchar_t) ≤ sizeof(long)
- sizeof(float) ≤ sizeof(double) ≤ sizeof(long double)
- sizeof(N) = sizeof(signed N) = sizeof(unsigned N)
为什么“稀奇古怪”的指针算术是危险的?
-
未定义行为(Undefined Behavior)
指针算术只能在同一连续内存块(如数组)内进行。超出范围的操作(例如跨越不同对象或越界访问)会导致未定义行为,可能导致程序崩溃、数据损坏或安全漏洞。int arr[5]; int *p = arr + 5; // 合法(指向末尾后一位,但不可解引用) int *q = p + 1; // 非法!未定义行为
-
类型大小的影响
指针加减的步长由指向类型的大小决定。如果类型不匹配或错误转换,可能导致意外的内存访问:char *pc = ...; int *pi = (int*)pc; pi++; // 实际增加的字节数为 sizeof(int),而非 1,可能跨越非法内存
-
对齐问题(Alignment)
某些架构(如ARM)要求特定类型必须对齐到特定地址。强制转换指针类型后进行算术运算可能导致未对齐访问,引发硬件异常或性能下降。 -
整数与指针的混淆
将指针强制转换为整数进行算术运算后再转回指针(如(int*) ((int)ptr + 1)
)是高度不可移植的,且可能因整数溢出或地址截断导致错误。 -
跨对象指针运算
不同对象的地址关系不可预测,即使它们看似相邻:int a, b; int *p = &a + 1; // 不一定指向 &b!
安全实践:如何正确使用指针算术?
-
仅在数组范围内操作
确保指针始终指向同一数组(或末尾后一位),避免越界:int arr[10]; for (int *p = arr; p != arr + 10; p++) { *p = 0; // 安全操作 }
-
使用标准库替代手动计算
优先使用容器(如C++的std::vector
、std::array
)或迭代器,而非原始指针:std::vector<int> vec(10); for (auto it = vec.begin(); it != vec.end(); ++it) { *it = 0; // 无需手动管理指针 }
-
避免类型双关(Type Punning)
如需处理不同数据类型,使用memcpy
或union
(需谨慎)代替指针强制转换:float f = 1.0; int i; memcpy(&i, &f, sizeof(f)); // 安全的方式复制字节
-
使用
offsetof
和容器抽象
对于结构体成员偏移,使用offsetof
宏而非手动计算:struct S { int a; char b; }; size_t offset = offsetof(S, b); // 安全获取成员偏移量
-
启用编译器的严格检查
开启编译选项(如-Wall -Wextra -Werror
)以捕获可疑操作,并利用静态分析工具(如Clang-Tidy)进行检查。
需要完全避免的操作
-
不同内存块的指针相减
int a[5], b[5]; ptrdiff_t diff = &b[0] - &a[0]; // 未定义行为!
-
整数与指针的随意转换
int *p = ...; intptr_t i = (intptr_t)p; i += 100; // 危险:可能溢出或破坏对齐 p = (int*)i; // 可能导致崩溃
-
通过指针算术绕过访问权限
例如,通过指针修改只读内存或私有数据结构的内部状态。
总结
指针算术是一把双刃剑:它提供了底层内存操作的灵活性,但也极易引入难以调试的错误。现代编程实践中,应优先使用更安全的抽象(如容器、智能指针、迭代器),仅在必要时谨慎使用原始指针,并严格遵循语言规范。记住:“Just because you can, doesn’t mean you should.”
1 注意不要越界访问数组,尤其不要在数组之外的区域写入内容。
C++允许静态的分配数组空间,也允许在栈上或者在自由存储上分配数组空间。特别要注意避免在接口中使用数组(比如作为函数的参数),因为数组名隐式转换成指针是C代码和C风格的C++代码中很多错误的根源,数组隐式转换为指针后,会丢失数组的大小信息。因此,在处理数组时,通常需要额外传递数组的大小。
1. 理解数组越界的风险
- 未定义行为(Undefined Behavior):访问数组外的内存可能覆盖其他变量、代码段或敏感数据,导致不可预测的结果。
- 安全漏洞:恶意用户可利用缓冲区溢出注入代码(如栈溢出攻击)。
- 数据损坏:越界写入可能破坏相邻数据结构,引发隐蔽的逻辑错误。
2. 常见越界场景及规避方法
场景1:循环条件错误
- 错误示例:
int arr[5]; for (int i = 0; i <= 5; i++) { // 越界访问 arr[5] arr[i] = i; }
- 修正方法:确保循环终止条件正确:
for (int i = 0; i < 5; i++) { // 正确:0 ≤ i < 5 arr[i] = i; }
场景2:指针算术越界
- 错误示例:
int arr[5]; int *p = arr; p += 5; // 允许指向末尾后一位,但... int value = *p; // 解引用导致未定义行为!
- 修正方法:仅在有效范围内解引用指针:
for (int *p = arr; p < arr + 5; p++) { *p = 0; // 安全操作 }
场景3:字符串操作溢出
- 错误示例:
char buffer[10]; strcpy(buffer, "这是一个超长字符串"); // 超出缓冲区大小
- 修正方法:使用安全函数并显式限制长度:
strncpy(buffer, "安全字符串", sizeof(buffer) - 1); buffer[sizeof(buffer) - 1] = '\0'; // 强制终止
场景4:动态内存越界
- 错误示例:
int *arr = malloc(5 * sizeof(int)); arr[5] = 10; // 越界写入
- 修正方法:始终跟踪分配的内存大小:
size_t size = 5; int *arr = malloc(size * sizeof(int)); if (index < size) { arr[index] = 10; // 安全访问 }
3. 防御性编程技巧
使用安全的数据结构
- C++容器:优先使用
std::vector
、std::array
或std::string
,它们自动管理内存并提供边界检查:std::vector<int> vec(5); vec.at(5) = 10; // 抛出 std::out_of_range 异常
启用编译器和工具检查
- 静态分析:使用编译器选项(如GCC的
-Wall -Werror
)或工具(Clang-Tidy)检测潜在越界。 - 动态检测:开启地址消毒剂(AddressSanitizer):
gcc -fsanitize=address -g program.c
验证外部输入
- 限制输入长度:处理用户输入时,始终验证数据长度:
char input[100]; fgets(input, sizeof(input), stdin); // 读取最多 99 字符
使用断言和异常处理
- 调试阶段:用断言检查关键索引:
assert(index >= 0 && index < array_size);
- 生产环境(C++):使用
try-catch
处理std::out_of_range
:try { value = vec.at(index); } catch (const std::out_of_range& e) { // 处理越界错误 }
4. 避免越界的代码规范
- 明确数组长度:传递数组时,始终附带长度参数:
void safe_function(int *arr, size_t length) { for (size_t i = 0; i < length; i++) { // 安全访问 } }
- 避免裸指针和手动算术:使用迭代器或范围循环:
for (auto& elem : vec) { /* 无需索引 */ }
- 优先使用安全库函数:
- 用
snprintf
替代sprintf
。 - 用
fgets
替代gets
。
- 用
总结
数组越界是程序稳定性和安全性的重大威胁。通过以下措施可显著降低风险:
- 严格检查索引范围。
- 使用安全的语言特性和库函数。
- 借助工具进行静态和动态检测。
- 编写防御性代码,验证所有外部输入。
始终牢记:“信任但要验证”(Trust but Verify)。即使代码逻辑看似正确,也要通过测试和工具确保无越界访问。
2 不要使用多维数组,用合适的容器替代它。
在C/C++中,原生多维数组(如int arr[3][4]
)虽然语法简单,但存在内存管理复杂、易出错、灵活性差等问题。现代编程实践中,应优先使用更安全、更易维护的容器替代它们。以下是详细的替代方案和最佳实践:
一、为什么不推荐原生多维数组?
- 静态大小限制
原生多维数组的大小必须在编译期确定,无法动态调整:int arr[10][20]; // 只能固定为10x20
- 内存不安全
越界访问无运行时检查,可能导致未定义行为:arr[10][25] = 42; // 越界但编译器不报错
- 传递和返回困难
数组作为函数参数时会退化为指针,丢失维度信息:void func(int arr[][20]); // 必须硬编码第二维大小
- 动态分配繁琐
手动分配/释放多维数组容易出错:int **arr = new int*[rows]; for (int i=0; i<rows; i++) arr[i] = new int[cols]; // 需要多层分配和释放
二、替代方案:使用现代容器
方案1:嵌套标准容器(推荐)
使用std::vector
或std::array
的嵌套结构,自动管理内存:
#include <vector>
#include <array>
// 动态二维数组
std::vector<std::vector<int>> vec2d(rows, std::vector<int>(cols));
// 固定大小三维数组
std::array<std::array<std::array<int, 5>, 4>, 3> arr3d;
// 安全访问(可选越界检查)
vec2d.at(0).at(1) = 42; // 带边界检查
方案2:扁平化一维容器
用一维容器模拟多维数组,提升内存局部性和性能:
// 二维数组 => 一维存储
class Matrix {
private:
size_t rows_, cols_;
std::vector<int> data_;
public:
Matrix(size_t r, size_t c) : rows_(r), cols_(c), data_(r * c) {}
int& operator()(size_t r, size_t c) {
return data_[r * cols_ + c];
}
};
Matrix mat(3, 4);
mat(1, 2) = 10; // 访问第二行第三列
方案3:使用第三方库(如Boost.MultiArray)
对于复杂需求,可直接使用成熟的多维数组库:
#include <boost/multi_array.hpp>
boost::multi_array<int, 3> arr(boost::extents[3][4][5]);
arr[0][1][2] = 42; // 安全访问
三、最佳实践
1. 优先选择标准库容器
- 动态大小:
std::vector<std::vector<T>>
- 固定大小:
std::array<std::array<T, N>, M>>
- 性能敏感场景:扁平化一维
std::vector<T>
2. 封装多维访问逻辑
通过类或函数封装索引计算,隐藏实现细节:
class Image {
private:
size_t width_, height_;
std::vector<uint8_t> pixels_;
public:
Image(size_t w, size_t h) : width_(w), height_(h), pixels_(w * h * 3) {}
uint8_t& pixel(size_t x, size_t y, size_t channel) {
return pixels_[(y * width_ + x) * 3 + channel];
}
};
3. 避免手动内存管理
- 使用RAII:容器自动释放内存,避免
new
/delete
。 - 异常安全:容器在异常发生时仍能正确清理资源。
4. 性能优化(必要时)
- 内存连续:扁平化存储提升缓存利用率。
- 预分配内存:对于动态容器,提前
reserve
避免多次扩容。 - 编译器优化:现代编译器对标准容器优化良好,通常无需手动优化。
四、需要避免的做法
1. 原生指针的多级动态分配
// 危险!容易内存泄漏
int **arr = new int*[rows];
for (int i=0; i<rows; i++)
arr[i] = new int[cols];
2. C风格数组的函数传参
void process(int arr[][20], int rows); // 不灵活且易错
3. 假设多维数组内存连续
int arr[3][4];
int *p = &arr[0][0];
p[12] = 5; // 假设3x4=12,但实际合法吗?(合法但依赖实现)
五、总结
场景 | 推荐方案 |
---|---|
动态多维数据 | std::vector 嵌套或扁平化一维容器 |
固定大小多维数据 | std::array 嵌套 |
高性能科学计算 | 扁平化一维容器 + 自定义访问接口 |
复杂多维操作 | Boost.MultiArray或Eigen等数学库 |
核心原则:
- 安全性:依赖标准容器的边界检查(如
at()
方法)。 - 可维护性:隐藏多维访问细节,提供清晰的接口。
- 零额外开销:现代编译器的优化使标准容器性能接近原生数组。
通过合理选择容器,可显著提升代码的健壮性和可维护性,同时避免内存相关错误。
3 用nullptr代替0和NULL。
在C++编程中,使用nullptr
替代0
和NULL
是一种现代且安全的实践。以下是详细的解释和最佳实践:
一、为什么用 nullptr
替代 0
和 NULL
?
1. 类型安全(Type Safety)
-
0
和NULL
的缺陷
0
是整数字面量,NULL
在C++中通常被定义为0
(或(void*)0
)。当传递空指针时,它们可能被隐式转换为整数类型,导致类型混淆:void func(int); // 重载函数1 void func(char*); // 重载函数2 func(0); // 调用 func(int) —— 可能不符合预期! func(NULL); // 仍可能调用 func(int)(依赖NULL的定义) func(nullptr); // 明确调用 func(char*)
-
nullptr
的优势
nullptr
的类型是std::nullptr_t
,只能隐式转换为指针类型,不会与整数混淆:int *p = nullptr; // 合法 int a = nullptr; // 编译错误:无法转换为整数
2. 避免重载歧义
在模板编程或重载函数中,nullptr
能明确表达空指针的意图:
template<typename T>
void process(T* ptr) { /* 处理指针 */ }
process(0); // 编译错误:无法推断 T 的类型
process(nullptr); // T 被推断为 void(或具体类型)
3. 代码清晰性
nullptr
明确表示空指针,而0
或NULL
可能被误解为整数或魔法值(Magic Number)。
二、如何正确使用 nullptr
?
1. 初始化所有指针
显式初始化指针为nullptr
,避免野指针:
int *p = nullptr; // 明确初始化为空
if (p == nullptr) { /* 安全检查 */ }
2. 函数参数和返回值
用nullptr
表示指针参数的默认值或空返回值:
class Resource {
public:
Resource(const char* path = nullptr) {
if (path) { /* 加载资源 */ }
}
};
Resource* loadResource() {
if (error) return nullptr;
return new Resource();
}
3. 与旧代码的兼容性
在混合使用旧代码时,逐步替换NULL
和0
:
// 旧代码
void legacy_func(int* p = NULL);
// 新代码调用
legacy_func(nullptr); // 安全替换
三、需要避免的陷阱
1. 不要将 nullptr
转换为布尔值
直接检查指针是否为nullptr
,而非隐式转换为bool
:
int *p = nullptr;
if (p) { ... } // 正确:隐式转换为 bool(false)
if (p != nullptr) { ... } // 更清晰的写法
2. 不要与整数比较
nullptr
不能与整数直接比较:
if (p == 0) { ... } // 不推荐(可能意外通过编译)
if (p == nullptr) { ... } // 正确
3. 不要用于非指针类型
nullptr
只能用于指针,不能用于整数或浮点数:
int a = nullptr; // 错误
float f = nullptr; // 错误
四、C++标准支持
- C++11 及更高版本:
nullptr
是C++11引入的关键字,需确保编译器支持。 - 向后兼容:若需兼容旧标准,可用宏模拟:
#if __cplusplus >= 201103L #define MY_NULLPTR nullptr #else #define MY_NULLPTR NULL #endif
五、总结
场景 | 用 nullptr | 用 0 /NULL |
---|---|---|
指针初始化/赋值 | ✅ 明确安全 | ❌ 潜在类型混淆 |
函数重载 | ✅ 避免歧义 | ❌ 可能调用错误重载 |
模板编程 | ✅ 正确推导类型 | ❌ 推断失败或错误 |
代码可读性 | ✅ 清晰表达意图 | ❌ 可能被误解为整数 |
核心建议:
- 全面替换:在新代码中始终使用
nullptr
,旧代码逐步替换。 - 静态检查:启用编译器警告(如
-Wzero-as-null-pointer-constant
)捕获遗留的0
或NULL
。 - 文档说明:在团队规范中明确要求使用
nullptr
。
通过使用nullptr
,可以显著提高代码的类型安全性、可读性和健壮性,避免许多由空指针引起的潜在问题。
4 与内置的C风格数组相比,优先选用容器(比如vector、array和valarray)。
一、为什么避免C风格数组?
-
手动内存管理风险
C风格数组的声明、分配和释放需手动控制,易引发内存泄漏或越界访问:int *arr = new int[10]; delete[] arr; // 必须匹配 new[] // 忘记释放或重复释放会导致崩溃
-
无边界检查
直接通过下标访问时,越界操作不会触发错误,导致未定义行为:int arr[5]; arr[5] = 42; // 越界写入,但编译器可能不报错
-
传递和返回困难
数组作为函数参数时会退化为指针,丢失长度信息:void process(int arr[]); // 无法直接获取数组大小
-
缺乏迭代器和算法支持
无法直接使用标准库算法(如std::sort
、std::find
):int arr[] = {3, 1, 4}; std::sort(arr, arr + 3); // 需手动计算范围
二、标准库容器的核心优势
1. std::vector
(动态数组)
- 自动内存管理:动态扩容/缩容,无需手动
new
/delete
。 - 安全访问:支持带边界检查的
.at(index)
方法。 - 迭代器和算法集成:
std::vector<int> vec = {5, 2, 8}; std::sort(vec.begin(), vec.end()); // 直接排序
- 内存连续:数据存储在连续内存中,兼容C API(通过
.data()
获取指针)。
2. std::array
(固定大小数组)
- 栈上分配:适合已知大小的场景,无动态内存开销。
- STL接口:提供
size()
、迭代器等标准方法:std::array<int, 3> arr = {1, 2, 3}; for (auto num : arr) std::cout << num; // 范围循环
3. std::valarray
(数值计算数组)
- 数学优化:支持向量化操作(如
+
、sin
、sum
):std::valarray<double> a = {1.0, 2.0}, b = {3.0, 4.0}; auto c = a + b; // c = {4.0, 6.0}
三、替换C风格数组的最佳实践
1. 动态数组场景
- 使用
std::vector
替代new[]
和指针:// 旧代码 int *arr = new int[10]; delete[] arr; // 新代码 std::vector<int> vec(10); // 自动管理内存
2. 固定大小数组场景
- 用
std::array
替代栈数组:// 旧代码 int arr[5] = {1, 2, 3, 4, 5}; // 新代码 std::array<int, 5> arr = {1, 2, 3, 4, 5};
3. 数值计算场景
- 用
std::valarray
替代手动循环:// 旧代码 double a[3] = {1.0, 2.0, 3.0}, b[3] = {4.0, 5.0, 6.0}; double c[3]; for (int i=0; i<3; i++) c[i] = a[i] + b[i]; // 新代码 std::valarray<double> a = {1.0, 2.0, 3.0}, b = {4.0, 5.0, 6.0}; auto c = a + b; // 向量化加法
四、需要保留C风格数组的场景
-
与C语言API交互
当调用C库函数时需要传递原始指针:std::vector<int> vec(100); c_api_function(vec.data(), vec.size());
-
极端性能优化
在嵌入式系统或高频交易等场景,需手动控制内存布局(但应先验证容器性能是否不足)。
五、性能对比与优化
场景 | C风格数组 | 标准容器 |
---|---|---|
内存分配 | 手动控制,易出错 | 自动管理(vector 可预分配) |
访问速度 | 相同(均为连续内存) | 相同 |
功能扩展 | 需手动实现 | 内置排序、查找、迭代器等 |
代码安全性 | 低(无边界检查) | 高(可选.at() 检查) |
- 预分配内存减少开销:
std::vector<int> vec; vec.reserve(1000); // 预分配空间,避免多次扩容
六、总结
需求 | 推荐容器 | 优势 |
---|---|---|
动态大小数组 | std::vector | 自动扩容、内存连续 |
固定大小数组 | std::array | 栈分配、无额外开销 |
数值计算 | std::valarray | 向量化操作、数学函数支持 |
兼容C API | C风格数组 + vector | 通过.data() 传递指针 |
核心原则:
- 默认选择容器:除非有明确需求(如兼容C API),否则优先使用标准库容器。
- 代码即文档:容器的使用明确表达了数据结构的意图(如动态性、固定大小)。
- 团队协作:统一使用容器可减少错误并提升代码可读性。
通过这一实践,可以显著减少内存错误、提升开发效率,并为代码的长期维护奠定坚实基础。
5 优先选用string,而不是以0结尾的char数组。
一、为什么不推荐C风格字符串?
-
手动内存管理风险
C风格字符串需要手动分配/释放内存,易导致内存泄漏或重复释放:char *str = new char[100]; // 需要手动管理 delete[] str; // 忘记释放则泄漏
-
缓冲区溢出漏洞
无内置长度检查,操作不当易引发溢出:char buf[10]; strcpy(buf, "This is a very long string!"); // 溢出覆盖相邻内存
-
功能贫乏
基础操作依赖C库函数(如strlen
、strcat
),代码冗长且易错:char s1[20] = "Hello"; char s2[] = "World"; strcat(s1, s2); // 需确保s1足够大
-
类型不安全
char*
可隐式转换为其他类型,导致逻辑错误:void log(int id); log("error"); // 编译通过但行为错误(字符串地址被转为int)
二、为什么优先使用 std::string
?
1. 自动内存管理
- 自动扩容:动态调整内存大小,无需手动分配:
std::string str; str = "A very long string..."; // 自动分配足够空间
2. 安全性保障
- 边界检查:通过
.at()
方法进行安全访问(越界时抛出std::out_of_range
):std::string s = "test"; s.at(4); // 抛出异常(索引越界)
- 防止溢出:
operator+=
、append()
等操作自动处理容量:std::string s = "Hello"; s += " World!"; // 无需担心溢出
3. 丰富的操作接口
- 字符串操作:直接支持拼接、查找、子串提取等:
std::string s = "C++ is powerful"; size_t pos = s.find("powerful"); // 返回索引位置 std::string sub = s.substr(0, 3); // "C++"
4. 类型安全与编码支持
- 明确类型:
std::string
与char*
类型不兼容,避免误用:void log(const std::string &msg); log("error"); // 隐式构造std::string,类型安全
- 多语言支持:与
std::wstring
、std::u16string
等配合处理Unicode。
5. 与STL无缝集成
- 算法兼容:可直接用于标准库算法:
std::string s = "algorithm"; std::sort(s.begin(), s.end()); // "aghilmort"
三、最佳实践
1. 替换C字符串的场景
-
初始化与赋值:
// 旧代码 const char *cstr = "Hello"; char buf[20]; strcpy(buf, cstr); // 新代码 std::string str = "Hello"; // 直接初始化 str = "New content"; // 自动释放旧内存
-
字符串拼接:
std::string path = "/usr"; path += "/local/bin"; // 无需计算缓冲区大小
2. 与C API交互
通过c_str()
或data()
兼容需要const char*
的接口:
std::string s = "Hello";
printf("%s\n", s.c_str()); // 保证以'\0'结尾
3. 性能优化
-
预分配内存:减少动态扩容次数:
std::string s; s.reserve(1000); // 预分配内存
-
使用移动语义(C++11+):
std::string getData() { std::string data = "Large data"; return data; // 移动而非拷贝 }
四、需要保留C风格字符串的场景
- 与C语言库交互
如调用fopen
、execvp
等需要const char*
参数的函数。 - 极端性能优化
在实时系统中需要绝对可控的内存操作(需严格验证必要性)。
五、总结
场景 | C风格字符串 | std::string |
---|---|---|
内存管理 | 手动分配/释放 | 自动管理 |
安全性 | 易溢出,无检查 | 边界检查,防溢出 |
字符串操作 | 依赖C库函数 | 内置丰富方法 |
类型安全 | 易误用为其他类型 | 明确类型 |
性能 | 低开销(但风险高) | 优化后接近原生(如预分配) |
核心原则:
- 默认使用
std::string
:除非有明确需求(如兼容C API),否则避免使用char[]
或char*
。 - 代码即文档:
std::string
明确表达“字符串”语义,而非原始内存块。 - 团队规范:在代码审查中禁止裸字符串操作,强制使用现代C++特性。
通过这一实践,可显著减少内存错误、提升开发效率,并增强代码的可读性和可维护性。
6 如果字符串字面增值常量中包含太多反斜线,则使用原始字符串。
在C++中,当字符串字面值包含大量反斜线(\
)时(如文件路径、正则表达式、转义序列等),使用**原始字符串字面值(Raw String Literals)**可以显著提升代码可读性并减少转义错误。以下是具体用法和示例:
一、原始字符串的语法
// 基本格式
R"(原始内容)"; // 默认分隔符对
R"delimiter(原始内容)delimiter"; // 自定义分隔符(避免与内容冲突)
- 核心特性:
- 反斜线
\
不再作为转义字符,直接视为普通字符。 - 换行符、引号等均保留原样(除非遇到结束分隔符)。
- 允许在字符串中直接包含未转义的双引号
"
。
- 反斜线
二、典型应用场景
1. 文件路径(避免重复反斜线)
// 普通字符串(需转义)
const char *path1 = "C:\\Program Files\\MyApp\\data\\file.txt";
// 原始字符串(直接书写)
const char *path2 = R"(C:\Program Files\MyApp\data\file.txt)";
2. 正则表达式(简化转义)
// 普通字符串(需转义反斜线)
std::regex pattern1("\\d{4}-\\d{2}-\\d{2}"); // 匹配日期YYYY-MM-DD
// 原始字符串(直接表达)
std::regex pattern2(R"(\d{4}-\d{2}-\d{2})");
3. 多行文本(保留换行和缩进)
const char *text = R"(
Hello, World!
This is a multi-line
raw string.
)";
4. 包含特殊字符的字符串
// 普通字符串需转义引号
const char *quote1 = "He said, \"Hello!\"";
// 原始字符串直接包含引号
const char *quote2 = R"(He said, "Hello!")";
三、处理复杂场景:自定义分隔符
当字符串内容包含默认结束符)"
时,需自定义分隔符避免冲突:
// 错误示例:内容中的 )" 导致提前终止
const char *bad = R"(内容中出现 )" 会导致语法错误)";
// 正确做法:使用自定义分隔符(如XYZ)
const char *good = R"XYZ(内容可以安全包含 )" 或其他字符)XYZ";
四、注意事项
-
兼容性
原始字符串字面值从C++11开始支持,确保编译器开启C++11或更高标准。 -
结束符选择
自定义分隔符应为唯一标识符(如_MY_DELIM_
),避免与字符串内容重复。 -
性能
原始字符串与普通字符串在运行时无性能差异,仅为语法糖。
五、对比总结
场景 | 普通字符串 | 原始字符串 |
---|---|---|
文件路径 | "C:\\dir\\file.txt" | R"(C:\dir\file.txt)" |
正则表达式 | "\\\\w+\\d{2}" | R"(\w+\d{2})" |
多行文本 | 需用\n 分隔 | 直接换行并保留缩进 |
包含引号 | "He said, \"Hi!\"" | R"(He said, "Hi!")" |
六、总结
- 优先使用原始字符串:当字符串包含反斜线、引号、多行文本时。
- 简化代码:减少转义字符的视觉干扰,提升可维护性。
- 灵活处理冲突:通过自定义分隔符解决内容与结束符冲突问题。
通过这一实践,可以显著减少因转义错误导致的调试时间,使代码更直观可靠。
7 const引用比普通引用更适合作为函数的实参。
在C++中,使用const
引用(如const T&
)作为函数参数通常比普通引用(T&
)更安全、更灵活。以下是具体原因和场景分析:
一、const
引用的核心优势
1. 避免意外修改
- 普通引用允许修改实参,可能导致调用者数据被意外篡改:
void process(int& x) { x = 42; // 修改了外部变量(可能不符合预期) } int a = 10; process(a); // a 被改为42
const
引用禁止修改,明确表达“只读”语义:void safe_process(const int& x) { // x = 42; // 编译错误!无法修改 }
2. 支持传递临时对象和字面量
- 普通引用无法绑定到临时对象或字面量:
void modify(int& x) { ... } modify(10); // 错误:字面量无法绑定到非const引用 modify(a + b); // 错误:临时对象无法绑定到非const引用
const
引用可以绑定到临时对象和字面量:void read_only(const int& x) { ... } read_only(10); // 合法 read_only(a + b); // 合法
3. 提高代码可读性
const
引用明确告知调用者:函数不会修改参数内容,降低心智负担。- 普通引用可能误导调用者:若函数不需要修改参数,使用普通引用会让调用者误以为参数可能被修改。
二、适用场景分析
1. 只读访问大对象
- 值传递(拷贝)开销大,适合用
const
引用避免拷贝:struct BigData { /* 包含大量成员 */ }; // 值传递(拷贝开销高) void process_by_value(BigData data); // const引用(无拷贝) void process_by_ref(const BigData& data); // 优先选择
2. 需要兼容多种输入类型
const
引用支持隐式类型转换:void print(const std::string& s); print("Hello"); // 合法:从`const char*`隐式构造临时string
3. 避免悬空引用风险
- 普通引用可能指向无效内存:
int& get_ref() { int x = 10; return x; // 返回局部变量的引用(悬空引用!) }
const
引用延长临时对象生命周期:const int& get_safe_ref() { return 42; // 临时对象生命周期延长到引用作用域结束 }
三、何时使用普通引用?
仅当函数需要修改传入的实参时使用普通引用:
// 示例:交换两个值
void swap(int& a, int& b) {
int tmp = a;
a = b;
b = tmp;
}
四、总结
场景 | 参数类型 | 说明 |
---|---|---|
只读访问参数(大对象或内置类型) | const T& | 避免拷贝,防止意外修改 |
需要修改参数 | T& | 明确允许修改 |
传递临时对象或字面量 | const T& | 普通引用无法绑定 |
核心原则:
- 默认优先使用
const
引用:除非需要修改参数,否则用const T&
。 - 避免过度使用普通引用:减少副作用,提升代码可维护性。
通过合理选择参数类型,可以显著提升代码的安全性、性能和可读性。
8 只有当需要转发和移动时才使用右值引用。
在C++中,右值引用(T&&
)的核心用途是支持移动语义和完美转发。遵循“仅在需要移动或转发时使用右值引用”的原则,可以避免滥用并提升代码的安全性与效率。以下是具体场景和最佳实践:
一、右值引用的适用场景
1. 实现移动语义
当需要高效转移资源(如动态内存、文件句柄)而非复制时,使用右值引用定义移动构造函数和移动赋值运算符:
class String {
public:
// 移动构造函数(从右值“窃取”资源)
String(String&& other) noexcept
: data_(other.data_), size_(other.size_) {
other.data_ = nullptr; // 置空源对象,避免重复释放
}
// 移动赋值运算符
String& operator=(String&& other) noexcept {
if (this != &other) {
delete[] data_;
data_ = other.data_;
size_ = other.size_;
other.data_ = nullptr;
}
return *this;
}
private:
char* data_;
size_t size_;
};
// 使用移动语义避免深拷贝
String a = "Hello";
String b = std::move(a); // 调用移动构造函数
2. 完美转发(Perfect Forwarding)
在泛型函数中保持参数的左值/右值属性,以便传递给下层函数:
template<typename T>
void wrapper(T&& arg) {
// 使用 std::forward 保持值类别(左值/右值)
target_function(std::forward<T>(arg));
}
void target_function(const std::string&); // 处理左值
void target_function(std::string&&); // 处理右值
std::string s = "test";
wrapper(s); // 传递左值,调用左值重载
wrapper(std::move(s)); // 传递右值,调用右值重载
二、应避免滥用右值引用的场景
1. 无需移动或转发的函数参数
若函数仅需读取参数内容,使用const
左值引用更安全:
// ❌ 错误:不需要移动,右值引用误导调用者
void print_name(std::string&& name) {
std::cout << name;
}
// ✅ 正确:使用 const 左值引用
void print_name(const std::string& name) {
std::cout << name;
}
2. 返回局部对象的右值引用
返回局部对象的右值引用会导致悬垂引用:
// ❌ 危险:返回局部变量的右值引用
std::string&& get_temp() {
std::string s = "temp";
return std::move(s); // s 在函数返回后被销毁!
}
// ✅ 正确:返回值(触发移动或拷贝)
std::string get_temp() {
return "temp";
}
3. 过度使用 std::move
在不需要转移资源时盲目移动,可能破坏左值对象状态:
std::vector<int> data = {1, 2, 3};
// ❌ 错误:data 后续可能被意外使用
auto processed = std::move(data);
// ✅ 仅在确定不再需要原对象时移动
if (should_take_ownership) {
auto processed = std::move(data);
}
三、右值引用的误用风险
误用场景 | 后果 | 修正方法 |
---|---|---|
返回局部对象的右值引用 | 悬垂引用,未定义行为 | 返回值(触发移动或拷贝) |
非模板函数参数使用T&& | 无法绑定左值,限制调用灵活性 | 改用const T& 或重载左右值版本 |
未置空源对象资源 | 重复释放或访问已移动对象 | 移动后置空源对象指针 |
四、总结:何时使用右值引用?
场景 | 代码示例 | 说明 |
---|---|---|
移动构造函数/赋值运算符 | String(String&&) | 高效转移资源,避免拷贝 |
完美转发 | template<typename T> void wrapper(T&& arg) | 保持参数值类别,与std::forward 配合 |
接收临时对象 | vec.push_back(std::string("temp")) | 利用移动优化性能 |
核心原则:
- 移动语义:仅在需要高效转移资源所有权时使用。
- 完美转发:仅在泛型代码中需要保持参数原始类型时使用。
- 明确意图:避免误导调用者,确保代码行为可预测。
遵循这些准则,可充分发挥右值引用的优势,同时规避潜在风险。
9 让表示所有权的指针位于句柄类的内部。
在C++中,将表示资源所有权的指针封装在句柄类(Handle Class)内部是一种重要的资源管理策略,其核心思想是通过RAII(Resource Acquisition Is Initialization)机制实现自动化的资源生命周期管理。以下是具体实现方式和最佳实践:
一、什么是句柄类(Handle Class)?
句柄类是指对外隐藏资源管理细节,通过类内部封装的所有权指针(如unique_ptr
、shared_ptr
)自动管理资源的类。
目标:
- 确保资源在析构时自动释放。
- 避免裸指针暴露,防止所有权混乱。
二、实现步骤
1. 定义句柄类并封装所有权指针
#include <memory>
class ResourceHandle {
private:
// 所有权指针(例如管理文件句柄、内存等)
std::unique_ptr<ResourceType> resource_;
public:
// 构造函数获取资源所有权
explicit ResourceHandle(ResourceType* raw_ptr)
: resource_(raw_ptr) {}
// 禁止拷贝(若需要拷贝语义,改用 shared_ptr)
ResourceHandle(const ResourceHandle&) = delete;
ResourceHandle& operator=(const ResourceHandle&) = delete;
// 允许移动(转移所有权)
ResourceHandle(ResourceHandle&&) noexcept = default;
ResourceHandle& operator=(ResourceHandle&&) noexcept = default;
// 提供资源访问接口(不暴露所有权指针)
ResourceType* get() const { return resource_.get(); }
ResourceType& operator*() const { return *resource_; }
ResourceType* operator->() const { return resource_.get(); }
// 析构时自动释放资源(通过 unique_ptr)
~ResourceHandle() = default;
};
2. 使用示例
// 假设 ResourceType 是某个需要管理的资源类型(例如文件句柄)
struct FileResource {
FILE* file;
explicit FileResource(const char* path) : file(fopen(path, "r")) {}
~FileResource() { if (file) fclose(file); }
};
int main() {
// 创建句柄类实例,转移资源所有权
ResourceHandle handle(new FileResource("data.txt"));
// 使用资源
if (handle->file) {
char buffer[100];
fread(buffer, 1, 100, handle->file);
}
// 离开作用域时,handle 析构,自动释放 FileResource
return 0;
}
三、关键设计原则
1. 所有权指针的选择
- 独占所有权:使用
std::unique_ptr
(默认选择)。 - 共享所有权:若需多个句柄共享资源,改用
std::shared_ptr
:class SharedResourceHandle { private: std::shared_ptr<ResourceType> resource_; // ... 其他代码与 unique_ptr 类似 };
2. 禁用拷贝语义(默认)
- 避免重复释放:若使用
unique_ptr
,必须禁用拷贝构造函数和拷贝赋值运算符。 - 支持移动语义:允许通过移动操作转移资源所有权。
3. 提供安全的资源访问
- 不返回内部指针:避免外部代码绕过句柄类直接操作资源。
- 必要时返回引用或值:
// 返回资源副本(安全但可能有性能开销) ResourceType get_resource_copy() const { return *resource_; } // 返回引用(需确保句柄生命周期覆盖使用范围) const ResourceType& get_resource_ref() const { return *resource_; }
四、对比裸指针直接管理
场景 | 裸指针管理 | 句柄类封装 |
---|---|---|
资源释放 | 需手动delete 或fclose | 析构函数自动释放 |
所有权传递 | 易出错(需文档约定) | 通过移动语义或智能指针明确所有权 |
异常安全 | 资源可能泄漏 | 异常发生时仍能正确释放资源 |
线程安全 | 需手动同步 | shared_ptr 引用计数原子操作提供基本安全 |
五、高级用法
1. 自定义删除器
通过unique_ptr
或shared_ptr
自定义资源释放逻辑:
// 管理文件句柄的句柄类(自定义删除器)
class FileHandle {
private:
std::unique_ptr<FILE, decltype(&fclose)> file_;
public:
explicit FileHandle(const char* path)
: file_(fopen(path, "r"), &fclose) {}
FILE* get() const { return file_.get(); }
// ... 其他接口
};
2. 工厂函数封装构造
隐藏资源创建细节,强制使用句柄类:
class DatabaseConnection {
private:
DatabaseConnection() = default; // 私有构造函数
friend class DatabaseHandle; // 仅允许句柄类构造
public:
void query(const std::string& sql) { /* ... */ }
};
class DatabaseHandle {
private:
std::unique_ptr<DatabaseConnection> conn_;
public:
static DatabaseHandle create() {
DatabaseHandle handle;
handle.conn_ = std::make_unique<DatabaseConnection>();
return handle;
}
// ... 其他接口
};
六、总结
- 核心原则:将资源所有权封装在句柄类内部,通过智能指针自动管理生命周期。
- 优势:避免资源泄漏、简化代码、强制明确所有权语义。
- 适用场景:文件句柄、网络连接、动态内存、GPU资源等需要明确管理的资源。
通过这种设计,可以显著提升代码的健壮性和可维护性,同时降低资源管理的心智负担。
10 在底层代码之外尽量不要使用void*。
在C++编程中,除非涉及底层内存操作或与C语言接口交互,否则应尽量避免使用void*
。这种通用指针类型会破坏类型安全,增加代码维护难度,并引入潜在风险。以下是详细分析和替代方案:
一、为什么避免使用void*
?
1. 类型不安全
void*
丢失类型信息,需手动强制转换,易引发未定义行为:void* data = new int(42); double* p = static_cast<double*>(data); // 错误类型转换,但编译器不报错
2. 内存管理困难
- 无法通过
delete
直接释放,需依赖外部类型信息:void release(void* ptr) { delete ptr; // ❌ 错误:无法确定类型,可能导致析构不完全 }
3. 破坏代码可读性
- 无法通过代码直接理解指针的预期用途,增加维护成本。
4. 与现代C++特性冲突
- 与模板、RAII、智能指针等现代特性不兼容,降低代码健壮性。
二、替代方案
1. 使用模板(泛型编程)
当需要处理多种类型时,优先用模板替代void*
:
// 泛型函数处理任意类型
template<typename T>
void process(T* data) {
// 明确类型,安全操作
}
process(new int(42)); // 实例化为 int 版本
process(new double(3.14)); // 实例化为 double 版本
2. 基于继承的多态
若需运行时多态,使用基类指针而非void*
:
class Base {
public:
virtual void execute() = 0;
virtual ~Base() = default;
};
class Derived : public Base {
public:
void execute() override { /* ... */ }
};
Base* obj = new Derived();
obj->execute(); // 安全调用
delete obj;
3. 类型安全容器
用std::any
(C++17)或std::variant
存储异构数据:
#include <any>
#include <variant>
// 存储任意类型(需运行时类型检查)
std::any data = 42;
if (data.type() == typeid(int)) {
int value = std::any_cast<int>(data);
}
// 存储有限类型集合(编译时类型安全)
std::variant<int, double, std::string> var = "Hello";
if (std::holds_alternative<std::string>(var)) {
std::cout << std::get<std::string>(var);
}
4. 回调函数中的用户数据
用类型擦除技术(如std::function
+lambda)替代void*
:
#include <functional>
void register_callback(std::function<void()> callback) {
// 存储回调,无需 void* 传递用户数据
}
int value = 100;
register_callback([&value] {
std::cout << value; // 通过捕获列表传递上下文
});
三、允许使用void*
的例外场景
1. 底层内存操作
与操作系统API、硬件寄存器或内存池交互时,可受限使用:
void* memory = mmap(/* ... */); // 系统级内存分配
int* buffer = static_cast<int*>(memory);
2. 与C语言接口交互
兼容C库时,在接口边界处转换类型,内部仍用安全类型:
// C 接口
extern "C" void c_library_function(void* data);
// C++ 封装
template<typename T>
void safe_wrapper(T* data) {
c_library_function(static_cast<void*>(data)); // 仅在接口处转换
}
四、总结
场景 | 错误做法 | 推荐替代方案 |
---|---|---|
处理多种数据类型 | void* + 强制转换 | 模板、std::variant 、std::any |
运行时多态 | void* + 类型标记 | 基类指针 + 虚函数 |
回调函数传递用户数据 | void* 上下文参数 | Lambda捕获、std::function |
内存管理 | 手动分配释放 void* | 智能指针(unique_ptr 等) |
核心原则:
- 默认禁用
void*
:除非涉及底层资源操作或必要的外部接口。 - 严格封装:若必须使用,将其限制在模块内部,并通过类型安全的接口暴露功能。
- 静态检查:启用编译器警告(如
-Wold-style-cast
)捕获不必要的类型转换。
通过遵循这些准则,可显著提升代码的健壮性和可维护性,减少与类型相关的潜在错误。
11 用const指针和const引用表示接口中不允许修改的部分。
在C++中,使用const
指针和const
引用可以有效明确接口中“只读”部分的语义,防止数据被意外修改,同时提高代码的安全性和可维护性。以下是具体用法和场景分析:
一、const
指针和引用的核心作用
- 保护数据不被修改:明确告知调用者,接口不会修改传入的数据。
- 增强代码可读性:通过类型系统直接表达设计意图。
- 支持常量对象:允许常量对象调用接口,避免编译错误。
二、使用场景与示例
1. 函数参数:传递只读数据
-
const
引用:用于传递大型对象,避免拷贝开销,且禁止修改:// 示例:打印字符串(不修改原数据) void print_string(const std::string& str) { std::cout << str; // str[0] = 'X'; // 编译错误:无法修改 const 引用 } // 调用 std::string s = "Hello"; print_string(s); // 安全传递
-
const
指针:当需要明确允许空指针(nullptr
)时使用:// 示例:计算数组和(数组内容不可修改) int sum_array(const int* arr, size_t size) { int total = 0; for (size_t i = 0; i < size; ++i) { total += arr[i]; // 只读访问 // arr[i] = 0; // 编译错误 } return total; } // 调用 int values[] = {1, 2, 3}; sum_array(values, 3); sum_array(nullptr, 0); // 允许空指针
2. 返回值:返回内部状态的只读视图
-
const
引用:返回类内部数据的只读引用,避免拷贝:class DataContainer { private: std::vector<int> data_; public: // 返回内部数据的只读视图 const std::vector<int>& get_data() const { return data_; } }; // 调用 DataContainer container; const auto& data = container.get_data(); // data.push_back(42); // 编译错误:const 引用禁止修改
-
const
指针:返回指向常量数据的指针:class Image { private: unsigned char* pixels_; public: // 返回只读像素数据 const unsigned char* get_pixels() const { return pixels_; } };
3. 成员函数:承诺不修改对象状态
const
成员函数:在成员函数后添加const
,表示该函数不会修改对象成员:class BankAccount { private: double balance_; public: // 只读接口:获取余额(不修改对象) double get_balance() const { // balance_ = 0; // 编译错误:const 成员函数禁止修改成员 return balance_; } };
三、常见错误与规避
1. 误用非const
参数
- 问题:函数意外修改输入数据,导致调用者数据损坏。
void bad_trim(std::string& str); // 可能修改 str
- 修正:除非明确需要修改,否则优先用
const
:std::string trimmed(const std::string& str); // 返回新字符串,原数据不变
2. 返回非const
内部数据引用
- 问题:外部代码通过引用修改内部状态,破坏封装性。
class UnsafeContainer { public: std::vector<int>& get_data() { return data_; } // 危险! };
- 修正:返回
const
引用或副本:const std::vector<int>& get_data() const { return data_; }
3. 忽略const
正确性
- 问题:无法在常量对象上调用非
const
成员函数。const BankAccount account; account.get_balance(); // 需确保 get_balance 是 const 成员函数
四、高级用法
1. 重载const
和非const
版本
为同一成员函数提供const
和非const
重载,支持不同上下文:
class TextBuffer {
private:
char* text_;
public:
// 非 const 版本允许修改
char& operator[](size_t index) {
return text_[index];
}
// const 版本只读访问
const char& operator[](size_t index) const {
return text_[index];
}
};
// 调用
TextBuffer buffer;
buffer[0] = 'A'; // 调用非 const 版本
const TextBuffer read_only_buffer;
char c = read_only_buffer[0]; // 调用 const 版本
2. mutable
成员
允许在const
成员函数中修改某些特定成员(如缓存、互斥锁):
class Cache {
private:
mutable std::mutex mutex_; // 可被 const 函数修改
mutable std::string cached_data_;
public:
std::string get_data() const {
std::lock_guard<std::mutex> lock(mutex_); // 允许修改 mutex_
return cached_data_;
}
};
五、总结
场景 | 推荐方式 | 说明 |
---|---|---|
传递大型对象 | const T& | 避免拷贝,禁止修改 |
允许空指针的只读数据 | const T* | 明确处理nullptr |
返回内部状态 | const T& 或 const T* | 防止外部修改 |
成员函数不修改对象 | 成员函数后加const | 支持常量对象调用 |
核心原则:
- 默认优先使用
const
:除非需要修改数据,否则参数和返回值尽量用const
修饰。 - 保持
const
正确性:确保成员函数、参数和返回值的const
语义一致。
通过合理使用const
指针和引用,可以显著提升代码的健壮性、可读性和可维护性。
12 引用比指针更适合作为函数的实参,不过当需要处理”对象缺失”的情况时例外。
在C++中,引用和指针均可用于向函数传递参数,但两者在语义、安全性和适用场景上存在显著差异。以下是详细的对比和场景分析,帮助你在不同情况下合理选择:
一、引用的核心优势
1. 语义明确且安全
- 不可为空:引用必须绑定到有效对象,无法指向空值(除非通过非法操作,如解引用空指针,但这属于未定义行为)。
- 无所有权暗示:引用仅表示对某个对象的别名,不涉及所有权管理(所有权由智能指针或对象生命周期管理)。
- 代码可读性:函数参数使用引用时,明确告知调用者“此参数必须有效且会被使用”。
2. 典型使用场景
- 必须存在的参数:函数要求参数一定有效且不能为空。
void process(const std::string& input) { // input 必须有效,无需检查空值 }
- 避免拷贝开销:传递大型对象时,用
const
引用避免拷贝。void print(const BigData& data) { // 读取 data,不修改且无拷贝 }
- 修改实参:通过非
const
引用允许函数修改外部对象。void increment(int& value) { value++; // 直接修改外部变量 }
二、指针的核心优势
1. 处理“对象缺失”
- 可为空(
nullptr
):指针参数允许传递空值,明确表示“无对象”。// 查找函数:可能返回空指针 const User* find_user(const std::string& name) { if (/*未找到*/) return nullptr; return &user; } // 调用方处理“对象缺失” if (const User* user = find_user("Alice")) { process(*user); }
2. 需要明确所有权转移
- 动态对象传递:当函数需要接管对象所有权时,用指针(通常结合智能指针)。
void take_ownership(std::unique_ptr<Resource> ptr) { // 接管资源所有权 }
3. 可选参数
- 参数可选性:指针参数可通过
nullptr
表示“无输入”。void log(const char* message = nullptr) { if (message) std::cout << message; }
三、对比总结
场景 | 推荐方式 | 示例 |
---|---|---|
参数必须存在且不能被修改 | const T& | void read(const Data& data) |
参数必须存在且需要被修改 | T& | void modify(std::string& str) |
参数可能不存在(可选) | T* (允许nullptr ) | void save(const char* filename = nullptr) |
传递动态对象的所有权 | std::unique_ptr<T> | void take(std::unique_ptr<Resource>) |
需要与C接口交互 | T* | void c_api_handler(void* user_data) |
四、最佳实践
-
默认优先使用引用:
- 函数参数若必须存在且无需处理空值,用引用(尤其是
const
引用)。 - 明确表达“此参数不可为空”,减少空值检查负担。
- 函数参数若必须存在且无需处理空值,用引用(尤其是
-
仅在必要时使用指针:
- 需要处理“对象缺失”或可选参数时,使用指针并检查
nullptr
。 - 动态资源管理优先用智能指针(如
unique_ptr
、shared_ptr
),而非原始指针。
- 需要处理“对象缺失”或可选参数时,使用指针并检查
-
避免混淆语义:
- 不要用非
const
引用模拟可选参数(如通过默认构造的引用参数),这会破坏代码可读性。 - 不要用指针传递必须存在的参数,除非接口设计明确要求(如某些C风格API)。
- 不要用非
五、示例场景分析
场景1:必须存在的参数
// 正确:引用确保参数有效
void encrypt(const std::string& input, std::string& output);
// 错误:调用者可能传递空指针
void encrypt(const std::string* input, std::string* output);
场景2:可选参数
// 正确:指针允许传递 nullptr
void render(const Mesh* mesh = nullptr) {
if (mesh) mesh->draw();
}
// 错误:引用无法表示“无参数”(需额外机制,如重载)
void render(const Mesh& mesh); // 必须传递有效对象
场景3:资源所有权转移
// 正确:智能指针明确所有权转移
void store_resource(std::unique_ptr<Database> db);
// 错误:原始指针无法清晰表达所有权
void store_resource(Database* db); // 是否需要 delete?调用者易混淆
六、总结
- 引用:用于必须存在的参数,强调安全性、避免拷贝,明确“无空值”。
- 指针:用于可选参数、对象缺失处理或资源所有权传递,需配合
nullptr
检查和智能指针。
通过合理选择引用或指针,可以提升代码的健壮性、可读性和维护性,同时精准表达设计意图。