【学习笔记】C 语言基础

225 阅读30分钟

前言

本文为我的 C 语言学习笔记,主要参考 MOOC 浙大翁恺的“程序设计—— C 语言入门”课程,还包含个人理解和补充内容。

鉴于本人水平有限,内容仅供大家参考,还望各位大佬批评指正。


第 1 章 程序设计和 C 语言

1.1 计算机和编程语言

1.1.1 计算机和程序设计的相关概念

为什么要学习程序设计?

为了更好地理解计算机的工作模式知道计算机擅长解决什么问题,从而让其按照自己的意图工作。

计算机的工作模式:按人为给定的步骤执行相应命令,且原则上不会出错。

程序:一组计算机能识别和执行的指令,一般是用特定的编程语言写的。

编程语言:用于描述计算机执行命令的步骤的特殊语言。

  • 编程语言可按抽象层级分为 3 类
    • 机器语言:用二进制代码表示的计算机能直接识别和执行指令集
    • 汇编语言:用一些单词缩写和符号来表示一些特定的指令,是机器语言的简化表达
    • 高级语言:更符合人类理解习惯的编程语言,例如 C ,Python 等。
  • 从机器语言到高级语言,是从抽象到具体的发展过程。

算法:计算的方法。算法就是人对计算机的计算步骤的描述。

1.1.2 程序执行的方式

程序执行分为解释执行编译执行两种。

  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 程序,有两种方式:
    1. 编辑器编译器的组合;
    2. 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 变量

设计一个找零的程序,要求根据输入的商品总价所付金额,计算并输出找零的金额

  • 找零程序
    1. 要能输入商品总价和所付金额;
    2. 有地方存储输入的数据;
    3. 输入的数据要能参与计算
#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)
  • 这里的 pricemoney 就是我们下面要讨论的变量

输入操作是在终端以行为单位进行的,当按下回车键时,标志着一行的结束。

在按下回车键之前,程序不会读取到任何输入。

然后,我们需要有存储输入数据的地方。于是便有了变量

变量:顾名思义,就是可以变化的量,是程序中的一个能存储数据的地方。 上述程序中的 pricemoneychange 都是变量。

在 C 语言中,要想使用变量,必须先定义变量。

上述程序中的 int price = 0; 就是在定义变量。它定义了一个名为 price,类型为 int,初始值为 0 的变量。

定义变量<类型> <变量名>;

变量名是一种“标识符”。

标识符:通俗地说就是人为设置的英文名称,用于区分不同的名字。

  • 标识符的构造规则
    1. 只能由字母、数字和下划线组成
    2. 不能与 C 语言的关键字重名。(比如 printf 就是 C 语言的关键字)

我们一般会根据变量的含义给变量命名,比如在上述程序中用 price 表示商品总价。

int price = 0; 中的 int变量类型。在 C 中,所有变量在定义时都要声明其数据类型,它指定了变量可以用于存放什么数据,而且变量也只能存放这种类型的数据。这里的 int 类型表示整数,所以 price 只能用于存放整数。

除了我们在代码开头定义的 pricemoney 变量外,我们还在代码中定义了变量 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 类型中的 truefalse 实质上就是 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
  • 将输入的正整数分解,按正序逐个输出分解后的数字。

  • 算法

    • 基本思路
      1. 输入正整数 n 。
      2. 确定除数 div ,div 为 10 的整数倍,且位数要与 n 相同。
      3. 用 n / div 分解出最高位数,用 n % div 去除 n 的最高位数。
      4. 输出分解出的数。
      5. 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() 函数的原型。

为什么要用函数?

  1. 代码复用性:将功能单一的重复性代码写成函数,能实现代码的复用,从而减少代码的重复,提高开发效率和可维护性。
  2. 提高可读性:通过阅读函数名称和注释,开发人员更容易理解函数对应的功能,因此代码更具可读性。
  3. 方便调试:程序出现 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}; ,这里的 元素数量 可省略。

遍历:将数组从头走到尾。

  • 数组的集成初始化除了上述的标准写法,还有如下几种特殊写法:
    1. int a[3] = {}; :生成数组 {0, 0, 0} 。C 语言会默认将没有人为赋值的元素赋值为 0 。
    2. 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 的矩阵:

[a11a12a13a14a15a21a22a23a24a25a31a32a33a34a35]\begin{bmatrix} a_{11} & a_{12} & a_{13} & a_{14} & a_{15} \\ a_{21} & a_{22} & a_{23} & a_{24} & a_{25} \\ a_{31} & a_{32} & a_{33} & a_{34} & a_{35} \end{bmatrix}

二维数组可通过二重循环进行遍历初始化,同样也可以进行集成初始化,例如 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 = &iint 表示指针变量指向的变量为 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~948~57
A~Z65~90
a~z97~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 仅起标志字符串结束的作用,不属于字符串的一部分。

  • 字符串变量的初始化
    1. char *string = "Hello";(用指针的形式定义)
    2. 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

参考

  1. 程序设计入门—— C 语言 - MOOC