本笔记使用教程:https://www.icourse163.org/course/ZJU-199001#/info
程序设计入门——C语言 (翁恺)
前言:
我还没学完,等我学完我整理一下顺序等我有时间了再整理吧还是
不整理了直接就按教的顺序来吧
赋值运算符:
在C语言中,赋值是一个运算符。
赋值是运算(自右向左),也有结果
a=b=6 --> a=(b=6)
嵌入式赋值:(不要使用)
1 2 3 4 5 |
int a = 6; int b; int c = 1 + (b = a); //(b = a)为赋值的结果 printf("%d\n", c); |
递增/递减运算符:
(可以单独使用,不要组合进表达式)
"++"和"--"是单目运算符,作用是给变量+1或-1。
1 2 3 4 |
count++; count += 1; count = count + 1; //以上三式结果一样 |
此运算符可放在变量的前面或者后面,被称为前缀形式/后缀形式。
比如++的前后缀形式、共同点为都会产生a = a + 1的效果。
a++的结果是a在加1之前的的值(先赋值后运算),而++a的结果是a加了1之后的值(先运算后赋值)。
减减同理。

关系运算符:
- == 相等
- != 不相等
- > 大于
- >= 大于或等于
- < 小于
- <= 小于或等于
判断是否相等的关系运算符的优先级比其他的要低,而连续的关系运算是从左到右进行的。
1 2 |
5 > 3 == 6 > 4 //连续判断 先判断左边 然后判断右边 最后判断是否相等 |
注释:
注释提供解释信息。
//在单行内使用
/**/可以多行使用,也可以用于一行内的注释
1 |
int ak = 47/*or36?*/, y = 9; |
常见的调试手段:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
//输入值 int x; //数字位数 int n = 0; scanf_s("%d", &x); printf("你输入的值是%d\n", x); while (x > 0) { printf("进入循环\n");//此处 n++; x /= 10; printf("x = %d, n = %d\n", x, n);//与此处 } printf("%d\n", n); |
输入1234时输出结果为:
1 2 3 4 5 6 7 8 9 10 |
你输入的值是1234 进入循环 x = 123, n = 3 进入循环 x = 12, n = 4 进入循环 x = 1, n = 5 进入循环 x = 0, n = 6 6 |
do-while:
do-while 循环是在循环的尾部检查它的条件。
do-while 循环与 while 循环类似,但是 do-while 循环会确保至少执行一次循环。
1 2 3 4 5 |
do { statement(s); }while( condition ); |
For循环:
for循环像一个计数循环:设定一个计数器,初始化它,然后在计数器到达某值之前,重复执行循环体,而每执行一轮循环,计数器值以一定步进进行调整,比如+1或-1。
1 2 3 4 |
for(i=0;i<5;i++) { printf("%d",i); } |
For可以读作"对于":
对于一开始的i=0,当i<5时,重复做循环体,每一轮循环在做完循环体内语句后,使得i++。
求和的程序计数器初始化应为0,求积的程序计数器初始化应为1。
bool类型:
加入头文件以使用bool类型
1 2 |
#include <stdbool.h> bool b = true; |
但是实际上bool是一个整数
逻辑运算:与或非
下表显示了C语言支持的所有关系逻辑运算符。假设变量A的值为1,变量B的值为0,则:
运算符 | 描述 | 实例 |
---|---|---|
&& | 称为逻辑与运算符。如果两个操作数都非零,则条件为真。 | (A && B) 为假。 |
|| | 称为逻辑或运算符。如果两个操作数中有任意一个非零,则条件为真。 | (A || B) 为真。 |
! | 称为逻辑非运算符。用来逆转操作数的逻辑状态。如果条件为真则逻辑非运算符将使其为假。 | !(A && B) 为真。 |

短路:
逻辑运算是自左向右运行的,如果左边的结果已经能够决定结果了,就不会做右边的计算。
a == 6 && b == 1
a == 6 && b += 1
对于与(&&)左边是false就不做右边了
对于或(||)左边是true就不做右边
因此最好不要把赋值组合进表达式
条件运算符:
条件运算符是C语言中唯一的一个三目运算符,其求值规则为:如果表达式1的值为真,则以表达式2 的值作为整个条件表达式的值,否则以表达式3的值作为整个条件表达式的值。条件表达式通常用于赋值语句之中。
1 2 3 4 5 6 7 8 9 10 11 |
表达式1 ? 表达式2 : 表达式3 //标准形式 if(a>b){ max = a; }else{ max = b; } //上下两式等价 max = (a>b) ? a : b; //如a>b为真,则把a赋予max,否则把b 赋予max。 |
不要使用嵌套的条件运算符
逗号运算:
逗号用来连接两个表达式,并以其右边的表达式的值作为他的结果。逗号的优先级是所有的运算符中最低的,所以他两边的表达式会先计算;逗号的组合关系是自左向右,所以左边的表达式会先计算,而右边的表达式的值就留下来作为逗号运算的结果。
主要是在for循环中使用。(目前来说只有这一个用处)
1 |
for(i=0, j=0; i<j; i++, j--) |
对于If语句的良好习惯:
在if或else后面总是用{}(即使是只有一条语句的时候)
避免在if的嵌套用法下出现else的对if的链接错误,
因为else总是链接最近的可连接的if。
级联的if-else if:
1 2 3 4 5 6 7 8 9 10 11 12 |
if ( x < 0 ) { f = -1; } else if ( x == 0 ) { f = 0; } else { f = 2; } |
事实上 第二个else是属于第一个if的 最后一个else是属于第二个if的,他们是层层递进的一个情况。
else if的出现是省掉了else内含另一个if的情况,使代码更整齐。

更灵活的代码:
(not‘单一出口’)
对比两种输出结果相同的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
int f; if (x < 0) { f = -1; } else if (x == 0) { f = 0; } else { f = 2 * x; } printf("%d", f); |
1 2 3 4 5 6 7 8 9 10 11 12 |
if (x < 0) { printf("-1"); } else if (x == 0) { printf("0"); } else { printf("%d", 2 * x); } |
左边更加灵活,在前十三行 都与怎么使用"f"没有关系,而右边就把代码写死了,f只能做一件事情,即printf结果。
左边的前十三行只是算出来,至于后面如参与其他计算或者是函数返回都可以,其使用了单一出口。
多路分支switch-case:
switch语句可以看作是一种基于计算的跳转,计算控制表达式的值后,程序会跳转到相匹配的case(分支标号)处。case只是入口,break为出口。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
switch (控制表达式) { case 常量: 语句; break; case 常量: 语句; break; case 常量: 语句; break; default: 语句; break; } |
- 控制表达式只能是整数型的结果
- 常量可以是常数,也可以是常数计算的表达式
- case只是说明switch内部位置的路标,在执行完分支中的最后一条语句后,如果后面没有break,就会顺序执行到下面的case里去,直到遇到一个break,或者switch结束为止。
事实上也可以写成这样:
1 2 3 4 5 6 7 |
switch (控制表达式) { case 常量:语句;break; case 常量:语句;break; case 常量:语句;break; default:语句;break; } |
关于模除:
大数字模除小数字:5 % 2 == 1
是因为5里面有两个2,剩下1,被除数为5 计算的基础就是5。
小数字模除大数字:2 % 3 == 2
是因为2里面没有3,所以2全剩下了,计算的基础(被除数)没有被改动。
一个关于平均数的例子:
算法:
- 初始化sum和count为0;
- 读入number;
- 如果number不是-1,则将number加入sum,并将count加到1,回到2;
- 如果number是-1,则计算和打印出sum/count(转换成浮点来计算)

1 2 3 4 5 6 7 8 9 10 |
do{ scanf_s("%d", &number); if (number != -1) { sum += number; count++; } } while (number != -1); printf("%f", 1.0 * sum / count); |
每次循环对number进行了两次判断
1 2 3 4 5 6 7 8 9 10 |
scanf_s("%d", &number); while (number != -1) { sum += number; count++; scanf_s("%d", &number); } printf("%f", 1.0 * sum / count); //注意转为浮点数 |
优化方法
一个猜数游戏:
让计算机来想一个数,让用户来猜,用户每输入一个数,就告诉他是大了还是小了,直到用户猜中为止,最后还要告诉用户猜了多少次。
因为需要不断重复让用户猜,所以需要用循环。在实际写出程序之前,我们可以先用文字描述程序的思路。核心重点是循环的条件。
文字设计:
- 计算机随机生成数字赋值给number;
- 初始化负责计次的count为0;
- 让用户输入一个数字a;
- count递增(++);
- 判断a和number的大小关系,如果a大输出"大"如果a小输出"小";
- 如果a与number是不相等的程序回到3;
- 否则程序输出猜中数字和猜测次数;
- 程序结束;
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
#include <stdio.h> #include <stdlib.h> #include <time.h> main(void) { srand(time(0)); int number = rand() % 100 + 1; int count = 0; int a = 0; printf("我已经想好了一个1到100之间的一个数。\n"); do { printf("猜猜这个数:"); scanf_s("%d", &a); count++; if (a > number) printf("你猜的数大了\n"); else if (a < number) printf("你猜的数小了\n"); } while (a != number); printf("答对了!你用了%d次就猜到了答案!\n", count); return 0; } |
rand()-(随机数生成):
目前不需要理解rand所需要的头文件和用法
1 2 3 4 5 6 7 8 9 10 11 |
#include <stdio.h> #include <stdlib.h> #include <time.h> main(void) { srand(time(0)); int a = rand(); return 0; } |
对一个大的随机数的处理(%100):
x % n 的结果是[0,n-1]的一个整数。
对一个数逆序:
整数的分解:
- 对一个整数 %10 就得到它的个位数;
- 对一个整数 /10 就去掉它的个位数;
- 重复%10 就能够得到接下来的十位数、百位数等;
实现代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
#include <stdio.h> main(void) { int x; scanf_s("%d", &x); //需要处理的数 int digit; //输入的x值的最后一位 int ret = 0; //将输出的结果 while (x > 0) { digit = x % 10; //printf("%d", digit); //直接输出逆序 可做结果 ret = ret * 10 + digit; printf("x=%d digit=%d ret=%d\n", x, digit, ret); //调试输出 不是需求结果 x /= 10; } printf("%d\n", ret); return 0; } |
一些注意:
- 重视warning提示
- 注意大括号小括号和分号
判断一个数是不是素数:
素数的定义:
只能被1和这个数本身整除的数,不包括1;
如2,3,5,7,11,13,17,19.....
实现代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
#include <stdio.h> main(void) { int x; scanf_s("%d", &x); int i; int isPrime = 1; //x是素数 for (i = 2; i < x; i++) //如果i比x小,那就用x除i { if (x % i == 0) //如果能整除 { isPrime = 0; //那就不是素数 break; } } if (isPrime == 1) { printf("是素数\n"); } else if (isPrime == 0) { printf("不是素数\n"); } else printf("未知错误"); return 0; } |
循环控制:
break:
直接跳出循环;
continue:
跳过循环这一轮剩下的语句进入下一轮循环;
右边是两者的流程图

输出一定的素数:
输出2到100以内的素数:
注意:使用了[循环嵌套]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
#include <stdio.h> main(void) { int x; for (x = 2; x < 100; x++) { int i; int isPrime = 1; //x是素数 for (i = 2; i < x; i++) //如果i比x小,那就用x除i { if (x % i == 0) //如果能整除 { isPrime = 0; //那就不是素数 break; } } if (isPrime == 1) { printf("%d ", x); } } return 0; } |
输出前五十个素数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
#include <stdio.h> main(void) { int x = 2; int cnt = 0;//计数器 while (cnt < 50)//限制输出五十个素数 { int i; int isPrime = 1; //x是素数 for (i = 2; i < x; i++) //如果i比x小,那就用x除i { if (x % i == 0) //如果能整除 { isPrime = 0; //那就不是素数 break; } } if (isPrime == 1) { printf("%d ", x); cnt++;//记录有多少个素数被输出 } x++; } printf("\n"); return 0; } |
用小数目的钱拼出一个较大数目的钱数:
输出全部的组合:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
#include <stdio.h> main(void) { int x; int one, two, five; scanf_s("%d", &x);//输入元 for (one = 1; one < x * 10; one++)//从1开始穷举,一直到x*10(x化为角)为止,下面两个同理 { for (two = 1; two * 2 < x * 10; two++) { for (five = 1; five * 5 < x * 10; five++) { if (one + two * 2 + five * 5 == x * 10)//如果结果相加为x*10 此句是筛选的条件 { printf("可用%d个1角和%d个2角和%d个5角得到%d元\n", one, two, five, x); } } } } return 0; } |
仅输出第一个找到的组合:
跳出多重循环
break接力
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
#include <stdio.h> main(void) { int x; int one, two, five; int exit = 0; scanf_s("%d", &x);//输入元 for (one = 1; one < x * 10; one++)//从1开始穷举,一直到x*10(x化为角)为止,下面两个同理 { for (two = 1; two * 2 < x * 10; two++) { for (five = 1; five * 5 < x * 10; five++) { if (one + two * 2 + five * 5 == x * 10)//如果结果相加为x*10 此句是筛选的条件 { printf("可用%d个1角和%d个2角和%d个5角得到%d元\n", one, two, five, x); exit = 1;//仅输出第一个找到的结果 break; } } if (exit)break;//break接力 } if (exit)break; } return 0; } |
goto跳出
goto跳出目前建议仅用在多重循环下跳出循环
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
#include <stdio.h> main(void) { int x; int one, two, five; //int exit = 0; scanf_s("%d", &x);//输入元 for (one = 1; one < x * 10; one++)//从1开始穷举,一直到x*10(x化为角)为止,下面两个同理 { for (two = 1; two * 2 < x * 10; two++) { for (five = 1; five * 5 < x * 10; five++) { if (one + two * 2 + five * 5 == x * 10)//如果结果相加为x*10 此句是筛选的条件 { printf("可用%d个1角和%d个2角和%d个5角得到%d元\n", one, two, five, x); goto out; } } } } out: return 0; } |
goto会破坏程序结构,增加理解难度
"不用goto的话可以用其他语句代替,比如for,while,具体情况的话看实际是什么,goto的话不是这个语句有问题,而是多用这个语句的话会限制你的思想,越到后面越难写出好程序。破坏了清晰的程序结构,使程序的可读性变差,甚至成为不可维护的'面条代码'。经常带来错误或隐患,比如它可能跳过了某些对象的构造、变量的初始化、重要的计算等语句。"
f(n) = 1 + 1/2 + 1/3 + 1/4 + ... + 1/n:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
#include <stdio.h> main(void) { int n; int i; double sum = 0.0; scanf_s("%d", &n);//输入最后一个1/n的分母n for (i = 1; i <= n; i++)//分母递增 { sum += 1.0 / i;//分数处理然后加到sum中 } printf("f(%d)=%f\n", n, sum); return 0; } |
f(n) = 1 - 1/2 + 1/3 - 1/4 + ... + 1/n:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
#include <stdio.h> main(void) { int n; int i; double sum = 0.0; int sign = 1;//正负 scanf_s("%d", &n);//输入最后一个1/n的分母n for (i = 1; i <= n; i++)//分母递增 { sum += sign * 1.0 / i;//分数处理然后加到sum中 sign = -sign;//控制正负 } printf("f(%d)=%f\n", n, sum); return 0; } |
求最大公约数:
辗转相除法:
条件:如果b等于0,计算结束,a就是最大公约数;
否则:计算a除以b的余数,让a等于b,而b等于那个余数;
然后回到第一步

这个计算过程就像是把数字从右边向左边推
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
#include <stdio.h> main(void) { int a, b; int t; scanf_s("%d %d", &a, &b); while (b != 0) { t = a % b; a = b; b = t; } printf("最大公约数为%d\n", a); return 0; } |
正序分解整数:
思路大致为:
将输入的x从左到右每一位提取出来然后加上空格输出
提取方法为 x除以和他位数相同的mask 然后x去掉提取的这个数 循环
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
/* 正序分解整数 如将12345 分解为1 2 3 4 5 12345 / 10000 = 1 x mask d 12345 % 10000 = 2345 x mask x 10000 / 10 = 1000 mask mask <-------------循环-------------> 2345 / 1000 = 2 2345 % 1000 = 345 1000 / 10 = 100 345 / 100 = 3 345 % 100 = 45 100 / 10 = 10 45 / 10 = 4 45 % 10 = 5 10 / 10 = 1 5 / 1 = 5 5 % 1 = 5 1 / 10 = 0 */ #include <stdio.h> main(void) { int x; //需要被正序分解的整数 scanf_s("%d", &x); int mask = 1; int t = x; while (t > 9) { t /= 10; //数出来x的位数 mask *= 10; //然后把mask变成和x相同位数的整数 } do { int d = x / mask; //得到第一位 printf("%d", d); //直接输出得到的这个第一位数字 if (mask > 9) printf(" "); //输出空格 if限制不输出最后一个空格 //也就是最后一轮的空格将不会被输出 x %= mask; //扔掉首位数字 mask /= 10; //mask跟随x一同减一位 } while (mask > 0); //注意while这里和上面的if的判断都是针对mask的,因为x可能会出现70000这样的数字导致x在运算过程中提前为0,mask却不会为0 printf("\n"); return 0; } |
数组:
定义:
数据可以存放在变量里,每一个变量有一个名字,有一个类型,还有它的生存空间。如果我们需要保存一些相同类型、相似含义、相同生存空间的数据,我们可以用数组来保存这些数据,而不是用很多个独立的变量。数组是长度固定的数据结构,用来存放指定的类型的数据。一个数组里可以有很多个数据,所有的数据的类型都是相同的。
格式:
<类型> 变量名称[元素数量]
—in grades[100]
—doube weight[20]
元素数量必须是整数,必须是常数(C99之前)
如int a[10]:
首先它是一个int的数组
它其中有10个单元:a[0]a[1]a[2]a[3]....a[9]
每个单元就是一个int类型的变量
可以出现在赋值的左边或右边
—a[2] = a[1] + 6;
数组的初始化:
在 C 中,可以逐个初始化数组,也可以使用一个初始化语句,如下所示:
1 |
double balance[5] = {1000.0, 2.0, 3.4, 7.0, 50.0}; |
大括号 { } 之间的值的数目不能大于在数组声明时在方括号 [ ] 中指定的元素数目。
如果省略掉了数组的大小,数组的大小则为初始化时元素的个数。因此,如果:
1 |
double balance[] = {1000.0, 2.0, 3.4, 7.0, 50.0}; |
将创建一个数组,它与上一个实例中所创建的数组是完全相同的。
可以只给部分元素赋值,当 { } 中值的个数少于元素个数时,只给前面部分元素赋值。例如:
1 |
int a[10]={12, 19, 22 , 993, 344}; |
表示只给 a[0]~a[4] 5 个元素赋值,而后面 5 个元素自动初始化为 0。
int a[10]={0};也是可以的。
赋值:
1 |
balance[4] = 50.0; |
上述的语句把数组中第五个元素的值赋为 50.0。所有的数组都是以 0 作为它们第一个元素的索引,也被称为基索引,数组的最后一个索引是数组的总大小减去 1。
数组的单元:
每个单元就是一个int类型的变量
使用数组时放在[]中的数字叫做下标或索引,从0开始计数
最大的下标个数为 数组的个数-1
有效的下标范围:
编译器和运行环境都不会检查数组下标是否越界,无论数组单元是作为左值还是右值
一旦程序运行,越界的数组访问可能造成问题,导致程序崩溃;
* segmentation fault
运气好的话不会造成严重的后果
所以这是程序员的责任来保证程序只使用有效的下标值:[0,数组的大小-1]
字符常量也可以作为下标,['A']表示‘A’的ASCII码对应的数组偏移量
输出大于平均数的整数:
AKA:对“一个关于平均数的例子”的修改
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
#include <stdio.h> main(void) { int x; double sum = 0; int cnt = 0; int number[100]; //定义数组 scanf_s("%d", &x); while (x != -1) //此while记录每一个用户输入的整数,直到用户输入-1为止 { number[cnt] = x; //将"number"中的"cnt"位置上的值赋值为"x" sum += x; cnt++; scanf_s("%d", &x); } if (cnt > 0) //用户输入-1时cnt不计数 为0 { int i; double avg = sum / cnt; printf("平均数为%f\n", avg); for (i = 0; i < cnt; i++) //遍历数组到有意义的那个数 { if (number[i] > avg) //使用数组中的元素 { printf("%d ", number[i]); } } } return 0; } |
用数组统计数字出现个数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
#include <stdio.h> main(void) { int x; int count[10]; //定义数组 int i; for (i = 0; i < 10; i++) //初始化数组 { count[i] = 0; } scanf_s("%d", &x); while (x != -1) { if (x >= 0 && x <= 9) { count[x]++; //在x这个单元加一计数 } scanf_s("%d", &x); } for (i = 0; i < 10; i++) //遍历数组输出 { printf("%d:%d\n", i, count[i]); } return 0; } |
初见函数:
出现代码复制是程序质量不良的表现
将来在修改与维护的时候很可能会遇到"不仅仅只维护一处"的问题
求和的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
#include <stdio.h> void sum(int begin, int end) { int i; int sum = 0; for (i = begin; i <= end; i++) { sum += i; } printf("%d到%d的和是%d\n", begin, end, sum); } int main() { sum(1, 10); sum(20, 30); sum(35, 45); return 0; } |
定义:
函数是一块代码,接收零个或多个参数,做一件事情,并返回零个或一个值。

返回类型:代表着函数返回什么类型数据 void指的是没有类型
参数表:参数表里列开来一个个参数,逗号分隔。每一个参数都是一个类型和一个名字的组。另:()起到了表示函数定义的重要作用 即使没有参数也需要()
调用:
函数名(参数值)
同上()起到了表示函数调用的重要作用 即使没有参数也需要()
有参数则需要给出正确的数量和顺序
这些值会被按照顺序依次用来初始化函数中的参数
从函数中返回值:
return做两件事:
return停止函数的执行
送回一个值
没有返回值的函数:
void 函数名(参数表)
此时不能使用带值的return
—可以没有return
——函数会在最后的大括号结束
所以调用的时候这个函数不能赋值给其他
如果有返回值,则必须使用带值的return
函数先后关系:

把函数写在上面是因为C的编译器自上而下顺序分析代码
编译器记住了声明的函数、函数参数类型和函数返回类型
然后在其他函数中调用的时候,它就知道这个函数使用的是否正确
整个函数放到下面的话编译器会猜测函数的类型和参数
希望把函数放到下面来使代码美观,可以在顶部进行函数的声明(告诉编译器函数长这样):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
#include <stdio.h> void wuhu(); //声明 函数原型 main(void) { wuhu(); //根据原型判断正确与否 return 0; } void wuhu() //定义 编译器还会检查一次定义和声明是否一致 { printf("芜湖!"); } |
函数的参数:
参数类型不匹配:
调用函数时给的值可以与参数的类型不匹配 是C语言传统上最大的漏洞
编译器总是悄悄的把类型转换号,但是这很可能并不是期望的结果
后续的语言,C++/Java在这方面很严格
传值:
C语言在调用函数时,永远只能传"值"给函数
并且如果main里有变量ab,那么另外一个函数也有ab变量没有任何影响
每个函数有自己的变量空间,参数也位于这个独立的空间之中,和其他函数没有关系
过去对于函数参数表中的参数,叫做"形式参数",调用函数时给的值,叫做"实际参数",这种称呼方式容易误会,不继续用这种方式称呼它们。
我们认为,他们是参数和值的关系


变量:
本地变量:
函数的每次运行,就产生了一个独立的变量空间,在这个空间中的变量,是函数的这次运行所独有的,称作本地变量(/局部变量/自动变量)
所以 定义在函数内部的变量就是本地变量
(参数也是本地变量)
生存期和作用域:
生存期:什么时候这个变量开始出现了,到什么时候它消亡了
作用域:在(代码的)什么范围内可以访问这个变量(这个变量可以起作用)
对于本地变量 这两个问题的答案是大括号内:也就是"块"
规则:
本地变量是定义在块内的,它可以是定义在函数的块内,也可以定义在语句的块内。

如图 i 的生存期和作用域限于if的括号内
在括号外面使用i会报错。
事实上随便创建一对大括号来定义变量
程序运行进入块之前,其中的变量不存在,离开这个块,其中的变量就消失了。
在块外面定义的变量,在块内仍然有效。
块里面定义了和外面同名的变量将会掩盖块外面的那个变量,在走出块之后,掩盖消失,这个变量的数值仍然是外面那个一开始的变量的数值。
在同一个块里不能定义同名的变量。
本地变量不会被默认初始化。
参数在进入函数的时候就被初始化了。
(使用函数的时候,提交的那个值就作为参数初始化的值。)
函数庶事:
逗号运算符?:
调用函数时的逗号与逗号运算符的区分:
调用函数时 圆括号内的时标点,不是运算符
如:f(a,b)
而如果再加一个括号则为运算符,此时只向函数传递一个参数
f((a,b))
函数中的函数?:
不可以在函数内进行函数的声明,C语言不允许函数嵌套定义
一些奇怪的写法:
int i, j, sum(int a, int b);
return (i);
这样写是可以的,但是不要这么写
第一句是作了函数的原型声明
第二局是给i加括号 可能会被误认为return是一个函数
另外,函数的声明原型可以写成如下的样式,但是第三种编译器还是会进行"猜"的步骤
viod f(int x);
viod f(int);
viod f();
关于main:
int mian()也是一个函数
return 0也是有意义的:
对于windows可以在批处理文件中判断if errorlevel 1....(判断的就是return返回值)
传统上,一个程序如果返回了0,则表示它正常的结束了;如果它返回任何非零的值,则表示程序运行错误了。
二维数组:
长什么样:
int a[3][5];
通常理解为a是一个3行5列的矩阵

二维数组的遍历:
二维数组需要两重循环,外面遍历行号,里面遍历列号。
1 2 3 4 5 6 7 8 9 |
int i, j; int a[3][5]; for (i = 0; i < 3; i++) { for (j = 0; j < 5; j++) { a[i][j] = i * j; } } |
例中:
a[i][j]表示的是一个int
表示第i行第j列上的单元
如果是a[i,j]逗号作为运算符 这个表达式等价于a[j],这对于一个二维数组是不合法的表达。
二维数组的初始化:
同一维数组一样,二维数组可以直接给它数值。
1 2 3 4 5 |
int a[][5] = { {1,2,3,4,5}, {0,1,2,3,4}, }; |
可以没有行数,列数是必须给出的,行数可以由编译器来数,缺少的数值自动补0
可以理解为:我们现在有一个数组,这个数组里面每一个单位都是一个五个数值的数组。第一个大括号表达的就是有五个int的一个数组作为a[0],第二个也就是有五个int的一个数组作为a[1]。
可能会有不带大括号的一连串的数字这种用法,在内存上,二维数组同一维数组是一样的排列,可以理解为编译器会把数字在矩阵里逐行的从左上角到右下角填满。
tic-tia-toe游戏(井字棋):
读入一个3x3的矩阵,矩阵中的数字为1表示该位置有一个x,为0表示o
程序判断这个矩阵中是否有获胜的一方,输出表示获胜的一方的字符x或o,或者输出无人获胜。
代码 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 |
#include <stdio.h> main(void) { int board[3][3]; int i, j; int numofx = 0; int numofo = 0; int result = -1; //-1:没人赢,1:x赢,0:o赢 //读入矩阵 for (i = 0; i < 3 && result == -1; i++) { for (j = 0; j < 3; j++) { scanf_s("%d", &board[i][j]); } } //检查行 for (i = 0; i < 3 && result == -1; i++) { //三行 numofo = numofx = 0; for (j = 0; j < 3; j++) { //每一行中顺序检测 if (board[i][j] == 1) { //记录此行的棋子数量以此来判断胜负 numofx++; } else { numofo++; } } if (numofo == 3) { //如果这一行有三个o result = 0; } else if (numofx == 3) { //如果这一行有三个x result = 1; } } //检查列 for (j = 0; j < 3 && result == -1; j++) { //三列 numofo = numofx = 0; for (i = 0; i < 3; i++) { //每一列中顺序检测 if (board[i][j] == 1) { //记录此列的棋子数量以此来判断胜负 numofx++; } else { numofo++; } } if (numofo == 3) { //如果这一列有三个o result = 0; } else if (numofx == 3) { //如果这一列有三个x result = 1; } } //检查对角线(左上到右下) numofo = numofx = 0; for (i = 0; i < 3 && result == -1; i++) { if (board[i][i] == 1) { numofx++; } else { numofo++; } } if (numofo == 3) { result = 0; } else if (numofx == 3) { result = 1; } //检查对角线(左下到右上) numofo = numofx = 0; for (i = 0; i < 3 && result == -1; i++) { if (board[i][3 - i - 1] == 1) { numofx++; } else { numofo++; } } if (numofo == 3) { result = 0; } else if (numofx == 3) { result = 1; } switch (result) { case -1: printf("棋盘平局"); break; case 1: printf("X棋胜利!"); break; case 0: printf("O棋胜利!"); break; } return 0; } |
数组运算:
继承初始化的定位:
用[n]在初始化数据中给出定位,没有定位的数据排在前面的数据后面,其他位置的值补零。适合初始数据特别稀疏的数组。
1 2 3 |
int a[10] = { [0] = 2, [2] = 3, 6, } |
这个初始化实例输出后是这样的:2 0 3 6 0 0 0 0 0 0
数组的大小:
使用siziof给出整个数组所占据的内容的大小,单位是字节。
1 |
sizeof(a)/sizeof(a[0]); |
sizeof(a[0])给出了数组中单个元素的大小,于是相除就得到了数组的单元个数。
这样的代码,在修改数组中初始的数据的时候,不需要修改遍历的代码。
数组初始化中最后加上逗号是一个古老的传统:
数组给数组赋值:

数组变量本身不能被赋值
要把一个数组的所有元素交给另一个数组,必须采用遍历:
遍历数组:
通常都是使用for循环,让循环变量i从0到<数组的长度,这样循环体内最大的i正好是数组最大的有效下标。在离开循环之后也就是结束的时候i正好是数组的长度,也就是正好是数组无效的那个下标。
所以会产生一个常见的错误:离开循环后继续用i的值作为数组元素的下标
另一个常见错误是循环结束条件是<=数组长度。
search函数:
//找出key在数组a中的位置
key 要寻找的数字
a 要寻找的数组
length 数组a的长度
如果找到,返回其在a中的位置;如果找不到,则返回-1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
/* 找出key在数组a中的位置 key 要寻找的数字 a 要寻找的数组 length 数组a的长度 如果找到,返回其在a中的位置;如果找不到,则返回-1 */ #include <stdio.h> int search(int key, int a[], int length); int main(void) { int a[] = { 2,4,6,7,1,3,5,9,11,13,23,14,32 }; int x; int loc; printf("请输入一个数字:"); scanf_s("%d", &x); loc = search(x, a, sizeof(a) / sizeof(a[0])); if (loc != -1) { printf("%d在第%d个位置上\n", x, loc); } else { printf("%d不存在\n", x); } return 0; } int search(int key, int a[], int length) { int ret = -1; //默认-1 int i; for (i = 0; i < length; i++) { //作遍历 if (a[i] == key) { //遍历的每一次都拿出数组的一个单元与key比对 ret = i; break; } } return ret; } |
注意 数组作为函数参数时,往往需要另一个参数来传入数组的大小。
因为作为参数时不能在[]中给出数组的大小,不能再利用sizeof来计算数组的元素个数。
素数优化算法 (暂时没听懂):
搜索:
线性搜索(遍历):
在一个数组中找到某个数的位置(或que)
搜索的例子(也没听懂):
二分搜索:
将数组中的数据从小到大排序,最左边的单元叫做left,最右边为right。
中间量为 mid = (left + right) / 2;
判断a[mid]与key的大小关系,假设a[mid] < key; 那么包括mid它自己的 向左的所有单元都比key要小。所以left移动到mid+1;
重复计算 mid = (left + right) / 2; (若有小数点则自动截断)然后判断大小,这时如果a[mid] > key; 那么right移动到mid-1;
在a[mid] = key的时候,就是找到了这个数据的位置
在left和right相等的时候,仍然没有找到key,那么程序应该会继续计算,导致left和right错开,所以二者错开就是结束循环的关键。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
#include <stdio.h> int search(int key, int a[], int len); main(void) { int k = 10; //需要去搜素的数据 int amount[100] = { 0 }; //将要被搜索的数组 int r = search(k, amount, sizeof(amount)/sizeof(amount[0])); return 0; } int search(int key, int a[], int len) { int ret = -1; //输出 int left = 0; int right = len -1; while (right >= left) { int mid = (left + right) / 2; if (a[mid] == key) { ret = mid; break; } else if (a[mid] < key) { left = mid + 1; } else { right = mid - 1; } } return ret; } |
排序初步(选择排序):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
#include <stdio.h> int max(int a[], int len); int main(void) { int a[] = { 2,45,6,12,87,34,90,24,23,11,65 }; int len = sizeof(a) / sizeof(a[0]); //a的长度 for (int i = len -1;i > 0;i--) { int maxid = max(a, i + 1); //取得最大数的位置 //交换位置 int t = a[i]; a[i] = a[maxid]; a[maxid] = t; } for (int i = 0; i < len; i++) { printf("%d ", a[i]); } return 0; } int max(int a[], int len) { int maxid = 0; //本次循环最大的那个单元的位置 for (int i = 1/*这里为1,少循环一遍*/; i < len; i++) { if (a[i] > a[maxid]) { //如果出现比之前最大的那个单元还大的单元 maxid = i; //就使这个单元的地址记录在maxid } }//运行完毕之后maxid为当前长度中最大的那个单元的位置 return maxid; } |
运算符&:
简介:
scanf("%d", &x);里的&
&的用处是获取变量的地址,他的操作数必须是变量。
int i; printf("%x", &i)
(将地址以十六进制输出)
地址的大小是否与int相同取决于编译器和架构的位数
int i; printf("%p", &i)
在32位的架构下,&i这个地址占用4字节
而64为的架构下,&i这个地址占用8字节
如果取地址的时候右边不是一个变量则会报错(error)
取一个相邻变量的地址:
1 2 3 4 |
int i = 0; int p = 0; printf("%p\n", &i); printf("%p\n", &p); |
以上代码输出的结果为:
1 2 |
0xbff81d6c 0xbff81d68 |
6c与68相差4字节
i 与 p

两个变量在内存中的位置是 i在更高的位置 p在更低的地方 他们分配在内存的堆栈当中。在堆栈里面的变量内存是自顶向下分配的,先写的变量地址更高,后写的变量地址更低,他们是紧挨着的,相差4,这个4就是sizeof(int)。
数组的地址:
输入以下代码:
1 2 3 4 5 |
int a[10]; printf("%p\n", &a); printf("%p\n", a); printf("%p\n", &a[0]); printf("%p\n", &a[1]); |
它的输出为:
1 2 3 4 |
0xbff8dd44 0xbff8dd44 0xbff8dd44 0xbff8dd48 |
可以看出数组中
&a == a == &a[0]
而a[0]与a[1]的地址差距是4,往后面看 相邻地址差距也都是4
对scanf的思考:
如果能够将取得的变量的地址传递给一个函数,能否通过这个地址在那个函数内访问这个变量?
:scanf("%d", &i);
scanf()的原型应该是怎样的?我们需要一个参数能保存别的变量的地址,如何表达能够保存地址的变量?
指针变量:
概念:
指针变量 就是保存地址的变量。
int i;
int* p = &i;
int* p,q;
int *p,q;
其中: int* p = &i;
*表示p(point)是一个指针,它指向的是一个int,它把i的地址交给p

i与p这两个变量,比如说i在0x2000的地方,那么p得到2000,此时p指向了i。
其中:
int* p,q;
int *p,q;
星号可以靠近int 也可以远离int而靠近变量
但他们都表示说p是一个指针,q只是一个普通的变量。
所以我们并不是说把星号加给了int,我们是把星号加给了p,我们说*p是一个int,于是p是一个指针了,并不是说p是int*这种类型。
普通变量的值是实际值
指针变量的值是具有实际值的变量的地址
在指针变量中不会放实际的值,他只会放别的变量的地址
作为参数的指针:
void f(int *p);
在被调用的时候得到了某个变量的地址:
int i=0; f(&i);
在函数里可以通过这个指针访问外面的这个i
访问指针变量指向的变量:
*是一个单目运算符,用来访问指针的值所表示的地址上的变量。
可以做右值也可以做左值:
int k = *p;
*p = k+1;
传入地址:
在有些编译器中 int i; scanf("%d", i);不会报错
因为编译器会认为i也是一个地址,因为地址与整形的样子是一样的
传入函数的数组成了什么?:
传入整形、指针的时候传入的是一个值,那么把数组作为一个参数传入函数的时候,它到底接收到了数组变量的什么东西?
函数参数表中的数组实际上是指针
sizeof(a) == sizeof(int*)
以下四种函数原型是等价的(它们在参数表中是等价的)
int sum(int *ar, int n);
int sum(int *, int);
int sum(int ar[], int n);
int sum(int [], int n);
数组变量是特殊的指针:
数组变量本身表达地址,所以
int a[10]; int *p = a; //无需用&取地址
但是数组的单元表达的是变量,需要用&取地址
而a == &a[0];
[]运算符可以对数组做,也可以对指针做
1 2 3 |
int *p = &min; //min为4 printf("*p = %d\n", *p); printf("p[0] = %d\n", p[0]); //输出结果相同为4 |
p[0]是说 “如果我以为p所指的地方是一个数组的话,那么他的第一个单元就是p[0]”
*运算符可以对指针做,也可以对数组做:
*a = 25
int *q = a;可以 而int b[] = a不可以 是因为
数组变量是const的指针,不能被赋值
可以把int b[]看作int * const b;
字符类型:
char是一种整数,也是一种特殊的类型:字符
因为可以用单引号表示字符的字面量 :'a','1'。
''也是一个字符
printf和scanf里用%c来输入输出字符
用%d输出整形1的结果是49
'1'的ASCII编码是49,所以当c==49时它代表'1'
字符计算:
1 2 3 |
char c = 'A'; c++; printf("%c\n", c); |
输出结果为B;c++改为c+=2输出结果则为C
一个字符加一个数字得到ASCII码表中那个数之后的字符
两个字符的减,得到他们在表中的距离。
大小写转换:
字母在ASCII表中是顺序排列的
大写字母和小写字母是分开排列的,并不在一起
'a'-'A'可以得到这两段之间的距离 于是
A+'a'-'A'可以把一个大写字母变成小写字母,而
a-'a'-'A'可以把一个小写字母变成大写字母
逃逸字符:
用来表达无法印出来的控制字符或特殊字符,它由一个反斜杠"\"开头,后面跟上另一个字符,这两个字符结合起来,组成了一个字符。

\b:
尤其是这些逃逸字符,不同的编译器会有不同的解释:例如\b,有的编译器会把它解释为回去一格但不删除,下一个输入进入后将被回退的字符给替换。
\t:
文本编辑器中,TAB的作用是到下一个固定的位置,而不是往下移动固定的字符数
\t也是一样的:如以下代码输出
1 2 |
printf("123\t456\n"); printf("12\t456\n"); |
输出为:
123 456
12 456
\n与\r:
回车与换行
字符串:
是以0或整数0结尾的一串字符
0或'\0'是一样的,但是和'0'不同(更倾向于'\0')
0标志字符串的结束,但它不是字符串的一部分
计算字符串长度的时候不包含这个0
字符串以数组的形式存在,以数组或指针的形式访问
更多的是以指针的形式
string.h里有很多处理字符串的函数
字符串变量:
定义一个字符串变量:
1 2 3 |
char *str = "Hello"; //定义一个指针str,它指向了一个里面内容是"Hello"的字符数组。(?) char word[] = "Hello"; //在此处定义一个字符数组,内容是"Hello" char line[10] = "Hello"; //定义一个叫line的十个字节大的字符数组,向里面放"Hello",注意这个Hello占用了6个字节,因为编译器会生成一个结尾的0。 |
字符串常量:
形如"Hello"叫做字符串的字面量或是字符串常量。
"Hello"会被编译器变成一个字符数组放在某处,这个数组的长度是6,结尾还有表示结束的0.(例如在printf与scanf中)
两个相邻的字符串常量会被联系起来:
1 2 |
printf("请分别身高的英尺和英寸," "如输入\"5 7\"表示5英尺7英寸") |
如果两个字符串之间没有任何符号那么它们会被连接起来成为一个大的字符串还有另外一种做法:
1 2 |
printf("请分别身高的英尺和英寸,\ 如输入\"5 7\"表示5英尺7英寸") |
反斜杠表示这一行的这个字符串还没有结束,下一行的字符串仍然是它的一部分。但是,第二行的两个TAB会被连到字符串内,所以需要删掉TAB使字符串贴到前面去。
1 2 |
printf("请分别身高的英尺和英寸,\ 如输入\"5 7\"表示5英尺7英寸") |
总结:
C语言的字符串是以字符数组的形态存在的
不能用运算符对字符串做运算
通过数组的方式可以遍历字符串
唯一特殊的地方就是字符串字面量可以用来初始化字符数组
以及标准库提供了一系列字符串函数
字符串变量:
如何初始化:
1 2 3 4 |
char* s = "Hello, world!"; char* s = "Hello, world!"; printf("s = %p\n", s); printf("s2= %p\n", s2); |
以上输出结果为
s = 0x67f8e
s2= 0x67f8e
能看出这两个字符串变量使用了相同的字符字面量初始化,结果他们被赋予了相同的值。而这个地址是很小的,它位于程序的代码段,他是只读的。
s是一个指针,初始化为指向一个字符串常量
由于这个常量所在的地方,所以实际上s是const char* s,但是由于历史的原因,编译器接受不带const的写法
但是试图对s所指的字符串做写入会导致严重的后果。
如果需要修改字符串,应该用数组。
char s[] = "Hello, world!"
之前的指针的意思为指向某一个地方的字符串
数组的意思是就在此处定义字符串
选择:指针or数组:
char *str = "Hello";
char word[] = "Hello";
数组:这个字符串在这里
作为本地变量空间自动被回收
指针:这个字符串不知道在哪里
处理参数
动态分配空间
如果要构造一个字符串:数组
如果要处理一个字符串:指针
有的教科书中会这样说char*:
char*是字符串
事实上,字符串可以表达为char*的形式,
但是char*不一定是字符串。
本意是指向字符的指针,可能指向的是字符的数组(就像int*一样,int*可能指向一个单个的int,也可能指向一个数组。)
只有它所指的字符数组有结尾的0,才能说他所指的是字符串。
字符串输入输出:
字符串赋值?:
1 2 3 |
char *t = "title"; char *s; s = t; |
并没有产生新的字符串,只是让指针s指向了t所指的字符串,对s的任何操作就是对t做的。
输入与输出:
char string[8];
scanf("%s", string);
printf("%s" string);
scanf读到的是一个单词(直到空格、tab或回车为止)
但是这个scanf是不安全的,因为不知道要读入的内容的长度。
可以用如下方法去限制它
scanf("%7s", string);
这个7限制它最多读入7个字符。
*在%和s之间的数字表示最多允许读入的字符的数量,这个数字应该比数组的大小小1。
常见错误:
char *string;
scanf("%s", string);
以为char*是字符串类型,定义了一个字符串类型的变量string就可以直接使用了。由于没有对string初始化为0,所以不一定每次运行都会失败。
像这种“在A环境运行正常的程序,拷贝到B环境就无法正常工作了”的情况,就是指针用错了。
实际上 第一行只是定义了一个指针,他是一个将来要去指向一个变量的指针变量,但是在这个时刻,他没有被初始化。
空字符串:
char buffer[100] = "";
这是一个空的字符串,buffer[0] == '\0'
char buffer[] = "";
这个数组的长度只有1
字符串函数:
strlen(计算):
1 |
size_t strlen(const char *s); |
返回s的字符串长度(不包括结尾的0)
(const意为函数不会修改字符串)
strcmp(比较):
1 |
int strcmp(const char *s1, const char *s2); |
比较两个字符串,返回:
0 : s1 == s2
1 : s1 > s2
-1 : s1 < s2
有一个计算的逻辑结果是abc > bbc
如果比较"abc"与"Abc",输出的结果是32,因为a与A的距离是32
比较"abc"与"abc "(结尾空格)输出结果是-32(与上一个无关联)因为在比对到第四个字符的时候,比对的是"/0"与"空格"空格的位置是32,所以是"0 - 32"结果为-32。
strcpy(复制):
1 |
char *strcpy(char *restrick dst, const char * restrict src); |
把src参数表达的的字符串拷贝到dst的那个空间里去。
(逐个字符的拷贝,包括"\0")
restrict表明src和dst不重叠(C99)
返回dst
为了能链起代码来
strcat(连接):
1 |
char *strcat(char *restrict s1, const char *restrict s2); |
逐字符把s2拷贝到s1的后面,接成一个长的字符串
s1需要有足够空间去容纳s2,s1的\0会直接被覆盖
*返回s1
安全问题:
strcpy和strcat都可能出现安全问题,如果目的地没有足够的空间会出现问题
尽可能不要使用它们
可以使用安全的版本:
1 2 |
char *strncpy(char *restrick dst, const char * restrict src, size_t n); char *strncat(char *restrick s1, const char * restrict s2, size_t n); |
对于cpy来说,n意味着能拷过去最多多少个字符
对于cat来说,n意味着能连上最多多少个字符
1 |
int strncmp(const char *s1, const char *s2, size_t n); |
只比较前n个字符,剩下的字符忽略掉
后话:
总算是学完了这个课程,感觉有很多地方有残缺,甚至是大段的删减,产生了少数量的断层,比如翁恺老师会突然来一句"我们之前讲过....."然而我一点印象都没有。
接下来会紧接着学进阶课程,我发现进阶课程也是分成的八周,看来开学之前可能学不完了,GG。