本文最后更新于 2025-03-18,学习久了要注意休息哟

第一章 构造类型

1.1 结构体

:one: 定义和声明

语法结构

在 C 语言中,可以通过以下语法来定义结构体(struct):

struct 结构体名 {
    成员类型 成员名;
    成员类型 成员名;
    ...
};
  • 其中,结构体名 是标识该结构体类型的名字。
  • 花括号 {} 中列出了该结构体的所有成员。
  • 每个成员由 “类型 + 成员名” 组成。

声明方式

全局/局部声明
结构体定义后,可以在全局或局部范围内声明结构体变量。例如:

struct Date {
    int year;
    int month;
    int day;
};

// 全局声明
struct Date today;

int main(void) {
    // 局部声明
    struct Date birthday;
    return 0;
}

typedef 别名优化
为了书写简洁,可以使用 typedef 给结构体类型定义一个别名:

typedef struct Date {
    int year;
    int month;
    int day;
} Date;

// 这样就可以直接使用 Date 来声明变量
Date today;

:two: 初始化和访问

初始化方式

顺序初始化
根据定义时的成员顺序进行初始化:

Date today = {2025, 3, 17};

以上方式会依次给 year, month, day 赋值。

指定成员初始化
可以只给部分成员赋值,未赋值的成员会使用默认值(一般为 0):

Date today = {.day = 17, .year = 2025};
// .month 未指定,默认初始化为0

访问方式

成员运算符 .
如果我们有一个结构体变量 today,访问其成员可以使用 . 运算符:

printf("Year: %d\n", today.year);

指针运算符 ->
如果有一个指向结构体的指针 Date *p, 则可通过 p->year 访问成员:

Date today = {2025, 3, 17};
Date *p = &today;
printf("Month: %d\n", p->month);

嵌套结构体

在结构体中可以定义另一个结构体类型,实现多层数据建模,例如:

typedef struct {
    int year;
    int month;
    int day;
} Date;

typedef struct {
    char name[50];
    int age;
    Date birthday;  // 嵌套另一个结构体类型
} Student;

:three: 内存对齐

内存对齐规则

  • 结构体中每个成员都会按照其类型大小与编译器默认对齐参数(一般是 4 或 8)的较小值进行对齐。
  • 成员之间可能会产生“填充字节(padding)”,以满足对齐需求。

结构体大小计算

结构体整体大小会对齐到其 最大对齐数 的整数倍。

例如:

struct Test {
    char a;     // 1字节
    int b;      // 4字节
    short c;    // 2字节
};

可能的内存布局为:

[ a (1字节) | 填充(3字节) | b (4字节) | c (2字节) | 填充(2字节) ]

最终结构体大小为 12 字节(具体依赖编译器及平台,但常见情况是如此)。

优化策略:成员排序

  • 为减少“内存空洞”,可将大类型的成员排在一起,把小类型的成员排在一起,从而降低填充浪费。
  • 例如,将 int 成员放在前面,shortchar 这样的小类型放在后面,通常可减少整体的填充字节数。

:four: 高级应用

结构体数组

例如管理一个班级的若干学生信息:

typedef struct {
    char name[50];
    int age;
    Date birthday;
} Student;

Student class[50];  // 数组形式存储50位学生信息

这样的顺序结构方便按下标进行管理和遍历。

结构体指针

动态内存分配

通过

malloc

calloc

等函数为结构体分配内存,并返回指针:

Student *p = malloc(sizeof(Student));
if (p != NULL) {
    strcpy(p->name, "Alice");
    p->age = 20;
    p->birthday.year = 2005;
    p->birthday.month = 6;
    p->birthday.day = 12;
    // ...
    free(p);
}

链表实现

使用结构体节点和指针可以构建链表等动态数据结构,在后续数据结构知识中尤为常见:

typedef struct Node {
    int data;
    struct Node *next;
} Node;

2.3 联合体类型

:one: 定义和声明

联合体(union)与结构体类似,也使用花括号来定义成员。

不同之处在于 所有成员共享同一段内存,因此联合体变量的大小取决于其“最长的成员”所需的内存。

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

同样可以用 typedef 方式来简化声明:

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

:two: 内存模型

  • 对于一个联合体,如上所示,ifstr 都存储在同一块内存起始地址。

  • 联合体大小 通常等于其最大成员所需的内存大小,并会根据对齐要求进行整体对齐。

  • 例如:

    union Data myData;
    myData.i = 10;
    printf("%d\n", myData.i);
    myData.f = 3.14f;
    // 此时再打印 myData.i,将得到无意义结果,因为内存被 f 覆盖。
    

:three: 注意事项

  1. 联合体常用于需要“节省内存”的场景,或者表明同一内存中存储的数据会以不同形式被解释(如网络协议解析、硬件寄存器访问等)。
  2. 需要清晰理解不同成员之间共享同一内存区域,“最后一次赋值”会覆盖之前的内容。
  3. 在使用联合体时,必须根据当前成员的类型进行正确访问,否则会得到未定义或意外的结果。

2.4 枚举类型

:one: 定义和声明

枚举类型(enum 用于定义一组 相关的整型常量

语法示例:

enum Color {
    RED,    // 默认从0开始
    GREEN,  // 依次为1
    BLUE    // 依次为2
};

// 使用
enum Color c = GREEN;

也可在定义时指定具体数值:

enum Day {
    MON = 1,
    TUE = 2,
    WED = 3,
    THU = 4,
    FRI = 5,
    SAT = 6,
    SUN = 7
};

:two: 类型特征

  1. 在 C 语言中,枚举类型实际上被编译器视作“整型”的一种。

  2. 省去频繁的 #define 或常量宏定义,方便调试和阅读。

  3. 如果不手动指定值,默认从 0 开始并依次加 1。

  4. 可以与 typedef 结合使用,或与 switch-case 等语句结合增强可读性:

typedef enum {
    SUCCESS = 0,
    ERROR   = -1
} Status;

Status func() {
    // ...
    return SUCCESS;
}

第二章 类型修饰与限定

2.1 ​类型限定符

:one: const:常量性

知识要点

  • const 表示“只读”属性,一旦初始化就不可修改。
  • 编译器会在一定程度上对带 const 的变量进行优化(如放到只读存储区等)。
  • const 并不等于真正的“绝对不可改变”,比如通过指针强制转换仍可能改写,但这是不推荐且危险的行为。

代码示例
下面演示使用 const 修饰一个整数变量,让其成为只读属性,并尝试修改它(会导致编译错误或警告)。

#include <stdio.h>

int main(void)
{
    // 声明并初始化一个 const 整型变量
    // 🤔 const修饰的变量不可修改
    const int age = 18;  // ✅这里赋初值是允许的

    // age = 20; 
    // ❌ 如果尝试修改会编译出错(或警告),提示“read-only variable”

    // 打印这个 const 整型变量
    printf("我的年龄是 %d\n", age);

    return 0;
}

编译 & 运行示例

gcc const_example.c -o const_example
./const_example

输出示例

我的年龄是 18

(如果你取消注释 age = 20;,则会产生编译错误或警告)

:two: volatile:防止编译器优化

知识要点

  • volatile 告知编译器:此变量的值可能在程序之外发生变化(如硬件寄存器、IO 端口、信号变量等)。
  • 避免编译器出于优化目的而缓存变量值;每次使用都必须从内存或寄存器获取最新值。

代码示例
模拟一个“硬件寄存器”不断更新的场景,让编译器不要进行优化:

#include <stdio.h>

// 模拟一个全局的“硬件寄存器”计数器
volatile int g_counter = 0;

int main(void)
{
    // 🤖 假设在真实环境下,这个g_counter可能会被中断或其他硬件更新
    // 👀 使用volatile,让编译器每次都读取这个全局变量的实时值

    printf("开始计数...\n");
    // 我们模拟循环10次,每次打印g_counter的值
    for(int i = 0; i < 10; i++)
    {
        // 模拟外部更新
        g_counter++;

        // 每次都从内存读取g_counter的值(因为它被volatile修饰)
        printf("计数器当前值: %d\n", g_counter);
    }

    return 0;
}

编译 & 运行示例

gcc volatile_example.c -o volatile_example
./volatile_example

输出示例(大致)

开始计数...
计数器当前值: 1
计数器当前值: 2
计数器当前值: 3
...
计数器当前值: 10

注意: 如果这个 g_counter 不是 volatile,编译器有可能在某些优化场景下,并不实时读取它的最新值,导致意料之外的错误。


:three: restrict:指针独占访问优化

知识要点

  • restrict 修饰的指针,表示该指针是访问该对象的唯一方式,没有其他指针会指向同一块内存。
  • 让编译器能够进行更激进的优化(尤其是在某些高性能场景,restrict 提高编译器对指针别名问题的分析优化能力)。
  • 仅在 C99 标准及以上可用。

代码示例
以一个数组拷贝函数示例来演示 restrict 的用法。注意,如果 srcdst 实际上会重叠,则使用 restrict 会导致未定义行为。

#include <stdio.h>
#include <string.h>

// 使用 restrict 修饰符,告诉编译器 src 与 dst 指向的内存不重叠
void my_mem_copy(char * restrict dst, const char * restrict src, size_t n)
{
    // 🚀 由于dst和src不会重叠,编译器可放心进行优化
    for(size_t i = 0; i < n; i++)
    {
        dst[i] = src[i];
    }
}

int main(void)
{
    char source[] = "Hello restrict!";
    char destination[20];

    // 复制字符串到destination中
    my_mem_copy(destination, source, strlen(source) + 1);

    printf("拷贝后的字符串: %s\n", destination);
    return 0;
}

编译 & 运行示例

gcc restrict_example.c -o restrict_example
./restrict_example

输出示例

拷贝后的字符串: Hello restrict!

2.2 存储类说明符

:one: auto

知识要点

  • auto 是默认的局部变量存储类型,C99 前写不写都一样。
  • 在 C++ 中则有更多含义(如类型推断),但在 C 语言里通常很少显式使用。

代码示例

#include <stdio.h>

int main(void)
{
    auto int num = 100; // 在C语言中,auto可省略,一般不会显式写出来
    printf("num = %d\n", num);
    return 0;
}

输出结果与省略 auto 完全相同,它只是表明这是一个自动存储类型(生命周期在当前代码块,执行完毕后释放)。

:two: register

知识要点

  • register 建议编译器将变量尽量存放在 CPU 寄存器中(以提高访问速度)。
  • 现代编译器一般都有完善的优化策略,register 仅仅是个建议,不一定起作用。
  • 此外,register 变量不能取地址(因为它可能放在寄存器中而非内存)。

代码示例

#include <stdio.h>

int main(void)
{
    register int count = 0; // 尝试让编译器把count放进CPU寄存器

    for(int i = 0; i < 5; i++)
    {
        count += i;
    }
    printf("累加结果: %d\n", count);

    // 下面的语句会编译报错或警告(视编译器而定):
    // printf("count的地址: %p\n", &count);

    return 0;
}

输出示例

累加结果: 10

:three: static

知识要点

  • 修饰局部变量时,使其在函数多次调用中维持值不丢失(只初始化一次)。
  • 修饰全局变量或函数时,限制其作用域仅限当前源文件(即内部链接)。
  • 还能修饰函数内部的静态变量等,许多场景下用于缓存或计数等。

代码示例 1:static 局部变量

#include <stdio.h>

// 定义一个测试函数
void foo(void)
{
    static int call_count = 0; // 👀只在第一次调用时初始化
    call_count++;
    printf("foo函数已被调用 %d 次\n", call_count);
}

int main(void)
{
    foo(); // 第1次
    foo(); // 第2次
    foo(); // 第3次

    return 0;
}

输出示例

foo函数已被调用 1 次
foo函数已被调用 2 次
foo函数已被调用 3 次

(如果 call_count 不使用 static,则每次调用时都会初始化为 0,导致每次打印都是 1 )

代码示例 2:static 限定全局作用域

  • static 修饰的全局变量或函数,只在本 .c 文件可见,不能被其他 .c 文件访问。
  • 如果我们在 my_file.c 中写 static int g_num = 42;,则在其他文件中无法直接引用 g_num

:four: extern

知识要点

  • 声明一个全局变量或函数在当前文件中“存在”,实际上定义在别的地方。
  • 常用在多文件编译时,如在 a.c 中定义了 int g_val = 100;,在 b.c 中要用 extern int g_val; 告诉编译器此变量存在即可。
  • 注意和 static 的对比:extern 面向外部链接static 面向内部链接

代码示例
(此示例需多文件协作,假设有 main.cdata.c)

data.c

#include <stdio.h>

// 定义一个全局变量,初始值为100
int g_val = 100;

void printVal(void)
{
    printf("当前的全局变量 g_val = %d\n", g_val);
}

main.c

#include <stdio.h>

// 声明而非定义,告诉编译器,这个变量在别的文件里
extern int g_val;
extern void printVal(void);

int main(void)
{
    // 在这里访问并修改 g_val
    g_val = 2023;
    printVal();

    return 0;
}

编译 & 运行示例

gcc main.c data.c -o extern_example
./extern_example

输出示例

当前的全局变量 g_val = 2023

2.3 类型重定义

:one: typedef` 的进阶用法

知识要点

  • typedef 常用于给复杂类型(如指针、结构体、函数指针等)重命名,使其简洁明了。

  • 一般的写法:typedef 原类型 新类型名;

  • 定义结构体、联合体等别名时常用:

    typedef struct MyStruct {
        int x;
        int y;
    } MyStruct_t;
    

代码示例

#include <stdio.h>

// 1. 给整型重定义
typedef int MyInt;

// 2. 给结构体重定义
typedef struct Point {
    int x;
    int y;
} Point_t;

// 3. 给指针重定义
typedef int* IntPtr;

int main(void)
{
    MyInt num = 50;  // 相当于 int num = 50;
    Point_t p = {10, 20};
    IntPtr pNum = &num;

    printf("num = %d, pNum指向的值 = %d\n", num, *pNum);
    printf("p = (%d, %d)\n", p.x, p.y);

    return 0;
}

输出示例

num = 50, pNum指向的值 = 50
p = (10, 20)

:two: 与宏定义的类型别名对比

知识要点

  • 宏定义的别名在预处理阶段完成文本替换,没有类型检查机制;而 typedef 则在编译阶段生效,具有更严格的类型检查。

  • 宏:

    #define INT_PTR int*
    
  • typedef
    

    typedef int* INT_PTR;
    

示例对比

#include <stdio.h>

// 宏定义一个指针类型
#define INT_PTR_MACRO int*

// 使用typedef定义一个指针类型
typedef int* INT_PTR_TYPEDEF;

int main(void)
{
    // 宏
    INT_PTR_MACRO a, b;
    // 👀 等价于 int* a, b; => b 其实是一个int类型,不是指针类型
    // 很容易产生误解

    // typedef
    INT_PTR_TYPEDEF c, d;
    // 👀 等价于 int *c, *d; => c和d都是int指针

    // 分配内存或赋值时就能看出区别
    int x = 10;
    int y = 20;
    a = &x; // 正确
    // b = &y; // ❌编译会出错或者警告,因为b实际上是int类型,不是指针

    c = &x;
    d = &y;
    printf("*c = %d, *d = %d\n", *c, *d);

    return 0;
}

输出示例

*c = 10, *d = 20

(如果你对 b 使用地址赋值,会有警告或报错,可观察差异)

:three: typedef 定义函数指针

知识要点

  • 函数指针在声明时往往比较复杂,通过 typedef 可以让声明清晰。

  • 格式:

    typedef 返回类型 (*别名)(参数类型列表);
    
  • 或者先声明函数指针再用 typedef,不过直接写也可以。

代码示例

#include <stdio.h>

// 1. 定义一个普通函数
int add(int a, int b)
{
    return a + b;
}

// 2. 使用typedef定义函数指针类型,加深理解
typedef int (*FuncPtr)(int, int);

int main(void)
{
    // 使用我们定义的函数指针类型
    FuncPtr fp = add;
    int result = fp(3, 5);  // 相当于 add(3,5)
    printf("3 + 5 = %d\n", result);

    return 0;
}

输出示例

3 + 5 = 8

第三章 高级类型特性

3.1 ​位域

:one: 位域结构体定义与内存布局

知识要点

  • 位域(Bit-Field)是结构体中按位分配存储空间的字段。
  • 可以精确指定成员占用的位数,如 unsigned int bit1 : 1;
  • 在某些场景(例如硬件寄存器映射、网络协议报文封装)中非常常用。
  • 注意编译器和平台的差异:不同编译器可能对位域的对齐方式或存储顺序有所不同(大端/小端相关、以及同字节对齐策略)。

代码示例
以下示例中我们演示一个“权限标志”结构体,将权限标志拆分为若干个位域(读/写/执行)。这只是为了演示位域的用法。

#include <stdio.h>

// 定义一个带位域的结构体,按位分配权限标志
// 🔐 read、write、exec 各占1位
struct Permission {
    unsigned int read  : 1; // 表示是否拥有 读 权限
    unsigned int write : 1; // 表示是否拥有 写 权限
    unsigned int exec  : 1; // 表示是否拥有 执行 权限
    unsigned int reserved : 29; // 预留位(用于对齐或后续扩展)
};

int main(void)
{
    // 声明一个Permission类型的变量
    struct Permission perm = {0};

    // 给权限标志赋值
    perm.read  = 1; // 可读
    perm.write = 0; // 不可写
    perm.exec  = 1; // 可执行

    // 打印各位域的值
    printf("读权限 = %u\n", perm.read);
    printf("写权限 = %u\n", perm.write);
    printf("执行权限 = %u\n", perm.exec);

    // 使用sizeof观察结构体大小
    printf("Permission结构体大小: %zu 字节\n", sizeof(perm));

    return 0;
}

编译 & 运行示例

gcc bitfield_example.c -o bitfield_example
./bitfield_example

输出示例(可能略有差异)

读权限 = 1
写权限 = 0
执行权限 = 1
Permission结构体大小: 4 字节

这里我们以 32 位无符号 int 为基础进行位域分配,因此最终结构体大小通常是4个字节(具体取决于编译器和平台)。

:two: 跨平台可移植性问题

知识要点

  • 不同编译器/平台对位域的对齐和填充方式可能不同,有些平台也可能从高位开始分配位域。
  • 如果在网络协议/硬件寄存器映射场景下使用,必须仔细阅读编译器文档或使用特定的 #pragma__attribute__ 来确保布局正确。
  • 在可移植性要求较高时,可能会更倾向于使用移位和掩码操作,以完全掌控比特布局。

(╯°□°)╯︵ ┻━┻ 如果一定要跨平台保持精确对齐,位域可能带来风险,需要进行更多测试或约束。

3.2 可变长度数组

:one: ​ 栈上动态数组的生命周期管理

知识要点

  • C99 标准中引入了可变长度数组(VLA)。其大小可在运行时由变量决定,而无需使用 malloc
  • 可变长度数组通常分配在栈上,函数返回后会自动释放。
  • 例如 int arr[n]; 其中 n 在运行时才确定大小。

代码示例
以下示例中,我们先让用户输入一个尺寸 n,再在栈上创建一个长度为 n 的整型数组,并进行简单操作。

#include <stdio.h>

int main(void)
{
    int n;

    printf("请输入数组大小: ");
    scanf("%d", &n); // 🚀 由用户决定数组大小

    // 声明一个可变长度数组(VLA)
    int arr[n]; // 在C99及以上编译器下合法

    // 填充数组
    for(int i = 0; i < n; i++)
    {
        arr[i] = i * 10;
    }

    // 输出数组
    printf("数组元素: ");
    for(int i = 0; i < n; i++)
    {
        printf("%d ", arr[i]);
    }
    printf("\n");

    return 0;
}

编译 & 运行示例

gcc vla_example.c -o vla_example -std=c99
./vla_example

输出示例(示例输入 5)

请输入数组大小: 5
数组元素: 0 10 20 30 40

:two: ​ C11 后的可选支持状态

知识要点

  • 在 C11 标准中,VLA 成为可选特性,并非所有编译器都必须支持。
  • 许多主流编译器(如 GCC、Clang)依然支持 VLA,但一些嵌入式编译器可能默认不支持或仅做部分支持。
  • 若在某些平台编译失败,可能需要加编译选项(如 -std=c99-std=gnu99)或改用动态分配。

小结: VLA 让代码更简洁,但在嵌入式栈空间有限的环境中要谨慎使用!否则( ̄▽ ̄)" 可能导致栈溢出。

3.3 柔性数组成员

:one: 动态结构体内存分配技巧

知识要点

  • 柔性数组成员是 C99 提供的一种特殊用法:在结构体中声明一个不定长的数组(数组大小为0或[]),必须作为结构体的最后一个成员。

  • 常见写法:

    struct S {
        int len;
        char buf[]; // 柔性数组成员
    };
    
  • 使用时需通过 malloc 分配整个结构体 + 数组的空间,并通过计算 len 来决定柔性数组的大小。

  • 可以减少一次指针操作或两次分配的麻烦,也常见于网络包、字符串处理等场景。

代码示例
以下示例演示如何使用柔性数组成员来存储一个变长字符串:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// 定义一个结构体,带柔性数组成员
struct FlexStr {
    int length;       // 记录字符串长度
    char data[];      // 柔性数组,用于存放字符数据
};

int main(void)
{
    const char *input = "Hello, Flexible Array!";
    int len = strlen(input);

    // 计算要分配的总大小 = 结构体本身大小 + 字符数组空间
    // 多分配1字节存放 '\0'
    struct FlexStr *p = malloc(sizeof(struct FlexStr) + (len + 1) * sizeof(char));
    if(!p) {
        perror("malloc failed");
        return -1;
    }

    // 初始化
    p->length = len;
    strcpy(p->data, input); // 拷贝字符串到柔性数组

    // 使用
    printf("字符串长度 = %d\n", p->length);
    printf("字符串内容 = %s\n", p->data);

    // 释放
    free(p);

    return 0;
}

编译 & 运行示例

gcc flexible_array_example.c -o flexible_array_example
./flexible_array_example

输出示例

字符串长度 = 23
字符串内容 = Hello, Flexible Array!

:two: 与指针数组成员的对比

知识要点

  • 如果用指针成员 char *data;,需要额外申请一段空间来存储字符数组。
  • 使用柔性数组则可将“结构体信息 + 数据”连续分配在同一块内存中,减少碎片、提高缓存局部性。
  • 缺点是必须一次性分配,也不能随意扩缩。

3.4 原子类型(Atomic Types)

:one: _Atomic 类型修饰符

知识要点

  • C11 引入 _Atomic 关键字,提供原子操作支持。

  • 适合多线程或并发编程环境,保证对该变量的读写是不可分割的(其他线程不会读到部分更新的数据)。

  • 常用例子:

    _Atomic int atomic_counter;
    
  • 也可通过 _Atomic(T) 的形式定义。

  • 比如:

    _Atomic(int) atomic_flag = 0;
    

:two: 原子操作的内存序模型

知识要点

  • 原子操作的内存序列可通过 stdatomic.h 中的函数来指定,如 memory_order_relaxedmemory_order_acquirememory_order_release 等。
  • 常用接口:
    • atomic_load, atomic_store
    • atomic_fetch_add, atomic_fetch_sub 等操作
  • 可精确控制读写在多线程环境下的可见性和同步顺序。

代码示例
下面演示在多线程场景下使用原子变量计数。这里仅给出核心代码逻辑,若需完整多线程示例,可配合 pthread 或其他线程库使用。

#include <stdio.h>
#include <stdatomic.h>
#include <pthread.h>

#define NUM_THREADS 4
#define LOOP_COUNT  1000000

// 原子整型计数器
_Atomic int atomic_counter = 0;

void* thread_func(void *arg)
{
    for(int i = 0; i < LOOP_COUNT; i++)
    {
        // 原子加1操作
        atomic_fetch_add(&atomic_counter, 1);
    }
    return NULL;
}

int main(void)
{
    pthread_t threads[NUM_THREADS];
    // 创建多个线程,对同一个原子计数器进行加1
    for(int i = 0; i < NUM_THREADS; i++)
    {
        pthread_create(&threads[i], NULL, thread_func, NULL);
    }

    // 等待所有线程结束
    for(int i = 0; i < NUM_THREADS; i++)
    {
        pthread_join(threads[i], NULL);
    }

    printf("atomic_counter 期望值: %d\n", NUM_THREADS * LOOP_COUNT);
    printf("atomic_counter 最终值: %d\n", atomic_counter);

    return 0;
}

编译 & 运行示例

gcc atomic_example.c -o atomic_example -lpthread
./atomic_example

输出示例(示意)

atomic_counter 期望值: 4000000
atomic_counter 最终值: 4000000

如果你尝试用普通 int 而不加锁或不使用原子操作,则多线程竞态条件可能会导致计数结果小于预期值!(┳_┳)