从零开始 / 一次只解释眼前的一步
普通函数:
参数、调用与返回。
怎样复用一段计算,同时说清输入和输出?把调用者、这次参数和返回结果分开记录,自己写出可检查的计数函数。
8小时是未经真人试学校准的设计估算。可分多次学习,遇到不清楚的地方保留预测、实际结果和疑问,之后再回修教材。手机可读图与做预测,编译需要电脑终端。
这一章怎样学
先读一小段,写下预测,再运行程序。每次只改一个条件,最后关掉示例,从空文件独立写一次。遇到错误,把第一条报错和自己的修复记下来;不用赶着把页面滚到底。
每次先指出当前在哪一次调用、正在操作谁的对象,再记录返回后继续的位置。每章学习记录单独保留;本章完成不自动通过M1或G0。
上一章能在main中完成一次计数。现在,同样的计算要用于0、1和4三份输入:是复制三遍循环,还是给这段工作一个名字?这一章要解决的是:一次计算怎样接收输入、交出结果,再让调用它的地方继续执行?
先备是新03:控制流。继续使用前三章的编译流程、标量、算术、分支和普通循环。每个下载文件各有自己的main,分别编译运行;不要把本章14个文件一起交给一次链接。
六节预估8小时,包含阅读、预测和编码,仍未经真人试学校准。先沿着一次调用走完,再比较两次调用。前五节的小整数例子限定在−100到100,保证本章的加减、取负和除法结果可表示;两份计数函数另外明确0到100的输入合同。
04.1 定义与调用 · 预估90分钟
给一段计算起名字之后,程序何时才会执行它
你想把“得到下一个整数”用在不同位置。把几行代码放进花括号还不够:调用者需要知道它叫什么、要交什么进去、能得到什么。这一节先读函数的外形,再追踪控制从哪里离开、又回到哪里。
本章采用一套简单的命名方式:以小写英文字母开头,后续可以放字母、数字和内部下划线_,例如next、sum_before、count_even_before。下划线是名字的一部分,和减法符号-不同;sum_before是一个完整名字。关键字不能用作自己的名字,大小写不同也是不同名字。本章不采用以下划线开头或连续含两个下划线的名字;这是一套可直接使用的命名子集,不是全部C++标识符规则。
函数定义(function definition)给一段工作规定名字、输入和函数体。读int next(int value):最前面的int是返回类型,表示正常交回的结果是一个整数;next是函数名;圆括号里的int value声明这次调用接收的整数形参(parameter);随后花括号是函数体。这里的int value不要求你预先在main里创建同名变量。
next(4)是函数调用(function call)。括号中的4是这次实际提供的实参(argument)。调用表达式会得到函数交回的结果,因此能放在已有的初始化写法里:int result{next(4)};先完成调用,取得结果,再完成result的初始化。结果尚未取得时,不能提前读取result。
普通函数定义放在main的花括号外;调用可以写在main里面,不表示另一个普通函数的定义也能放进去。本节定义放在main前,让编译器在遇到调用时已经见过接口。源文件中定义写在前,不表示运行先执行它:程序仍从main开始;只有执行到调用,才进入对应函数体。函数的结束与main的结束也不是同一件事。
在这个返回int的函数内,return value + 1;先计算表达式,再把整数结果交回调用处,结束这一次函数调用。它不输出到终端;输出仍需要原来的std::cout语句。调用处拿到结果后,继续完成尚未完成的那条语句,再进入下一条语句。
沿着一次调用走完整条路线
#include <iostream>
int next(int value) {
return value + 1;
}
int main() {
int result{next(4)};
std::cout << result << '\n';
return 0;
}
- 运行进入main,遇到result的初始化,需要先得到
next(4)的值。 - 进入next,本次参数value得到4;计算
value + 1得5。 - return把5交回,result的初始化完成,result为5。
- main继续到输出语句,显示5和换行。main的return 0才是本次程序的成功退出状态。
先读本节新增的四个编译选项
保存、编译、运行的流程沿用新01;从本节开始,命令增加四个用于编译诊断(diagnostics)的选项。诊断是编译器给出的检查信息:warning表示警告,单有警告不一定阻止生成程序;error表示错误,本次构建不能成功。先知道工具怎样处理这些信息,再判断一次编译的结果。
| 新增选项 | 在这里要求工具做什么 |
|---|---|
-Wall |
开启一组常用编译警告;名字中的all不表示所有警告。 |
-Wextra |
再开启一组额外的编译警告;与-Wall合用仍不是全部诊断。 |
-Wpedantic |
增加与所选语言标准有关的诊断;本命令由已学的-std=c++20选择标准。 |
-Werror |
把编译警告按错误处理,让出现这些警告的构建失败。它改变处理方式,不代表开启所有警告。 |
因此,下面命令遇到编译警告也会拒绝本次构建,已学的shell &&就不会启动右边的程序。目录里可能仍有上次生成的程序,不能手动运行它来证明新源码通过。反过来,没有警告也不能证明计算结果正确,还要核对实际输出。四个选项是工具接口,不改变C++语句本身的执行规则;具体诊断集合随工具版本变化,可查Clang官方诊断选项说明。
在下载文件所在目录使用这组新增选项:clang++ -std=c++20 -Wall -Wextra -Wpedantic -Werror definition.cpp -o definition && ./definition。输出应为5。只把调用里的4改成0,重新编译,结果应为1;没有必要改函数定义。
若一个函数接收两个输入,形参之间用逗号分隔,例如接口中的int value, int amount;调用中的shifted(4, 3)也用逗号分隔实参。位置负责对应:4交给第一个形参,3交给第二个。这里的逗号是参数列表的分隔符,不是要求两个输入的求值都从左到右发生。本章多个实参只用没有修改动作的小数值;需要按顺序做的动作先分别写成完整语句。04.5会给出这个双参数函数的完整程序。
小练习:5去了哪里
不运行程序,指出definition.cpp中哪一行进入next、哪一行把5交回、哪一行才真正输出。只把main里接收结果的名字result全部改为answer,会改变next里的参数名或输出吗?
提示与答案:把“调用结果”和“终端输出”分开
int result{next(4)};触发调用;next的return交回5;后面的cout才输出。把main中的声明和使用都改为answer只改变那一个局部名字,next仍使用自己的value,输出仍为5。名字不必相同才能传值。
04.2 按值参数 · 预估90分钟
函数里加一,究竟改了谁
上一节用字面量4作为输入。现在输入来自main中的对象x:函数能读取这个值,但函数体里的赋值是否会改到x?要回答这个问题,必须为这次调用和调用者分别记状态,不能仅凭“都从4开始”就把它们画成同一个对象。
参数写作int value时,本章使用的是按值传参(pass by value):每次调用都有自己的整数参数对象,用实参当时的值初始化它。next(x)读取x,把这个整数值交给本次value。对这个参数做value += 1;只改本次value;不会经由这个int参数自动写回x。这条结论限定于本章的整数副本,不是说任何函数都不能产生外部影响,例如函数仍然可以输出文字。
return value;读取参数里的整数值,把结果交回;返回结果不会自动改写调用者的源对象。调用者可以用结果初始化另一个对象,也可以通过已学赋值明确更新x。给参数初值、在函数里更新、交回结果、调用者保存结果,是四个不同动作。
调用也可以独立写成一条语句:next(x);。函数仍执行,仍产生返回结果;这条语句没有要求保存这个结果,结果便被丢弃。它不是取消调用,也不是把返回值自动写回x。本章普通函数没有要求必须使用返回值的额外属性,不引入新的转换写法来丢弃结果。
完整例一:同一个调用,三种接收方式
#include <iostream>
int next(int value) {
value += 1;
return value;
}
int main() {
int x{4};
int y{next(x)};
std::cout << x << ' ' << y << '\n';
next(x);
std::cout << x << ' ' << y << '\n';
x = next(x);
std::cout << x << ' ' << y << '\n';
return 0;
}
| main执行的动作 | 这次next中的变化 | 动作后main的状态 | 输出 |
|---|---|---|---|
| 用next(x)初始化y | value从4变5,交回5 | x=4,y=5 | 4 5 |
| 单独调用next(x) | 新一次value从4变5,结果未保存 | x=4,y=5 | 4 5 |
| x = next(x) | 又一次value从4变5,交回5再赋给x | x=5,y=5 | 5 5 |
参数修改与调用者赋值是两件事
x已经初始化为4;y的初始化需要先完成next(x),此时没有可读取的y结果。
读取x的4,把4交给本次整数参数value。两个状态分别记录。
value += 1使本次value变5,没有语句要求修改main的x。
return交回5,main用它初始化y,然后输出4 5;这次调用已经结束。图不描述参数的物理存储或精确销毁时点。
单独next(x)仍从4开始计算5,但返回结果没有保存;main再输出4 5。
第三次next仍接收x当时的4,交回5;x = next(x)的赋值把5写入x。y保持5,输出5 5。
查看所有步骤的文字与数值
- 1 · main准备接收第一次结果
main的x:4;本次next:尚未进入;main的y:等待结果
x已经初始化为4;y的初始化需要先完成next(x),此时没有可读取的y结果。
- 2 · 用当前x初始化本次参数
main的x:4;next参数:value=4;main的y:等待结果
读取x的4,把4交给本次整数参数value。两个状态分别记录。
- 3 · 在next内更新参数
main的x:4;next参数:value=5;main的y:等待结果
value += 1使本次value变5,没有语句要求修改main的x。
- 4 · 返回5,完成y的初始化
main的x:4;main的y:5;终端输出:4 5
return交回5,main用它初始化y,然后输出4 5;这次调用已经结束。图不描述参数的物理存储或精确销毁时点。
- 5 · 第二次调用,结果未保存
main的x:4;main的y:5;这次结果:5被丢弃
单独next(x)仍从4开始计算5,但返回结果没有保存;main再输出4 5。
- 6 · 第三次调用后由main赋值
main的x:5;main的y:5;终端输出:5 5
第三次next仍接收x当时的4,交回5;x = next(x)的赋值把5写入x。y保持5,输出5 5。
每一行调用都是新的一次工作,不会沿用上一次value里的5。第三行x变成5的直接原因是main写了赋值;y仍是最初保存的独立整数,没有自动跟随关系。
阶梯第一步:先写预测,再只改输入
盖住状态表,用原输入x=4写出三行输出,并给每次调用的参数单独画格子。然后只把main的x初值改为0,先预测再运行;不要同时改函数体。
提示与答案:每次参数都重新从当前x取值
原输入三行为4 5、4 5、5 5。改为0后三行为0 1、0 1、1 1。第二次调用仍执行,但main没有保存返回值;第三次才由赋值更新x。两次实验都不是函数通过值参数直接改到x。
04.3 返回路径与void · 预估60分钟
函数从另一条路离开,调用者还会继续吗
一个计算可能在分支里就知道答案,不必再执行余下语句。另一些函数只负责显示一条消息,不需要交回整数。我们要把这两类接口分开,并检查函数有没有遗漏一条正常到达末尾的路径。
普通函数中的**提前返回(early return)**结束当前调用,回到调用者继续。它与新03的main提前退出共享return的跳转形式,但目标不同:magnitude返回3,不表示整个程序以3退出;main还会执行自己的后续语句。
下面的magnitude只接收−100到100:负数取负,零和正数原样返回。本章不把这个受限例子推广到所有int值;新02已讲过整数结果必须可表示。
#include <iostream>
int magnitude(int value) {
if (value < 0) {
return -value;
}
return value;
}
int main() {
int result{magnitude(-3)};
std::cout << result << '\n';
std::cout << "done" << '\n';
return 0;
}
输入−3使条件为真,return交回3,函数里后面的return value;不执行。main取得3后输出它,仍然输出done,最后以0结束程序。输入改为0或3会跳过负数分支,走末尾return,输出分别为0、done或3、done。
对本章普通的非void函数,不能让一条执行路径正常走到函数体末尾而没有交回结果。看下面故意缺少返回的版本:
#include <iostream>
int magnitude(int value) {
if (value < 0) {
return -value;
}
}
int main() {
std::cout << magnitude(3) << '\n';
return 0;
}
输入3时条件为假,执行会到右花括号,却没有提供所需int结果。这样的普通非void函数若实际走到末尾,行为未定义,不存在可以保证的输出。本章的Clang严格编译会用返回路径警告加-Werror拒绝它,我们只检查编译诊断,不运行坏路径。语言错误不等于任何编译器、任何选项都保证拒绝。 main有已学过的末尾等效return 0特例,不能把这个特例借给其他int函数。
没有要交回的值,也是一种明确接口
返回类型写作void,表示这次调用不提供可供保存的结果值。return;没有后面的表达式,可以提前结束这个void调用;正常走到void函数末尾也会回到调用者。return;不是“返回整数0”,不能把void调用保存成一个int。
#include <iostream>
void show_if_positive(int value) {
if (value <= 0) {
return;
}
std::cout << value << '\n';
}
int main() {
show_if_positive(0);
show_if_positive(3);
std::cout << "done" << '\n';
return 0;
}
第一次输入0,执行return;,跳过函数内输出,main接着发起第二次调用。输入3时条件假,输出3,走到void函数末尾返回;main随后输出done。完整输出为3、done。注意:函数没有返回值,却仍有输出这个可观察效果。
小练习:返回值不是退出码,void也不是零
在returns.cpp中,函数交回3后main为什么还能输出done?在void.cpp中,只把第一次实参0改为−2或2,各输出什么?说明“忽略一个int结果”与“调用没有结果的void函数”哪里不同。
提示与答案:先确定return所在的函数
return在magnitude内,只结束magnitude;main继续自己的下一步。void首个实参改为−2仍输出3、done,改为2则输出2、3、done。忽略int结果时调用确实产生了整数,只是调用者未保存;void调用本来就没有这样一个结果值。两种调用都可能实际执行函数体并输出。
04.4 局部作用域与调用过程 · 预估90分钟
同一个名字出现三次,怎样找到正在操作的对象
main有result,另一个函数也有result;函数内部还可能有嵌套块。仅靠名字做一张总表会把几个不同对象合并。我们先区分名字能在哪里用,再给每次活动调用保留独立的一页记录。
新03已教块内名字不能拿到块外使用。调用者的局部名字也不会因为发起调用就进入被调函数的作用域:next不能直接用main的局部名字x来访问那个对象,参数提供的是明确的输入接口。本节补充遮蔽(shadowing):更内层的块可以声明与外层相同的局部名字;从这个声明开始,在内层使用该名字会找到内层对象。外层对象没有因此被赋新值或消失,离开内层块后名字又对应外层对象。不要在函数体的最外层块再次声明与参数相同的名字;下例使用的是if引入的更内层块。
#include <iostream>
int adjusted(int value) {
int result{value};
if (value > 0) {
int value{9};
result += value;
std::cout << value << '\n';
}
std::cout << value << '\n';
return result;
}
int main() {
int x{4};
int result{adjusted(x)};
std::cout << x << ' ' << result << '\n';
return 0;
}
输入x=4时,adjusted的value为4,result也为4。进入if后声明另一个value,初值9;result += value;中的result在外层,value在内层,所以result变13。块内输出9。离开块后输出外层value的4,再交回result的13。main最后输出4 13,因此三行是9、4、4 13。这份教学例子故意使用同名变量让你辨认;自己的程序通常应选择更容易区分的名字。
一个函数还可以调用另一个函数
函数调用期间,调用者会等待被调用函数给出结果,再继续完成原语句。调用链(call chain)记录这种正在等待的关系;我们用调用栈(call stack)的逻辑图逐层表示当前工作。图只表达控制和各次调用的值,不保证编译器一定在内存中创建同形状的栈框,也不规定每个参数在某条机器指令时销毁。
#include <iostream>
int next(int value) {
return value + 1;
}
int twice_next(int value) {
int first{next(value)};
int second{next(first)};
return second;
}
int main() {
int result{twice_next(4)};
std::cout << result << '\n';
int again{twice_next(0)};
std::cout << again << '\n';
return 0;
}
第一次main调用twice_next(4):它的value是4;调用next(value)得到5,保存到first;再调用next(first)得到6,保存到second;最后把6交回main。每次next都有自己的参数,本次next读4,下次next读5,没有共享一个会累积的参数对象。
先完成内层调用,再回到等待的位置
main等待result的初始化;twice_next本次参数value得到4。
twice_next等待first的初值;next本次参数得到4,函数体将计算4+1。
next交回5;twice_next完成first的初始化,继续到second那条语句。
这是另一次next,输入来自first的5;新参数得到5,再计算加一。
twice_next用6完成second的初始化,接下来执行自己的return second。
twice_next交回6,main完成result的初始化,并输出6。没有跳回main开头。
main随后调用twice_next(0),这次得到first=1、second=2;返回2后初始化again并输出2,不承接前一次的局部值。
查看所有步骤的文字与数值
- 1 · main进入twice_next(4)
main:等待result;twice_next:value=4;next:尚未调用
main等待result的初始化;twice_next本次参数value得到4。
- 2 · 为first调用next(value)
main:仍在等待;twice_next:first待接收;next参数:value=4
twice_next等待first的初值;next本次参数得到4,函数体将计算4+1。
- 3 · 内层返回5,外层继续
main:仍在等待;twice_next:first=5;本次next:调用结束
next交回5;twice_next完成first的初始化,继续到second那条语句。
- 4 · 为second再调用next(first)
main:仍在等待;twice_next:second待接收;next参数:value=5
这是另一次next,输入来自first的5;新参数得到5,再计算加一。
- 5 · 内层返回6
main:仍在等待;twice_next:second=6;本次next:调用结束
twice_next用6完成second的初始化,接下来执行自己的return second。
- 6 · 外层返回6,main继续
main:result=6;twice_next:调用结束;终端输出:6
twice_next交回6,main完成result的初始化,并输出6。没有跳回main开头。
- 7 · 后一次从输入0重新开始
新一次外调用:输入0;本次中间值:first=1,second=2;随后输出:2
main随后调用twice_next(0),这次得到first=1、second=2;返回2后初始化again并输出2,不承接前一次的局部值。
第二次main调用twice_next(0),重新从value=0开始,first得1、second得2,main输出2。因此完整输出是6、2。没有使用递归;两条next调用写成两条完整语句,明确先完成first再计算second。
小练习:名字和调用分别记账
shadow.cpp只把main的x改为0,会建立内层value吗,最终哪两行会输出?nested-calls.cpp只把第一个twice_next的实参4改为1,后一次输入0的调用会被前一次影响吗?
提示与答案:分支不进入,内层声明也不执行
x=0使if条件假,没有建立内层value。函数输出外层0,再交回result的0,main输出0 0,共两行。嵌套调用第一个输入改为1后得到3,后一次仍从0开始得到2;输出3、2。各次调用的普通局部对象分别初始化,不会沿用前一次调用的局部整数。
04.5 声明、默认参数与普通重载 · 预估90分钟
调用处需要知道多少,才能把实参交对
前面总把完整函数体写在main上方。如果想把实现放后面,调用处仍需要事先知道接口。我们还会遇到两种方便写法:省略一个常用输入,以及让同名函数接收不同类型。它们都要先有明确规则,不能凭名字相同就猜调用哪一个。
**函数声明(function declaration)**可以只说明返回类型、名字和参数类型,以分号结束,不提供函数体。例如int next(int value);让后面的调用知道next接收int、返回int。定义包含实际函数体,也声明了函数;它负责提供具体工作。声明本身不会执行一次函数调用,也不会给main创建名为value的对象。
#include <iostream>
int next(int value);
int main() {
std::cout << next(4) << '\n';
return 0;
}
int next(int value) {
return value + 1;
}
这里main前先给声明,main后再给匹配的定义,运行仍输出5。声明让调用可以接受类型检查,但完整程序仍需要对应定义;把声明当作实现并不能让函数凭空产生结果。当前文件内先把两者区分清楚,多文件编译和链接会在新17成为正式练习。
少写一个实参,必须知道由什么补上
在参数后写= 1可以规定默认实参(default argument)。它表示调用处省略该位置时,使用指定的输入;不是每次进入函数都把明确传入的值强行改成1。调用从右侧省略:下面可以省略amount,不能省略value却只交amount。本节只有两个普通参数,默认值是一个小整数字面量。
#include <iostream>
int shifted(int value, int amount = 1) {
return value + amount;
}
int main() {
std::cout << shifted(4) << '\n';
std::cout << shifted(4, 3) << '\n';
return 0;
}
shifted(4)使用amount=1,返回5;shifted(4, 3)明确提供amount=3,返回7。只把定义中的默认1改为2,第一行变6、第二行仍7。只把第二次调用改为shifted(4, 0),在原默认1的版本里两行是5、4。
默认实参必须在调用处可见的声明中给出;本例定义在main前,所以符合要求。如果之后把声明和定义分开,只在选定的声明处给同一个默认值,不在相同作用域的定义里重复指定。这里说的是本章普通函数接口,尚未用模板或成员函数的额外规则。
同一个名字,按实参类型选择
**普通函数重载(function overloading)**允许同一名字有不同的参数列表。调用处先根据可见候选及实参类型选择函数,再执行所选函数体。本节仅比较一个int参数与一个double参数,实参也恰好是int或double;无需靠最终输出去猜编译时选了谁。
#include <iostream>
int half(int value) {
return value / 2;
}
double half(double value) {
return value / 2.0;
}
int main() {
std::cout << half(5) << '\n';
std::cout << half(5.0) << '\n';
return 0;
}
整数字面量5类型是int,所以half(5)匹配int版本,整数除法结果为2。5.0类型是double,匹配double版本,结果2.5。转换和整数除法来自新02;这里的新规则是根据实参类型选择不同函数体。若把整数实参改为6,只有第一行变3;把double实参改为6.0,只有第二行变3。
不能仅凭返回类型区分两个普通函数:下面两个half的参数列表都是单个int,第二个声明为返回double,不能把它变成另一份合法重载。
#include <iostream>
int half(int value) {
return value / 2;
}
double half(int value) {
return value / 2.0;
}
int main() {
std::cout << half(5) << '\n';
return 0;
}
这份文件预期编译拒绝。main接收结果用什么类型也不能修复这两个冲突的定义;应当让接口有合理区别,或只保留符合需求的一份函数。本章不做混杂许多隐式转换的候选选择题,所用重载只需上面的精确类型匹配。
小练习:三个看似相近的接口动作
说明声明、默认实参、重载各解决什么问题。defaults.cpp中把默认1改为2后,为什么显式传3的结果不变?为什么不能用double接收变量来修复return-overload-error.cpp?
提示与答案:依次看可见性、有没有省略、参数类型
声明让调用处先知道接口;默认实参给省略的右侧输入提供值;重载让不同参数列表共用名字。明确提供3时不使用默认值,仍计算4+3=7。只改接收变量的类型没有改变那两个函数冲突的参数列表,不能把非法的定义集合变合法。
04.6 函数合同与小检查 · 预估60分钟
结果为零时,怎样知道它是答案而不是失败
现在可以把上一章的循环装进函数,但调用者还需要知道哪些输入可接受、结果代表什么,以及失败时怎么识别。函数名和int返回类型都不能独自说明这些约定。我们把这些约定写在计算之前,再用已学if检查它们。
**函数合同(function contract)在这里指清楚写下的输入范围、成功结果和失败表示,不是某个额外C++关键字。对sum_before,我们约定:0≤n≤100时返回0到n减1的总和;范围外返回−1。合法输入的结果最小为0,最大为0+1+…+99=4950,因此−1不会与合法答案混淆。这个失败码(error code)**是函数自己的约定,与main的进程退出状态另算。
完整例二:让调用者检查一个有边界的函数
#include <iostream>
int sum_before(int n) {
if (n < 0 || n > 100) {
return -1;
}
int total{0};
for (int i{0}; i < n; ++i) {
total += i;
}
return total;
}
int main() {
int n{4};
int result{sum_before(n)};
if (result < 0) {
std::cout << "invalid" << '\n';
return 1;
}
if (sum_before(0) != 0 || sum_before(1) != 0) {
std::cout << "check failed" << '\n';
return 1;
}
std::cout << result << '\n';
return 0;
}
输入4时先通过范围检查,total从0开始,普通for处理0、1、2、3,依次累计0、1、3、6,return交回6。main的result为6,不走invalid分支;随后用两次调用检查sum_before(0)和sum_before(1)都为0,最后输出6并退出0。
这两条小检查用的是新03的比较和短路:发现任意一个结果不符,就输出check failed并让main退出1。检查不是因为函数名看起来正确就通过,而是比较真实调用结果。没有使用尚未教过的断言宏、异常或容器。
只把main的n改为0、1或100,输出分别0、0、4950,程序退出0。改为−1或101,sum_before先返回−1,不进入循环;main识别负值,输出invalid并退出1。函数返回−1没有直接结束整个程序,是main读到失败码后选择return 1。
阶梯第二步:把已理解的循环提取成函数
关掉完整例,从新03已懂的累计过程出发,在新文件中定义sum_before,接收一个int n并返回int。先限定调用只提供0到100,再自己写清总数在哪里初始化、循环结束后怎样交回。main分别检查0、1和4,结果应为0、0、6。完成后再按上面合同补范围拒绝。
提示与答案:输入放在参数,结果离开循环后交回
接口是int sum_before(int n),total属于每一次调用。把原来的main内n初始化改成形参输入,循环仍用i从0到n之前,return total放在循环之后。正常范围输出0、0、6;补拒绝后在total和循环之前检查范围并返回−1。完整例中的函数就是一份可对照实现,但先完成自己的文件。
阶梯第三步:每条路径都有返回,还可能早得不对
下面能编译,每一条结束路径也都交回整数。输入4,合同仍要求总和6;先预测实际结果,再找出导致差异的那一条return。
#include <iostream>
int sum_before(int n) {
int total{0};
for (int i{0}; i < n; ++i) {
total += i;
return total;
}
return total;
}
int main() {
int result{sum_before(4)};
std::cout << result << '\n';
return 0;
}
提示与答案:第一轮后还有哪些整数没处理
第一轮i=0,total还是0,内层return立刻交回0,后面的1、2、3都没有处理。修复是移除循环体内的提前return,保留函数末尾已有的return;结果变6。仍要检查n=0与n=1:两者正确总和都为0。仅拿n=0测试坏版本也得到0,会掩盖“过早结束”的逻辑错误。这里的错误不是缺返回,更不是编译器负责理解了你的求和合同。
阶梯第四步:从空文件独立迁移
定义count_even_before:0≤n≤100时统计0到n减1中的偶数,范围外返回−1。main至少发起两次调用,每次都说明本次局部count如何初始化。建议依次用0、1、5,再重复一次5;四个结果应为0、1、3、3。不要把上一章main整段原封不动复制四遍。
提示:每次调用都从自己的count=0开始
先检查输入范围,再创建本次count,普通for沿半开区间走,每逢i的余数为0就给count加1,最后return count。调用者可以分别保存结果再输出,便于区分四次调用。
参考实现与答案:完成自己的版本后再展开
#include <iostream>
int count_even_before(int n) {
if (n < 0 || n > 100) {
return -1;
}
int count{0};
for (int i{0}; i < n; ++i) {
if (i % 2 == 0) {
count += 1;
}
}
return count;
}
int main() {
int first{count_even_before(0)};
int second{count_even_before(1)};
int third{count_even_before(5)};
int again{count_even_before(5)};
std::cout << first << '\n';
std::cout << second << '\n';
std::cout << third << '\n';
std::cout << again << '\n';
return 0;
}四行依次为0、1、3、3。第二次的count不接着第一次累计,第四次也不会因为前一次输入5而从3开始。只把最后一次输入改为1,前三行不变,第四行变1。把第一次输入改为100,第一行变50,后面仍是1、3、3。
还可把第一次输入改为−1或101,函数交回−1,第一行就显示−1,后面三行仍为1、3、3。这份main只是展示各次返回值,没有把负值翻译成进程失败状态,所以仍会走到自己的return 0。 若产品需求是非法输入立即结束程序,应像sum-before.cpp一样由调用者明确判断并选择退出;不能把两个示例的main合同混用。
学完后,回答章首的问题:调用表达式提供一次入口,参数给这次计算独立输入,return交回结果;调用者继续执行,并负责决定怎样保存或检查结果。接下来到页面末尾关掉参考做复述,留下自己的源码、预测和实际输出。
关掉参考,再做一次
把理解说出来
先完成正文的独立迁移,再回答下面六题。它们只检验本章内容;打开答案、编译成功或填写用时,都不会自动通过本章,更不代表通过 G0。
定义与路径
next写在main前,是否先执行?int y{next(4)};中哪一步初始化y,哪一步才输出?
对照推理与英文回答
程序从main开始。调用进入next,本次参数得到4,return交回5后y完成初始化;只有cout才输出。定义提供规则,不会因为在文件前就先运行。
Execution starts in main. The call initializes the parameter, runs next, and produces five to initialize y. Returning a value does not print it. The definition appearing first does not make it run first.代码预测与找错
x=4时,保存next(x)到y、单独next(x);、最后x=next(x),各自改变什么?
对照推理与英文回答
每次参数独立从当前x取值。三次调用后输出分别4 5、4 5、5 5。丢弃结果没有取消调用;x最后变化来自调用者的赋值,而非参数直接写回。
Each call receives its own integer parameter. Saving the result initializes y. Ignoring the result still executes the call. Only the explicit assignment in the caller changes x to five.返回与失败边界
普通int函数实际漏返回、普通函数返回3、void的return;,这三件事分别意味着什么?
对照推理与英文回答
普通非void函数若实际正常走到末尾而没有返回是未定义行为,不默认给0。普通函数返回3只把结果交回调用者,main可继续并退出0。void的return;结束当前调用且不提供整数结果。
Falling off an ordinary non-void function has undefined behavior. Returning three from a helper passes a value to its caller; it does not set the process exit code. A bare return from a void function provides no result value.名字与连续调用
shadow例的内层value=9会改外层4吗?twice_next(4)后再twice_next(0),为什么不是继续从6算?
对照推理与英文回答
内层声明遮蔽外层名字,但没有自动赋值给外层。结果累计使用内层9后是13,外层value仍4。新的twice_next从新的参数0开始,各次局部变量分别初始化,因此两次结果6、2。调用图只表达逻辑继续点。
Shadowing introduces a separate local object. It does not overwrite the outer value. A later call initializes its own parameter and locals, so the two results are six and two. The diagram models control flow, not a required physical stack layout.接口选择与追问
声明、默认实参、重载分别做什么?shifted(4,3)会用默认1吗?half(5)为何选int版本?
对照推理与英文回答
声明在调用前给出可见接口;默认实参只补省略的尾部输入;重载根据参数列表与实参类型选候选。显式3覆盖省略需求,返回7。这里只有int和double两个单参候选,5为int,精确匹配int版本。只改变返回类型不能形成合法重载。
A declaration makes the interface visible. A default argument supplies an omitted trailing input. Overload resolution selects a function from the argument types. An explicit three is used as supplied. With these two candidates, an int argument matches the int overload. Return type alone cannot distinguish overloads.合同与独立解释
sum_before(0)=0是失败吗?count_even_before返回-1会自动让进程退出失败吗?为什么过早return的n=0测试会漏错?
对照推理与英文回答
0是合法空区间总和;-1按函数合同表示拒绝,但调用者必须自己判断再选择退出状态。计偶数展示程序仅输出返回值,所以非法变体仍退出0。错误早退程序n=0不进循环也返回0,需n=4才能暴露遗漏1、2、3。
Zero is a valid empty-range sum. Minus one is a function-level failure marker; the caller decides how to handle it and what process status to return. An empty input can hide an early-return bug, so a nonempty case is also necessary.和正文是同一份源码
示例文件
先自己输入和预测,卡住时再下载对照。文件名相同不代表内容相同;把它们放在单独的练习目录中,避免覆盖自己的作品。
- definition.cpp定义与调用:跟踪一次next
- value-copy.cpp完整例一:保存、丢弃与显式赋回
- returns.cpp普通函数返回后,main继续
- missing-return.cpp故意编译失败:缺少正常返回路径 · 故意用于编译报错
- void.cppvoid没有结果值,仍可以执行输出
- shadow.cpp同名遮蔽:外层与内层分别记账
- nested-calls.cpp逐句嵌套调用与两次独立工作
- declarations.cpp先给声明,再在main后定义
- defaults.cpp省略尾部输入时的默认实参
- overloads.cppint与double精确匹配两份函数
- return-overload-error.cpp故意编译失败:只改返回类型 · 故意用于编译报错
- sum-before.cpp完整例二:有范围和失败码的累计
- early-return.cpp能编译的错误:第一轮过早返回
- count-even-before.cpp独立迁移:四次调用各自计数
完整编译、输出和失败路径验证随本地构建记录保留;这里的程序没有调用加速卡。
可选的学习反馈
记下你真正花的时间
每完成一个学习时段,再填实际分钟。环境准备、阅读推演、独立编码和卡点排查分别记录,避免同一段时间重复计算。离开吃饭或做其他事情的时间不算进去。
记录只保存在你的浏览器,可导出给我复盘。留空表示尚未记录,不等于零耗时;页面停留时间不会自动计为学习。不要把开发者检查时间填进来。
尚无真实试学用时。
换设备:导入记录,或取回损坏的旧记录
导入会合并时段,相同编号不重复累加;发生冲突会保留现有记录。
阅读记录与课程验收分别保存。
本章资料与查证
- N4861 C++20草案:函数接口与参数 ↗
04.1;返回类型、参数列表与名称。 固定版本文本的HTML镜像,并非ISO官网。
- N4861 C++20草案:普通函数定义 ↗
04.1;定义位于普通块外,函数体的规则。 固定版本文本的HTML镜像,并非ISO官网。
- N4861 C++20草案:实参、参数与调用 ↗
04.1–04.2;实参初始化参数、参数对象的适用边界。 固定版本文本的HTML镜像,并非ISO官网。
- N4861 C++20草案:多个实参的求值边界 ↗
04.1与04.4;不推断从左到右的求值顺序。 固定版本文本的HTML镜像,并非ISO官网。
- N4861 C++20草案:返回到调用者 ↗
04.1–04.3;控制与返回结果。 固定版本文本的HTML镜像,并非ISO官网。
- N4861 C++20草案:void、结果和末尾返回 ↗
04.3;普通非void漏返回及void正常到尾。 固定版本文本的HTML镜像,并非ISO官网。
- N4861 C++20草案:main的返回特例 ↗
04.3;区别普通函数结果与程序结束。 固定版本文本的HTML镜像,并非ISO官网。
- N4861 C++20草案:块作用域与遮蔽 ↗
04.4;同名内外层对象。 固定版本文本的HTML镜像,并非ISO官网。
- N4861 C++20草案:参数名的作用域 ↗
04.4;形参不能在函数体最外层再声明。 固定版本文本的HTML镜像,并非ISO官网。
- N4861 C++20草案:局部声明的执行 ↗
04.4;每次执行声明重新初始化本次局部对象。 固定版本文本的HTML镜像,并非ISO官网。
- N4861 C++20草案:抽象机与实现 ↗
04.4;逻辑调用图不规定物理栈布局。 固定版本文本的HTML镜像,并非ISO官网。
- N4861 C++20草案:声明与定义 ↗
04.5;定义本身也是声明。 固定版本文本的HTML镜像,并非ISO官网。
- N4861 C++20草案:默认实参 ↗
04.5;省略尾部输入时补值。 固定版本文本的HTML镜像,并非ISO官网。
- N4861 C++20草案:默认实参的重复声明边界 ↗
04.5;在选定可见声明给值,不重复指定。 固定版本文本的HTML镜像,并非ISO官网。
- N4861 C++20草案:标准转换的匹配等级 ↗
04.5;本章int/double两个候选的精确匹配。 固定版本文本的HTML镜像,并非ISO官网。
- N4861 C++20草案:不能仅按返回类型重载 ↗
04.5;同参数列表的冲突。 固定版本文本的HTML镜像,并非ISO官网。
- Clang官方:返回路径诊断 ↗
04.3;工具警告与语言规则区分,实际编译器版本看本地执行记录。
本章独立解释所需读法;资料用于核对与补充。工具版本、操作系统和实际执行状态见自己的运行记录。