文章目录
一、结构体的基本概念
C语言已经提供了内置类型,如char
,short
,float
,double
等,这些内置类型无法描述一个复杂的对象。因此C语言提供了结构体这种自定义的数据类型。它允许我们将不同类型的数据组合在一起,形成一个新的自定义数据类型。这一特性极大地增强了 C 语言处理复杂数据的能力,在实际编程中有着广泛的应用。
1.1结构体的声明
结构体使用关键字 struct
来定义
例如,想要描述一个学生:
struct student
{
char name[20]; // 名字
int age; //年龄
double height; //身高
double weight; //体重
char id[16] //学号
};//分号不可以少
结构体是一些值的集合,这些值称为成员变量。结构体中的每个成员可以是不同的数据类型,如标量,数组,指针甚至是其他结构体。
1.2 结构体变量的创建
1.2.1 在结构体声明同时创建
struct student
{
char name[20];
int age;
double height ;
double weight;
char id[16]
} s1,s2;
结构体声明时,创建变量是可选选项。此时创建的s1,s2是全局变量。
1.2.2 在main函数中创建
#include <stdio.h>
struct student
{
char name[20];
int age;
double height;
double weight;
char id[16]
};
int main( )
{
struct student s3;
struct student s4;
return 0;
}
此时创建的变量是局部变量。
1.2.3 匿名结构体
在声明结构体时,可以省略标签(tag),形成匿名结构体:
// 匿名结构体类型
struct
{
int a;
char b;
float c;
} x; // 直接创建变量x
struct
{
int a;
char b;
float c;
} a[20], *p; // 创建数组a和指针p
匿名结构体的特点是:如果没有对其重命名,基本上只能使用一次。
需要注意的是,编译器会把两个成员列表相同的匿名结构体视为不同的类型,因此下面的赋值是非法的:
p = &x; // 错误:编译器认为 p和 x的类型不同
1.3 结构体变量的初始化
1.3.1 按照默认顺序
struct student s3 = {"daisy",20,170,50,"2410"};
如果结构体成员变量中包含结构体:
#include <stdio.h>
struct S
{
char c;
int n;
};
struct A
{
struct S s;
int* p;
char arr[10];
};
int main()
{
struct A a = { {'1',1},NULL,"hhhh"};
}
1.3.2 指定成员初始化
struct student s3 = {.age = 30,.weight=60,.name="Rare",.height=180,.id="2410"};
1.4 结构体成员访问
- 使用操作符
.
可以直接访问结构体中的成员
例如:打印结构体成员:
#include <stdio.h>
struct student
{
char name[20]; // 名字
int age; //年龄
double height; //身高
double weight; //体重
char id[16]; //学号
};
int main()
{
struct student s1 = {"daisy",20,170,50,"2410"};
printf("%s %d %f %f %s\n",s1.name, s1.age,s1.height,s1.weight,s1.id);
}
- 使用操作符
->
可以间接访问成员,一般用于访问指针变量指向的内容
#include <stdio.h>
struct Point
{
int x;
int y;
};
int main()
{
struct Point p = {3, 4};
struct Point *ptr = &p;
ptr->x = 10;
ptr->y = 20;
printf("x = %d y = %d\n", ptr->x, ptr->y);
return 0;
}
1.5 结构体的自引用
结构体可以包含指向自身类型的指针,这在数据结构中非常常见(如链表、树等)。
错误的自引用方式:
struct Node
{
int data;
struct Node next; // 错误:会导致结构体大小无限大
};
正确的自引用方式:
struct Node
{
int data;//数据域
struct Node* next; // 指针域
//正确:使用指针
};
使用typedef
对结构体重命名时,需要注意自引用的正确写法:
错误写法:
typedef struct
{
int data;
Node* next; // 错误:此时Node尚未定义
} Node;
正确写法:
typedef struct Node
{
int data;
struct Node* next; // 正确:使用struct Node
} Node;
二、结构体内存对齐
计算结构体的大小是C语言中的一个重要考点,涉及到内存对齐规则。
2.1 对齐规则
- 结构体的第一个成员对齐到与结构体变量起始位置偏移量为0的地址处。
- 其他成员变量要对齐到某个"对齐数"的整数倍的地址处。
对齐数=编译器默认的一个对齐数 与 该成员变量大小的较小值.- VS中默认对齐数为8
- Linux中gcc没有默认对齐数,对齐数就是成员自身的大小
- 结构体总大小为最大对齐数(结构体中所有成员对齐数的最大值)的整数倍。
- 如果嵌套了结构体,嵌套的结构体成员对齐到自己的最大对齐数的整数倍处,结构体的整体大小是所有最大对齐数(含嵌套结构体的)的整数倍。
2.2 示例分析
练习1:
struct S1
{
char c1; // 大小1,对齐数1
int i; // 大小4,对齐数4(VS环境)
char c2; // 大小1,对齐数1
};
printf("%d\n", sizeof(struct S1)); // 输出结果:12
分析:
- c1从偏移0开始,占用1字节(0)
- i需要对齐到4的倍数,从偏移4开始,占用4字节(4-7)
- c2从偏移8开始,占用1字节(8)
- 总大小需为最大对齐数4的倍数,因此总大小为12
练习2:
struct S2
{
char c1; // 1字节
char c2; // 1字节
int i; // 4字节
};
printf("%d\n", sizeof(struct S2)); // 输出结果:8
分析:
- c1在0,c2在1,共占用2字节
- i对齐到4的倍数,从4开始,占用4字节(4-7)
- 总大小8,是最大对齐数4的倍数
练习3:
struct S3
{
double d; // 8字节
char c; // 1字节
int i; // 4字节
};
printf("%d\n", sizeof(struct S3)); // 输出结果:16
分析:
- d在0-7(8字节)
- c在8(1字节)
- i需要对齐到4的倍数,从12开始(12-15)
- 总大小16,是最大对齐数8的倍数
练习4(嵌套结构体):
struct S4
{
char c1; // 1字节
struct S3 s3; // 16字节(最大对齐数8)
double d; // 8字节
};
printf("%d\n", sizeof(struct S4)); // 输出结果:32
分析:
- c1在0(1字节)
- s3需要对齐到自身最大对齐数8的倍数,从8开始,占用16字节(8-23)
- d对齐到8的倍数,从24开始,占用8字节(24-31)
- 总大小32,是最大对齐数8的倍数
2.3 内存对齐的原因
平台原因(移植性):
某些硬件平台只能访问特定地址的数据,否则会抛出异常。性能原因:
对齐的内存访问只需一次操作,未对齐的可能需要两次。例如,访问未对齐的8字节数据可能需要读取两个8字节块并拼接。
内存对齐是"空间换时间"的策略。设计结构体时,应将占用空间小的成员集中在一起,以减少内存浪费(如S2比S1更节省空间)。
2.4 修改默认对齐数
可以使用#pragma pack
预处理指令修改默认对齐数:
#include <stdio.h>
#pragma pack(1) // 设置默认对齐数为1
struct S
{
char c1; // 1字节
int i; // 4字节
char c2; // 1字节
};
#pragma pack() // 取消设置,还原默认
int main()
{
printf("%d\n", sizeof(struct S)); // 输出6(1+4+1)
return 0;
}
当对齐方式不合适时,可通过此指令优化内存使用。
三、结构体传参
结构体传参有两种方式:传值和传地址。
struct S
{
int data[1000];
int num;
};
struct S s = {{1,2,3,4}, 1000};
// 传值
void print1(struct S s)
{
printf("%d\n", s.num);
}
// 传地址
void print2(struct S* ps)
{
printf("%d\n", ps->num);
}
int main()
{
print1(s); // 传结构体
print2(&s); // 传地址
return 0;
}
推荐使用传地址方式,原因是:
- 函数传参时参数需要压栈,结构体过大时,传值会导致大量的时间和空间开销
- 传地址仅传递4或8字节(指针大小),效率更高
- 若需要修改结构体内容,传地址是必要的
四、结构体实现位段
位段(bit-field)是结构体的一种特殊形式,用于节省内存空间,尤其适合存储只需要少量bit位的数据。
4.1 位段的声明
位段的声明与结构体类似,但有两个区别:
- 位段成员通常是
int
、unsigned int
、signed int
或char
类型 - 成员名后有一个冒号和数字(表示占用的bit位数)
struct A
{
int _a:2; // 占用2个bit位
int _b:5; // 占用5个bit位
int _c:10; // 占用10个bit位
int _d:30; // 占用30个bit位
};
printf("%d\n", sizeof(struct A)); // 输出8
分析:
- 前三个成员共占用2+5+10=17个bit位
- 加上_d的30位,总共47位
- 位段以4字节(int)或1字节(char)为单位开辟空间,因此需要8字节(64位)
4.2 位段的内存分配
存在矛盾:
- 位段空间按4字节(int)或1字节(char)的方式开辟
- 位段成员在内存中的分配方向(左到右或右到左)未定义
- 当剩余空间不足时,是否利用剩余空间未定义
示例:
struct S
{
char a:3; // 3位
char b:4; // 4位
char c:5; // 5位
char d:4; // 4位
};
struct S s = {0};
s.a = 10; // 二进制1010(只取低3位:010)
s.b = 12; // 二进制1100
s.c = 3; // 二进制11
s.d = 4; // 二进制100
在VS环境中的内存分配(每次分配1字节,分配方向为从右到左):
- 第一个字节:a占3位(010),b占4位(1100),共7位,剩余1位未使用
- 第二个字节:c占5位(00011)
- 第三个字节:c剩余0位,d占4位(0100)
4.3 位段的跨平台问题
位段存在以下跨平台问题,需谨慎使用:
- int位段的符号性不确定(有符号/无符号)
- 位段最大位数不确定(16位机器最大16,32位机器最大32)
- 成员分配方向(左到右/右到左)未定义
- 剩余空间处理方式不确定
4.4 位段的应用场景
位段适合存储不需要完整字节的数据,例如网络协议中的数据报格式:
使用位段可以精确控制每个字段的位数,显著减少数据报大小,提高网络传输效率。
4.5 位段使用注意事项
位段成员没有地址,因此不能使用&
操作符,也不能直接用scanf
输入:
struct A
{
int _a:2;
int _b:5;
};
int main()
{
struct A sa = {0};
// scanf("%d", &sa._b); // 错误:不能取位段成员地址
// 正确方式
int b = 0;
scanf("%d", &b);
sa._b = b;
return 0;
}
结构体是C语言中实现复杂数据类型的基础,掌握结构体的声明、初始化、内存对齐规则和传参方式,对于编写高效、可维护的代码至关重要。位段作为结构体的特殊形式,在需要节省内存的场景(如嵌入式系统、网络协议)中非常有用,但需注意其跨平台限制。
通过合理设计结构体,我们可以将相关数据组织在一起,提高代码的可读性和效率。理解内存对齐原理,能帮助我们写出更节省内存的程序;而正确的传参方式,则能提升程序性能。这些知识不仅是C语言学习的重点,也是后续学习数据结构(如链表、树、图)的基础。