Chapter 7:Functions: C++'s Programming Modules

对齐第6版 Chapter 7:掌握函数原型、按值传递、数组/二维数组/C 字符串/结构体参数、递归与函数指针,并用前后置条件验证调用。

学习目标

  • 能解释函数原型、调用和定义各自提供的证据,并写出参数范围、返回值、副作用与失败语义
  • 能实现函数与数组、二维数组、C 字符串、结构体、string/array 对象的参数契约,指出传递后丢失的形状信息
  • 能推导递归的基例、推进与栈深度,并使用函数指针把满足同一签名的操作作为参数传递

机制总览

Chapter 7:Functions: C++'s Programming Modules:机制路径

  1. 1

    为什么函数是契约而不只是代码片段

    函数把一项工作命名并形成调用边界。编译器能通过函数原型检查参数和返回类型,却不知道 count 是否匹配数组、指针是否可空、分数是否在 0–100、失败怎样报告。这些语义需要前置条件、后置条件和测试补齐。

  2. 2

    函数原型让调用点可被独立检查

    函数原型声明名字、返回类型和参数类型。调用出现时,编译器据此检查实参数量与可转换性;定义可以位于另一个源文件,最终由链接器解析。

  3. 3

    按值传递建立独立形参对象

    普通参数按值传递:调用表达式先求实参值,再用它初始化函数调用帧中的形参。修改形参不改变调用者对象,除非值本身是指针且函数通过该地址修改 pointee。

先按顺序建立机制,再进入实验切换阶段并检查失效证据。

章级决策实验

Chapter 7:Functions: C++'s Programming Modules:机制与证据

切换《Chapter 7:Functions: C++'s Programming Modules》的三个关键教学阶段,先解释机制,再用运行与失败证据验证结论。

选择推理阶段

当前阶段 · 为什么函数是契约而不只是代码片段

函数把一项工作命名并形成调用边界。编译器能通过函数原型检查参数和返回类型,却不知道 count 是否匹配数组、指针是否可空、分数是否在 0–100、失败怎样报告。这些语义需要前置条件、后置条件和测试补齐。

可核验证据

从干净构建开始,以固定输入运行本节示例,再加入一个边界或故障场景验证「为什么函数是契约而不只是代码片段」的状态变化。

学完《Chapter 7:Functions: C++'s Programming Modules》后,应能从输入和前置条件推导状态变化,并用可重复的构建、运行或边界测试证明结果。

失效—证据矩阵

Chapter 7:Functions: C++'s Programming Modules:失效与核验

为什么函数是契约而不只是代码片段

典型失效

若只复述「为什么函数是契约而不只是代码片段」结论而不追踪状态、所有权和失败路径,示例扩展成多文件或多对象程序后就容易偏离预期。

核验证据

从干净构建开始,以固定输入运行本节示例,再加入一个边界或故障场景验证「为什么函数是契约而不只是代码片段」的状态变化。

函数原型让调用点可被独立检查

典型失效

若只复述「函数原型让调用点可被独立检查」结论而不追踪状态、所有权和失败路径,示例扩展成多文件或多对象程序后就容易偏离预期。

核验证据

从干净构建开始,以固定输入运行本节示例,再加入一个边界或故障场景验证「函数原型让调用点可被独立检查」的状态变化。

按值传递建立独立形参对象

典型失效

若只复述「按值传递建立独立形参对象」结论而不追踪状态、所有权和失败路径,示例扩展成多文件或多对象程序后就容易偏离预期。

核验证据

从干净构建开始,以固定输入运行本节示例,再加入一个边界或故障场景验证「按值传递建立独立形参对象」的状态变化。

每个判断都必须能落到观测、测试或产物,不能只凭代码表面推测。

为什么函数是契约而不只是代码片段

函数把一项工作命名并形成调用边界。编译器能通过函数原型检查参数和返回类型,却不知道 count 是否匹配数组、指针是否可空、分数是否在 0–100、失败怎样报告。这些语义需要前置条件、后置条件和测试补齐。

函数原型让调用点可被独立检查

函数原型声明名字、返回类型和参数类型。调用出现时,编译器据此检查实参数量与可转换性;定义可以位于另一个源文件,最终由链接器解析。

double average(const double values[], int count);  // prototype
 
int main() {
    double samples[3]{2.0, 4.0, 9.0};
    std::cout << average(samples, 3) << '\n';
}

参数名在原型中可省略,但写出有意义的名字能帮助读者理解单位和方向。声明与定义必须表示同一函数;返回类型差异或命名空间不一致可能让调用编译、链接却找不到匹配定义。

按值传递建立独立形参对象

普通参数按值传递:调用表达式先求实参值,再用它初始化函数调用帧中的形参。修改形参不改变调用者对象,除非值本身是指针且函数通过该地址修改 pointee。

int increment(int value) {
    return value + 1;
}
 
int original{4};
int next{increment(original)};  // original == 4, next == 5

大 struct 按值会复制成员,但有独立性;只读大对象可在 Chapter 8 使用 const 引用。不要仅因“复制慢”把所有参数改指针,指针还引入可空、别名和生命周期契约。

数组参数会调整为指针并丢失长度

在函数参数中,double values[]double* values 表示同一种参数形状。sizeof(values) 得到指针大小,不是调用数组总字节;函数必须另收 count,或使用携带长度的容器/视图。

double average(const double values[], int count) {
    if (count <= 0) {
        throw std::invalid_argument{"count must be positive"};
    }
    double total{0.0};
    for (int i{0}; i < count; ++i) {
        total += values[i];
    }
    return total / count;
}

前置条件包括 values 指向至少 count 个存活 double、count 为正、每次访问在 [0,count)const 防止通过该参数修改元素,但不证明非空和长度。

二维数组参数必须保留内层尺度

int grid[][3] 调整为“指向含 3 个 int 的数组”的指针,列数 3 是指针步长的一部分,外层行数仍需另传。row/col 两层范围分别验证。

int sum_grid(const int grid[][3], int rows) {
    int total{0};
    for (int row{0}; row < rows; ++row) {
        for (int col{0}; col < 3; ++col) {
            total += grid[row][col];
        }
    }
    return total;
}

若行列都运行时变化,内置二维数组参数不再合适;可以使用平坦 vector 加显式 strides,或封装矩阵类型让维度与数据共同移动。

C 字符串、struct 与标准对象保留不同信息

C 字符串参数通常是 const char*,长度靠 \0 扫描,无法知道缓冲容量。struct 按值传递会复制全部成员;指向 struct 的指针只传地址。std::string 按值携带自己的长度和所有权,std::array 引用还能在类型中保留固定尺度。

设计接口时问:函数真正需要一个值、一个只读记录、一个可写对象、一个地址范围,还是一个文本视图?把所有情况都写成裸指针会抹平差异并增加前置条件。

递归把问题缩小到基例

递归函数直接或间接调用自身。正确递归需要可立即回答的基例,以及每次调用严格接近基例的推进量;否则调用帧持续增长直到栈资源耗尽。

unsigned long long factorial(unsigned n) {
    if (n <= 1) return 1;           // base case
    return n * factorial(n - 1);    // progress
}

数学递归正确不等于机器表示安全:factorial 很快溢出,n 来源也要限制。先预测调用树和最大深度,再测试 0、1、2、上界与上界外输入。

函数指针把操作选择变成参数

函数也有地址。函数指针类型包含返回类型和参数列表,调用者可把满足签名的目标传给高阶函数,实现排序比较、数值变换或回调。

int square(int value) { return value * value; }
int negate(int value) { return -value; }
 
int apply(int value, int (*operation)(int)) {
    return operation(value);
}
 
int result = apply(5, &square);

签名一致只证明类型形状,不证明目标语义、异常、副作用或线程要求。间接调用需要四套证据:签名匹配、目标非空、目标代码仍可执行,以及调用效果符合约定。

先预测调用前后对象值、参数帧、范围和返回结果,再执行。对递归画出每一帧 n,对函数指针记录实际目标;“进入了同一个函数”不能说明复制、间接调用和范围契约相同。

三步验收一个函数模块

分步1 / 3

第一步:写原型与语义契约

在调用前声明签名,并补充合法域、结果、副作用、所有权和失败行为;从同一头文件供声明与定义使用。

小结

  • 函数原型提供编译期调用检查,完整契约还需范围、副作用、所有权和失败语义
  • 按值传递建立独立参数,函数内修改不直接写回调用者
  • 数组参数调整为首元素 pointer 并丢失长度,二维数组只在类型中保留内层维度
  • C 字符串、string、array 和 struct 参数携带不同形状,参数形式应反映容量与写权限
  • 递归靠基例和推进终止,函数 pointer 靠完整签名与目标生命周期安全间接调用

练习

  1. 问题 1:数组契约。 mean(values,count) 对 count=0、null 和普通范围应怎样规定?
  1. 问题 2:二维参数。 为什么 const int (*grid)[3] 仍要另传 rows?
  1. 问题 3:递归与 callback。 设计 factorial 和 apply 的最小故障测试。

名词解释

名词解释

本章出现的专业名词,用大白话再讲一遍。

函数契约
函数调用前置条件、返回后保证、副作用与失败语义的约定。
函数原型
声明函数名字、参数类型与返回类型以检查调用的接口。
按值传递
以实参值初始化独立形参对象的传参机制。
数组参数调整
数组形参转换为首元素 pointer、长度不随类型携带的规则。
参数形状
调用边界仍保留的类型、维度、长度、容量和组合信息。
递归
函数调用自身并以基例和推进量终止的控制结构。
函数指针
保存匹配函数地址并支持间接调用的 pointer 类型。

讨论

评论区加载中…