C语言:从入门到进阶笔记(完整版)
时间:2023-05-01 03:37:00
全文约20w文字(初稿中难免会出现错误、排版问题或不准确)
?? 前言
本系列适用于接触过C语言或对C语言有基本了解的读者,适用于复习、巩固和巩固基础。共18章,每章分为几个部分,部分章节有配套练习,本系列有三套C语言笔试题和详细答案分析。第一章由于字数原因,以贴链接的方式展示。由于作者水平有限,时间紧迫,这种教学中的错误和不准确性是不可避免的。我也想知道这些错误,并希望读者批评和纠正它们!博客从第一章更新到最后一章持续了四个月,难免会出现排版、代码风格、图标使用是不可避免的。请理解。文章中有许多表情符号,旨在减少读者在阅读过程中的无聊,有些表情符号可以生动地记住一些重要的知识点,但有些章节的表情符号较少。如果有机会,我将继续改进这一系列教程。这是第一次发布,还有很多缺点需要改进。谢谢你的支持。
?? 本文为整合篇,二十万字左右,由于篇幅较大,如果你觉得很难阅读,如果你想有选择地学习和阅读,可订阅专栏 —— 维生素C语言 ,可选择相应的章节观看。
?? 参考文献 / 资料
Microsoft. MSDN(Microsoft Developer Network)[EB/OL]. []. .
林锐. 《高质量C/C 编程指南》[M]. 1.0. 电子工业, 2001.7.24.
陈正冲. 《C语言深度解剖》[M]. 第三版. 北京航空航天大学出版社, 2019.
俞甲子 / 石凡 / 潘爱民. 程序员的自我修养[M]. 电子工业出版社, 2009-4.
百度百科[EB/OL]. []. https://baike.baidu.com/.
比特科技. C语言基础[EB/OL]. 2021[2021.8.31]. .
比特科技. C语言进阶[EB/OL]. 2021[2021.8.31]. .
第一章 - 初识C语言
第二章 - 分支和循环
一、语句
0x00 什么是语句
?? C语言中,有一个分号( ;)隔开的是句子。
?? 这些都是句子:
( 一行里只有 ;我们称之为语句 "空语句" )
int main(void) { printf("hello world!\n"); // 语句; 3 5; // 语句; ; // 空语句; }
0x01 真与假
?? 定义: 0是假的,非0是真的(比如1是真的,0是假的)
二、分支语句
0x00 if 语句
?? 单 if 语句演示:
int main(void) { int age = 0; scanf("%d", &age); if ( age >= 18 ) printf("成年\n"); return 0; }
?? if...else 演示:
int main(void) { int age = 0; scanf("%d", &age); if ( age >= 18 ) printf("成年\n"); else printf("未成年"); return 0; }
?? 多分支演示:
int main(void) { int age = 0; scanf("%d", &age); if(age<18) { printf("少年\n"); } else if(age>=18 && age<30) { printf("青年\n"); } else if(age>=30 && age<50) { printf("中年\n"); } else if(age>=50 && age<120) { printf("老年\n"); } else { printf("请输入正确的年龄\n"); } return 0; }
?? 判断一个数是否为奇数:
int main(void) { int n = 0; scanf("%d", &n); if(n % 2 == 0) { printf("不是奇数\n"); } else { printf("是奇数\n"); } return 0; }
0x01 代码块
?? 若条件确定,需执行多个语句,应使用代码块,一对大括号,即代码块。
?? 建议:无论是一行语句还是多行语句,建议加大括号。
?? 不增加括号隐患:悬空 else
? 将打印以下代码 abc 吗?
int main(void) { int a = 0; int b = 2; if ( a == 1 ) if ( b == 2 ) printf("123\n"); else printf("abc\n"); return 0; }
?? 运行结果:(什么都没打印出来)
?? 分析:因为没有大括号,else 与离它最近的一个if相结合( 即内部 if ),所以即使 else 与外部 if 对应,也没用。
?? 修正:添加大括号后,代码的逻辑可以更清晰!
int main(void) { int a = 0; int b = 2; if(a == 1) { if(b == 2) { printf("hehe\n"); } } else { printf("haha\n"); } return 0; }
?? 运行结果: abc
0x02 码风格
代码一:可读性不好,但是节省空间
代码二:可读性强
代码三:我们希望 hello 不被打印出来,但是事实上他打印出来了;
int main()
{
int num = 0;
if ( num = 5 ) {
printf("hello\n"); // = 赋值 == 判断相等;
}
return 0;
}
🔑 解析:为什么会这样呢?因为在 if 语句中 num = 5 相当于重新赋值了。
💬 为了防止把一个等号写成两个等号,发生这样的BUG,我们可以这么写:
int main()
{
int num = 0;
if (5 == num) {
printf("hehe\n");
}
return 0;
}
这样写,如果不小心写成了 "=",运行都运行不了,可以让自己很容易地发现问题。这是种好的代码风格!未来如果涉及到常量和变量相比较,比较相等与否,我们不妨把变量放在双等号的右边,常量放在左边,以防不小心少打一个 "=" ,导致程序出错。
📚 关于 return 0
int test() {
if (1) {
return 0; // 当return 0 执行了,下面的代码都不会执行了;
}
printf("hehe\n");
return 1;
}
int main(void) {
test();
return 0;
}
0x04 switch 语句
📚 介绍:switch 语句是一种多分支语句,常常用于多分支的情况。一个标准 switch 语句的组成:
① case 语句项:后面接常量表达式(类型只能是整型和枚举类型)。
② break 语句:用来跳出 switch 语句,实际效果是把语句列表划分为不同的部分。
③ default 子句:默认执行的语句,当所有 case 都无法与 switch 的值相匹配时执行。
📌 注意事项:
1. case 和 default 后面记得加 :(冒号),而不是分号。
2. 在 switch 语句中可以出现if语句。
3. switch 后面必须是整型常量表达式。
4. 每个 switch 语句后面只能有一个 default。
5. 不一定非要加 default,也可以不加。
📜 建议:
1. 在最后一个 case 语句的后面也加上一条 break 语句,以防未来增添语句项时遗漏。
2. 建议在每个 switch 中都加入 default 子句,甚至在后边再加一个 break 都不过分。
💬 switch 用法演示:用户输入一个数字x,返回星期(eg. 1 >>> 星期一)
int main(void) {
int day = 0;
scanf("%d", &day);
switch (day) {
case 1:
printf("星期一\n");
break; // 跳出switch
case 2:
printf("星期二\n");
break;
case 3:
printf("星期三\n");
break;
case 4:
printf("星期四\n");
break;
case 5:
printf("星期五\n");
break;
case 6:
printf("星期六\n");
break;
case 7:
printf("星期日\n");
break;
default: // 默认执行的语句;
break;
}
return 0;
}
💬 多 case 同一个结果情况演示:输入1-5,输出 工作日;输入6-7,输出休息日;其他数字返回error
int main(void) {
int day = 0;
scanf("%d", &day);
switch (day) {
case 1:
case 2:
case 3:
case 4:
case 5:
printf("工作日\n");
break;
case 6:
case 7:
printf("休息日\n");
break; // 末尾加上break是个好习惯;
default:
printf("输入错误\n");
break; // 这里可以不加break,但是加上是个好习惯;
}
return 0;
}
❓ 下列代码输出值是多少?
int main(void) {
int n = 1;
int m = 2;
switch(n) {
case 1:
m++;
case 2:
n++;
case 3:
switch(n) {
case 1:
n++;
case 2:
m++;
n++;
break;
}
case 4:
m++;
break;
default:
break;
}
printf("m = %d, n = %d\n", m, n);
return 0;
}
💡 答案:m = 5, n = 3
🔑 解析:因为n=1,所以进入switch后执行case1的语句m++,此时m=3,由于该语句项末尾没有break,继续向下流到case2的语句n++,此时n=2,又没有break,流向case3,case3中又嵌了一个switch(n),此时因n=2,执行内部switch的case2的语句m++和n++,此时m=4,n=3,后面有break,跳出内部switch,但是外部switch的case3后面依然没有break,所以流向case4,m++,此时m=5,后面终于有break了。运行下来后的结果为 m=5,n=3。
三、循环语句
0x00 while 循环
📚 定义:当满足条件时进入循环,进入循环后,当条件不满足时,跳出循环。
📌 注意事项:while 循环条件将会比循环体多执行一次。
while 循环中,当条件表达式成立时,才会执行循环体中语句,每次执行期间,都会对循环因子进行修改(否则就成为死循环),修改完成后如果 while 条件表达式成立,继续循环,如果不成立,循环结束。
💬 while死循环:表达式结果如果为非0,为真,循环就执行
int main(void) {
while(1)
printf("hehe\n");
return 0;
}
🚩 运行结果如下:
💬 while 循环打印 1~10 的数字:
int main(void) {
int i = 1;
while(i<=10) {
printf("%d ", i);
i++;
}
return 0;
}
🚩 运行结果: 1 2 3 4 5 6 7 8 9 10
0x01 break 语句
📚 break 语句在 while 循环中的效果:
在 while 循环中,break 用于永久地终止循环。
int main(void) {
int i = 1;
while(i <= 10) {
if(5 == i) // i=5时停止循环;
break;
printf("%d ", i);
i++;
}
return 0;
}
🚩 运行结果:1 2 3 4
0x02 continue 语句
📚 continue 语句:
int main()
{
int i = 1;
while(i<=10) {
if(i==5) {
continue; // 跳至判断部分;
}
printf("%d ", i);
i++;
}
return 0;
}
🚩 运行结果: 1 2 3 4(程序会一直判断)
0x03 getchar 和 putchar
📚 getchar:
从流(stream)或键盘上,读取一个字符。
返回值:如果正确,返回ASCII值;如果读取错误吗,返回 EOF(文件结束标志)。
📚 putchar:单纯的输出一个字符。
💬 getchar 使用方法演示: "输入什么就返回什么"
int main(void) {
int ch = getchar();
putchar(ch); // 输出一个字符;
return 0;
}
🚩 运行结果:(假设输入a) a
💬 getchar 与 while 的结合: "一直从键盘上读取字符的程序"
int main(void) {
int ch = 0;
// ctrl+z - getchar 就读取结束;
while ( (ch = getchar()) != EOF ) {
putchar(ch);
}
return 0;
}
❓ 如果想停止输入,怎么办?
💡 解决方法: 输入 ctrl + z 可以使 getchar 结束读取。
💬 getchar 只打印数字:
int main(void) {
int ch = 0;
while( (ch=getchar()) != EOF ) {
if(ch<'0' || ch>'9') {
continue; // 发现不是数字,跳回判断部分,重新getchar;
}
putchar(ch);
}
return 0;
}
💬 清理缓冲区:用户输入密码后,让用户确认(Y/N)
int main(void) {
char password[20] = {0};
printf("请输入密码:>");
scanf("%s", password);
printf("请确认密码(Y/N) :>");
int ch = getchar();
if(ch == 'Y') {
printf("确认成功\n");
} else {
printf("确认失败\n");
}
return 0;
}
🚩 运行结果:(假设用户输入了123456;Y)确认失败
❓ 为什么还没有让用户确认(Y/N)就显示确认失败了?
🔑 解析:输入函数并不是从键盘上读取,而是从缓冲区中读取内容的;键盘输入123456时敲下回车键,此时为 “123456\n”,这时scanf将123456取走,getchar读取到的就是“\n”了,因为“\n”不是Y,执行了else的结果,所以显示确认失败。
💡 解决方案:在 scanf 后加上一个“读取 \n ”的 getchar()
int main(void) {
char password[20] = {0};
printf("请输入密码:>");
scanf("%s", password);
printf("请确认密码(Y/N) :>");
// 清刷缓冲区;
getchar()
int ch = getchar();
if(ch == 'Y') {
printf("确认成功\n");
} else {
printf("确认失败\n");
}
return 0;
}
🚩 (假设用户输入了123456;Y)确认成功
🚩 (假设用户输入了123 456;Y)确认失败
❓“用户输入了空格,确认Y,为什么显示确认失败?”
🔑 解析:刚才加入的一个getchar()处理掉了空格,导致后面“\n”没人管了;
💡 解决方案:加入循环
int main(void) {
char password[20] = {0};
printf("请输入密码:>");
scanf("%s", password);
printf("请确认密码(Y/N) :>");
// 清理缓冲区的多个字符;
int tmp = 0;
while( (tmp = getchar()) != '\n' ) {
;
}
int ch = getchar();
if(ch == 'Y') {
printf("确认成功\n");
} else {
printf("确认失败\n");
}
return 0;
}
🚩 (假设用户输入了123 456;Y)确认成功
0x04 for 循环
📚 定义:
① 表达式1:初始化部分,用于初始化循环变量。
② 表达式2:条件判断部分,用于判断循环终止。
③ 表达式3:调整部分,用于循环条件的调整。
📌 注意事项:
① 为了防止for循环失去控制,禁止在for循环体内修改循环变量。
② for循环内的表达式可以省略,但是得注意。
📜 建议:
① 建议使用“左闭区间,右开区间”的写法:
for( i=0; i<10; i++ ) 左闭,右开区间 ✅
for( i=0; i<=9; i++ ) 左右都是闭区间 ❎
② 不要在for循环体内修改循环变量,防止for循环失去控制。
💬 for 的使用方法演示
① 利用 while 循环打印1~10数字:
int main(void) {
int i = 1; // 初始化
while(i<=10) { //判断部分
printf("%d ", i);
i++; // 调整部分
}
return 0;
}
🚩 运行结果:1 2 3 4 5 6 7 8 9 10
② 利用 for 循环打印1~10数字:
int main(void) {
int i = 0;
for(i=1; i<=10; i++) {
printf("%d ", i);
}
return 0;
}
🚩 运行结果:1 2 3 4 5 6 7 8 9 10
💬 break 语句在 for 循环中的效果:
int main(void) {
int i = 0;
for(i=1; i<=10; i++) {
if(i==5) { // 当i==5时;
break; // 直接跳出循环;
}
printf("%d ", i);
}
}
🚩 运行结果:1 2 3 4
❓ 什么没有打印5?
🔑 解析:因为当 i==5 时,break 跳出了循环,循环中 break 之后的语句全都不再执行,printf 位于 break 之后,所以5自然不会被打印出来;
💬 continue 在 for 循环中的效果
if 中的 continue 会陷入死循环,但是在 for 中并不会:
int main(void) {
int i = 0;
for(i=1; i<=10; i++) {
if(i == 5)
continue; // 跳至调整部分(i++);
printf("%d ", i);
}
}
🚩 运行结果:1 2 3 4 5 6 7 8 9 10
❓ 这里为什么又没打印 5?
🔑 解析:因为当 i==5 时,continue 跳至调整部分,此时 i++,i 为6。同上,所以5自然不会被打印。i 为6时,if 不成立,继续打印,最终结果为 1 2 3 4 6 7 8 9 10(跳过了5的打印);
💬 for 循环体内修改循环变量的后果:
int main(void) {
int i = 0;
for (i=0; i<10; i++) {
if (i = 5) {
printf("haha\n");
}
printf("hehe\n");
}
return 0;
}
🚩 hehehahahehehaha…… 💀死循环
0x05 for 循环的嵌套
📚 定义:
① for 循环是允许嵌套的;
② 外部的 for 循环称为外部循环,内部的 for 循环称为内部循环;
💬 for 嵌套的演示:
int main(void) {
int i = 0;
int j = 0;
for (i=0; i<10; i++) {
for (j=0; j<10; j++) {
printf("hehe\n");
}
}
// 10x10 == 100
return 0;
}
🚩 (打印了100个hehe)
0x06 for 循环的省略
📚 for 循环的省略:
① for 循环的 "初始化、判断部分、调整部分" 都可以省略。
② 判断部分的省略 - 判断部分恒为真 - 死循环 💀。
③ 如果不是非常熟练,建议不要省略。
💬 判断部分的省略:
int main(void) {
// 判断部分恒为真 - 死循环
for(;;) {
printf("hehe\n");
}
return 0;
}
🚩 hehehehehehe…… 💀死循环
💬 省略带来的弊端
假设我们希望下列代码能打印 9 个呵呵:
int main(void) {
int i = 0;
int j = 0;
for(; i<3; i++) {
for(; j<3; j++) {
printf("hehe\n");
}
}
return 0;
}
🚩 运行结果:hehe hehe hehe (只打印了3个)
🔑 解析:因为 i=0,内部 for 打印了3次 hehe,此时 j=3,这时 i++,j因为没有初始化,所以此时 j仍然是3,而判断部分要求 j<3,自然就不再打印了,程序结束。
❓ 请问要循环多少次?
int main(void) {
int i = 0;
int k = 0;
int count = 0;
for(i=0,k=0; k=0; i++,k++) {
k++;
count++;
}
printf("count:%d", count);
return 0;
}
💡 答案:count = 0,一共循环0次。
🔑 解析:判断部分 k=0,赋值为 0 时为假,所以一次都不会循环。
0x07 do...while 循环
📚 定义:在检查 while() 条件是否为真之前,该循环首先会执行一次 do{} 之内的语句,然后在 while() 内检查条件是否为真,如果条件为真,就会重复 do...while 这个循环,直至 while() 为假。
📌 注意事项:
① do...while 循环的特点:循环体至少执行一次。
② do...while 的使用场景有限,所以不是经常使用。
③ 简单地说就是:不管条件成立与否,先执行一次循环,再判断条件是否正确。
💬 do...while 使用方法演示:
int main(void) {
int i = 1;
do {
printf("%d ", i);
i++;
} while(i<=10);
return 0;
}
🚩 运行结果: 1 2 3 4 5 6 7 8 9 10
💬 break 语句在 do...while 循环中的效果:
int main(void) {
int i = 1;
do {
if(i==5) {
break;
}
printf("%d ", i);
i++;
} while(i<10);
return 0;
}
🚩 运行结果:1 2 3 4
💬 continue 语句在 do...while 循环中的效果:
int main(void) {
int i = 1;
do {
if(i == 5)
continue;
printf("%d ", i);
i++;
}
while(i<=10);
return 0;
}
0x08 goto 语句
📚 C语言中提供了可以随意滥用的 goto 语句和标记跳转的标号。最常见的用法就是终止程序在某些深度嵌套的结构的处理过程。
“ goto 语句存在着争议”
1. goto 语句确实有害,应当尽量避免。
2. 理论上讲goto语句是没有必要的,实践中没有goto语句也可以很容易的写出代码。
3. 完全避免使用 goto 语句也并非是个明智的方法,有些地方使用 goto 语句,会使程序流程 更清楚、效率更高。
📌 注意事项:goto 语句只能在一个函数内跳转。
💬 可以考虑使用 goto 的情形:
for(...) {
for(...) {
for(...) {
// HOW TO ESCAPE?
}
}
}
💬 体会 goto 语句的特点:
int main(void) {
flag:
printf("hehe\n");
printf("haha\n");
goto flag;
return 0;
}
🚩 hehehahahehehaha (💀 死循环)
💬 goto实战:一个关机程序
C语言提供的用于执行系统命令的函数:system()
关机指令:shutdown -s -t 60 (60秒后关机)
取消关机:shutdown -a
#include
#include
#include
int main(void) {
char input[20] = {0}; // 存放输入的信息;
system("shutdown -s -t 60"); // 关机指令;
printf("[系统提示] 计算机将在一分钟后关机 (取消指令:/cancel) \n");
again:
printf("C:\\Users\\Admin> ");
scanf("%s", &input);
if(strcmp(input, "/cancel") == 0) {
system("shutdown -a"); // 取消关机;
printf("[系统提示] 已取消。\n");
} else {
printf("'%s' 不是内部或外部命令,未知指令。\n", input);
printf("\n");
goto again;
}
return 0;
}
第三章 - 函数
本章将对于C语言函数的定义和用法进行讲解,并且对比较难的递归部分进行详细画图解析,并对栈和栈溢出进行一个简单的叙述。同样,考虑到目前处于基础阶段,本章配备练习便于读者巩固。
一、函数
0x00 函数的定义
📚 数学中,f(x) = 2*x+1、f(x, y) = x + y 是函数...
在计算机中,函数是一个大型程序中的某部分代码,由一个或多个语句块组成;
它负责完成某项特定任务,并且相较于其他代码,具备相对的独立性;
📌注意事项:
1. 函数设计应追求“高内聚低耦合”;
(即:函数体内部实现修改了,尽量不要对外部产生影响,否则:代码不方便维护)
2. 设计函数时,尽量做到谁申请的资源就由谁来释放;
3. 关于return,一个函数只能返回一个结果;
4. 不同的函数术语不同的作用域,所以不同的函数中定义相同的名字并不会造成冲突;
5. 函数可以嵌套调用,但是不能嵌套定义,函数里不可以定义函数;
7. 函数的定义可以放在任意位置,但是函数的声明必须放在函数的使用之前;
📜 箴言:
1. 函数参数不宜过多,参数越少越好;
2. 少用全局变量,全局变量每个方法都可以访问,很难保证数据的正确性和安全性;
0x01 主函数
( 这里不予以赘述,详见第一章)
📌 注意事项
1. C语言规定,在一个源程序中,main函数的位置可任意;
2. 如果在主函数之前调用了那些函数,必须在main函数前对其所调用函数进行声明,或包含其被调用函数的头文件;
0x02 库函数
❓ 为什么会有库函数?
📚 “库函数虽然不是业务性的代码,但在开发过程中每个程序员都可能用得到,为了支持可移植性和提高程序的效率,所以C语言基础库中提供了库函数,方便程序员进行软件开发”
📌 注意事项:库函数的使用必须要包含对应的头文件;
📜 箴言:要培养一个查找学习的好习惯;
💡 学习库函数
1. MSDN;
2. c++:www.cplusplus.com;
3. 菜鸟教程:C 语言教程 | 菜鸟教程;
🔺 简单的总结:
IO函数、字符串操作函数、字符操作函数、内存操作函数、时间/日期函数、数学函数、其他库函数;
💬 参照文档,学习几个库函数:
“strcpy - 字符串拷贝”
#include
#include // Required Header;
int main()
{
char arr1[20] = {0}; // strDestination;
char arr2[] = "hello world"; // strSource;
strcpy(arr1, arr2);
printf("%s\n", arr1);
return 0;
}
🚩 >>> hello world
💬 参照文档,试着学习几个库函数:
“memset - 内存设置”
#include
#include // Requested Header
int main()
{
char arr[] = "hello world"; // dest
memset(arr, 'x', 5); // (dest, c, count)
printf("%s\n", arr);
return 0;
}
🚩 >>> xxxxx world
0x03 自定义函数
❓ 何为自定义函数?
“顾名思义,全部由自己设计,赋予程序员很大的发挥空间”
📚 自定义函数和其他函数一样,有函数名、返回值类型和函数参数;
1. ret_type 为返回类型;
2. func_name 为函数名;
3. paral 为函数参数;
💬 自定义函数的演示
“需求:写一个函数来找出两个值的较大值”
int get_max(int x, int y) { // 我们需要它返回一个值,所以返回类型为int;
int z = 0;
if (x > y)
z = x;
else
z = y;
return z; // 返回z - 较大值;
}
int main()
{
int a = 10;
int b = 20;
// 函数的调用;
int max = get_max(a, b);
printf("max = %d\n", max);
return 0;
}
🚩 >>> max = 20
0x04 函数的参数
📚 实际参数(实参)
1. 真实传给函数的参数叫实参(实参可以是常量、变量、表达式、函数等);
2. 无论实参是何种类型的量,进行函数调用时,必须有确定的值,以便把这些值传送给形参;
📚 形式参数(形参)
1. 形参实例化后相当于实参的一份临时拷贝,修改形参不会改变实参;
2. 形式参数只有在函数被调用的过程中才实例化;
3. 形式参数在函数调用完后自动销毁,只在函数中有效;
📌 注意事项:
1. 形参和实参可以同名;
2. 函数的形参一般都是通过参数压栈的方式传递的;
3. “形参很懒”:形参在调用的时才实例化,才会开辟内存空间;
0x05 函数的调用
📚 传值调用
1. 传值调用时,形参是实参的一份临时拷贝;
2. 函数的形参和实参分别占用不同内存块,对形参的修改不会影响实参;
3. 形参和实参使用的不是同一个内存地址;
📚 传址调用
1. 传址调用时可通过形参操作实参;
2. 传址调用是把函数外部创建的变量的内存地址传递给函数参数的一种调用函数的方式;
3. 使函数内部可以直接操作函数外部的变量(让函数内外的变量建立起真正的联系);
💬 交换两个变量的内容
// void,表示这个函数不返回任何值,也不需要返回;
void Swap(int x, int y) {
int tmp = 0;
tmp = x;
x = y;
y = tmp;
}
int main()
{
int a = 10;
int b = 20;
// 写一个函数 - 交换2个整形变量的值
printf("交换前:a=%d b=%d\n", a, b);
Swap(a, b);
printf("交换后:a=%d b=%d\n", a, b);
return 0;
}
🚩 >>> 交换前:a=10 b=20 交换后:a=10 b=20
❓ “为何没有交换效果?是哪里出问题了吗?”
🔑 解析:Swap在被调用时,实参传给形参,其实形参是实参的一份临时拷贝。因为改变型形参并不能改变实参,所以没有交换效果;
💡 解决方案:使用传址调用(运用指针)
// 因为传过去的是两个整型地址,所以要用int*接收;
void Swap2(int* pa, int* pb) { // 传址调用;
int tmp = *pa; // *将pa解引用;
*pa = *pb;
*pb = tmp;
}
int main()
{
int a = 10;
int b = 20;
printf("交换前:a=%d b=%d\n", a, b);
Swap2(&a, &b); // 传入的是地址;
printf("交换后:a=%d b=%d\n", a, b);
return 0;
}
0x06 函数的嵌套调用
📚 函数和函数之间可以有机合成的;
void new_line() {
printf("hehe ");
}
void three_line() {
int i = 0;
for (i=0; i<3; i++)
new_line(); // three_line又调用三次new_line;
}
int main()
{
three_line(); // 调用three_line;
return 0;
}
🚩 >>> hehe hehe hehe
0x07 函数的链式访问
📚 把一个函数的返回值作为另外一个函数的参数
int main()
{
/* strlen - 求字符串长度 */
int len = strlen("abc");
printf("%d\n", len);
printf("%d\n", strlen("abc")); // 链式访问
/* strcpy - 字符串拷贝 */
char arr1[20] = {0};
char arr2[] = "bit";
strcpy(arr1, arr2);
printf("%s\n", arr1);
printf("%s\n", strcpy(arr1, arr2)); // 链式访问
return 0;
}
💭 面试题
“结果是什么?”
int main()
{
printf("%d", printf("%d", printf("%d", 43)));
return 0;
}
🚩 >>> 4321
🔑 解析: printf函数的作用是打印,但是它也有返回值,printf的返回值是返回字符的长度;printf调用printf再调用printf("%d", 43),首先打印出43,返回字符长度2,打印出2,printf("%d", printf("%d", 43)) 又返回字符长度1,打印出1;所以为4321;
“我们可以试着再MSDN里查找printf函数的详细介绍”
0x08 函数的声明和定义
📚 函数的声明
1. 为了告诉编译器函数名、参数、返回类型是什么,但是具体是不是存在,无关紧要;
2. 函数必须保证“先声明后使用”,函数的声明点到为止即可;
3. 函数的声明一般要放在头文件中;
📚 函数的定义:是指函数的具体实现,交代函数的功能实现;
int main()
{
int a = 10;
int b = 20;
/* 函数的声明 */
int Add(int, int);
int c = Add(a, b);
printf("%d\n", c);
return 0;
}
/* 函数的定义 */
int Add(int x, int y) {
return x + y;
}
二、函数的递归
0x00 递归的定义
📚 程序调用自身称为递归(recursion)
1. 递归策略只需要少量的程序就可以描述解题过程所需要的多次重复计算,大大减少代码量;
2. 递归的主要思考方式在于:把大事化小;
📌 注意事项:
1. 存在跳出条件,每次递归都要逼近跳出条件;
2. 递归层次不能太深,避免堆栈溢出;
💬 递归演示
“接收一个整型值,按照顺序打印它的每一位(eg. 输入1234,输出 1 2 3 4)”
void space(int n)
{
if (n > 9)
{
space(n / 10);
}
printf("%d ", n % 10);
}
int main()
{
int num = 1234;
space(num);
return 0;
}
🚩 >>> 1 2 3 4
🔑 解析:
0x01 堆栈溢出
📚 堆栈溢出现象 - stackoverflow
1. 水满则溢,堆栈也有容量限制,当其超出限制,就会发生溢出;
2. 堆栈溢出可以理解为“吃多了吐”,队列溢出就是“吃多了拉”;
3. 程序员的知乎:Stack Overflow - Where Developers Learn, Share, & Build Careers
💀 危害:
1. 堆栈溢出时会访问不存在的RAM空间,造成代码跑飞,此时无法获取溢出时上下文数据,也无法对后续的程序修改提供有用信息;
2. 造成安全威胁,常见的攻击类型有:修改函数的返回地址,使其指向攻击代码,当函数调用结束时程序跳转到攻击者设定的地址,修改函数指针,长跳转缓冲区来找到可溢出的缓冲区;
💬 堆栈溢出现象演示;
void test(int n) {
if(n < 10000) {
test(n + 1);
}
}
int main()
{
test(1);
return 0;
}
0x02 递归的用法
💬 手写strlen函数
1. “创建临时变量count方法”
int my_strlen(char* str) {
int count = 0;
while (*str != '\0') {
count++;
str++;
}
return count;
}
int main()
{
char arr[] = "abc";
int len = my_strlen(arr); // 传过去的是首元素地址;
printf("len = %d\n", len);
return 0;
}
🚩 >>> len = 3
2. “不创建临时变量,利用递归完成”
/*
my_strlen("abc");
1 + my_strlen("bc");
1 + 1 + my_strlen("c");
1 +1 + 1 + my_strlen("");
1 + 1 + 1 + 0
3
*/
int rec_strlen(char* str) {
if (*str != '\0')
return 1 + rec_strlen(str+1);
else
return 0;
}
int main()
{
char arr[] = "abc";
int len = rec_strlen(arr);
printf("len = %d\n", len);
return 0;
}
🚩 >>> len = 3
0x03 递归与迭代
❓ 何为迭代:
“重复执行程序中的循环,直到满足某条件时才停止,亦称为迭代”
📚 迭代法:也称辗转法,是一种不断用变量的旧值递推新值的过程;
💬 求n的阶乘(不考虑溢出);
“阶乘公式: n! = n(n-1)”
int Fac(int n) {
if (n <= 1)
return 1;
else
return Fac(n-1) * n;
}
int main()
{
int n = 0;
scanf("%d", &n);
int ret = Fac(n);
printf("%d\n", ret);
return 0;
}
💬 求第n个斐波那契数(不考虑溢出);
“斐波拉契数列:0,1,1,2,3,5,8,13,21,34,55...”
int Fib(int n) {
if (n <= 2)
return 1;
else
return Fib(n-1) + Fib(n-2);
}
int main()
{
int n = 0;
scanf("%d", &n);
int ret = Fib(n);
printf("第%d个斐波拉契数为%d\n", n, ret);
return 0;
}
🚩 >>> (假设输入10) 第10个斐波那契数为55
>>> (假设输入20)第20个斐波那契数为6765
>>> (假设输入50)...(程序运行中,似乎卡住了)
0x04 非递归
❓ 我们发现了问题,如果用Fib这个函数计算第50个斐波那契数字的时候需耗费很长的时间;
使用Fic函数求10000的阶乘(不考虑结果的正确性),程序会崩溃;
🔑 耗费很长时间的原因是 Fib函数在调用的过程中很多计算其实在一直重复,比如计算第50个斐波那契数就要计算第49个,计算第49个斐波那契数就要计算第48个……以此类推;
💡 优化方法:将递归改写为非递归;
📜 箴言:
1. 许多问题是以递归的形式进行解释的,这只是因为他比非递归的形式更为清晰;
2. 但是这些问题的迭代实现往往比递归实现效率更高,虽然代码的可读性稍微差些;
3. 当一个问题相当复杂,难以用迭代实现时,此时递归实现的简洁性便可以补偿运行时开销;
💬 使用非递归的方式写;
1 1 2 3 5 8 13 21 34 55...
a b c
int Fib(int n) {
int a = 1;
int b = 1;
int c = 1;
while (n > 2) {
c = a + b;
a = b;
b = c;
n--;
}
return c;
}
int main()
{
int n = 0;
scanf("%d", &n);
int ret = Fib(n);
printf("%d\n", ret);
return 0;
}
💬 非递归方式求阶乘
int fac(int n) {
int ret = 1;
while(n > 1) {
ret *= n;
n -= 1;
}
return ret;
}
int main()
{
int n = 0;
scanf("%d", &n);
int ret = fac(n);
printf("%d\n", ret);
return 0;
}
三、练习
0x00 练习1
1. 写一个函数可以判断一个数是不是素数;
2. 写一个函数判断一年是不是闰年;
3. 写一个函数,实现一个整形有序数组的二分查找;
4. 写一个函数,每调用一次这个函数,就会将num的值增加1;
💬 写一个is_prime()函数可以判断一个数是不是素数;
“质数是指在大于1的自然数中,除了1和它本身以外不再有其他因数的自然数。”
#include
int is_prime(int n) {
int i = 0;
for(i=2; i
💬 写一个 is_leap_year 函数判断一年是不是闰年;
int is_leap_year(int y) {
if((y % 4 == 0) && (y % 100 != 0) || (y % 400 == 0))
return 1;
else
return 0;
}
int main()
{
int year = 0;
printf("请输入年份: ");
scanf("%d", &year);
if(is_leap_year(year) == 1)
printf("%d年是闰年\n", year);
else
printf("不是闰年\n");
return 0;
}
💬 写一个函数,实现一个整形有序数组的二分查找;
“ int arr[] = {1,2,3,4,5,6,7,8,9,10}; ”
int binary_search(int arr[], int k, int sz) {
int left = 0;
int right = sz - 1;
while(left <= right) {
int mid = (left + right) / 2;
if(arr[mid] < k)
left = mid + 1;
else if(arr[mid] > k)
right = mid - 1;
else
return mid;
}
return -1;
}
int main()
{
int arr[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int sz = sizeof(arr) / sizeof(arr[0]);
int k = 0;
printf("请输入要查找的值: ");
scanf("%d", &k);
int ret = binary_search(arr, k, sz);
if(ret == -1)
printf("找不到\n");
else
printf("找到了,下标为%d\n", ret);
return 0;
}
💬 写一个函数,每调用一次这个函数,就会将num的值增加1;
void Add(int* pnum) {
(*pnum)++;
}
int main()
{
int num = 0;
Add(&num);
printf("%d\n", num);
Add(&num);
printf("%d\n", num);
Add(&num);
printf("%d\n", num);
return 0;
}
🚩 >>> 1 2 3
0x01 练习2
1. 实现一个函数,判断一个数是不是素数,利用上面实现的函数打印100到200之间的素数;
2. 交换两个整数,实现一个函数来交换两个整数的内容;
3. 自定义乘法口诀表,实现一个函数,打印乘法口诀表,口诀表的行数和列数自己指定;
💬 实现一个函数,判断一个数是不是素数;
“利用上面实现的函数打印100到200之间的素数,打印出一共有多少个素数”
int is_prime(int n) {
int j = 0;
for(j=2; j
🚩 >>> 101 103 107 109 113 127 131 137 139 149 151 157 163 167 173 179 181 191 193 197 199 一共有21个素数
💬 交换两个整数;
“实现一个函数来交换两个整数的内容”
void Swap(int* pa, int* pb) {
int tmp = 0;
tmp = *pa;
*pa = *pb;
*pb = tmp;
}
int main()
{
int a = 10;
int b = 20;
printf("交换前: a=%d, b=%d\n", a, b);
Swap(&a, &b);
printf("交换后: a=%d, b=%d\n", a, b);
return 0;
}
🚩 >>> 交换前: a=10, b=20 交换后: a=20, b=10
自定义乘法口诀表;
“实现一个函数,打印乘法口诀表,口诀表的行数和列数自己指定”
(eg.输入9,输出9*9口诀表,输出12,输出12*12的乘法口诀表。)
void formula_table(int line)
{
int i = 0;
for(i=1; i<=line; i++) {
int j = 0;
for(j=1; j<=i; j++) {
printf("%dx%d=%-2d ", j, i, i*j);
}
printf("\n");
}
}
int main()
{
int line = 0;
printf("请定义行数: > ");
scanf("%d", &line);
formula_table(line);
return 0;
}
0x02 练习3
1. 字符串逆序,非递归方式的实现和递归方式的实现;
2. 写一个函数DigitSum(n),输入一个非负整数,返回组成它的数字之和;
3. 编写一个函数实现n的k次方,使用递归实现;
💬 字符串逆序
编写一个函数 reverse_string(char * string);
将参数字符串中的字符反向排列,不是逆序打印;
要求:不能使用C函数库中的字符串操作函数;
(eg. char arr[] = "abcdef"; 逆序之后数组的内容变成:fedcba)
非递归实现:
int my_strlen(char* str) {
if(*str != '\0') {
return 1 + my_strlen(str + 1);
}
return 0;
}
void reverse_string(char* str) {
int len = my_strlen(str);
int left = 0;
int right = len - 1;
while(left < right) {
char tmp = str[left];
str[left] = str[right];
str[right] = tmp;
left++;
right--;
}
}
int main()
{
char arr[] = "abcdef";
reverse_string(arr);
printf("%s\n", arr);
return 0;
}
🚩 >>> fedcba
递归实现:
1. [] 写法
int my_strlen(char* str) {
int count = 0;
while(*str != '\0') {
count++;
str++;
}
return count;
}
void reverse_string(char *str) {
int len = my_strlen(str);
int left = 0; // 最左下标
int right = len - 1; // 最右下标
char tmp = str[left];
str[left] = str[right];
str[right] = '\0';
// 判断条件
if(my_strlen(str + 1) >= 2) {
reverse_string(str + 1);
}
str[right] = tmp;
}
int main()
{
char arr[] = "abcdef";
reverse_string(arr);
printf("%s\n", arr);
return 0;
}
2. *写法
int my_strlen(char* str) {
if(*str != '\0') {
return 1 + my_strlen(str + 1);
}
return 0;
}
void reverse_string(char* str) {
int len = my_strlen(str);
char tmp = *str;
*str = *(str + len-1);
*(str + len-1) = '\0';
if(my_strlen(str + 1) >= 2) {
reverse_string(str + 1);
}
*(str + len-1) = tmp;
}
int main()
{
char arr[] = "abcdef";
reverse_string(arr);
printf("%s\n", arr);
return 0;
}
💬 写一个递归函数DigitSum(n),输入一个非负整数,返回组成它的数字之和;
“调用DigitSum(1729),则应该返回1+7+2+9,它的和是19”(eg. 输入:1729,输出:19)
int digit_sum(int n) {
if (n > 9) {
return digit_sum(n / 10) + (n % 10);
} else {
return 1;
}
}
int main()
{
int n = 1729;
int ret = digit_sum(n);
printf("%d\n", ret);
return 0;
}
🚩 >>> 19
🔑 解析:
digit_sum(1729)
digit_sum(172) + 9
digit_sum(17) + 2 + 9
digit_sum(1) + 7 + 2 + 9
1+7+2+9 = 19
💬 编写一个函数实现n的k次方,使用递归实现
“递归实现n的k次方”
double Pow(int n, int k) {
if (k == 0)
return 1.0;
else if(k > 0)
return n * Pow(n, k-1);
else // k < 0
return 1.0 / (Pow(n, -k));
}
int main()
{
int n = 0;
int k = 0;
scanf("%d^%d", &n, &k);
double ret = Pow(n, k);
printf("= %lf\n", ret);
return 0;
}
🚩 >>> (假设输入 2^3)8.000000 (假设输入 2^-3)0.125000
🔑 解析:
1. k=0,结果为1;
2. k>0,因为n的k次方等同于n乘以n的k次方-1,可以通过这个“大事化小”;
3. k<0,k为负指数幂时可化为 1 / n^k
第四章 - 数组
前言
本章将对C语言的数组进行讲解,从一维数组开始讲起。已经学了三个章节了,所以本章还附加了三子棋和扫雷两个简单的小游戏,读者可以试着写一写,增加编程兴趣,提高模块化编程思想。
一、一维数组
0x00 何为数组
📚 数组,即为一组相同类型的元素的集合;
0x01 一维数组的创建
📚 数组的创建
① type_t:数组的元素类型;
② arr_name:数组名;
③ const_n:常量表达式,用于指定数组大小;
📌 注意事项
① 数组创建,[ ] 中要给定常量,不能使用变量;
② 数组 [ ] 中的内容如果不指定大小(不填),则需要初始化;
💬 一维数组创建方法演示
💬 const_n中要给定一个常量,不能使用变量
int main()
{
int count = 10;
int arr[count]; // error
return 0;
}
#define N 10
int main()
{
int arr2[N]; // yes
return 0;
}
0x02 一维数组的初始化
📚 初始化:在创建数组的同时给数组的内容置一些合理的初始值;
💬 初始化演示
int main()
{
int arr1[10]; // 创建一个大小为10的int类型数组
char arr2[20]; // 创建一个大小为20的char类型数组
float arr3[1]; // 创建一个大小为1的float类型数组
double arr4[] = {0}; // 创建一个不指定大小的double类型数组(需要初始化)
return 0;
}
💬 字符数组初始化
int main()
{
char ch1[5] = {'b', 'i', 't'};
char ch2[] = {'b', 'i', 't'};
char ch3[5] = "bit"; // 'b', 'i', 't', '\0', '0'
char ch4[] = "bit"; // 'b', 'i', ''t, '\0'
return 0;
}
💬 字符数组初始化的两种写法
双引号写法自带斜杠0,花括号写法不自带斜杠0(需要手动添加)
int main()
{
char ch5[] = "bit"; // b, i, t, \0 【自带斜杠0】
char ch6[] = {'b', 'i', 't'}; // b i t 【不自带斜杠0】
printf("%s\n", ch5);
printf("%s\n", ch6);
return 0;
}
没有 \0 时,strlen读取时并不会知道什么时候结束,strlen:遇到斜杠0就停止
int main()
{
char ch5[] = "bit"; // b, i, t, \0 【自带斜杠0】
char ch6[] = {'b', 'i', 't'}; // b i t 【不自带斜杠0】
printf("%d\n", strlen(ch5));
printf("%d\n", strlen(ch6));
return 0;
}
🚩 >>> 3 随机值
💡 当然,你可以给他手动加上一个斜杠0,这样就不会是随机值了;
int main()
{
char ch5[] = "bit"; // b, i, t, \0 【自带斜杠0】
char ch6[] = {'b', 'i', 't', '\0'}; // b, i, t, + '\0' 【手动加上斜杠0】
printf("%d\n", strlen(ch5));
printf("%d\n", strlen(ch6));
return 0;
}
🚩 >>> 3 3
0x03 一维数组的使用
📚 下标引用操作符: [ ] ,即数组访问操作符;
📚 数组的大小计算方法:整个数组的大小除以一个字母的大小
💬 打印一维数组
可以利用 for 循环,逐一打印数组
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
int sz = sizeof(arr) / sizeof(arr[0]);
int i = 0;
for(i = 0; i < sz; i++)
printf("%d ", arr[i]);
return 0;
}
🚩 >>> 1 2 3 4 5 6 7 8 9 10
🔺 总结:
① 数组是使用下标来访问的,下标从0开始;
② 可以通过计算得到数组的大小;
0x04 一维数组在内存中的存储
📚 按地址的格式打印:%p (十六进制的打印)
💬 一维数组的存储方式
int main()
{
int arr[10] = {0};
int i = 0;
int sz = sizeof(arr) / sizeof(arr[0]);
for(i = 0; i < sz; i++)
printf("&arr[%d] = %p\n", i, &arr[i]);
return 0;
}
🚩 运行结果如下:
💡 仔细检视输出结果可知:随着数组下标的增长,元素的地址也在有规律的递增;
🔺 结论:数组在内存中时连续存放的;
二、二维数组
0x00 二维数组的创建
📚 二维数组 [行] [列]
① const_n1:行
② const_n2: 列
💬 二维数组的创建
int main()
{
int arr[3][4]; // 创建一个3行4列的int型二维数组;
/*
0 0 0 0
0 0 0 0
0 0 0 0
*/
char arr[3][5]; // 创建一个3行5列的char型二维数组;
double arr[2][4]; // 创建一个2行4列的double型二维数组;
return 0;
}
0x01 二维数组的初始化
📚 初始化:在创建数组的同时给数组的内容置一些合理的初始值;
📌 注意事项:
① 二维数组初始化时,行可以省略,但是列不可以省略;
② 二维数组在内存中也是连续存放的;
💬 初始化演示
int main()
{
int arr[3][4] = {1,2,3,4,5};
/*
1 2 3 4
5 0 0 0
0 0 0 0
*/
int arr[3][4] = {1,2,3,4,5,6,7,8,9,10,11,12}; // 完全初始化
int arr2[3][4] = {1,2,3,4,5,6,7}; // 不完全初始化 - 后面补0;
int arr3[3][4] = {
{1,2}, {3,4}, {4,5}}; // 指定;
/*
1 2 0 0
3 4 0 0
4 5 0 0
*/
return 0;
}
💬 关于 " 行可以省略,列不可以省略 "
int main()
{
int arr1[][] = {
{2,3}, {4,5}}; // error
int arr2[3][] = {
{2,3}, {4,5}}; // error
int arr2[][4] = {
{2,3}, {4,5}}; // √
return 0;
}
0x03 二维数组的使用
💬 打印二维数组
同样是通过下标的方式,利用两个 for 循环打印
int main()
{
int i = 0;
int j = 0;
for (i = 0; i < 3; i++) {
for (j = 0; j < 4; j++)
printf("%d", arr4[i][j]); // 二维数组[行][列];
printf("\n"); // 换行;
}
}
💬 二维数组在内存中的存储
int main()
{
int arr[3][4];
int i = 0;
int j = 0;
for(i = 0; i < 3; i++) {
for(j = 0; j < 4; j++)
printf("&arr[%d][%d] = %p\n", i, j, &arr[i][j]);
}
return 0;
}
🚩 运行结果如下:
💡 仔细检视输出结果,我们可以分析到其实二维数组在内存中也是连续存存放的;
🔺 结论:二维数组在内存中也是连续存放的;
三、数组作为函数参数
0x00 关于数组名
📚 数组名是首元素的地址(有两个例外)
⭕ 例外1:
sizeof(数组名) 计算的是整个数组的大小
💬 验证
int main()
{
int arr[10] = {0};
printf("%d\n", sizeof(arr));
return 0;
}
🚩 >>> 40
⭕ 例外2:
& 数组名 表示整个数组,取出的是整个数组的地址
0x01 冒泡排序(Bubble Sort)
📚 冒泡排序核心思想:两两相邻元素进行比较,满足条件则交换;
① 先确认趟数;
② 写下一趟冒泡排序的过程;
③ 最后进行交换;
📌 注意事项:
① int arr [ ] 本质上是指针,int * arr ;
② 数组传参时,实际上传递的是数组的首元素地址;
③ sz 变量不能在 bubble_sort内部计算,需要在外部计算好再传递进去;
💬 冒泡排序:请编写一个bubble_sort ( ) 函数,升序,int arr[] = {9,8,7,6,5,4,3,2,1,0} ;
#include
void bubble_sort (int arr[], int sz) // 形参arr本质上是指针 int* arr
{
/* 确认趟数 */
int i = 0;
for(i = 0; i < sz; i++)
{
/* 一趟冒泡排序干的活 */
int j = 0;
for(j = 0; j <= (sz-1-i); j++) // -1:最后一趟不用排,-i:减去已经走过的趟
{
/* 如果前面数比后面数大,就交换 */
if(arr[j] > arr[j + 1])
{
/* 创建临时变量交换法 */
int tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
}
}
}
}
int main(void)
{
int arr[] = {9,8,7,6,5,4,3,2,1,0};
int sz = sizeof(arr) / sizeof(arr[0]);
/* 冒泡排序 */
bubble_sort(arr, sz); // 数组传参的时候,传递的是首元素的地址
/* 打印数组 */
int i = 0;
for(i=0; i<=sz; i++)
printf("%d ", arr[i]);
return (0);
}
🚩 >>> 0 1 2 3