当前位置: 首页 > news >正文

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)

为什么“稀奇古怪”的指针算术是危险的?

  1. 未定义行为(Undefined Behavior)
    指针算术只能在同一连续内存块(如数组)内进行。超出范围的操作(例如跨越不同对象或越界访问)会导致未定义行为,可能导致程序崩溃、数据损坏或安全漏洞。

    int arr[5];
    int *p = arr + 5;  // 合法(指向末尾后一位,但不可解引用)
    int *q = p + 1;    // 非法!未定义行为
    
  2. 类型大小的影响
    指针加减的步长由指向类型的大小决定。如果类型不匹配或错误转换,可能导致意外的内存访问:

    char *pc = ...;
    int *pi = (int*)pc; 
    pi++;  // 实际增加的字节数为 sizeof(int),而非 1,可能跨越非法内存
    
  3. 对齐问题(Alignment)
    某些架构(如ARM)要求特定类型必须对齐到特定地址。强制转换指针类型后进行算术运算可能导致未对齐访问,引发硬件异常或性能下降。

  4. 整数与指针的混淆
    将指针强制转换为整数进行算术运算后再转回指针(如(int*) ((int)ptr + 1))是高度不可移植的,且可能因整数溢出或地址截断导致错误。

  5. 跨对象指针运算
    不同对象的地址关系不可预测,即使它们看似相邻:

    int a, b;
    int *p = &a + 1;  // 不一定指向 &b!
    

安全实践:如何正确使用指针算术?

  1. 仅在数组范围内操作
    确保指针始终指向同一数组(或末尾后一位),避免越界:

    int arr[10];
    for (int *p = arr; p != arr + 10; p++) {
        *p = 0; // 安全操作
    }
    
  2. 使用标准库替代手动计算
    优先使用容器(如C++的std::vectorstd::array)或迭代器,而非原始指针:

    std::vector<int> vec(10);
    for (auto it = vec.begin(); it != vec.end(); ++it) {
        *it = 0; // 无需手动管理指针
    }
    
  3. 避免类型双关(Type Punning)
    如需处理不同数据类型,使用memcpyunion(需谨慎)代替指针强制转换:

    float f = 1.0;
    int i;
    memcpy(&i, &f, sizeof(f)); // 安全的方式复制字节
    
  4. 使用 offsetof 和容器抽象
    对于结构体成员偏移,使用offsetof宏而非手动计算:

    struct S { int a; char b; };
    size_t offset = offsetof(S, b); // 安全获取成员偏移量
    
  5. 启用编译器的严格检查
    开启编译选项(如-Wall -Wextra -Werror)以捕获可疑操作,并利用静态分析工具(如Clang-Tidy)进行检查。


需要完全避免的操作

  1. 不同内存块的指针相减

    int a[5], b[5];
    ptrdiff_t diff = &b[0] - &a[0]; // 未定义行为!
    
  2. 整数与指针的随意转换

    int *p = ...;
    intptr_t i = (intptr_t)p;
    i += 100; // 危险:可能溢出或破坏对齐
    p = (int*)i; // 可能导致崩溃
    
  3. 通过指针算术绕过访问权限
    例如,通过指针修改只读内存或私有数据结构的内部状态。


总结

指针算术是一把双刃剑:它提供了底层内存操作的灵活性,但也极易引入难以调试的错误。现代编程实践中,应优先使用更安全的抽象(如容器、智能指针、迭代器),仅在必要时谨慎使用原始指针,并严格遵循语言规范。记住:“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::vectorstd::arraystd::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. 避免越界的代码规范

  1. 明确数组长度:传递数组时,始终附带长度参数:
    void safe_function(int *arr, size_t length) {
        for (size_t i = 0; i < length; i++) {
            // 安全访问
        }
    }
    
  2. 避免裸指针和手动算术:使用迭代器或范围循环:
    for (auto& elem : vec) { /* 无需索引 */ }
    
  3. 优先使用安全库函数
    • snprintf 替代 sprintf
    • fgets 替代 gets

总结

数组越界是程序稳定性和安全性的重大威胁。通过以下措施可显著降低风险:

  • 严格检查索引范围
  • 使用安全的语言特性和库函数
  • 借助工具进行静态和动态检测
  • 编写防御性代码,验证所有外部输入

始终牢记:“信任但要验证”(Trust but Verify)。即使代码逻辑看似正确,也要通过测试和工具确保无越界访问。

2 不要使用多维数组,用合适的容器替代它。

在C/C++中,原生多维数组(如int arr[3][4])虽然语法简单,但存在内存管理复杂、易出错、灵活性差等问题。现代编程实践中,应优先使用更安全、更易维护的容器替代它们。以下是详细的替代方案和最佳实践:


一、为什么不推荐原生多维数组?

  1. 静态大小限制
    原生多维数组的大小必须在编译期确定,无法动态调整:
    int arr[10][20]; // 只能固定为10x20
    
  2. 内存不安全
    越界访问无运行时检查,可能导致未定义行为:
    arr[10][25] = 42; // 越界但编译器不报错
    
  3. 传递和返回困难
    数组作为函数参数时会退化为指针,丢失维度信息:
    void func(int arr[][20]); // 必须硬编码第二维大小
    
  4. 动态分配繁琐
    手动分配/释放多维数组容易出错:
    int **arr = new int*[rows];
    for (int i=0; i<rows; i++) 
        arr[i] = new int[cols]; // 需要多层分配和释放
    

二、替代方案:使用现代容器

方案1:嵌套标准容器(推荐)

使用std::vectorstd::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替代0NULL是一种现代且安全的实践。以下是详细的解释和最佳实践:


一、为什么用 nullptr 替代 0NULL

1. 类型安全(Type Safety)
  • 0NULL 的缺陷
    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 明确表示空指针,而0NULL可能被误解为整数或魔法值(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. 与旧代码的兼容性

在混合使用旧代码时,逐步替换NULL0

// 旧代码
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
    

五、总结

场景nullptr0/NULL
指针初始化/赋值✅ 明确安全❌ 潜在类型混淆
函数重载✅ 避免歧义❌ 可能调用错误重载
模板编程✅ 正确推导类型❌ 推断失败或错误
代码可读性✅ 清晰表达意图❌ 可能被误解为整数

核心建议

  • 全面替换:在新代码中始终使用nullptr,旧代码逐步替换。
  • 静态检查:启用编译器警告(如-Wzero-as-null-pointer-constant)捕获遗留的0NULL
  • 文档说明:在团队规范中明确要求使用nullptr

通过使用nullptr,可以显著提高代码的类型安全性、可读性和健壮性,避免许多由空指针引起的潜在问题。

4 与内置的C风格数组相比,优先选用容器(比如vector、array和valarray)。

一、为什么避免C风格数组?

  1. 手动内存管理风险
    C风格数组的声明、分配和释放需手动控制,易引发内存泄漏或越界访问:

    int *arr = new int[10];
    delete[] arr;  // 必须匹配 new[]
    // 忘记释放或重复释放会导致崩溃
    
  2. 无边界检查
    直接通过下标访问时,越界操作不会触发错误,导致未定义行为:

    int arr[5];
    arr[5] = 42;  // 越界写入,但编译器可能不报错
    
  3. 传递和返回困难
    数组作为函数参数时会退化为指针,丢失长度信息:

    void process(int arr[]);  // 无法直接获取数组大小
    
  4. 缺乏迭代器和算法支持
    无法直接使用标准库算法(如std::sortstd::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(数值计算数组)
  • 数学优化:支持向量化操作(如+sinsum):
    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风格数组的场景

  1. 与C语言API交互
    当调用C库函数时需要传递原始指针:

    std::vector<int> vec(100);
    c_api_function(vec.data(), vec.size());
    
  2. 极端性能优化
    在嵌入式系统或高频交易等场景,需手动控制内存布局(但应先验证容器性能是否不足)。


五、性能对比与优化

场景C风格数组标准容器
内存分配手动控制,易出错自动管理(vector可预分配)
访问速度相同(均为连续内存)相同
功能扩展需手动实现内置排序、查找、迭代器等
代码安全性低(无边界检查)高(可选.at()检查)
  • 预分配内存减少开销
    std::vector<int> vec;
    vec.reserve(1000);  // 预分配空间,避免多次扩容
    

六、总结

需求推荐容器优势
动态大小数组std::vector自动扩容、内存连续
固定大小数组std::array栈分配、无额外开销
数值计算std::valarray向量化操作、数学函数支持
兼容C APIC风格数组 + vector通过.data()传递指针

核心原则

  • 默认选择容器:除非有明确需求(如兼容C API),否则优先使用标准库容器。
  • 代码即文档:容器的使用明确表达了数据结构的意图(如动态性、固定大小)。
  • 团队协作:统一使用容器可减少错误并提升代码可读性。

通过这一实践,可以显著减少内存错误、提升开发效率,并为代码的长期维护奠定坚实基础。

5 优先选用string,而不是以0结尾的char数组。

一、为什么不推荐C风格字符串?

  1. 手动内存管理风险
    C风格字符串需要手动分配/释放内存,易导致内存泄漏或重复释放:

    char *str = new char[100];  // 需要手动管理
    delete[] str;  // 忘记释放则泄漏
    
  2. 缓冲区溢出漏洞
    无内置长度检查,操作不当易引发溢出:

    char buf[10];
    strcpy(buf, "This is a very long string!");  // 溢出覆盖相邻内存
    
  3. 功能贫乏
    基础操作依赖C库函数(如strlenstrcat),代码冗长且易错:

    char s1[20] = "Hello";
    char s2[] = "World";
    strcat(s1, s2);  // 需确保s1足够大
    
  4. 类型不安全
    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::stringchar*类型不兼容,避免误用:
    void log(const std::string &msg);
    log("error"); // 隐式构造std::string,类型安全
    
  • 多语言支持:与std::wstringstd::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风格字符串的场景

  1. 与C语言库交互
    如调用fopenexecvp等需要const char*参数的函数。
  2. 极端性能优化
    在实时系统中需要绝对可控的内存操作(需严格验证必要性)。

五、总结

场景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";

四、注意事项

  1. 兼容性
    原始字符串字面值从C++11开始支持,确保编译器开启C++11或更高标准。

  2. 结束符选择
    自定义分隔符应为唯一标识符(如_MY_DELIM_),避免与字符串内容重复。

  3. 性能
    原始字符串与普通字符串在运行时无性能差异,仅为语法糖。


五、对比总结

场景普通字符串原始字符串
文件路径"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_ptrshared_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_; }
    

四、对比裸指针直接管理

场景裸指针管理句柄类封装
资源释放需手动deletefclose析构函数自动释放
所有权传递易出错(需文档约定)通过移动语义或智能指针明确所有权
异常安全资源可能泄漏异常发生时仍能正确释放资源
线程安全需手动同步shared_ptr引用计数原子操作提供基本安全

五、高级用法

1. 自定义删除器

通过unique_ptrshared_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::variantstd::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*(允许nullptrvoid save(const char* filename = nullptr)
传递动态对象的所有权std::unique_ptr<T>void take(std::unique_ptr<Resource>)
需要与C接口交互T*void c_api_handler(void* user_data)

四、最佳实践

  1. 默认优先使用引用

    • 函数参数若必须存在且无需处理空值,用引用(尤其是const引用)。
    • 明确表达“此参数不可为空”,减少空值检查负担。
  2. 仅在必要时使用指针

    • 需要处理“对象缺失”或可选参数时,使用指针并检查nullptr
    • 动态资源管理优先用智能指针(如unique_ptrshared_ptr),而非原始指针。
  3. 避免混淆语义

    • 不要用非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检查和智能指针。

通过合理选择引用或指针,可以提升代码的健壮性、可读性和维护性,同时精准表达设计意图。

相关文章:

  • Grafana Loki
  • 深度学习实战车辆目标跟踪与计数
  • 全栈网络安全|渗透测试-1
  • 网络初级复习作业
  • react+ts+eslint+prettier 配置教程
  • 【AI】AI开源IDE:CLine源码分析报告
  • 54-WLAN 无线局域网配置方案-三层
  • 云曦25开学考复现
  • React-异步队列执行方法useSyncQueue
  • MySql的in和join对比谁更高效
  • JVM_八股场景题
  • 【含文档+PPT+源码】Python爬虫人口老龄化大数据分析平台的设计与实现
  • Kubernetes开发环境minikube | 开发部署apache tomcat web集群应用
  • VUE2脚手架的下载与安装
  • 【含文档+PPT+源码】基于Python爬虫二手房价格预测与可视化系统的设计与实现
  • php虚拟站点提示No input file specified时的问题及权限处理方法
  • Web3 的隐私保护机制:如何保障个人数据安全
  • 【今日EDA行业分析】2025年3月8日
  • http协议的三次握手机制
  • Spring源码探析(一):SpringApplication构造函数核心逻辑
  • 从良渚到三星堆:一江水串起了5000年的文明对话
  • 新任重庆市垫江县委副书记刘振已任县政府党组书记
  • 在美国,为什么夏季出生的孩子更容易得流感?
  • 首次带人形机器人走科技节红毯,傅利叶顾捷:没太多包袱,很多事都能从零开始
  • 《日出》华丽的悲凉,何赛飞和赵文瑄演绎出来了
  • 雷军内部演讲回应质疑:在不服输、打不倒方面,没人比我们更有耐心