12.5
C++ 常量左值引用传递总结
常量左值引用(const lvalue reference)可以绑定到可修改左值、不可修改左值和右值,比非常量引用更灵活 。它既避免了参数拷贝的开销,又保证函数不会修改被引用的值 。learncpp
核心优势
参数传递规则
基本类型采用值传递,因为拷贝成本低;类类型采用常量引用传递,因为拷贝可能很昂贵 。判断标准:如果对象大小 ≤ 2个指针大小且无额外初始化成本,则视为”廉价拷贝” 。learncpp
特殊情况 :learncpp
值传递:枚举类型、
std::string_view、std::span、迭代器引用传递:需要修改的参数、不可拷贝类型(如
std::ostream)、智能指针、有虚函数的类型
字符串参数最佳实践
优先使用 std::string_view 而非 const std::string& 。std::string_view 参数能高效处理所有三种字符串类型(std::string、std::string_view、C风格字符串),而 const std::string& 只对 std::string 参数高效,传递其他类型会导致昂贵的临时对象创建 。learncpp
注意事项
使用引用传递时,确保实参类型与引用类型匹配,否则会发生类型转换并创建临时对象,失去引用传递的性能优势 。learncpp
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关键字 (推荐)
就像true和false是布尔字面量,nullptr是专门表示空指针的字面量。cpp1
2
3
4
5int* ptr { nullptr }; // 初始化为空
int value { 5 };
int* ptr2 { &value };
ptr2 = nullptr; // 也可以通过赋值,让原本有效的指针变为空指针
3. 致命禁忌:解引用空指针
切记: 永远不要对空指针进行解引用 (*ptr) 操作。
- 后果: 导致 未定义行为 (Undefined Behavior),通常会直接导致程序崩溃。
- 原因: 空指针不指向任何地址,去一个不存在的地址取值是不合逻辑的。
1 | int* ptr { nullptr }; |
4. 如何安全地检查指针?
在使用指针前,必须检查它是否为空。指针可以隐式转换为布尔值:
- 空指针 转为
false - 非空指针 转为
true
1 | if (ptr) { |
5. 悬空指针 (Dangling Pointers) Vs 空指针
这是一个非常重要的概念区别:
- 空指针 (Null Pointer): 明确知道它不指向任何东西 (值为 nullptr)。
- 悬空指针 (Dangling Pointer): 指向的内存已经被释放了,但指针里还留着旧地址。
警告: if (ptr) 只能检测指针是否为 nullptr,无法检测指针是否悬空。
最佳策略:
- 如果一个指针不指向有效对象,必须将其设为
nullptr。 - 当指针指向的对象被销毁后,程序员必须手动将该指针置为
nullptr(C++ 不会自动帮你做)。
这样,你只需要检查if (ptr)就能确信指针是安全的。
6. 历史遗留问题:0 和 NULL
在老旧代码中,你可能会看到用 0 或 NULL 来表示空指针。
- 建议: 在现代 C++ 中避免使用它们,一律使用
nullptr。nullptr类型更安全,语义更清晰。
7. 终极建议:优先使用引用 (References)
引用 (Reference) 必须在创建时初始化,且不能为“空”,也不能重置指向。这规避了空指针和大部分悬空指针的问题。
总结: 除非你需要指针的特殊能力(如改变指向、指向空值),否则优先使用引用。
一句话总结全篇:
用 nullptr 初始化指针;解引用前用 if (ptr) 检查;对象销毁后把指针置空;能用引用就别用指针。
12.9
这一课的核心在于区分指针本身(地址)和指针指向的数据(值)谁是不可变的。
我们可以通过一个“星星(*)分界法”来精炼记忆:
const在*左边:指向的内容是常量(指向常量的指针)。const在*右边:指针本身是常量(常量指针)。
1. 指向常量的指针 (Pointer to const)
写法: const int* ptr 或 int const* ptr
- 特点: 你不能通过这个指针去修改它指向的值。
- 逻辑: 它认为自己指向的是“常量”,即使原变量不是常量,通过它也改不了。
- 能做什么: 可以改变
ptr指向的地址(换个地方指)。
举例:
1 | int x = 5; |
2. 常量指针 (Const pointer)
写法: int* const ptr
- 特点: 指针持有的地址一旦初始化就不能再变。
- 注意: 必须在定义时初始化。
- 能做什么: 虽然地址不能变,但如果指向的是普通变量,你可以修改那个变量的值。
举例:
1 | int x = 5; |
3. 指向常量的常量指针 (Const Pointer to const)
写法: const int* const ptr
- 特点: 全面锁死。地址不能改,指向的值也不能通过它改。
举例:
1 | int x = 5; |
核心知识点补充
为什么 int* ptr = &const_var; 会报错?
如果你有一个 const int x = 5;,你不能用普通的 int* ptr 去指向它。
- 原因:
x本身承诺不改变,但int* ptr拥有修改权限。C++ 为了防止你“绕过限制”去修改x,直接在编译阶段禁止这种赋值。
指向常量的指针 Vs 引用
const int* ptr 和 const 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. 语法实现
对比三种传递方式:
1 | void byValue(std::string s); // 拷贝一份,慢 |
3. 三大关键知识点
① 修改原值
如果参数是 int* ptr(非 const 指针),函数内部可以通过解引用修改外部变量:
1 | void change(int* ptr) { |
② Const 的正确用法 (最佳实践)
- 指针指向的内容不可变:
const int* ptr(最常用,保护数据不被意外修改)。 - 指针本身不可变:
int* const ptr(几乎不用,因为参数本来就是副本)。 - 准则:除非需要修改原值,否则一律用
const T*。
③ 空指针检查 (Null Checking) —— 最重要!
与引用不同,指针可以是空的 (nullptr)。直接解引用空指针会导致程序崩溃。
- 防御式编程:在函数开始处检查指针是否有效。cpp
1
2
3
4void print(int* ptr) {
if (!ptr) return; // “早返回”模式,防止崩溃
std::cout << *ptr;
}
4. 地址传递 Vs 引用传递 (该选哪个?)
| 特性 | 引用传递 (Pass by Reference) | 地址传递 (Pass by Address) |
|---|---|---|
| 安全性 | 高(引用不能为空) | 低(必须检查 nullptr) |
| 语法 | 简洁(像用普通变量) | 繁琐(满屏 * 和 &) |
| 灵活性 | 较低 | 较高(可以重新指向别处,或表示“无值”) |
| 字面量 | const & 支持传递 5, “Hi” 等 |
不支持直接取地址 &5 (报错) |
5. 总结建议:黄金法则
- 首选引用传递:它更安全、语法更干净。
- 次选地址传递:只有当你明确需要处理“空值”(即参数可以不传,传入
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
7void 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一份地址)。
- 按引用传递:底层通常也是编译器通过指针实现的。
- 结论:之所以起这么多名字,是为了区分我们的意图——你是想改那个“值”本身,还是想通过“地址”去改它背后的“对象”。
💡 老师给你的复习口诀:
- 想传可选参数? 用指针并给默认
nullptr。 - 想在函数里改指针指向? 记得写成
int*&。 - 永远不要用
0或NULL代替空指针,始终用nullptr。 - 语法记不住? 记住
int*&是合法的,而int&*(指针指向引用) 是非法的(报错了反过来写就行)。
有没有哪一部分需要我再深入举例的?
12.12
你好!这一节的内容非常核心,同时也容易踩坑。为了让你能够轻松掌握,我把原版课件的内容提炼为核心动机、语法与机制、致命陷阱(必看)、常见误区以及指针版本这几个模块来讲解。
1. 核心动机:为什么要“按引用返回”?
一句话总结:为了省钱(性能)。
- 按值返回 (Return by value):函数返回时,会把结果复制一份给调用者。如果是
int这种小类型还好,但如果是std::string或大对象,复制非常昂贵。 - 按引用返回 (Return by reference):返回的是对象本身的“别名”,不发生复制,速度极快。
写法: 在返回类型后加 &(引用)或 const &(常量引用)。
1 | std::string& func1(); // 返回引用,可读可写 |
2. 致命陷阱:千万不要返回局部变量的引用!
这是本节课最重要的知识点。
规则: 按引用返回的对象,必须在函数结束后依然存活。
错误示范(Dangling Reference 悬垂引用):
你试图返回一个在函数内部创建的局部变量。函数一结束,局部变量被销毁(内存释放),你返回的引用指向了一块“废弃内存”。cpp1
2
3
4
5
6
7
8
9const std::string& badFunction() {
std::string localName = "Test";
return localName; // ❌ 危险!函数结束 localName 就销毁了
}
int main() {
// undefined behavior (未定义行为),程序可能崩溃或乱码
std::cout << badFunction();
}关于“生命周期延长”的误解:
虽然 C++ 有时会延长临时对象的生命周期,但这跨越不了函数边界。一旦出了函数,临时对象必死无疑,引用也就失效了。
3. 那么,哪些情况是安全的?
只有当返回对象的生命周期长于函数本身时,按引用返回才是安全的。主要有以下两种情况:
情况 A:返回传入的引用参数
如果你把一个对象按引用传进函数,函数再把它按引用传出来,这是安全的。因为该对象在调用者那边本来就存在。
1 | // 比较两个字符串,返回较小的那个(由调用者传入,肯定存在) |
情况 B:返回静态变量 (Static Variable)
静态变量在程序结束前一直存在,所以返回它的引用是技术上安全的,但有“副作用”(见下文)。
1 | const int& getStatic() { |
4. 常见误区与细节
误区 1:滥用非 const 静态变量
虽然返回静态变量安全,但如果返回的是非 const 引用,会导致所有调用者共享同一个状态,容易产生莫名其妙的 Bug。
- 建议: 除非为了特定的设计(如单例模式),否则尽量避免返回非 const 的局部静态变量。
误区 2:接收返回值时发生了“意外复制”
即使函数返回的是引用,如果你用普通变量去接收,还是会发生复制。
1 | const int& getRef(); // 假设返回一个引用 |
特性:可以通过返回值修改对象
如果函数返回的是非 const 引用,你可以把它放在等号左边赋值!
1 | int& max(int& x, int& y) { |
5. 按地址返回 (Return by Address)
这与按引用返回几乎一样,只是返回的是指针(Type*)。
- 优点: 可以返回
nullptr(空指针),表示“没找到”或“无对象”。 - 缺点: 调用者必须记得检查是否为
nullptr,否则程序会崩溃。 - 规则: 同样不能返回局部变量的地址。
最佳实践:
除非你需要返回“空”的概念(比如查找失败返回 nullptr),否则优先使用按引用返回,因为它更安全、更符合 C++ 习惯。
总结(精炼版)
- 为了性能:对象较大时,优先考虑
const &返回。 - 铁律:绝对不要返回函数内部局部变量的引用(或指针)。
- 安全场景:返回通过参数传进来的引用,或者类的成员变量(在类的方法中)。
- 接收方式:想避免复制,接收变量也要声明为引用类型(如
const Type& obj = func();)。 - 引用 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
语法繁琐:调用者必须先定义(并初始化)变量,哪怕只是为了接收一个值。无法像返回值那样直接用于临时表达式(如
std::cout << func())。语义不明确:
代码
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++ 的规范:
尽量避免使用 Out parameters 。如果需要返回一个值,直接使用返回值。如果需要返回多个值,考虑使用
struct或std::pair/std::tuple。learncpp如果必须使用(例如需要原地修改对象以提升性能):
对于 Non-optional(非可选)的输出参数,优先使用 Pass by Reference(引用传递),而不是指针。
可以通过命名约定(如后缀
_out)来提示这是一个输出参数。
性能考量:只有当对象 Copying(拷贝)的代价极其昂贵时,才考虑使用 Out parameters 来避免返回值的拷贝开销 。learncpp
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 ptr或auto 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)
- 想要引用? 永远显式写
auto&。 - 想要 Const 引用? 永远显式写
const auto&。- 虽然
auto&对const对象会自动推导为 const 引用,但写全了能防止你误修改,意图也更清晰。
- 虽然
- 想要指针? 推荐写
auto*。- 这能防止你误把对象当指针,也能更清晰地控制
const(例如const auto*就是指向常量的指针)。
- 这能防止你误把对象当指针,也能更清晰地控制
auto默认不仅丢const(顶层),还丢引用&。 只有底层const(比如指针指向的内容) 会被保留。
12.15
同学们好,欢迎来到 C++ 课堂。今天我们要讨论一个在编写鲁棒(Robust)程序时经常遇到的难题:如何优雅地处理那些“可能失败”的函数?
在 C++17 之前,我们处理这类问题的方法通常不太理想。今天我们将学习一个现代化的利器:std::optional。
1. 现状:传统的“尴尬”解决方案
假设我们要写一个整数除法函数。如果用户输入除数是 0,函数显然无法计算出结果。这时候该返回什么?
方法 A:使用“哨兵值”(Sentinel Values)
这是一种很古老的方法:返回一个不可能出现的特殊值。例如,如果计算失败就返回 0 或者 INT_MIN。
1 | double reciprocal(double x) { |
这种方法的弊端:
- 歧义性(半谓词问题): 如果函数合法的计算结果恰好也是这个哨兵值呢?比如
0/5的结果也是0。你无法分辨这是“成功算出了 0”还是“出错了”。 - 缺乏标准: 有的函数返回
-1表示错误,有的返回0,有的返回nullptr。程序员必须查阅手册才能记住每个函数用哪个值。
方法 B:返回 bool,结果用引用传出
这也是常见的做法,但它强迫你先定义一个变量,而且代码逻辑会变得支离破碎。
2. 新的选择:std::optional
C++17 引入了 std::optional<T>。你可以把它想象成一个“特殊的盒子”:
- 这个盒子里面可能装着一个类型为
T的值; - 这个盒子也可能是空的。
示例:用 std::optional 重写除法
1 |
|
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)很像啊?”
虽然语法相似,但它们的内存模型完全不同:
- 指针是“引用语义”: 指针指向的是内存里的另一个地方。你不能返回一个局部变量的地址,否则函数结束变量销毁,指针就失效了。
std::optional是“值语义”: 盒子和里面的数据是连在一起的。当你返回std::optional时,里面的值会被复制或移动给调用者。所以它可以安全地返回函数内部计算出的结果。
5. 进阶应用:作为可选的函数参数
除了作为返回值,std::optional 也可以作为函数参数,表示这个参数是“可选的”。
1 | void printID(std::optional<int> id = std::nullopt) { |
老师的特别提醒(性能注意):
虽然这很方便,但 std::optional<T> 会创建 T 的副本。
- 如果
T是简单类型(如int,double),放心使用。 - 如果
T是复杂的类型(如大字符串std::string或大的vector),不要直接传std::optional<T>。- 建议: 在这种情况下,传统的
const T*(指针)或者函数重载通常是更好的选择,因为它们不会产生多余的内存拷贝。
- 建议: 在这种情况下,传统的
6. 总结与最佳实践
在现代 C++ 编程中,请遵循以下原则:
- 优先使用
std::optional返回可能失败的结果,而不是使用特定的数字(哨兵值)。它能让你的代码“自解释”——别人一看函数签名就知道这个函数可能拿不到结果。 - 获取值之前务必检查。永远不要在没有
if判断的情况下直接使用*result,除非你 100% 确定它一定有值。 - 善用
value_or()。它可以极大地简化你的逻辑代码,减少if-else的层数。
课后思考: 如果一个函数不仅需要表达“失败”,还需要告诉你“为什么失败”(比如是因为权限不足还是因为找不到文件),std::optional 还够用吗?
(提示:可以去了解一下 C++23 的新特性 std::expected,那是我们进阶课程的内容。)
今天的课就讲到这里,下课!
