到现在为止,你所有代码都挤在 main 里。同一段逻辑出现第二次,就得整段复制——改要改两处,错就两处一起错。函数是把一段逻辑装进一个名字里:参数决定喂它什么,返回值决定它交回什么,函数名替你记住“这段在干什么”。
给出平面直角坐标系上不在同一条直线上的三个点 A、B、C 的坐标,求三角形 ABC 的周长。
输入三行,每行两个实数,表示一个点的横、纵坐标(允许是小数)。
输出一行一个实数,为三角形周长,保留两位小数。
三条边公式一模一样,只有坐标不同。不用函数,sqrt(dx*dx+dy*dy) 要抄三遍。先看 5 张语法卡。
| 组成 | 含义 / 格式要求 |
|---|---|
返回类型 | 结果是什么类型:算距离 double,判质数 bool,只做事不出结果 void。 |
形参表 | 占位符,每个参数各自声明,类型写全:(double x1, double y1)。 |
return 值; | 把结果交回调用处,并立刻结束函数。 |
调用 | 实参按位置一一抄进形参,然后跳进函数体。 |
double area(double w, double h); // 前置声明:定义在 main 之后就靠它
int main() {
cout << area(3, 4) << "\n"; // 实参 3、4 抄给形参 w、h → 12
return 0;
}
double area(double w, double h) { // 定义:真正实现
return w * h;
}示例省略 #include <bits/stdc++.h> 与 using namespace std;,所有卡片同理。
定义在 main 之后必须先声明,否则报 area was not declared in this scope。函数最多 return 一个值,要带出两个结果只能靠引用参数。
| 组成 | 含义 / 格式要求 |
|---|---|
形参 int x | 调用时把实参的值拷一份进来,两个变量互不相干。 |
改 x | 改的是副本。函数返回后副本销毁,实参一个字节都没变。 |
代价 | 传大对象要整体复制:10⁵ 个 int 的数组按值传进去,等于多拷 400 KB。 |
void addOne(int x) { x += 1; }
int main() {
int a = 0;
addOne(a);
cout << a << "\n"; // 0,不是 1
}想改实参只有两条路:引用参数,或把新值 return 出来再赋回 a = addOne(a);。
int、double、char 这类小类型,值传递最省事。
| 组成 | 含义 / 格式要求 |
|---|---|
int &x | & 写在类型后、变量名前。x 与实参是同一块内存的两个名字。 |
读 / 写 x | 读写 x 就是读写实参,函数返回后实参真的变了。 |
实参要求 | 必须是能取地址的变量。传 f(3)、f(a + b) 直接编译错误。 |
void addOne(int &x) { x += 1; } // 只多了一个 &
int main() {
int a = 0;
addOne(a);
cout << a << "\n"; // 1
}引用参数是“函数返回多个结果”的标准做法:void getMinMax(int a[], int n, int &mn, int &mx),调用后两个变量都被写好。
所以 mySwap(int a, int b) 什么也没干,mySwap(int &a, int &b) 才对。
| 组成 | 含义 / 格式要求 |
|---|---|
const | 承诺函数内不修改参数。写 s[0] = 'x' 直接编译错误。 |
& | 不复制,直接引用原对象,省掉拷贝开销。 |
用在哪 | 参数是大对象(string、结构体、数组)且只读时,一律 const T &。 |
int countChar(const string &s, char c) {
int cnt = 0;
for (int i = 0; i < (int)s.size(); ++i)
if (s[i] == c) ++cnt; // 只读,符合 const 的承诺
return cnt;
}
// countChar("banana", 'a') == 3小类型不要加 &:const int &x 比 int x 还慢一点。
| 组成 | 含义 / 格式要求 |
|---|---|
出口 | 最小规模直接给答案。没有出口就是无限递归,必然 RE。 |
递推 | 把规模 n 化成规模 n−1 的同类问题,规模必须严格变小。 |
栈帧 | 每层调用都留一份自己的局部变量,返回时才释放。深度 × 每帧大小 = 实际占用。 |
1LL * | 强制按 long long 相乘。20! = 2432902008176640000,早就超 int。 |
long long fact(int n) {
if (n <= 1) return 1;
return 1LL * n * fact(n - 1);
}
// fact(5) = 120, fact(20) = 2432902008176640000本节的立场:先会读递归,再决定用不用。递归深度一般别超过 10⁵ 层。
题目只有一件事:三条边相加。而“求一条边”要做三次。
所以把“求一条边”抽成只依赖 4 个形参的纯函数,再在 main 里调用三次。这种函数最好验算——随手喂两组坐标就能判断对不对,不用管程序的其他部分。
#include <bits/stdc++.h>
using namespace std;
// 只依赖 4 个形参,不碰任何全局状态,好测、可复用
double dist(double x1, double y1, double x2, double y2) {
double dx = x1 - x2;
double dy = y1 - y2;
return sqrt(dx * dx + dy * dy); // 勾股定理
}
int main() {
double ax, ay, bx, by, cx, cy;
cin >> ax >> ay >> bx >> by >> cx >> cy;
// 三条边同一个公式,只有坐标不同 —— 这正是函数的用武之地
double ans = dist(ax, ay, bx, by)
+ dist(bx, by, cx, cy)
+ dist(cx, cy, ax, ay);
cout << fixed << setprecision(2) << ans << "\n"; // fixed 才表示"小数位数"
return 0;
}| 代码片段 | 含义与理由 |
|---|---|
double dist(double x1, ..., double y2) | 返回 double:距离带小数,不能收成 int。4 个形参的顺序必须和调用时完全一致。 |
double dx = x1 - x2; | dx、dy 是 dist 的局部变量,出了函数就销毁,和 main 里的同名变量互不干扰。 |
return sqrt(dx * dx + dy * dy); | sqrt 的参数与返回值都是 double,与返回类型一致,不做隐式转换。 |
cin >> ax >> ... >> cy; | 六个实数一次读完。cin 自动跳过换行,所以输入分三行也正确。 |
dist(a...) + dist(b...) + dist(c...) | 三次独立调用:每次把新实参抄进形参,第一次的局部变量早已销毁,互不影响。 |
cout << fixed << setprecision(2) << ans; | fixed 表示“小数点后几位”。只写 setprecision(2) 是“总共两位有效数字”,会输出 12 而不是 12.00。 |
数组名传进函数时只传了首地址,长度在这条路上丢了。所以形参里必须再给一个 n,而且不能在函数里用 sizeof 反算长度——那时 a 已经是指针,sizeof(a) 恒为 8。
int sumOf(const int a[], int n) { // const int a[] 等价于 const int *a
int s = 0;
for (int i = 0; i < n; ++i) s += a[i];
return s;
}
// 调用 sumOf(x, 5);在 main 里 sizeof(x)/sizeof(x[0]) 才等于 5二维数组同理,但第二维必须写出:void f(int a[][105], int n, int m)。
| 写在哪 | 叫什么 | 初值 | 活到什么时候 | 内存区 |
|---|---|---|---|---|
函数或 {} 内 | 局部变量 | 随机值(垃圾) | 出了这层花括号就销毁 | 栈,约 1~8 MB |
| 所有函数之外 | 全局变量 | 自动为 0 | 整个程序运行期间 | 静态区,可用几百 MB |
结论很硬:int a[1000005] 占 4 MB,写在 main 里会栈溢出(本地可能还能跑,评测机 RE);写到所有函数外面就没事,还自动清零。数组长度超过 10⁵ 就放全局。
但全局变量谁都能改,出了问题极难定位。所以只有“很大的数组”和“全程序共享的表”才放全局,能用参数传的一律用参数。
想让函数改实参却忘写 &。程序能跑、输出还是原值,WA。典型是 mySwap(int a, int b):函数里换得很认真,main 里一点没变。
在函数里写 sizeof(a) / sizeof(a[0]) 算长度。a 已退化为指针,结果恒为 2,循环只跑 2 次,WA。
把大数组写在 main 里。int a[1000005] 超过栈容量,本地正常、评测机 RE。
递归忘写出口,或出口永远不成立。症状是 RE(栈溢出)或 TLE,不是编译错误,第一次遇到很难想到。
函数定义写在 main 后面又没声明。报 xxx was not declared in this scope,提示指向调用行,容易误以为自己名字打错了。
不看本页,把 P5735 敲一遍并提交。
做三组对照:① 把 dist 的形参与 main 里的变量全改成 int,输入 0 0 / 0 3 / 4 0,答案仍是 12.00(sqrt 会把 int 转成 double);② 输入改成 0.5 0 / 0 3 / 4 0,看 cin >> int 只吃下 0、把 .5 留在流里造成后续读入全部失效;③ 各写一个 addOne(int x) 和 addOneRef(int &x),调用后打印 a,对比 0 与 1;④ 在 main 里开 int big[1000005] 并赋值,看程序是否直接崩。
改一改:把三次 dist 收成 double perimeter(double ax, double ay, double bx, double by, double cx, double cy),让 main 只剩两行。再想一步:要改成“返回最长的一条边”,需要几个引用参数?
把递归写成方程,就能在上机前判断够不够快。设 T(n) 是规模 n 的耗时:
T(n) = T(n−1) + c:每层只做常数工作,展开得 T(n) = cn = O(n)。阶乘递归、数组递归求和都属于这档。
T(n) = 2T(n−1) + c:第 k 层有 2ᵏ 个调用,总数 2⁰+2¹+…+2ⁿ = 2ⁿ⁺¹ − 1 = O(2ⁿ)。朴素递归求斐波那契就是这样:f(40) 约 10⁸ 次调用,f(45) 约 2×10⁹ 次,必然 TLE。
等比求和(q ≠ 1):a₁ + a₁q + … + a₁q^(m−1) = a₁(qᵐ − 1)/(q − 1)。上面那个 2ⁿ⁺¹ − 1 就是 q = 2 的特例。
处理 f(n) = a·f(n−1) + b 的通法是换元。以节末挑战题为例,逆向递推 f(d) = 2f(d+1) + 2,两边同加 2 得 f(d) + 2 = 2(f(d+1) + 2)。令 g(d) = f(d) + 2 就是纯等比,由 g(n) = 3 倒推 g(1) = 3·2ⁿ⁻¹,所以 f(1) = 3·2ⁿ⁻¹ − 2:n = 10 时 3×512 − 2 = 1534,与程序输出一致。
栈的硬上限:Windows 默认 1 MB,Linux 默认 8 MB;一层栈帧通常 32~96 字节。于是递归深度上限约 1 万层到 10 万层——读到“n ≤ 10⁶ 还要递归”,就该改循环或手写栈了。
double dist(double, double);),定义里必须写名字。为什么声明允许省略?省略了以后函数体里还能用这些参数吗?pair 行不行?没找到时返回什么?mySwap(int a, int b) 什么都没干,而标准库的 swap(a, b) 有效?它的签名属于哪种传参?sumOf(const int a[], int n) 去掉 const,函数体里多了哪些可能?这会引入什么风险?int sum(int a[]) 里 sizeof(a) 不等于数组字节数,以及正确做法。int a[1000000] 为什么 RE,改成全局为什么就好了。猴子第一天摘下若干桃子,当即吃了一半,还不过瘾,又多吃了一个。第二天早上又将剩下的吃掉一半,又多吃一个。以后每天早上都吃了前一天剩下的一半零一个。
到第 n 天早上想再吃时,发现只剩下一个桃子了。求第一天共摘了多少个桃子。
输入一个正整数 n(1 ≤ n ≤ 30)。输出一个整数,表示第一天摘的桃子数。
别从第一天往后推——你不知道第一天有多少个。反过来,从第 n 天的 1 个往前倒推。
设第 d 天早上有 f(d) 个。吃掉一半再多吃一个后剩下的是 f(d+1),把它写成 f(d) 的表达式。
写出反向公式后,既可以用一层循环从 n 倒着算到 1,也可以写成一个递归函数。
第 d 天有 f(d) 个,吃掉一半再多吃一个,剩下 f(d) − f(d)/2 − 1 = f(d)/2 − 1,这个量就是 f(d+1)。
所以 f(d+1) = f(d)/2 − 1,反解得 f(d) = (f(d+1) + 1) × 2。
已知 f(n) = 1,用上式从 d = n−1 倒推到 d = 1,答案就是 f(1)。
为什么对:每步都是等价代数变形,没丢解;f(d) 必为偶数,倒推结果一定是整数。
写成函数 int peach(int d):d == n 返回 1,否则返回 (peach(d+1)+1)*2 —— 这就是本节讲的递归雏形。
#include <bits/stdc++.h>
using namespace std;
int main() {
int n;
cin >> n;
// 从第 n 天早上的 1 个,倒推回第 1 天
int cnt = 1;
for (int d = n - 1; d >= 1; --d)
cnt = (cnt + 1) * 2; // f(d) = (f(d+1) + 1) * 2
cout << cnt << "\n";
return 0;
}| 片段 | 作用 |
|---|---|
int cnt = 1; | 初始值是第 n 天早上的桃子数,即已知的 1。cnt 表示“当前这一天的桃子数”。 |
for (int d = n - 1; d >= 1; --d) | 从第 n−1 天倒推到第 1 天,共 n−1 步。n = 1 时一步不跑,直接输出 1,正合题意。 |
cnt = (cnt + 1) * 2; | 把 f(d) = (f(d+1)+1)×2 翻成代码:先加 1 再乘 2,顺序不能反。 |
cout << cnt << "\n"; | 此时 cnt 就是 f(1),第一天摘的桃子数。 |
cnt = cnt * 2 + 1。这是另一条递推,不是“剩一半再多吃一个”的逆运算。for (int d = n; d >= 1; --d),多推一步,答案变大。long long。