从零开始 / 一次只解释眼前的一步
引用与const:
副本和同一个对象。
函数怎样直接更新调用者?从几个名字对应几份整数开始,追踪只读路径和借用期限,自己写出能正确处理别名的交换函数。
7小时是未经真人试学校准的设计估算。可分多次学习,遇到不清楚的地方保留预测、实际结果和疑问,之后再回修教材。手机可读图与做预测,编译需要电脑终端。
这一章怎样学
先读一小段,写下预测,再运行程序。每次只改一个条件,最后关掉示例,从空文件独立写一次。遇到错误,把第一条报错和自己的修复记下来;不用赶着把页面滚到底。
每次先数对象,再连名字与对象的关系,记录哪条路径可以写以及目标是否仍存活。每章学习记录单独保留;本章完成不自动通过M1或G0。
上一章的next能算出5,调用者的x却仍是4。现在有另一种要求:函数直接更新调用者的那个整数。签名只差一个&,调用后为什么不同? 这一章从对象关系出发,逐步选择副本、可写借用和只读借用。
先备是新04:普通函数。五节预计7小时,含推演、编码与检查,尚未经真人试学校准。本章只使用标量和已学的普通函数;正常示例中的引用都借用已初始化、仍存活的具名整数。本章练习从−100到100的小整数起步,加一流程最多连续增加两次,结果不超过102;这些运算都在int范围内。
05.1 对象与别名 · 预估90分钟
两个名字,是否一定有两份整数
你已经能画出a和它的副本copy。现在新增一个名字r:后续通过r赋值时,到底应该擦掉哪一个格子的旧数字?先确定名字对应的对象,再算表达式的值。
int copy{a};创建另一个整数对象,初值来自此刻的a。这是已学的值复制。int& r{a};则声明一个左值引用(lvalue reference):在本章的具名整数场景中,r是已有对象a的另一个名字,也叫别名。逐项读:int&表示对int的引用,r是引用名,花括号里的a是它绑定的对象。这里的&属于引用声明,不是两个条件之间的&&。
建立这个引用没有创建第二份供r独立保存的int。画图时只给a一个整数格,在旁边写“r也访问a”;不能另画一个r=4并让它自己变化。这是语言层面的对象关系,不承诺编译器必须或完全不必为引用安排额外存储。
本章的可写引用必须在声明时绑定一个已经初始化、可写且仍活着的int对象。绑定(binding)确定引用访问谁;读取r就读取该对象,给r赋值就向该对象写值。这个引用建立后不会改绑:r = b;是读取b当前的整数,再写入r所引用的a,不是让r以后跟随b。赋值与初始化仍是不同动作。
完整例一:赋值之后再改来源
#include <iostream>
int main() {
int a{4};
int b{7};
int copy{a};
int& r{a};
r = b;
b = 10;
std::cout << a << ' ' << b << ' ' << r << ' ' << copy << '\n';
return 0;
}
| 执行到哪里 | 三个整数对象 | 引用r访问谁 |
|---|---|---|
| 创建a、b、copy之后 | a=4,b=7,copy=4 | r尚未建立 |
int& r{a}之后 |
三个值不变 | a |
| r = b之后 | a=7,b=7,copy=4 | 仍是a |
| b = 10之后 | a=7,b=10,copy=4 | 仍是a |
输出按a、b、r、copy的顺序是7 10 7 4。最后b变成10,没有任何语句再写a,所以r也读到7。copy是早先保存的独立整数,保持4。
r增加一个名字,不增加一份整数
a为4,b为7;copy读取a当时的4。此时r的声明尚未执行。
int& r{a}让r访问a;图把a与r写在同一格,三个整数值都不变。
r没有改绑;a/r现在都读7,独立copy仍为4。
b变成10,没有新的赋值写入a,所以r仍读到a的7。
读取同一个a两次:先用名字a,后来用名字r;完整输出7 10 7 4。图表达对象关系,不给引用承诺具体机器存储。
查看所有步骤的文字与数值
- 1 · 三个整数先分别初始化
a:4;b:7;copy:4
a为4,b为7;copy读取a当时的4。此时r的声明尚未执行。
- 2 · r绑定a
a,也由r访问:4;b:7;copy:4
int& r{a}让r访问a;图把a与r写在同一格,三个整数值都不变。
- 3 · r = b读取7再写入a
a / r:7;b:7;copy:4
r没有改绑;a/r现在都读7,独立copy仍为4。
- 4 · b = 10只修改b
a / r:7;b:10;copy:4
b变成10,没有新的赋值写入a,所以r仍读到a的7。
- 5 · 按a、b、r、copy输出
a / r:7;b:10;copy:4
读取同一个a两次:先用名字a,后来用名字r;完整输出7 10 7 4。图表达对象关系,不给引用承诺具体机器存储。
阶梯第一步:先预测,再只换一个条件
盖住表,先画出三个对象与r的关系,再写输出。运行之后只把r = b;改为r = 6;,其余语句不变,重新预测并编译。
提示与答案:先回答赋值的左边最终落在哪个对象
原程序输出7 10 7 4。修改后,r把6写入a,随后b独自变10,输出6 10 6 4。两次都只有a、b、copy三份整数;r没有独立的整数值可供“重新绑定”。
一个不能建立的可写引用
下面故意用字面量4初始化可写引用。4是直接写出的数值,不是本章已经创建的可写整数变量;这种int&绑定不合法。
#include <iostream>
int main() {
int& r{4};
std::cout << r << '\n';
return 0;
}
沿用新04已经讲过的严格编译选项。正常文件可用clang++ -std=c++20 -Wall -Wextra -Wpedantic -Werror alias.cpp -o alias && ./alias。对binding-error.cpp只执行编译、阅读拒绝信息,不运行旧产物;它没有一个应当猜测的终端输出。
小练习:怎样给这个引用一个合法目标
先声明并初始化int value{4};,再把错误声明改成int& r{value};,后面的输出得到4。这里修的是绑定对象,并非去掉严格选项来隐藏错误。本章正常例子持续使用这样的具名对象;其他绑定形式不作为本章练习的输入。
05.2 参数方向 · 预估90分钟
调用者希望保留原值,还是更新原对象
有的函数只算一个新值,有的函数负责把当前值加一。调用形式看起来很相似,接口却需要说明不同承诺。本节先确定参数访问关系,再比较调用前后的状态。
已学的int value按值形参有自己的整数对象。参数改了,调用者没有自动变化。现在把形参写成int& value:每次调用时,这个引用形参绑定实参对象;本章传入一个现存可写int,例如x。函数体通过value写入的就是x,调用写作add_one(x),调用处不用再写&。这称为按引用传参(pass by reference)。
还需要一种只读的参数关系。先读本节的最低合同:const int& value仍是引用,不复制一份int;在函数体内,可以通过value读取借来的对象,不能通过value给它赋值。这里的只读引用(reference to const)限制这条访问路径。本节只从可写的具名x借读;下一节再比较const对象与其他访问路径。
| 本节签名里的参数 | 是否新建整数参数副本 | 通过这个参数能否修改调用者的整数 |
|---|---|---|
| int value | 是 | 不能由这个副本直接写回 |
| int& value | 否,绑定现存对象 | 可以 |
| const int& value | 否,借读现存对象 | 不能 |
这里的“参数方向”是接口说明:把数据读进来,还是借这个名字修改原对象。引用形式不自动说明函数一定会改值,必须同时读函数合同与实现。对一个小int,按值传入常常已经足够清楚;本章用只读引用建立读法,不宣称它比复制小int更快。
完整例二:得到新值与原地修改
#include <iostream>
int next(int value) {
value += 1;
return value;
}
void add_one(int& value) {
value += 1;
}
int next_read(const int& value) {
return value + 1;
}
int main() {
int x{4};
int result{next(x)};
std::cout << x << ' ' << result << '\n';
add_one(x);
std::cout << x << ' ' << result << '\n';
int observed{next_read(x)};
std::cout << observed << ' ' << x << '\n';
return 0;
}
- main的x为4。next的参数副本从4变5并返回5,result保存5;x仍4,输出
4 5。 - add_one的引用参数绑定x,
value += 1把x从4改到5。这次没有整数返回值,函数结束后main继续,输出5 5。 - next_read通过只读引用读到x当前的5,计算并按值返回6,observed保存6。它没有通过引用修改x,最后输出
6 5。
return与参数传递分别决定两件事:函数如何访问输入,以及调用者得到什么结果。next_read接收引用但返回一个整数值,observed不是x的别名。void函数也可以经可写引用改变调用者,不能把“没有返回值”读成“没有作用”。
阶梯第二步:补可写参数
关掉完整例,从空文件写next和add_one。next保留按值参数,返回加一后的结果;add_one用void返回,要求直接把调用者加一。main从x=4开始,先保存next(x),再调用add_one(x),检查x和保存的结果都为5。
提示与答案:只把需要直接写回的参数变为引用
next的接口是int next(int value),add_one是void add_one(int& value)。两个函数体都可用已学的加一语句,但前者改变参数副本、还须返回;后者改变x本身。调用next后x仍4,调用add_one后x才为5。不要为实现写回加全局变量。
阶梯第三步:实现与合同不一致
下面函数名和调用位置都对,合同仍要求更新调用者。函数先输出自己修改后的value,main再输出x。先预测这两行,再定位关系画错在哪里。
#include <iostream>
void add_one(int value) {
value += 1;
std::cout << value << '\n';
}
int main() {
int x{4};
add_one(x);
std::cout << x << '\n';
return 0;
}
提示与答案:改的是谁的整数
实际先输出5,再输出4。value是本次独立的按值参数,函数能读到自己加一后的5,却并没有写入x。只把形参int value改为int& value,原样保留函数体和调用,两行就都输出5。既不是缺return,也不需要把void改成int;缺的是与“直接更新调用者”匹配的参数关系。
05.3 const访问路径 · 预估90分钟
一条路不能写,是否所有地方都不能改
同一个整数可能有原名,也可能有只读引用。看见const之后,必须确认它描述的是对象,还是眼前这条访问路径。这关系到下一次读取是否仍能看到变化。
新02已讲过const int fixed{9};:这个对象本身是const,初始化后不能用普通赋值改变。现在比较int value{4}; const int& read{value};:value对象可写,read只提供读取它的路径。read = 7;不允许;通过原名value = 7;合法,而且下次读取read也得到7。只读引用不是保存一份冻结快照。
只读引用也可以绑定已经存在的const对象,例如const int& fixed_read{fixed};。它不会把const对象变成可写对象。相反,用int& write{fixed};试图获得可写引用不合法:它丢掉了原有的只读限制。这里没有引入绕过const的转换。
#include <iostream>
int main() {
int value{4};
const int& read{value};
value = 7;
std::cout << value << ' ' << read << '\n';
const int fixed{9};
const int& fixed_read{fixed};
std::cout << fixed << ' ' << fixed_read << '\n';
return 0;
}
第一次输出7 7:value已被原名改成7,read仍访问同一个对象。第二次输出9 9:fixed和fixed_read也访问同一份整数,但这个对象自身不可写。四个名字对应两份int,不是四份。
两种拒绝,错误位置不同
第一份错误出现在通过只读引用赋值的语句:
#include <iostream>
int main() {
int value{4};
const int& read{value};
read = 5;
std::cout << value << ' ' << read << '\n';
return 0;
}
第二份错误发生得更早,引用初始化试图去掉目标的const限制:
#include <iostream>
int main() {
const int fixed{4};
int& write{fixed};
std::cout << write << '\n';
return 0;
}
这两份都只用严格命令检查编译拒绝,不运行。前者可把赋值左边改为原有可写名字value,输出变5 5;后者可把write的声明改成const int&来借读,输出4,但这样仍不能用它赋值。
小练习与答案:怎样保存不再跟随的值
在const-paths.cpp中,若希望read一直显示最初的4,应在改value之前保存一个独立整数,例如将声明换成const int read{value};。现在read是const整数副本,不是引用;随后输出7 4。fixed一组仍输出9 9。改变的是对象关系,不能把原来的只读引用当作历史快照。
05.4 auto与引用推导 · 预估60分钟
不写int以后,副本和借用由什么决定
auto能省去重复写类型,但不能省去你对对象关系的判断。已有的auto副本规则遇到引用名时,最容易让人误以为“来源是引用,结果也是引用”。本节只研究从现存具名整数初始化变量。
对本节的标量初始化,auto copy = r;读取r访问的整数,创建自己的int副本;来源名是引用也不会自动保留引用。auto& same = r;明确要求引用,same与r访问同一个对象。const auto& read = r;也借用同一个对象,但只能通过read读取。
#include <iostream>
int main() {
int value{4};
int& r{value};
auto copy = r;
auto& same = r;
const auto& read = r;
copy = 9;
same = 7;
std::cout << value << ' ' << copy << ' ' << same << ' ' << read << '\n';
value = 10;
std::cout << value << ' ' << copy << ' ' << same << ' ' << read << '\n';
return 0;
}
copy最初保存4,之后独立改成9。same把value改成7,因此第一行是7 9 7 7。接着原名value改成10,第二行是10 9 10 10。value与copy是两份整数,r、same、read都访问value;这些名字并不对应五份整数。
auto&不会解除已经存在的只读限制
如果来源是const对象,或者通过const引用去读取可写对象,auto&会保留来源表达式呈现的const。它不会穿透只读引用去替你找一条可写路径。用表格明确三个来源:a为可写int,fixed为const int,read为借用a的const int引用。
| 初始化来源 | auto copy = 来源 | auto& same = 来源 | const auto& view = 来源 |
|---|---|---|---|
| 可写名字a | int副本,可写 | int引用,可写 | const int引用,只读 |
| const名字fixed | int副本,可写 | const int引用,只读 | const int引用,只读 |
| 只读引用名read | int副本,可写 | const int引用,只读 | const int引用,只读 |
这里省略const的auto副本只让新副本可写,没有修改来源的限制。这张表限定为本章的具名int场景;不使用函数返回类型推导、数组或其他auto形式。
#include <iostream>
int main() {
int value{4};
const int& read{value};
auto copy = read;
auto& same = read;
const auto& again = read;
copy = 6;
value = 7;
std::cout << value << ' ' << copy << ' ' << same << ' ' << again << '\n';
const int fixed{9};
auto fixed_copy = fixed;
auto& fixed_read = fixed;
fixed_copy = 10;
std::cout << fixed << ' ' << fixed_copy << ' ' << fixed_read << '\n';
return 0;
}
read借用可写value,same和again继续沿只读路径借用它。copy独立改6,value由原名改7,第一行输出7 6 7 7。后面fixed是9,fixed_copy可改10,fixed_read仍借读9,第二行输出9 10 9。
小练习与答案:去掉一个&,关系怎样改变
在auto-access.cpp中只把auto& same = r;改成auto same = r;,same现在是初值4的独立整数,随后被改为7。第一行变4 9 7 4;value改10之后第二行变10 9 7 10。再回到原文件,另做一次独立修改:把auto-from-const.cpp的value = 7;改为same = 7;,应该编译拒绝,因为same从read推导出的路径是只读的。这是两个分别重编译的实验,不要把两次修改混在一起。
05.5 借用有效期 · 预估90分钟
引用还写在代码里,目标是否还活着
前面每次读取都有一个仍在作用域内的整数作为目标。现在把借用关系跨过一个花括号或函数返回,先判断对象何时结束,再决定哪一次访问仍有效。名字的可见范围和对象的存活时间需要分别记录。
生命周期(lifetime)描述一个对象从开始存在到结束存在的区间。对本章这些普通局部int,执行声明并完成初始化后可以使用;离开定义它的块时,这个局部对象的生命周期结束,函数return也会离开相应的局部块。新03/04的作用域规则回答“这里能用哪个名字”,生命周期回答“那个对象现在还存在吗”。
借用(borrowing)在这里表示通过引用访问其他地方已有的对象;借用者不负责让这个具名整数继续存活。引用名离开自己的块不会销毁仍在外层的被借对象;反过来,被借对象先结束后,残留的引用也不能把它变回有效对象。这样的引用称为悬空引用(dangling reference),不能再用它读取或写入已经结束的对象。
#include <iostream>
int main() {
int value{4};
{
int& r{value};
r = 7;
std::cout << r << '\n';
}
std::cout << value << '\n';
return 0;
}
value在外层main块中活着。内层r借用它并写7,先输出7;内层块结束,r这个名字不再可见,但外层value仍存在,因此第二次合法输出也是7。不能把“r离开块”画成value也被销毁。
返回的是整数值,还是一个对象的访问关系
前面函数的返回类型是int,调用者取得整数值。现在读int& same_object(int& value):最前面的int&表示这个函数返回一个int引用;形参里的int&表示借用调用者的对象。在函数里return value;让结果仍访问这个对象,没有复制出新的int。调用者用int& r{same_object(x)};继续绑定同一对象。两个&分别描述结果和输入,不能只看其中一个。
#include <iostream>
int& same_object(int& value) {
return value;
}
int main() {
int x{4};
int& r{same_object(x)};
r = 7;
std::cout << x << ' ' << r << '\n';
return 0;
}
x在main中是4,函数的参数引用借用x,返回的引用也访问x;main的r绑定x。函数结束并没有结束main中的x,因此r写7合法,最后输出7 7。安全依据是x在这次访问时仍活着,不是“返回引用总是安全”。
常见错误:把自己的局部对象借出去
下面把目标改成bad内部创建的local。先按生命周期分析,再只做编译诊断检查;不运行这份错误程序,也不把它的输出猜成4或某个随机整数。
#include <iostream>
int& bad() {
int local{4};
return local;
}
int main() {
int& r{bad()};
std::cout << r << '\n';
return 0;
}
bad里的local初始化为4;return给出的引用关系指向它,但函数离开时local的生命周期结束。main中的r所对应的目标已经结束,输出语句试图读取悬空目标,没有可由C++规则保证的正确输出,这是未定义行为。返回引用不为这个局部对象续命。
在本机Clang的严格选项下,这份文件应因返回局部对象引用的诊断而编译失败。这里只执行clang++ -std=c++20 -Wall -Wextra -Wpedantic -Werror return-local-reference.cpp -o return-local-reference并查看信息;不附加运行命令。诊断与语言错误后果分别记录:别的选项或工具可能接受源文件,接受也不会让随后读取悬空引用变得合法。
对本章已经存在的具名整数,改成const引用同样不能延长其寿命。某些其他绑定形式有专门规则;本章正常示例的输入范围已明确限定为具名整数,不把“const引用总能延寿”当作规则。后续临时对象专题会单独建立那些合同。
引用关系越过return,不等于目标继续存活
这里只推演错误文件;尚未进入bad,local还没有开始存在。
声明执行并完成初始化,local为4,当前可以访问它。
返回类型是int&,交出的关系指向这个local,没有创建独立结果整数。
进入main的后续操作时,bad的局部local已经结束;r对应失效目标。这里不把结束的对象标成0或随机值。
通过r读取结束的对象属于未定义行为;不能以某次偶然输出作为正确性证据。这个程序不运行。
这是另一份安全程序:local仍活着时读取4,调用者用结果初始化自己的int。local结束后result仍可独立读取和修改。
查看所有步骤的文字与数值
- 1 · 调用bad之前
main:等待调用;bad的local:尚未建立;返回引用:尚未产生
这里只推演错误文件;尚未进入bad,local还没有开始存在。
- 2 · bad创建local
main:仍在等待;bad的local:活着:4;引用关系:尚待return
声明执行并完成初始化,local为4,当前可以访问它。
- 3 · return local交出引用关系
main:尚未继续;bad的local:此刻仍活着;返回关系:借用local
返回类型是int&,交出的关系指向这个local,没有创建独立结果整数。
- 4 · bad离开,local结束
main的r:目标已失效;bad的local:生命周期结束;合法输出:不能预测
进入main的后续操作时,bad的局部local已经结束;r对应失效目标。这里不把结束的对象标成0或随机值。
- 5 · 输出语句不能补救
拟执行操作:读取r;对象条件:已不满足;本课操作:只看诊断
通过r读取结束的对象属于未定义行为;不能以某次偶然输出作为正确性证据。这个程序不运行。
- 6 · 换用按值返回的sample程序
sample的local:已结束;main的result:独立整数4;随后可做:改为7并输出
这是另一份安全程序:local仍活着时读取4,调用者用结果初始化自己的int。local结束后result仍可独立读取和修改。
可以交出值,就不必交出局部对象的借用
#include <iostream>
int sample() {
int local{4};
return local;
}
int main() {
int result{sample()};
std::cout << result << '\n';
result = 7;
std::cout << result << '\n';
return 0;
}
sample的返回类型是int。它在local仍活着时读取4,把这个整数值交回;main用这个结果初始化自己的result。local随后结束,不会使独立result失效,所以先输出4,再把result改7并输出7。这里不需要保留指向local的引用。
小练习与答案:指出最后一次合法访问
在borrow-scope.cpp中,内层最后一次输出r合法,因为value还活着;内层结束后可读value,不能再直接使用已经超出名字作用域的r。在return-local-reference.cpp中,读取local以执行普通按值返回本可发生在它仍活着时,但错误程序实际选择了返回引用;到了main输出时目标已经结束。修复可把bad的返回类型改为int,并把main的接收声明改为int result{bad()};,输出result,得到4。只改调用者为普通int接收而保留bad返回引用,仍会从失效目标读值,不是修复。
阶梯第四步:两个参数也可能借同一个对象
别名也可能发生在两个参数之间。设一个函数的两个参数都是int引用,如果调用时两处实参都写x,这两个参数便都访问x;两个形参名字不保证两个独立对象。赋值时仍沿各自的绑定关系找到目标。接下来的调用实参只是变量名,没有实参内的修改动作,不需要猜参数求值的先后顺序。
从空文件写void swap_values(int& left, int& right),交换两个调用者的整数。先创建一个普通int保存left的旧值,再完成两次赋值;不使用还没学过的库交换接口。对x=2、y=9,调用后应为9、2。再调用swap_values(x, x),同一个对象作为两个实参时,值应保持不变。
提示:保存值的时机决定能否找回旧值
如果直接先写left=right,再写right=left,第一次赋值已经丢掉left旧值。要先用独立整数saved保存它;saved是副本,不跟随left变化。接着从right读值写left,最后从saved读旧值写right。
参考实现与逐步答案:先完成自己的版本再展开
#include <iostream>
void swap_values(int& left, int& right) {
int saved{left};
left = right;
right = saved;
}
int main() {
int x{2};
int y{9};
swap_values(x, y);
std::cout << x << ' ' << y << '\n';
swap_values(x, x);
std::cout << x << ' ' << y << '\n';
return 0;
}第一次进入函数,left借x=2、right借y=9。saved先保存2;left=right把x改9;right=saved把y改2。输出9 2。第二次left和right都借x=9,saved保存9;两次赋值都向同一个x写9,y保持2,因此第二行仍为9 2。同对象测试不要求额外分支,但必须用这三步推演证明它有效。
只把初始x改为−2,第一次输出9 -2,第二次同对象调用后仍为9 -2。再独立测两对象初值相等、以及包含0的情形。它们验证接口允许的对象关系和输入,不是只背住2与9的交换结果。
回到章首:调用后是否改变原对象,先看形参有没有借用它,再看函数体通过哪条路径读写;接着确认这条路径是否只读、目标是否还活着。保存自己的对象关系图、预测与实测结果,再到章末做中英复述。
关掉参考,再做一次
把理解说出来
先完成正文的独立迁移,再回答下面六题。它们只检验本章内容;打开答案、编译成功或填写用时,都不会自动通过本章,更不代表通过 G0。
对象关系与预测
a=4、b=7、copy复制a、r引用a。执行r=b再b=10后,四个名字分别读到什么?有几份整数?
对照推理与英文回答
a=7、b=10、r=7、copy=4;只有a、b、copy三份整数。r=b写入a,没有把r改绑到b。copy是建立时保存的独立值。
There are three integer objects. Assigning b through r writes seven into a; it does not rebind r. After b changes, a and r still read seven, while copy remains four.参数方向与追问
next(x)返回5却不改x,void add_one(x)没有整数返回却能改x,为什么?
对照推理与英文回答
next的int参数是独立副本,返回结果由调用者接收。add_one的int&参数绑定x,写参数就是写x。void只表示不提供结果值,并不禁止通过参数修改对象。
The value parameter in next is a separate integer. The reference parameter in add_one aliases x, so writing through it changes x. A void return type means no result value is produced; it does not mean the function has no effect.const与代码找错
int value=4,const int& read=value;value改7后read会变吗?把赋值左边改成read,或把value改成const对象呢?
对照推理与英文回答
经原名value改7后read也读7,因为它不是快照。read自身是只读路径,不能用它赋值。若value对象本来const,初始化之后也不能经原名赋值;不能用int&去掉这种限制。
A read-only reference is not a snapshot. It observes seven after the mutable object changes through its original name. Writing through the read-only reference is invalid. If the object itself is const, ordinary assignment through its original name is invalid too.auto预测与边界
read是借用可写value的const int引用。auto copy=read与auto& same=read有什么不同?value后来改变,谁会跟随?
对照推理与英文回答
copy是新的可写int副本,不跟随value变化。same继续借用value,但从read推导出的路径仍是只读的;value通过原名改变后,same能读到新值,不能用same改值。
Auto by value creates a separate writable integer. Auto with an ampersand preserves the read-only access presented by read. That alias sees later changes to value, but cannot be used to modify it.寿命与错误分类
返回局部int的值和返回局部int的引用有什么区别?编译器接受坏程序能证明它安全吗?
对照推理与英文回答
按值返回在local活着时读取整数,调用者保存独立结果。局部引用返回后,目标已结束;随后读取没有规定的正确结果。严格Clang可能因警告被提升为错误而拒绝编译,但接受源文件也不证明悬空访问有效。
Returning the integer value lets the caller keep an independent result. Returning a reference to the local leaves a dangling reference after the local lifetime ends. Compiler acceptance does not make a later access valid, and no particular output is guaranteed.独立迁移与别名追问
swap_values(x,y)怎样保存两个旧值?若第二次传入swap_values(x,x),为什么不需要额外分支?
对照推理与英文回答
先用独立int saved保存left的旧值,再left=right,最后right=saved。第一次x=2/y=9变为9/2;同对象调用时两个参数都借x,saved保存9,两次都写回9,因此x保持9、y保持2。
Save the old left value in a separate integer before either assignment. With two different objects, the next two assignments exchange their values. With the same object passed twice, both parameters alias it and every write restores the same saved value. The object therefore remains unchanged.和正文是同一份源码
示例文件
先自己输入和预测,卡住时再下载对照。文件名相同不代表内容相同;把它们放在单独的练习目录中,避免覆盖自己的作品。
- alias.cpp完整例一:赋值不是改绑
- binding-error.cpp故意编译失败:字面量不能这样绑定可写引用 · 故意用于编译报错
- parameter-directions.cpp完整例二:副本、写回与只读输入
- wrong-parameter.cpp能运行的错误:修改的是参数副本
- const-paths.cpp只读路径仍能看到原对象的新值
- const-write-error.cpp故意编译失败:经只读引用赋值 · 故意用于编译报错
- const-bind-error.cpp故意编译失败:去掉绑定目标的const · 故意用于编译报错
- auto-access.cppauto副本与两种引用
- auto-from-const.cpp从只读名字推导auto与auto&
- borrow-scope.cpp内层引用结束,外层对象仍存活
- return-reference.cpp返回对调用者对象的引用
- return-local-reference.cpp仅编译诊断:返回局部引用的错误 · 故意用于编译报错
- return-value.cpp按值交回独立结果
- swap-values.cpp独立交换与同对象双参数
完整编译、输出和失败路径验证随本地构建记录保留;这里的程序没有调用加速卡。
可选的学习反馈
记下你真正花的时间
每完成一个学习时段,再填实际分钟。环境准备、阅读推演、独立编码和卡点排查分别记录,避免同一段时间重复计算。离开吃饭或做其他事情的时间不算进去。
记录只保存在你的浏览器,可导出给我复盘。留空表示尚未记录,不等于零耗时;页面停留时间不会自动计为学习。不要把开发者检查时间填进来。
尚无真实试学用时。
换设备:导入记录,或取回损坏的旧记录
导入会合并时段,相同编号不重复累加;发生冲突会保留现有记录。
阅读记录与课程验收分别保存。
本章资料与查证
- WG21 N4861:C++20工作草案 ↗
固定版本第一方原件,2020-04-01;下列HTML为同版本定位镜像。
- N4861 C++20草案:引用声明与是否需要存储 ↗
05.1–05.2;普通引用初始化与形参绑定,不承诺具体存储。 固定版本HTML镜像,并非ISO官网。
- N4861 C++20草案:引用初始化后不改绑 ↗
05.1;赋值写入被引用对象。 固定版本HTML镜像,并非ISO官网。
- N4861 C++20草案:直接绑定与const限制 ↗
05.1–05.3;本章具名int及不合法绑定。 固定版本HTML镜像,并非ISO官网。
- N4861 C++20草案:const访问路径 ↗
05.3;路径只读不等于可写对象被冻结。 固定版本HTML镜像,并非ISO官网。
- N4861 C++20草案:auto的推导入口 ↗
05.4;只讲具名标量的使用结果,不要求定义模板。 固定版本HTML镜像,并非ISO官网。
- N4861 C++20草案:推导中的引用与const ↗
05.4;副本与引用推导差异,固定C++20条款。 固定版本HTML镜像,并非ISO官网。
- N4861 C++20草案:普通局部对象的存储期 ↗
05.5;本章普通局部int到退出块为止。 固定版本HTML镜像,并非ISO官网。
- N4861 C++20草案:对象与引用的生命周期 ↗
05.5;名字作用域与对象存活分别判断。 固定版本HTML镜像,并非ISO官网。
- N4861 C++20草案:生命周期结束后的访问 ↗
05.5;不得读取悬空引用对应的对象。 固定版本HTML镜像,并非ISO官网。
- N4861 C++20草案:返回结果与局部对象结束的次序 ↗
05.5;按值结果与局部引用的区别。 固定版本HTML镜像,并非ISO官网。
- Clang:返回局部地址或引用的诊断 ↗
05.5;诊断是工具行为,实际选项和版本见运行记录。
本章独立解释所需读法;资料用于核对与补充。工具版本、操作系统和实际执行状态见自己的运行记录。