Catalog
  1. 1. C语言学习笔记
    1. 1.1. C 程序结构
    2. 1.2. C 基本语法
    3. 1.3. C 数据类型
      1. 1.3.1. 整数类型
      2. 1.3.2. 浮点类型
      3. 1.3.3. void 类型
    4. 1.4. C 变量
    5. 1.5. C 常量
      1. 1.5.1. 整数常量
      2. 1.5.2. 浮点常量
      3. 1.5.3. 字符常量
      4. 1.5.4. 字符串常量
      5. 1.5.5. 定义常量
        1. 1.5.5.1. #define 预处理器
        2. 1.5.5.2. const 关键字
    6. 1.6. C 存储类
      1. 1.6.1. auto 存储类
      2. 1.6.2. register 存储类
      3. 1.6.3. static 存储类
      4. 1.6.4. extern 存储类
    7. 1.7. C 运算符
      1. 1.7.1. 算术运算符
      2. 1.7.2. 关系运算符
      3. 1.7.3. 逻辑运算符
      4. 1.7.4. 赋值运算符
      5. 1.7.5. 杂项运算符 ↦ sizeof & 三元
      6. 1.7.6. C 中的运算符优先级
    8. 1.8. C 判断
      1. 1.8.1. 判断语句
        1. 1.8.1.1. C switch语句
      2. 1.8.2. ? : 运算符(三元运算符)
    9. 1.9. C 循环
      1. 1.9.1. 循环类型
        1. 1.9.1.1. for 循环
        2. 1.9.1.2. do…while 循环
      2. 1.9.2. 循环控制语句
        1. 1.9.2.1. continue 语句
        2. 1.9.2.2. goto 语句
    10. 1.10. C 函数
      1. 1.10.1. 定义函数
      2. 1.10.2. 函数声明
      3. 1.10.3. 调用函数
      4. 1.10.4. 函数参数
    11. 1.11. C 作用域规则
      1. 1.11.0.1. 局部变量
      2. 1.11.0.2. 全局变量
      3. 1.11.0.3. 形式参数
      4. 1.11.0.4. 初始化局部变量和全局变量
  2. 1.12. C 数组
    1. 1.12.1. 声明数组
    2. 1.12.2. 初始化数组
    3. 1.12.3. 访问数组元素
    4. 1.12.4. C 中数组详解
  3. 1.13. C 枚举
  4. 1.14. C 指针
    1. 1.14.1. 什么是指针?
    2. 1.14.2. 如何使用指针?
    3. 1.14.3. C 中的 NULL 指针
    4. 1.14.4. C 指针详解
  5. 1.15. C 函数指针与回调函数
    1. 1.15.1. 函数指针
    2. 1.15.2. 回调函数
  6. 1.16. C 字符串
  7. 1.17. C 结构体
    1. 1.17.1. 定义结构
    2. 1.17.2. 结构体变量的初始化
    3. 1.17.3. 访问结构成员
    4. 1.17.4. 结构作为参数
    5. 1.17.5. 指向结构的指针
    6. 1.17.6. 位域
      1. 1.17.6.1. 位域的定义和位域变量的说明
    7. 1.17.7. 位域的使用
  8. 1.18. C 共用体
    1. 1.18.1. 定义共用体
    2. 1.18.2. 访问共用体成员
  9. 1.19. C 位域
    1. 1.19.1. 位域声明
  10. 1.20. C typedef
    1. 1.20.1. typedef vs #define
  11. 1.21. C 输入 & 输出
    1. 1.21.1. 标准文件
    2. 1.21.2. getchar() & putchar() 函数
    3. 1.21.3. gets() & puts() 函数
    4. 1.21.4. scanf() 和 printf() 函数
  12. 1.22. C 文件读写
    1. 1.22.1. 打开文件
    2. 1.22.2. 关闭文件
    3. 1.22.3. 写入文件
    4. 1.22.4. 读取文件
    5. 1.22.5. 二进制 I/O 函数
  13. 1.23. C 预处理器
    1. 1.23.1. 预处理器实例
    2. 1.23.2. 预定义宏
    3. 1.23.3. 预处理器运算符
      1. 1.23.3.0.1. 宏延续运算符(\)
      2. 1.23.3.0.2. 字符串常量化运算符(#)
      3. 1.23.3.0.3. 标记粘贴运算符(##)
      4. 1.23.3.0.4. defined() 运算符
  14. 1.23.4. 参数化的宏
  • 1.24. C 头文件
    1. 1.24.1. 引用头文件的语法
    2. 1.24.2. 引用头文件的操作
    3. 1.24.3. 只引用一次头文件
    4. 1.24.4. 有条件引用
  • 1.25. C 强制类型转换
    1. 1.25.1. 整数提升
    2. 1.25.2. 常用的算术转换
  • 1.26. C 错误处理
    1. 1.26.1. errno、perror() 和 strerror()
    2. 1.26.2. 被零除的错误
    3. 1.26.3. 程序退出状态
  • 1.27. C 递归
    1. 1.27.1. 数的阶乘
    2. 1.27.2. 斐波那契数列
  • 1.28. C 可变参数
  • 1.29. C 内存管理
    1. 1.29.1. 动态分配内存
    2. 1.29.2. 重新调整内存的大小和释放内存
  • 1.30. C 命令行参数
  • C语言学习笔记

    学习C语言,自己做的笔记。其实大多是抄菜鸟教程的,只是是自己理解要记的部分。

    C语言学习笔记

    C 程序结构

    C 程序主要包括以下部分:

    • 预处理器指令
    • 函数
    • 变量
    • 语句 & 表达式
    • 注释
    1
    2
    3
    4
    5
    6
    7
    8
    9
    #include <stdio.h>

    int main()
    {
    /* 我的第一个 C 程序 */
    printf("Hello, World! \n");

    return 0;
    }
    1. 程序的第一行 *#include * 是预处理器指令,告诉 C 编译器在实际编译之前要包含 stdio.h 文件。
    2. 下一行 int main() 是主函数,程序从这里开始执行。
    3. 下一行 // 将会被编译器忽略,这里放置程序的注释内容。它们被称为程序的注释。
    4. 下一行 printf(…) 是 C 中另一个可用的函数,会在屏幕上显示消息 “Hello, World!”。
    5. 下一行 return 0; 终止 main() 函数,并返回值 0。

    C 基本语法

    C 程序由各种令牌组成,令牌可以是关键字、标识符、常量、字符串值,或者是一个符号。

    下表列出了 C 中的保留字。这些保留字不能作为常量名、变量名或其他标识符名称。

    关键字 说明
    auto 声明自动变量
    break 跳出当前循环
    case 开关语句分支
    char 声明字符型变量或函数返回值类型
    const 声明只读变量
    continue 结束当前循环,开始下一轮循环
    default 开关语句中的”其它”分支
    do 循环语句的循环体
    double 声明双精度浮点型变量或函数返回值类型
    else 条件语句否定分支(与 if 连用)
    enum 声明枚举类型
    extern 声明变量或函数是在其它文件或本文件的其他位置定义
    float 声明浮点型变量或函数返回值类型
    for 一种循环语句
    goto 无条件跳转语句
    if 条件语句
    int 声明整型变量或函数
    long 声明长整型变量或函数返回值类型
    register 声明寄存器变量
    return 子程序返回语句(可以带参数,也可不带参数)
    short 声明短整型变量或函数
    signed 声明有符号类型变量或函数
    sizeof 计算数据类型或变量长度(即所占字节数)
    static 声明静态变量
    struct 声明结构体类型
    switch 用于开关语句
    typedef 用以给数据类型取别名
    unsigned 声明无符号类型变量或函数
    union 声明共用体类型
    void 声明函数无返回值或无参数,声明无类型指针
    volatile 说明变量在程序执行中可被隐含地改变
    while 循环语句的循环条件

    C 数据类型

    在 C 语言中,数据类型指的是用于声明不同类型的变量或函数的一个广泛的系统。变量的类型决定了变量存储占用的空间,以及如何解释存储的位模式。

    C 中的类型可分为以下几种:

    序号 类型与描述
    1 基本类型: 它们是算术类型,包括两种类型:整数类型和浮点类型。
    2 枚举类型: 它们也是算术类型,被用来定义在程序中只能赋予其一定的离散整数值的变量。
    3 void 类型: 类型说明符 void 表明没有可用的值。
    4 派生类型: 它们包括:指针类型、数组类型、结构类型、共用体类型和函数类型。

    整数类型

    下表列出了关于标准整数类型的存储大小和值范围的细节:

    类型 存储大小 值范围
    char 1 字节 -128 到 127 或 0 到 255
    unsigned char 1 字节 0 到 255
    signed char 1 字节 -128 到 127
    int 2 或 4 字节 -32,768 到 32,767 或 -2,147,483,648 到 2,147,483,647
    unsigned int 2 或 4 字节 0 到 65,535 或 0 到 4,294,967,295
    short 2 字节 -32,768 到 32,767
    unsigned short 2 字节 0 到 65,535
    long 4 字节 -2,147,483,648 到 2,147,483,647
    unsigned long 4 字节 0 到 4,294,967,295

    浮点类型

    下表列出了关于标准浮点类型的存储大小、值范围和精度的细节:

    类型 存储大小 值范围 精度
    float 4 字节 1.2E-38 到 3.4E+38 6 位小数
    double 8 字节 2.3E-308 到 1.7E+308 15 位小数
    long double 16 字节 3.4E-4932 到 1.1E+4932 19 位小数

    头文件 float.h 定义了宏,在程序中可以使用这些值和其他有关实数二进制表示的细节。下面的实例将输出浮点类型占用的存储空间以及它的范围

    void 类型

    void 类型指定没有可用的值。它通常用于以下三种情况下:

    序号 类型与描述
    1 函数返回为空 C 中有各种函数都不返回值,或者您可以说它们返回空。不返回值的函数的返回类型为空。例如 void exit (int status);
    2 函数参数为空 C 中有各种函数不接受任何参数。不带参数的函数可以接受一个 void。例如 int rand(void);
    3 指针指向 void 类型为 void * 的指针代表对象的地址,而不是类型。例如,内存分配函数 void *malloc( size_t size ); 返回指向 void 的指针,可以转换为任何数据类型。

    C 变量

    变量其实只不过是程序可操作的存储区的名称。C 中每个变量都有特定的类型,类型决定了变量存储的大小和布局,该范围内的值都可以存储在内存中,运算符可应用于变量上。

    变量的名称可以由字母、数字和下划线字符组成。它必须以字母或下划线开头。大写字母和小写字母是不同的,因为 C 是大小写敏感的。

    类型 描述
    char 通常是一个字节(八位)。这是一个整数类型。
    int 对机器而言,整数的最自然的大小。
    float 单精度浮点值。单精度是这样的格式,1位符号,8位指数,23位小数。img
    double 双精度浮点值。双精度是1位符号,11位指数,52位小数。img
    void 表示类型的缺失。

    实例

    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>

    // 函数外定义变量 x 和 y
    int x;
    int y;
    int addtwonum()
    {
    // 函数内声明变量 x 和 y 为外部变量
    extern int x;
    extern int y;
    // 给外部变量(全局变量)x 和 y 赋值
    x = 1;
    y = 2;
    return x+y;
    }

    int main()
    {
    int result;
    // 调用函数 addtwonum
    result = addtwonum();

    printf("result 为: %d",result);
    return 0;
    }

    当上面的代码被编译和执行时,它会产生下列结果:

    1
    result 为: 3

    C 常量

    整数常量

    整数常量可以是十进制、八进制或十六进制的常量。前缀指定基数:0x 或 0X 表示十六进制,0 表示八进制,不带前缀则默认表示十进制。

    整数常量也可以带一个后缀,后缀是 U 和 L 的组合,U 表示无符号整数(unsigned),L 表示长整数(long)。后缀可以是大写,也可以是小写,U 和 L 的顺序任意。

    下面列举几个整数常量的实例:

    1
    2
    3
    4
    5
    212         /* 合法的 */
    215u /* 合法的 */
    0xFeeL /* 合法的 */
    078 /* 非法的:8 不是八进制的数字 */
    032UU /* 非法的:不能重复后缀 */

    以下是各种类型的整数常量的实例:

    1
    2
    3
    4
    5
    6
    7
    85         /* 十进制 */
    0213 /* 八进制 */
    0x4b /* 十六进制 */
    30 /* 整数 */
    30u /* 无符号整数 */
    30l /* 长整数 */
    30ul /* 无符号长整数 */

    浮点常量

    浮点常量由整数部分、小数点、小数部分和指数部分组成。您可以使用小数形式或者指数形式来表示浮点常量。

    当使用小数形式表示时,必须包含整数部分、小数部分,或同时包含两者。当使用指数形式表示时, 必须包含小数点、指数,或同时包含两者。带符号的指数是用 e 或 E 引入的。

    下面列举几个浮点常量的实例:

    1
    2
    3
    4
    5
    3.14159       /* 合法的 */
    314159E-5L /* 合法的 */
    510E /* 非法的:不完整的指数 */
    210f /* 非法的:没有小数或指数 */
    .e55 /* 非法的:缺少整数或分数 */

    字符常量

    字符常量是括在单引号中,例如,’x’ 可以存储在 char 类型的简单变量中。

    字符常量可以是一个普通的字符(例如 ‘x’)、一个转义序列(例如 ‘\t’),或一个通用的字符(例如 ‘\u02C0’)。

    在 C 中,有一些特定的字符,当它们前面有反斜杠时,它们就具有特殊的含义,被用来表示如换行符(\n)或制表符(\t)等。下表列出了一些这样的转义序列码:

    转义序列 含义
    \ \ 字符
    ' ‘ 字符
    " “ 字符
    ? ? 字符
    \a 警报铃声
    \b 退格键
    \f 换页符
    \n 换行符
    \r 回车
    \t 水平制表符
    \v 垂直制表符
    \ooo 一到三位的八进制数
    \xhh . . . 一个或多个数字的十六进制数

    字符串常量

    1
    2
    3
    4
    5
    6
    7
    "hello, dear"

    "hello, \

    dear"

    "hello, " "d" "ear"

    定义常量

    在 C 中,有两种简单的定义常量的方式:

    1. 使用 #define 预处理器。
    2. 使用 const 关键字。

    #define 预处理器

    下面是使用 #define 预处理器定义常量的形式:

    1
    #define identifier value

    实例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    #include <stdio.h>

    #define LENGTH 10
    #define WIDTH 5
    #define NEWLINE '\n'

    int main()
    {

    int area;

    area = LENGTH * WIDTH;
    printf("value of area : %d", area);
    printf("%c", NEWLINE);

    return 0;
    }

    当上面的代码被编译和执行时,它会产生下列结果:

    1
    value of area : 50

    const 关键字

    您可以使用 const 前缀声明指定类型的常量,如下所示:

    1
    const type variable = value;

    实例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    #include <stdio.h>

    int main()
    {
    const int LENGTH = 10;
    const int WIDTH = 5;
    const char NEWLINE = '\n';
    int area;

    area = LENGTH * WIDTH;
    printf("value of area : %d", area);
    printf("%c", NEWLINE);

    return 0;
    }

    当上面的代码被编译和执行时,它会产生下列结果:

    1
    value of area : 50

    C 存储类

    存储类定义 C 程序中变量/函数的范围(可见性)和生命周期。这些说明符放置在它们所修饰的类型之前。下面列出 C 程序中可用的存储类:

    • auto
    • register
    • static
    • extern

    auto 存储类

    auto 存储类是所有局部变量默认的存储类。

    1
    2
    3
    4
    {
    int mount;
    auto int month;
    }

    上面的实例定义了两个带有相同存储类的变量,auto 只能用在函数内,即 auto 只能修饰局部变量。

    register 存储类

    register 存储类用于定义存储在寄存器中而不是 RAM 中的局部变量。这意味着变量的最大尺寸等于寄存器的大小(通常是一个词),且不能对它应用一元的 ‘&’ 运算符(因为它没有内存位置)。

    1
    2
    3
    {
    register int miles;
    }

    寄存器只用于需要快速访问的变量,比如计数器。还应注意的是,定义 ‘register’ 并不意味着变量将被存储在寄存器中,它意味着变量可能存储在寄存器中,这取决于硬件和实现的限制。

    static 存储类

    static 存储类指示编译器在程序的生命周期内保持局部变量的存在,而不需要在每次它进入和离开作用域时进行创建和销毁。因此,使用 static 修饰局部变量可以在函数调用之间保持局部变量的值。

    static 修饰符也可以应用于全局变量。当 static 修饰全局变量时,会使变量的作用域限制在声明它的文件内。

    全局声明的一个 static 变量或方法可以被任何函数或方法调用,只要这些方法出现在跟 static 变量或方法同一个文件中。

    以下实例演示了 static 修饰全局变量和局部变量的应用:

    实例

    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>

    /* 函数声明 */
    void func1(void);

    static int count=10; /* 全局变量 - static 是默认的 */

    int main()
    {
    while (count--) {
    func1();
    }
    return 0;
    }

    void func1(void)
    {
    /* 'thingy' 是 'func1' 的局部变量 - 只初始化一次
    * 每次调用函数 'func1' 'thingy' 值不会被重置。
    */
    static int thingy=5;
    thingy++;
    printf(" thingy 为 %d , count 为 %d\n", thingy, count);
    }

    实例中 count 作为全局变量可以在函数内使用,thingy 使用 static 修饰后,不会在每次调用时重置。

    可能您现在还无法理解这个实例,因为我已经使用了函数和全局变量,这两个概念目前为止还没进行讲解。即使您现在不能完全理解,也没有关系,后续的章节我们会详细讲解。当上面的代码被编译和执行时,它会产生下列结果:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    thingy 为 6 , count 为 9
    thingy 为 7 , count 为 8
    thingy 为 8 , count 为 7
    thingy 为 9 , count 为 6
    thingy 为 10 , count 为 5
    thingy 为 11 , count 为 4
    thingy 为 12 , count 为 3
    thingy 为 13 , count 为 2
    thingy 为 14 , count 为 1
    thingy 为 15 , count 为 0

    extern 存储类

    extern 存储类用于提供一个全局变量的引用,全局变量对所有的程序文件都是可见的。当您使用 extern 时,对于无法初始化的变量,会把变量名指向一个之前定义过的存储位置。

    当您有多个文件且定义了一个可以在其他文件中使用的全局变量或函数时,可以在其他文件中使用 extern 来得到已定义的变量或函数的引用。可以这么理解,extern 是用来在另一个文件中声明一个全局变量或函数。

    extern 修饰符通常用于当有两个或多个文件共享相同的全局变量或函数的时候,如下所示:

    第一个文件:main.c

    实例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    #include <stdio.h>

    int count ;
    extern void write_extern();

    int main()
    {
    count = 5;
    write_extern();
    }

    第二个文件:support.c

    实例:

    1
    2
    3
    4
    5
    6
    7
    8
    #include <stdio.h>

    extern int count;

    void write_extern(void)
    {
    printf("count is %d\n", count);
    }

    这会产生 a.out 可执行程序,当程序被执行时,它会产生下列结果:

    1
    count is 5

    C 运算符

    算术运算符

    下表显示了 C 语言支持的所有算术运算符。假设变量 A 的值为 10,变量 B 的值为 20,则:

    运算符 描述 实例
    + 把两个操作数相加 A + B 将得到 30
    - 从第一个操作数中减去第二个操作数 A - B 将得到 -10
    * 把两个操作数相乘 A * B 将得到 200
    / 分子除以分母 B / A 将得到 2
    % 取模运算符,整除后的余数 B % A 将得到 0
    ++ 自增运算符,整数值增加 1 A++ 将得到 11
    自减运算符,整数值减少 1 A– 将得到 9

    关系运算符

    下表显示了 C 语言支持的所有关系运算符。假设变量 A 的值为 10,变量 B 的值为 20,则:

    运算符 描述 实例
    == 检查两个操作数的值是否相等,如果相等则条件为真。 (A == B) 为假。
    != 检查两个操作数的值是否相等,如果不相等则条件为真。 (A != B) 为真。
    > 检查左操作数的值是否大于右操作数的值,如果是则条件为真。 (A > B) 为假。
    < 检查左操作数的值是否小于右操作数的值,如果是则条件为真。 (A < B) 为真。
    >= 检查左操作数的值是否大于或等于右操作数的值,如果是则条件为真。 (A >= B) 为假。
    <= 检查左操作数的值是否小于或等于右操作数的值,如果是则条件为真。 (A <= B) 为真。

    逻辑运算符

    下表显示了 C 语言支持的所有关系逻辑运算符。假设变量 A 的值为 1,变量 B 的值为 0,则:

    运算符 描述 实例
    && 称为逻辑与运算符。如果两个操作数都非零,则条件为真。 (A && B) 为假。
    || 称为逻辑或运算符。如果两个操作数中有任意一个非零,则条件为真。 (A || B) 为真。
    ! 称为逻辑非运算符。用来逆转操作数的逻辑状态。如果条件为真则逻辑非运算符将使其为假。 !(A && B) 为真。

    赋值运算符

    下表列出了 C 语言支持的赋值运算符:

    运算符 描述 实例
    = 简单的赋值运算符,把右边操作数的值赋给左边操作数 C = A + B 将把 A + B 的值赋给 C
    += 加且赋值运算符,把右边操作数加上左边操作数的结果赋值给左边操作数 C += A 相当于 C = C + A
    -= 减且赋值运算符,把左边操作数减去右边操作数的结果赋值给左边操作数 C -= A 相当于 C = C - A
    *= 乘且赋值运算符,把右边操作数乘以左边操作数的结果赋值给左边操作数 C *= A 相当于 C = C * A
    /= 除且赋值运算符,把左边操作数除以右边操作数的结果赋值给左边操作数 C /= A 相当于 C = C / A
    %= 求模且赋值运算符,求两个操作数的模赋值给左边操作数 C %= A 相当于 C = C % A
    <<= 左移且赋值运算符 C <<= 2 等同于 C = C << 2
    >>= 右移且赋值运算符 C >>= 2 等同于 C = C >> 2
    &= 按位与且赋值运算符 C &= 2 等同于 C = C & 2
    ^= 按位异或且赋值运算符 C ^= 2 等同于 C = C ^ 2
    |= 按位或且赋值运算符 C |= 2 等同于 C = C | 2

    实例

    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
    #include <stdio.h>

    main()
    {
    int a = 21;
    int c ;

    c = a;
    printf("Line 1 - = 运算符实例,c 的值 = %d\n", c );

    c += a;
    printf("Line 2 - += 运算符实例,c 的值 = %d\n", c );

    c -= a;
    printf("Line 3 - -= 运算符实例,c 的值 = %d\n", c );

    c *= a;
    printf("Line 4 - *= 运算符实例,c 的值 = %d\n", c );

    c /= a;
    printf("Line 5 - /= 运算符实例,c 的值 = %d\n", c );

    c = 200;
    c %= a;
    printf("Line 6 - %= 运算符实例,c 的值 = %d\n", c );

    c <<= 2;
    printf("Line 7 - <<= 运算符实例,c 的值 = %d\n", c );

    c >>= 2;
    printf("Line 8 - >>= 运算符实例,c 的值 = %d\n", c );

    c &= 2;
    printf("Line 9 - &= 运算符实例,c 的值 = %d\n", c );

    c ^= 2;
    printf("Line 10 - ^= 运算符实例,c 的值 = %d\n", c );

    c |= 2;
    printf("Line 11 - |= 运算符实例,c 的值 = %d\n", c );

    }

    当上面的代码被编译和执行时,它会产生下列结果:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    Line 1 - =  运算符实例,c 的值 = 21
    Line 2 - += 运算符实例,c 的值 = 42
    Line 3 - -= 运算符实例,c 的值 = 21
    Line 4 - *= 运算符实例,c 的值 = 441
    Line 5 - /= 运算符实例,c 的值 = 21
    Line 6 - %= 运算符实例,c 的值 = 11
    Line 7 - <<= 运算符实例,c 的值 = 44
    Line 8 - >>= 运算符实例,c 的值 = 11
    Line 9 - &= 运算符实例,c 的值 = 2
    Line 10 - ^= 运算符实例,c 的值 = 0
    Line 11 - |= 运算符实例,c 的值 = 2

    杂项运算符 ↦ sizeof & 三元

    下表列出了 C 语言支持的其他一些重要的运算符,包括 sizeof? :

    运算符 描述 实例
    sizeof() 返回变量的大小。 sizeof(a) 将返回 4,其中 a 是整数。
    & 返回变量的地址。 &a; 将给出变量的实际地址。
    * 指向一个变量。 *a; 将指向一个变量。
    ? : 条件表达式

    示例:

    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
    #include <stdio.h>

    int main()
    {
    int a = 4;
    short b;
    double c;
    int* ptr;

    /* sizeof 运算符实例 */
    printf("Line 1 - 变量 a 的大小 = %lu\n", sizeof(a) );
    printf("Line 2 - 变量 b 的大小 = %lu\n", sizeof(b) );
    printf("Line 3 - 变量 c 的大小 = %lu\n", sizeof(c) );

    /* & 和 * 运算符实例 */
    ptr = &a; /* 'ptr' 现在包含 'a' 的地址 */
    printf("a 的值是 %d\n", a);
    printf("*ptr 是 %d\n", *ptr);

    /* 三元运算符实例 */
    a = 10;
    b = (a == 1) ? 20: 30;
    printf( "b 的值是 %d\n", b );

    b = (a == 10) ? 20: 30;
    printf( "b 的值是 %d\n", b );
    }

    当上面的代码被编译和执行时,它会产生下列结果:

    1
    2
    3
    4
    5
    6
    7
    Line 1 - 变量 a 的大小 = 4
    Line 2 - 变量 b 的大小 = 2
    Line 3 - 变量 c 的大小 = 8
    a 的值是 4
    *ptr 是 4
    b 的值是 30
    b 的值是 20

    C 中的运算符优先级

    类别 运算符 结合性
    后缀 () [] -> . ++ - - 从左到右
    一元 + - ! ~ ++ - - (type)* & sizeof 从右到左
    乘除 * / % 从左到右
    加减 + - 从左到右
    移位 << >> 从左到右
    关系 < <= > >= 从左到右
    相等 == != 从左到右
    位与 AND & 从左到右
    位异或 XOR ^ 从左到右
    位或 OR | 从左到右
    逻辑与 AND && 从左到右
    逻辑或 OR || 从左到右
    条件 ?: 从右到左
    赋值 = += -= *= /= %=>>= <<= &= ^= |= 从右到左
    逗号 , 从左到右

    实例

    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()
    {
    int a = 20;
    int b = 10;
    int c = 15;
    int d = 5;
    int e;

    e = (a + b) * c / d; // ( 30 * 15 ) / 5
    printf("(a + b) * c / d 的值是 %d\n", e );

    e = ((a + b) * c) / d; // (30 * 15 ) / 5
    printf("((a + b) * c) / d 的值是 %d\n" , e );

    e = (a + b) * (c / d); // (30) * (15/5)
    printf("(a + b) * (c / d) 的值是 %d\n", e );

    e = a + (b * c) / d; // 20 + (150/5)
    printf("a + (b * c) / d 的值是 %d\n" , e );

    return 0;
    }

    当上面的代码被编译和执行时,它会产生下列结果:

    1
    2
    3
    4
    (a + b) * c / d 的值是 90
    ((a + b) * c) / d 的值是 90
    (a + b) * (c / d) 的值是 90
    a + (b * c) / d 的值是 50

    C 判断

    判断语句

    C 语言提供了以下类型的判断语句。点击链接查看每个语句的细节。

    语句 描述
    if 语句 一个 if 语句 由一个布尔表达式后跟一个或多个语句组成。
    if…else 语句 一个 if 语句 后可跟一个可选的 else 语句,else 语句在布尔表达式为假时执行。
    嵌套 if 语句 您可以在一个 ifelse if 语句内使用另一个 ifelse if 语句。
    switch 语句 一个 switch 语句允许测试一个变量等于多个值时的情况。
    嵌套 switch 语句 您可以在一个 switch 语句内使用另一个 switch 语句。

    C switch语句

    C 语言中 switch 语句的语法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    switch(expression){
    case constant-expression :
    statement(s);
    break; /* 可选的 */
    case constant-expression :
    statement(s);
    break; /* 可选的 */

    /* 您可以有任意数量的 case 语句 */
    default : /* 可选的 */
    statement(s);
    }

    switch 语句必须遵循下面的规则:

    • switch 语句中的 expression 是一个常量表达式,必须是一个整型或枚举类型。
    • 在一个 switch 中可以有任意数量的 case 语句。每个 case 后跟一个要比较的值和一个冒号。
    • case 的 constant-expression 必须与 switch 中的变量具有相同的数据类型,且必须是一个常量或字面量。
    • 当被测试的变量等于 case 中的常量时,case 后跟的语句将被执行,直到遇到 break 语句为止。
    • 当遇到 break 语句时,switch 终止,控制流将跳转到 switch 语句后的下一行。
    • 不是每一个 case 都需要包含 break。如果 case 语句不包含 break,控制流将会 继续 后续的 case,直到遇到 break 为止。
    • 一个 switch 语句可以有一个可选的 default case,出现在 switch 的结尾。default case 可用于在上面所有 case 都不为真时执行一个任务。default case 中的 break 语句不是必需的。

    流程图

    C 中的 switch 语句

    实例:

    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>

    int main ()
    {
    /* 局部变量定义 */
    char grade = 'B';

    switch(grade)
    {
    case 'A' :
    printf("很棒!\n" );
    break;
    case 'B' :
    case 'C' :
    printf("做得好\n" );
    break;
    case 'D' :
    printf("您通过了\n" );
    break;
    case 'F' :
    printf("最好再试一下\n" );
    break;
    default :
    printf("无效的成绩\n" );
    }
    printf("您的成绩是 %c\n", grade );

    return 0;
    }

    当上面的代码被编译和执行时,它会产生下列结果:

    1
    2
    做得好
    您的成绩是 B

    ? : 运算符(三元运算符)

    我们已经在前面的章节中讲解了 条件运算符 ? :,可以用来替代 if…else 语句。它的一般形式如下:

    1
    Exp1 ? Exp2 : Exp3;

    实例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    #include<stdio.h>

    int main()
    {
    int num;

    printf("输入一个数字 : ");
    scanf("%d",&num);

    (num%2==0)?printf("偶数"):printf("奇数");
    }

    C 循环

    循环类型

    C 语言提供了以下几种循环类型。点击链接查看每个类型的细节。

    循环类型 描述
    while 循环 当给定条件为真时,重复语句或语句组。它会在执行循环主体之前测试条件。
    for 循环 多次执行一个语句序列,简化管理循环变量的代码。
    do…while 循环 除了它是在循环主体结尾测试条件外,其他与 while 语句类似。
    嵌套循环 您可以在 while、for 或 do..while 循环内使用一个或多个循环。

    for 循环

    语法

    C 语言中 for 循环的语法:

    1
    2
    3
    4
    for ( init; condition; increment )
    {
    statement(s);
    }

    下面是 for 循环的控制流:

    1. init 会首先被执行,且只会执行一次。这一步允许您声明并初始化任何循环控制变量。您也可以不在这里写任何语句,只要有一个分号出现即可。
    2. 接下来,会判断 condition。如果为真,则执行循环主体。如果为假,则不执行循环主体,且控制流会跳转到紧接着 for 循环的下一条语句。
    3. 在执行完 for 循环主体后,控制流会跳回上面的 increment 语句。该语句允许您更新循环控制变量。该语句可以留空,只要在条件后有一个分号出现即可。
    4. 条件再次被判断。如果为真,则执行循环,这个过程会不断重复(循环主体,然后增加步值,再然后重新判断条件)。在条件变为假时,for 循环终止。

    流程图

    C 中的 for 循环

    实例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    #include <stdio.h>

    int main ()
    {
    /* for 循环执行 */
    for( int a = 10; a < 20; a = a + 1 )
    {
    printf("a 的值: %d\n", a);
    }

    return 0;
    }

    当上面的代码被编译和执行时,它会产生下列结果:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    a 的值: 10
    a 的值: 11
    a 的值: 12
    a 的值: 13
    a 的值: 14
    a 的值: 15
    a 的值: 16
    a 的值: 17
    a 的值: 18
    a 的值: 19

    do…while 循环

    语法

    C 语言中 do…while 循环的语法:

    1
    2
    3
    4
    5
    do
    {
    statement(s);

    }while( condition );

    流程图

    C 中的 do...while 循环

    实例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    #include <stdio.h>

    int main ()
    {
    /* 局部变量定义 */
    int a = 10;

    /* do 循环执行 */
    do
    {
    printf("a 的值: %d\n", a);
    a = a + 1;
    }while( a < 20 );

    return 0;
    }

    当上面的代码被编译和执行时,它会产生下列结果:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    a 的值: 10
    a 的值: 11
    a 的值: 12
    a 的值: 13
    a 的值: 14
    a 的值: 15
    a 的值: 16
    a 的值: 17
    a 的值: 18
    a 的值: 19

    循环控制语句

    控制语句 描述
    break 语句 终止循环switch 语句,程序流将继续执行紧接着循环或 switch 的下一条语句。
    continue 语句 告诉一个循环体立刻停止本次循环迭代,重新开始下次循环迭代。
    goto 语句 将控制转移到被标记的语句。但是不建议在程序中使用 goto 语句。

    continue 语句

    流程图

    C continue 语句

    实例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    #include <stdio.h>

    int main ()
    {
    /* 局部变量定义 */
    int a = 10;

    /* do 循环执行 */
    do
    {
    if( a == 15)
    {
    /* 跳过迭代 */
    a = a + 1;
    continue;
    }
    printf("a 的值: %d\n", a);
    a++;

    }while( a < 20 );

    return 0;
    }

    当上面的代码被编译和执行时,它会产生下列结果:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    a 的值: 10
    a 的值: 11
    a 的值: 12
    a 的值: 13
    a 的值: 14
    a 的值: 16
    a 的值: 17
    a 的值: 18
    a 的值: 19

    goto 语句

    语法

    C 语言中 goto 语句的语法:

    1
    2
    3
    4
    goto label;
    ..
    .
    label: statement

    流程图

    C goto 语句

    实例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    #include <stdio.h>

    int main ()
    {
    /* 局部变量定义 */
    int a = 10;

    /* do 循环执行 */
    LOOP:do
    {
    if( a == 15)
    {
    /* 跳过迭代 */
    a = a + 1;
    goto LOOP;
    }
    printf("a 的值: %d\n", a);
    a++;

    }while( a < 20 );

    return 0;
    }

    当上面的代码被编译和执行时,它会产生下列结果:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    a 的值: 10
    a 的值: 11
    a 的值: 12
    a 的值: 13
    a 的值: 14
    a 的值: 16
    a 的值: 17
    a 的值: 18
    a 的值: 19

    C 函数

    函数是一组一起执行一个任务的语句。每个 C 程序都至少有一个函数,即主函数 main() ,所有简单的程序都可以定义其他额外的函数。

    定义函数

    C 语言中的函数定义的一般形式如下:

    1
    2
    3
    4
    return_type function_name( parameter list )
    {
    body of the function
    }

    在 C 语言中,函数由一个函数头和一个函数主体组成。下面列出一个函数的所有组成部分:

    • 返回类型:一个函数可以返回一个值。return_type 是函数返回的值的数据类型。有些函数执行所需的操作而不返回值,在这种情况下,return_type 是关键字 void
    • 函数名称:这是函数的实际名称。函数名和参数列表一起构成了函数签名。
    • 参数:参数就像是占位符。当函数被调用时,您向参数传递一个值,这个值被称为实际参数。参数列表包括函数参数的类型、顺序、数量。参数是可选的,也就是说,函数可能不包含参数。
    • 函数主体:函数主体包含一组定义函数执行任务的语句。

    实例:

    以下是 max() 函数的源代码。该函数有两个参数 num1 和 num2,会返回这两个数中较大的那个数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    /* 函数返回两个数中较大的那个数 */
    int max(int num1, int num2)
    {
    /* 局部变量声明 */
    int result;

    if (num1 > num2)
    result = num1;
    else
    result = num2;

    return result;
    }

    函数声明

    函数声明会告诉编译器函数名称及如何调用函数。函数的实际主体可以单独定义。

    函数声明包括以下几个部分:

    1
    return_type function_name( parameter list );

    针对上面定义的函数 max(),以下是函数声明:

    1
    int max(int num1, int num2);

    在函数声明中,参数的名称并不重要,只有参数的类型是必需的,因此下面也是有效的声明:

    1
    int max(int, int);

    当您在一个源文件中定义函数且在另一个文件中调用函数时,函数声明是必需的。在这种情况下,您应该在调用函数的文件顶部声明函数。

    调用函数

    实例:

    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
    #include <stdio.h>

    /* 函数声明 */
    int max(int num1, int num2);

    int main ()
    {
    /* 局部变量定义 */
    int a = 100;
    int b = 200;
    int ret;

    /* 调用函数来获取最大值 */
    ret = max(a, b);

    printf( "Max value is : %d\n", ret );

    return 0;
    }

    /* 函数返回两个数中较大的那个数 */
    int max(int num1, int num2)
    {
    /* 局部变量声明 */
    int result;

    if (num1 > num2)
    result = num1;
    else
    result = num2;

    return result;
    }

    把 max() 函数和 main() 函数放一块,编译源代码。当运行最后的可执行文件时,会产生下列结果:

    1
    Max value is : 200

    函数参数

    如果函数要使用参数,则必须声明接受参数值的变量。这些变量称为函数的形式参数

    形式参数就像函数内的其他局部变量,在进入函数时被创建,退出函数时被销毁。

    当调用函数时,有两种向函数传递参数的方式:

    调用类型 描述
    传值调用 该方法把参数的实际值复制给函数的形式参数。在这种情况下,修改函数内的形式参数不会影响实际参数。
    引用调用 通过指针传递方式,形参为指向实参地址的指针,当对形参的指向操作时,就相当于对实参本身进行的操作。

    默认情况下,C 使用传值调用来传递参数。一般来说,这意味着函数内的代码不能改变用于调用函数的实际参数。

    C 作用域规则

    任何一种编程中,作用域是程序中定义的变量所存在的区域,超过该区域变量就不能被访问。C 语言中有三个地方可以声明变量:

    1. 在函数或块内部的局部变量
    2. 在所有函数外部的全局变量
    3. 形式参数的函数参数定义中

    局部变量

    在某个函数或块的内部声明的变量称为局部变量。它们只能被该函数或该代码块内部的语句使用。局部变量在函数外部是不可知的。

    全局变量

    全局变量是定义在函数外部,通常是在程序的顶部。全局变量在整个程序生命周期内都是有效的,在任意的函数内部能访问全局变量。

    形式参数

    函数的参数,形式参数,被当作该函数内的局部变量,如果与全局变量同名它们会优先使用。

    全局变量与局部变量在内存中的区别

    • 全局变量保存在内存的全局存储区中,占用静态的存储单元;
    • 局部变量保存在栈中,只有在所在函数被调用时才动态地为变量分配存储单元。

    初始化局部变量和全局变量

    当局部变量被定义时,系统不会对其初始化,您必须自行对其初始化。定义全局变量时,系统会自动对其初始化,如下所示:

    数据类型 初始化默认值
    int 0
    char ‘\0’
    float 0
    double 0
    pointer NULL

    C 数组

    C 语言支持数组数据结构,它可以存储一个固定大小的相同类型元素的顺序集合。数组是用来存储一系列数据,但它往往被认为是一系列相同类型的变量。

    声明数组

    在 C 中要声明一个数组,需要指定元素的类型和元素的数量,如下所示:

    1
    type arrayName [ arraySize ];

    这叫做一维数组。arraySize 必须是一个大于零的整数常量,type 可以是任意有效的 C 数据类型。例如,要声明一个类型为 double 的包含 10 个元素的数组 balance,声明语句如下:

    1
    double balance[10];

    初始化数组

    1
    double balance[5] = {1000.0, 2.0, 3.4, 7.0, 50.0};

    访问数组元素

    1
    balance[4] = 50.0;

    C 中数组详解

    在 C 中,数组是非常重要的,我们需要了解更多有关数组的细节。下面列出了 C 程序员必须清楚的一些与数组相关的重要概念:

    概念 描述
    多维数组 C 支持多维数组。多维数组最简单的形式是二维数组。
    传递数组给函数 您可以通过指定不带索引的数组名称来给函数传递一个指向数组的指针。
    从函数返回数组 C 允许从函数返回数组。
    指向数组的指针 您可以通过指定不带索引的数组名称来生成一个指向数组中第一个元素的指针。

    C 枚举

    枚举是 C 语言中的一种基本数据类型,它可以让数据更简洁,更易读。

    枚举语法定义格式为:

    1
    enum 枚举名 {枚举元素1,枚举元素2,……};

    接下来我们举个例子,比如:一星期有 7 天,如果不用枚举,我们需要使用 #define 来为每个整数定义一个别名:

    1
    2
    3
    4
    5
    6
    7
    #define MON  1
    #define TUE 2
    #define WED 3
    #define THU 4
    #define FRI 5
    #define SAT 6
    #define SUN 7

    这个看起来代码量就比较多,接下来我们看看使用枚举的方式:

    1
    2
    3
    4
    enum DAY
    {
    MON=1, TUE, WED, THU, FRI, SAT, SUN
    };

    实例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    #include<stdio.h>

    enum DAY
    {
    MON=1, TUE, WED, THU, FRI, SAT, SUN
    };

    int main()
    {
    enum DAY day;
    day = WED;
    printf("%d",day);
    return 0;
    }

    以上实例输出结果为:

    1
    3

    C 指针

    每一个变量都有一个内存位置,每一个内存位置都定义了可使用连字号(&)运算符访问的地址,它表示了在内存中的一个地址。请看下面的实例,它将输出定义的变量地址:

    实例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    #include <stdio.h>

    int main ()
    {
    int var1;
    char var2[10];

    printf("var1 变量的地址: %p\n", &var1 );
    printf("var2 变量的地址: %p\n", &var2 );

    return 0;
    }

    当上面的代码被编译和执行时,它会产生下列结果:

    1
    2
    var1 变量的地址: 0x7fff5cc109d4
    var2 变量的地址: 0x7fff5cc109de

    什么是指针?

    指针是一个变量,其值为另一个变量的地址,即,内存位置的直接地址。就像其他变量或常量一样,您必须在使用指针存储其他变量地址之前,对其进行声明。指针变量声明的一般形式为:

    1
    type *var-name;

    在这里,type 是指针的基类型,它必须是一个有效的 C 数据类型,var-name 是指针变量的名称。用来声明指针的星号 * 与乘法中使用的星号是相同的。但是,在这个语句中,星号是用来指定一个变量是指针。以下是有效的指针声明:

    1
    2
    3
    4
    int    *ip;    /* 一个整型的指针 */
    double *dp; /* 一个 double 型的指针 */
    float *fp; /* 一个浮点型的指针 */
    char *ch; /* 一个字符型的指针 */

    所有实际数据类型,不管是整型、浮点型、字符型,还是其他的数据类型,对应指针的值的类型都是一样的,都是一个代表内存地址的长的十六进制数。

    不同数据类型的指针之间唯一的不同是,指针所指向的变量或常量的数据类型不同。

    如何使用指针?

    使用指针时会频繁进行以下几个操作:定义一个指针变量、把变量地址赋值给指针、访问指针变量中可用地址的值。这些是通过使用一元运算符 ***** 来返回位于操作数所指定地址的变量的值。下面的实例涉及到了这些操作:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    #include <stdio.h>

    int main ()
    {
    int var = 20; /* 实际变量的声明 */
    int *ip; /* 指针变量的声明 */

    ip = &var; /* 在指针变量中存储 var 的地址 */

    printf("Address of var variable: %p\n", &var );

    /* 在指针变量中存储的地址 */
    printf("Address stored in ip variable: %p\n", ip );

    /* 使用指针访问值 */
    printf("Value of *ip variable: %d\n", *ip );

    return 0;
    }

    当上面的代码被编译和执行时,它会产生下列结果:

    1
    2
    3
    Address of var variable: bffd8b3c
    Address stored in ip variable: bffd8b3c
    Value of *ip variable: 20

    C 中的 NULL 指针

    在变量声明的时候,如果没有确切的地址可以赋值,为指针变量赋一个 NULL 值是一个良好的编程习惯。赋为 NULL 值的指针被称为指针。

    NULL 指针是一个定义在标准库中的值为零的常量。请看下面的程序:

    实例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    #include <stdio.h>

    int main ()
    {
    int *ptr = NULL;

    printf("ptr 的地址是 %p\n", ptr );

    return 0;
    }

    当上面的代码被编译和执行时,它会产生下列结果:

    1
    ptr 的地址是 0x0

    在大多数的操作系统上,程序不允许访问地址为 0 的内存,因为该内存是操作系统保留的。然而,内存地址 0 有特别重要的意义,它表明该指针不指向一个可访问的内存位置。但按照惯例,如果指针包含空值(零值),则假定它不指向任何东西。

    如需检查一个空指针,您可以使用 if 语句,如下所示:

    1
    2
    if(ptr)     /* 如果 p 非空,则完成 */
    if(!ptr) /* 如果 p 为空,则完成 */

    C 指针详解

    在 C 中,有很多指针相关的概念,这些概念都很简单,但是都很重要。下面列出了 C 程序员必须清楚的一些与指针相关的重要概念:

    概念 描述
    指针的算术运算 可以对指针进行四种算术运算:++、–、+、-
    指针数组 可以定义用来存储指针的数组。
    指向指针的指针 C 允许指向指针的指针。
    传递指针给函数 通过引用或地址传递参数,使传递的参数在调用函数中被改变。
    从函数返回指针 C 允许函数返回指针到局部变量、静态变量和动态内存分配。

    C 函数指针与回调函数

    函数指针

    函数指针是指向函数的指针变量。

    通常我们说的指针变量是指向一个整型、字符型或数组等变量,而函数指针是指向函数。

    函数指针可以像一般函数一样,用于调用函数、传递参数。

    函数指针变量的声明:

    1
    typedef int (*fun_ptr)(int,int); // 声明一个指向同样参数、返回值的函数指针类型

    实例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    #include <stdio.h>

    int max(int x, int y)
    {
    return x > y ? x : y;
    }

    int main(void)
    {
    /* p 是函数指针 */
    int (* p)(int, int) = & max; // &可以省略
    int a, b, c, d;

    printf("请输入三个数字:");
    scanf("%d %d %d", & a, & b, & c);

    /* 与直接调用函数等价,d = max(max(a, b), c) */
    d = p(p(a, b), c);

    printf("最大的数字是: %d\n", d);

    return 0;
    }

    编译执行,输出结果如下:

    1
    2
    请输入三个数字:1 2 3
    最大的数字是: 3

    回调函数

    函数指针作为某个函数的参数

    函数指针变量可以作为某个函数的参数来使用的,回调函数就是一个通过函数指针调用的函数。

    简单讲:回调函数是由别人的函数执行时调用你实现的函数。

    以下是来自知乎作者常溪玲的解说:

    你到一个商店买东西,刚好你要的东西没有货,于是你在店员那里留下了你的电话,过了几天店里有货了,店员就打了你的电话,然后你接到电话后就到店里去取了货。在这个例子里,你的电话号码就叫回调函数,你把电话留给店员就叫登记回调函数,店里后来有货了叫做触发了回调关联的事件,店员给你打电话叫做调用回调函数,你到店里去取货叫做响应回调事件。

    实例:

    实例中 populate_array 函数定义了三个参数,其中第三个参数是函数的指针,通过该函数来设置数组的值。

    实例中我们定义了回调函数 getNextRandomValue,它返回一个随机值,它作为一个函数指针传递给 populate_array 函数。

    populate_array 将调用 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
    #include <stdlib.h>  
    #include <stdio.h>

    // 回调函数
    void populate_array(int *array, size_t arraySize, int (*getNextValue)(void))
    {
    for (size_t i=0; i<arraySize; i++)
    array[i] = getNextValue();
    }

    // 获取随机值
    int getNextRandomValue(void)
    {
    return rand();
    }

    int main(void)
    {
    int myarray[10];
    populate_array(myarray, 10, getNextRandomValue);
    for(int i = 0; i < 10; i++) {
    printf("%d ", myarray[i]);
    }
    printf("\n");
    return 0;
    }

    编译执行,输出结果如下:

    1
    16807 282475249 1622650073 984943658 1144108930 470211272 101027544 1457850878 1458777923 2007237709

    C 字符串

    在 C 语言中,字符串实际上是使用 null 字符 ‘\0’ 终止的一维字符数组。因此,一个以 null 结尾的字符串,包含了组成字符串的字符。

    实例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    #include <stdio.h>

    int main ()
    {
    char greeting[6] = {'H', 'e', 'l', 'l', 'o', '\0'};

    printf("Greeting message: %s\n", greeting );

    return 0;
    }

    当上面的代码被编译和执行时,它会产生下列结果:

    1
    Greeting message: Hello

    C 中有大量操作字符串的函数:

    序号 函数 & 目的
    1 strcpy(s1, s2); 复制字符串 s2 到字符串 s1。
    2 strcat(s1, s2); 连接字符串 s2 到字符串 s1 的末尾。
    3 strlen(s1); 返回字符串 s1 的长度。
    4 strcmp(s1, s2); 如果 s1 和 s2 是相同的,则返回 0;如果 s1<s2 则返回小于 0;如果 s1>s2 则返回大于 0。
    5 strchr(s1, ch); 返回一个指针,指向字符串 s1 中字符 ch 的第一次出现的位置。
    6 strstr(s1, s2); 返回一个指针,指向字符串 s1 中字符串 s2 的第一次出现的位置。

    实例:

    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>
    #include <string.h>

    int main ()
    {
    char str1[12] = "Hello";
    char str2[12] = "World";
    char str3[12];
    int len ;

    /* 复制 str1 到 str3 */
    strcpy(str3, str1);
    printf("strcpy( str3, str1) : %s\n", str3 );

    /* 连接 str1 和 str2 */
    strcat( str1, str2);
    printf("strcat( str1, str2): %s\n", str1 );

    /* 连接后,str1 的总长度 */
    len = strlen(str1);
    printf("strlen(str1) : %d\n", len );

    return 0;
    }

    当上面的代码被编译和执行时,它会产生下列结果:

    1
    2
    3
    strcpy( str3, str1) :  Hello
    strcat( str1, str2): HelloWorld
    strlen(str1) : 10

    C 结构体

    C 数组允许定义可存储相同类型数据项的变量,结构是 C 编程中另一种用户自定义的可用的数据类型,它允许您存储不同类型的数据项。

    结构用于表示一条记录,假设您想要跟踪图书馆中书本的动态,您可能需要跟踪每本书的下列属性:

    • Title
    • Author
    • Subject
    • Book ID

    定义结构

    为了定义结构,您必须使用 struct 语句。struct 语句定义了一个包含多个成员的新的数据类型,struct 语句的格式如下:

    1
    2
    3
    4
    5
    6
    struct tag { 
    member-list
    member-list
    member-list
    ...
    } variable-list ;

    tag 是结构体标签。

    member-list 是标准的变量定义,比如 int i; 或者 float f,或者其他有效的变量定义。

    variable-list 结构变量,定义在结构的末尾,最后一个分号之前,您可以指定一个或多个结构变量。下面是声明 Book 结构的方式:

    1
    2
    3
    4
    5
    6
    7
    struct Books
    {
    char title[50];
    char author[50];
    char subject[100];
    int book_id;
    } book;

    在一般情况下,tag、member-list、variable-list 这 3 部分至少要出现 2 个。以下为实例:

    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
    //此声明声明了拥有3个成员的结构体,分别为整型的a,字符型的b和双精度的c
    //同时又声明了结构体变量s1
    //这个结构体并没有标明其标签
    struct
    {
    int a;
    char b;
    double c;
    } s1;

    //此声明声明了拥有3个成员的结构体,分别为整型的a,字符型的b和双精度的c
    //结构体的标签被命名为SIMPLE,没有声明变量
    struct SIMPLE
    {
    int a;
    char b;
    double c;
    };
    //用SIMPLE标签的结构体,另外声明了变量t1、t2、t3
    struct SIMPLE t1, t2[20], *t3;

    //也可以用typedef创建新类型
    typedef struct
    {
    int a;
    char b;
    double c;
    } Simple2;
    //现在可以用Simple2作为类型声明新的结构体变量
    Simple2 u1, u2[20], *u3;

    在上面的声明中,第一个和第二声明被编译器当作两个完全不同的类型,即使他们的成员列表是一样的,如果令 t3=&s1,则是非法的。

    结构体的成员可以包含其他结构体,也可以包含指向自己结构体类型的指针,而通常这种指针的应用是为了实现一些更高级的数据结构如链表和树等。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    //此结构体的声明包含了其他的结构体
    struct COMPLEX
    {
    char string[100];
    struct SIMPLE a;
    };

    //此结构体的声明包含了指向自己类型的指针
    struct NODE
    {
    char string[100];
    struct NODE *next_node;
    };

    如果两个结构体互相包含,则需要对其中一个结构体进行不完整声明,如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    struct B;    //对结构体B进行不完整声明

    //结构体A中包含指向结构体B的指针
    struct A
    {
    struct B *partner;
    //other members;
    };

    //结构体B中包含指向结构体A的指针,在A声明完后,B也随之进行声明
    struct B
    {
    struct A *partner;
    //other members;
    };

    结构体变量的初始化

    和其它类型变量一样,对结构体变量可以在定义时指定初始值。

    实例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    #include <stdio.h>

    struct Books
    {
    char title[50];
    char author[50];
    char subject[100];
    int book_id;
    } book = {"C 语言", "RUNOOB", "编程语言", 123456};

    int main()
    {
    printf("title : %s\nauthor: %s\nsubject: %s\nbook_id: %d\n", book.title, book.author, book.subject, book.book_id);
    }

    执行输出结果为:

    1
    2
    3
    4
    title : C 语言
    author: RUNOOB
    subject: 编程语言
    book_id: 123456

    访问结构成员

    为了访问结构的成员,我们使用成员访问运算符(.)。成员访问运算符是结构变量名称和我们要访问的结构成员之间的一个句号。您可以使用 struct 关键字来定义结构类型的变量。下面的实例演示了结构的用法:

    实例

    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
    #include <stdio.h>
    #include <string.h>

    struct Books
    {
    char title[50];
    char author[50];
    char subject[100];
    int book_id;
    };

    int main( )
    {
    struct Books Book1; /* 声明 Book1,类型为 Books */
    struct Books Book2; /* 声明 Book2,类型为 Books */

    /* Book1 详述 */
    strcpy( Book1.title, "C Programming");
    strcpy( Book1.author, "Nuha Ali");
    strcpy( Book1.subject, "C Programming Tutorial");
    Book1.book_id = 6495407;

    /* Book2 详述 */
    strcpy( Book2.title, "Telecom Billing");
    strcpy( Book2.author, "Zara Ali");
    strcpy( Book2.subject, "Telecom Billing Tutorial");
    Book2.book_id = 6495700;

    /* 输出 Book1 信息 */
    printf( "Book 1 title : %s\n", Book1.title);
    printf( "Book 1 author : %s\n", Book1.author);
    printf( "Book 1 subject : %s\n", Book1.subject);
    printf( "Book 1 book_id : %d\n", Book1.book_id);

    /* 输出 Book2 信息 */
    printf( "Book 2 title : %s\n", Book2.title);
    printf( "Book 2 author : %s\n", Book2.author);
    printf( "Book 2 subject : %s\n", Book2.subject);
    printf( "Book 2 book_id : %d\n", Book2.book_id);

    return 0;
    }

    当上面的代码被编译和执行时,它会产生下列结果:

    1
    2
    3
    4
    5
    6
    7
    8
    Book 1 title : C Programming
    Book 1 author : Nuha Ali
    Book 1 subject : C Programming Tutorial
    Book 1 book_id : 6495407
    Book 2 title : Telecom Billing
    Book 2 author : Zara Ali
    Book 2 subject : Telecom Billing Tutorial
    Book 2 book_id : 6495700

    结构作为参数

    您可以把结构作为函数参数,传参方式与其他类型的变量或指针类似。您可以使用上面实例中的方式来访问结构变量:

    实例

    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
    #include <stdio.h>
    #include <string.h>

    struct Books
    {
    char title[50];
    char author[50];
    char subject[100];
    int book_id;
    };

    /* 函数声明 */
    void printBook( struct Books book );
    int main( )
    {
    struct Books Book1; /* 声明 Book1,类型为 Books */
    struct Books Book2; /* 声明 Book2,类型为 Books */

    /* Book1 详述 */
    strcpy( Book1.title, "C Programming");
    strcpy( Book1.author, "Nuha Ali");
    strcpy( Book1.subject, "C Programming Tutorial");
    Book1.book_id = 6495407;

    /* Book2 详述 */
    strcpy( Book2.title, "Telecom Billing");
    strcpy( Book2.author, "Zara Ali");
    strcpy( Book2.subject, "Telecom Billing Tutorial");
    Book2.book_id = 6495700;

    /* 输出 Book1 信息 */
    printBook( Book1 );

    /* 输出 Book2 信息 */
    printBook( Book2 );

    return 0;
    }
    void printBook( struct Books book )
    {
    printf( "Book title : %s\n", book.title);
    printf( "Book author : %s\n", book.author);
    printf( "Book subject : %s\n", book.subject);
    printf( "Book book_id : %d\n", book.book_id);
    }

    当上面的代码被编译和执行时,它会产生下列结果:

    1
    2
    3
    4
    5
    6
    7
    8
    Book title : C Programming
    Book author : Nuha Ali
    Book subject : C Programming Tutorial
    Book book_id : 6495407
    Book title : Telecom Billing
    Book author : Zara Ali
    Book subject : Telecom Billing Tutorial
    Book book_id : 6495700

    指向结构的指针

    您可以定义指向结构的指针,方式与定义指向其他类型变量的指针相似,如下所示:

    1
    struct Books *struct_pointer;

    现在,您可以在上述定义的指针变量中存储结构变量的地址。为了查找结构变量的地址,请把 & 运算符放在结构名称的前面,如下所示:

    1
    struct_pointer = &Book1;

    为了使用指向该结构的指针访问结构的成员,您必须使用 -> 运算符,如下所示:

    1
    struct_pointer->title;

    让我们使用结构指针来重写上面的实例,这将有助于您理解结构指针的概念:

    实例

    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
    #include <stdio.h>
    #include <string.h>

    struct Books
    {
    char title[50];
    char author[50];
    char subject[100];
    int book_id;
    };

    /* 函数声明 */
    void printBook( struct Books *book );
    int main( )
    {
    struct Books Book1; /* 声明 Book1,类型为 Books */
    struct Books Book2; /* 声明 Book2,类型为 Books */

    /* Book1 详述 */
    strcpy( Book1.title, "C Programming");
    strcpy( Book1.author, "Nuha Ali");
    strcpy( Book1.subject, "C Programming Tutorial");
    Book1.book_id = 6495407;

    /* Book2 详述 */
    strcpy( Book2.title, "Telecom Billing");
    strcpy( Book2.author, "Zara Ali");
    strcpy( Book2.subject, "Telecom Billing Tutorial");
    Book2.book_id = 6495700;

    /* 通过传 Book1 的地址来输出 Book1 信息 */
    printBook( &Book1 );

    /* 通过传 Book2 的地址来输出 Book2 信息 */
    printBook( &Book2 );

    return 0;
    }
    void printBook( struct Books *book )
    {
    printf( "Book title : %s\n", book->title);
    printf( "Book author : %s\n", book->author);
    printf( "Book subject : %s\n", book->subject);
    printf( "Book book_id : %d\n", book->book_id);
    }

    当上面的代码被编译和执行时,它会产生下列结果:

    1
    2
    3
    4
    5
    6
    7
    8
    Book title : C Programming
    Book author : Nuha Ali
    Book subject : C Programming Tutorial
    Book book_id : 6495407
    Book title : Telecom Billing
    Book author : Zara Ali
    Book subject : Telecom Billing Tutorial
    Book book_id : 6495700

    位域

    有些信息在存储时,并不需要占用一个完整的字节,而只需占几个或一个二进制位。例如在存放一个开关量时,只有 0 和 1 两种状态,用 1 位二进位即可。为了节省存储空间,并使处理简便,C 语言又提供了一种数据结构,称为”位域”或”位段”。

    所谓”位域”是把一个字节中的二进位划分为几个不同的区域,并说明每个区域的位数。每个域有一个域名,允许在程序中按域名进行操作。这样就可以把几个不同的对象用一个字节的二进制位域来表示。

    典型的实例:

    • 用 1 位二进位存放一个开关量时,只有 0 和 1 两种状态。
    • 读取外部文件格式——可以读取非标准的文件格式。例如:9 位的整数。

    位域的定义和位域变量的说明

    位域定义与结构定义相仿,其形式为:

    1
    2
    3
    4
    5
    6
    struct 位域结构名 
    {

    位域列表

    };

    其中位域列表的形式为:

    1
    类型说明符 位域名: 位域长度

    例如:

    1
    2
    3
    4
    5
    struct bs{
    int a:8;
    int b:2;
    int c:6;
    }data;

    说明 data 为 bs 变量,共占两个字节。其中位域 a 占 8 位,位域 b 占 2 位,位域 c 占 6 位。

    让我们再来看一个实例:

    1
    2
    3
    4
    5
    6
    7
    8
    struct packed_struct {
    unsigned int f1:1;
    unsigned int f2:1;
    unsigned int f3:1;
    unsigned int f4:1;
    unsigned int type:4;
    unsigned int my_int:9;
    } pack;

    在这里,packed_struct 包含了 6 个成员:四个 1 位的标识符 f1..f4、一个 4 位的 type 和一个 9 位的 my_int。

    对于位域的定义尚有以下几点说明:

    • 一个位域存储在同一个字节中,如一个字节所剩空间不够存放另一位域时,则会从下一单元起存放该位域。也可以有意使某位域从下一单元开始。例如:
    1
    2
    3
    4
    5
    6
    struct bs{
    unsigned a:4;
    unsigned :4; /* 空域 */
    unsigned b:4; /* 从下一单元开始存放 */
    unsigned c:4
    }
    • 在这个位域定义中,a 占第一字节的 4 位,后 4 位填 0 表示不使用,b 从第二字节开始,占用 4 位,c 占用 4 位。
    • 由于位域不允许跨两个字节,因此位域的长度不能大于一个字节的长度,也就是说不能超过8位二进位。如果最大长度大于计算机的整数字长,一些编译器可能会允许域的内存重叠,另外一些编译器可能会把大于一个域的部分存储在下一个字中。
    • 位域可以是无名位域,这时它只用来作填充或调整位置。无名的位域是不能使用的。例如:
    1
    2
    3
    4
    5
    6
    struct k{
    int a:1;
    int :2; /* 该 2 位不能使用 */
    int b:3;
    int c:2;
    };

    从以上分析可以看出,位域在本质上就是一种结构类型,不过其成员是按二进位分配的。

    位域的使用

    位域的使用和结构成员的使用相同,其一般形式为:

    1
    2
    位域变量名.位域名
    位域变量名->位域名

    位域允许用各种格式输出。

    请看下面的实例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    main(){
    struct bs{
    unsigned a:1;
    unsigned b:3;
    unsigned c:4;
    } bit,*pbit;
    bit.a=1; /* 给位域赋值(应注意赋值不能超过该位域的允许范围) */
    bit.b=7; /* 给位域赋值(应注意赋值不能超过该位域的允许范围) */
    bit.c=15; /* 给位域赋值(应注意赋值不能超过该位域的允许范围) */
    printf("%d,%d,%d\n",bit.a,bit.b,bit.c); /* 以整型量格式输出三个域的内容 */
    pbit=&bit; /* 把位域变量 bit 的地址送给指针变量 pbit */
    pbit->a=0; /* 用指针方式给位域 a 重新赋值,赋为 0 */
    pbit->b&=3; /* 使用了复合的位运算符 "&=",相当于:pbit->b=pbit->b&3,位域 b 中原有值为 7,与 3 作按位与运算的结果为 3(111&011=011,十进制值为 3) */
    pbit->c|=1; /* 使用了复合位运算符"|=",相当于:pbit->c=pbit->c|1,其结果为 15 */
    printf("%d,%d,%d\n",pbit->a,pbit->b,pbit->c); /* 用指针方式输出了这三个域的值 */
    }

    上例程序中定义了位域结构 bs,三个位域为 a、b、c。说明了 bs 类型的变量 bit 和指向 bs 类型的指针变量 pbit。这表示位域也是可以使用指针的。

    C 共用体

    共用体是一种特殊的数据类型,允许您在相同的内存位置存储不同的数据类型。您可以定义一个带有多成员的共用体,但是任何时候只能有一个成员带有值。共用体提供了一种使用相同的内存位置的有效方式。

    定义共用体

    为了定义共用体,您必须使用 union 语句,方式与定义结构类似。union 语句定义了一个新的数据类型,带有多个成员。union 语句的格式如下:

    1
    2
    3
    4
    5
    6
    7
    union [union tag]
    {
    member definition;
    member definition;
    ...
    member definition;
    } [one or more union variables];

    union tag 是可选的,每个 member definition 是标准的变量定义,比如 int i; 或者 float f; 或者其他有效的变量定义。在共用体定义的末尾,最后一个分号之前,您可以指定一个或多个共用体变量,这是可选的。下面定义一个名为 Data 的共用体类型,有三个成员 i、f 和 str:

    1
    2
    3
    4
    5
    6
    union Data
    {
    int i;
    float f;
    char str[20];
    } data;

    现在,Data 类型的变量可以存储一个整数、一个浮点数,或者一个字符串。这意味着一个变量(相同的内存位置)可以存储多个多种类型的数据。您可以根据需要在一个共用体内使用任何内置的或者用户自定义的数据类型。

    共用体占用的内存应足够存储共用体中最大的成员。例如,在上面的实例中,Data 将占用 20 个字节的内存空间,因为在各个成员中,字符串所占用的空间是最大的。下面的实例将显示上面的共用体占用的总内存大小:

    实例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    #include <stdio.h>
    #include <string.h>

    union Data
    {
    int i;
    float f;
    char str[20];
    };

    int main( )
    {
    union Data data;

    printf( "Memory size occupied by data : %d\n", sizeof(data));

    return 0;
    }

    当上面的代码被编译和执行时,它会产生下列结果:

    1
    Memory size occupied by data : 20

    访问共用体成员

    为了访问共用体的成员,我们使用成员访问运算符(.)。成员访问运算符是共用体变量名称和我们要访问的共用体成员之间的一个句号。您可以使用 union 关键字来定义共用体类型的变量。下面的实例演示了共用体的用法:

    实例

    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>
    #include <string.h>

    union Data
    {
    int i;
    float f;
    char str[20];
    };

    int main( )
    {
    union Data data;

    data.i = 10;
    data.f = 220.5;
    strcpy( data.str, "C Programming");

    printf( "data.i : %d\n", data.i);
    printf( "data.f : %f\n", data.f);
    printf( "data.str : %s\n", data.str);

    return 0;
    }

    当上面的代码被编译和执行时,它会产生下列结果:

    1
    2
    3
    data.i : 1917853763
    data.f : 4122360580327794860452759994368.000000
    data.str : C Programming

    在这里,我们可以看到共用体的 if 成员的值有损坏,因为最后赋给变量的值占用了内存位置,这也是 str 成员能够完好输出的原因。现在让我们再来看一个相同的实例,这次我们在同一时间只使用一个变量,这也演示了使用共用体的主要目的:

    实例

    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 <string.h>

    union Data
    {
    int i;
    float f;
    char str[20];
    };

    int main( )
    {
    union Data data;

    data.i = 10;
    printf( "data.i : %d\n", data.i);

    data.f = 220.5;
    printf( "data.f : %f\n", data.f);

    strcpy( data.str, "C Programming");
    printf( "data.str : %s\n", data.str);

    return 0;
    }

    当上面的代码被编译和执行时,它会产生下列结果:

    1
    2
    3
    data.i : 10
    data.f : 220.500000
    data.str : C Programming

    在这里,所有的成员都能完好输出,因为同一时间只用到一个成员。

    C 位域

    如果程序的结构中包含多个开关量,只有 TRUE/FALSE 变量,如下:

    1
    2
    3
    4
    5
    struct
    {
    unsigned int widthValidated;
    unsigned int heightValidated;
    } status;

    这种结构需要 8 字节的内存空间,但在实际上,在每个变量中,我们只存储 0 或 1。在这种情况下,C 语言提供了一种更好的利用内存空间的方式。如果您在结构内使用这样的变量,您可以定义变量的宽度来告诉编译器,您将只使用这些字节。例如,上面的结构可以重写成:

    1
    2
    3
    4
    5
    struct
    {
    unsigned int widthValidated : 1;
    unsigned int heightValidated : 1;
    } status;

    现在,上面的结构中,status 变量将占用 4 个字节的内存空间,但是只有 2 位被用来存储值。如果您用了 32 个变量,每一个变量宽度为 1 位,那么 status 结构将使用 4 个字节,但只要您再多用一个变量,如果使用了 33 个变量,那么它将分配内存的下一段来存储第 33 个变量,这个时候就开始使用 8 个字节。让我们看看下面的实例来理解这个概念:

    实例

    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>
    #include <string.h>

    /* 定义简单的结构 */
    struct
    {
    unsigned int widthValidated;
    unsigned int heightValidated;
    } status1;

    /* 定义位域结构 */
    struct
    {
    unsigned int widthValidated : 1;
    unsigned int heightValidated : 1;
    } status2;

    int main( )
    {
    printf( "Memory size occupied by status1 : %d\n", sizeof(status1));
    printf( "Memory size occupied by status2 : %d\n", sizeof(status2));

    return 0;
    }

    当上面的代码被编译和执行时,它会产生下列结果:

    1
    2
    Memory size occupied by status1 : 8
    Memory size occupied by status2 : 4

    位域声明

    在结构内声明位域的形式如下:

    1
    2
    3
    4
    struct
    {
    type [member_name] : width ;
    };

    下面是有关位域中变量元素的描述:

    元素 描述
    type 整数类型,决定了如何解释位域的值。类型可以是整型、有符号整型、无符号整型。
    member_name 位域的名称。
    width 位域中位的数量。宽度必须小于或等于指定类型的位宽度。

    带有预定义宽度的变量被称为位域。位域可以存储多于 1 位的数,例如,需要一个变量来存储从 0 到 7 的值,您可以定义一个宽度为 3 位的位域,如下:

    1
    2
    3
    4
    struct
    {
    unsigned int age : 3;
    } Age;

    上面的结构定义指示 C 编译器,age 变量将只使用 3 位来存储这个值,如果您试图使用超过 3 位,则无法完成。让我们来看下面的实例:

    实例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    #include <stdio.h>
    #include <string.h>

    struct
    {
    unsigned int age : 3;
    } Age;

    int main( )
    {
    Age.age = 4;
    printf( "Sizeof( Age ) : %d\n", sizeof(Age) );
    printf( "Age.age : %d\n", Age.age );

    Age.age = 7;
    printf( "Age.age : %d\n", Age.age );

    Age.age = 8; // 二进制表示为 1000 有四位,超出
    printf( "Age.age : %d\n", Age.age );

    return 0;
    }

    当上面的代码被编译时,它会带有警告,当上面的代码被执行时,它会产生下列结果:

    1
    2
    3
    4
    Sizeof( Age ) : 4
    Age.age : 4
    Age.age : 7
    Age.age : 0

    C typedef

    ​ C 语言提供了 typedef 关键字,您可以使用它来为类型取一个新的名字。下面的实例为单字节数字定义了一个术语 BYTE

    1
    typedef unsigned char BYTE;

    在这个类型定义之后,标识符 BYTE 可作为类型 unsigned char 的缩写,例如:

    1
    BYTE  b1, b2;

    按照惯例,定义时会大写字母,以便提醒用户类型名称是一个象征性的缩写,但您也可以使用小写字母,如下:

    1
    typedef unsigned char byte;

    您也可以使用 typedef 来为用户自定义的数据类型取一个新的名字。例如,您可以对结构体使用 typedef 来定义一个新的数据类型名字,然后使用这个新的数据类型来直接定义结构变量,如下:

    实例

    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
    #include <stdio.h>
    #include <string.h>

    typedef struct Books
    {
    char title[50];
    char author[50];
    char subject[100];
    int book_id;
    } Book;

    int main( )
    {
    Book book;

    strcpy( book.title, "C 教程");
    strcpy( book.author, "Runoob");
    strcpy( book.subject, "编程语言");
    book.book_id = 12345;

    printf( "书标题 : %s\n", book.title);
    printf( "书作者 : %s\n", book.author);
    printf( "书类目 : %s\n", book.subject);
    printf( "书 ID : %d\n", book.book_id);

    return 0;
    }

    当上面的代码被编译和执行时,它会产生下列结果:

    1
    2
    3
    4
    书标题 : C 教程
    书作者 : Runoob
    书类目 : 编程语言
    书 ID : 12345

    typedef vs #define

    #define 是 C 指令,用于为各种数据类型定义别名,与 typedef 类似,但是它们有以下几点不同:

    • typedef 仅限于为类型定义符号名称,#define 不仅可以为类型定义别名,也能为数值定义别名,比如您可以定义 1 为 ONE。
    • typedef 是由编译器执行解释的,#define 语句是由预编译器进行处理的。

    下面是 #define 的最简单的用法:

    实例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    #include <stdio.h>

    #define TRUE 1
    #define FALSE 0

    int main( )
    {
    printf( "TRUE 的值: %d\n", TRUE);
    printf( "FALSE 的值: %d\n", FALSE);

    return 0;
    }

    当上面的代码被编译和执行时,它会产生下列结果:

    1
    2
    TRUE 的值: 1
    FALSE 的值: 0

    C 输入 & 输出

    当我们提到输入时,这意味着要向程序填充一些数据。输入可以是以文件的形式或从命令行中进行。C 语言提供了一系列内置的函数来读取给定的输入,并根据需要填充到程序中。

    当我们提到输出时,这意味着要在屏幕上、打印机上或任意文件中显示一些数据。C 语言提供了一系列内置的函数来输出数据到计算机屏幕上和保存数据到文本文件或二进制文件中。

    标准文件

    C 语言把所有的设备都当作文件。所以设备(比如显示器)被处理的方式与文件相同。以下三个文件会在程序执行时自动打开,以便访问键盘和屏幕。

    标准文件 文件指针 设备
    标准输入 stdin 键盘
    标准输出 stdout 屏幕
    标准错误 stderr 您的屏幕

    文件指针是访问文件的方式,本节将讲解如何从屏幕读取值以及如何把结果输出到屏幕上。

    C 语言中的 I/O (输入/输出) 通常使用 printf() 和 scanf() 两个函数。

    scanf() 函数用于从标准输入(键盘)读取并格式化, printf() 函数发送格式化输出到标准输出(屏幕)。

    实例

    1
    2
    3
    4
    5
    6
    #include <stdio.h>      // 执行 printf() 函数需要该库
    int main()
    {
    printf("菜鸟教程"); //显示引号中的内容
    return 0;
    }

    编译以上程序,输出结果为:

    1
    菜鸟教程

    实例解析:

    • 所有的 C 语言程序都需要包含 main() 函数。 代码从 main() 函数开始执行。
    • printf() 用于格式化输出到屏幕。printf() 函数在 “stdio.h” 头文件中声明。
    • stdio.h 是一个头文件 (标准输入输出头文件) and #include 是一个预处理命令,用来引入头文件。 当编译器遇到 printf() 函数时,如果没有找到 stdio.h 头文件,会发生编译错误。
    • return 0; 语句用于表示退出程序。

    %d 格式化输出整数

    1
    2
    3
    4
    5
    6
    7
    #include <stdio.h>
    int main()
    {
    int testInteger = 5;
    printf("Number = %d", testInteger);
    return 0;
    }

    编译以上程序,输出结果为:

    1
    Number = 5

    在 printf() 函数的引号中使用 “%d” (整型) 来匹配整型变量 testInteger 并输出到屏幕。

    %f 格式化输出浮点型数据

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    #include <stdio.h>
    int main()
    {
    float f;
    printf("Enter a number: ");
    // %f 匹配浮点型数据
    scanf("%f",&f);
    printf("Value = %f", f);
    return 0;
    }

    getchar() & putchar() 函数

    int getchar(void) 函数从屏幕读取下一个可用的字符,并把它返回为一个整数。这个函数在同一个时间内只会读取一个单一的字符。您可以在循环内使用这个方法,以便从屏幕上读取多个字符。

    int putchar(int c) 函数把字符输出到屏幕上,并返回相同的字符。这个函数在同一个时间内只会输出一个单一的字符。您可以在循环内使用这个方法,以便在屏幕上输出多个字符。

    请看下面的实例:

    实例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    #include <stdio.h>

    int main( )
    {
    int c;

    printf( "Enter a value :");
    c = getchar( );

    printf( "\nYou entered: ");
    putchar( c );
    printf( "\n");
    return 0;
    }

    当上面的代码被编译和执行时,它会等待您输入一些文本,当您输入一个文本并按下回车键时,程序会继续并只会读取一个单一的字符,显示如下:

    1
    2
    3
    4
    $./a.out
    Enter a value :runoob

    You entered: r

    gets() & puts() 函数

    char *gets(char *s) 函数从 stdin 读取一行到 s 所指向的缓冲区,直到一个终止符或 EOF。

    int puts(const char *s) 函数把字符串 s 和一个尾随的换行符写入到 stdout

    实例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    #include <stdio.h>

    int main( )
    {
    char str[100];

    printf( "Enter a value :");
    gets( str );

    printf( "\nYou entered: ");
    puts( str );
    return 0;
    }

    当上面的代码被编译和执行时,它会等待您输入一些文本,当您输入一个文本并按下回车键时,程序会继续并读取一整行直到该行结束,显示如下:

    1
    2
    3
    4
    $./a.out
    Enter a value :runoob

    You entered: runoob

    scanf() 和 printf() 函数

    int scanf(const char *format, …) 函数从标准输入流 stdin 读取输入,并根据提供的 format 来浏览输入。

    int printf(const char *format, …) 函数把输出写入到标准输出流 stdout ,并根据提供的格式产生输出。

    format 可以是一个简单的常量字符串,但是您可以分别指定 %s、%d、%c、%f 等来输出或读取字符串、整数、字符或浮点数。还有许多其他可用的格式选项,可以根据需要使用。如需了解完整的细节,可以查看这些函数的参考手册。现在让我们通过下面这个简单的实例来加深理解:

    实例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    #include <stdio.h>
    int main( ) {

    char str[100];
    int i;

    printf( "Enter a value :");
    scanf("%s %d", str, &i);

    printf( "\nYou entered: %s %d ", str, i);
    printf("\n");
    return 0;
    }

    当上面的代码被编译和执行时,它会等待您输入一些文本,当您输入一个文本并按下回车键时,程序会继续并读取输入,显示如下:

    1
    2
    3
    4
    $./a.out
    Enter a value :runoob 123

    You entered: runoob 123

    在这里,应当指出的是,scanf() 期待输入的格式与您给出的 %s 和 %d 相同,这意味着您必须提供有效的输入,比如 “string integer”,如果您提供的是 “string string” 或 “integer integer”,它会被认为是错误的输入。另外,在读取字符串时,只要遇到一个空格,scanf() 就会停止读取,所以 “this is test” 对 scanf() 来说是三个字符串。

    C 文件读写

    上一章我们讲解了 C 语言处理的标准输入和输出设备。本章我们将介绍 C 程序员如何创建、打开、关闭文本文件或二进制文件。

    一个文件,无论它是文本文件还是二进制文件,都是代表了一系列的字节。C 语言不仅提供了访问顶层的函数,也提供了底层(OS)调用来处理存储设备上的文件。本章将讲解文件管理的重要调用。

    打开文件

    您可以使用 fopen( ) 函数来创建一个新的文件或者打开一个已有的文件,这个调用会初始化类型 FILE 的一个对象,类型 FILE 包含了所有用来控制流的必要的信息。下面是这个函数调用的原型:

    1
    FILE *fopen( const char * filename, const char * mode );

    在这里,filename 是字符串,用来命名文件,访问模式 mode 的值可以是下列值中的一个:

    模式 描述
    r 打开一个已有的文本文件,允许读取文件。
    w 打开一个文本文件,允许写入文件。如果文件不存在,则会创建一个新文件。在这里,您的程序会从文件的开头写入内容。如果文件存在,则该会被截断为零长度,重新写入。
    a 打开一个文本文件,以追加模式写入文件。如果文件不存在,则会创建一个新文件。在这里,您的程序会在已有的文件内容中追加内容。
    r+ 打开一个文本文件,允许读写文件。
    w+ 打开一个文本文件,允许读写文件。如果文件已存在,则文件会被截断为零长度,如果文件不存在,则会创建一个新文件。
    a+ 打开一个文本文件,允许读写文件。如果文件不存在,则会创建一个新文件。读取会从文件的开头开始,写入则只能是追加模式。

    如果处理的是二进制文件,则需使用下面的访问模式来取代上面的访问模式:

    1
    "rb", "wb", "ab", "rb+", "r+b", "wb+", "w+b", "ab+", "a+b"

    关闭文件

    为了关闭文件,请使用 fclose( ) 函数。函数的原型如下:

    1
    int fclose( FILE *fp );

    如果成功关闭文件,fclose( ) 函数返回零,如果关闭文件时发生错误,函数返回 EOF。这个函数实际上,会清空缓冲区中的数据,关闭文件,并释放用于该文件的所有内存。EOF 是一个定义在头文件 stdio.h 中的常量。

    C 标准库提供了各种函数来按字符或者以固定长度字符串的形式读写文件。

    写入文件

    下面是把字符写入到流中的最简单的函数:

    1
    int fputc( int c, FILE *fp );

    函数 fputc() 把参数 c 的字符值写入到 fp 所指向的输出流中。如果写入成功,它会返回写入的字符,如果发生错误,则会返回 EOF。您可以使用下面的函数来把一个以 null 结尾的字符串写入到流中:

    1
    int fputs( const char *s, FILE *fp );

    函数 fputs() 把字符串 s 写入到 fp 所指向的输出流中。如果写入成功,它会返回一个非负值,如果发生错误,则会返回 EOF。您也可以使用 int fprintf(FILE *fp,const char *format, …) 函数来写把一个字符串写入到文件中。尝试下面的实例:

    注意:请确保您有可用的 tmp 目录,如果不存在该目录,则需要在您的计算机上先创建该目录。

    /tmp 一般是 Linux 系统上的临时目录,如果你在 Windows 系统上运行,则需要修改为本地环境中已存在的目录,例如: C:\tmpD:\tmp等。

    实例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    #include <stdio.h>

    int main()
    {
    FILE *fp = NULL;

    fp = fopen("/tmp/test.txt", "w+");
    fprintf(fp, "This is testing for fprintf...\n");
    fputs("This is testing for fputs...\n", fp);
    fclose(fp);
    }

    当上面的代码被编译和执行时,它会在 /tmp 目录中创建一个新的文件 test.txt,并使用两个不同的函数写入两行。接下来让我们来读取这个文件。

    读取文件

    下面是从文件读取单个字符的最简单的函数:

    1
    int fgetc( FILE * fp );

    fgetc() 函数从 fp 所指向的输入文件中读取一个字符。返回值是读取的字符,如果发生错误则返回 EOF。下面的函数允许您从流中读取一个字符串:

    1
    char *fgets( char *buf, int n, FILE *fp );

    函数 fgets() 从 fp 所指向的输入流中读取 n - 1 个字符。它会把读取的字符串复制到缓冲区 buf,并在最后追加一个 null 字符来终止字符串。

    如果这个函数在读取最后一个字符之前就遇到一个换行符 ‘\n’ 或文件的末尾 EOF,则只会返回读取到的字符,包括换行符。您也可以使用 int fscanf(FILE *fp, const char *format, …) 函数来从文件中读取字符串,但是在遇到第一个空格字符时,它会停止读取。

    实例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    #include <stdio.h>

    int main()
    {
    FILE *fp = NULL;
    char buff[255];

    fp = fopen("/tmp/test.txt", "r");
    fscanf(fp, "%s", buff);
    printf("1: %s\n", buff );

    fgets(buff, 255, (FILE*)fp);
    printf("2: %s\n", buff );

    fgets(buff, 255, (FILE*)fp);
    printf("3: %s\n", buff );
    fclose(fp);

    }

    当上面的代码被编译和执行时,它会读取上一部分创建的文件,产生下列结果:

    1
    2
    3
    4
    1: This
    2: is testing for fprintf...

    3: This is testing for fputs...

    首先,fscanf() 方法只读取了 This,因为它在后边遇到了一个空格。其次,调用 fgets() 读取剩余的部分,直到行尾。最后,调用 fgets() 完整地读取第二行。

    二进制 I/O 函数

    下面两个函数用于二进制输入和输出:

    1
    2
    3
    4
    5
    size_t fread(void *ptr, size_t size_of_elements, 
    size_t number_of_elements, FILE *a_file);

    size_t fwrite(const void *ptr, size_t size_of_elements,
    size_t number_of_elements, FILE *a_file);

    这两个函数都是用于存储块的读写 - 通常是数组或结构体。

    C 预处理器

    C 预处理器不是编译器的组成部分,但是它是编译过程中一个单独的步骤。简言之,C 预处理器只不过是一个文本替换工具而已,它们会指示编译器在实际编译之前完成所需的预处理。我们将把 C 预处理器(C Preprocessor)简写为 CPP。

    所有的预处理器命令都是以井号(#)开头。它必须是第一个非空字符,为了增强可读性,预处理器指令应从第一列开始。下面列出了所有重要的预处理器指令:

    指令 描述
    #define 定义宏
    #include 包含一个源代码文件
    #undef 取消已定义的宏
    #ifdef 如果宏已经定义,则返回真
    #ifndef 如果宏没有定义,则返回真
    #if 如果给定条件为真,则编译下面代码
    #else #if 的替代方案
    #elif 如果前面的 #if 给定条件不为真,当前条件为真,则编译下面代码
    #endif 结束一个 #if……#else 条件编译块
    #error 当遇到标准错误时,输出错误消息
    #pragma 使用标准化方法,向编译器发布特殊的命令到编译器中

    预处理器实例

    分析下面的实例来理解不同的指令。

    1
    #define MAX_ARRAY_LENGTH 20

    这个指令告诉 CPP 把所有的 MAX_ARRAY_LENGTH 替换为 20。使用 #define 定义常量来增强可读性。

    1
    2
    #include <stdio.h>
    #include "myheader.h"

    这些指令告诉 CPP 从系统库中获取 stdio.h,并添加文本到当前的源文件中。下一行告诉 CPP 从本地目录中获取 myheader.h,并添加内容到当前的源文件中。

    1
    2
    #undef  FILE_SIZE
    #define FILE_SIZE 42

    这个指令告诉 CPP 取消已定义的 FILE_SIZE,并定义它为 42。

    1
    2
    3
    #ifndef MESSAGE
    #define MESSAGE "You wish!"
    #endif

    这个指令告诉 CPP 只有当 MESSAGE 未定义时,才定义 MESSAGE。

    1
    2
    3
    #ifdef DEBUG
    /* Your debugging statements here */
    #endif

    这个指令告诉 CPP 如果定义了 DEBUG,则执行处理语句。在编译时,如果您向 gcc 编译器传递了 -DDEBUG 开关量,这个指令就非常有用。它定义了 DEBUG,您可以在编译期间随时开启或关闭调试。

    预定义宏

    ANSI C 定义了许多宏。在编程中您可以使用这些宏,但是不能直接修改这些预定义的宏。

    描述
    DATE 当前日期,一个以 “MMM DD YYYY” 格式表示的字符常量。
    TIME 当前时间,一个以 “HH:MM:SS” 格式表示的字符常量。
    FILE 这会包含当前文件名,一个字符串常量。
    LINE 这会包含当前行号,一个十进制常量。
    STDC 当编译器以 ANSI 标准编译时,则定义为 1。

    让我们来尝试下面的实例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    #include <stdio.h>

    main()
    {
    printf("File :%s\n", __FILE__ );
    printf("Date :%s\n", __DATE__ );
    printf("Time :%s\n", __TIME__ );
    printf("Line :%d\n", __LINE__ );
    printf("ANSI :%d\n", __STDC__ );

    }

    当上面的代码(在文件 test.c 中)被编译和执行时,它会产生下列结果:

    1
    2
    3
    4
    5
    File :test.c
    Date :Jun 2 2012
    Time :03:36:24
    Line :8
    ANSI :1

    预处理器运算符

    C 预处理器提供了下列的运算符来帮助您创建宏:

    宏延续运算符(\)

    一个宏通常写在一个单行上。但是如果宏太长,一个单行容纳不下,则使用宏延续运算符(\)。例如:

    1
    2
    #define  message_for(a, b)  \
    printf(#a " and " #b ": We love you!\n")
    字符串常量化运算符(#)

    在宏定义中,当需要把一个宏的参数转换为字符串常量时,则使用字符串常量化运算符(#)。在宏中使用的该运算符有一个特定的参数或参数列表。例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    #include <stdio.h>

    #define message_for(a, b) \
    printf(#a " and " #b ": We love you!\n")

    int main(void)
    {
    message_for(Carole, Debra);
    return 0;
    }

    当上面的代码被编译和执行时,它会产生下列结果:

    1
    Carole and Debra: We love you!
    标记粘贴运算符(##)

    宏定义内的标记粘贴运算符(##)会合并两个参数。它允许在宏定义中两个独立的标记被合并为一个标记。例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    #include <stdio.h>

    #define tokenpaster(n) printf ("token" #n " = %d", token##n)

    int main(void)
    {
    int token34 = 40;

    tokenpaster(34);
    return 0;
    }

    当上面的代码被编译和执行时,它会产生下列结果:

    1
    token34 = 40

    这是怎么发生的,因为这个实例会从编译器产生下列的实际输出:

    1
    printf ("token34 = %d", token34);

    这个实例演示了 token##n 会连接到 token34 中,在这里,我们使用了字符串常量化运算符(#)标记粘贴运算符(##)

    defined() 运算符

    预处理器 defined 运算符是用在常量表达式中的,用来确定一个标识符是否已经使用 #define 定义过。如果指定的标识符已定义,则值为真(非零)。如果指定的标识符未定义,则值为假(零)。下面的实例演示了 defined() 运算符的用法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    #include <stdio.h>

    #if !defined (MESSAGE)
    #define MESSAGE "You wish!"
    #endif

    int main(void)
    {
    printf("Here is the message: %s\n", MESSAGE);
    return 0;
    }

    当上面的代码被编译和执行时,它会产生下列结果:

    1
    Here is the message: You wish!

    参数化的宏

    CPP 一个强大的功能是可以使用参数化的宏来模拟函数。例如,下面的代码是计算一个数的平方:

    1
    2
    3
    int square(int x) {
    return x * x;
    }

    我们可以使用宏重写上面的代码,如下:

    1
    #define square(x) ((x) * (x))

    在使用带有参数的宏之前,必须使用 #define 指令定义。参数列表是括在圆括号内,且必须紧跟在宏名称的后边。宏名称和左圆括号之间不允许有空格。例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    #include <stdio.h>

    #define MAX(x,y) ((x) > (y) ? (x) : (y))

    int main(void)
    {
    printf("Max between 20 and 10 is %d\n", MAX(10, 20));
    return 0;
    }

    当上面的代码被编译和执行时,它会产生下列结果:

    1
    Max between 20 and 10 is 20

    C 头文件

    头文件是扩展名为 .h 的文件,包含了 C 函数声明和宏定义,被多个源文件中引用共享。有两种类型的头文件:程序员编写的头文件和编译器自带的头文件。

    在程序中要使用头文件,需要使用 C 预处理指令 #include 来引用它。前面我们已经看过 stdio.h头文件,它是编译器自带的头文件。

    引用头文件相当于复制头文件的内容,但是我们不会直接在源文件中复制头文件的内容,因为这么做很容易出错,特别在程序是由多个源文件组成的时候。

    A simple practice in C 或 C++ 程序中,建议把所有的常量、宏、系统全局变量和函数原型写在头文件中,在需要的时候随时引用这些头文件。

    引用头文件的语法

    使用预处理指令 #include 可以引用用户和系统头文件。它的形式有以下两种:

    1
    #include <file>

    这种形式用于引用系统头文件。它在系统目录的标准列表中搜索名为 file 的文件。在编译源代码时,您可以通过 -I 选项把目录前置在该列表前。

    1
    #include "file"

    这种形式用于引用用户头文件。它在包含当前文件的目录中搜索名为 file 的文件。在编译源代码时,您可以通过 -I 选项把目录前置在该列表前。

    引用头文件的操作

    #include 指令会指示 C 预处理器浏览指定的文件作为输入。预处理器的输出包含了已经生成的输出,被引用文件生成的输出以及 #include 指令之后的文本输出。例如,如果您有一个头文件 header.h,如下:

    1
    char *test (void);

    和一个使用了头文件的主程序 program.c,如下:

    1
    2
    3
    4
    5
    6
    7
    int x;
    #include "header.h"

    int main (void)
    {
    puts (test ());
    }

    编译器会看到如下的代码信息:

    1
    2
    3
    4
    5
    6
    7
    int x;
    char *test (void);

    int main (void)
    {
    puts (test ());
    }

    只引用一次头文件

    如果一个头文件被引用两次,编译器会处理两次头文件的内容,这将产生错误。为了防止这种情况,标准的做法是把文件的整个内容放在条件编译语句中,如下:

    1
    2
    3
    4
    5
    6
    #ifndef HEADER_FILE
    #define HEADER_FILE

    the entire header file file

    #endif

    这种结构就是通常所说的包装器 #ifndef。当再次引用头文件时,条件为假,因为 HEADER_FILE 已定义。此时,预处理器会跳过文件的整个内容,编译器会忽略它。

    有条件引用

    有时需要从多个不同的头文件中选择一个引用到程序中。例如,需要指定在不同的操作系统上使用的配置参数。您可以通过一系列条件来实现这点,如下:

    1
    2
    3
    4
    5
    6
    7
    #if SYSTEM_1
    # include "system_1.h"
    #elif SYSTEM_2
    # include "system_2.h"
    #elif SYSTEM_3
    ...
    #endif

    但是如果头文件比较多的时候,这么做是很不妥当的,预处理器使用宏来定义头文件的名称。这就是所谓的有条件引用。它不是用头文件的名称作为 #include 的直接参数,您只需要使用宏名称代替即可:

    1
    2
    3
    #define SYSTEM_H "system_1.h"
    ...
    #include SYSTEM_H

    SYSTEM_H 会扩展,预处理器会查找 system_1.h,就像 #include 最初编写的那样。SYSTEM_H 可通过 -D 选项被您的 Makefile 定义。

    C 强制类型转换

    强制类型转换是把变量从一种类型转换为另一种数据类型。例如,如果您想存储一个 long 类型的值到一个简单的整型中,您需要把 long 类型强制转换为 int 类型。您可以使用强制类型转换运算符来把值显式地从一种类型转换为另一种类型,如下所示:

    1
    (type_name) expression

    请看下面的实例,使用强制类型转换运算符把一个整数变量除以另一个整数变量,得到一个浮点数:

    实例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    #include <stdio.h>

    int main()
    {
    int sum = 17, count = 5;
    double mean;

    mean = (double) sum / count;
    printf("Value of mean : %f\n", mean );

    }

    当上面的代码被编译和执行时,它会产生下列结果:

    1
    Value of mean : 3.400000

    这里要注意的是强制类型转换运算符的优先级大于除法,因此 sum 的值首先被转换为 double 型,然后除以 count,得到一个类型为 double 的值。

    类型转换可以是隐式的,由编译器自动执行,也可以是显式的,通过使用强制类型转换运算符来指定。在编程时,有需要类型转换的时候都用上强制类型转换运算符,是一种良好的编程习惯。

    整数提升

    整数提升是指把小于 intunsigned int 的整数类型转换为 intunsigned int 的过程。请看下面的实例,在 int 中添加一个字符:

    实例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    #include <stdio.h>

    int main()
    {
    int i = 17;
    char c = 'c'; /* ascii 值是 99 */
    int sum;

    sum = i + c;
    printf("Value of sum : %d\n", sum );

    }

    当上面的代码被编译和执行时,它会产生下列结果:

    1
    Value of sum : 116

    在这里,sum 的值为 116,因为编译器进行了整数提升,在执行实际加法运算时,把 ‘c’ 的值转换为对应的 ascii 值。

    常用的算术转换

    常用的算术转换是隐式地把值强制转换为相同的类型。编译器首先执行整数提升,如果操作数类型不同,则它们会被转换为下列层次中出现的最高层次的类型:

    Usual Arithmetic Conversion

    实例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    #include <stdio.h>

    int main()
    {
    int i = 17;
    char c = 'c'; /* ascii 值是 99 */
    float sum;

    sum = i + c;
    printf("Value of sum : %f\n", sum );

    }

    当上面的代码被编译和执行时,它会产生下列结果:

    1
    Value of sum : 116.000000

    在这里,c 首先被转换为整数,但是由于最后的值是 float 型的,所以会应用常用的算术转换,编译器会把 i 和 c 转换为浮点型,并把它们相加得到一个浮点数。

    C 错误处理

    C 语言不提供对错误处理的直接支持,但是作为一种系统编程语言,它以返回值的形式允许您访问底层数据。在发生错误时,大多数的 C 或 UNIX 函数调用返回 1 或 NULL,同时会设置一个错误代码 errno,该错误代码是全局变量,表示在函数调用期间发生了错误。您可以在 errno.h 头文件中找到各种各样的错误代码。

    所以,C 程序员可以通过检查返回值,然后根据返回值决定采取哪种适当的动作。开发人员应该在程序初始化时,把 errno 设置为 0,这是一种良好的编程习惯。0 值表示程序中没有错误。

    errno、perror() 和 strerror()

    C 语言提供了 perror()strerror() 函数来显示与 errno 相关的文本消息。

    • perror() 函数显示您传给它的字符串,后跟一个冒号、一个空格和当前 errno 值的文本表示形式。
    • strerror() 函数,返回一个指针,指针指向当前 errno 值的文本表示形式。

    让我们来模拟一种错误情况,尝试打开一个不存在的文件。您可以使用多种方式来输出错误消息,在这里我们使用函数来演示用法。另外有一点需要注意,您应该使用 stderr 文件流来输出所有的错误。

    实例

    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>
    #include <errno.h>
    #include <string.h>

    extern int errno ;

    int main ()
    {
    FILE * pf;
    int errnum;
    pf = fopen ("unexist.txt", "rb");
    if (pf == NULL)
    {
    errnum = errno;
    fprintf(stderr, "错误号: %d\n", errno);
    perror("通过 perror 输出错误");
    fprintf(stderr, "打开文件错误: %s\n", strerror( errnum ));
    }
    else
    {
    fclose (pf);
    }
    return 0;
    }

    当上面的代码被编译和执行时,它会产生下列结果:

    1
    2
    3
    错误号: 2
    通过 perror 输出错误: No such file or directory
    打开文件错误: No such file or directory

    被零除的错误

    在进行除法运算时,如果不检查除数是否为零,则会导致一个运行时错误。

    为了避免这种情况发生,下面的代码在进行除法运算前会先检查除数是否为零:

    实例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    #include <stdio.h>
    #include <stdlib.h>

    main()
    {
    int dividend = 20;
    int divisor = 0;
    int quotient;

    if( divisor == 0){
    fprintf(stderr, "除数为 0 退出运行...\n");
    exit(-1);
    }
    quotient = dividend / divisor;
    fprintf(stderr, "quotient 变量的值为 : %d\n", quotient );

    exit(0);
    }

    当上面的代码被编译和执行时,它会产生下列结果:

    1
    除数为 0 退出运行...

    程序退出状态

    通常情况下,程序成功执行完一个操作正常退出的时候会带有值 EXIT_SUCCESS。在这里,EXIT_SUCCESS 是宏,它被定义为 0。

    如果程序中存在一种错误情况,当您退出程序时,会带有状态值 EXIT_FAILURE,被定义为 -1。所以,上面的程序可以写成:

    实例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    #include <stdio.h>
    #include <stdlib.h>

    main()
    {
    int dividend = 20;
    int divisor = 5;
    int quotient;

    if( divisor == 0){
    fprintf(stderr, "除数为 0 退出运行...\n");
    exit(EXIT_FAILURE);
    }
    quotient = dividend / divisor;
    fprintf(stderr, "quotient 变量的值为: %d\n", quotient );

    exit(EXIT_SUCCESS);
    }

    当上面的代码被编译和执行时,它会产生下列结果:

    1
    quotient 变量的值为 : 4

    C 递归

    递归指的是在函数的定义中使用函数自身的方法。

    举个例子:
    从前有座山,山里有座庙,庙里有个老和尚,正在给小和尚讲故事呢!故事是什么呢?”从前有座山,山里有座庙,庙里有个老和尚,正在给小和尚讲故事呢!故事是什么呢?’从前有座山,山里有座庙,庙里有个老和尚,正在给小和尚讲故事呢!故事是什么呢?……’”

    语法格式如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    void recursion()
    {
    statements;
    ... ... ...
    recursion(); /* 函数调用自身 */
    ... ... ...
    }

    int main()
    {
    recursion();
    }

    流程图:

    img

    C 语言支持递归,即一个函数可以调用其自身。但在使用递归时,程序员需要注意定义一个从函数退出的条件,否则会进入死循环。

    递归函数在解决许多数学问题上起了至关重要的作用,比如计算一个数的阶乘、生成斐波那契数列,等等。

    数的阶乘

    下面的实例使用递归函数计算一个给定的数的阶乘:

    实例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    #include <stdio.h>

    double factorial(unsigned int i)
    {
    if(i <= 1)
    {
    return 1;
    }
    return i * factorial(i - 1);
    }
    int main()
    {
    int i = 15;
    printf("%d 的阶乘为 %f\n", i, factorial(i));
    return 0;
    }

    当上面的代码被编译和执行时,它会产生下列结果:

    1
    15 的阶乘为 1307674368000.000000

    斐波那契数列

    下面的实例使用递归函数生成一个给定的数的斐波那契数列:

    实例

    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>

    int fibonaci(int i)
    {
    if(i == 0)
    {
    return 0;
    }
    if(i == 1)
    {
    return 1;
    }
    return fibonaci(i-1) + fibonaci(i-2);
    }

    int main()
    {
    int i;
    for (i = 0; i < 10; i++)
    {
    printf("%d\t\n", fibonaci(i));
    }
    return 0;
    }

    当上面的代码被编译和执行时,它会产生下列结果:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    0    
    1
    1
    2
    3
    5
    8
    13
    21
    34

    C 可变参数

    有时,您可能会碰到这样的情况,您希望函数带有可变数量的参数,而不是预定义数量的参数。C 语言为这种情况提供了一个解决方案,它允许您定义一个函数,能根据具体的需求接受可变数量的参数。下面的实例演示了这种函数的定义。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    int func(int, ... ) 
    {
    .
    .
    .
    }

    int main()
    {
    func(2, 2, 3);
    func(3, 2, 3, 4);
    }

    请注意,函数 func() 最后一个参数写成省略号,即三个点号(),省略号之前的那个参数是 int,代表了要传递的可变参数的总数。为了使用这个功能,您需要使用 stdarg.h 头文件,该文件提供了实现可变参数功能的函数和宏。具体步骤如下:

    • 定义一个函数,最后一个参数为省略号,省略号前面可以设置自定义参数。
    • 在函数定义中创建一个 va_list 类型变量,该类型是在 stdarg.h 头文件中定义的。
    • 使用 int 参数和 va_start 宏来初始化 va_list 变量为一个参数列表。宏 va_start 是在 stdarg.h 头文件中定义的。
    • 使用 va_arg 宏和 va_list 变量来访问参数列表中的每个项。
    • 使用宏 va_end 来清理赋予 va_list 变量的内存。

    现在让我们按照上面的步骤,来编写一个带有可变数量参数的函数,并返回它们的平均值:

    实例

    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>
    #include <stdarg.h>

    double average(int num,...)
    {

    va_list valist;
    double sum = 0.0;
    int i;

    /* 为 num 个参数初始化 valist */
    va_start(valist, num);

    /* 访问所有赋给 valist 的参数 */
    for (i = 0; i < num; i++)
    {
    sum += va_arg(valist, int);
    }
    /* 清理为 valist 保留的内存 */
    va_end(valist);

    return sum/num;
    }

    int main()
    {
    printf("Average of 2, 3, 4, 5 = %f\n", average(4, 2,3,4,5));
    printf("Average of 5, 10, 15 = %f\n", average(3, 5,10,15));
    }

    当上面的代码被编译和执行时,它会产生下列结果。应该指出的是,函数 average() 被调用两次,每次第一个参数都是表示被传的可变参数的总数。省略号被用来传递可变数量的参数。

    1
    2
    Average of 2, 3, 4, 5 = 3.500000
    Average of 5, 10, 15 = 10.000000

    C 内存管理

    本章将讲解 C 中的动态内存管理。C 语言为内存的分配和管理提供了几个函数。这些函数可以在 **** 头文件中找到。

    序号 函数和描述
    1 void *calloc(int num, int size); 在内存中动态地分配 num 个长度为 size 的连续空间,并将每一个字节都初始化为 0。所以它的结果是分配了 num*size 个字节长度的内存空间,并且每个字节的值都是0。
    2 void free(void *address); 该函数释放 address 所指向的内存块,释放的是动态分配的内存空间。
    3 void *malloc(int num); 在堆区分配一块指定大小的内存空间,用来存放数据。这块内存空间在函数执行完成后不会被初始化,它们的值是未知的。
    4 void *realloc(void *address, int newsize); 该函数重新分配内存,把内存扩展到 newsize

    注意:void * 类型表示未确定类型的指针。C、C++ 规定 void * 类型可以通过类型转换强制转换为任何其它类型的指针。

    动态分配内存

    编程时,如果您预先知道数组的大小,那么定义数组时就比较容易。例如,一个存储人名的数组,它最多容纳 100 个字符,所以您可以定义数组,如下所示:

    1
    char name[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
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>

    int main()
    {
    char name[100];
    char *description;

    strcpy(name, "Zara Ali");

    /* 动态分配内存 */
    description = (char *)malloc( 200 * sizeof(char) );
    if( description == NULL )
    {
    fprintf(stderr, "Error - unable to allocate required memory\n");
    }
    else
    {
    strcpy( description, "Zara ali a DPS student in class 10th");
    }
    printf("Name = %s\n", name );
    printf("Description: %s\n", description );
    }

    当上面的代码被编译和执行时,它会产生下列结果:

    1
    2
    Name = Zara Ali
    Description: Zara ali a DPS student in class 10th

    上面的程序也可以使用 calloc() 来编写,只需要把 malloc 替换为 calloc 即可,如下所示:

    1
    calloc(200, sizeof(char));

    当动态分配内存时,您有完全控制权,可以传递任何大小的值。而那些预先定义了大小的数组,一旦定义则无法改变大小。

    重新调整内存的大小和释放内存

    当程序退出时,操作系统会自动释放所有分配给程序的内存,但是,建议您在不需要内存时,都应该调用函数 free() 来释放内存。

    或者,您可以通过调用函数 realloc() 来增加或减少已分配的内存块的大小。让我们使用 realloc() 和 free() 函数,再次查看上面的实例:

    实例

    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
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>

    int main()
    {
    char name[100];
    char *description;

    strcpy(name, "Zara Ali");

    /* 动态分配内存 */
    description = (char *)malloc( 30 * sizeof(char) );
    if( description == NULL )
    {
    fprintf(stderr, "Error - unable to allocate required memory\n");
    }
    else
    {
    strcpy( description, "Zara ali a DPS student.");
    }
    /* 假设您想要存储更大的描述信息 */
    description = (char *) realloc( description, 100 * sizeof(char) );
    if( description == NULL )
    {
    fprintf(stderr, "Error - unable to allocate required memory\n");
    }
    else
    {
    strcat( description, "She is in class 10th");
    }

    printf("Name = %s\n", name );
    printf("Description: %s\n", description );

    /* 使用 free() 函数释放内存 */
    free(description);

    当上面的代码被编译和执行时,它会产生下列结果:

    1
    2
    Name = Zara Ali
    Description: Zara ali a DPS student.She is in class 10th

    您可以尝试一下不重新分配额外的内存,strcat() 函数会生成一个错误,因为存储 description 时可用的内存不足。

    C 命令行参数

    执行程序时,可以从命令行传值给 C 程序。这些值被称为命令行参数,它们对程序很重要,特别是当您想从外部控制程序,而不是在代码内对这些值进行硬编码时,就显得尤为重要了。

    命令行参数是使用 main() 函数参数来处理的,其中,argc 是指传入参数的个数,argv[] 是一个指针数组,指向传递给程序的每个参数。下面是一个简单的实例,检查命令行是否有提供参数,并根据参数执行相应的动作:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    #include <stdio.h>

    int main( int argc, char *argv[] )
    {
    if( argc == 2 )
    {
    printf("The argument supplied is %s\n", argv[1]);
    }
    else if( argc > 2 )
    {
    printf("Too many arguments supplied.\n");
    }
    else
    {
    printf("One argument expected.\n");
    }
    }

    使用一个参数,编译并执行上面的代码,它会产生下列结果:

    1
    2
    $./a.out testing
    The argument supplied is testing

    使用两个参数,编译并执行上面的代码,它会产生下列结果:

    1
    2
    $./a.out testing1 testing2
    Too many arguments supplied.

    不传任何参数,编译并执行上面的代码,它会产生下列结果:

    1
    2
    $./a.out
    One argument expected

    应当指出的是,argv[0] 存储程序的名称,argv[1] 是一个指向第一个命令行参数的指针,argv[n] 是最后一个参数。如果没有提供任何参数,argc 将为 1,否则,如果传递了一个参数,*argc** 将被设置为 2。

    多个命令行参数之间用空格分隔,但是如果参数本身带有空格,那么传递参数的时候应把参数放置在双引号 “” 或单引号 ‘’ 内部。让我们重新编写上面的实例,有一个空间,那么你可以通过这样的观点,把它们放在双引号或单引号””””。让我们重新编写上面的实例,向程序传递一个放置在双引号内部的命令行参数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    #include <stdio.h>

    int main( int argc, char *argv[] )
    {
    printf("Program name %s\n", argv[0]);

    if( argc == 2 )
    {
    printf("The argument supplied is %s\n", argv[1]);
    }
    else if( argc > 2 )
    {
    printf("Too many arguments supplied.\n");
    }
    else
    {
    printf("One argument expected.\n");
    }
    }

    使用一个用空格分隔的简单参数,参数括在双引号中,编译并执行上面的代码,它会产生下列结果:

    1
    2
    3
    4
    $./a.out "testing1 testing2"

    Progranm name ./a.out
    The argument supplied is testing1 testing2
    Author: 九指
    Link: /2019/11/06/C%E8%AF%AD%E8%A8%80%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/
    Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.
    Donate
    • 微信
    • 支付寶