C语言结构体详解

一、结构体类型的声明:打造自己的数据模板

结构体的本质是 "数据模板"—— 它定义了一组数据的组合方式,但本身不占用内存空间。声明一个结构体的基本语法如下:

struct 标签名 
{
    成员类型 成员名;
    //更多成员...
} 变量列表; //分号不能省略!

1.1 基本声明:描述具体对象

比如我们要描述一个学生,需要包含姓名、年龄、性别和学号,结构体声明如下:

struct Stu 
{
    char name[20]; //姓名
    int age;       //年龄
    char sex[5];   //性别
    char id[20];   //学号
}; //分号必须有!

这里的struct Stu就是一个结构体类型,我们可以用它来创建具体的变量。

1.2 特殊声明:匿名结构体

如果结构体只需要使用一次,可以省略 "标签名",称为匿名结构体:

struct 
{
    int a;
    char b;
    float c;
} x; //直接创建变量x,该结构体类型无法再次使用

注意:匿名结构体的类型是唯一的,即使成员完全相同,也会被编译器视为不同类型。例如下面的代码是非法的:

struct { int a; char b; } x;
struct { int a; char b; } *p;
p = &x; // 错误!编译器认为两者类型不同

1.3 结构体自引用:构建链表等递归结构

在实现链表、树等数据结构时,我们需要结构体中包含自身类型的成员,这称为自引用。但注意不能直接包含结构体本身(会导致无限嵌套,大小无法计算),而应该用指针:

//错误示例:包含自身变量,大小无限大
struct Node 
{
    int data;
    struct Node next; //错误!
};

//正确示例:包含自身指针
struct Node 
{
    int data;
    struct Node* next; //指针大小固定(4或8字节),合理
};

如果用typedef给结构体重命名,需避免匿名结构体提前使用新名称: 

//错误示例:匿名结构体中提前使用Node
typedef struct 
{
    int data;
    Node* next; //此时Node还未定义
} Node;

//正确示例:先声明带标签的结构体
typedef struct Node 
{
    int data;
    struct Node* next;
} Node; //正确,Node是struct Node的别名

 

二、结构体变量的创建和初始化

定义好结构体类型后,就可以创建变量并初始化了,主要有两种方式:

2.1 按成员顺序初始化

按结构体中成员的声明顺序依次赋值:

struct Stu 
{
    char name[20];
    int age;
    char sex[5];
    char id[20];
};

int main() 
{
    //按顺序初始化:姓名、年龄、性别、学号
    struct Stu s = {"张三", 20, "男", "20230818001"};
    //访问成员(用.操作符)
    printf("姓名:%s,年龄:%d\n", s.name, s.age);
    return 0;
}

2.2 指定成员初始化

可以通过成员名指定赋值,顺序不限:

int main() 
{
    //指定成员初始化,顺序可以打乱
    struct Stu s2 = {.age = 18, .name = "李四", .id = "20230818002", .sex = "女"};
    printf("姓名:%s,年龄:%d\n", s2.name, s2.age); //输出"李四,18"
    return 0;
}

 

三、重点:结构体内存对齐

结构体的大小计算是 C 语言的高频考点,也是难点。为什么struct {char a; int b;}的大小不是1+4=5字节?这就要理解内存对齐的规则。

3.1 对齐规则:决定成员的存储位置

内存对齐是编译器为了提高访问效率而采用的策略,具体规则如下:

  1. 第一个成员永远对齐到结构体起始位置(偏移量为 0)。
  2. 其他成员需对齐到 "对齐数" 的整数倍位置。
    对齐数 = min (编译器默认对齐数,成员自身大小)。
    • VS 默认对齐数为 8,Linux(gcc)默认无对齐数(对齐数 = 成员大小)。
  3. 结构体总大小必须是所有成员 "最大对齐数" 的整数倍。
  4. 嵌套结构体时,嵌套的结构体成员需对齐到自身最大对齐数的整数倍;整个结构体的总大小是所有成员(含嵌套结构体)最大对齐数的整数倍。

3.2 实例分析:用图理解内存布局

我们结合例子,用内存布局图直观展示(以 VS 环境为例,默认对齐数 8)。

例 1:struct S1
struct S1 
{
    char c1;  //大小1
    int i;    //大小4
    char c2;  //大小1
};

  • 步骤 1c1是第一个成员,放在偏移量 0 处,占 1 字节(0~0)。
  • 步骤 2i的对齐数 = min (8, 4) = 4,需放在 4 的整数倍位置(偏移量 4),占 4 字节(4~7)。
  • 步骤 3c2的对齐数 = min (8, 1) = 1,放在 8(7+1)处,占 1 字节(8~8)。
  • 总大小:最大对齐数是 4,需满足 4 的整数倍。当前已用 9 字节,向上取整到 12 字节。
偏移量:0 1 2 3 4 5 6 7 8 9 10 11
成员:  c1 空 空 空 i  i  i  i c2 空 空 空

 sizeof(struct S1) = 12

 例 2:struct S2(成员顺序优化)

struct S2 
{
    char c1;  //1字节
    char c2;  //1字节
    int i;    //4字节
};
  • 步骤 1c1在 0(0~0),c2对齐数 1,接在 1(1~1)。
  • 步骤 2i对齐数 4,放在 4(4~7)。
  • 总大小:最大对齐数 4,总大小 8(0~7),刚好是 4 的倍数。
偏移量:0 1 2 3 4 5 6 7
成员:  c1 c2 空 空 i  i  i  i

sizeof(struct S2) = 8(比 S1 节省 4 字节,可见成员顺序的重要性)。 

例 3:嵌套结构体 struct S4 

struct S3 
{
    double d; //8字节
    char c;   //1字节
    int i;    //4字节
};
struct S4 
{
    char c1;  //1字节
    struct S3 s3; //嵌套S3
    double d; //8字节
};

先算struct S3的大小:

  • d在 0(0~7),对齐数 8。
  • c对齐数 1,放在 8(8~8)。
  • i对齐数 4,需放在 12(8+1=9,最近的 4 的倍数是 12),占 12~15。
  • 最大对齐数 8,总大小需是 8 的倍数(当前 16 字节,满足)。所以sizeof(S3)=16

再算struct S4

  • c1在 0(0~0)。
  • s3是嵌套结构体,最大对齐数 8,需放在 8 的倍数(偏移 8),占 8~23(16 字节)。
  • d对齐数 8,放在 24(23+1=24,8 的倍数),占 24~31(8 字节)。
  • 最大对齐数 8,总大小 32(31-0+1=32,是 8 的倍数)。
偏移量:0 1-7 8-23(s3) 24-31(d)
成员:  c1  空   S3的16字节   d的8字节

sizeof(S4)=32。 

3.3 为什么需要内存对齐?

  1. 平台兼容性:某些硬件只能访问特定地址的数据(如只能从 4 的倍数地址读 int),否则抛出异常。
  2. 性能优化:对齐的内存只需 1 次访问,未对齐可能需要 2 次。例如:若 int 放在偏移 1 处,处理器需先读 0~3 字节,再读 4~7 字节,拼接后才能得到完整 int。

本质是用空间换时间的策略。

3.4 如何优化:平衡对齐和空间

要减少内存浪费,应将小成员集中在一起(如 S2 比 S1 节省空间)。例如:

//差:小成员分散
struct Bad 
{
    char a;    //1
    double b;  //8
    char c;    //1
}; //大小:24(0+8+8+8,最大对齐数8)

//好:小成员集中
struct Good 
{
    char a;    //1
    char c;    //1
    double b;  //8
}; //大小:16(0+1+6空+8,总16)

3.5 修改默认对齐数

#pragma pack(n)可以修改默认对齐数(n 为 1、2、4、8 等),#pragma pack()恢复默认。

#pragma pack(1) //设置默认对齐数1(无对齐)
struct S 
{
    char c1; //1
    int i;   //4
    char c2; //1
};
#pragma pack() //恢复默认

 此时对齐数 = 成员大小,总大小 = 1+4+1=6 字节。

四、结构体传参:传地址更优

结构体传参时有两种方式:传值和传地址。推荐传地址

struct S 
{ 
    int data[1000]; 
    int num; 
};

//传值:复制整个结构体,开销大
void print1(struct S s) 
{ 
    printf("%d", s.num); 
}

//传地址:只复制指针(4/8字节),高效
void print2(struct S* ps) 
{ 
    printf("%d", ps->num); 
}

原因:函数传参时参数需压栈,结构体过大时压栈开销大,导致性能下降。

总结

结构体是 C 语言中自定义复杂类型的核心工具,掌握以下几点:

  • 声明时注意匿名结构体的限制和自引用的正确方式(用指针)。
  • 初始化支持按顺序和指定成员两种方式。
  • 内存对齐是重点:理解 4 条规则,通过实例画图分析大小,记住 "空间换时间" 的本质,合理安排成员顺序优化空间。
  • 传参优先传地址,减少开销。

通过练习结构体大小计算,能更深入理解内存对齐的细节,应对考试和实际开发中的问题。

 

 

 

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值