摘要
本课研究数据经由哪些途径进入函数、函数的结果又经由哪些途径出来。默认情况下,C++ 按值传递实参:形参是一个副本,函数对它所做的一切都不会影响原件。引用则不同,它是一个已经存在的存储单元的第二个名字,能让函数修改调用者的变量;常量引用则能在不复制、也不能改动的情况下读取一份大的数据。为了返回多个结果,标准库提供了 std::pair 和 std::tuple,用结构化绑定来接收它们。接下来是名字的作用域(在哪里可见)和变量的生存期(何时存在),以及名字的遮蔽和 static 局部变量;最后讨论比它所引用的变量活得更久的引用。统一这一切的概念是程序各部分之间的数据流:复制与共享的对比,以及作为隔离机制的名字可见性。
学习目标
- 根据实参是按值传递还是按引用传递,预测函数对实参的作用。
- 为每个形参选择传递方式(小类型按值,大数据按常量引用,要修改的按引用),并说明选择的理由。
- 用
std::pair或std::tuple返回多个结果,并用结构化绑定接收它们。 - 确定程序中每个变量的作用域和生存期,包括
static局部变量和被另一个变量遮蔽的变量。 - 识别指向已不存在的局部变量或临时值的引用,以及预告它的编译器警告,并加以改正。
按值传递
形参是一个副本
函数一课说明了,每次调用都会在栈上创建一个栈帧,其中有函数的形参和局部变量,返回时栈帧随之消失。还需要弄清楚的是,形参和初始化它的实参之间是什么关系。C++ 的默认答案是最简单的:形参是一个新的局部变量,用实参值的一个副本来初始化。如果是这样,修改自己形参的函数修改的只是它的副本,调用者的变量不会改变。下面的程序用一个试图交换两个整数的函数来检验这一点。
#include <print>// 试图交换 a 和 b;只交换了它们的副本。void swapValues(int a, int b){ const int saved{a}; a = b; b = saved; std::println("调用中:a = {}, b = {}", a, b);}int main(){ int x{1}; int y{2}; swapValues(x, y); std::println("调用后:x = {}, y = {}", x, y);}调用中:a = 2, b = 1调用后:x = 1, y = 2在函数内部,交换做得正确;调用之后,x 和 y 保持原值。编译器不发出警告,因为没有什么可警告的:这段代码交换的是两个局部变量,它们在函数结束时就消失了。图中跟踪了调用期间栈上的各个栈帧。
main 的栈帧包含 x 和 y;调用在它上面创建 swapValues 的栈帧,带有自己的存储单元。
证明
调用之前,
main的栈帧有两个存储单元:存放 1 的x和存放 2 的y。调用创建
swapValues的栈帧。它的形参a和b是新的存储单元,实参的值被复制到其中:1 和 2。从这一刻起,a与x之间除了初值相同之外,再没有任何联系。局部变量
saved在同一个栈帧中创建,保存a的一个副本。两次赋值交换了
a和b。发生变化的存储单元全都在上面那个栈帧里;main的存储单元没有被触及。返回时,
swapValues的栈帧连同其中所做的交换整个消失。x和y仍然是 1 和 2。
副本保护了什么
按值传递不是缺陷,而是使函数变得合理的规则。第一,实参可以是任何表达式,而不只是变量:swapValues(x + 1, 7) 是合法的,因为形参是用一个值初始化的,不论这个值从何而来。第二,调用者不必阅读函数体,仅凭声明就知道自己的变量不会改变。第三,在函数内部,形参只是又一个局部变量,可以用作工作变量,而不会在外面造成后果。
#include <print>// n 的十进制位数。// 前置条件:n >= 0。// 后置条件:返回 n 的位数(0 为 1 位)。int digitCount(int n){ int count{1}; while (n >= 10) { n = n / 10; ++count; } return count;}int main(){ const int population{48'210}; std::println("{} 有 {} 位数字", population, digitCount(population));}48210 有 5 位数字函数不断把形参除以 10,直到只剩一位,从而毁掉了它;由于这是一个副本,population 毫发无损,并且可以是 const。由此得出一条适用于整门课程的阅读规则:如果一个形参没有后面一节将要研究的符号 &,那么不论函数对它做什么,都不能修改调用者的变量。
正在加载内容…
已完成
登录账户以保存您的进度