3.1 引用与指针:两种落笔方式


3.1 引用与指针:两种落笔方式

本节摘要:引用是"必须挂靠已有账户"的别名,声明即绑定、终身不改、不可为空;指针是可空可改签的票据,灵活但自带悬垂风险。选型的核心问题是:这笔账会不会有"暂时没有对象"的阶段?会,用指针;不会,用引用。所有权转移需求则交给第 2 章的智能指针。

本章开头说过,账目流转的成本取决于票据类型,而最基础的两类票据就是引用与指针。它们都"不拥有对象"(销账责任在第 2 章已各归其主),区别在记账的姿势:引用声明那一刻就必须挂到已有账户上,从此是账户的别名;指针可以空着手存在,随时签发、随时改签、随时作废。本节先看清两种姿势的账本语义,再给接口设计一张选型表。

先看一笔账

同一个函数的三种传参写法,账目行为完全不同:

#include <cstdio> void by_value(int v) { // 抄账:拿到的是副本 v = 100; } void by_ref(int& r) { // 别名:r 就是调用方那个账户的另一个名字 r = 100; } void by_ptr(int* p) { // 票据:可能空手,可能指账 if (p) *p = 100; // 空票据检查:指针的日常义务 } int main() { int account = 7; by_value(account); std::printf("按值传递后 %d\n", account); // 7:副本改了,正主没动 by_ref(account); std::printf("引用传递后 %d\n", account); // 100:别名写的就是正主 by_ptr(&account); std::printf("指针传递后 %d\n", account); // 100:票据 deref 后写正主 by_ptr(nullptr); std::printf("空票据安全通过(函数内已检查)\n"); return 0; }

输出确定:

按值传递后 7 引用传递后 100 指针传递后 100 空票据安全通过(函数内已检查)

按值是抄一笔新账,原账不动;引用与指针都能改到原账。真正的差别藏在约束里:by_ref 不可能收到"空引用"——调用方必须拿一个真实对象来绑定;by_ptr 收空票据是合法的,函数必须自查。这个差别就是接口的语义文档:参数写引用,等于向调用方承诺"这儿必须有个对象";写指针,等于声明"这儿允许没有"。

图 3-1 引用与指针:别名与票据

图 3-1 引用与指针:别名与票据

一、两种票据各自的悬垂形态

第 1 章见过悬垂指针,引用同样会悬垂,只是死法更隐蔽:

#include <cstdio> int& dangling_ref() { int local = 5; return local; // 返回局部变量的引用:坏账 } // local 在此销户,别名指向已销账户 int* dangling_ptr() { int local = 5; return &local; // 同样是坏账,编译器通常会警告 } int main() { // 下面两行都是未定义行为:引用的悬垂与指针的悬垂同源 // std::printf("%d\n", dangling_ref()); // std::printf("%d\n", *dangling_ptr()); std::printf("两条路径都不该走:返回局部对象的引用或地址都是坏账\n"); return 0; }

悬垂的根源不是票据种类,而是票据的寿命超过了账户的寿命。引用寿命受限于被绑定对象,写起来舒服,但也因此更容易在不知情中超期——比如把临时对象的引用存进长命成员。审计口诀:引用适合"函数调用期间借一眼"的场景(参数、返回引用链),凡是要"存下来慢慢用"的引用,先确认被引对象的寿命覆盖存储期,否则改用值或智能指针。

指针的额外风险是空与改签,对应两条纪律:可能为空就要检查(或改用引用把"不许空"写进类型);改签前想清楚谁还在用旧签。第 2 章的 unique_ptr 把"改签"升级成了显式过户,能用它的场合优先。

二、案例:流水线四站传参的改造成本核算

背景:本章主线里的请求处理链,第一版把请求对象按值传递,四站下来请求体被深拷贝四次。

操作:逐站改造。日志站只读——改成 const 引用,一次拷贝立刻消失;校验站只读——同改 const 引用;路由站要修改路由标记——改成普通引用;落库站需要接管请求体——改成按值加 std::move 调用(写法见 3.3)。全程没有改任何业务逻辑,只换票据类型。

结果:深拷贝从四次降到零次(前三站零拷贝,末站是过户),高峰期单请求耗时下降一成左右,与预估吻合。

解读:这张核算单说明传参选型不是风格问题,是成本问题。每一站先问两个问题:要不要改账?要不要接管账? 都不——const 引用;要改——引用;要接管——值加移动或智能指针。两个问题的答案组合穷尽了传参场景,这也是本节选型表的全部内容。

变式:跨线程传递没有"借用"可言——被借对象的寿命可能撑不过使用者,所以跨线程一律传值或传智能指针(所有权随线程交接),这条纪律在 5.1 会从并发角度再次论证。泛型中转代码的传参另有讲究(万能引用),那是 3.4 的主题。

💡 关键直觉:引用和指针都不承担销账责任,它们只是"记账的笔"。选笔的原则是让类型替你守住不变量——"必须有对象"写进引用类型,"可能没有"写进指针类型,"责任归属"写进智能指针类型。让非法状态无法表达,审计工作量自然归零。

问题:函数返回引用还是返回指针,审计上怎么判?

判据是这笔返回值是否可能"暂时无账可指"。查找类接口(在表里找一条记录)经常找不到,返回指针并允许空值是诚实的签名——调用方被迫检查;而访问类接口(取容器中已知存在的元素)找不到就是调用方的程序错误,返回引用把"必有对象"的承诺写进类型,空检查的义务反而免了。两者都有一条共同的坏账红线:被引对象不能随本次调用销户。返回局部变量的引用或地址是 3.1 开头演示过的死局;返回成员或入参所指对象的引用才在安全清单上。还有一个实用折中值得知道:拿不准时返回值类型,让移动语义(3.3)把返回的过户成本压到近零——多数场景里"返回值"已经不再是昂贵的选择。

本节要点回顾

  • 引用是别名:声明即绑定、终身不改、不可为空,适合"必须有对象"的借用。
  • 指针是票据:可空、可改签,"允许没有"的语义必须写出来并检查。
  • 悬垂同源:票据寿命超过账户寿命就是坏账,与票据种类无关;存储引用前先核寿命。
  • 两问选型:不改账用 const 引用,改账用引用,接管用值加移动或智能指针。
  • 跨线程不借用:线程边界只传值或传所有权,借用只在同一线程的调用栈内成立。
  • 成本是设计的:流水线四站零拷贝的改造证明,传参选型直接写在耗时账单上。

下一节给流转加上安全带:const 如何在类型层面把"只读条款"交给编译器执行,顶层与底层 const 各自冻结什么——审计时如何一眼看出一笔账动没动得了。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U