Skip to content

C复习

约 4380 字大约 15 分钟

2025-07-08

  • 若使用bool类型,可以

    #include <stdbool.h>
    //或
    #define FALSE 0
    #define TRUE 1
  • 传统风格注释/*...*/不能嵌套,单行注释//...可以嵌套在传统风格注释中

  • 变量命名(用户所定义的标识符)只允许以数字、字母、下划线_开头。

  • printf函数格式化

    printf("%-1.2d")
    //'-'代表左对齐
    //'1'是至少占用字符空间
    //'.2'是精度
  • 常见转义字符表

    转义字符含义ASCII 值
    \a响铃(警报)0x07
    \b退格(Backspace)0x08
    \f换页(Form Feed)0x0C
    \n换行(Newline)0x0A
    \r回车(Carriage Return)0x0D
    \t水平制表符(Tab)0x09
    \v垂直制表符0x0B
    \\反斜杠0x5C
    \'单引号0x27
    \"双引号0x22
    \?问号0x3F
    \0空字符(Null)0x00
    \xhh十六进制字符(如 \x41 是 'A')自定义
    \ooo八进制字符(如 \101 是 'A')自定义
  • %i也可用于读写整数,但会自动通过00x前缀识别八进制或十六进制

  • 基本格式说明符​

    格式符类型输出示例说明
    %dint-123十进制有符号整数
    %uunsigned int456十进制无符号整数
    %ounsigned int777八进制无符号整数
    %xunsigned int1a3f十六进制小写(无符号)
    %Xunsigned int1A3F十六进制大写(无符号)
    %ffloat/double3.141593小数形式浮点数(默认6位小数)
    %efloat/double3.141593e+00科学计数法小写
    %Efloat/double3.141593E+00科学计数法大写
    %gfloat/double3.14159自动选择 %f 或 %e(更简洁)
    %Gfloat/double3.14159自动选择 %f 或 %E
    %ccharA单个字符
    %schar* (字符串)Hello字符串
    %pvoid*0x7ffd3456指针地址(十六进制)
    %%-%输出百分号本身
  • 宽度和精度控制​

    格式作用示例输出结果
    %5d最小宽度为5(右对齐)printf("%5d", 10)10
    %-5d最小宽度为5(左对齐)printf("%-5d", 10)10
    %05d宽度5,不足补0printf("%05d", 10)00010
    %.2f保留2位小数printf("%.2f", 3.14159)3.14
    %8.2f宽度8,保留2位小数printf("%8.2f", 3.14)3.14
  • 长度修饰符(用于整数和浮点数)​

    修饰符类型示例说明
    %hdshort int-123短整型
    %ldlong int-123456长整型
    %lldlong long int-123456长长整型
    %luunsigned long123456无符号长整型
    %Lflong double3.141593长双精度浮点数
  • scnaf()安全性问题

    scanf("%s", str) 可能导致缓冲区溢出。
    ​解决方案​​:使用 %nsn 为最大长度):

    char str[10];
    scanf("%9s", str);  // 最多读取 9 个字符 + '\0'
  • 在C89中,负数除法的结果可以向上或向下取整。C99中,除法的结果全部向0取整。

  • 一元运算符的优先级 > 二元运算符,比如-i * -k == (-i) * (-k)

    二元运算符都是左结合的(left associative),比如i - j - k == (i - j) - k

    一元运算符都是右结合的(right associative),比如-+i == -(+i)

  • 赋值运算符是右结合的,i = j = k = 0; 等价于i = (j = (k = 0))

    int i, j, k;
    i = 1;
    k = 1 + (j = i);
    printf("%d,%d,%d", i, j, k); //prints "1,1,2"
  • 复合赋值运算符与=一样是右结合的,i += j += k == i += (j += k)

  • 运算符优先级列表

    1. 数组下标、圆括号、成员选择(对象)、成员选择(指针) 运算符: [], (), ., -> 结合方向: 左到右

    2. 单目运算符 运算符: -, ~, ++, --, *, &, !, (类型), sizeof 结合方向: 右到左

    3. 乘法、除法、取模 运算符: *, /, % 结合方向: 左到右

    4. 加法、减法 运算符: +, - 结合方向: 左到右

    5. 位移运算 运算符: <<, >> 结合方向: 左到右

    6. 关系运算符 运算符: >, >=, <, <= 结合方向: 左到右

    7. 相等运算符 运算符: ==, != 结合方向: 左到右

    8. 按位与 运算符: & 结合方向: 左到右

    9. 按位异或 运算符: ^ 结合方向: 左到右

    10. 按位或 运算符: | 结合方向: 左到右

    11. 逻辑与 运算符: && 结合方向: 左到右

    12. 逻辑或 运算符: || 结合方向: 左到右

    13. 条件运算符 运算符: ?: 结合方向: 右到左

    14. 赋值运算符 运算符: =, /=, *=, %=, +=, -=, <<=, >>=, &=, ^=, |= 结合方向: 右到左

    15. 逗号运算符 运算符: , 结合方向: 左到右

​C 语言运算符优先级完整表格(从高到低)​

以下是 C 语言中所有运算符的优先级顺序,​​同一行的运算符优先级相同​​,按结合性决定计算顺序。


​1. 最高优先级(从左到右结合)​

运算符描述示例
()函数调用func(a, b)
[]数组下标arr[5]
.结构体成员访问obj.member
->指针结构体成员访问ptr->member
++ -- (后缀)后自增/自减i++arr[i--]

​2. 单目运算符(从右到左结合)​

运算符描述示例
++ -- (前缀)前自增/自减++i--j
+ -正负号-5+3.14
! ~逻辑非、按位取反!flag~mask
(type)强制类型转换(int)3.14
*解引用*ptr
&取地址&var
sizeof计算大小sizeof(int)

​3. 乘除取模(从左到右结合)​

运算符描述示例
* / %乘、除、取模a * b10 % 3

​4. 加减(从左到右结合)​

运算符描述示例
+ -加、减a + bx - y

​5. 位移(从左到右结合)​

运算符描述示例
<< >>左移、右移a << 2b >> 1

​6. 关系运算符(从左到右结合)​

运算符描述示例
< <= > >=大小比较a < bx >= 10

​7. 相等性判断(从左到右结合)​

运算符描述示例
== !=等于、不等于a == bx != 0

​8. 按位与(从左到右结合)​

运算符描述示例
&按位与flags & MASK

​9. 按位异或(从左到右结合)​

运算符描述示例
^按位异或a ^ b

​10. 按位或(从左到右结合)​

运算符描述示例
`

​11. 逻辑与(从左到右结合)​

运算符描述示例
&&逻辑与a > 0 && b < 10

​12. 逻辑或(从左到右结合)​

运算符描述示例
|

​13. 条件运算符(从右到左结合)​

运算符描述示例
?:三元条件a > b ? a : b

​14. 赋值运算符(从右到左结合)​

运算符描述示例
= += -= *= /= %= <<= >>= &= `= ^=`赋值及复合赋值

​15. 逗号运算符(从左到右结合)​

运算符描述示例
,逗号(顺序求值)a = 1, b = 2

​关键记忆技巧​

  1. ​括号最高​​:() > [] > -> > .
  2. ​单目右结合​​:++ -- ! ~ (type) * & sizeof
  3. ​算术 > 移位 > 比较 > 位运算 > 逻辑 > 条件 > 赋值 > 逗号​

​经典问题分析​

​问题 1:*ptr++ 是什么含义?​

  • 等价于 *(ptr++)(因为后缀 ++ 优先级高于 *)。
  • 先返回 *ptr,然后 ptr 自增。

​问题 2:a & b == c 的陷阱​

  • 实际解析为 a & (b == c)(因为 == 优先级高于 &)。
  • 正确写法:(a & b) == c

​问题 3:a = b = c 的执行顺序​

  • 从右到左结合:a = (b = c)

​总结​

  1. ​优先级决定运算顺序​​,​​结合性决定同优先级时的方向​​。

  2. 不确定时​​用括号明确优先级​​(如 (a & b) == c)。

  3. 避免依赖复杂表达式,拆分为多行更安全。


  • %运算符要求操作数是整数,浮点数使用fmod()函数

  • a[i++] += 2

  • 在 C 中,​​同一表达式内对同一变量的多次修改是未定义行为​​。a[i++] = a[i++] + 2

  • int的取值范围为-2^31 ~ 2^31 - 1

  • (double)a / b`` (double)(a / b)

  • 补码的规则:对于有符号数,补码的最高位是符号位,“0” 表示正数,“1” 表示负数。当符号位为 “1” 时,求其原码的方法是:先对除符号位以外的其他位取反,然后再加 1。

    1. 原码中的零:存在歧义

    • 定义:原码的最高位为符号位,其余位表示绝对值。
    • 问题
      对于 0,存在两种编码方式:
      • +0:符号位为 0,数值位全 0 → 0000 0000(8 位)。
      • -0:符号位为 1,数值位全 0 → 1000 0000(8 位)。
    • 缺陷
      同一个数值(0)有两种不同的二进制表示,导致以下问题:
      1. 逻辑冗余:计算机需要额外判断两种零是否相等。
      2. 浪费编码:占用了一个额外的编码位置(原码中 1000 0000 本可用于表示其他数值)。

    2. 补码中的零:唯一表示

    • 定义:正数的补码是其本身,负数的补码是原码取反加 1。
    • 推导
      • +0 的原码为 0000 0000,补码保持不变 → 0000 0000
      • -0 的原码为 1000 0000,补码计算步骤:
        1. 取反:1111 1111
        2. 加 1:1 0000 0000 → 溢出最高位后结果为 0000 0000
    • 结论
      无论正零还是负零,补码都表示为 0000 0000,消除了歧义。
  • 十六进制数 0xff 转换为十进制数是 255,转换为二进制数是 11111111

    • 十六进制转十进制的计算方法:在十六进制中,f 代表 15,根据位权展开计算,0xff 即 15×161+15×160=255。
  • sizeof(a) 是 int 类型的大小,通常为 4 字节(取决于编译器和系统)

  • ~是按位取反运算符

  • int x = 20;~x-21

  • fabs()即为浮点数版本的abs()

  • 在 C 语言中,函数参数传递方式分为值传递(单项值传递)和地址传递

  • 每进入更深一层的递归时,问题的规模相对于前一次递归不一定是变化的,可能减小(阶乘)、不变(DFS)甚至增大(斐波那契),具体取决于递归算法的设计和实现。

  • #include "" 的搜索规则

    • 编译器行为

      1. 优先搜索用户指定的目录(如果路径被显式指定)。
        例如:#include "src/header.h" 会搜索当前目录下的 src 子目录。
      2. 若未指定路径,则:
        • 先搜索当前源文件所在的目录
        • 再搜索编译器或 IDE 设置的用户自定义目录(如项目配置的头文件路径)。
        • 最后搜索标准系统目录(如 /usr/include 或编译器安装路径)。
  • #include <> 的搜索规则

    • 编译器行为

      • 直接跳过当前目录,仅搜索:
        1. 标准系统目录(如 GCC 的 /usr/include)。
        2. 编译器或 IDE 设置的系统头文件路径(如 -I 选项指定的路径)。
  • #pragma once 告诉编译器,包含该指令的头文件在一个编译单元(通常是一个源文件及其包含的所有头文件)中只被处理一次。当编译器第一次遇到包含 #pragma once 的头文件时,它会记住这个头文件已经被包含,并在后续的编译过程中忽略对该头文件的再次包含。

  • 在 C 语言中,#include 预处理指令主要用于包含头文件(.h 文件),但从语法上来说,它可以包含任何文本文件

  • 在 C 语言中,外部变量和静态变量都存储在静态存储区。当它们未显式赋初值时,系统会自动将它们初始化为 0(对于数值型变量)或空指针(对于指针型变量)。

  • void f(int b)
    {
        static int a = 10;
        a = a - b;
        printf(":%d", a);
    }
    int main()
    {
        static int a = 2;
        {
            f(a);
            static int a = 1;
            f(a);
        }
        return 0;
    }
    // f(a)中的a和外面的a不是同一个a
    // 最终输出8 7
  • 数组省略只可省略第一维。但

    场景是否可省略第一维示例
    声明并初始化✅ 可省略int arr[][3] = { ... };
    未初始化❌ 不可省略int arr[2][3];
    结构体 / 联合体成员❌ 不可省略struct { int arr[2][3]; };
    函数参数(指针形式)❌ 不可省略void func(int (*arr)[2][3]);
    typedef 定义数组类型❌ 不可省略typedef int ArrType[2][3];
    动态内存分配(malloc)❌ 不可省略int (*arr)[3] = malloc(2*...);
  • 若要移动指针,需显式转换

    若需要遍历数组,应将数组名转换为指针变量:

    char s[10] = "hello";
    char *p = s;  // p指向s[0]
    p++;          // ✅ p现在指向s[1](即'e')

    5. 数组名与指针的行为差异

    操作数组名 s指针 p
    s++❌ 编译错误✅ 指针自增
    sizeof(s)返回数组总大小(如 10)返回指针大小(如 4/8)
    &s类型为 char (*)[10]类型为 char**
    作为函数参数隐式转换为 char*直接传递指针
  • 指针之间可以互相比较。数组的地址是连续的,所以可以比较。

  • C语言中访问数组a[i]允许写成i[a],因为*(a + i) == *(i + a)

  • C语言中,字符串常量存储在只读数据段,多个相同的字符串常量在内存中只会存储一份。strlen()不计算\0

  • #include <stdio.h>
    
    int main()
    {
        char *ch;
        scanf("%s", ch);
        switch(ch)
        {
            case "1":
                printf("1");
                break;
        }
    }

    问题分析

    1. 未初始化的指针
      char *ch 声明了一个指针,但没有分配内存。scanf("%s", ch) 会尝试将输入写入随机内存地址,导致段错误

    2. 使用字符串字面量作为 switch 的 case
      switch 语句只能比较整数类型(如 intchar)或枚举类型,而 "1" 是字符串字面量(const char*),无法直接用于 case

    3. 缺少输入长度限制
      scanf("%s", ch) 没有指定最大输入长度,可能导致缓冲区溢出。

  • #include <stdio.h>
    
    int main() {
        int day = 3;
        switch (day) {  // 整型表达式
            case 1:
                printf("Monday\n");
                break;
            case 2:
                printf("Tuesday\n");
                break;
            case 3: case 4:  // 合并 case
                printf("Midweek\n");
                break;
            default:  // 可选
                printf("Another day\n");
        }
        return 0;
    }
  • case可以合并。

  • swtich语句中case后面必须是常量表达式,不可以是变量。并且要求类型必须和switch表达式中的类型一致。


  • 数组的初始化只能在声明时进行,后续赋值只能逐个元素操作。

  • 声明数组时如果没有进行初始化,则必须提供所有维度的长度。

  • \x是十六进制转义序列,在字符串定义中会被转化成十六进制的数字

  • char str[50];
    strcpy(str, "guess-what?");
    strcpy(&str[3], "none");
    strcat(str, "blue?");
    //str为guenoneblue?
  • char *c = "sysuer";
    sizeof(c) == 4;
  • const int*可以指向int类型变量,但不能通过指针修改其值。

    int*不能指向const int类型变量。

  • 函数指针不支持算术运算,因为函数在内存中的布局不是连续的。但可以进行相等性的比较操作,判断是否为同一个函数。

  • int arr[3][3] = {1,4,7,2,5,8,3,6,9};
    int (*p)[3] = arr; //p是指向一个大小为3的数组的指针
    int a = *p[1]; // a == 2
    int b = **p + 1; // b == 2
    int c = **(p + 1); // c == 2
    int d = *(*p + 1) + 1); // d == 5
  • int* restrict p 如果指针p指向的对象之后需要修改,那么该对象不会允许通过除指针p之外的任何方式访问。

  • C语言数据类型大小

    在C语言中,数据类型决定了变量存储占用的空间以及如何解释存储的位模式。不同的数据类型在不同的系统中可能占用不同的字节数和位数。以下是C语言中常见数据类型的大小和取值范围。

    整数类型

    有符号整数类型

    • signed char: 1字节,取值范围为-128到127。

    • short int: 2字节,取值范围为-32,768到32,767。

    • int: 4字节,取值范围为-2,147,483,648到2,147,483,647。

    • long int: 4字节,取值范围为-2,147,483,648到2,147,483,647。

    • long long int: 8字节,取值范围为-9,223,372,036,854,775,808到9,223,372,036,854,775,807。

    无符号整数类型

    • unsigned char: 1字节,取值范围为0到255。

    • unsigned short int: 2字节,取值范围为0到65,535。

    • unsigned int: 4字节,取值范围为0到4,294,967,295。

    • unsigned long int: 4字节,取值范围为0到4,294,967,295。

    • unsigned long long int: 8字节,取值范围为0到18,446,744,073,709,551,615。

    浮点类型

    • float: 4字节,取值范围为±3.4e38,精确到6位小数。

    • double: 8字节,取值范围为±1.7e308,精确到15位小数。

    • long double: 12字节,取值范围为±1.19e4932,精确到18位小数。

  • 结构化程序的三种结构:顺序结构、选择结构、循环结构。goto语句不算。

  • includedefineprintf不是C语言关键字。

  • 程序调试的任务是诊断和改正程序中的错误。

  • 结构化程序可以完成任何复杂的任务。

  • 使用strcpy时,目标空间必须可变,不可以是常量字符串。

  • strlen不止遇到\0会终止,遇到0也会。但得看清楚是'0'还是\0\,前者ASCII编码是48。

  • 二维数组第二维长度不可以省略。

  • int a[1][2], *p;
    p = a;
    // 这里的p是指向int类型的指针,不能赋a,a是int**类型指针
  • 字符数组不能用=赋值,除非在初始化的时候。字符串字面量char *str则可以。

    字符数组和字符串是两个东西。

  • 宏定义 #define square(x) x * x 存在运算符优先级问题,导致意外结果。例如:

    int a = 3;
    printf("%d", square(a + 1));  // 预期 16,实际计算 3 + 1 * 3 + 1 = 7

    正确写法应添加括号确保运算顺序:

    #define square(x) ((x) * (x))
  • 小写字母的ASCII码大于大写字母。

  • 两个指针变量不可以相加。

  • int func(char *s)
    {
        char *t = s;
        while (*t++);
        return t - s;
    }

    该函数不能计算字符串长度,若要计算应该return t - s - 1;

  • 结构体类型名是由关键字struct和结构体名组成的。

  • 注意fprintf()读写的是字符数据,对象是文本文件(ASCII文件), 而getw()putw()fread()fwrite()是读写二进制数据,对象是二进制文件;

贡献者: Sherlock