这一章在干嘛?
前面各章我们一直在”用”函数(比如
strlen()),这一章开始自己”造”函数。C++ 程序就像乐高积木:main()是总装车间,函数是一个个标准零件。本章把零件的定义、原型、调用三件套讲清,再逐一攻克处理数组、字符串、结构体的函数,以及两个进阶玩法:函数调用自己的递归,和把函数当参数传的函数指针。
7.1 函数复习
使用一个 C++ 函数,要做三件事:提供函数定义(真正的可执行代码)、提供函数原型(给编译器的说明书)、调用函数。用库函数时前两件别人已做好(如 cstring 头文件里就有 strlen() 的原型),自己写函数则三件都得亲自动手:
// calling.cpp -- 定义、原型和调用一个函数
#include <iostream>
void simple(); // 函数原型
int main()
{
using namespace std;
cout << "main() will call the simple() function:\n";
simple(); // 函数调用
cout << "main() is finished with the simple() function.\n";
return 0;
}
// 函数定义
void simple()
{
using namespace std;
cout << "I'm but a simple function.\n";
}执行到 simple(); 时 main() 暂停、控制权转交,干完活后控制权回到 main() 接着走——就像你读菜谱做到一半打电话问朋友,朋友说完挂断,你从刚才那一步继续。
7.2 定义函数与原型
void 函数(无返回值)通用格式:void functionName(parameterList) { statement(s); return; },return 可省。有返回值的函数必须用 return 把值交回调用者,值的类型要能转换成声明的返回类型:
void cheers(int n) // 无返回值
{
for (int i = 0; i < n; i++)
std::cout << "Cheers! ";
std::cout << std::endl;
}
double cube(double x) // x 乘 x 再乘 x
{
return x * x * x; // 一个 double 值
}cube(1.2) 的”值”就是 1.728。唯一不能当返回值的类型是数组——不过结构体、对象、指针都行(数组可以塞进结构体里曲线返回)。多个 return 时执行到第一个就退出,后面的 else 写出来只为方便阅读。返回值的物理机制是:把返回值复制到约定的 CPU 寄存器或内存位置,调用方按原型说明的类型去取——原型和定义必须一致,就像给快递员和公司前台各留一份相同的取件描述。

▲ 图 7.1 典型的返回值机制
原型为什么必须存在
原型向编译器描述接口:返回值类型、参数个数与类型。以 double volume = cube(side); 为例:原型让编译器检查参数是否给对,并知道函数返回时该”取几个字节、怎么解释”。编译器不能靠”往下翻文件看定义”代替原型:一是效率太低,二是函数可能根本不在本文件里(C++ 允许程序拆成多个独立编译的文件,或函数在库中)。所以必须靠原型提前”挂号”,这也是 C++ 惯例把 main() 放最前面的原因。
原型语法与它给你的保护
原型就是”函数头 + 分号”,参数名可省:double cube(double x);、void cheers(int); 都行。C++ 中空括号 void say_hi(); 等价于 void say_hi(void); 表示无参;ANSI C 中它却表示”懒得声明参数”。C++ 里真正的”不管参数”要用省略号 void say_bye(...);,一般只在对接 printf() 这类变参 C 函数时用。
原型带来三大保护:返回值被正确处理、参数个数被检查、参数类型不匹配时自动转换。例如 cheers(cube(2));:2 先转成 2.0 传给 cube(),返回的 8.0 又转成 int 8 传给 cheers()。当然转换不是万能的——把 8.33E27 塞给 int 会丢数据,整数也永远不会被自动转成结构体或指针。这种编译期检查叫静态类型检查,能把许多运行时才爆的雷提前排掉。
7.3 按值传递
C++ 默认按值传递(pass by value):把实参的值复制一份,赋给函数里新建的变量。调用 cube(side)(side 为 5)时,函数创建新变量 x 并初始化为 5,之后函数玩的是”复印件”,原件毫发无伤。术语约定:传进来的叫实参(argument),接收它的叫形参(parameter),实参传递的本质是”用实参初始化形参”。

▲ 图 7.2 按值传递
包括形参在内,函数内声明的变量都是局部变量:调用时分配内存,函数结束就释放。main() 里的 x 和别的函数里的 x 是两个毫不相干的变量,就像加州的 Albany 和纽约州的 Albany 重名但无关。

▲ 图 7.3 局部变量
多个参数用逗号隔开;注意同类型参数也必须逐个声明,void fufu(float a, b) 是非法的。核心示例 Listing 7.3(twoarg.cpp):n_chars(ch, times) 打印字符 ch 共 times 遍,函数体内 while (n-- > 0) cout << c; 把形参 n 从 times 减到 0,可结束后 main() 打印 times 仍是原值——函数里怎么折腾形参,都动不到外面的实参。顺带一提:读字符用 cin >> ch 而不是 cin.get(ch),因为 >> 会自动跳过空格和换行,而按回车产生的换行符会被 cin.get() 当成有效输入。
void n_chars(char c, int n) // 显示字符 c 共 n 次
{
while (n-- > 0)
cout << c;
}再来个有点数学味的例子
计算彩票中奖概率:从 51 个数里选 6 个,中奖概率是 1/R,其中 R = (51×50×…×46)/(6×5×…×1)。技巧是边乘边除,让中间结果保持小、避免溢出:(10/2)×(9/1) 和 (10×9)/(2×1) 结果一样,但前者中间值只有 5,后者冲到 90。
// lotto.cpp -- 中奖概率(main() 循环读入两个数并调用,此处展示核心函数)
long double probability(unsigned numbers, unsigned picks)
{
long double result = 1.0; // 局部变量
long double n;
unsigned p;
for (n = numbers, p = picks; p > 0; n--, p--)
result = result * n / p;
return result;
}原书 main() 里循环读入 total 和 choices 并调用 probability(total, choices),输入 49 和 6 得到 You have one chance in 1.39838e+007 of winning.——选择越多,中奖越渺茫。这个函数展示了两种局部变量:形参(numbers、picks)从调用者那里拿值,普通局部变量(result、n、p)在函数体内自己产生。
常见坑:以为改形参就能改实参
按值传递下,形参是复印件。想真正修改调用者的数据,得传地址(指针)或引用(第 8 章),而不是祈祷函数里改
n能让外面的times变。
7.4 函数和数组
数组名在大多数语境下会被 C++ 解释为首元素地址:cookies == &cookies[0](例外:数组声明本身、sizeof 数组名 得整个数组大小、&数组名 得整个数组的地址)。因此把数组传给函数,传的其实是地址,函数头写法:
int sum_arr(int arr[], int n) // arr = 数组名,n = 大小方括号只是”提示性记号”——它真正的含义是 int *arr,两者只在函数头/原型中完全等价。函数体里照样可以像数组一样用 arr[i],所以 Listing 7.5(arrfun1.cpp)对 8 个元素的 {1,2,4,8,16,32,64,128} 求和,输出 Total cookies eaten: 255。

▲ 图 7.4 告诉函数数组的位置、类型和元素个数
传地址意味着什么
函数并没有拿到数组的副本,只拿到三样信息:在哪(首地址)、什么类型、有几个(靠你另外传 n),然后直接操作原件。这不违反按值传递——传的那个”值”就是一个地址。好处是省去复制大数组的开销;风险是可能误改原数据,这正是 const 的用武之地。两个重要恒等式(指针加 1 实际移动所指类型的字节数):
arr[i] == *(ar + i) // 值的两种写法
&arr[i] == ar + i // 地址的两种写法下面的程序(Listing 7.6,arrfun2.cpp)把这一点演示得淋漓尽致:
// arrfun2.cpp -- 数组参数演示
#include <iostream>
const int ArSize = 8;
int sum_arr(int arr[], int n);
int main()
{
int cookies[ArSize] = {1, 2, 4, 8, 16, 32, 64, 128};
std::cout << cookies << " = array address, ";
std::cout << sizeof cookies << " = sizeof cookies\n";
int sum = sum_arr(cookies, ArSize);
std::cout << "Total cookies eaten: " << sum << std::endl;
sum = sum_arr(cookies, 3); // 一个"谎言"
std::cout << "First three eaters ate " << sum << " cookies.\n";
sum = sum_arr(cookies + 4, 4); // 另一个"谎言"
std::cout << "Last four eaters ate " << sum << " cookies.\n";
return 0;
}
int sum_arr(int arr[], int n)
{
int total = 0;
std::cout << arr << " = arr, ";
std::cout << sizeof arr << " = sizeof arr\n";
for (int i = 0; i < n; i++)
total = total + arr[i];
return total;
}输出里 cookies 和 arr 打印出相同地址,但 sizeof cookies 是 32(整个数组)、sizeof arr 只有 4(一个指针)——所以必须单独传元素个数,指针自己不知道数组多长。三个关键观察:① 传的确实是地址;② 指针不携带大小信息;③ 既然函数只认”起始地址 + 个数”,你就可以”撒谎”:传 cookies + 4 就是告诉它”数组从第 5 个元素开始”,于是后四个元素被求和(&cookies[4] 与 cookies + 4 完全等价)。
常见坑:想在原型里用方括号传大小
void fillArray(int arr[size]);是错误原型。数组大小必须作为单独的int参数传:void fillArray(int arr[], int size);
区间(range)传法
另一种方案是传两个指针:一个指向开头,一个指向”最后一个元素的下一个位置”(one past the end,STL 正是这么泛化的)。Listing 7.8(arrfun4.cpp)把求和函数改成区间版,调用如 sum_arr(cookies, cookies + ArSize)(全数组)、sum_arr(cookies, cookies + 3)(前 3 个)、sum_arr(cookies + 4, cookies + 8)(后 4 个):
// arrfun4.cpp -- 使用数组区间的函数
int sum_arr(const int * begin, const int * end)
{
const int * pt;
int total = 0;
for (pt = begin; pt != end; pt++)
total = total + *pt;
return total;
}循环条件是 pt != end:当 pt 走到”末尾之后”就停。end - begin 恰好等于区间元素个数。注意 begin 和 end 的顺序不能传反。
用 const 保护数组,并拼成完整程序
只读函数必须声明”我只看不改”:void show_array(const double ar[], int n);——const double ar[] 即 const double *ar,ar[0] += 10; 会被编译器当场拦截。下面的”房产管理”程序(Listing 7.7,arrfun3.cpp)展示自底向上编程:先设计数据(数组)和对数据的操作(填充、显示、重估值三个函数),再拼装 main():
// arrfun3.cpp -- 数组函数与 const
#include <iostream>
const int Max = 5;
// 函数原型
int fill_array(double ar[], int limit);
void show_array(const double ar[], int n); // 不修改数据
void revalue(double r, double ar[], int n);
int main()
{
using namespace std;
double properties[Max];
int size = fill_array(properties, Max);
show_array(properties, size);
if (size > 0)
{
cout << "Enter revaluation factor: ";
double factor;
while (!(cin >> factor)) // 输入非法则要求重输
{
cin.clear();
while (cin.get() != '\n')
continue;
cout << "Bad input; Please enter a number: ";
}
revalue(factor, properties, size);
show_array(properties, size);
}
cout << "Done.\n";
return 0;
}
int fill_array(double ar[], int limit)
{
using namespace std;
double temp;
int i;
for (i = 0; i < limit; i++)
{
cout << "Enter value #" << (i + 1) << ": ";
cin >> temp;
if (!cin) // 输入非法
{
cin.clear();
while (cin.get() != '\n')
continue;
cout << "Bad input; input process terminated.\n";
break;
}
else if (temp < 0) // 负数作为结束信号
break;
ar[i] = temp;
}
return i;
}
// 下面的函数可以使用(但不能修改)ar 指向的数组
void show_array(const double ar[], int n)
{
using namespace std;
for (int i = 0; i < n; i++)
cout << "Property #" << (i + 1) << ": $" << ar[i] << endl;
}
// 把 ar[] 的每个元素乘以 r
void revalue(double r, double ar[], int n)
{
for (int i = 0; i < n; i++)
ar[i] *= r;
}输入五处房产的值与重估因子 0.8 后,各房产同步缩水;也可以中途输负数提前结束输入。这种”先想数据再想操作”的思路正是 OOP 的起点。
7.5 指针与 const
const 和指针组合有两种玩法,方向完全相反:
int sloth = 3;
const int * ps = &sloth; // 指向 const 的指针:不能通过 ps 改值
int * const finger = &sloth; // const 指针:不能改指向*ps是 const、ps不是——ps 可以改指向别人,但不能用它改值;finger是 const、*finger不是——finger 死心塌地指着 sloth,但可以改 sloth 的值;- 双重保险:
const double * const stick = &trouble;指向和值都锁死。

▲ 图 7.5 指向 const 的指针与 const 指针
一个微妙点:const int * pt = &age; 并不要求 age 真的是 const——它只是说”经由 pt 这条路不许改”。你可以直接 age = 20;,但不许 *pt = 20;。赋值规则一句话:
const 数据的地址只能交给指向 const 的指针;普通数据的地址既可以给普通指针,也可以给指向 const 的指针(仅一级间接时)。
const float g_earth = 9.80;
const float * pe = &g_earth; // 合法
const float g_moon = 1.63;
float * pm = &g_moon; // 非法!否则就能用 pm 偷改 const两级间接(指向指针的指针)时这条规则会失效(能绕出改 const 的漏洞),所以编译器禁止混合赋值。此外,const int months[] = {...} 这种 const 数组不能传给参数为 int arr[] 的函数——那等于把 const 地址塞给普通指针,必须把形参声明成 const int arr[]。
通关标准:
能默写三种组合(
const int *、int * const、const int * const)并说清各自锁的是什么;给函数传数组时能习惯性补上const——它既防手滑改数据,又让函数同时能接收 const 和非 const 数组。
7.6 函数和二维数组
二维数组名的类型是”指向含 4 个 int 的数组的指针”,所以原型必须写明列数:
int sum(int (*ar2)[4], int size); // 正确:指针指向 int[4]
int sum(int *ar2[4], int size); // 错误:这是 4 个指针组成的数组
int sum(int ar2[][4], int size); // 等价且更易读的写法括号不能省:int *ar2[4] 会因优先级被解读成”4 个 int 指针的数组”,而函数参数不能是数组。指针类型里写死了列数(4),所以列数不用单独传,行数由 size 决定——同一个函数既能处理 int a[100][4] 也能处理 int b[6][4]。函数体内可以照常使用 ar2[r][c],它其实是 *(*(ar2 + r) + c) 的简写:ar2 + r 定位第 r 行,*(ar2 + r) 得到该行(行内首元素的地址),再 + c、解引用得到具体元素。注意这里不用 const 修饰,因为 ar2 是”指向指针的指针”,超出了 const 一级保护的范围。
7.7 函数和 C 风格字符串
字符串的三种形态——char 数组、字符串常量、指向字符串的 char 指针——统统是 char * 类型,所以都能作为参数传给同一个函数:
char ghost[15] = "galloping";
char * str = "galumphing";
int n1 = strlen(ghost); // ghost 是 &ghost[0]
int n2 = strlen(str); // 指针
int n3 = strlen("gamboling"); // 字符串常量的地址与普通数组的区别:字符串自带终止空字符 \0,因此不用传长度,函数自己找到 \0 就停。Listing 7.9(strgfun.cpp)统计字符出现次数,对 “minimum” 数出 3 个 ‘m’,对 “ululate” 数出 2 个 ‘u’:
// strgfun.cpp -- 使用字符串参数的函数
unsigned int c_in_str(const char * str, char ch)
{
unsigned int count = 0;
while (*str) // *str 为 '\0' 时退出
{
if (*str == ch)
count++;
str++; // 指针移到下一个字符
}
return count;
}核心套路是标准的字符串遍历模板:while (*str) { ...; str++; }——只要当前字符不是空字符就继续走。
返回字符串:函数不能直接返回数组,但可以返回字符串的地址。Listing 7.10(strgback.cpp)用 new 创建一个由 n 个相同字符组成的串并返回指针:
// strgback.cpp -- 返回 char 指针的函数
char * buildstr(char c, int n)
{
char * pstr = new char[n + 1];
pstr[n] = '\0'; // 终止字符串
while (n-- > 0)
pstr[n] = c; // 从后往前填
return pstr;
}
// main() 中每次调用后都要 delete [] ps; 释放内存注意两个细节:n 个可见字符要申请 n+1 字节给空字符留位;填串从后往前(n-- 先用后减),省掉一个下标变量。这个设计的代价是:调用方要记得 delete [],忘了就内存泄漏——第 12 章会用类的构造/析构函数把这事自动化。
7.8 函数和结构体
结构体比数组好伺候:它像基本类型一样是”一个整体”,可以按值传递、整体赋值、当返回值,没有”数组名变地址”的花活;想要地址得显式写 &。缺点是大结构按值传递要整份复制,费时费内存——这时可以改传地址。
按值传递结构体
// travel.cpp -- 结构体与函数
#include <iostream>
struct travel_time { int hours; int mins; };
const int Mins_per_hr = 60;
travel_time sum(travel_time t1, travel_time t2);
void show_time(travel_time t);
int main()
{
using namespace std;
travel_time day1 = {5, 45}; // 5 小时 45 分
travel_time day2 = {4, 55}; // 4 小时 55 分
travel_time trip = sum(day1, day2);
cout << "Two-day total: ";
show_time(trip);
travel_time day3 = {4, 32};
cout << "Three-day total: ";
show_time(sum(trip, day3)); // 返回值直接当参数
return 0;
}
travel_time sum(travel_time t1, travel_time t2)
{
travel_time total;
total.mins = (t1.mins + t2.mins) % Mins_per_hr;
total.hours = t1.hours + t2.hours +
(t1.mins + t2.mins) / Mins_per_hr;
return total;
}
void show_time(travel_time t)
{
using namespace std;
cout << t.hours << " hours, " << t.mins << " minutes\n";
}输出为 Two-day total: 10 hours, 40 minutes 和 Three-day total: 15 hours, 12 minutes。travel_time 用起来和内置类型没区别:声明变量、当返回值、当参数都行。show_time(sum(trip, day3)) 先算 sum() 的返回值,再把那个值(而不是函数本身)传给 show_time()。
再看坐标转换(Listing 7.12,strctfun.cpp):直角坐标 rect{x, y} 转极坐标 polar{distance, angle},用勾股定理求距离、atan2(y, x) 求角度(别用 atan(),它分不清相差 180° 的方向):
// strctfun.cpp -- 结构体参数函数
#include <iostream>
#include <cmath>
struct polar { double distance; double angle; };
struct rect { double x; double y; };
polar rect_to_polar(rect xypos);
void show_polar(polar dapos);
int main()
{
using namespace std;
rect rplace;
polar pplace;
cout << "Enter the x and y values: ";
while (cin >> rplace.x >> rplace.y) // 巧妙利用 cin
{
pplace = rect_to_polar(rplace);
show_polar(pplace);
cout << "Next two numbers (q to quit): ";
}
cout << "Done.\n";
return 0;
}
polar rect_to_polar(rect xypos)
{
polar answer;
answer.distance =
sqrt(xypos.x * xypos.x + xypos.y * xypos.y);
answer.angle = atan2(xypos.y, xypos.x);
return answer; // 返回一个 polar 结构体
}
void show_polar(polar dapos)
{
using namespace std;
const double Rad_to_deg = 57.29577951;
cout << "distance = " << dapos.distance;
cout << ", angle = " << dapos.angle * Rad_to_deg;
cout << " degrees\n";
}输入 30 40 得到 distance = 50, angle = 53.1301 degrees。这里有个值得学的技巧:while (cin >> rplace.x >> rplace.y) 用输入本身当循环条件——cin >> 表达式的值还是 cin 对象,测试语境下被转成 bool(成功为 true)。输入 q 时检测到非数字,返回 false 结束循环,所有合法数字都能正常接收,不像”输入负数退出”那种做法会牺牲一部分数字的输入资格。(非数字输入会设置错误状态,若后面还要输入,记得 cin.clear() 复位。)

▲ 图 7.6 直角坐标(Micromips 相对 Byteville)

▲ 图 7.7 极坐标(Micromips 相对 Byteville)
传结构体的地址
想省去整份复制的开销,就传地址(Listing 7.13,strctptr.cpp)。三个改动要点:调用时传 &pplace;形参声明为 const polar *(只读时加 const);成员访问用 -> 而不是 .。rect_to_polar() 则改为传两个指针——一个指向待转换的(const),一个指向接收结果的(不能 const,因为要写),不再返回结构体:
// strctptr.cpp -- 指向结构体的指针参数(结构与调用处同上,函数改为)
void rect_to_polar(const rect *pxy, polar *pda);
void show_polar(const polar *pda);
// 调用处:rect_to_polar(&rplace, &pplace); show_polar(&pplace);
void show_polar(const polar *pda)
{
using namespace std;
const double Rad_to_deg = 57.29577951;
cout << "distance = " << pda->distance;
cout << ", angle = " << pda->angle * Rad_to_deg;
cout << " degrees\n";
}
void rect_to_polar(const rect *pxy, polar *pda)
{
pda->distance =
sqrt(pxy->x * pxy->x + pxy->y * pxy->y);
pda->angle = atan2(pxy->y, pxy->x);
}对用户来说它和按值版本表现一模一样——区别全在幕后:一个玩副本,一个玩原件。
7.9 函数和 string 与 array 对象
string 对象更像结构体而不是数组:可整体赋值、可整体传参。声明 string 对象数组就和声明 int 数组一样自然。Listing 7.14(topfive.cpp)让用户输入 5 个最爱的天文景观并回显:string list[SIZE]; 声明数组,getline(cin, list[i]); 读入每个元素,display() 的形参 const string sa[] 是指向 string 对象的指针,sa[i] 就是第 i 个 string 对象,用法与内置类型无异。
C++11 的 array 模板类同理。要修改对象就传地址,只读就按值传(Listing 7.15,arrobj.cpp,四季开销统计):
//arrobj.cpp -- 使用 array 对象的函数(C++11)
#include <iostream>
#include <array>
#include <string>
// 常量数据
const int Seasons = 4;
const std::array<std::string, Seasons> Snames =
{"Spring", "Summer", "Fall", "Winter"};
// 修改 array 对象的函数
void fill(std::array<double, Seasons> * pa);
// 不修改 array 对象的函数
void show(std::array<double, Seasons> da);
int main()
{
std::array<double, Seasons> expenses;
fill(&expenses);
show(expenses);
return 0;
}
void fill(std::array<double, Seasons> * pa)
{
using namespace std;
for (int i = 0; i < Seasons; i++)
{
cout << "Enter " << Snames[i] << " expenses: ";
cin >> (*pa)[i];
}
}
void show(std::array<double, Seasons> da)
{
using namespace std;
double total = 0.0;
cout << "\nEXPENSES\n";
for (int i = 0; i < Seasons; i++)
{
cout << Snames[i] << ": $" << da[i] << endl;
total += da[i];
}
cout << "Total Expenses: $" << total << endl;
}注意 fill() 里的 (*pa)[i]:pa 是指针,*pa 才是对象,括号是为了压过运算符优先级。show() 按值传会复制整个对象,效率不高;指针写法高效但别扭。两个痛点(效率 + 书写)都将在第 8 章用引用一招化解。
7.10 递归
C++ 函数可以调用自己(但 main() 不行),这叫递归(recursion)。标准模板是:把递归调用放进 if 里,保证链条终会断开:
void recurs(argumentlist)
{
statements1
if (test)
recurs(arguments)
statements2
}只要 test 为真,每次调用执行完 statements1 就钻进下一层;test 变假后,从最深层开始逐层”回填”执行 statements2。于是:statements1 按调用顺序执行一遍,statements2 按相反顺序执行一遍。
// recur.cpp -- 使用递归
#include <iostream>
void countdown(int n);
int main()
{
countdown(4); // 调用递归函数
return 0;
}
void countdown(int n)
{
using namespace std;
cout << "Counting down ... " << n << endl;
if (n > 0)
countdown(n-1); // 函数调用自己
cout << n << ": Kaboom!\n";
}Counting down ... 4 ← 第 1 层,递归层数加深
Counting down ... 3 ← 第 2 层
Counting down ... 2 ← 第 3 层
Counting down ... 1 ← 第 4 层
Counting down ... 0 ← 第 5 层,最后一次递归调用
0: Kaboom! ← 第 5 层,开始回退
1: Kaboom! / 2: Kaboom! / 3: Kaboom! / 4: Kaboom! ← 依次回退到第 1 层每层递归都有自己的一组变量——打印 &n 会看到 5 个不同地址的 n,同一层在”下钻”和”回退”两个阶段地址相同。
递归更炫的用法是分而治之(divide-and-conquer):把任务反复拆成两个更小的同类任务。Listing 7.17(ruler.cpp)递归画标尺:main() 先把 66 格的字符数组填成空格、两端放 '|',然后循环 6 次以递增的 level 调用 subdivide() 并打印,每打一行就重置数组。核心的递归函数如下:
// ruler.cpp -- 用递归细分标尺
void subdivide(char ar[], int low, int high, int level)
{
if (level == 0)
return;
int mid = (high + low) / 2;
ar[mid] = '|';
subdivide(ar, low, mid, level - 1);
subdivide(ar, mid, high, level - 1);
}
▲ 程序输出:每多一层递归,标尺上的刻度就密一倍
subdivide() 每次调用自己两次(左半、右半),调用次数按 2 的幂爆炸式增长(1→2→4→8…,6 层填满 64 个位置)。所以这种递归层数少时优雅简洁,层数多就是灾难。
常见坑:忘了终止条件或终止条件够不着
递归没有
if刹车,或刹车条件永远不成立,函数就会一层层无限钻下去,耗尽内存栈后崩溃。写递归先写”什么时候停”,再写”怎么拆小”。
7.11 函数指针
函数也有地址——就是它机器码的起始位置。把函数 A 的地址传给函数 B,B 就能在运行时”换着花样”调用不同函数。三步走:
第一步,取地址:函数名不带括号就是地址。process(think) 传的是函数地址,thought(think()) 传的是函数返回值,天差地别。
第二步,声明指针:照抄目标函数原型,把函数名换成 (*pf):
double pam(int); // 原型
double (*pf)(int); // pf 指向"一个 int 参数、返回 double"的函数
pf = pam; // pf 现在指向 pam()括号是保命的:double (*pf)(int) 是函数指针,double *pf(int) 则是”返回指针的函数”。赋值时签名和返回类型必须完全匹配,pf = ned;(签名不符)和 pf = ted;(返回类型不符)都会被编译器拒绝。
第三步,用指针调用:(*pf)(5) 和 pf(5) 都合法(C++ 的历史妥协——两种写法逻辑上矛盾,但都被允许)。(*pf)(5) 更丑,却时刻提醒你”这里用的是函数指针”。
完整示例(Listing 7.18,fun_ptr.cpp):estimate() 的估时算法可以随时替换:
// fun_ptr.cpp -- 函数指针
#include <iostream>
double betsy(int);
double pam(int);
// 第二个参数是"一个 int 参数、返回 double"的函数的指针
void estimate(int lines, double (*pf)(int));
int main()
{
using namespace std;
int code;
cout << "How many lines of code do you need? ";
cin >> code;
cout << "Here's Betsy's estimate:\n";
estimate(code, betsy);
cout << "Here's Pam's estimate:\n";
estimate(code, pam);
return 0;
}
double betsy(int lns) { return 0.05 * lns; }
double pam(int lns)
{
return 0.03 * lns + 0.0004 * lns * lns;
}
void estimate(int lines, double (*pf)(int))
{
using namespace std;
cout << lines << " lines will take ";
cout << (*pf)(lines) << " hour(s)\n";
}输入 30 行代码时,Betsy 估 1.5 小时、Pam 估 1.26 小时。将来 Ralph 想用自己的算法,不必改 estimate() 一行代码,只需提供签名一致的 ralph()——这就是回调思想,也是虚函数的底层机制。
函数指针数组与 auto/typedef
签名相同的多个函数可以放进函数指针数组,用循环逐个调用:
const double * f1(const double ar[], int n); // 三个原型写法不同但签名相同
const double * (*pa[3])(const double *, int) = {f1,f2,f3}; // 函数指针数组
auto pb = pa; // 指向数组首元素(C++11 自动推断)
auto pc = &pa; // 指向整个数组(多一级间接)[] 优先级高于 *,所以 *pa[3] 表示”3 个指针的数组”;要表示”指向数组的指针”得写 (*pd)[3]。手写全称是这般惨烈:
const double *(*(*pd)[3])(const double *, int) = &pa;所以 C++11 的 auto 和 typedef 是救命的:
typedef const double *(*p_fun)(const double *, int); // p_fun 成为类型名
p_fun pa[3] = {f1,f2,f3}; // 3 个函数指针的数组
p_fun (*pd)[3] = &pa; // 指向该数组的指针还要区分 pa 与 &pa:数值可能相同,但 pa 是”首元素(单个指针)的地址”,&pa 是”整个数组的地址”,类型不同、加 1 跨越的距离也不同。原书 Listing 7.19(arfupt.cpp)用三个签名相同的小函数完整演示了单指针、指针数组、二级指针、指向数组的指针四种调用姿势,输出里”地址:值”成对出现——地址是 av 数组元素的位置,值是该位置的 double。这些写法看着眼晕,但虚函数(第 13 章)的常见实现正是这套技术,好在编译器替你兜底。
自测:
int sum_arr(int arr[], int n)中 arr 到底是什么?是指向 int 的指针(
int *arr的同义写法,仅在函数头/原型中二者等价)。函数内arr[i]实际是*(arr + i),操作的是原数组而非副本。
自测:
const int * pt与int * const pt有何区别?前者锁值:不能通过 pt 改所指向的值,但可以改 pt 的指向;后者锁指向:pt 只能指这一个地方,但可以改所指向的值。口诀——const 在
*左边锁值,在*右边锁指针。
自测:为什么
sum_arr(cookies, ArSize)里必须单独传元素个数?因为传入的只是首地址(一个指针),
sizeof arr得到的是指针本身的大小(如 4 字节),不是数组大小。函数无从知道数组多长,只能靠你明说。
自测:递归 countdown(4) 中,
Kaboom!为什么按 0、1、2、3、4 的顺序打印?每层调用先执行
Counting down再下钻,最深层(n=0)条件为假后执行自己的Kaboom!并返回,各层依次回退,所以回退阶段的输出与下钻阶段顺序相反——每层的局部 n 是各自独立的。
自测:
estimate(code, pam)中传的 pam 是什么?和pam(code)有何不同?
pam(不带括号)是函数的地址,estimate() 可在内部通过指针调用它;pam(code)是立即调用函数并传递其返回值。前者传”厨师的地址”,后者传”做好的菜”。
通关标准:
能独立完成:① 给数组处理函数正确搭配 const 与大小参数;② 把按值传结构体改为传地址(别忘了
->);③ 手写一个带终止条件的递归函数并解释它的下钻/回退过程;④ 不查书写出double (*pf)(int)并用它调用函数。