C语言结构体完全指南:从基础到底层内存布局

张开发
2026/4/21 0:32:46 15 分钟阅读

分享文章

C语言结构体完全指南:从基础到底层内存布局
引言在C语言中数组让我们能够存储一组相同类型的数据。但现实世界中的实体往往包含不同类型的信息——比如一个学生有姓名字符串、年龄整数、学号长整数、成绩浮点数。如何用一个变量来表示这些不同类型的数据呢结构体struct就是为了解决这个问题而生的。它允许我们将多个不同类型的变量组合成一个整体形成一个新的数据类型。今天我将从内存底层视角出发全面讲解C语言中结构体的定义、初始化、访问、内存对齐规则以及如何构建一个简单的学生管理系统。第一部分什么是结构体一、数组与结构体的对比特性数组结构体元素类型必须相同可以不同内存布局连续存储连续存储可能有对齐空隙访问方式通过下标arr[i]通过成员名stu.name类型本质相同类型的集合不同类型的集合赋值操作不能直接赋值可以直接赋值#include stdio.h #include string.h // 定义结构体类型相当于画图纸 struct student { char name[20]; // 姓名字符数组 int age; // 年龄整型 long long ID; // 学号长整型 double grade; // 成绩浮点型 }; // 注意分号不能少 int main() { // 使用结构体类型定义变量相当于按照图纸造房子 struct student stu1; struct student stu2; printf(struct student 大小: %zu 字节\n, sizeof(struct student)); return 0; }重要理解struct student是一个类型名就像int一样。struct student和int都是类型都可以用来定义变量。数组名可以退化为首元素地址但结构体变量名不能退化为地址。第二部分结构体的内存对齐核心难点一、什么是内存对齐为了提高CPU访问内存的效率编译器会在结构体成员之间插入“空隙”填充字节使每个成员都位于其自然边界上。这是C语言中一个容易被忽略但非常重要的概念。对齐规则每个成员的起始偏移量必须是其自身大小的整数倍结构体的总大小必须是最大成员大小的整数倍二、成员顺序影响结构体大小#include stdio.h // 示例1成员顺序不合理浪费空间 struct S1 { char c1; // 偏移量0占用1字节 // 偏移量1-3填充3字节为了对齐int int i; // 偏移量4-7占用4字节 char c2; // 偏移量8占用1字节 // 偏移量9-11填充3字节为了整体对齐 }; // 总大小12字节 // 示例2成员顺序合理节省空间 struct S2 { char c1; // 偏移量0占用1字节 char c2; // 偏移量1占用1字节 // 偏移量2-3填充2字节为了对齐int int i; // 偏移量4-7占用4字节 }; // 总大小8字节 // 示例3包含double的结构体 struct S3 { double d; // 偏移量0-7占用8字节 char c; // 偏移量8占用1字节 // 偏移量9-11填充3字节为了对齐int int i; // 偏移量12-15占用4字节 }; // 总大小16字节最大成员double是8字节16是8的倍数 int main() { printf(sizeof(struct S1): %zu\n, sizeof(struct S1)); // 12 printf(sizeof(struct S2): %zu\n, sizeof(struct S2)); // 8 printf(sizeof(struct S3): %zu\n, sizeof(struct S3)); // 16 // 结论将较大的成员放在前面可以节省内存 return 0; }三、复杂结构体的内存布局#include stdio.h // S5包含数组的结构体 struct s5 { char a[7]; // 偏移量0-6占用7字节对齐到1 // 偏移量7填充1字节为了对齐int数组 int b[2]; // 偏移量8-15占用8字节int是4字节数组连续 float c; // 偏移量16-19占用4字节 double d; // 偏移量24-31占用8字节对齐到8 char e[11]; // 偏移量32-42占用11字节 // 偏移量43-47填充5字节整体对齐到8的倍数48 }; // 总大小48字节 // S6包含结构体数组的嵌套结构体 struct s6 { char a; // 偏移量0占用1字节 // 偏移量1-7填充7字节为了对齐s5数组 struct s5 b[9]; // 偏移量8-4319 * 48 432字节占用432字节 struct s6* c[10]; // 偏移量440-51910 * 8 80字节占用80字节 double d; // 偏移量520-527占用8字节 short e[100]; // 偏移量528-727100 * 2 200字节占用200字节 }; // 总大小728字节 int main() { printf(sizeof(struct s5): %zu\n, sizeof(struct s5)); // 48 printf(sizeof(struct s6): %zu\n, sizeof(struct s6)); // 728 return 0; }四、手动控制对齐#pragma pack#include stdio.h // 默认对齐 struct Normal { char c; int i; char d; }; // 12字节 // 强制按1字节对齐减少空间降低访问效率 #pragma pack(1) struct Packed { char c; int i; char d; }; #pragma pack() // 强制按2字节对齐 #pragma pack(2) struct Packed2 { char c; int i; // int原本需要4字节对齐现在只按2字节对齐 char d; }; #pragma pack() int main() { printf(默认对齐: %zu\n, sizeof(struct Normal)); // 12 printf(1字节对齐: %zu\n, sizeof(struct Packed)); // 6 printf(2字节对齐: %zu\n, sizeof(struct Packed2));// 8 return 0; }第三部分结构体的定义与初始化一、三种定义方式#include stdio.h #include string.h // 方式1先定义类型再定义变量最常用 struct student { char name[20]; int age; long long ID; double grade; }; struct student stu1; // 定义变量 // 方式2定义类型的同时定义变量 struct teacher { char name[20]; int years; double salary; } t1, t2; // 同时定义了两个变量 // 方式3定义类型的同时定义变量并省略类型名不推荐 struct { char title[50]; int pages; } book1, book2; int main() { // 方式1的变量使用 strcpy(stu1.name, 张三); stu1.age 20; stu1.ID 2024001; stu1.grade 95.5; return 0; }二、初始化的多种方式#include stdio.h #include string.h struct student { char name[20]; int age; long long ID; double grade; }; int main() { // 方式1定义时直接初始化按顺序 struct student stu1 {张三, 21, 2331051646, 100}; // 方式2定义后逐个赋值 struct student stu2; strcpy(stu2.name, 李四); stu2.age 19; stu2.ID 2431272427; stu2.grade 100; // 方式3指定成员初始化C99未指定的成员初始化为0 struct student stu3 {.name 王五, .grade 100}; // 方式4全部初始化为0 struct student stu4 {0}; // 方式5通过输入赋值 struct student stu5; printf(请输入姓名: ); scanf(%s, stu5.name); // 数组名本身就是地址不需要 printf(请输入年龄: ); scanf(%d, stu5.age); printf(请输入学号: ); scanf(%lld, stu5.ID); printf(请输入成绩: ); scanf(%lf, stu5.grade); return 0; }第四部分结构体的访问与传参一、访问结构体成员#include stdio.h #include string.h struct student { char name[20]; int age; long long ID; double grade; }; int main() { struct student stu {张三, 20, 2024001, 95.5}; // 方法1通过变量名直接访问使用点运算符 . printf(姓名: %s\n, stu.name); printf(年龄: %d\n, stu.age); printf(学号: %lld\n, stu.ID); printf(成绩: %.2f\n, stu.grade); // 修改成员 stu.age 21; stu.grade 98.0; // 方法2通过指针间接访问使用箭头运算符 - struct student* p stu; printf(通过指针 - 姓名: %s\n, p-name); printf(通过指针 - 年龄: %d\n, p-age); // 等价写法不推荐太繁琐 printf(通过指针 - 学号: %lld\n, (*p).ID); return 0; }二、结构体作为函数参数传值 vs 传地址重要结构体作为函数参数时默认是值传递整个结构体被拷贝。如果结构体较大传值会消耗大量内存和时间。推荐使用指针传递。#include stdio.h #include string.h struct student { char name[20]; int age; long long ID; double grade; }; // 方式1传值会拷贝整个结构体占用40字节 void printByValue(struct student s) { printf(传值 - 姓名: %s, 年龄: %d\n, s.name, s.age); // 修改不会影响原变量 s.age 100; } // 方式2传地址只拷贝指针占8字节推荐 void printByPointer(struct student* p) { printf(传地址 - 姓名: %s, 年龄: %d\n, p-name, p-age); // 修改会影响原变量 p-age 100; } // 方式3传地址只读使用const保护 void printByConstPointer(const struct student* p) { printf(只读 - 姓名: %s, 年龄: %d\n, p-name, p-age); // p-age 100; // 错误const修饰不能修改 } int main() { struct student stu {李四, 20, 2024002, 88.5}; printByValue(stu); printf(传值后原变量年龄: %d\n, stu.age); // 仍然是20 printByPointer(stu); printf(传地址后原变量年龄: %d\n, stu.age); // 变成了100 printByConstPointer(stu); return 0; }传值与传地址对比方式语法拷贝大小能否修改原变量推荐程度传值void func(struct Student s)整个结构体不能不推荐大结构体传地址void func(struct Student* p)指针8字节能推荐传const地址void func(const struct Student* p)指针8字节不能只读时推荐第五部分结构体数组一、定义与初始化#include stdio.h struct student { char name[20]; int age; long long ID; double grade; }; int main() { // 定义并初始化结构体数组 struct student tl[10] { {张三, 21, 233533333, 100}, {吉五, 18, 20664630, 100}, {杨六, 18, 85633513, 100}, {郑七, 18, 8653233343, 100}, {齐八, 19, 9864561, 100}, {汪九, 19, 798465323, 100}, {茹十, 18, 986532, 100}, {尹11, 19, 1, 100}, }; int len sizeof(tl) / sizeof(tl[0]); printf(姓名 年龄 学号 成绩\n); for (int i 0; i len; i) { printf(%s %d %lld %.2f\n, tl[i].name, tl[i].age, tl[i].ID, tl[i].grade); } // 结构体数组的地址特性 printf(tl %p\n, tl); // 指向第一个元素 printf(tl 1 %p\n, tl 1); // 跳过一个结构体大小 printf(tl %p\n, tl); // 整个数组的地址 printf(tl 1 %p\n, tl 1); // 跳过整个数组 return 0; }二、结构体数组的遍历函数#include stdio.h #include string.h struct student { char name[20]; int age; long long ID; double grade; }; // 打印单个学生 void printStudent(const struct student* p) { printf(%-10s %-6d %-12lld %-8.2f\n, p-name, p-age, p-ID, p-grade); } // 打印所有学生 void printAllStudents(struct student arr[], int len) { printf(%-10s %-6s %-12s %-8s\n, 姓名, 年龄, 学号, 成绩); printf(----------------------------------------\n); for (int i 0; i len; i) { printStudent(arr[i]); } } // 计算平均年龄 double calcAverageAge(struct student arr[], int len) { double sum 0; for (int i 0; i len; i) { sum arr[i].age; } return sum / len; } // 统计不同长度姓名的数量注意UTF-8中文在不同平台字节数不同 void countNameLength(struct student arr[], int len) { int twoChars 0; // 2个汉字 int threeChars 0; // 3个汉字 int fourChars 0; // 4个汉字 for (int i 0; i len; i) { int byteLen strlen(arr[i].name); // VS中一个汉字2字节VSCode中一个汉字3字节 // 这里按实际字节数判断 if (byteLen 4) { twoChars; } else if (byteLen 6) { threeChars; } else if (byteLen 8) { fourChars; } } printf(两字姓名: %d人\n, twoChars); printf(三字姓名: %d人\n, threeChars); printf(四字姓名: %d人\n, fourChars); } int main() { struct student tl[10] { {张三, 21, 233533333, 100}, {吉五, 18, 20664630, 100}, {杨六, 18, 85633513, 100}, {郑七, 18, 8653233343, 100}, {齐八, 19, 9864561, 100}, {汪九, 19, 798465323, 100}, {茹十, 18, 986532, 100}, {尹11, 19, 1, 100}, }; int len sizeof(tl) / sizeof(tl[0]); printAllStudents(tl, len); printf(\n平均年龄: %.2f\n, calcAverageAge(tl, len)); printf(\n); countNameLength(tl, len); return 0; }第六部分typedef 的使用一、为什么需要 typedeftypedef用于给已有类型起别名可以简化代码书写。#include stdio.h #include string.h // 不使用 typedef每次都要写 struct student struct student { char name[20]; int age; }; // 使用 typedef可以省略 struct 关键字 typedef struct teacher { char name[20]; int years; } Teacher; // Teacher 是 struct teacher 的别名 // 更简洁的写法直接定义类型别名 typedef struct { char title[50]; int pages; } Book; int main() { // 不使用 typedef struct student stu1; strcpy(stu1.name, 张三); // 使用 typedef Teacher t1; t1.years 10; Book b1; b1.pages 300; // 也可以给基本类型起别名 typedef int Integer; typedef unsigned long long ULL; Integer num 100; ULL bigNum 123456789012345ULL; return 0; }第七部分综合示例——学生管理系统#include stdio.h #include string.h #define MAX_NAME 20 #define MAX_STUDENTS 100 // 学生结构体 typedef struct Student { char name[MAX_NAME]; int age; long long id; double score; } Student; // 系统结构体 typedef struct System { Student students[MAX_STUDENTS]; // 学生数组 int count; // 当前学生数量 int capacity; // 最大容量 } System; // 初始化系统 void initSystem(System* sys) { sys-count 0; sys-capacity MAX_STUDENTS; } // 添加学生 int addStudent(System* sys) { if (sys-count sys-capacity) { printf(系统已满无法添加\n); return -1; } Student* s sys-students[sys-count]; printf(请输入姓名: ); scanf(%s, s-name); printf(请输入年龄: ); scanf(%d, s-age); printf(请输入学号: ); scanf(%lld, s-id); printf(请输入成绩: ); scanf(%lf, s-score); sys-count; printf(添加成功当前学生人数: %d\n, sys-count); return 0; } // 打印单个学生 void printStudent(const Student* s) { printf(%-10s %-6d %-12lld %-8.2f\n, s-name, s-age, s-id, s-score); } // 打印所有学生 void printAllStudents(const System* sys) { if (sys-count 0) { printf(暂无学生数据\n); return; } printf(\n 学生列表 \n); printf(%-10s %-6s %-12s %-8s\n, 姓名, 年龄, 学号, 成绩); printf(----------------------------------------\n); for (int i 0; i sys-count; i) { printStudent(sys-students[i]); } printf(\n\n); } // 按姓名查找学生 int findStudentByName(const System* sys, const char* name) { for (int i 0; i sys-count; i) { if (strcmp(sys-students[i].name, name) 0) { return i; } } return -1; } // 删除学生 int deleteStudent(System* sys, const char* name) { int index findStudentByName(sys, name); if (index -1) { printf(未找到学生: %s\n, name); return -1; } // 将后面的元素向前移动 for (int i index; i sys-count - 1; i) { sys-students[i] sys-students[i 1]; } sys-count--; printf(删除成功\n); return 0; } // 计算平均成绩 double getAverageScore(const System* sys) { if (sys-count 0) return 0; double sum 0; for (int i 0; i sys-count; i) { sum sys-students[i].score; } return sum / sys-count; } // 菜单显示 void showMenu() { printf(\n 学生管理系统 \n); printf(1. 添加学生\n); printf(2. 查看所有学生\n); printf(3. 查找学生\n); printf(4. 删除学生\n); printf(5. 统计平均成绩\n); printf(0. 退出\n); printf(\n); printf(请选择: ); } int main() { System sys; initSystem(sys); int choice; do { showMenu(); scanf(%d, choice); getchar(); // 清除缓冲区 switch (choice) { case 1: addStudent(sys); break; case 2: printAllStudents(sys); break; case 3: { char name[MAX_NAME]; printf(请输入要查找的姓名: ); scanf(%s, name); int idx findStudentByName(sys, name); if (idx ! -1) { printf(找到学生:\n); printStudent(sys.students[idx]); } else { printf(未找到学生: %s\n, name); } break; } case 4: { char name[MAX_NAME]; printf(请输入要删除的姓名: ); scanf(%s, name); deleteStudent(sys, name); break; } case 5: { double avg getAverageScore(sys); printf(全班平均成绩: %.2f\n, avg); break; } case 0: printf(退出系统...\n); break; default: printf(无效选择请重新输入\n); } } while (choice ! 0); return 0; }第八部分结构体常见陷阱一、结构体不能直接比较#include stdio.h #include string.h struct Point { int x; int y; }; int main() { struct Point p1 {1, 2}; struct Point p2 {1, 2}; // 错误不能直接比较结构体 // if (p1 p2) { } // 编译错误 // 正确逐个成员比较 if (p1.x p2.x p1.y p2.y) { printf(两个点相同\n); } // 正确使用 memcmp注意内存对齐的填充字节 if (memcmp(p1, p2, sizeof(struct Point)) 0) { printf(内存内容相同\n); } return 0; }二、结构体赋值是深拷贝还是浅拷贝#include stdio.h #include string.h struct Person { char name[20]; int* p; // 指针成员 }; int main() { int x 10; struct Person p1 {张三, x}; struct Person p2; // 结构体赋值是浅拷贝逐字节复制 p2 p1; printf(p1.name: %s, p2.name: %s\n, p1.name, p2.name); printf(p1.p: %p, p2.p: %p\n, p1.p, p2.p); // 两个指针指向同一地址 // 修改 p2 的指针指向的内容p1 也会受影响 *(p2.p) 20; printf(p1 指向的值: %d\n, *(p1.p)); // 20 // 字符数组是深拷贝因为数组内容被复制了 strcpy(p2.name, 李四); printf(p1.name: %s, p2.name: %s\n, p1.name, p2.name); // 不同 return 0; }总结一、结构体核心要点概念说明定义不同类型数据的集合内存连续存储但有内存对齐对齐规则成员偏移量是自身大小的倍数整体大小是最大成员的倍数访问点运算符.指针用箭头-传参推荐传地址节省空间、可修改typedef给结构体起别名简化代码二、内存对齐规则总结规则说明成员偏移量必须是该成员大小的整数倍整体大小必须是最大成员大小的整数倍优化技巧将较大的成员放在前面手动控制#pragma pack(n)强制按n字节对齐三、常见错误提醒错误说明忘记分号struct Student { ... }后面的分号不能少直接比较结构体不支持需要逐个成员比较传值导致效率低大结构体用指针传递忽略内存对齐影响结构体大小和跨平台兼容性结构体自引用不能包含自身类型的非指针成员需要指针结构体是C语言中组织不同类型数据的强大工具是面向对象编程思想在C语言中的体现。掌握结构体的定义、初始化、访问、内存对齐和传参是学习链表、文件操作等高级内容的基础。学习建议理解结构体内存对齐优化结构体布局函数传参优先使用指针使用 typedef 简化代码多练习构建现实世界的数据模型

更多文章