
06_C语言-类型篇
本文最后更新于 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
成员放在前面,short
、char
这样的小类型放在后面,通常可减少整体的填充字节数。
: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: 内存模型
-
对于一个联合体,如上所示,
i
、f
、str
都存储在同一块内存起始地址。 -
联合体大小 通常等于其最大成员所需的内存大小,并会根据对齐要求进行整体对齐。
-
例如:
union Data myData; myData.i = 10; printf("%d\n", myData.i); myData.f = 3.14f; // 此时再打印 myData.i,将得到无意义结果,因为内存被 f 覆盖。
:three: 注意事项
- 联合体常用于需要“节省内存”的场景,或者表明同一内存中存储的数据会以不同形式被解释(如网络协议解析、硬件寄存器访问等)。
- 需要清晰理解不同成员之间共享同一内存区域,“最后一次赋值”会覆盖之前的内容。
- 在使用联合体时,必须根据当前成员的类型进行正确访问,否则会得到未定义或意外的结果。
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: 类型特征
-
在 C 语言中,枚举类型实际上被编译器视作“整型”的一种。
-
省去频繁的
#define
或常量宏定义,方便调试和阅读。 -
如果不手动指定值,默认从 0 开始并依次加 1。
-
可以与
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
的用法。注意,如果 src
和 dst
实际上会重叠,则使用 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.c
和 data.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 = #
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_relaxed
、memory_order_acquire
、memory_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
而不加锁或不使用原子操作,则多线程竞态条件可能会导致计数结果小于预期值!(┳_┳)
- 感谢你赐予我前进的力量