C语言结构体详解与实战技巧

「开学季干货」:聚焦知识梳理与经验分享 10w+人浏览 203人参与

一、结构体介绍-自定义类型

解释
结构体是C语言中一种用户自定义的数据类型,它允许将多个不同类型的数据项组合成一个单一的单位。结构体用于表示具有多个属性的实体。

示例

// 定义坐标点结构体
struct Point {
    int x;
    int y;
};

// 定义学生信息结构体
struct Student {
    char name[20];
    int age;
    float score;
};

注意事项

  • 结构体定义以分号结束,很多初学者会忘记这个分号

  • 结构体定义不分配内存,只有声明变量时才分配内存

  • 结构体名通常首字母大写,提高代码可读性

  • 结构体定义可以放在函数内(局部)或函数外(全局)

二、结构体的声明

1. 结构体声明

解释
结构体声明有三种方式,每种方式有不同的使用场景。

示例

c

// 方式1:标准声明
struct Person {
    char name[30];
    int age;
};

// 方式2:声明同时定义变量
struct Employee {
    char name[30];
    int id;
} emp1, emp2;

// 方式3:使用typedef简化
typedef struct {
    char name[30];
    double salary;
} Worker;
注意事项:

方式1需要每次都写struct关键字,方式3更简洁

方式2适用于需要立即使用的情况

结构体可以前向声明:struct Person;(不完全类型)

2. 结构体变量的定义和初始化

解释
结构体变量可以在声明时或之后初始化,支持多种初始化方式。

示例

// 声明时初始化
struct Student {
    char name[20];
    int age;
} stu1 = {"张三", 20};

// 指定成员初始化(C99)
struct Student stu2 = {.name = "李四", .age = 21};

// 逐个成员初始化
struct Student stu3;
strcpy(stu3.name, "王五");
stu3.age = 22;

注意事项

  • 初始化时成员顺序必须与定义时一致

  • 字符串赋值必须使用strcpy,不能直接赋值

  • 部分初始化时,未指定的成员自动初始化为0

  • 结构体变量可以在定义时初始化,但不能整体赋值

3. 结构体变量访问成员

解释
使用点运算符(.)访问结构体变量的成员。

示例

struct Student stu = {"张三", 20};

// 访问成员
printf("姓名: %s\n", stu.name);
printf("年龄: %d\n", stu.age);

// 修改成员
stu.age = 21;
strcpy(stu.name, "张小三");

注意事项

  • 点运算符优先级很高,仅次于括号

  • 访问不存在的成员会导致编译错误

  • 数组成员访问要注意边界检查

  • const结构体的成员不能被修改

4. 结构体的自引用

解释
结构体可以包含指向自身类型的指针,用于实现链表、树等数据结构。

示例

// 正确的自引用
struct Node {
    int data;
    struct Node* next;  // 指向同类型的指针
};

// 错误的自引用
struct WrongNode {
    int data;
    struct WrongNode node;  // 错误!不能包含自身实例
};

注意事项

  • 结构体不能直接包含自身实例,会导致无限递归

  • 只能包含指向自身类型的指针

  • 使用typedef时要注意声明顺序

  • 链表操作时要正确处理指针关系

三、结构体数组

解释
结构体数组用于存储多个相同类型的结构体实例。

示例

struct Student {
    char name[20];
    int score;
};

// 定义并初始化结构体数组
struct Student class[3] = {
    {"张三", 90},
    {"李四", 85},
    {"王五", 78}
};

// 访问数组元素
for(int i = 0; i < 3; i++) {
    printf("学生%d: %s, 分数: %d\n", 
           i+1, class[i].name, class[i].score);
}

注意事项

  • 数组下标从0开始,不要越界访问

  • 结构体数组作为函数参数时会退化为指针

  • 大结构体数组可能占用大量内存,考虑动态分配

  • 遍历时注意性能,特别是嵌套结构体

四、结构体与指针及函数传参

1. 指向结构体变量的指针

解释
指针可以指向结构体变量,通过指针访问结构体成员。

示例

struct Student stu = {"张三", 20};
struct Student *pStu = &stu;  // 指向结构体的指针

// 通过指针访问成员
printf("姓名: %s\n", (*pStu).name);  // 方式1
printf("年龄: %d\n", pStu->age);     // 方式2(推荐)

注意事项

  • 使用->运算符更简洁,(*ptr).member方式容易出错

  • 指针必须指向有效的结构体变量

  • 空指针解引用会导致段错误

  • 指针算术运算以结构体大小为步长

2. 指针访问成员变量

解释
箭头运算符(->)是访问指针指向结构体成员的简便方式。

示例

struct Point {
    int x;
    int y;
};

struct Point pt = {10, 20};
struct Point *ptr = &pt;

// 修改成员值
ptr->x = 100;    // 等价于 (*ptr).x = 100
ptr->y = 200;    // 等价于 (*ptr).y = 200

// 读取成员值
int x_val = ptr->x;
int y_val = ptr->y;

注意事项

  • ->运算符优先级很高

  • 确保指针指向有效的内存地址

  • const指针不能修改成员值

  • 多级指针访问需要多次解引用

3. STM32寄存器映射

解释
在嵌入式开发中,结构体常用于寄存器映射,提供硬件访问的抽象层。

示例

// GPIO寄存器结构体
typedef struct {
    volatile uint32_t MODER;    // 模式寄存器
    volatile uint32_t OTYPER;   // 输出类型寄存器
    volatile uint32_t OSPEEDR;  // 输出速度寄存器
    volatile uint32_t PUPDR;    // 上拉下拉寄存器
    volatile uint32_t IDR;      // 输入数据寄存器
    volatile uint32_t ODR;      // 输出数据寄存器
} GPIO_TypeDef;

// 寄存器映射
#define GPIOA_BASE    0x40020000U
#define GPIOA         ((GPIO_TypeDef *)GPIOA_BASE)

// 使用
GPIOA->MODER = 0xABADF00D;  // 设置GPIOA模式
uint32_t input = GPIOA->IDR; // 读取输入数据

注意事项

  • 必须使用volatile关键字防止编译器优化

  • 寄存器地址必须精确匹配硬件手册

  • 注意结构体对齐和填充字节

  • 位域操作要小心,不同编译器实现可能不同

4. 结构体传参

解释
结构体可以作为函数参数传递,支持值传递和指针传递。

示例

// 值传递(产生拷贝)
void printStudent(struct Student stu) {
    printf("姓名: %s, 年龄: %d\n", stu.name, stu.age);
}

// 指针传递(推荐,高效)
void modifyStudent(struct Student *pStu) {
    pStu->age += 1;
    strcpy(pStu->name, "修改后的名字");
}

// const指针传递(防止修改)
void displayStudent(const struct Student *pStu) {
    printf("姓名: %s\n", pStu->name);  // 可以读取
    // pStu->age = 100;  // 错误!不能修改
}

注意事项

  • 大结构体应该使用指针传递,避免拷贝开销

  • 如果不需要修改,使用const指针保护数据

  • 值传递会创建副本,修改不影响原结构体

  • 指针传递要注意空指针检查

五、结构体在内存的存储

1. 结构体内存对齐

解释
为了提高访问效率,编译器会对结构体成员进行内存对齐。

示例

struct Example1 {
    char a;      // 1字节
    int b;       // 4字节
    char c;      // 1字节
}; // 总大小可能是12字节(不是6字节)

struct Example2 {
    int b;       // 4字节
    char a;      // 1字节
    char c;      // 1字节
}; // 总大小可能是8字节(优化后)

注意事项

  • 对齐规则因编译器和平台而异

  • 成员顺序影响结构体总大小

  • 使用#pragma pack可以修改对齐方式

  • 网络传输时要考虑字节序和对齐问题

2. 内存对齐的原因

解释
内存对齐主要出于性能考虑,现代CPU通常以字长为单位访问内存。

原因

  • 性能优化:对齐的内存访问更快

  • 硬件要求:某些架构要求特定对齐

  • 原子操作:对齐访问可以保证原子性

注意事项

  • 不同平台的对齐要求可能不同

  • 嵌入式系统可能有关键的对齐要求

  • 跨平台数据传输要处理对齐问题

  • 调试时要注意内存布局

3. 修改默认对齐数

解释
可以使用预处理指令修改编译器的默认对齐设置。

示例

#pragma pack(1)  // 设置为1字节对齐
struct TightPacked {
    char a;
    int b;
    char c;
}; // 大小=1+4+1=6字节

#pragma pack()   // 恢复默认对齐

// 另一种方式(GCC)
struct __attribute__((packed)) TightStruct {
    char a;
    int b;
    char c;
};

注意事项

  • 修改对齐可能影响性能

  • 打包的结构体可能无法在某些平台上运行

  • 硬件寄存器映射通常需要严格对齐

  • 跨平台代码要小心使用#pragma pack

4. 实现offsetof宏

解释
offsetof宏用于获取结构体成员相对于结构体起始地址的偏移量。

示例

// 标准库中的定义
#define offsetof(type, member) ((size_t)&(((type *)0)->member))

// 使用示例
struct Test {
    char a;
    int b;
    double c;
};

printf("a偏移: %zu\n", offsetof(struct Test, a));  // 0
printf("b偏移: %zu\n", offsetof(struct Test, b));  // 4
printf("c偏移: %zu\n", offsetof(struct Test, c));  // 8

注意事项

  • offsetof是编译时计算,不会产生运行时开销

  • 不能用于位域成员

  • 结果类型是size_t,适合指针运算

  • 在嵌入式开发中常用于寄存器访问

六、枚举

1. 定义枚举类型

解释
枚举是一种用户定义的类型,用于定义一组命名的整数常量。

示例

// 定义枚举类型
enum Color {
    RED,    // 0
    GREEN,  // 1
    BLUE    // 2
};

// 使用typedef简化
typedef enum {
    MONDAY,    // 0
    TUESDAY,   // 1
    WEDNESDAY, // 2
    THURSDAY,  // 3
    FRIDAY,    // 4
    SATURDAY,  // 5
    SUNDAY     // 6
} Weekday;

注意事项

  • 枚举常量默认从0开始,依次递增

  • 可以指定特定值:enum {A=1, B=2, C=4};

  • 枚举类型大小通常是int的大小

  • 不同枚举类型的常量不能混用

2. 枚举的使用

解释
枚举变量只能取枚举中定义的值,提高了代码的可读性和安全性。

示例

enum TrafficLight {RED, YELLOW, GREEN};

// 声明枚举变量
enum TrafficLight light = RED;

// 使用switch语句
switch(light) {
    case RED:
        printf("停止\n");
        break;
    case YELLOW:
        printf("准备\n");
        break;
    case GREEN:
        printf("通行\n");
        break;
}

// 枚举与整数的转换
int value = GREEN;          // 枚举转整数
enum TrafficLight light2 = 1;  // 整数转枚举(可能不安全)

注意事项

  • 枚举提供了类型检查,比直接使用整数更安全

  • 不要假设枚举值的具体数值

  • 枚举可以提升代码可读性和可维护性

  • 整数转枚举可能产生无效值

3. 用枚举限定STM32模式的取值

解释
在嵌入式开发中,枚举常用于限定硬件配置的合法取值。

示例

// GPIO模式枚举
typedef enum {
    GPIO_MODE_INPUT,      // 输入模式
    GPIO_MODE_OUTPUT,     // 输出模式
    GPIO_MODE_AF,         // 复用功能
    GPIO_MODE_ANALOG      // 模拟模式
} GPIO_Mode_TypeDef;

// 使用枚举配置GPIO
void GPIO_Init(GPIO_TypeDef *GPIOx, GPIO_Mode_TypeDef mode) {
    // 只能传入枚举中定义的值
    uint32_t temp = GPIOx->MODER;
    temp &= ~(0x3U << (pin * 2));
    temp |= (mode << (pin * 2));
    GPIOx->MODER = temp;
}

// 正确使用
GPIO_Init(GPIOA, GPIO_MODE_OUTPUT);

// 错误使用(编译可能通过,但逻辑错误)
GPIO_Init(GPIOA, 5);  // 5不是有效的模式值

注意事项

  • 枚举提供编译时检查,减少配置错误

  • 枚举值应该与硬件寄存器值匹配

  • 使用枚举而不是魔数(magic number)

  • 文档化枚举值的含义和用法

4. 枚举的优点

解释
枚举相比直接使用整数常量有多重优势。

优点

  1. 类型安全:编译器检查赋值合法性

  2. 代码可读性:有意义的名称代替魔数

  3. 可维护性:修改只需改一处定义

  4. 调试友好:调试器显示名称而不是数字

  5. 自动编号:无需手动维护常量值

注意事项

  • 枚举不是类型安全的(C语言的限制)

  • 不同枚举类型的值可能意外相等

  • 枚举值的作用域是全局的,注意命名冲突

  • C++中的enum class提供真正的类型安全

七、联合体-共用体

1. 联合体的定义及初始化

解释
联合体是所有成员共享同一块内存空间的数据结构,同一时间只能使用一个成员。

示例

// 定义联合体
union Data {
    int i;
    float f;
    char str[20];
};

// 初始化
union Data data;
data.i = 10;        // 现在data.f和data.str无意义
printf("%d\n", data.i);

data.f = 3.14;      // 现在data.i被覆盖
printf("%f\n", data.f);

// 初始化指定成员
union Data data2 = {.f = 2.718};  // C99标准

注意事项

  • 联合体大小等于最大成员的大小

  • 同一时间只能有一个成员有效

  • 读取未初始化的成员是未定义行为

  • 常用于类型转换和节省内存

2. 用联合体判断计算机存储方式

解释
利用联合体可以检测处理器的字节序(大端序或小端序)。

示例

union EndianTest {
    int value;
    char bytes[sizeof(int)];
};

union EndianTest test;
test.value = 0x12345678;

if(test.bytes[0] == 0x78) {
    printf("小端序(Little Endian)\n");
} else if(test.bytes[0] == 0x12) {
    printf("大端序(Big Endian)\n");
} else {
    printf("未知字节序\n");
}

注意事项

  • 字节序影响网络通信和文件存储

  • 嵌入式系统通常是小端序

  • 网络协议通常使用大端序(网络字节序)

  • 跨平台数据传输需要处理字节序转换

3. 联合体大小的计算

解释
联合体的大小至少等于最大成员的大小,还要考虑内存对齐。

示例

union Example1 {
    char a;       // 1字节
    int b;        // 4字节
    double c;     // 8字节
}; // 大小=8字节(对齐到double)

union Example2 {
    char arr[10]; // 10字节
    int b;        // 4字节
}; // 大小=12字节(对齐到4字节)

printf("Example1大小: %zu\n", sizeof(union Example1));
printf("Example2大小: %zu\n", sizeof(union Example2));

注意事项

  • 联合体大小必须是最大成员对齐要求的倍数

  • 包含数组时,大小可能比数组本身大

  • 使用#pragma pack会影响联合体大小

  • 嵌入式系统中要注意联合体的内存布局

八、结尾

总结
结构体、枚举和联合体是C语言中强大的工具,它们提供了数据抽象和组织的能力。正确使用这些特性可以大大提高代码的可读性、可维护性和效率。

最终建议

  1. 选择合适的工具:根据需求选择结构体、枚举或联合体

  2. 注意内存布局:特别是嵌入式开发中的对齐要求

  3. 使用typedef简化:提高代码可读性

  4. 错误检查:始终验证指针和输入的有效性

  5. 文档化:为复杂结构体添加注释说明

最后的思考

C语言给了你足够的力量来 shoot yourself in the foot(自找麻烦),
但也给了你所有工具来 build amazing things(创造奇迹)。
结构体、枚举、联合体就是这些工具中的重要组成部分。
使用它们 wisely(明智地)!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值