关于我

考研视角下的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;
}
  1. 表达式的结果必须是整型类型: 包括 int char short 等,不能是 float、double、字符串。
  2. case 后面必须是“常量值”:
    • 例如 case ‘A’:
    • 例如 case 10 + 5:(常量表达式)
    • 不能是变量
  3. break 控制是否跳出 switch
  4. 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 指针的值传递

指针的值传递看起来像引用传递其实指针本质还是值传递

  1. 通过指针“修改外部变量”:
void change(int *p) {
    *p = 100;
}

int main() {
    int a = 10;
    change(&a);
    printf("%d\n", a); // 输出 100
}

//本质:
// 传的是:地址的值
// p 是地址的拷贝
// 通过地址,改到了 a
// --> 仍然是值传递
  1. 指针本身也传值:
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 数组的初始化和访问

记住两个细小的知识点,其他的如下代码所示:

  1. 在定义语句里面 = 是初始化符号;在定义语句之外, = 是赋值的意思,赋值不能使用初始化列表
  2. 定义语句里面用[]来规定数组的长度;非定义语句里面的[]用来根据下标访问元素
#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 越界问题2 这是因为[]运算符本身通过计算地址再访问,即使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]

总是在探索未知