为什么需要函数: 回文素数案例与设计原则
函数定义与调用 (案例: 素数判断)
返回类型 void 与无参函数
函数声明
参数传递 (传值、swap 陷阱、类型转换)
return 与程序终止
输入 \(m\)、\(n\) (\(m \le n\)), 完成三个任务:
样例: 输入 100 200 → 回文素数 101 131 151 181 191, 素数个数 21, 200 不是回文数
现场演示: 只用之前学的循环, 不用函数, 把这个程序写出来
先想想: 三个任务里, 哪道检查会被反复使用? 第 5 周是不是写过其中一道?
⚡ 在 VS Code 中打开 palindrome-prime.c (参考实现)/* 三个任务: 不用函数 */
#include <stdio.h>
int main(void)
{
int m, n, i, d, rev, t;
int count = 0;
printf("输入 m n (m<=n): ");
scanf("%d%d", &m, &n);
for (i = m; i <= n; i++) {
/* 任务 1: 输出回文素数 */
/* 素数检查 (第一次) */
int is_prime = 1;
for (d = 2; d <= i / d; d++) {
if (i % d == 0) {
is_prime = 0;
break;
}
}
/* 回文检查 (第一次) */
t = i; rev = 0;
while (t > 0) { rev = rev * 10 + t % 10; t = t / 10; }
if (is_prime && rev == i) {
printf("%d ", i);
}
/* 任务 2: 统计素数个数 */
/* 素数检查 (第二次, 一字不差) */
is_prime = 1;
for (d = 2; d <= i / d; d++) {
if (i % d == 0) {
is_prime = 0;
break;
}
}
if (is_prime) {
count++;
}
}
printf("\n素数个数: %d\n", count);
/* 任务 3: 判断 n 是不是回文数 */
/* 回文检查 (第二次, 一字不差) */
t = n; rev = 0;
while (t > 0) { rev = rev * 10 + t % 10; t = t / 10; }
if (rev == n) {
printf("%d 是回文数\n", n);
} else {
printf("%d 不是回文数\n", n);
}
return 0;
}
素数检查出现两次、回文检查出现两次——一模一样的代码, 纯复制粘贴
三个任务里混着四段检查, main 已经 40 多行, 想看清"程序在干什么"都难
素数检查要改 (比如 d * d <= i 优化成 d <= i / d), 两处都要改; 漏改一处, 统计和输出对不上, 结果难以排查
/* 三个任务: 用函数 */
#include <stdio.h>
int is_prime(int n) /* 素数检查: 写一次 */
{
int d;
if (n <= 1) { /* 1 和 0 不是素数 */
return 0;
}
for (d = 2; d <= n / d; d++) {
if (n % d == 0) {
return 0;
}
}
return 1;
}
int is_palindrome(int n) /* 回文检查: 写一次 */
{
int rev = 0, t = n;
while (t > 0) {
rev = rev * 10 + t % 10;
t = t / 10;
}
return rev == n;
}
int main(void)
{
int m, n, i, count = 0;
printf("输入 m n (m<=n): ");
scanf("%d%d", &m, &n);
for (i = m; i <= n; i++) {
/* 任务 1: 输出回文素数 */
if (is_prime(i) && is_palindrome(i)) {
printf("%d ", i);
}
/* 任务 2: 统计素数个数 */
if (is_prime(i)) {
count++;
}
}
printf("\n素数个数: %d\n", count);
/* 任务 3: 判断 n 是不是回文数 */
if (is_palindrome(n)) {
printf("%d 是回文数\n", n);
} else {
printf("%d 不是回文数\n", n);
}
return 0;
}
两道检查各写一遍; is_prime 调用 2×(n-m+1) 次, is_palindrome 只调用素数个数 + 1 次 (&& 短路)——零复制
main 只剩三个任务的一行行调用, 程序在干什么一目了然
检查要改? 只改一处
if (is_prime(i) && is_palindrome(i)): 函数的返回值可以直接当条件用——这是今天要学的核心玩法
顺手补了 n <= 1 守卫: 原版把 1 当素数; 用 1 10 跑一遍会看到行为变化——测试发现旧 bug.
同样一段检查要抄很多遍, 改 bug 就要改很多处——函数给代码起名字, 一次写好、处处使用
同样的逻辑只写一遍, 需要时调用即可 —— 改 bug 只改一处
程序拆成小块, 更容易调试、理解 —— 每块只做一件事
隐藏实现细节, 只暴露有限、必要的接口
刚才的 is_prime 和 is_palindrome 就是例证: 各写一遍、反复调用——复用、模块化、封装, 一个案例全用上
函数是打包在一起并起名字的一组语句——每个函数本质上都是一个自带声明和语句的小程序; 好处是代码复用 (同一段逻辑只写一次)、模块化 (程序分成更容易理解和修改的小块)、可在其他程序中重用。
其实你早就在用函数了——printf 和 scanf 就是标准库提供的函数, 它们的声明来自头文件:
int printf(const char *format, ...);
int scanf(const char *format, ...);
char str[1024];
scanf("%s", str);
int a, b;
scanf("%d%d", &a, &b);
printf("a + b is : %d", a + b);
返回类型 函数名(形式参数)
{
声明
语句
}
double
average(double a, double b)
{
return (a + b) / 2;
}
函数的 返回类型 是返回值的类型
规则: 不能返回数组; void 表示没有返回值; C89 省略默认 int; C99 不允许省略
返回类型很冗长时 (如 unsigned long int), 也有的习惯把它单独放一行——左栏 double 就是这么放的
double average(double a, double b)
{
double sum; /* declaration */
sum = a + b; /* statement */
return sum / 2; /* statement */
}
每个形式参数前都要说明类型, 参数之间用逗号分隔
没有形式参数时, 括号里写 void
由大括号包围, 里面可以写声明和语句
函数体内声明的变量, 其他函数无法访问或修改
C89: 声明必须写在语句之前; C99: 声明与语句可以混合 (使用前声明过即可)
返回类型为 void 的函数, 函数体可以为空:
void sort(int a[], int n)
{
}
空函数体 = 占位 (stub): 开发时先把所有函数定义好 (哪怕空着), 程序就能编译、运行
之后再逐个填充实现 —— 像盖房子: 先搭建框架, 再逐步完善
先把函数骨架建好, 程序就能编译、运行; 排序的实现, 等数组作参数讲完再填
开场案例里的 is_prime——现在逐行拆解它的定义。之前用循环写过素数判断, 今天把它函数化.
int is_prime(int n)
{
int d;
if (n <= 1) { /* 1 和 0 不是素数 */
return 0;
}
for (d = 2; d <= n / d; d++) {
if (n % d == 0) {
return 0; /* 找到因子, 不是素数 */
}
}
return 1; /* 没找到因子, 是素数 */
}
开头的 int 是返回类型: 是素数返回 1, 不是返回 0
n 是形式参数, 表示待判断的数; 调用时复制实际参数的值
函数体由大括号包围, 里面是声明 int d 和循环
函数可以有多条 return: 执行到哪条, 就从哪条返回
调用需要函数名和实际参数: is_prime(97)、is_prime(m) 都是合法调用
实际参数可以是任意同类型的表达式, 不一定是变量
/* prime.c: 输出 2 ~ 100 的全部素数 */
#include <stdio.h>
int is_prime(int n)
{
int d;
if (n <= 1) { /* 1 和 0 不是素数 */
return 0;
}
for (d = 2; d <= n / d; d++) {
if (n % d == 0) {
return 0;
}
}
return 1;
}
int main(void)
{
int i;
for (i = 2; i <= 100; i++) {
if (is_prime(i)) {
printf("%d ", i);
}
}
printf("\n");
return 0;
}
把 is_prime 的调用放在 if 里, 返回值直接当条件用——非 void 函数的调用就是一个值
2 ~ 100 的素数:
2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97
对比之前: 那时要判断一个数, 得把循环抄一遍; 现在写一次, 判断 99 次
练习: 用同样方式写 gcd(a, b)——辗转相除法 (大数除小数取余, 直到余数为 0)
函数没有返回值, 返回类型就写 void:
void print_count(int n)
{
printf("%d\n", n);
}
没有参数时, 括号里写 void, 调用时也必须写空括号:
void print_line(void)
{
printf("====\n");
}
void 是一种没有值的类型
print_count 的调用必须自成一个语句: print_count(i);
print_line 的调用: print_line();——括号不能省, 否则编译器把它当成变量
average(x, y)
print_count(i)
print_line()
print_line; /*** WRONG ***/
print_count(i);
print_line();
avg = average(x, y);
if (average(x, y) > 0) {
printf("Average is positive\n");
}
printf("The average is %g\n", average(x, y));
函数调用由函数名和用括号括起来的实际参数列表组成:
如果没有括号, 就无法调用函数:
该语句是合法的, 但没有任何作用.
void函数的调用后边始终跟着分号, 使该调用成为语句:
调用非void函数会产生一个值, 该值可存储在变量中或打印等:
average(x, y); /* discards return value */
num_chars = printf("Hi, Mom!\n");
printf("Hi, Mom!\n"); /* discards return value */
(void) printf("Hi, Mom!\n");
(void)——让别人知道是故意的, 而不是忘了非 void 函数的返回值不需要时, 可以把它丢掉——average(x, y); 就是表达式语句: 算出值, 不保存
printf 返回它打印的字符个数——num_chars = printf("Hi, Mom!\n"); 之后 num_chars 为 9 (可自行验证)
通常我们直接丢弃 printf 的返回值
#include <stdio.h>
int main(void)
{
double x, y, z;
printf("Enter three numbers: ");
scanf("%lf%lf%lf", &x, &y, &z);
printf("Average of %g and %g: %g\n", x, y, average(x, y));
printf("Average of %g and %g: %g\n", y, z, average(y, z));
printf("Average of %g and %g: %g\n", x, z, average(x, z));
return 0;
}
double average(double a, double b)
{
return (a + b) / 2;
}
C 不要求函数的定义在调用之前——可以把 average 的定义放在 main 之后; 但 main 调用它时, 编译器没有任何信息.
不报错, 而是假设它返回 int——为该函数创建了隐式声明; 实参个数与类型也无法检查
后面遇到真实定义 (返回 double) 才会报错
避免方法一: 定义总写在调用之前——可惜有时做不到, 且顺序不自然、难读
#include <stdio.h>
double average(double a, double b); /* 声明 */
int main(void)
{
double x, y, z;
…
printf("Average: %g\n", average(x, y));
…
}
double average(double a, double b) /* 定义 */
{
return (a + b) / 2;
}
返回类型 函数名(参数); /* 原型 */
double average(); /* 旧式声明 */
调用前先声明函数, 给编译器一份 "简要介绍"; 声明必须与定义一致 (返回类型、参数个数与类型)
这类声明称为函数原型; 形参名可省略 (如 double average(double, double);), 但最好写上
旧式声明 double average(); 是 C89 遗留: 括号为空, 编译器不检查参数——尽量不用
C99 规则: 调用前必须先声明或定义, 否则出错 (上一页的隐式声明正是反面教材)
计算数字 x 的 n 次幂 \(x^n\):
int power(int x, int n)
{
int i, result = 1;
for (i = 1; i <= n; i++) {
result = result * x;
}
return result; /* x 的 n 次幂 */
}
调用: printf("%d\n", power(2, 5)); → 输出 32
C 里参数是按值传递的:
调用函数时, 计算出每个实际参数的值, 并将其赋值给相应的形式参数
函数里改形参不影响实参——形参只是实参的副本
按值传递既有优点也有缺点:
优点: 形参可以当函数内的普通变量用, 少声明几个变量 (下一页的 power 就这么写)
缺点: 想在函数里 "改外面" 的变量, 做不到——下一页的 decompose 就是典型例子
int power(int x, int n)
{
int result = 1;
while (n-- > 0) {
result = result * x;
}
return result;
}
void decompose(double x, long int_part, double frac_part)
{
int_part = (long) x;
frac_part = x - int_part;
}
decompose(3.14159, i, d);
n 是原始指数的副本, 函数内可以直接修改它 (while (n-- > 0)), 不再需要变量 i
想在函数里 "改外面" 的变量就做不到——decompose 想分解出整数部分和小数部分, 修改形参也没用
decompose(3.14159, i, d) 之后, i、d 纹丝不动
后面的指针部分里, decompose 才能真正奏效 (swap 同理)
void swap(int a, int b) {
int t = a; a = b; b = t; /* 交换的是 a、b 的值 */
}
int main(void) {
int x = 3, y = 5;
swap(x, y);
printf("x = %d, y = %d\n", x, y);
return 0;
}
运行结果:
x = 3, y = 5
传值调用: a、b 拿到 x、y 的值 (即副本), 函数内交换的是 a、b 的值, 与外面的 x、y 没有关系
换个角度看: 如果调用 swap(x + 6, y + 5), 传进去的是表达式的值——显然没人指望 x、y 被改; swap(x, y) 传的同样是值, 不是变量本身
这是新手最困惑的现象之一: 函数明明执行了, 外界却看不到任何变化
记住这个悬念——后面的指针部分, 用 swap(&x, &y) 就能真正交换
C 允许实际参数的类型与形式参数的类型不匹配: 怎么转换, 取决于调用前编译器有没有见过原型
double average(double a, double b)
{
return (a + b) / 2;
}
int main(void)
{
int x = 3, y = 5;
printf("%g\n", average(x, y)); /* int 实参 */
return 0;
}
见过原型 (本页): 每个参数的值被隐式转换为形参的类型, 就像赋值一样
average(x, y): x、y 是 int, 传给 double 形参时自动转成 double——安全, 结果正确 (输出 4)
没见过原型 (下一页): 执行默认参数提升, 危险——square 的例子
#include <stdio.h>
int main(void)
{
double x = 3.0;
printf("Square: %d\n", square(x));
return 0;
}
int square(int n)
{
return n * n;
}
printf("Square: %d\n", square((int) x));
float 实参 → 转 double; char/short 实参 → 转 int (整数提升)
编译器不知道 square 要 int——x 是 double, 提升对它无效, 函数却按 int 解读传入的 double → 结果无效
调用前强制转换: square((int) x)
更好的办法: 调用前提供原型 (C99 里没声明就调用直接是错误)
return 表达式;
return 0;
return status;
return n >= 0 ? n : 0;
非 void 函数必须用 return 指定返回值
表达式通常是常量或变量, 也可以是更复杂的表达式 (n >= 0 ? n : 0)
return 表达式的类型与返回类型不匹配时, 会被隐式转换为返回类型
声明返回 int 却 return 了 double, 值被截断成 int
return; /* return in a void function */
void print_int(int i)
{
if (i < 0) {
return;
}
printf("%d", i);
}
void print_count(int n)
{
printf("%d\n", n);
return; /* OK, but not needed */
}
不带表达式的 return; 只用于 void 函数, 作用是提前返回——左栏 print_int: i < 0 时直接返回, 后面的打印不执行
void 函数末尾的 return; 可以写, 但没必要 (左栏 print_count), 函数自然结束
main 可以不写 return (默认返回 0); 其他非 void 函数必须显式 return
非 void 函数忘了写 return, 再去用它的返回值——行为是未定义的: 可能拿到垃圾值、可能是 0、可能崩溃, 每次运行都可能不同, 编译器还不一定报错
int main(void)
{
…
}
main()
{
…
}
通常 main 的返回类型是 int
老式 C 常省略返回类型 (默认 int)——C99 起不合法, 不要省略; 参数列表里的 void 省略合法, 但最好写上
main 的返回值是状态码: 正常结束返回 0; 异常结束返回非 0
每个程序都返回状态码是好习惯
exit(0); /* normal termination */
exit(EXIT_SUCCESS);
exit(EXIT_FAILURE);
return 表达式;
exit(表达式);
结束程序的两种方式: main 里 return, 或调用 <stdlib.h> 的 exit——参数含义相同 (状态码)
0 有点模糊, 所以 C 提供宏: EXIT_SUCCESS (正常) / EXIT_FAILURE (异常), 定义在 <stdlib.h>, 通常为 0 和 1
return 只有由 main 执行时才结束程序
exit 任何函数里调用都会结束程序
return 的作用是什么?
is_prime(9) 和 is_prime(11) 各返回什么?is_prime 里的 return 0 执行后, 循环还会继续吗?
void swap(int a, int b) {
int t = a; a = b; b = t;
}