> 结构体:从声明、初始化、指针数组到零长数组、地址对齐、位段、可移植性、占位符,附堆结构体数组抽卡 Demo + 对齐示意图 。
一、结构体基础:声明·定义·初始化
基本概念
C语言提供了众多的基本类型,但现实生活中的对象一般都不是单纯的整型、浮点型或字符串,而是这些基本类型的综合体。比如一个学生,典型地应该拥有学号(整型)、姓名(字符串)、分数(浮点型)、性别(枚举)等不同侧面的属性,这些所有的属性都不应该被拆分开来,而是应该组成一个整体,代表一个完整的学生。
在C语言中,可以使用结构体来将多种不同的数据类型组装起来,形成某种现实意义的自定义的变量类型。结构体本质上是一种自定义类型。
基础操作
① 声明(不产生内存)
向编译器系统明确我们自己定义的数据类型的模版,声明语句中不会产生任何的内存分配和释放。
(只是告诉编译器,自定义类型的样子,并不需要实际申请空间)
语法:
- 声明语句一般写于函数体外(与全局变量位置相当)
- struct 是结构体声明的关键字
- 结构体标签 用于区分不同的结构体
{ }
内部 结构体成员 , 可以是任何类型的数据,甚至也可以是一个结构体(嵌套)- 结构体的声明语句结尾必须加 ; 分号
struct 结构体标签
{成员1; // 每一个成员之间使用分号隔开成员2;...
}; // 结构体的声明语句结尾必须加 ; 分号实例:
// 声明结构体类型
//(并不是定义结构体变量,不产生内存空间)
struct Cat
{char * Color ;char * Name ;float Weight;float Long ;int Age ;//.......///.........
};
定义
当我们声明了一个结构体变量类型后,便可以把他在内存内存中定义出来,可以是堆,也可以是栈,也可是静态数据。
// 全局的结构体变量
struct Cat Tom1 ;int main(int argc, char const *argv[])
{// 局部的结构体变量struct Cat Tom2;// 在堆中申请结构体变量的内存空间struct Cat * Ptr = malloc( sizeof(struct Cat) );return 0;
}
初始化
结构体的初始化有两种方案
- 顺序初始化类似数组的元素初始化
// 顺序初始化(必须从头到尾依次初始化,中间不能留空)
struct Cat Tom3 = { "Red" , "Tom" , 19.34 , 60.45 , 380
};
- 指定成员初始化【推荐】
// 指定成员初始化
// (没有顺序要求,可以任意初始化已知属性,未知的可以忽略)
struct Cat Tom4 ={.Name = "Tom",.Age = 498 ,.Color = "Blue",
};
使用(访问)
访问结构体的成员也有两种方式:
- 对于普通结构体变量的访问可以使用.作为成员引用符
Tom4.Weight = 78.56;
Tom4.Long = 1.76 ;
- 对于结构体指针变量的访问可以使用 -> 作为成员引用符
void showCat( const struct Cat * cat )
{// -> 成员引用符号(指针)printf("Name:%s\n" , cat->Name) ;printf("Color:%s\n" , cat->Color) ;printf("Weight:%f\n" , cat->Weight) ;printf("Long:%f\n" , cat->Long) ;printf("Age:%d\n" , cat->Age) ;
}
结构体数组
概念: 它是一个数组,每一个元素都是结构体类
语法:
// 声明结构体类型
struct Stud
{int Num ;char * Name ;int Age ;
};// 定义但未初始化
struct Stud arr [3] ;// 定义并初始化一部分数据
struct Stud arr1 [3] = {{ 123 , "Even" , 25 }, // 顺序初始化{ .Age=18 , .Name="erGou" , .Num=321 }, // 指定成员初始化
};

对于以上问题的解决:
可以把结构体中的指针替换成数组,因此当我们申请结构体变量时,期内部的数组内存会一并分配,不需要使用堆内存,使用堆内存时需要添加额外管理代码。
// 声明结构体类型
struct Stud
{int Num ;char Name [32];int Age ;
};
结构体指针
概念: 它是一个指针,指向的数据类型为结构体。
语法:
struct stud * ptr ;// ptr 指向了结构体数组的第一个元素的入口地址
struct Stud * ptr = arr1 ;
printf("Name:%s\n" , ptr->Name) ;
printf("Age:%d\n" , ptr->Age) ;
printf("Num:%d\n" , ptr->Num) ;
结构体指针数组
概念: 它是一个数组,该数组中存放了多个结构体类型的指针。
语法:
struct Stud * arr[5] ;
结构体数组指针
概念: 它是一个指针,该指针指向的是一个数组,而数组中每一个元素都是结构体。
语法:
struct Stud (*ptr) [5] ;
结构体指针数组指针
概念: 它是一个指针,指向的是一个数组,数组中每一个元素都是指向结构体类型的指针。
语法:
struct Stud * (*ptr) [5] ;
二、结构体声明语句的变形
变形1: 声明时顺便定义变量
struct Node
{int Num ;char Name [32];
} Even , *Ptr ;
// 在生声明结构体类型时,顺便定义了两个变量
// Even普通结构体变量
// *Ptr 结构体的指针变量
变形2: 省略标签
这种写法并不多见,可以用于进制定义新的结构体类型,一般用于结构体内部嵌套的小结构体。
// 由于省略的结构体的标签
// 因此该结构体无法单独定义任何变量
// 只允许在声明的同时定义变量
struct
{int Num ;char Name [32];
} Even , *Ptr ;
// 在生声明结构体类型时,顺便定义了两个变量
// Even普通结构体变量
// *Ptr 结构体的指针变量// 在结构体内部嵌套一个小结构体时
// 如果不希望该小结构体被用户单独定义变量
// 可以把他的标签省略
// 在声明时直接定义变量
struct Stud
{int Num ;char Name [32];// 嵌套在大结构体内部的一个属性变量// 该书属性变量无法被单独定义struct {float Socre ;int Type ;char Msg[16];} Info ;};
变形3: 给类型取别名【常见用法】
// typedef用于给某一个类型取别名
typedef struct Node
{int Num ;char Name [32];
} Node_t , *P_Node_t ;
// Node_t 是 struct Node 的别名
// struct Node Even ; 等价于 Node_t Even ;
// P_Node_t 是 struct Node * 的别名
// struct Node * p ; 等价于 P_Node_t p ;// 以下语句中虽然没有了结构体的标签
// 但是有取别名,因此依然可以使用别名进行定义变量
typedef struct
{int Num ;char Name [32];
} Test_t , *P_Test_t ;
三、零长数组
概念: 长度为0的数组,可以把他放置于结构体的最后一个元素,并在给结构体申请堆内存空间时多分配一些,用于存储额外信息。
示例:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>struct Stud
{int Num;char Name [32];// 使用小结构体来前调某一部分数据是一个整体struct {int Len ; // 用于记录数组MSG的实际长度char Msg [0] ;// 在结构体的末尾定义一个零长度的数组// 因此零长数组可以用于兼顾特殊性以及普适性}Info;
};int main(int argc, char const *argv[])
{// 申请内存空间时,可以给结构体多申请一些空间// 用于存储特殊的描述信息struct Stud * ptr = malloc( sizeof(struct Stud) + 16 );ptr->Info.Len = 16 ; // 马上使用Len来标记多得到的内存大小,避免后期访问造成越界printf("零长数组的大小:%ld\n" , sizeof(ptr->Info.Msg) ) ;strncpy( ptr->Info.Msg , "零长数组" , ptr->Info.Len);ptr->Num = 444 ;strncpy( ptr->Name , "Even" , sizeof(ptr->Name) );printf("%s %d %s\n" , ptr->Name ,ptr->Num , ptr->Info.Msg) ;int arr [10];arr[0] = 123 ;*(arr+0) = 345 ;ptr->Num = 123 ;(*ptr).Num = 456 ;return 0;
}
四、结构体的尺寸
①CPU字长
字长的概念指的是处理器在一条指令中的数据处理能力,当然这个能力还需要搭配操作系统的设定,比如常见的32位系统、64位系统,指的是在此系统环境下,处理器一次存储处理的数据可以达32位或64位。

②地址对齐
CPU字长确定之后,相当于明确了系统每次存取内存数据时的边界,以32位系统为例,32位意味着CPU每次存取都以4字节为边界,因此每4字节可以认为是CPU存取内存数据的一个单元。
如果存取的数据刚好落在所需单元数之内,那么我们就说这个数据的地址是对齐的,如果存取的数据跨越了边界,使用了超过所需单元的字节,那么我们就说这个数据的地址是未对齐的。
(地址对齐的操作注意用以就是让CPU能一次性读取绝不分成两次,能两次读取绝不分成三次)
地址未对齐的情形


从图中可以明显看出,数据本身占据了8个字节,在地址未对齐的情况下,CPU需要分3次才能完整地存取完这个数据,但是在地址对齐的情况下,CPU可以分2次就能完整地存取这个数据。
总结:
如果一个数据满足以最小单元数存放在内存中,则称它地址是对齐的,否则是未对齐的。地址对齐的含义用大白话说就是1个单元能塞得下的就不用2个;2个单元能塞得下的就不用3个。
如果发生数据地址未对齐的情况,有些系统会直接罢工,有些系统则降低性能。
③普通变量的m值
以32位系统为例,由于CPU存取数据总是以4字节为单元,因此对于一个尺寸固定的数据而言,当它的地址满足某个数的整数倍时,就可以保证地址对齐。这个数就被称为变量的m值。
根据具体系统的字长,和数据本身的尺寸,m值是可以很简单计算出来的。
- 举例:
char c; // 由于c占1个字节,因此c不管放哪里地址都是对齐的,因此m=1
short s; // 由于s占2个字节,因此s地址只要是偶数就是对齐的,因此m=2
int i; // 由于i占4个字节,因此只要i地址满足4的倍数就是对齐的,因此m=4
double f; // 由于f占8个字节,因此只要f地址满足4的倍数(64位系统则为8)就是对齐的,因此m=4/8printf("%p\n", &c); // &c = 1*N,即:c的地址一定满足1的整数倍
printf("%p\n", &s); // &s = 2*N,即:s的地址一定满足2的整数倍
printf("%p\n", &i); // &i = 4*N,即:i的地址一定满足4的整数倍
printf("%p\n", &f); // &f = 4*N,即:f的地址一定满足4的整数倍
注意,变量的m值跟变量本身的尺寸有关,但它们是两个不同的概念。
- 手工干预变量的m值:
char c __attribute__((aligned(32))); // 将变量 c 的m值设置为32
语法:
attribute
机制是GNU特定语法,属于C语言标准语法的扩展。attribute
前后都是双下划线,aligned
两边是双圆括号。attribute
语句,出现在变量定义语句中的分号前面,变量标识符后面。attribute
机制支持多种属性设置,其中aligned
用来设置变量的 m 值属性。- 一个变量的 m 值只能提升,不能降低,且只能为正的2的n次幂。
分类 | 属性 | 作用描述 | 应用场景 |
---|---|---|---|
性能优化 | inline |
建议编译器将函数内联展开,减少函数调用开销 | 小型频繁调用的函数 |
always_inline |
强制编译器内联函数,忽略优化设置 | 关键性能路径的短函数 | |
aligned |
控制变量或结构体的内存对齐方式 | SIMD指令、缓存优化、硬件寄存器映射 | |
hot |
标记热点函数,优化代码布局 | 频繁执行的函数 | |
cold |
标记冷门函数,减少缓存污染 | 错误处理、罕见分支 | |
代码检查 | format |
检查格式化字符串参数的类型安全性 | printf/scanf风格的可变参数函数 |
noreturn |
标记永不返回的函数 | 退出程序、无限循环、异常终止函数 | |
nonnull |
检查指针参数不能为NULL | 必须非空的函数参数 | |
warn_unused_result |
强制检查函数返回值 | 必须处理返回值的函数 | |
deprecated |
标记已弃用的函数或变量 | 库版本管理、API迁移 | |
内存布局控制 | packed |
取消结构体填充,紧凑内存布局 | 网络协议、硬件寄存器、二进制格式 |
aligned |
指定变量或结构体的对齐要求 | 缓存行对齐、DMA缓冲区、原子操作 | |
section |
将代码或数据放入特定段 | 引导代码、加密段、特殊内存区域 | |
cleanup |
变量作用域结束时自动调用清理函数 | 自动资源管理、RAII模式 | |
语言功能扩展 | constructor |
在main函数前自动执行的初始化函数 | 库初始化、全局构造器 |
destructor |
在main函数后自动执行的清理函数 | 资源释放、全局析构器 | |
constructor(priority) |
指定初始化函数的执行优先级 | 依赖关系明确的初始化 | |
destructor(priority) |
指定清理函数的执行优先级 | 依赖关系明确的清理 | |
cleanup |
变量离开作用域时自动调用清理函数 | 自动内存管理、文件关闭 | |
兼容性与链接 | weak |
声明弱符号,允许被重定义 | 库函数重载、默认实现 |
alias |
为函数或变量创建别名 | API兼容性、函数重命名 | |
visibility |
控制符号的可见性(default/hidden) | 动态库符号导出控制 | |
used |
防止未使用的符号被优化删除 | 强制保留特定函数或变量 | |
deprecated |
标记已弃用的接口,编译时警告 | 向后兼容、API演进 |
④结构体的M值
概念:
- 结构体的M值,取决于其成员的m值的最大值。即:M = max{m1, m2, m3, …};
- 结构体的和地址和尺寸,都必须等于M值的整数倍。
示例:
struct node
{short a; // 尺寸=1,m值=2double b; // 尺寸=8,m值=4char c; // 尺寸=1,m值=1
};struct node n; // M值 = max{2, 4, 1} = 4;
以上结构体成员存储分析:
1.结构体的M值等于4,这意味着结构体的地址、尺寸都必须满足4的倍数。
2.成员a的m值等于2,但a作为结构体的首元素,必须满足M值约束,即a的地址必须是4的倍数
3.成员b的m值等于4,因此在a和b之间,需要填充2个字节的无效数据(一般填充0)
4.成员c的m值等于1,因此c紧挨在b的后面,占一个字节即可。
5.结构体的M值为4,因此成员c后面还需填充3个无效数据,才能将结构体尺寸凑足4的倍数。
⑤可移植性
可移植指的是相同的一段数据或者代码,在不同的平台中都可以成功运行。
- 对于数据来说,有两方面可能会导致不可移植:
- 数据尺寸发生变化 (比如Long 在32位 = 4 在64位=8)
- 数据位置发生变化 (比如long 在32位中M= 4 在64位M=8)
第一个问题,起因是基本的数据类型在不同的系统所占据的字节数不同造成的,解决办法可以参考之前的知识可移植性数据类型即可。现在主要讨论第二个问题。
考虑结构体:
struct node
{int8_t a;int32_t b;int16_t c;
};
以上结构体,在不同的的平台中,成员的尺寸是固定不变的,但由于不同平台下各个成员的m值可能会发生改变,因此成员之间的相对位置可能是飘忽不定的,这对数据的可移植性提出了挑战。
解决的办法有两种:
- 第一,固定每一个成员的m值,也就是每个成员之间的塞入固定大小的填充物固定位置:
- 该方法可以保证数据依然存在地址对齐的优势(访问相对高效)
struct node
{int8_t a __attribute__((aligned(1))); // 将 m 值固定为1int64_t b __attribute__((aligned(8))); // 将 m 值固定为8int16_t c __attribute__((aligned(2))); // 将 m 值固定为2
};
- 第二,将结构体压实,也就是每个成员之间不留任何空隙:
- 该方案取消的成员之间的空隙,也就是无视了地址对齐对于访问效率会有一定的破坏
struct node
{int8_t a;int64_t b;int16_t c;
} __attribute__((packed));//示例
#pragma pack(push, 1) // 保存当前对齐状态,并设置为 1 字节对齐
struct MyPackedStruct {char a; // 1 byteint b; // 4 bytes (现在可以从任何地址开始了)short c; // 2 bytes// 没有 padding 被插入
};
#pragma pack(pop) // 恢复之前保存的对齐状态// 现在这个结构体的大小是 1 + 4 + 2 = 7 字节
⑥结构体的占位符
占位符顾名思义,可以在声明结构体成员是把每一个成员所占的二进制位的数量进行设置,做到极致的内存利用率。但是需要注意数据溢出的问题!!
#include <stdio.h>struct Node
{// 结构体中使用:可以直接指定该成员占用的二进制位的位数unsigned int Num:2; // Num 只占用两个二进制位char Type:2;// Type 只占用两个二进制位short s:4 ; // s 只占用四个二进制位// 因此以上三个数据加起来也就刚好一个字节char ch ;short sh ;
};int main(int argc, char const *argv[])
{printf("%ld\n" , sizeof( struct Node ));struct Node data ;data.Num = 4 ;printf("%d\n" , data.Num );return 0;
}