结构体

结构体的声明

常规声明

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', 100, 3.14, "hello world" };
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

结构体的对齐规则:

  1. 第一个成员在与结构体变量偏移量为0的地址处
  2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处
    对齐数 = 编译器默认的一个对齐数(msvc默认是8,gcc没有默认对齐数)与该成员大小的较小值
    gcc直接把成员大小作为对齐数
  3. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍
  4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍

为什么存在内存对齐:

  1. 平台原因(移植原因):
    不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
  2. 性能原因:
    数据结构(尤其是栈)应该尽可能地在自然边界上对齐,原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。

结构体的内存对齐是拿空间来换取时间的做法

在设计结构体的时候,既要满足对齐,又要节省空间,可以通过让占用空间小的成员放在一起

修改默认对齐数

#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修饰,防止结构体内容改变

位段

位段的声明

位段的声明和结构是类似的,有两个不同:

  1. 位段的成员必须是int,unsigned int或signed int等整型
  2. 位段的成员名后边有一个冒号和一个数字
  3. 一个位段的声明类型一般都是相同的

比如:

1
2
3
4
5
6
struct A
{
int a: 2;
int b: 3;
int c: 5;
};

位段 — 二进制位
int a: 2 表示a只有两个比特位

位段的内存分配

  1. 位段的成员可以是int,unsigned int,signed int或者是char(整形家族)类型
  2. 位段的空间上是按照需要以4个字节(int)或者1个字节(char)的方式来开辟的
  3. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段

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

位段的跨平台问题

  1. int位段被当成有符号数还是无符号数是不确定的
  2. 位段中最大位的数目不能确定(16位机器最大16,32位机器最大32,写成27,在16位机器会出问题
  3. 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义
  4. 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的

总结:跟结构体相比,位段可以达到同样的效果,但是可以很好的节省空间,但是有跨平台的问题存在

位段的应用

网络数据传输会封装很多固定位数据,使用位段有利于节省空间,统一数据类型