从零开始 / 一次只解释眼前的一步

普通函数:
参数、调用与返回。

怎样复用一段计算,同时说清输入和输出?把调用者、这次参数和返回结果分开记录,自己写出可检查的计数函数。

先备:基础01–036 小节 · 预计共 8 小时阅读、推演与编码合计

8小时是未经真人试学校准的设计估算。可分多次学习,遇到不清楚的地方保留预测、实际结果和疑问,之后再回修教材。手机可读图与做预测,编译需要电脑终端。

这一章怎样学

先读一小段,写下预测,再运行程序。每次只改一个条件,最后关掉示例,从空文件独立写一次。遇到错误,把第一条报错和自己的修复记下来;不用赶着把页面滚到底。

每次先指出当前在哪一次调用、正在操作谁的对象,再记录返回后继续的位置。每章学习记录单独保留;本章完成不自动通过M1或G0。

上一章能在main中完成一次计数。现在,同样的计算要用于0、1和4三份输入:是复制三遍循环,还是给这段工作一个名字?这一章要解决的是:一次计算怎样接收输入、交出结果,再让调用它的地方继续执行?

先备是新03:控制流。继续使用前三章的编译流程、标量、算术、分支和普通循环。每个下载文件各有自己的main,分别编译运行;不要把本章14个文件一起交给一次链接。

六节预估8小时,包含阅读、预测和编码,仍未经真人试学校准。先沿着一次调用走完,再比较两次调用。前五节的小整数例子限定在−100到100,保证本章的加减、取负和除法结果可表示;两份计数函数另外明确0到100的输入合同。

04.1 定义与调用 · 预估90分钟

给一段计算起名字之后,程序何时才会执行它

你想把“得到下一个整数”用在不同位置。把几行代码放进花括号还不够:调用者需要知道它叫什么、要交什么进去、能得到什么。这一节先读函数的外形,再追踪控制从哪里离开、又回到哪里。

本章采用一套简单的命名方式:以小写英文字母开头,后续可以放字母、数字和内部下划线_,例如nextsum_beforecount_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;
}
  1. 运行进入main,遇到result的初始化,需要先得到next(4)的值。
  2. 进入next,本次参数value得到4;计算value + 1得5。
  3. return把5交回,result的初始化完成,result为5。
  4. 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
停下来,看一次变化

参数修改与调用者赋值是两件事

main的x4本次next尚未进入main的y等待结果1 / 6 · 参数与结果main的x4本次next尚未进入main的y等待结果1 / 6
1 · main准备接收第一次结果

x已经初始化为4;y的初始化需要先完成next(x),此时没有可读取的y结果。

1 / 6
查看所有步骤的文字与数值
  1. 1 · main准备接收第一次结果

    main的x:4;本次next:尚未进入;main的y:等待结果

    x已经初始化为4;y的初始化需要先完成next(x),此时没有可读取的y结果。

  2. 2 · 用当前x初始化本次参数

    main的x:4;next参数:value=4;main的y:等待结果

    读取x的4,把4交给本次整数参数value。两个状态分别记录。

  3. 3 · 在next内更新参数

    main的x:4;next参数:value=5;main的y:等待结果

    value += 1使本次value变5,没有语句要求修改main的x。

  4. 4 · 返回5,完成y的初始化

    main的x:4;main的y:5;终端输出:4 5

    return交回5,main用它初始化y,然后输出4 5;这次调用已经结束。图不描述参数的物理存储或精确销毁时点。

  5. 5 · 第二次调用,结果未保存

    main的x:4;main的y:5;这次结果:5被丢弃

    单独next(x)仍从4开始计算5,但返回结果没有保存;main再输出4 5。

  6. 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 54 55 5。改为0后三行为0 10 11 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、done3、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等待resulttwice_nextvalue=4next尚未调用1 / 7 · 逻辑调用过程main等待resulttwice_nextvalue=4next尚未调用1 / 7
1 · main进入twice_next(4)

main等待result的初始化;twice_next本次参数value得到4。

1 / 7
查看所有步骤的文字与数值
  1. 1 · main进入twice_next(4)

    main:等待result;twice_next:value=4;next:尚未调用

    main等待result的初始化;twice_next本次参数value得到4。

  2. 2 · 为first调用next(value)

    main:仍在等待;twice_next:first待接收;next参数:value=4

    twice_next等待first的初值;next本次参数得到4,函数体将计算4+1。

  3. 3 · 内层返回5,外层继续

    main:仍在等待;twice_next:first=5;本次next:调用结束

    next交回5;twice_next完成first的初始化,继续到second那条语句。

  4. 4 · 为second再调用next(first)

    main:仍在等待;twice_next:second待接收;next参数:value=5

    这是另一次next,输入来自first的5;新参数得到5,再计算加一。

  5. 5 · 内层返回6

    main:仍在等待;twice_next:second=6;本次next:调用结束

    twice_next用6完成second的初始化,接下来执行自己的return second。

  6. 6 · 外层返回6,main继续

    main:result=6;twice_next:调用结束;终端输出:6

    twice_next交回6,main完成result的初始化,并输出6。没有跳回main开头。

  7. 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.

和正文是同一份源码

示例文件

先自己输入和预测,卡住时再下载对照。文件名相同不代表内容相同;把它们放在单独的练习目录中,避免覆盖自己的作品。

完整编译、输出和失败路径验证随本地构建记录保留;这里的程序没有调用加速卡。

可选的学习反馈

记下你真正花的时间

每完成一个学习时段,再填实际分钟。环境准备、阅读推演、独立编码和卡点排查分别记录,避免同一段时间重复计算。离开吃饭或做其他事情的时间不算进去。

记录只保存在你的浏览器,可导出给我复盘。留空表示尚未记录,不等于零耗时;页面停留时间不会自动计为学习。不要把开发者检查时间填进来。

尚无真实试学用时。

    按开始学习本章前的情况选择;已经会 C++ 时选择“会 C++”。起点随每条时段保存,之后改选不会重标旧记录。旧记录缺少起点时单独保留;已有基础者的用时不用于校准零基础预算。

      换设备:导入记录,或取回损坏的旧记录

      导入会合并时段,相同编号不重复累加;发生冲突会保留现有记录。

      阅读记录与课程验收分别保存。

      本章资料与查证

      本章独立解释所需读法;资料用于核对与补充。工具版本、操作系统和实际执行状态见自己的运行记录。