前言
本文为我的 C 语言学习笔记,主要参考 MOOC 浙大翁恺的“程序设计—— C 语言入门”课程,还包含个人理解和补充内容。
鉴于本人水平有限,内容仅供大家参考,还望各位大佬批评指正。
第 1 章 程序设计和 C 语言
1.1 计算机和编程语言
1.1.1 计算机和程序设计的相关概念
为什么要学习程序设计?
为了更好地理解计算机的工作模式,知道计算机擅长解决什么问题,从而让其按照自己的意图工作。
计算机的工作模式:按人为给定的步骤执行相应命令,且原则上不会出错。
程序:一组计算机能识别和执行的指令,一般是用特定的编程语言写的。
编程语言:用于描述计算机执行命令的步骤的特殊语言。
- 编程语言可按抽象层级分为 3 类
- 机器语言:用二进制代码表示的计算机能直接识别和执行的指令集。
- 汇编语言:用一些单词缩写和符号来表示一些特定的指令,是机器语言的简化表达。
- 高级语言:更符合人类理解习惯的编程语言,例如 C ,Python 等。
- 从机器语言到高级语言,是从抽象到具体的发展过程。
算法:计算的方法。算法就是人对计算机的计算步骤的描述。
1.1.2 程序执行的方式
程序执行分为解释执行和编译执行两种。
- 解释执行:原程序经解释程序解释后直接执行,中间不生产新的程序。
- 编译执行:原程序经编译器编译后,生成由机器语言编写的新程序,计算机直接执行这个新程序。
由两种不同的程序执行方式,产生了两种不同类型的高级编程语言:解释型语言(Python)和编译型语言(C)。
1.2 C 语言
为什么是 C ?
因为 C 语言在工业界应用广泛(操作系统、嵌入式系统),且现代编程语言几乎都是 C-like 语言,与 C 的语法差异小。
- C 标准
- 1989 年,ANSI 发布了第一个 C 标准——ANSI C
- 1990 年,ISO 接受了 ANSI 的标准,C89 诞生。
- 1995 和 1999 年,历经两次更新——C95 和 C99。
- 2011 年,最新标准——C11。
1.3 第一个程序
1.3.1 Windows 配置 C 环境
C 语言是编译型语言,因此 C 程序需要编译后才能运行。
- 要想在电脑上编写和运行 C 程序,有两种方式:
- 编辑器和编译器的组合;
- IDE(集成开发环境)
这里我选择用第一种方式来配置 C 环境。编译器选择 TDM-GCC,编辑器选择 VS Code。
GCC:GNU Complier Collection,GNU 编译器套件。
1.3.2 Hello World
#include <stdio.h>
int main()
{
printf("Hello World!\n");
return 0;
}
Hello World!
printf(""):将""里的内容原封不动的输出。""里的内容叫字符串。\n:换行符,使结果输出后光标换行。
1.3.3 简单的计算
#include <stdio.h>
int main()
{
printf("%d\n", 1);
printf("1+1=%d\n", 1+1);
printf("2-1=%d\n", 2-1);
printf("1x2=%d\n", 1*2);
printf("4/2=%d\n", 4/2);
printf("5/2的余数为: %d\n", 5%2);
printf("(1+2x3-4)/3=%d\n", (1+2*3-4)/3);
return 0;
}
1
1+1=2
2-1=1
1x2=2
4/2=2
5/2的余数为: 1
(1+2x3-4)/3=1
%d:将字符串后面的一个整数在此输出。
简单的 C 语言运算符:
| 符号 | 含义 |
|---|---|
| + | 加 |
| - | 减 |
| * | 乘 |
| / | 除 |
| % | 取余 |
第 2 章 变量和运算
2.1 变量
设计一个找零的程序,要求根据输入的商品总价和所付金额,计算并输出找零的金额。
- 找零程序
- 要能输入商品总价和所付金额;
- 要有地方存储输入的数据;
- 输入的数据要能参与计算。
#include <stdio.h>
int main()
{
int price = 0;
int money;
printf("请输入商品总价和所付金额(元):");
scanf("%d %d", &price, &money);
int change = money - price;
printf("找您%d元。", change);
return 0;
}
请输入商品总价和所付金额(元):29 100
找您71元。
2.1.1 变量的定义
首先,我们需要实现输入的功能。这里使用了 scanf() 函数来实现输入功能。
%d:表示输入的是整数。 两个%d之间用空格隔开,表示有两个输入。&price:表示输入的第一个数为商品总价。(示例中为 29)&money:表示输入的第二个数为所付金额。(示例中为 100)- 这里的
price和money就是我们下面要讨论的变量。
输入操作是在终端以行为单位进行的,当按下回车键时,标志着一行的结束。
在按下回车键之前,程序不会读取到任何输入。
然后,我们需要有存储输入数据的地方。于是便有了变量。
变量:顾名思义,就是可以变化的量,是程序中的一个能存储数据的地方。
上述程序中的 price ,money 和 change 都是变量。
在 C 语言中,要想使用变量,必须先定义变量。
上述程序中的 int price = 0; 就是在定义变量。它定义了一个名为 price,类型为 int,初始值为 0 的变量。
定义变量:<类型> <变量名>;
变量名是一种“标识符”。
标识符:通俗地说就是人为设置的英文名称,用于区分不同的名字。
- 标识符的构造规则:
- 只能由字母、数字和下划线组成。
- 不能与 C 语言的关键字重名。(比如 printf 就是 C 语言的关键字)
我们一般会根据变量的含义给变量命名,比如在上述程序中用 price 表示商品总价。
int price = 0; 中的 int 是变量类型。在 C 中,所有变量在定义时都要声明其数据类型,它指定了变量可以用于存放什么数据,而且变量也只能存放这种类型的数据。这里的 int 类型表示整数,所以 price 只能用于存放整数。
除了我们在代码开头定义的 price 和 money 变量外,我们还在代码中定义了变量 change 。这是 C99 标准支持的形式,在早期的 ANSI C 中是不支持的。
2.1.2 变量的初始化
我们还需要让变量参与计算,就得给予变量相应的值,即需要给变量赋值。
int price = 0; 中的 = 不是数学意义上的等于。在 C 语言中,= 表示赋值,是一种动作。
变量的初始化:在定义变量的同时给变量赋值。例如 int price = 0; 。
变量初始化:<类型> <变量名> = <初始值>
变量在使用前必须先对其进行赋值,否则在后续计算中可能会出现问题。
所以,要养成变量初始化的习惯。
变量定义还可以组合,但此时初始化需要给每个变量分别赋值。例如 int price = 0, money = 0;
前面提到 = 是一种“动作”,它实际上就是 C 中的一种运算符,称为赋值运算符。
有运算符的式子称为表达式,如 int price = 0; 。
了解上述概念后,我们再回过头来看 scanf() 这个函数。
scanf("%d %d", &price, &money); 的意思是将输入的第一个整数(第一个 %d)赋值给 price (&price),将第二个整数赋值给 money 。
在被赋值的变量前要有 & 。
两个 %d 中间是用空格隔开的。当然,也可以用 , 等符号隔开,只不过在输入中( "" 里面),必须输入同样的符号,才能保证变量被正确赋值。(所以建议用空格隔开!)
2.1.3 常量
常量:不变的量,定义后不能被重新赋值。
比如我们可以设定 money 始终等于 100,记 const int MONEY = 100; 。
一般我们用字母全部大写的标识符来表示常量。
定义常量:const <类型> <常量名> = <常量值>
这里的 const 是一个修饰符,表示“不变”的属性。
- 使用常量的好处:
- 增加程序的可读性;
- 方便更改程序中表达意思相同的量的值。(比如在定义处更改
MONEY的值,整个程序中的MONEY的值都会更改。)
2.2 浮点数
#include <stdio.h>
int main()
{
int foot = 0, inch = 0;
printf("请分别输入英尺数和英寸数:");
scanf("%d %d", &foot, &inch);
double meters = (foot + inch / 12.0) * 0.3048;
printf("身高为:%f\n", meters);
return 0;
}
请分别输入英尺数和英寸数:5 7
身高为:1.701800
这是一个“将英尺和英寸转换为米计量”的程序,输入为 5 和 7,输出为 1.701800,是一个小数。
在 C 语言中,我们称小数为浮点数。“浮点”是指小数点是浮动的。所以,在 C 语言中 1 和 1.0 是截然不同的两个数,前者的数据类型为整数,后者则为浮点数。
- 注意:
- 整数和整数运算的结果只能是整数;
- 整数和浮点数运算的结果是浮点数。
程序中的 meters 变量的类型为 double 型,表示浮点数。除了 double 外,还有 float 类型也能表示浮点数。
-
二者的区别:
double:表示双精度浮点数;float:表示单精度浮点数。
-
double类型的浮点数的输入和输出:- 输入:
scanf("%lf", &...) - 输出:
printf("%f", ...)
- 输入:
2.3 表达式
2.3.1 算子和运算符
我们在 2.1.2节]] 提到过表达式的概念,这里给出其准确定义。
表达式是一系列运算符和算子的组合。
- 运算符:进行运算的“动作”。
- 算子:参与运算的“值”,可以是常数,也可以是变量等。
运算符的优先级:
| 优先级 | 运算符 | 含义 | 运算顺序 | 举例 |
|---|---|---|---|---|
| 1 | + | 单目不变 | 从右向左 | +b |
| 1 | - | 单目取反 | 从右向左 | -b |
| 2 | * | 乘 | 从左向右 | a*b |
| 2 | / | 除 | 从左向右 | a/b |
| 2 | % | 取余 | 从左向右 | a%b |
| 3 | + | 加 | 从左向右 | a+b |
| 3 | - | 减 | 从左向右 | a-b |
| 4 | = | 赋值 | 从右向左 | a=b |
上述优先级最高的为单目运算符,“单目”是指运算符仅作用于一个算子。
2.3.2 复合赋值和递增递减
5 个算数运算符(+ - * / %)可和 = 结合,构成复合赋值运算符(+=,-=,*=,/=,%=)。例如,a += 1; 等价于 a = a + 1 。
- 递增运算符:
++给变量 +1。 - 递减运算符:
--给变量 -1。 - 二者都是单目运算符
而 ++ 或 -- 的位置可以放在变量的前面,称为前缀形式;也可以放在变量的后面,称为后缀形式。
但两种形式对应的表达式的值不同。
- ++a = a
- a++ = a + 1
- 但无论那种形式,变量的值都会 +1(a = a + 1)。
第 3 章 判断
3.1 if 语句
#include <stdio.h>
int main()
{
int h1, m1, h2, m2;
printf("请输入时间1(x时x分):");
scanf("%d %d", &h1, &m1);
printf("请输入时间2(x时x分):");
scanf("%d %d", &h2, &m2);
// 将时间单位统一转化为“分钟”
int t1 = h1 * 60 + m1;
int t2 = h2 * 60 + m2;
int t = t1 - t2; /* 计算时差 */
if (t < 0) {
t = -t;
}
int h3 = t / 60;
int m3 = t % 60;
printf("Time Difference: %d h %d m", h3, m3);
return 0;
}
请输入时间1(x时x分):1 20
请输入时间2(x时x分):5 40
Time Difference: 4 h 20 m
这是一个“计算时差的程序”,由于时间 1 可能早于时间 2(即 t1 < t2 ),所以加了 if 语句来防止出现时间为负的情况。
if 语句:if ( <判断条件> ) { } ,意思是如果(括号中)条件成立,则执行 { } 内的代码。
上述程序中还出现了注释。
注释:用来解释代码的含义,使程序更容易被读者理解。但对计算机而已,注释毫无意义且会被忽略。
//:单行注释/**/:多行注释或行内注释
3.2 判断的条件
3.2.1 关系运算
上述程序中的判断条件( t < 0 )为关系运算。
关系运算符:
| 运算符 | 含义 |
|---|---|
| == | 相等 |
| != | 不相等 |
| 大于 | |
| >= | 大于等于 |
| < | 小于 |
| <= | 小于等于 |
关系运算的结果:关系成立时为 1,不成立时为 0。如 5 == 3 的结果就是 0。
优先级:算术运算符 > 关系运算符 > 赋值运算符
在关系运算符中,== 和 != 比其他运算符优先级低,同级运算符的运算顺序是从左到右。
3.2.2 逻辑运算
除去关系运算,逻辑运算也可以作 if 语句的判断条件。
逻辑运算:对逻辑量的运算,结果为 0 或 1 。
C 语言中没有原生的逻辑类型( bool 类型),但可通过 #include <stdbool.h> 来使用 bool 类型。
#include <stdio.h>
#include <stdbool.h>
int main()
{
bool x; // 定义一个bool变量
x = true;
printf("%d\n", x);
x = false;
printf("%d", x);
return 0;
}
1
0
可以看到 bool 类型中的 true 和 false 实质上就是 1 和 0 ,因此它们用的是 %d 进行输出。
逻辑运算符:
| 运算符 | 含义 | 实例 | 结果 |
|---|---|---|---|
| ! | 非 | !a | 与 a 的逻辑类型相反:例如 a 为 true (1) 结果就是 false (0) 。 |
| && | 与 | a && b | 只有 a 和 b 都为 true 时,结果为 true,其余情况为 false。 |
| || | 或 | a || b | 只有 a 和 b 都为 false 时,结果为 false,其余情况为 true。 |
优先级:低于关系运算符,但 ! 是个例外,因为它是单目运算符,故优先级高于关系运算符。且 “与”的优先级高于“非” 。
逻辑运算从左到右进行。对于“与”和“非”,若左侧表达式已经能决定整个逻辑运算表达式的结果,则不会再执行右侧的表达式。
3.2.3 条件运算和逗号运算
条件运算:( <条件> )? <条件满足时的值> : <条件不满足时的值> ;
#include <stdio.h>
int main()
{
int x = 1;
int a = (x > 0)? 1 : 0;
printf("%d", a);
return 0;
}
1
条件运算符的优先级高于赋值运算符,低于其他所有运算符。
逗号运算符:用来连接两个表达式,其结果为逗号右边的表达式。
逗号运算符是所有运算符中优先级最低的。
C 中的逗号运算符几乎全用在 for 语句中,例如 for (i=0, j=10; i<j; i++, j--) 。
3.3 if-else 语句
3.3.1 if-else 的基本用法
#include <stdio.h>
int main()
{
int price = 0;
int money;
printf("请输入商品总价和所付金额(元):");
scanf("%d %d", &price, &money);
int change = money - price;
if (change < 0) printf("您的所付金额不够,还差%d元。", -change);
else {
printf("找您%d元。", change);
}
return 0;
}
请输入商品总价和所付金额(元):100 50
您的所付金额不够,还差50元。
在之前的找零程序中,我们没有考虑所付金额小于商品总价的情况,这里对其进行了补全。
相较于单一的 if 语句,这里补充了 else 语句,构成了完整的 if else 语句。
else 语句:else { } ,接在 if 语句后面,意思是如果 if 中的条件不成立,则执行 { } 内的代码。
其实当 { } 内只有一条语句时,if 和 else 语句可以不加 { } 。如程序中的 if (change < 0) printf("您的所付金额不够,还差%d元。", -change); 。
3.3.2 if-else 的嵌套
#include <stdio.h>
int main()
{
int a, b, c;
scanf("%d %d %d", &a, &b, &c);
int max;
if (a > b) {
if (a > c) max = a;
else max = c;
}
else {
if (b > c) max = b;
else max = c;
}
printf("Max=%d", max);
return 0;
}
1 2 3
Max=3
这是一个“找出三个整数中最大的数”的程序。
if-else 的嵌套指的就是在 if 或 else 的 { } 内再次使用 if-else 。
注意:else 总是与离它最近的 if 相匹配,与“是否缩进”无关。因此在使用 if-else 时,最好养成写 { } 的习惯。
3.3.3 if-else 的级联
#include <stdio.h>
int main()
{
int n;
printf("请输入4位及以下的正整数:");
scanf("%d", &n);
if (n/1000 > 0) printf("输入数字位数为4。");
else if (n/100 > 0) printf("输入数字位数为3。");
else if (n/10 > 0) printf("输入数字位数为2。");
else printf("输入数字位数为1。");
return 0;
}
请输入4位及以下的正整数:9999
输入数字位数为4。
这是一个“判断 4 位及以下的正整数的数字位数”的程序。可以看到这里运用了多次并列的 if else 语句,是一个多重判断。这就是 if-else 的级联。
3.4 switch-case 语句
如果我们需要对一个问题分不同的情况来讨论,我们可以用 if-else 的级联。在 C 中其实还有另一种方法来处理这种问题,那就是 switch-case 语句。
#include <stdio.h>
int main()
{
int month;
scanf("%d", &month);
switch (month) {
case 1: printf("January"); break;
case 2: printf("February"); break;
case 3: printf("March"); break;
case 4: printf("April"); break;
case 5: printf("May"); break;
case 6: printf("June"); break;
case 7: printf("July"); break;
case 8: printf("August"); break;
case 9: printf("September"); break;
case 10: printf("October"); break;
case 11: printf("November"); break;
case 12: printf("December"); break;
default: printf("输入有误!");
}
return 0;
}
9
September
这是一个“根据输入的数字,输出相应的月份的英文单词”的程序。
switch-case 语句:switch ( <控制表达式> ) { case <常量> : 语句 ... default: 语句} ,控制表达式的值等于哪个常量,就从哪个 case 处开始执行程序,直到遇到 break; 为止;若不等于任何一个常量,则执行 default 后面的语句。这里的控制表达式必须为整数型结果;常量可以是常数,也可以是常数的表达式。
break; 的意思是“跳出 switch-case”。如果一个 case 里没有 break; ,则当它里面的语句执行完毕时,还会继续执行下一个 case 里的语句,知道遇见 break; 为止。
第 4 章 循环
4.1 while 循环
3.3.3 中的“判断数字位数”的程序局限性很大,不能应对“数字位数较多”的情况。
除了像 3.3.3 程序那样按位数对数字进行分类,从而确定数字位数外,我们还可以让计算机来“数”数字的位数。这里要用到的方法就是循环。
#include <stdio.h>
int main()
{
int n;
printf("请输入任意的整数:");
scanf("%d", &n);
int i = 0; // 计数
// 考虑负数和零的情况
if (n < 0) n = -n;
else if (n == 0) i = 1;
// 计算正整数的位数
while (n > 0) {
n /= 10;
i++;
}
printf("输入数字的位数为:%d", i);
return 0;
}
请输入任意的整数:123456789
输入数字的位数为:9
请输入任意的整数:-12345678
输入数字的位数为:8
请输入任意的整数:0
输入数字的位数为:1
这是一个“可以判断任意整数的位数”的程序,当中用到了 while 循环。
其算法的核心是 n /= 10 ,就是通过不断除以 10 来缩小数字的位数(相当于从个位开始数到最高位)。
while 语句:while ( <循环条件> ) { <循环体> } ,首先判断条件是否成立,若成立则执行循环体中的语句,否则直接跳过 while 循环。执行完循环体中的语句后会再次判断条件是否成立,然后重复上述步骤,直到循环结束。
注意:循环体内必须要有能影响条件成立的语句,否则会进入死循环。
4.2 do-while 循环
#include <stdio.h>
int main()
{
int n;
printf("请输入任意的整数:");
scanf("%d", &n);
int i = 0; // 计数
// 变负为正
if (n < 0) n = -n;
// 计算位数
do {
n /= 10;
i ++;
} while (n > 0);
printf("输入数字的位数为:%d", i);
return 0;
}
请输入任意的整数:123456789
输入数字的位数为:9
请输入任意的整数:0
输入数字的位数为:1
相较于之前的程序,这个新程序并没有将 0 作为一种情况单独考虑,而是使用 do-while 循环实现对 0 的位数的判断。
do-while 语句:do { <循环体> } while ( <循环条件> ); ,do-while 语句会在进入循环时,先执行一边循环体中的代码,再判断循环条件是否成立,从而决定循环是否继续进行。
do-while 循环适用于循环必须执行一次的程序。
4.3 for 循环
现在考虑设计一个“能计算正整数阶乘”的程序。
#include <stdio.h>
int main()
{
int n;
printf("请输入任意正整数:");
scanf("%d", &n);
int i = 2, fac = 1;
while (i <= n) {
fac *= i;
i++;
}
printf("该整数的阶乘为:%d", fac);
return 0;
}
请输入任意正整数:5
该整数的阶乘为:120
这是用 while 循环实现的程序。
不难发现,循环条件对 i 做了限定,且每次循环都有 i++ 。像这样的循环,在 C 中还可以用 for 循环表示。
#include <stdio.h>
int main()
{
int n;
printf("请输入任意正整数:");
scanf("%d", &n);
int fac = 1;
for (int i=2; i<=n; i++) {fac *= i;}
printf("该整数的阶乘为:%d", fac);
return 0;
}
for 语句:for ( <初始动作>; <循环条件>; <执行动作> ) { <循环体> }
- 执行动作在执行完循环体后进行;
- 循环的控制变量可直接在 for 中初始化,如
int i=2;,但被定义在循环内的变量只能在循环内使用; - 括号中的三个表达式都可以省略,但
;不能省略; - 循环体内为单个语句时,大括号可省略。
通过比较上述两个程序可以看出,for 循环是完全可以用 while 循环等价替换的。
for 循环适用于循环次数固定的程序。
4.4 循环控制
- 判断一个数是否为素数
#include <stdio.h>
int main()
{
int n;
scanf("%d", &n);
int isPrime = 1; // 判断变量
for (int i=2; i<n; i++) {
if (n % i == 0) {
printf("%d不是素数。", n);
isPrime = 0;
break;
}
}
if (isPrime == 1) printf("%d是素数。", n);
return 0;
}
13
13是素数。
素数:只能被 1 和自身整除的数。
这里用到了 isPrime 作为判断变量,来判断 n 是否为素数,从而决定程序的输出。
在 3.4 节我们提到了 break; 语句,其作用为跳出 switch-case 。这里用在循环中的 break; ,表示跳出该循环。
在循环中还可以用 continue; 语句,表示跳过这一轮循环剩余的语句,直接进入下一轮循环。
4.5 循环的嵌套
- 找出一个能构成 2 元的 1 角、2 角和 5 角的组合
#include <stdio.h>
int main()
{
int finished = 0; // 判断变量
for (int a=1; a<=20; a++) {
for (int b=1; b<=10; b++) {
for (int c=1; c<=4; c++) {
if (a+2*b+5*c == 20) {
printf("%d个1角,%d个2角,%d个5角可组成2元。", a, b, c);
finished = 1;
break;
}
}
if (finished) break;
}
if (finished) break;
}
return 0;
}
1个1角,2个2角,3个5角可组成2元。
在循环里面用循环就叫循环的嵌套。这里用到了判断变量来告诉我们什么时候该结束循环,不过由于 break; 只能作用于其所在的循环,所以我们用了三次 break; 才实现了嵌套循环的跳出。(continue; 也一样只能作用于其所在的循环)
其实我们也可以用 goto 语句来实现循环的跳出,简化程序。
#include <stdio.h>
int main()
{
for (int a=1; a<=20; a++) {
for (int b=1; b<=10; b++) {
for (int c=1; c<=4; c++) {
if (a+2*b+5*c == 20) {
printf("%d个1角,%d个2角,%d个5角可组成2元。", a, b, c);
goto out;
}
}
}
}
out:
return 0;
}
goto 语句:goto <标号>; ,程序跳转到标号所在位置。
虽然 goto 语句十分方便,但为了不破坏程序的结构和保持良好的可读性,我们应尽量不使用 goto 语句。
4.6 循环的应用
- 输入一系列非零整数,要求输入 0 时,程序输出这些非零整数的平均数。
#include <stdio.h>
int main()
{
// Input
int n, sum=0, i=0;
do {
scanf("%d", &n);
sum += n;
i++;
} while (n != 0);
// Output
/*
1.平均数可能为浮点数。
2.输入0时,i多加了一次。
*/
printf("Average: %lf", 1.0*sum/(i-1));
return 0;
}
1 -1 2 -2 3 -3 7 0
Average: 1.000000
- 输入一串整数,要求让其逆序输出。
#include <stdio.h>
int main()
{
// Input
int n;
scanf("%d", &n);
// Reverse
int r = 0;
while (n != 0) {
int i = n % 10; // 取余:i为n的个位数
r = r * 10 + i; // 把i放在r的个位上
n /= 10; // 移除n的个位数,减少n的位数
}
// Output
printf("Reverse: %d", r);
return 0;
}
-123456789
Reverse: -987654321
-
将输入的正整数分解,按正序逐个输出分解后的数字。
-
算法
- 基本思路
- 输入正整数 n 。
- 确定除数 div ,div 为 10 的整数倍,且位数要与 n 相同。
- 用 n / div 分解出最高位数,用 n % div 去除 n 的最高位数。
- 输出分解出的数。
- div /= 10 ,然后重复步骤 3 和 4 ,直到div == 1 。
- 优化:优化输出。
- 基本思路
#include <stdio.h>
int main()
{
// Input
int n;
scanf("%d", &n);
// 确定除数
int div = 1;
while (n/div >= 10) {
div *= 10;
}
// printf("div=%d\n", div); // 调试代码
// Splite
for (; div != 1; div/=10) {
// printf("n=%d, div=%d\n", n, div);
printf("%d ", n/div);
n %= div;
}
printf("%d", n); // 输出最后一位数
return 0;
}
1
1
1000
1 0 0 0
1234
1 2 3 4
我们常在代码中变量改变后的某些关键位置处使用 printf( ) 来查看变量的变化情况,从而帮助我们调试和修改代码。
第 5 章 函数
5.1 函数的定义和声明
#include <stdio.h>
int sum(int begin, int end)
{
int sum;
for (int i = begin; i <= end; i++)
{
sum += i;
}
return sum;
}
int main()
{
printf("%d\n", sum(1, 100));
return 0;
}
5050
这段代码中包含一个求和函数,用于求 1+2+3+...+100 的值。
5.1.1 函数的定义
C 中函数的定义类似数学中的 y = f(x) ,它指的是能接收 0 个或多个参数,返回 0 个或 1 个值的代码。
函数的定义:<函数头> { <函数体> }
函数头:<返回值类型> <函数名>( <参数表> ) ,如上述的 int sum(int begin, int end) 。
函数的调用:<函数名>( <参数值> ) ,这里的 () 必须要有,它意味着函数的存在。如果需要给定参数,则需要按给定顺序输入正确数量的参数。比如,上述代码中对 sum() 函数的调用 sum(1, 100) ,将输入的参数值带入原函数中,得到 begin = 1, end = 100 ,数量和顺序是一一对应的。
函数的返回:return <返回值>; ,return 语句意味着函数执行结束。在一个函数中可以使用多个 return ,但这不符合“单一出口”原则,不利于代码的后期维护,所以写函数的时候,尽量还是用一个 return 比较好。
其实我们从一开始就已经接触过“函数”了—— main() 就是一个函数!int main() 表明它返回值类型为 int 型,而每段 main() 函数结尾的 return 0; 就是返回 0 作为 main() 函数的返回值。这个 0 会返回给计算机中调用 main() 函数的程序中。
有时我们能看到像 f(void) 这样的函数头,void 表明该函数无参数。有时我们又能看见像 f() 这样的函数头,当括号内没有任何内容时,表明函数的参数未知。二者是有一定区别的,最好不要写出第二种形式的函数头。但 main() 函数是个例外,它的括号内即使不写任何内容,也可以表示“无参数”。
函数无法嵌套定义,即不能在函数内定义函数。
5.1.2 函数的声明
函数和变量一样,也需要先声明,后使用。
所以在本章开头的程序中,我们要在 main() 函数前面定义 sum() 函数。
但通常来讲,我们更希望 main() 函数能在代码的最上面,方便我们能一眼看到程序的核心代码。于是就有了函数的原型(声明)。
函数的原型(声明):<函数头>; ,只需将函数头复制,粘贴到 main() 函数之前,加一个“ ; ”即可。声明就是在告诉编译器函数长什么样子。
实际上,函数的原型中可以不写参数名,但参数类型是必须要有的。
#include <stdio.h>
int max(int a, int b);
int main()
{
printf("Max=%d\n", max(17, 18));
return 0;
}
int max(int a, int b)
{
int max = a;
if (b > max) max = b;
return max;
}
这个程序中有 max() 函数,它的返回值是两个数中较大的那个数的值。
我们可以看到,在 main() 函数前有 int max(int a, int b); ,这就是 max() 函数的原型。
为什么要用函数?
- 代码复用性:将功能单一的重复性代码写成函数,能实现代码的复用,从而减少代码的重复,提高开发效率和可维护性。
- 提高可读性:通过阅读函数名称和注释,开发人员更容易理解函数对应的功能,因此代码更具可读性。
- 方便调试:程序出现 bug 时,可以只用检查相应的函数是否出现错误,而无须对整个程序做检查。
5.2 函数的参数和变量
上节中我们提到,函数的参数必须按正确的顺序和数量传递,但并没有强制要求传入参数的类型与定义时的类型一致。实际上,当传入的参数类型与声明的不一致时,C 编译器会自动将传入的类型转换为声明的类型。但并不推荐这样做,因为后续的编程语言在这方面十分严格。
调用函数时,只能传递值,不能传递变量,因为每个函数都有自己的变量空间,其变量位于这个独立的空间中,不能在其他函数的变量空间内起作用。
本地变量(局部变量):定义在函数内部的变量。函数每次运行都会生成一个独立的变量空间,在这个空间中的变量就是这次运行所产生的本地变量。
变量是具有生存期和作用域的。
生存期:变量从出现到消亡的期间。
作用域:程序可以在什么范围内访问这个变量,即变量起作用的空间。
本地变量的生存期和作用域都在 { }(块)内。
- 本地变量的规则
- 定义在块内;
- 进入块前,其中的变量不存在;离开块后,其中的变量随之消失;
- 块外面定义的变量可在块里面起作用;
- 块内定义的与外面定义的变量同名时,前者会掩盖后者;
- 一个块内不能有同名变量;
- 本地变量在使用前需要初始化;
- 参数在进入函数的时候被初始化了。
第 6 章 数组
6.1 数组的定义和初始化
在 4.6 节中我们写了一个计算非零整数平均数的程序,但该程序并没有存储我们输入的数字,而是让每一次输入的数字直接参与运算。这里给出了利用“数组”重新编写的程序。
- 计算输入非零整数的平均数
#include <stdio.h>
int main()
{
const int SIZE = 100; // size of the array
int num[SIZE]; // 定义一个可存放100个int型元素的数组
// Input:通过遍历数组进行初始化
int cnt = 0; // 计数
for (; cnt < SIZE; cnt++)
{
scanf("%d", &num[cnt]);
if (num[cnt] == 0) break; // 输入0时退出
}
// Sum:通过遍历数组进行求和
int sum = 0;
for (int i = 0; i <= cnt; i++)
{
sum += num[i];
}
// Output
printf("Average=%f\n", 1.0*sum/cnt);
return 0;
}
1 -1 2 -2 3 -3 7 0
Average=1.000000
6.1.1 数组的定义
数组:<类型> <变量名>[元素数量] ,数组就是一种存储数据的容器。
- 元素数量必须为整数;
- 所有元素具有先相同的数据类型;
- 一旦创建,不能改变大小;
- 数组元素在内存中是连续排列的。
数组中的一个单元就是类型为数组的类型的变量。我们通过索引来对数组各单元作出区分。
索引:数组单元 [] 内的数字,对于数组 a[n] ,索引从 0 开始,一直到 n-1 。例如,a[0] 是数组 a[n] 的第一个单元。
6.1.2 数组的初始化
我们可以利用循环来遍历数组,从而对数组做初始化,如上述程序所示。我们还可以集成初始化数组,例如 int a[3] = {1, 2, 3}; ,这里的 元素数量 可省略。
遍历:将数组从头走到尾。
- 数组的集成初始化除了上述的标准写法,还有如下几种特殊写法:
int a[3] = {};:生成数组 {0, 0, 0} 。C 语言会默认将没有人为赋值的元素赋值为 0 。int a[3] = { [1]=1 };:生成数组 {0, 1, 0} 。在数组的集成初始化过程中,可定位赋值。
6.2 二维数组
- 构造一个元素全为 1 的 3x5 矩阵
#include <stdio.h>
int main()
{
int a[3][5];
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 5; j++)
{
a[i][j] = 1;
}
}
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 5; j++)
{
printf("%d ", a[i][j]);
}
printf("\n");
}
return 0;
}
1 1 1 1 1
1 1 1 1 1
1 1 1 1 1
上述程序中定义的数组是一个二维数组,即 a[3][5] 。我们可以将其理解为一个 3x5 的矩阵:
二维数组可通过二重循环进行遍历初始化,同样也可以进行集成初始化,例如 int a[][5] = { {1, 1, 1, 1, 1}, {1, 1, 1, 1, 1}, {1, 1, 1, 1, 1} }; ,这里的行数可以省略。
二维数组在内存中是按行优先,从左到右顺序排列的。
6.3 数组的应用
还记得我们在 4.4 节写的判断素数的程序吗?这里我们将利用数组对其作进一步讨论。
- 构造包含前 100 个素数的素数表,利用素数表判断一个 500 以内正整数是否为素数,若是,则输出其在素数表中的位置。
#include <stdio.h>
int isPrime(int x, int prime[], int count);
int main()
{
// 构造素数表
int prime[100] ; // 构造包含前100个素数的素数表
const int SIZE = sizeof(prime) / sizeof(prime[0]); // size of prime[]
int cnt = 0; // 计数
for (int i=2; cnt<SIZE; i++) // 2是最小的素数,所以从2开始
{
if (isPrime(i, prime, cnt))
{
prime[cnt++] = i; // prime[cnt] = i; cnt++;
}
}
/*Test: Output prime[100]*/
// for (int i=0; i<SIZE; i++)
// {
// printf("%d ", prime[i]);
// }
/*判断一个500以内的数是否为素数*/
// Input
int num;
printf("请输入500以内的正整数:");
scanf("%d", &num);
// 判断是否为素数
if (isPrime(num, prime, cnt))
{
printf("%d是素数,", num);
// Search in prime[]
for (int i=0; i<cnt; i++)
{
if (num == prime[i]) printf("位于素数表中的第%d位。", i+1);
}
}
else printf("%d不是素数。", num);
return 0;
}
int isPrime(int x, int prime[], int count)
{
/*
Parameters:
1.x: 待判断的数
2.prime[]: 素数表
3.count: 当前素数表中存储的素数数量
*/
for (int i=0; i<count; i++)
{
if (x != prime[i]) // 把自身除外
{
if (x % prime[i] == 0) return 0;
}
}
return 1;
}
请输入500以内的正整数:323
323不是素数。
请输入500以内的正整数:379
379是素数,位于素数表中的第75位。
程序中设定了 SIZE 常量来表示数组 prime 的容量:
sizeof(prime):数组prime所占字节数;sizeof(prime[0]):数组prime中一个元素所占字节数。
函数 isPrime() 将数组作为了参数。当数组作为参数时,不能在 [] 中指定数组容量,也不能在函数体内用 sizeof() 函数来计算数组容量。因此我们还在 isPrime() 函数的参数中加入了 count 参数来对数组容量作限定。
- 选择排序问题 给定一个乱序的 int 型数组,要求将数组内的元素按从小到大的顺序进行重排。
#include <stdio.h>
int main()
{
int a[10] = {5, 17, 29, 32, 77, 13, 6, 1, 10, 4};
const int SIZE = sizeof(a) / sizeof(a[0]); // SIZE = 10
// Sort
for (int cnt = SIZE-1; cnt > 0; cnt--) // 将最大的数排到最后一位,然后cnt--
{
int max = a[cnt];
for (int i = 0; i <= cnt ; i++)
{
if (a[i] > max)
{
// Swap
max = a[i];
a[i] = a[cnt];
a[cnt] = max;
}
}
}
// Print
for (int i = 0; i < SIZE; i++)
{
printf("%d ", a[i]);
}
return 0;
}
1 4 5 6 10 13 17 29 32 77
第 7 章 指针
7.1 取地址运算
在 scanf() 函数中,我们在输入对应的变量前会加一个 & ,它其实是一种单目运算符,称为取地址运算符。
& :获取变量的地址。
地址:变量存储在计算机内存中的位置。
7.2 指针变量
指针变量:用于保存地址的变量。
指针变量的初始化 :int *p = &i ,int 表示指针变量指向的变量为 int 型,p 是指针变量,初始化时指向变量 i ,p 的初始值为变量 i 的地址。
* :一种单目运算符,用于访问指针地址所指向的变量的值。
#include <stdio.h>
int main()
{
int i = 0;
int *p = &i;
printf("%d\n", p);
printf("%d\n", *p);
return 0;
}
1969224500
0
第一个输出为指针变量的值(地址的整数值);第二个输出为指针变量所指的变量的值(即变量 i 的值)。
用指针变量作参数,可以使函数能访问函数外的变量,从而修改函数外变量的值。
如此我们就可以实现 swap() 函数(用于交换两个整数变量数值的函数)。
#include <stdio.h>
void swap(int *a, int *b);
int main()
{
int a=0, b=1;
swap(&a, &b); // 注意传入的参数为变量的地址
printf("a=%d,b=%d\n", a, b);
return 0;
}
void swap(int *a, int *b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
a=1,b=0
7.3 指针和数组
在 6.3 节中,我们写了一个“判断素数”的函数,它的参数中包含数组,当时我们还对其作出了一些限定。
实际上,函数的参数表中的数组实际上是数组的指针,即传递进函数的“数组变量”实际上是数组的地址。所以,当我们用数组作为函数参数时,在 [] 输入数字来限定数组大小是没有意义的,且 sizeof() 的运算对象也是数组的指针。
#include <stdio.h>
void array(int array[]);
int main()
{
int a[] = {0, 1, 2, 3, 4, 5, 6, 7};
printf("1.%d\n", sizeof(a));
printf("2.%d\n", sizeof(&a));
array(a);
return 0;
}
void array(int a[])
{
printf("3.%d", sizeof(a));
}
1.32
2.8
3.8
- 输出 1:数组所占的字节数;
- 输出 2:数组地址所占的字节数;
- 输出 3:传入函数的数组指针所占的字节数;
数组变量是特殊的指针变量,它实际上是一个 const 指针变量,因此不能被赋值。
- 数组变量本身表达地址,无需用
&来取地址; 但数组的单元表达的是变量,需要用&来取地址。 []运算符可以用于数组,也可用于指针。*运算符可以用于指针,也可用于数组。
#include <stdio.h>
int main()
{
int a[] = {0, 1, 2, 3, 4, 5};
printf("%d\n", *a); // 输出数组地址所对应的元素的值
int *p = a;
printf("%d", p[1]);
return 0;
}
0
1
数组元素在计算机中是按顺序依次存放的,数组的地址就是数组首位元素的地址。
第 8 章 字符和字符串
8.1 字符
C 语言中最小的整数类型是 char 类型,但我们更常称其为字符类型(character)。
字符:用 '' 包裹的字符,如 'a' 。用 %c 来规定输入/输出;也可以用 %d 来输出其 ASCII 码。
常用字符对应的 ASCII 码表
| 字符 | ASCII 码 |
|---|---|
| 0~9 | 48~57 |
| A~Z | 65~90 |
| a~z | 97~122 |
在 C 语言中还有一种特殊字符,称为逃逸字符,它们是用来表达无法打印出来的控制字符或特殊字符。比如要用 printf() 打印出 " 时,需要在其前面加上 \ 。例,printf("\"Hello World!\""); 的输出是 "Hello World!" 。
逃逸字符表
| 字符 | 含义 |
|---|---|
| \b | 回退一格 |
| \t | 到下个制表位 |
| \n | 换行 |
| \r | 回车 |
| \" | 双引号 |
| \' | 单引号 |
| \\ | 反斜杠本身 |
制表位:打印内容时,每行固定的位置。
可用 \t 来获得将上下两行打印的内容对齐的效果(类似表格的效果)。
#include <stdio.h>
int main()
{
printf
(
"Odd\tEven\n"
"13579\t02468"
);
return 0;
}
Odd Even
13579 02468
8.2 字符串变量
字符数组:char word[] = {'H','e','l','l','o'}; ,不是字符串。
字符串:char word[] = {'H','e','l','l','o','\0'}; ,以 0 或 '\0' 结尾的字符数组才是字符串,注意不能以字符 '0' 结尾。结尾的 0 仅起标志字符串结束的作用,不属于字符串的一部分。
- 字符串变量的初始化:
char *string = "Hello";(用指针的形式定义)char string[] = "Hello";(用数组的形式定义)
""括起来的部分组成了字符串,它会被计算机转化字符数组的形式存储。- 字符串结尾的 0 由编译器自动进行补充。
两个相邻的字符串会自动连接在一起。 如 printf("1""2"); 打印出 12 。
使用指针的形式定义字符串变量时,计算机实际上做的事情是先在内存中生成一个相应的字符串常量,然后定义该指针,让其指向那个字符串常量。因此,这样定义的字符串变量是只读的。 如果想要定义字符可变的字符串,应该使用字符数组的形式去定义。
char *string; 不是字符串,它仅仅是一个字符类型的指针。只有初始化后(string 指向一个字符串常量),它才能被称为字符串。
8.3 字符串运算
字符串的赋值: 用指针定义的字符串变量之间的赋值,实则是指针变量的赋值,结果是让被赋值的指针指向用于赋值的指针所指向的字符串变量。而用数组定义的字符串变量无法直接被赋值。
字符串的输入和输出:%s 标志着字符串的输入和输出。
#include <stdio.h>
int main()
{
char *str1 = "Hello";
char str2[5];
scanf("%5s", str2); // 限定最多读入5个字符
printf("%s %s",str1,str2);
return 0;
}
Boy s
Hello Boy
World!
Hello World
使用 scanf() 读入字符串时,一次只能读入一个单词(遇到空格、tab 、回车就会停止)。
%ns (n 为正整数)表明输入时最多只能读 n 个字符,这样做能有效避免字符数组的越界。
C 语言有一个专门用来处理字符串的函数库—— <string.h> 。只要在代码开头输入 #include <string.h> ,即可使用里面的所有函数。
size strlen( const char *s ):- 参数:字符串常量(指针形式的字符串变量)
- 返回值:字符串长度(不包括结尾的 0)
int strcmp( const char *s1, const char *s2 ):- 比较两个字符串,返回 s1-s2 的值。
- s1-s2 的值为二者首个相异字符相减得到的值。
int strncmp( s1, s2, n ):- 比较两个字符串的前 n 个字符,返回前 n 个字符组成的字符串的差值。
char* strcpy( char* s1, const char* s2 ):- 把 s2 指向的字符串拷贝到 s1 上。
- 要求:
- s1 和 s2 是不重叠的。
- s1 是字符数组,是可写入的字符串。
- s1 的空间足够容纳 s2 。
- 返回值:字符串 s1 。
char* strcat( char* s1, cosnt char s2 ):- 把 s2 拷贝到 s1 后面。
strcpy()和strcat()函数都要求 s1 要有足够的空间,因此使用它们时,会存在安全隐患。所有更推荐以下两个安全版本:char* strncpy( s1, s2, n )char* strncat( s1, s2, n )- 参数 n 是用来限定字符串长度的。
#include <stdio.h>
#include <string.h>
int main()
{
char *a = "123";
char *b = "124";
printf("length of a: %ld\n", strlen(a)); // %ld: long int
printf("a-b=%d\n", strcmp(a, b));
char c[7];
strcpy(c, a);
printf("c=%s\n", c);
strcat(c, b);
printf("c=%s\n", c);
return 0;
}
length of a: 3
a-b=-1
c=123
c=123124