结构体
结构体的声明
常规声明
1 2 3 4
| struct tag { member-list; }variable-list;
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| struct Stu { char name[20]; char tele[12]; char sex[10]; int age; }s4,s5,s6;
struct Stu s3; int main(void) { struct Stu s1; struct Stu s2; return 0; }
|
s1,s2是局部变量,s4,s5,s6是全局变量
特殊的声明
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| struct { int a; char b; float c; }x;
struct { int a; char b; float c; }a[20];
|
匿名结构体类型创建时必须给出变量名称
1 2 3 4 5 6 7
| struct { int a; char b; float c; }* psa;
|
结构体的自引用
1 2 3 4 5 6
| struct Node { int data; struct Node n; };
|
1 2 3 4 5
| struct Node { int data; struct Node* next; };
|
结构体重命名
1 2 3 4 5
| typedef struct Node { int data; struct Node* next; }Node;
|
不能这样写:
1 2 3 4 5
| typedef struct { int data; Node* next; }Node;
|
结构体变量的定义和初始化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| #include <stdio.h> struct T { double weight; short age; }; struct S { char c; struct T st; int a; double d; char arr[20]; }; int main(void) { struct S s = { 'c', { 55.6, 30 }, 100, 3.14, "hello world" }; printf("%c %lf %d %d %lf %s\n", s.c, s.st.weight, s.st.age, s.a, s.d, s.arr); return 0; }
|
结构体内存对齐
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| #include <stdio.h> struct S1 { char c1; int a; char c2; }; struct S2 { char c1; char c2; int a; }; int main(void) { struct S1 s1 = { 0 }; struct S2 s2 = { 0 }; printf("%d\n", sizeof(s1)); printf("%d\n", sizeof(s2)); return 0; }
|
输出12 8
结构体的对齐规则:
- 第一个成员在与结构体变量偏移量为0的地址处
- 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处
对齐数 = 编译器默认的一个对齐数(msvc默认是8,gcc没有默认对齐数)与该成员大小的较小值
gcc直接把成员大小作为对齐数
- 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍
- 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍
为什么存在内存对齐:
- 平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
- 性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐,原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
结构体的内存对齐是拿空间来换取时间的做法
在设计结构体的时候,既要满足对齐,又要节省空间,可以通过让占用空间小的成员放在一起
修改默认对齐数
#pragma pack(4)
设置默认对齐数为4
#pragma pack ()
取消设置的默认对齐数
计算偏移量
offsetof(TYPE, MEMBER)
包含在stddef.h
中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| #include <stdio.h> #include <stddef.h> struct S { char c; int i; double d; }; int main(void) { printf("%d\n", offsetof(struct S, c)); printf("%d\n", offsetof(struct S, i)); printf("%d\n", offsetof(struct S, d)); return 0; }
|
输出0 4 8
结构体传参
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| struct S { int a; char c; double d; }; void Init(struct S tmp) { tmp.a = 100; tmp.c = 'w'; tmp.d = 3.14; } int main(void) { struct S s = { 0 }; Init(s); return 0; }
|
这种传参方式是错误的,只是值传递,不会改变s
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| struct S { int a; char c; double d; }; void Init(struct S* ps) { ps->a = 100; ps->c = 'w'; ps->d = 3.14; } int main(void) { struct S s = { 0 }; Init(&s); return 0; }
|
传入地址是正确的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| #include <stdio.h> struct S { int a; char c; double d; }; void Init(struct S* ps) { ps->a = 100; ps->c = 'w'; ps->d = 3.14; }
void Print1(struct S tmp) { printf("%d %c %lf\n", tmp.a, tmp.c, tmp.d); }
void Print2(struct S* ps) { printf("%d %c %lf\n", ps->a, ps->c, ps->d); } int main(void) { struct S s = { 0 }; Init(&s); Print1(s); Print2(&s); return 0; }
|
在不改变结构体内容时传值和传址都可以
传址更好,传去的只是地址,占用空间更小,传值如果结构体内容过多,会降低运行速度
为了防止传址导致结构体内容改变,可以在传址是加入const
修饰,防止结构体内容改变
位段
位段的声明
位段的声明和结构是类似的,有两个不同:
- 位段的成员必须是int,unsigned int或signed int等整型
- 位段的成员名后边有一个冒号和一个数字
- 一个位段的声明类型一般都是相同的
比如:
1 2 3 4 5 6
| struct A { int a: 2; int b: 3; int c: 5; };
|
位段 — 二进制位
int a: 2
表示a只有两个比特位
位段的内存分配
- 位段的成员可以是int,unsigned int,signed int或者是char(整形家族)类型
- 位段的空间上是按照需要以4个字节(int)或者1个字节(char)的方式来开辟的
- 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段
32位平台下:
每32bit(四个字节)可以开辟多个整型,如果剩余比特位小于下一个整型需要的比特位,则舍弃,另开辟32bit
如果是char则一次开辟一个字节(8bit)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| #include <stdio.h> struct A { int a: 2; int b: 3; int c: 30; }; int main(void) { struct A a; printf("%d\n", sizeof(a)); return 0; }
|
输出8
a,b在一起共用四个字节,c另开辟四个字节
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| #include <stdio.h> struct A { char a: 3; char b: 4; char c: 5; char d: 4; }; int main(void) { struct A a; a.a = 10; a.b = 20; a.c = 3; a.d = 4; return 0; }
|
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0| b | a | | c | d
一共三个字节
a = 10是1010,超限只能存010 — 2
b = 20是10100,超限只能存0100 — 4
c = 3是011,存00011 — 3
d = 4是100,存0100 — 4
位段的跨平台问题
- int位段被当成有符号数还是无符号数是不确定的
- 位段中最大位的数目不能确定(16位机器最大16,32位机器最大32,写成27,在16位机器会出问题
- 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义
- 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的
总结:跟结构体相比,位段可以达到同样的效果,但是可以很好的节省空间,但是有跨平台的问题存在
位段的应用
网络数据传输会封装很多固定位数据,使用位段有利于节省空间,统一数据类型