自定义数据类型:结构体(C语言进阶)

时间:2023-01-07 17:54:15
  • 结构体类型的声明
  • 结构体的自引用
  • 结构体内存对齐
  • 结构体传参

自学b站“鹏哥C语言”笔记。

一、结构体类型的声明

详见文章【初识结构体】第一部分。补充说明:

匿名结构体类型:省略结构体标签(tag)

struct
{
int a;
char b;
float c;
}x;
struct
{
int a;
char b;
float c;
}* p;
int main()
{
p = &x;
return 0;
}

注意:上述代码会报错。因为编译器认为这两个匿名结构体是不同的,则​​p = &x;​​会出错。

二、结构体的自引用

详见文章【初识结构体】第二部分。补充说明:

错误示例

struct Node
{
int data;
struct Node 0;
};

注意:上述结构体Node的存储空间不可知

正确示例:运用指针

struct Node
{
int data;
struct Node* next;
}

注意:自引用不能使用匿名结构体类型,否则会出错。

三、结构体内存对齐

1.结构体内存对齐的规则

(1)结构体第一个成员放在与结构体变量偏移量为0的地址处。

(2)结构体其他成员要对齐到某个数字(对齐数)的整数倍的地址处。

对齐数 = min { 编译器默认的一个对齐数,该成员大小 }

VS中默认的对齐数是8

(3)结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。

(4)特例:嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体总大小是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

例1:一般情况

#include <stdio.h>
struct S1
{
char c1;
int a;
char c2;
}

struct S2
{
char c1;
char c2;
int a;
}

int main()
{
struct S1 s1 = {0};
struct S2 s2 = {0};
printf("%d\n", sizeof(s1));
printf("%d\n", sizeof(s2));
}

输出结果:12 8

解析

char是1字节,int是4字节,VS中默认的对齐数是8。

那么,c1对齐数=min{1,8}=1

a对齐数=min{4,8}=4

c2对齐数=min{1,8}=1

S1的内存:第一行的数字指的是靠左这条线上的指针

0

1

2

3

4

5

6

7

8

9











c1

a

c2


最大对齐数=max{1,4,1}=4

目前,所有成员所占大小为9字节,不是最大对齐数的倍数。

因此,应补齐到12字节。

0

1

2

3

4

5

6

7

8

9

10

11

12














c1

a

c2


同理,S2的内存:

0

1

2

3

4

5

6

7

8










c1

c2

a


最大对齐数=max{1,4,1}=4

目前,所有成员所占大小为8字节,是最大对齐数的倍数。

因此,S2所占内存就是8字节。

例2:结构体嵌套情况

#include <stdio.h>
struct S3
{
double d;
char c;
int i;
};

struct S4
{
char c1;
struct S3 s3;
double d;
};

int main()
{
printf("%d\n", sizeof(struct S4));
}

输出结果:32

解析

先计算S3:

double是8字节,char是1字节,int是4字节,VS中默认对齐数是8。

那么,double对齐数=min{8,8}=8

char对齐数=min{1,8}=1

int对齐数=min{4,8}=4

最大对齐数=max{8,1,4}=8

S3内存:和例1同理,得16字节

0

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16


















d

c

i


再计算S4:

char是1字节,S3是16字节,double是8字节,VS中默认对齐数是8。

那么,char对齐数=min{1,8}=1

S3对齐数=S3自己的最大对齐数=8

double对齐数=min{8,8}=8

最大对齐数=max{1,8,8}=8

S4内存:32字节

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


































c1

s3

d


2.内存对齐的意义

为什么存在内存对齐?

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

总结:结构体的内存对齐是用空间换取时间的做法。

3.设计结构体

一个好的结构体设计,要能做到既节省空间又节省时间

节省时间已经通过内存对齐实现了。

我们需要在满足内存对齐的前提下,尽量节省空间:让占用空间小的成员尽量集中在一起

4.修改默认对齐数

​#pragma​​可以更改默认对齐数。

#pragma pack(4)//设置默认对齐数为4
struct S
{
char c1;
double d;
}
#取消设置的默认对齐数

5.宏

​offsetof(struct tag, 成员变量名)​​​能够输出成员相对首地址的偏移量,引用前要写​​#include <stddef.h>​​。

例1:

#include <stdio.h>
#include <stddef.h>
struct S
{
char c;
int i;
double d;
}

int main()
{
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:

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()
{
struct S s = {0};
Init(s);
return 0;
}

运行后s仍然都是0。原因是传参传的是s值,改的是tmp,并没有改变s。

改进:

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()
{
struct S s = {0};
Init(&s);
return 0;
}

运行后s改变了。原因是传参传的是s的地址,改变tmp的同时,也改变了s。

改进:打印出改变后的s值

#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()
{
struct S s = {0};
Init(&s);
Print1(s);
Print2(&s);
return 0;
}

注意:

Print1函数的传参传的是s的值,可以实现。原因是该函数的目标不需要改变s的值。

当然,用传递s的值来实现也是可以的,如Print2函数。