考研视角下的C语言(中)
考研视角下的C语言(中)
第七章_选择结构
7.1 if 的用法
- 单分支结构(if):只有在条件为真时执行语句。
- 单分支结构(if):只有在条件为真时执行语句。
- 多分支结构(if + else if + else):用于多个条件的判断,依次匹配第一个为真的分支。
if 的分支结构用法如下:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main() {
int score;
printf("请输入分数:");
scanf("%d", &score);
//if 和 else 后面不加 {} 匹配的都是单行语句,即使后面是单行语句建议还是加上 {}
// --- 单分支:判断是否及格 ---
if (score >= 60) {
printf("及格了!\n");
}
// --- 双分支:判断是否优秀 ---
if (score >= 90) {
printf("优秀!\n");
}
else {
printf("不是优秀。\n");
}
// --- 多分支:判断具体等级 ---
if (score >= 90) {
printf("等级:A\n");
}
else if (score >= 75) {
printf("等级:B\n");
}
else if (score >= 60) {
printf("等级:C\n");
}
else {
printf("等级:D\n");
}
return 0;
}
else 就近原则总结(重要!易错点): 在C语言中else 永远匹配最近的、没有配对的 if,不是匹配上面缩进对齐的 if,别和python搞混了
7.2 Switch的用法
基本语法:
switch (表达式) {
case 常量1:
语句1;
break;
case 常量2:
语句2;
break;
...
default:
默认语句;
break;
}
- 表达式的结果必须是整型类型: 包括 int char short 等,不能是 float、double、字符串。
- case 后面必须是“常量值”:
- 例如 case ‘A’:
- 例如 case 10 + 5:(常量表达式)
- 不能是变量
- break 控制是否跳出 switch
- default 是所有 case 都不匹配时执行的分支:相当于 if-else 里的 “else”。
判断月份天数代码,用switch实现:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main() {
int year, month;
printf("请输入年份和月份,中间用空格隔开:");
scanf("%d%d", &year, &month);
int day;
switch (month) {
case 1:case 3:case 5:case 7:case 8:case 10: case 12:
day = 31;
printf("当前月份天数为%d", day);
break;
case 4:case 6:case 9:case 11:
day = 30;
printf("当前月份天数为%d", day);
break;
case 2:
day = 28 + (year % 400 == 0 || year % 4 == 0 && year % 100 != 0);
printf("当前月份天数为%d", day);
break;
default:
printf("输入错误");
}
return 0;
}
第八章_循环结构
8.1 goto和循环
goto实现函数内部的跳转,先写标签,再写goto,可以实现循环。 goto语句要尽量放在if语句里面,不然可能会产生死循环,示例如下:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main() {
int a = 0;
leab:
a++;
if (a <= 100) {
goto leab;
}
printf("%d\n", a);
return 0;
}
goto的有害性(迪杰斯特拉提出goto有害):
- 代码的可读性下降
- 性能问题(破坏了局部性)
8.2 while 循环的使用
8.2.1 while循环使用规则及示例(翻转整数)(小写转大写)
//使用规则
while (条件) {
循环体语句;
}
//翻转整数
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main() {
int input = 0;
int result = 0;
printf("请输入一个整数:\n");
scanf("%d", &input);
while (input / 10 != 0) { //一定关注这个边界点
result *= 10;
result += input % 10;
input /= 10;
}
printf("翻转后的整数为%d", result);
return 0;
}
//小写转大写
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main() {
char ch = ' ';
while (scanf("%c", &ch), ch != '\n') { //能够输入任意长度的字符
if (ch >= 'a' && ch <= 'z') {
ch -= 32;
}
printf("%c",ch);
}
printf("\n");
}
8.2.2 do..while循环
理解成先执行一次的while循环,使用规则及示例:
do {
语句;
} while (条件);
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main() {
int total = 0;
int i = 0;
do {
total += i;
i++;
} while (i <= 100);
printf("%d", total);
printf("\n");
}
8.3 for循环的使用
for循环相比while循环能够将循环代码和主业务代码分离,更加清晰方便阅读,示例如下:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main() {
int total = 0;
for (int a = 0; a <= 100; a++) {
total += a;
}
printf("%d", total);
printf("\n");
}
8.4 continue和break
- break 跳出整个循环
- continue 跳过当前这一次循环
//while 循环中使用continue要特别注意
int i = 0;
while (i < 5) {
if (i == 2)
continue; // 可能死循环
i++;
}
//break实现不确定性的循环次数
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main() {
int total = 0;
int i = 0;
while (1) {
if (i == 10) {
break;
}
i++;
total += i;
}
printf("%d", total);
printf("\n");
return 0;
}
第九章_枚举练习
1.找寻三位的水仙花数
2.找寻(0,n)以内的完数
3.判断质数

第十章_函数
10.1 定义及用法
函数是有名字、可被调用、可返回值(或不返回)、有独立函数体的程序单元,(if / while / for / do…while / switch等都不是函数) 函数可以简单理解成一个主程序的组件(mian()也是函数哦),用法示例如下:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
//函数返回类型 函数名称 函数参数列表(形参) 函数体 ----函数的声明(通知编译器函数的信息)
void is_triangle(int a, int b, int c) {
if (a + b > c && a + c > b && c + b > a) {
printf("This is an triangle\n");
}
else {
printf("This is not an triangle\n");
}
}
int main() {
int a1 = 1, a2 = 2, a3 = 3;
for (int i = 0; i < 3; i++) {
scanf("%d %d %d", &a1, &a2, &a3);
is_triangle(a1, a2, a3);
}
return 0;
}
10.2 函数运行的内存原理
下图介绍下关于在第二章中讲到的更为详细的内存模型,函数的调用影响的就是栈区,这里补充两点:已初始化的全局变量分配在数据段,普通局部变量分配在栈区
代码如下,可自行打断点调试在"堆栈调用"中查看:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
void func1() {
}
void func2() {
func1();
}
int main() {
func1(); //此处断点,逐语句查看
func2();
return 0;
}
10.3 作用域
10.3.1 定义及效果
决定变量的作用域的唯一标准就是 {} ,只有 {} 才产生作用域
在内部作用域可以起一个和外部变量重名的变量,会起到一个隐藏的效果:

10.3.2 全局变量作用域
- 定义在 所有函数外
- 作用域:整个文件
- 所有函数都可访问
10.3.3 局部变量作用域
只要不是全局变量,都是局部变量
- 函数级局部变量(这就是我们常说的局部变量):
- 定义在函数 {} 内
- 作用域:整个函数
- 块级局部变量:
- 定义在 {} 形成的代码块内(如 if / while / for / do…while / switch)
- 作用域:当前 {}
#include <stdio.h>
#define abc 123 //不是变量,只有“生效范围”,没有作用域
/* ① 全局变量:函数外 */
int g = 100;
void test() {
/* ② 函数级局部变量 */
int a = 10;
if (a > 0) {
/* ③ 块级局部变量(if 内) */
int b = 20;
printf("a = %d\n", a); // ✅ 可以
printf("b = %d\n", b); // ✅ 可以
printf("g = %d\n", g); // ✅ 可以
}
printf("a = %d\n", a); // ✅ 可以
// printf("b = %d\n", b); // ❌ 错误:b 只在 if {} 内
}
int main() {
test();
printf("g = %d\n", g); // ✅ 全局变量可用
// printf("a = %d\n", a); // ❌ 错误:a 是 test 的局部变量
return 0;
}
10.4 生存期
全局变量:
- 全局变量的生存期:从程序开始执行(main 之前)到程序结束(return 0; 之后)
局部变量:
- 函数级局部变量的生存期:函数被调用时创建 → 函数返回时销毁
- 块级局部变量的生存期: 进入 {} 并执行到定义语句时 → 离开 {}(if / while / for / do…while / switch等)

10.5 值传递
C 语言只有值传递。引用传递是C++的。
函数调用时,实参的值被拷贝一份给形参,函数内对形参的修改不会影响实参本身。
10.5.1 最基本的值传递示例
#include <stdio.h>
void change(int x) {
x = 100;
}
int main() {
int a = 10;
change(a);
printf("%d\n", a); // 输出 10
}
10.5.2 指针的值传递
指针的值传递看起来像引用传递其实指针本质还是值传递
- 通过指针“修改外部变量”:
void change(int *p) {
*p = 100;
}
int main() {
int a = 10;
change(&a);
printf("%d\n", a); // 输出 100
}
//本质:
// 传的是:地址的值
// p 是地址的拷贝
// 通过地址,改到了 a
// --> 仍然是值传递
- 指针本身也传值:
void change(int *p) {
p = NULL;
}
int main() {
int a = 10;
int *q = &a;
change(q);
printf("%p\n", q); // q 仍然指向 a
}
//改的是 p
//q 不变
除此之外,C语言的值传递还有结构体的值传递和数组的值传递,但不在本章节讨论之中
第十一章_数组
11.1 数组的概念
数组是由相同类型元素组成的一组连续存储的变量集合,通过下标来访问各个元素。
11.2 数组的定义
一维数组的定义:类型 数组名[元素个数] ( [ ] 里的表达式尽量不要使用变量,用宏定义都行)
11.3 数组的初始化和访问
记住两个细小的知识点,其他的如下代码所示:
- 在定义语句里面 = 是初始化符号;在定义语句之外, = 是赋值的意思,赋值不能使用初始化列表
- 定义语句里面用[]来规定数组的长度;非定义语句里面的[]用来根据下标访问元素
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main() {
//-----------------数组的初始化-------------------------
//最正统的初始化
int arr[5] = { 1,2,3,4,5 };
//减少初始化列表长度,多余的元素会自动补0
int arr[5] = { 1,2,3 };
//初始化列表长度不允许大于数组长度
int arr[3] = { 1,2,3,4,5 };
//数组长度是可以自动推断的
int arr[] = { 1,2,3 };
int arr[]; //有初始化列表的情况下才能省略强度
//申请一个长度为1024的数组,内容全是0
int arr[1024] = { 0 };
//在定义语句里面 = 是初始化符号
// 在定义语句之外, = 是赋值的意思,赋值不能使用初始化列表
int arr[5] = { 1,2,3,4,5 }; //允许
int arr[5];
arr[5] = { 1,2,3,4,5 }; //不被允许
//---------------------数组的访问--------------------------
int arr[5] = { 1,2,3,4,5 }; //定义语句里面用[]来规定数组的长度
for (int i = 0; i < 5; i++) {
printf("arr[%d] = %d\n", i, arr[i]);//非定义语句里面的[]用来根据下标访问元素,比如这里的[]就不该出现5了
}
return 0;
}
11.4 数组的内存布局和越界问题
一维数组的首地址和arr[0]的地址是一样的,如果想要访问某个元素,不需要知道数组的长度,公式如下:
arr[i]的地址 = 数组首地址 + i * sizeof(元素类型)
具体数组在内存中的结构如下图所示:
同理上述的公式其实也是 [ ] 运算符的本质:
- 先计算地址,再访问元素
- x [y] ≡ * (x + y) 而且有一个操作数的类型必须是“指向某类型的指针”或“可退化为指针的数组表达式”,故arr[1]其实等价于1[arr]。
那这样还会引出越界问题:
可以看见,明明没有修改a的值,但是a的值变成了12
这是因为[]运算符本身通过计算地址再访问,即使arr数组的最大长度是到arr[4],通过 [] 运算符也可修改后面的位置,比如变量a可以看做是arr[11]
11.5 局部数组的长度限制
在前面函数的章节已经讲过,函数被调用时,在栈上为这个函数分配的一块“工作空间”,这个工作空间我们叫做栈帧,这个栈帧通常包含:
- 局部变量
- 局部数组
- 函数参数
- 返回地址
- 保存的寄存器值
栈帧的默认大小为1M字节(220 字节)比一百万略大比一百二十万略小,那么局部数组如果过大,那就可能会触发栈溢出这个异常,如图所示:
当然也能进行修改默认大小,如图所示:

11.6 数组作为函数参数
在 C 语言中,数组作为函数参数传递时会发生退化(退化为指针),数组名退化为指向首元素的起始地址;在被调函数中,该形式参数实际是一个指针,而不是数组本身。
联想下之前11.4提到的 [ ] 运算符运算公式,也不需要数组长度的,就能知道为什么传递一个指针给函数就能够进行这个数组的访问了。
如图所示,并不会把整个数组都传过去,只传首地址:
需要用到数组长度信息的函数应该再定义一个新的参数,在实际中我们看见的代码常常是这个样子的:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#define LEN 5
void func(int* arr, int length) {
//在被调函数中,数组会退化成一个地址,丢失了长度信息
printf("func sizeof(arr) = %d\n", sizeof(arr));
for (int i = 0; i < length; ++i) {
printf("%d ", arr[i]);//[]运算符不需要长度信息
}
printf("\n");
}
int main() {
//在主调函数的位置,我们是知道数组的长度信息
int arr[LEN] = { 1,2,3,4,5 };
printf("main sizeof(arr) = %d\n", sizeof(arr));
for (int i = 0; i < sizeof(arr) / sizeof(int); ++i) {
printf("%d ", arr[i]);
}
printf("\n");
func(arr, sizeof(arr) / sizeof(int)); //数组这个整体作为实参时,不需要[]
return 0;
}
11.7 二维数组的基本概念
从数学角度看:
- 一维数组 –> 向量
- 二维数组 –> 矩阵
从计算机角度看:
二维数组也是一个一维数组,在内存中是行优先存储:

11.8 二维数组的初始化
如下列代码所示:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main() {
// 二维数组的方式初始化
int arr[2][3] = { {1,2,3},{4,5,6} };
// 一维数组的方式初始化
int arr[2][3] = { 1,2,3,4,5,6 };
// 下面两行初始化是不同的,可以自行打断点看
int arr[2][3] = { {1,2},{3,4} }; //内存中为 1 2 0 3 4 0
int arr[2][3] = { 1,2,3,4 }; //内存中为 1 2 3 4 0 0
return 0;
}
11.9 二维数组的访问和传递
11.9.1 二维数组的访问
二维数组找地址先做行偏移再做列偏移,比如在int arr[2][3]中找arr[i][j]的公式如下:
arr首地址 + i * sizeof(int) * 3 + j * sizeof(int)
具体访问方式如下列代码所示:
#include <stdio.h>
int main() {
int arr[2][3] = { {1,2,3},{4,5,6} };
for (int i = 0; i < 2; ++i) { // 遍历每一行
for (int j = 0; j < 3; ++j) { // 遍历每一列
printf("%3d", arr[i][j]);
}
printf("\n");
}
return 0;
}
注意:虽然二维数组是行优先存储,但是用下面这种方式进行访问是不对的
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main() {
// 二维数组的方式初始化
int arr[2][3] = { {1,2,3},{4,5,6} };
printf("%d\n", arr[1]); //arr[1]此刻是一个指针,应用%p
return 0;
}
这是因为在数组元素进行赋值,加减,函数调用的时候,除了最外层元素,都发生了退化,变成了一个指针指向下一维数组的首地址,比如在二维数组中arr[i]相对于arr[i][j]就相当于一维数组中的arr相对于arr[i]。像下面这样才是正确的:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
void func(int* arr) {
for (int i = 0; i < 3; i++) {
printf("%d ", arr[i]);
}
}
int main() {
// 二维数组的方式初始化
int arr[2][3] = { {1,2,3},{4,5,6} };
// printf("%d\n", arr[1]);
func(arr[1]);
return 0;
}
11.9.2 二维数组的传递
前面说过一维数组被调时丢失元素长度信息,而二维数组在被调是丢失的是行长度信息,列长度信息则被保留,如何理解呢,我们来看下不同表达式对应的实际类型:
| 表达式 | 类型 |
|---|---|
| arr | int [2][3] |
| arr 传参后 | int (*)[3](和(int *)[3]区别) |
| arr[1] | int [3] |
| arr[1] 传参后 | int * |
简单来说,在这个例子里面arr++对应的是3个int类型长度添加(数学上是一列),而arr[1]++对应的只是1个int类型长度添加(数学上是一列里面的一个元素) 调用示例如下:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
void func1(int (*arr)[3]) { //int arr[][3]也可以
printf("%d\n", arr[0][0]); // 虽然arr指向的是数组首地址,但是arr[0]对应的仍然是一个数组,联想我们前面讲过的[]运算符运算公式
}
void func2(int* arr) { //int arr[]也可以
printf("%d\n", arr[0]);
}
int main() {
// 二维数组的方式初始化
int arr[2][3] = { {1,2,3},{4,5,6} };
func1(arr);
func2(arr[0]);
return 0;
}
扩展:多维数组在函数调用时,数组名退化为指向下一维数组的指针,因此只丢失最外层(行)维度 比如:int a[4][5][6]; 丢失最外层 4,参数类型为int (*p)[5][6]