avatar

目录
learncpp_12_pointer_and_reference

12.5

C++ 常量左值引用传递总结

常量左值引用(const lvalue reference)可以绑定到可修改左值、不可修改左值和右值,比非常量引用更灵活 。它既避免了参数拷贝的开销,又保证函数不会修改被引用的值 。learncpp

核心优势

  • 通用性强:可以接受任何类型的实参(左值、右值、字面量)learncpp

  • 性能优化:避免拷贝参数,特别是对于大型对象learncpp

  • 安全性:保证函数内部无法修改参数值learncpp

参数传递规则

基本类型采用值传递,因为拷贝成本低;类类型采用常量引用传递,因为拷贝可能很昂贵 。判断标准:如果对象大小 ≤ 2个指针大小且无额外初始化成本,则视为”廉价拷贝” 。learncpp

特殊情况learncpp

  • 值传递:枚举类型、std::string_viewstd::span、迭代器

  • 引用传递:需要修改的参数、不可拷贝类型(如std::ostream)、智能指针、有虚函数的类型

字符串参数最佳实践

优先使用 std::string_view 而非 const std::string&std::string_view 参数能高效处理所有三种字符串类型(std::stringstd::string_view、C风格字符串),而 const std::string& 只对 std::string 参数高效,传递其他类型会导致昂贵的临时对象创建 。learncpp

注意事项

使用引用传递时,确保实参类型与引用类型匹配,否则会发生类型转换并创建临时对象,失去引用传递的性能优势 。learncpp

  1. https://www.learncpp.com/cpp-tutorial/pass-by-const-lvalue-reference/

12.6

常量左值引用传参 (Pass by Const Lvalue reference) 复盘总结

1. 核心定义与功能

  • 绑定范围:常量左值引用可绑定至可变左值、不可变左值及右值(如字面量)。
  • 核心优势:在避免对象拷贝的同时,保证函数内部无法修改实参值。
  • 右值支持意义:允许字面量或临时对象通过引用方式传递,增强了函数接口的通用性。

2. 类型转换与临时对象

  • 转换机制:实参与形参类型不匹配但可转换时,编译器会创建临时对象供引用绑定。
  • 性能隐患:引用传参中的隐式转换会导致预料之外的临时对象构造,产生额外的内存与计算开销。
  • 类型匹配原则:使用引用传递时应确保实参类型与形参完全一致,以维持性能优势。

3. 传参方式的选择准则

  • 按值传递 (Pass by value):适用于基础类型(int, double等)、枚举类型、视图类(std::string_view, std::span)及迭代器。
  • 按常量引用传递 (Pass by const reference):适用于大多数类类型(Class types)及拷贝开销大的对象。
  • 按引用传递 (Pass by reference):仅用于需要修改实参值、处理不可拷贝类型(如std::ostream)或需避免对象切片(Object slicing)的情况。

4. 性能考量与底层开销

  • 拷贝成本:取决于对象大小(内存占用)及初始化设置成本(如分配动态内存、打开文件)。
  • 访问成本:引用参数需二次寻址(先查引用地址,再访目标内存),值参数可直接访问寄存器或内存。
  • 编译器优化:按值传递由于不存在别名风险(Aliasing),更利于编译器进行激进优化。
  • “廉价拷贝”标准:对象大小不超过两个内存地址长度(2 words)且无额外初始化成本。

5. 字符串传参专项建议

  • 首选标准:优先通过值传递 std::string_view,而非常量引用传递 std::string
  • std::string_view 优势:能以极低成本处理 std::string、C 风格字符串及子串,无需触发昂贵的转换构造。
  • const std::string& 适用场景:需调用旧版 C++ 接口、必须处理以空字符结尾的字符串,或需调用仅接受 std::string 的第三方库。

12.7

基础操作符:取地址与解引用

  • 取地址操作符 (&):返回其操作数在内存中的地址(通常为十六进制)。
    • 示例:std::cout << &x;
    • 注意:对于多字节对象,返回其第一个字节的地址。
  • 解引用操作符 (*):返回指定内存地址处的值,且该值为左值。
    • 示例:std::cout << *(&x); // 输出变量 x 的值
    • 逻辑关系:&* 互为逆运算。

指针的概念与定义

  • 指针定义:指针是一个存储内存地址的变量。
  • 指针类型:在基本类型后加 * 表示该类型的指针。
    • 示例:int* ptr; // 指向 int 类型的指针
  • 声明规则* 符号在声明多个变量时必须逐一重复。
    • 示例:int* ptr1, * ptr2; // 正确定义两个指针
  • 最佳实践:声明时将 * 紧贴类型名(如 int* ptr),且避免在同一行定义多个变量。

指针的初始化与赋值

  • 野指针 (Wild Pointer):未初始化的指针包含随机垃圾地址,解引用会导致未定义行为。
  • 初始化:应始终初始化指针,通常赋予某个变量的地址。
    • 示例:int* ptr{ &x };
  • 指针赋值的两种方式
    • 改变指向:ptr = &y;(修改指针存储的地址)
    • 修改值:*ptr = 6;(通过指针修改目标对象的值)

指针与 Lvalue 引用

  • 相同点:都提供了间接访问另一个对象的能力。
  • 差异点
    • 显式性:指针需要显式使用 &*;引用是隐式的。
    • 对象性质:指针是对象(占用内存),引用不是对象。
    • 重新绑定:指针可以随时改变指向;引用一旦绑定不可更改。
    • 有效性:引用必须初始化且不能为空;指针可以不初始化(不建议)或指向空。

内存属性与安全性

  • 指针大小:取决于执行架构,与指向的数据类型无关。
    • 32位架构:指针占用 4 字节。
    • 64位架构:指针占用 8 字节。
  • 悬挂指针 (Dangling Pointer):指向已被销毁对象的指针。
    • 后果:解引用悬挂指针会导致未定义行为(程序崩溃或数据损坏)。
    • 示例:在内部作用域内将变量地址赋给外部指针,作用域结束后该指针变为悬挂状态。

12.8

你好!这篇关于 空指针 (Null pointers) 的课件确实比较详尽。为了让你更高效地掌握,我将其核心知识点提炼如下。

简单来说,空指针就是一个“不指向任何东西”的特殊状态。

1. 什么是空指针 (Null Pointer)?

普通指针存储的是内存地址,而空指针持有一个特殊的“空值”,表示它目前没有指向任何对象

2. 如何创建空指针?

现代 C++ 有两种推荐方式,最佳实践是使用 nullptr 关键字

  • 方法 A:值初始化 (Value Initialization)
    cpp
    1
    int* ptr {}; // ptr 现在是空指针,不持有任何地址
  • 方法 B:使用 nullptr 关键字 (推荐)
    就像 truefalse 是布尔字面量,nullptr 是专门表示空指针的字面量。
    cpp
    1
    2
    3
    4
    5
    int* ptr { nullptr }; // 初始化为空

    int value { 5 };
    int* ptr2 { &value };
    ptr2 = nullptr; // 也可以通过赋值,让原本有效的指针变为空指针

3. 致命禁忌:解引用空指针

切记: 永远不要对空指针进行解引用 (*ptr) 操作。

  • 后果: 导致 未定义行为 (Undefined Behavior),通常会直接导致程序崩溃。
  • 原因: 空指针不指向任何地址,去一个不存在的地址取值是不合逻辑的。
cpp
1
2
int* ptr { nullptr };
// std::cout << *ptr; // 错误!程序会崩溃。

4. 如何安全地检查指针?

在使用指针前,必须检查它是否为空。指针可以隐式转换为布尔值:

  • 空指针 转为 false
  • 非空指针 转为 true
cpp
1
2
3
4
5
6
7
if (ptr) {
// 指针非空,可以安全解引用
std::cout << *ptr;
} else {
// 指针为空,处理错误逻辑
std::cout << "ptr is null";
}

5. 悬空指针 (Dangling Pointers) Vs 空指针

这是一个非常重要的概念区别:

  • 空指针 (Null Pointer): 明确知道它不指向任何东西 (值为 nullptr)。
  • 悬空指针 (Dangling Pointer): 指向的内存已经被释放了,但指针里还留着旧地址。

警告: if (ptr) 只能检测指针是否为 nullptr无法检测指针是否悬空

最佳策略:

  1. 如果一个指针不指向有效对象,必须将其设为 nullptr
  2. 当指针指向的对象被销毁后,程序员必须手动将该指针置为 nullptr(C++ 不会自动帮你做)。
    这样,你只需要检查 if (ptr) 就能确信指针是安全的。

6. 历史遗留问题:0 和 NULL

在老旧代码中,你可能会看到用 0NULL 来表示空指针。

  • 建议: 在现代 C++ 中避免使用它们,一律使用 nullptrnullptr 类型更安全,语义更清晰。

7. 终极建议:优先使用引用 (References)

引用 (Reference) 必须在创建时初始化,且不能为“空”,也不能重置指向。这规避了空指针和大部分悬空指针的问题。

总结: 除非你需要指针的特殊能力(如改变指向、指向空值),否则优先使用引用


一句话总结全篇:
nullptr 初始化指针;解引用前用 if (ptr) 检查;对象销毁后把指针置空;能用引用就别用指针。

12.9

这一课的核心在于区分指针本身(地址)指针指向的数据(值)谁是不可变的。

我们可以通过一个“星星(*)分界法”来精炼记忆:

  • const* 左边:指向的内容是常量(指向常量的指针)。
  • const* 右边:指针本身是常量(常量指针)。

1. 指向常量的指针 (Pointer to const)

写法: const int* ptrint const* ptr

  • 特点: 你不能通过这个指针去修改它指向的值。
  • 逻辑: 它认为自己指向的是“常量”,即使原变量不是常量,通过它也改不了。
  • 能做什么: 可以改变 ptr 指向的地址(换个地方指)。

举例:

cpp
1
2
3
4
5
6
int x = 5;
const int y = 10;
const int* ptr = &x; // 允许:指向一个普通变量,但ptr视其为常量

*ptr = 7; // 报错!不能通过ptr修改值
ptr = &y; // 允许!ptr可以改指向y的地址


2. 常量指针 (Const pointer)

写法: int* const ptr

  • 特点: 指针持有的地址一旦初始化就不能再变。
  • 注意: 必须在定义时初始化。
  • 能做什么: 虽然地址不能变,但如果指向的是普通变量,你可以修改那个变量的值。

举例:

cpp
1
2
3
4
5
6
int x = 5;
int y = 6;
int* const ptr = &x; // ptr从此锁死在x的地址上

ptr = &y; // 报错!指针本身是const,不能换地址
*ptr = 7; // 允许!x的值变成了7


3. 指向常量的常量指针 (Const Pointer to const)

写法: const int* const ptr

  • 特点: 全面锁死。地址不能改,指向的值也不能通过它改。

举例:

cpp
1
2
3
4
5
int x = 5;
const int* const ptr = &x;

ptr = &y; // 报错!
*ptr = 7; // 报错!


核心知识点补充

为什么 int* ptr = &const_var; 会报错?

如果你有一个 const int x = 5;,你不能用普通的 int* ptr 去指向它。

  • 原因: x 本身承诺不改变,但 int* ptr 拥有修改权限。C++ 为了防止你“绕过限制”去修改 x,直接在编译阶段禁止这种赋值。

指向常量的指针 Vs 引用

const int* ptrconst int& ref 非常相似:它们都允许指向(或引用)一个非 const 的变量,并将其视为 const


总结表(背诵口诀)

声明方式 谁是 Const? 改变地址 (ptr = &y) 改变值 (*ptr = 7)
int* ptr OK OK
const int* ptr 指向的值 OK ❌ 报错
int* const ptr 指针本身 ❌ 报错 OK
const int* const ptr 都是 ❌ 报错 ❌ 报错

一句话总结: const* 左边保护“值”,const* 右边保护“地址”。

12.10

这一课的核心是讲解 地址传递 (Pass by Address)。为了让你快速掌握,我将其精炼为四个维度:是什么、怎么用、安全隐患、以及与引用的对比


1. 什么是地址传递?

核心定义:函数参数不再是对象本身,而是对象的指针(地址)

  • 本质:函数内部创建了一个指针副本,指向外部的同一个变量。
  • 优点:和引用传递一样,!因为它只拷贝 4 或 8 字节的地址,不拷贝大对象。

2. 语法实现

对比三种传递方式:

cpp
1
2
3
4
5
6
7
8
9
10
void byValue(std::string s);      // 拷贝一份,慢
void byRef(const std::string& s); // 直接用原件,快
void byAddr(const std::string* s);// 拿到原件的地址,快

int main() {
std::string str = "Hello";
byValue(str);
byRef(str);
byAddr(&str); // 注意:必须用 & 取地址
}

3. 三大关键知识点

① 修改原值

如果参数是 int* ptr(非 const 指针),函数内部可以通过解引用修改外部变量:

cpp
1
2
3
void change(int* ptr) {
*ptr = 100; // 修改了外部 main 函数里的变量
}

② Const 的正确用法 (最佳实践)

  • 指针指向的内容不可变const int* ptr(最常用,保护数据不被意外修改)。
  • 指针本身不可变int* const ptr(几乎不用,因为参数本来就是副本)。
  • 准则:除非需要修改原值,否则一律用 const T*

③ 空指针检查 (Null Checking) —— 最重要!

与引用不同,指针可以是空的 (nullptr)。直接解引用空指针会导致程序崩溃。

  • 防御式编程:在函数开始处检查指针是否有效。
    cpp
    1
    2
    3
    4
    void print(int* ptr) {
    if (!ptr) return; // “早返回”模式,防止崩溃
    std::cout << *ptr;
    }

4. 地址传递 Vs 引用传递 (该选哪个?)

特性 引用传递 (Pass by Reference) 地址传递 (Pass by Address)
安全性 高(引用不能为空) 低(必须检查 nullptr
语法 简洁(像用普通变量) 繁琐(满屏 *&
灵活性 较低 较高(可以重新指向别处,或表示“无值”)
字面量 const & 支持传递 5, “Hi” 等 不支持直接取地址 &5 (报错)

5. 总结建议:黄金法则

  1. 首选引用传递:它更安全、语法更干净。
  2. 次选地址传递:只有当你明确需要处理“空值”(即参数可以不传,传入 nullptr)或者为了兼容 C 语言接口时,才使用它。

一句话口诀:

能用引用就用引用,必须用地址才用地址。 (Pass by reference when you can, pass by address when you must.)

12.11

你好!这篇课件是关于“按地址传递”(Pass by address)的进阶内容。为了让你高效掌握,我将复杂的表述精炼为四个核心知识点,并配上直观的对比。


1. 实现“可选参数” (Optional Arguments)

指针与引用最大的区别是:指针可以为 nullptr。利用这个特性,我们可以让函数参数变成“可选的”。

  • 做法:给指针参数一个默认值 nullptr
  • 举例
    cpp
    1
    2
    3
    4
    5
    6
    7
    void printID(const int* id = nullptr) {
    if (id) std::cout << "ID: " << *id;
    else std::cout << "未知用户";
    }
    // 调用:
    printID(&myID); // 传地址
    printID(); // 不传,默认走 nullptr 分支
  • 老师提醒:虽然好用,但函数重载通常是更好的方案。因为重载更安全(不用担心空指针),且支持字面量(如 printID(10))。

2. 修改指针的指向 (Changing what a pointer points at)

这是一个大坑!很多人以为传了指针就能修改指针本身。

  • 现象:如果你直接把指针传进函数,系统会复制一份地址给形参。你在函数里改形参的指向,函数外的原指针完全没变
  • 需求:如果你想在函数内部,让函数外的那个指针“改行”指向别处,你需要按引用传递指针
  • 语法int*& refptr(从右往左读:是一个引用,引用的是一个 int* 指针)。
  • 对比举例
    cpp
    1
    2
    3
    4
    5
    // 错误改法:ptr2 只是原指针的副本
    void nullify(int* ptr2) { ptr2 = nullptr; }

    // 正确改法:refptr 是原指针的别名
    void nullify(int*& refptr) { refptr = nullptr; }

3. 为什么必须用 nullptr?(0 vs NULL vs nullptr)

这是为了解决函数重载的歧义

  • 问题0 是整数,NULL 在某些编译器下也是整数 0
  • 冲突:如果你有两个重载函数 func(int)func(int*)
    • 调用 func(0):会进 int 版本。
    • 调用 func(NULL):可能进 int 版本,也可能报错。
    • 调用 func(nullptr)永远只会int* 指针版本。
  • 知识点nullptr 有自己的类型 std::nullptr_t,它只能表示空指针。

4. 哲学总结:万物皆“按值传递”

课件最后提到了一个深刻的底层逻辑:

  • 按地址传递:其实是把“地址”这个数值按值传递(Copy一份地址)。
  • 按引用传递:底层通常也是编译器通过指针实现的。
  • 结论:之所以起这么多名字,是为了区分我们的意图——你是想改那个“值”本身,还是想通过“地址”去改它背后的“对象”。

💡 老师给你的复习口诀:

  1. 想传可选参数? 用指针并给默认 nullptr
  2. 想在函数里改指针指向? 记得写成 int*&
  3. 永远不要用 0NULL 代替空指针,始终用 nullptr
  4. 语法记不住? 记住 int*& 是合法的,而 int&* (指针指向引用) 是非法的(报错了反过来写就行)。

有没有哪一部分需要我再深入举例的?

12.12

你好!这一节的内容非常核心,同时也容易踩坑。为了让你能够轻松掌握,我把原版课件的内容提炼为核心动机语法与机制致命陷阱(必看)常见误区以及指针版本这几个模块来讲解。


1. 核心动机:为什么要“按引用返回”?

一句话总结:为了省钱(性能)。

  • 按值返回 (Return by value):函数返回时,会把结果复制一份给调用者。如果是 int 这种小类型还好,但如果是 std::string 或大对象,复制非常昂贵。
  • 按引用返回 (Return by reference):返回的是对象本身的“别名”,不发生复制,速度极快。

写法: 在返回类型后加 &(引用)或 const &(常量引用)。

cpp
1
2
std::string&       func1(); // 返回引用,可读可写
const std::string& func2(); // 返回常量引用,只读(最常用)

2. 致命陷阱:千万不要返回局部变量的引用!

这是本节课最重要的知识点。

规则: 按引用返回的对象,必须在函数结束后依然存活

  • 错误示范(Dangling Reference 悬垂引用):
    你试图返回一个在函数内部创建的局部变量。函数一结束,局部变量被销毁(内存释放),你返回的引用指向了一块“废弃内存”。

    cpp
    1
    2
    3
    4
    5
    6
    7
    8
    9
    const std::string& badFunction() {
    std::string localName = "Test";
    return localName; // ❌ 危险!函数结束 localName 就销毁了
    }

    int main() {
    // undefined behavior (未定义行为),程序可能崩溃或乱码
    std::cout << badFunction();
    }
  • 关于“生命周期延长”的误解:
    虽然 C++ 有时会延长临时对象的生命周期,但这跨越不了函数边界。一旦出了函数,临时对象必死无疑,引用也就失效了。


3. 那么,哪些情况是安全的?

只有当返回对象的生命周期长于函数本身时,按引用返回才是安全的。主要有以下两种情况:

情况 A:返回传入的引用参数

如果你把一个对象按引用传进函数,函数再把它按引用传出来,这是安全的。因为该对象在调用者那边本来就存在。

cpp
1
2
3
4
5
6
7
8
9
10
11
// 比较两个字符串,返回较小的那个(由调用者传入,肯定存在)
const std::string& minStr(const std::string& a, const std::string& b) {
return (a < b) ? a : b;
}

int main() {
std::string s1 = "Apple";
std::string s2 = "Banana";
// s1 和 s2 在 main 中都活着,所以安全
std::cout << minStr(s1, s2);
}

情况 B:返回静态变量 (Static Variable)

静态变量在程序结束前一直存在,所以返回它的引用是技术上安全的,但有“副作用”(见下文)。

cpp
1
2
3
4
const int& getStatic() {
static int x = 10; // static 变量生命周期贯穿整个程序
return x; // ✅ 安全,x 不会被销毁
}

4. 常见误区与细节

误区 1:滥用非 const 静态变量

虽然返回静态变量安全,但如果返回的是非 const 引用,会导致所有调用者共享同一个状态,容易产生莫名其妙的 Bug。

  • 建议: 除非为了特定的设计(如单例模式),否则尽量避免返回非 const 的局部静态变量。

误区 2:接收返回值时发生了“意外复制”

即使函数返回的是引用,如果你用普通变量去接收,还是会发生复制。

cpp
1
2
3
4
5
6
const int& getRef(); // 假设返回一个引用

int main() {
int a = getRef(); // ⚠️ 发生复制!a 只是值的拷贝
const int& b = getRef(); // ✅ 没有复制,b 是真正的引用
}

特性:可以通过返回值修改对象

如果函数返回的是非 const 引用,你可以把它放在等号左边赋值!

cpp
1
2
3
4
5
6
7
8
9
int& max(int& x, int& y) {
return (x > y) ? x : y;
}

int main() {
int a = 5, b = 6;
max(a, b) = 10; // max 返回了 b 的引用,这行代码相当于 b = 10;
// 此时 a=5, b=10
}

5. 按地址返回 (Return by Address)

这与按引用返回几乎一样,只是返回的是指针(Type*)。

  • 优点: 可以返回 nullptr(空指针),表示“没找到”或“无对象”。
  • 缺点: 调用者必须记得检查是否为 nullptr,否则程序会崩溃。
  • 规则: 同样不能返回局部变量的地址。

最佳实践:
除非你需要返回“空”的概念(比如查找失败返回 nullptr),否则优先使用按引用返回,因为它更安全、更符合 C++ 习惯。


总结(精炼版)

  1. 为了性能:对象较大时,优先考虑 const & 返回。
  2. 铁律绝对不要返回函数内部局部变量的引用(或指针)。
  3. 安全场景:返回通过参数传进来的引用,或者类的成员变量(在类的方法中)。
  4. 接收方式:想避免复制,接收变量也要声明为引用类型(如 const Type& obj = func();)。
  5. 引用 vs 地址:默认用引用;只有需要表示“无/空”时,才用指针。

12.13

这是一份关于 C++ 中 In parameters(输入参数)和 Out parameters(输出参数)的精炼讲解。

通常情况下,函数通过 Return values(返回值)向调用者(Caller)传递数据。但在某些特定场景下,我们需要通过参数本身来“返回”数据。

以下是核心概念的详细解析:

1. In Parameters (输入参数)

这是最常见的参数类型。主要用于将数据传入函数供其使用,函数不会修改这些参数的值 。learncpp

  • 传递方式:通常按 Value(值传递)或 Const Reference(常量引用传递)。

  • 特点:调用者传入数据,函数读取数据。

cpp

void print(int x) { // x 是 In parameter std::cout << x; }

2. Out Parameters (输出参数)

当一个函数需要返回多个值,或者无法使用返回值时(例如返回值已被占用),可以使用 Out parameters

  • 定义:主要用于将信息从函数内部“带回”给调用者 。learncpp

  • 传递方式:必须按 Non-const Reference(非 const 引用)或指针传递。

  • 工作原理:函数通过引用直接修改调用者作用域内的变量,从而实现数据的“返回”。

示例:同时获取正弦和余弦值

cpp

void getSinCos(double degrees, double& sinOut, double& cosOut) { // sinOut 和 cosOut 是 Out parameters // 函数内部直接修改这两个变量,也就是修改了 main 函数中的 sin 和 cos double radians = degrees * 3.14159 / 180.0; sinOut = std::sin(radians); cosOut = std::cos(radians); } int main() { double sin = 0.0; double cos = 0.0; // 调用者必须先定义变量来接收结果 getSinCos(30.0, sin, cos); // 此时 sin 和 cos 的值已经被 getSinCos 修改了 return 0; }

3. Out Parameters 的缺点

虽然功能强大,但 Out parameters 这种写法并不自然,存在以下缺陷 :learncpp

  1. 语法繁琐:调用者必须先定义(并初始化)变量,哪怕只是为了接收一个值。无法像返回值那样直接用于临时表达式(如 std::cout << func())。

  2. 语义不明确

    • 代码 getSinCos(degrees, sin, cos) 看起来像是在传入三个值。

    • 如果读者不看函数声明,很难分辨哪些是输入,哪些会被修改。

    • 注: 虽然使用 Pass by Address(传地址,如 func(&x))能显式看出 x 可能被修改,但会引入空指针检查的复杂性,因此不推荐仅为了可读性而改用指针。

4. In-Out Parameters (输入输出参数)

这是一种特殊情况。参数既包含输入数据供函数使用,又在函数结束时承载修改后的结果返回 。learncpp

  • 场景:你需要修改一个现有的对象,而不是创建一个新的。

  • 传递方式:同样使用 Non-const Reference

cpp

void modifyFoo(Foo& inout) { // 读取 inout 的当前状态,并进行修改 }

5. Best Practice (最佳实践)

根据现代 C++ 的规范:

  1. 尽量避免使用 Out parameters 。如果需要返回一个值,直接使用返回值。如果需要返回多个值,考虑使用 structstd::pair/std::tuplelearncpp

  2. 如果必须使用(例如需要原地修改对象以提升性能):

    • 对于 Non-optional(非可选)的输出参数,优先使用 Pass by Reference(引用传递),而不是指针。

    • 可以通过命名约定(如后缀 _out)来提示这是一个输出参数。

  3. 性能考量:只有当对象 Copying(拷贝)的代价极其昂贵时,才考虑使用 Out parameters 来避免返回值的拷贝开销 。learncpp

  1. https://www.learncpp.com/cpp-tutorial/in-and-out-parameters/

12.14

C++ 类型推导复盘总结(指针、引用与 const)

1. 引用推导的核心规则

  • 引用剥离原则:使用 auto 推导时,初始值设定项中的引用限定符(&)会被默认丢弃。
    • auto ref { getRef() }; // 结果为 std::string 而非 std::string&
  • 显式重加引用:若需推导结果为引用,必须显式使用 auto&
    • auto& ref { getRef() }; // 结果为 std::string&
  • 推导差异成因:引用在语义上代表其指向的对象,而指针代表地址本身,故推导时保留指针而丢弃引用。

2. 顶层 Const 与 底层 Const 的区分

  • 顶层 Const (Top-level):限定对象本身不可变。
    • const int x;int* const ptr;(指针本身是常量)
  • 底层 Const (Low-level):限定所指向或引用的对象不可变。
    • const int& ref;const int* ptr;(指向的内容是常量)
  • 复合情况:指针可以同时拥有顶层和底层 const。
    • const int* const ptr; // 左侧为底层,右侧为顶层

3. Const 限定符的推导行为

  • 顶层 Const 丢弃规则auto 会自动丢弃初始值中的顶层 const,但会保留底层 const。
  • 引用转 Const 逻辑:推导 const T& 时,先丢弃引用得到 const T(此时变为顶层 const),随后顶层 const 也被丢弃,最终剩 T
  • 显式重加 Const:若需保留常量属性,建议显式添加 const auto
    • const auto& ref { getConstRef() }; // 显式声明为常引用,增强可读性

4. 指针推导与 auto* 的使用

  • 指针保留原则auto 推导不会丢弃指针限定符(*)。
    • auto ptr { getPtr() }; // 推导为 T*
  • auto 与 auto* 的区别auto* 强制要求初始值必须是指针类型,否则编译报错。
    • auto* ptr { &x }; // 明确意图,确保推导结果为指针
  • Const 指针推导细则
    • const auto ptrauto const ptr:推导为常指针(T* const)。
    • const auto* ptr:推导为指向常量的指针(const T*)。
    • auto* const ptr:推导为常指针(T* const)。

5. Constexpr 的特殊性

  • 非类型属性constexpr 不是类型系统的一部分,auto 永远不会自动推导为 constexpr
  • 显式声明:必须手动为变量添加 constexpr 关键字。
    • constexpr auto ref { getConstRef() };
  • 常量引用组合:定义指向常量的 constexpr 引用时,需同时使用两者。
    • constexpr const auto& ref { hello };

6. 最佳实践建议

  • 显式原则:即使编译器可以隐式推导,也应显式添加 const&* 以明确编程意图。
  • 安全性建议:优先使用 auto* 代替 auto 来推导指针,利用编译器检查确保类型匹配。
  • 可读性标准:当推导结果为常引用时,坚持写 const auto& 即使 auto& 有时也能保留底层 const。

老师总结(Summary)

  1. 想要引用? 永远显式写 auto&
  2. 想要 Const 引用? 永远显式写 const auto&
    • 虽然 auto&const 对象会自动推导为 const 引用,但写全了能防止你误修改,意图也更清晰。
  3. 想要指针? 推荐写 auto*
    • 这能防止你误把对象当指针,也能更清晰地控制 const(例如 const auto* 就是指向常量的指针)。
  4. auto 默认不仅丢 const (顶层),还丢引用 & 只有底层 const (比如指针指向的内容) 会被保留。

12.15

同学们好,欢迎来到 C++ 课堂。今天我们要讨论一个在编写鲁棒(Robust)程序时经常遇到的难题:如何优雅地处理那些“可能失败”的函数?

在 C++17 之前,我们处理这类问题的方法通常不太理想。今天我们将学习一个现代化的利器:std::optional


1. 现状:传统的“尴尬”解决方案

假设我们要写一个整数除法函数。如果用户输入除数是 0,函数显然无法计算出结果。这时候该返回什么?

方法 A:使用“哨兵值”(Sentinel Values)

这是一种很古老的方法:返回一个不可能出现的特殊值。例如,如果计算失败就返回 0 或者 INT_MIN

cpp
1
2
3
4
double reciprocal(double x) {
if (x == 0.0) return 0.0; // 这里的 0.0 就是“哨兵值”,表示出错了
return 1.0 / x;
}

这种方法的弊端:

  1. 歧义性(半谓词问题): 如果函数合法的计算结果恰好也是这个哨兵值呢?比如 0/5 的结果也是 0。你无法分辨这是“成功算出了 0”还是“出错了”。
  2. 缺乏标准: 有的函数返回 -1 表示错误,有的返回 0,有的返回 nullptr。程序员必须查阅手册才能记住每个函数用哪个值。

方法 B:返回 bool,结果用引用传出

这也是常见的做法,但它强迫你先定义一个变量,而且代码逻辑会变得支离破碎。


2. 新的选择:std::optional

C++17 引入了 std::optional<T>。你可以把它想象成一个“特殊的盒子”

  • 这个盒子里面可能装着一个类型为 T 的值;
  • 这个盒子也可能是空的。

示例:用 std::optional 重写除法

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
#include <optional> // 必须包含这个头文件

// 现在函数返回的是“一个可能包含 int 的盒子”
std::optional<int> doIntDivision(int x, int y) {
if (y == 0)
return std::nullopt; // 返回一个代表“空”的特殊符号

return x / y; // 正常返回结果,编译器会自动把它包装进盒子
}

int main() {
auto result = doIntDivision(10, 2);

if (result) { // 1. 直接判断盒子是否有值(隐式转为 bool)
std::cout << "结果是: " << *result << '\n'; // 2. 像指针一样用 * 提取值
} else {
std::cout << "错误:除数不能为 0\n";
}
}

3. std::optional 的基本操作指南

作为一名开发者,你需要掌握以下四种基本用法:

需求 代码示例 说明
初始化为空 std::optional<int> o{};std::nullopt 明确表示目前没值
检查是否有值 if (o)o.has_value() 推荐直接用 if(o),更简洁
获取值(不安全) *o 如果盒子是空的,这样做会导致未定义行为
获取值(带异常) o.value() 如果为空,会抛出 std::bad_optional_access 异常
获取值(兜底法) o.value_or(42) 如果有值就取值,如果为空就返回 42(非常实用!)

4. 深度思考:它和指针有什么区别?

很多同学会问:“老师,这看起来和返回指针(成功返回地址,失败返回 nullptr)很像啊?”

虽然语法相似,但它们的内存模型完全不同:

  1. 指针是“引用语义”: 指针指向的是内存里的另一个地方。你不能返回一个局部变量的地址,否则函数结束变量销毁,指针就失效了。
  2. std::optional 是“值语义”: 盒子和里面的数据是连在一起的。当你返回 std::optional 时,里面的值会被复制或移动给调用者。所以它可以安全地返回函数内部计算出的结果。

5. 进阶应用:作为可选的函数参数

除了作为返回值,std::optional 也可以作为函数参数,表示这个参数是“可选的”。

cpp
1
2
3
4
5
6
7
8
9
void printID(std::optional<int> id = std::nullopt) {
if (id) std::cout << "ID: " << *id << "\n";
else std::cout << "匿名用户\n";
}

int main() {
printID(123); // 传入 int,自动包装
printID(); // 使用默认值 nullopt
}

老师的特别提醒(性能注意):

虽然这很方便,但 std::optional<T> 会创建 T 的副本。

  • 如果 T 是简单类型(如 int, double),放心使用。
  • 如果 T 是复杂的类型(如大字符串 std::string 或大的 vector),不要直接传 std::optional<T>
    • 建议: 在这种情况下,传统的 const T*(指针)或者函数重载通常是更好的选择,因为它们不会产生多余的内存拷贝。

6. 总结与最佳实践

在现代 C++ 编程中,请遵循以下原则:

  1. 优先使用 std::optional 返回可能失败的结果,而不是使用特定的数字(哨兵值)。它能让你的代码“自解释”——别人一看函数签名就知道这个函数可能拿不到结果。
  2. 获取值之前务必检查。永远不要在没有 if 判断的情况下直接使用 *result,除非你 100% 确定它一定有值。
  3. 善用 value_or()。它可以极大地简化你的逻辑代码,减少 if-else 的层数。

课后思考: 如果一个函数不仅需要表达“失败”,还需要告诉你“为什么失败”(比如是因为权限不足还是因为找不到文件),std::optional 还够用吗?
(提示:可以去了解一下 C++23 的新特性 std::expected,那是我们进阶课程的内容。)

今天的课就讲到这里,下课!


评论