Appearance
函数:把代码打包成积木
预计学习时长:约 3 小时(仅 L1 主线,建议分两次完成)· 运行环境:VS2026 / gcc / clang 均可
🎯 学习目标
学完本讲,你将能够:
- 说清库函数与自定义函数的区别,用官网文档查一个没学过的库函数
- 按"返回类型 + 函数名 + 形参表 + 函数体"四要素写出自定义函数
- 解释"形参是实参的一份临时拷贝",并说出为什么函数内改形参不影响实参
- 正确使用 return,说清 6 条高频注意事项
- 把数组作为参数传给函数,并解释为什么必须同时传元素个数
- 写出嵌套调用与链式访问的代码,读懂
printf套printf的输出 - 用"声明 + 定义"的方式组织单文件与多文件程序,说清 static / extern 的作用
🔗 先修知识
- 第 7 讲:数组:本讲要把数组传进函数
- 第 6 讲:循环:函数体里大量使用循环
- 第 5 讲:分支:return 与 if 的配合
- 第 3 讲:数据类型和变量:返回类型的隐式转换
先修自检(三问)
int arr[10];用 sizeof 怎么算元素个数?void这个词在第 3 讲里表示什么意思?- if 分支里 return 之后,函数后面的语句还会执行吗?
一、函数是什么
数学里你见过函数:y = kx + b,给一个 x 就得到一个 y。C 语言的函数(function)类似,有些书把它翻译成子程序,这个翻译其实更贴切。
一句话定义:函数就是完成某项特定任务的一小段代码。
它带来两个好处:
| 好处 | 说明 |
|---|---|
| 分解任务 | 一个大计算任务拆成若干小任务,每个小任务一个函数,程序由无数小函数组合而成 |
| 复用 | 一个功能写一次,到处调用,不用复制粘贴 |
C 语言里你见到的函数分两类:
- 库函数:别人已经写好的,如
printf、scanf、sqrt - 自定义函数:你自己写的
二、库函数
2.1 标准库与头文件
一个容易混淆的事实:C 语言标准规定了语法规则,但本身不提供库函数。ANSI C 标准规定了一批常用函数的标准(叫什么、参数是什么、返回什么、做什么用),各家编译器厂商按这个标准给出实现——这些函数就叫库函数。
所以 printf 在 VS 里和在 gcc 里,底层实现代码完全不同,但对你的用法完全一样。
这些库函数按功能分组,在不同的头文件里声明:数学的在 <math.h>,字符串的在 <string.h>,输入输出的在 <stdio.h>……库函数不用一口气全学,用到哪个查哪个,各个击破。
2.2 怎么查一个库函数:以 sqrt 为例
sqrt 求平方根,原型是:
🧩 函数原型(示意片段) · 仅说明语法,不能单独编译
c
double sqrt(double x);读法:函数名 sqrt;参数是一个 double 值;返回值类型是 double(计算结果)。
📄 use_sqrt.c · ✅ 完整程序(可直接复制编译)
c
#include <stdio.h>
#include <math.h> // sqrt 在这里声明,不能漏
int main()
{
double d = 16.0;
double r = sqrt(d);
printf("%lf\n", r);
return 0;
}text
4.000000⚠️ 库函数务必包含对应头文件。不带头文件时,编译器只见过你的调用、不知道函数的真实长相,可能给出警告甚至错误的默认假设,行为不可预期。
📘 提高(L2):库函数文档的一般格式
查文档时按这个顺序读,效率最高:
- 函数原型——参数类型和返回类型
- 功能介绍——它到底做什么
- 参数与返回值说明——边界情况(如 sqrt 传入负数会怎样)
- 代码举例——最快上手的部分
- 输出结果——验证你的理解
- 相关知识链接——顺藤摸瓜
常用查询站点:
- cppreference 中文:
zh.cppreference.com/w/c/header - cplusplus.com:
legacy.cplusplus.com/reference/clibrary/
三、自定义函数
3.1 语法形式
自定义函数和库函数的形式一模一样:
🧩 示意片段 · 仅说明语法,不能单独编译
c
ret_type fun_name(形式参数)
{
函数体
}把函数想象成一座小型加工厂:
| 工厂 | 函数 |
|---|---|
| 原材料(送进去) | 参数(可以 0 个也可以多个) |
| 车间加工 | 函数体(完成计算的过程) |
| 产品(送出来) | 返回值 |
四个部分各有讲究:
ret_type返回类型:表示计算结果的类型;写void表示什么都不返回fun_name函数名:方便调用,要根据功能起得有含义- 参数:交代清楚个数、每个的类型、名字;写
void明确表示没有参数 {}函数体:真正干活的部分
3.2 实例:写一个加法函数
需求:输入两个整数,用函数算出它们的和。先搭好框架,函数部分留空:
c
#include <stdio.h>
int main()
{
int a = 0, b = 0;
scanf("%d %d", &a, &b);
// TODO:调用加法函数,结果放进 r
printf("%d\n", r);
return 0;
}分析:功能叫 Add 合适;需要接收 2 个 int;结果也是 int。于是:
📄 add.c · ✅ 完整程序(可直接复制编译)
c
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int Add(int x, int y) // 返回 int,收两个 int
{
int z = x + y;
return z; // 把结果交回调用处
}
int main()
{
int a = 0, b = 0;
scanf("%d %d", &a, &b);
int r = Add(a, b); // 调用,拿到返回值
printf("%d\n", r);
return 0;
}Add 还能再简化,一行搞定:
c
int Add(int x, int y)
{
return x + y;
}函数名、参数、返回类型都按实际需要灵活设计,没有固定套路。
四、形参和实参
用 Add 这段代码来区分两个概念:
c
int Add(int x, int y) // x、y 是形参(定义处括号里的)
{ ... }
int r = Add(a, b); // a、b 是实参(调用处真实传进去的)| 概念 | 全称 | 位置 | 特点 |
|---|---|---|---|
| 实参 | 实际参数 | 调用处 | 真实传递的值,真实占用内存 |
| 形参 | 形式参数 | 定义处 | 只是"形式上"存在;不调用就不申请内存 |
形参的实例化:只有函数被调用时,形参才为了接收实参的值而向内存申请空间。这就是"形式参数"名字的由来。
最关键的一条:形参和实参是各自独立的内存。调试 Add 能观察到——x、y 确实拿到了 a、b 的值,但它们的地址和 a、b 完全不同。所以:
形参是实参的一份临时拷贝。
推论:在函数里改形参,函数外的实参不会有任何变化。这个坑下面用实验验证。
五、return 语句
return 是函数"交回产品"的动作,六条注意事项:
- return 后可以是数值,也可以是表达式——表达式先算,再返回结果:
return x + y; - 可以什么都不写:
return;——适合返回类型是void的函数 - 执行 return,函数彻底返回,后面的代码不再执行(哪怕只有一行)
- 返回值类型与函数返回类型不一致时,自动隐式转换成函数的返回类型
- 函数里有 if 分支时,要保证每条路径都有 return,否则可能编译报错或行为不可预期
- 函数写了返回类型却没有 return,返回值是未知的
📄 return_demo.c · ✅ 完整程序(可直接复制编译)
c
#include <stdio.h>
int demo(double x)
{
int r = (int)x; // 第 4 条:double 自动转成 int(截断)
return r;
printf("这行永远看不到\n"); // 第 3 条:return 之后的语句不再执行
}
void hello(void)
{
printf("hello\n");
return; // 第 2 条:void 可以只写 return;
}
int main()
{
int r = demo(3.9); // 第 1、4 条:先算表达式,再返回结果
printf("r = %d\n", r);
hello();
return 0;
}text
r = 3
hello上面
return后面那行 printf 不会打印(实测输出里没有它)。注意 gcc 对这种死代码默认不报警告,VS 在较高等级(/W4)下会报warning C4702: 无法访问的代码——编译器不提醒,不代表代码是对的,写完 return 就别再往下堆语句。
📜 标准卡(L2 选读):缺少 return 与隐式 int
- C 标准:C11 §6.9.1p12——函数执行到结尾却没有 return,而调用方用到了这个返回值,行为未定义(🔴);C99 起取消了"隐式 int"(§6.9.1p2 要求声明说明符必须含类型),不写返回类型不再默认 int,而是编译错误。
- gcc:
-Wall下报warning: control reaches end of non-void function;不写返回类型报 error。 - MSVC:报
error C2144/error C4716: 必须返回一个值。 - 结论:老教材"不写返回类型默认是 int"的说法来自 C89,现代编译器已不认,务必显式写
int或void。
六、数组做函数参数
需求:写两个函数,一个把整型数组全部置为 -1,一个打印数组内容。
先想最朴素的版本:
c
int main()
{
int arr[] = {1,2,3,4,5,6,7,8,9,10};
set_arr(); // 要改数组,就得把数组传进来
print_arr(); // 要打印,也得把数组传进来
return 0;
}函数内部要遍历数组,就得同时知道数组和元素个数,所以要传 2 个参数:
c
int main()
{
int arr[] = {1,2,3,4,5,6,7,8,9,10};
int sz = sizeof(arr) / sizeof(arr[0]);
set_arr(arr, sz);
print_arr(arr, sz);
return 0;
}数组传参的六条规则(背下来):
| # | 规则 |
|---|---|
| 1 | 形参要和实参个数匹配 |
| 2 | 实参是数组,形参也可以写成数组形式 |
| 3 | 形参是一维数组,大小可以省略 |
| 4 | 形参是二维数组,行可以省略,列不能省略 |
| 5 | 数组传参时,形参不会创建新数组 |
| 6 | 形参操作的数组和实参的数组是同一个数组 |
第 5、6 条和第 4 节"临时拷贝"矛盾吗?不矛盾——普通变量传参是拷贝值,数组传参传的是位置,所以函数能真的改到原数组。完整示例:
📄 arr_param.c · ✅ 完整程序(可直接复制编译)
c
#include <stdio.h>
void set_arr(int arr[], int sz) // 一维数组形参:大小省略
{
for (int i = 0; i < sz; i++)
{
arr[i] = -1;
}
}
void print_arr(int arr[], int sz)
{
for (int i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
}
int main()
{
int arr[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int sz = (int)(sizeof(arr) / sizeof(arr[0]));
print_arr(arr, sz); // 原样
set_arr(arr, sz); // 真的改到了原数组
print_arr(arr, sz); // 全是 -1
return 0;
}text
1 2 3 4 5 6 7 8 9 10
-1 -1 -1 -1 -1 -1 -1 -1 -1 -1⚠️ 还记得第 7 讲埋的伏笔吗?
sizeof(arr)/sizeof(arr[0])在函数内部会失灵——因为形参并不创建一个真正的数组,它只是"指向原数组的引用",sizeof算出的不再是整个数组的大小。这就是第 1 条规则里"必须把元素个数一起传进去"的根本原因。
📘 提高(L2):亲眼看看 sizeof 失灵
📄 sizeof_fail.c · ✅ 完整程序(可直接复制编译)
c
#include <stdio.h>
void inside(int arr[]) // 形参数组
{
printf("函数内 sizeof(arr) = %zu\n", sizeof(arr));
}
int main()
{
int arr[10] = {0};
printf("main 中 sizeof(arr) = %zu\n", sizeof(arr)); // 40
inside(arr);
return 0;
}在 64 位机器上,main 里是 40,函数里是 8——两者已经不是一个东西了。原因到第 12 讲:指针才讲透,现在只要记住结论:数组传参后 sizeof 失灵,个数必须额外传。
七、嵌套调用和链式访问
7.1 嵌套调用:函数之间互相调用
函数像乐高零件,零件互相配合才能搭出大玩具——稍大的程序全靠函数之间有效调用。
需求:算某年某月有多少天。拆成两个函数:
is_leap_year():判断闰年get_days_of_month():先调用is_leap_year,再按月算天数
📄 nest_call.c · ✅ 完整程序(可直接复制编译)
c
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int is_leap_year(int y)
{
if (((y % 4 == 0) && (y % 100 != 0)) || (y % 400 == 0))
return 1;
else
return 0;
}
int get_days_of_month(int y, int m)
{
// 下标 1~12 对应 1~12 月;下标 0 占位,让"月份=下标"直接对上
int days[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
int day = days[m];
if (is_leap_year(y) && m == 2) // 嵌套调用
day += 1;
return day;
}
int main()
{
int y = 0, m = 0;
scanf("%d %d", &y, &m);
printf("%d\n", get_days_of_month(y, m));
return 0;
}输入 2024 2 输出 29。这段代码里的调用关系:
text
main ──► scanf / printf / get_days_of_month
└──► is_leap_year⚠️ 函数可以嵌套调用,但不能嵌套定义——不能在函数体里再定义一个函数。
7.2 链式访问:把返回值当参数
把一个函数的返回值直接当作另一个函数的参数,像链条一样串起来。
📄 chain.c · ✅ 完整程序(可直接复制编译)
c
#include <stdio.h>
#include <string.h>
int main()
{
// 分两步写
int len = strlen("abcdef");
printf("%d\n", len);
// 链式访问:strlen 的返回值直接作为 printf 的参数
printf("%d\n", strlen("abcdef"));
return 0;
}text
6
6一段经典趣题——printf 的返回值是什么?
📄 chain_printf.c · ✅ 完整程序(可直接复制编译)
c
#include <stdio.h>
int main()
{
printf("%d", printf("%d", printf("%d", 43)));
return 0;
}关键:printf 返回实际打印到屏幕上的字符个数。逐层拆解:
- 最里层
printf("%d", 43)打印43(2 个字符),返回 2 - 中间层
printf("%d", 2)打印2(1 个字符),返回 1 - 最外层
printf("%d", 1)打印1(1 个字符)
屏幕上最终是:
text
4321八、函数的声明和定义
8.1 单个文件:先声明后使用
把函数定义写在调用之前,一切正常:
🧩 示意片段 · 仅说明顺序,不能单独编译
c
int is_leap_year(int y) { ... } // 定义在前
int main() { is_leap_year(y); } // 调用在后 ✅如果把定义挪到调用之后:
🧩 示意片段 · 故意展示问题写法,不能单独编译
c
int main() { is_leap_year(y); } // 调用在前
int is_leap_year(int y) { ... } // 定义在后 ⚠️ 有警告编译器是从第一行往下扫描的:扫到调用处时还没见过这个函数,就会给出警告(gcc 报 implicit declaration of function;VS 报 C4013)。解决办法——调用之前先声明:
📄 declare.c · ✅ 完整程序(可直接复制编译)
c
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int is_leap_year(int y); // 函数声明:只交代返回类型、名字、参数类型
int main()
{
int y = 0;
scanf("%d", &y);
if (is_leap_year(y))
printf("闰年\n");
else
printf("非闰年\n");
return 0;
}
// 定义放在后面也完全可以
int is_leap_year(int y)
{
if (((y % 4 == 0) && (y % 100 != 0)) || (y % 400 == 0))
return 1;
else
return 0;
}两条规则:
- 函数调用必须"先声明后使用"
- 定义本身就是一种特殊的声明,所以定义写在调用之前也能通过编译
声明中参数的名字可以省略,只保留类型:int is_leap_year(int); 同样合法。
8.2 多个文件:企业级写法
实际项目的代码量不会都塞在一个文件里,通常按功能拆分。惯例是:
| 内容 | 放哪 |
|---|---|
| 函数声明、类型声明 | 头文件 .h |
| 函数实现(定义) | 源文件 .c |
三个文件配合:
🧩 add.c · 示意片段(需与下面两个文件组成工程才能编译)
c
// 函数的定义
int Add(int x, int y)
{
return x + y;
}🧩 add.h · 示意片段
c
// 函数的声明
int Add(int x, int y);🧩 test.c · 示意片段
c
#include <stdio.h>
#include "add.h" // 自己的头文件用引号
int main()
{
int c = Add(10, 20);
printf("%d\n", c);
return 0;
}运行结果 30。test.c 只靠 add.h 知道"有个 int Add(int,int) 可用",链接时再找到 add.c 编译出的实现——这就是"声明与实现分离"。
📘 提高(L2):尖括号与双引号的区别
#include <stdio.h> 用尖括号,编译器去系统头文件目录找;#include "add.h" 用双引号,先去当前项目目录找,找不到再去系统目录。自己写的头文件一律用双引号。头文件防重复包含的写法见第 17 讲:预处理。
九、static 和 extern
这两个关键字的理解需要先铺垫两个概念。
9.1 作用域和生命周期
| 概念 | 含义 | 局部变量 | 全局变量 |
|---|---|---|---|
| 作用域 | 名字可用的代码范围 | 所在局部范围 | 整个工程 |
| 生命周期 | 从创建(申请内存)到销毁(收回内存)的时间段 | 进作用域开始、出作用域结束 | 整个程序运行期间 |
9.2 static 修饰局部变量
对比两段代码,只差一个 static:
📄 static_local.c · ✅ 完整程序(可直接复制编译)
c
#include <stdio.h>
void test1(void)
{
int i = 0; // 每次进函数重新创建并赋 0
i++;
printf("%d ", i);
}
void test2(void)
{
static int i = 0; // 只创建一次,出函数不销毁
i++;
printf("%d ", i);
}
int main()
{
for (int j = 0; j < 5; j++)
test1();
printf("\n");
for (int j = 0; j < 5; j++)
test2();
printf("\n");
return 0;
}text
1 1 1 1 1
1 2 3 4 5原理:static 修饰局部变量改变了生命周期,本质是改变了存储位置——普通局部变量存在栈区,被 static 修饰后存到静态区,和全局变量一样直到程序结束才销毁;但作用域不变,仍只能在函数内访问。
💡 使用建议:一个变量出了函数你还想保留它的值,等下次进函数接着用,就加
static。
9.3 static 修饰全局变量 / 9.4 static 修饰函数
两者的效果完全一样,都是把"外部链接"变成"内部链接"——只能在本源文件内使用。
普通全局变量 / 普通函数:默认具有外部链接属性,其他文件用 extern 声明一下就能用。
c
/* add.c */
int g_val = 2018;
int Add(int x, int y) { return x + y; }c
/* test.c */
#include <stdio.h>
extern int g_val; // 声明外部符号
extern int Add(int x, int y);
int main()
{
printf("%d %d\n", g_val, Add(2, 3));
return 0;
}正常链接,输出 2018 5。
加上 static 之后:
c
/* add.c */
static int g_val = 2018; // 或 static int Add(int x, int y){...}c
/* test.c */
extern int g_val; // 照样声明链接阶段报错(VS:error LNK2001: 无法解析的外部符号;gcc:undefined reference to 'g_val')——因为那个符号已经不对其他文件开放了。
| 用法 | 效果 | 什么时候用 |
|---|---|---|
static 修饰局部变量 | 生命周期延长到整个程序,作用域不变 | 函数内需要"记住上次值" |
static 修饰全局变量 | 只在本源文件可见 | 内部使用的状态,不想被别的文件碰 |
static 修饰函数 | 只在本源文件可见 | 工具函数、内部实现,避免名字冲突 |
extern 声明 | 引用其他文件定义的符号 | 跨文件使用全局变量/函数 |
📜 标准卡(L2 选读):链接属性
- C 标准:C11 §6.2.2——具有文件作用域的对象/函数默认是外部链接;用
static修饰则变为内部链接;§6.2.4p3——static存储期的局部变量,生命周期是整个程序执行期,且只用常量表达式初始化。 - gcc / MSVC:行为一致;跨文件引用被 static 隐藏的符号都在链接期报错,不是编译期。
- 结论:
extern声明一个 static 符号是 🟢 明确的标准行为(内部链接不可跨文件可见)。
⚠️ 常见坑与报错(本讲汇总)
| 你看到的现象 | 原因 | 修法 |
|---|---|---|
| warning C4013 / implicit declaration | 调用在定义之前又没声明 | 调用前加一行函数声明 |
| 函数里改了变量,外面没变 | 形参是实参的临时拷贝 | 用返回值带回结果(想直接改外部变量要等第 12 讲指针) |
| 函数内 sizeof 算出的元素个数是 2 或 8 | 数组传参后 sizeof 失灵 | 把元素个数作为第二个参数传进去 |
| error C4716: 必须返回一个值 | 非 void 函数某条路径没有 return | 每条分支都补 return,或把返回类型改成 void |
| 链接错误 LNK2019 / undefined reference | 声明了函数但没有实现,或没把 .c 加入工程 | 检查定义是否存在、源文件是否参与编译 |
| 用了 sqrt 却报未定义 | 忘了 #include <math.h> | 库函数必须包含对应头文件 |
| VS 报 error C4996: 'scanf' | VS 认为 scanf 不安全 | 文件顶部加 #define _CRT_SECURE_NO_WARNINGS |
error: too many arguments to function | 声明与定义的参数个数/类型不一致 | 声明照抄定义,或让 IDE 自动生成声明 |
🛠️ 动手练习
规则:先独立做,卡住了再依次展开提示,不要一上来就看答案。
📊 本讲网页练习进度0 / 5(0%)
进度自动保存在本浏览器;编程题不计数,请在编辑器中完成。
练习 1 · ⭐ 基础 · 选择判断
知识点:库函数与头文件
题干:下列说法正确的是?(多选)
练习 2 · ⭐ 基础 · 读输出
知识点:形参是临时拷贝
题干:写出输出:
📄 ex2.c · ✅ 完整程序
c
#include <stdio.h>
void try_change(int x)
{
x = 100;
}
int main()
{
int a = 10;
try_change(a);
printf("%d\n", a);
return 0;
}请写出输出:
空①
练习 3 · ⭐ 基础 · 读输出
知识点:链式访问与 printf 返回值
题干:写出输出:
📄 ex3.c · ✅ 完整程序
c
#include <stdio.h>
int main()
{
printf("%d", printf("%d", printf("%d", 43)));
return 0;
}请写出屏幕上显示的最终结果:
空①
练习 4 · ⭐ 基础 · 程序填空
知识点:数组做函数参数
题干:补全形参表,让函数能接收数组并遍历它:
🧩 待补全片段
c
void print_arr(______, ______)
{
for (int i = 0; i < sz; i++)
printf("%d ", arr[i]);
}第一空填形参(数组),第二空填形参(个数):
空①空②
练习 5 · ⭐ 基础 · 找错改错
知识点:声明与 return
题干:下面代码有两处问题,请填出修正:
🧩 待改错片段
c
#include <stdio.h>
int main()
{
printf("%d\n", Square(5));
return 0;
}
int Square(int x)
{
if (x > 0)
return x * x;
}第一空:在 main 之前应补一行 ____;第二空:函数还缺什么?请写出补在函数末尾的那一行。
空①空②
练习 6 · ⭐⭐ 提高 · 写小程序
知识点:函数封装 + 循环
🛠 动手编程题 · 请在 VS2026(或你的编辑器)中完成
写一个函数 `int max_of_three(int a, int b, int c)` 返回三个数中的最大值,在 main 中调用它并打印结果。
✅ 过关标准:传入 3 9 5 输出 9;再测 1 1 1 与 -2 -5 -1。
网页内无法练习写代码,亲手敲、亲手编译才能真正学会。下方按顺序展开 思路 → 步骤 → 答案。
💡 提示 1(思路)
函数内部还是第 7 讲那招"打擂台":先取 a 当冠军,b、c 依次挑战。
🧭 提示 2(步骤)
int m = a;if (b > m) m = b;if (c > m) m = c;return m;
🔑 参考答案与解释
c
#include <stdio.h>
int max_of_three(int a, int b, int c)
{
int m = a;
if (b > m)
m = b;
if (c > m)
m = c;
return m;
}
int main()
{
printf("%d\n", max_of_three(3, 9, 5));
printf("%d\n", max_of_three(1, 1, 1));
printf("%d\n", max_of_three(-2, -5, -1));
return 0;
}要点:初始值必须是 a 而不是 0,否则三个负数时会返回错误的 0。函数只管算、只管 return,打印交给调用者——职责分开是好代码的第一步。
练习 7 · ⭐⭐ 提高 · 写小程序
知识点:嵌套调用 + 数组查表
🛠 动手编程题 · 请在 VS2026(或你的编辑器)中完成
自己写 `is_leap_year` 和 `get_days_of_month` 两个函数:输入年和月,输出这个月的天数。要求用数组存 12 个月的天数,而不是 12 个 if。
✅ 过关标准:输入 2024 2 输出 29;输入 1900 2 输出 28;输入 2026 4 输出 30。
网页内无法练习写代码,亲手敲、亲手编译才能真正学会。下方按顺序展开 思路 → 步骤 → 答案。
💡 提示 1(思路)
天数表写成 13 个元素的数组,下标 0 放个 0 占位,这样"月份"就能直接当下标用。
🧭 提示 2(步骤)
int days[] = {0,31,28,31,30,31,30,31,31,30,31,30,31};int day = days[m];if (is_leap_year(y) && m == 2) day++;return day;
🔑 参考答案与解释
c
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int is_leap_year(int y)
{
return ((y % 4 == 0) && (y % 100 != 0)) || (y % 400 == 0);
}
int get_days_of_month(int y, int m)
{
int days[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
int day = days[m];
if (is_leap_year(y) && m == 2)
day += 1;
return day;
}
int main()
{
int y = 0, m = 0;
scanf("%d %d", &y, &m);
printf("%d\n", get_days_of_month(y, m));
return 0;
}改进点:is_leap_year 直接 return 表达式;——条件本身算出来就是 1 或 0,省掉 if/else 两行。用数组查表代替一长串 if,代码更短也更好改。
练习 8 · ⭐⭐⭐ 挑战 · 写小程序
知识点:函数封装判断逻辑
🛠 动手编程题 · 请在 VS2026(或你的编辑器)中完成
写一个函数 `int is_prime(int n)`:n 是素数返回 1,否则返回 0(规定 1 不是素数)。在 main 中打印 1~100 之间所有的素数,用空格分隔。
✅ 过关标准:输出以 2 3 5 7 开头、以 89 97 结尾,共 25 个素数。
网页内无法练习写代码,亲手敲、亲手编译才能真正学会。下方按顺序展开 思路 → 步骤 → 答案。
💡 提示 1(思路)
试除法:拿 n 去除以 2 到 n-1 之间的每个数,只要有一个能整除就不是素数。
🧭 提示 2(步骤)
- 先处理 n ≤ 1,直接 return 0
- for 循环 i 从 2 到 n-1:
if (n % i == 0) return 0; - 循环走完都没返回,说明是素数,
return 1; - main 里 for 循环 1~100,
if (is_prime(i)) printf("%d ", i);
🔑 参考答案与解释
c
#include <stdio.h>
int is_prime(int n)
{
if (n <= 1)
return 0;
for (int i = 2; i < n; i++)
{
if (n % i == 0)
return 0; // 一发现因子立刻返回
}
return 1;
}
int main()
{
for (int i = 1; i <= 100; i++)
{
if (is_prime(i))
printf("%d ", i);
}
printf("\n");
return 0;
}要点:return 在循环里出现就是"提前收工"——发现因子立刻结束函数,不用设标志变量。想再优化(只试到 √n)见第 6 讲末尾的 L3 素数优化。
📝 小结与自测
本讲主线:函数 = 返回类型 + 函数名 + 形参表 + 函数体;调用时实参把值拷贝给形参,return 把结果送回调用处;数组传参传的是位置,所以能改到原数组,但个数必须一起传;函数可以互相嵌套调用(不能嵌套定义),返回值可以作为另一个函数的参数(链式访问);用"声明 + 定义"组织代码,多文件时声明进 .h、实现进 .c;static 把东西限制在本文件内(局部变量则延长生命周期),extern 用来引用别处的符号。
自测四问:
- 为什么函数里
sizeof(arr)/sizeof(arr[0])算出的个数不对?正确做法是什么? printf("%d", printf("hi"));会打印什么?为什么?- 函数定义写在调用之后,需要做什么才能正常编译?声明里参数名能省略吗?
static修饰局部变量和修饰全局变量,效果分别是什么?
🔬 选学拓展(L3)
以下内容超出主线要求
适合学有余力的同学,不影响后续学习。
L3-1:函数递归预告——自己调用自己
本讲的调用都是"甲函数调乙函数"。还有一种特殊的:函数调用自己,叫递归。比如求阶乘:n! = n × (n-1)!,把问题变成"更小的同一个问题"。
递归是第 9 讲的完整主题,这里只留一个悬念:
📄 recursion_teaser.c · ✅ 完整程序(可直接复制编译)
c
#include <stdio.h>
int fact(int n)
{
if (n <= 1)
return 1; // 出口:没有它会无限调用下去
return n * fact(n - 1); // 自己调自己
}
int main()
{
printf("%d\n", fact(5));
return 0;
}text
120试着回答:如果没有 if (n <= 1) return 1; 这四行,程序会怎样?(答案在第 9 讲)
L3-2:为什么"函数内交换两个数"做不到
经典需求:写 void Swap(int x, int y) 交换两个变量的值。
📄 swap_fail.c · ✅ 完整程序(可直接复制编译)
c
#include <stdio.h>
void Swap(int x, int y)
{
int t = x;
x = y;
y = t;
printf("函数内:x=%d y=%d\n", x, y);
}
int main()
{
int a = 10, b = 20;
Swap(a, b);
printf("函数外:a=%d b=%d\n", a, b);
return 0;
}text
函数内:x=20 y=10
函数外:a=10 b=20函数里明明换成功了,外面纹丝不动——因为换的是临时拷贝。要真的交换,必须把变量的地址传进去:
c
void Swap(int *px, int *py); // 需要指针指针是第 12 讲的内容,学到那里我们会回来把这个函数补完。现在先记住:值传递改不了外面的变量。
L3-3:main 的返回值到底给谁
本讲所有程序都以 return 0; 收尾。这个 0 去哪了?它返回给调用你的操作系统:0 通常表示"正常结束",非 0 表示出错。在命令行里能亲眼看到:
text
# Windows PowerShell
.\myprog.exe
echo $LASTEXITCODE # 显示 main 返回的数字📜 标准卡(L3 选读):main 的返回类型与 0
- C 标准:C11 §5.1.2.2.3——返回类型为 int 的 main,执行到结尾时行为等同于
return 0;(🟢 标准保证);返回给环境的值,0 和 EXIT_SUCCESS 表示成功实现定义。 - gcc / MSVC:均按此实现;
return 0;可以不写,但建议写上,意图更清晰。