C语言——结构体详解

目录

结构体

结构体的声明

结构体的初始化

结构的特殊声明——匿名结构体

结构体的自引用

结构体内存对齐

结构体传参

结构体实现位段

这个位段的大小是多少?

位段的内存分配

位段的跨平台问题

位段的应用

使用位段的注意事项

结语


结构体

在C语言中,整形我们可以用int来描述,字符我们可以用char来描述。但是如果我们现在要描述一个学生,你就会发现无法用int或char来描述一个学生。结构体,就是用来解决这个问题的

结构体的声明

struct  tag

{

        member-list;

} variable - list;

 如上,struct tag就是结构体变量的变量名,比如我想描述一个学生,那么我就可以写成:

struct student

{

        ;

};

要描述一个学生,就需要一些该学生的信息,如:姓名,性别,学生证号等等。那么我们就可以这么写:

struct student

{

        char name[ 5 ];

        char gender[ 6 ];  //male 或 female

        int age;

};

 就像整形的类型是int字符的类型是char我们定义出来的结构体变量的类型就是struct student,如下:

int a;
char a;
struct student a;

同时,下方的variable - list是我们定义变量的地方,如下:

#include<stdio.h>

struct student
{
	char name[5];
	char gender[6];//male 或 female
	int age;
}a;//等价于struct student a

int main()
{
	struct student c;
	return 0;
}

结构体的初始化

对于结构体,我们应该一个一个初始化,如下:

#include<stdio.h>

struct student
{
	char name[20];
	char gender[6];//male 或 female
	int age;
};

int main()
{
	struct student a = { "张三","male",18 };
	struct student b = { "王五","male",30 };

	struct student c = { .gender = "female",.age = 12,.name = "静香" };
	return 0;
}

我们可以看到,结构体的初始化是需要把每一个都初始化的,但是不一定要按照顺序,我们可以通过点操作符——  .  来找到结构体的成员

但是需要注意的是,如果要初始化一个字符的话,那我们就需要用到 “ ” 

结构的特殊声明——匿名结构体

在结构体中有这样一种类型——匿名结构体,如下:

struct
{
	char name[20];
	char gender[6];
	int age;
} a;

我们会看到,这个结构体很不一样,因为他都没有名字!而我们也不能去引用他,如上我们可以创建一个结构体变量 struct student a; 但是这个我们会发现根本没法引用

而且使用匿名结构体变量时,我们会碰到一些特殊的场景,如下:

#include<stdio.h>

struct
{
	char name[20];
	char gender[6];
	int age;
} a;

struct
{
	char name[20];
	char gender[6];
	int age;
} *b;

int main()
{
	b = &a;
	return 0;
}

当我们运行程序的时候,编辑器就会报错

这是因为编辑器会认为,这是两个不同的东西,所以我们没有办法将两者联系到一快儿

所以我们什么时候才要使用匿名结构体变量呢?

只有该结构体只会使用一次时,才会用到匿名结构体变量

当然,如果我们还想使用这个匿名结构体变量的话,我们还可以用 typedef 对结构体进行重命名

//情况1
typedef struct
{
	char name[20];
	char gender[6];
	int age;
}Stu;


//情况2
typedef struct abc
{
	char name[20];
	char gender[6];
	int age;
}Stu;

如上,情况1 和情况2 是完全相等的,都是结构体变量Stu

结构体的自引用

而在结构体里,还有关于结构体的自引用

这方面的知识多用于初阶数据结构中的链表,我们能通过结构体的自引用找到其他相同类型的结构体

在这里我简单说一下链表是一个什么东西:

如上是一个数组,我们通过首元素地址就能顺藤摸瓜找到整个数组

这时我们来想一个问题:如果有8节车厢,每一节车厢之间都需要钥匙才能通过,而我们手上就只有第一节车厢的钥匙,那我们该如何走到尾车厢呢?

答案是:在每一节车厢都放着通往下一节车厢的钥匙

我们在每一个链表节点里面放着下一个节点的地址,而我们通过地址就能够找到下一个节点

而结构体自引用的写法如下:

#include<stdio.h>

struct Stu
{
	int age;
	struct Stu* next;
};

注意,当我们用到的是结构体指针时,我们就需要使用 -> (箭头操作符)来找到结构体指针指向的内容

还需注意的是,我们如果要使用typedef 来给结构体重命名的话,我们在结构体内部是不能使用我们重命名的名字的,如下:

//正确做法
typedef struct Stu
{
	int age;
	struct Stu* next;
}Stu;

//错误做法
typedef struct Stu
{
	int age;
	Stu* next;
}Stu;

结构体内存对齐

结构体的内存对齐是结构体的重点,讲的是结构体在内存中的存储方式

结构体内存对齐有如下几条规则:

  1. 结构体的第一个成员要对其到结构体变量偏移量为0的地址处
  2. 对齐数成员变量大小编辑器默认对齐数相比之下的较小值,而每个成员变量都要对齐到对齐数的整数倍的地址上
  3. 结构体的总大小最大对齐数的整数倍
  4. 如果在该结构体中嵌套了一个结构体变量作为成员,那么该结构体成员的最大对齐数就是其内部最大成员变量的对齐数

#pragma     这个指令可以修改默认对齐数,如:

#pragma pack(1)  //  此处将默认对齐数修改为 1

看到这里可能有些人会觉得很难,接下来我就来逐个给各位讲讲

首先我们先来看这么一串代码:

#include<stdio.h>

struct Stu1
{
	char a;
	int b;
	char c;
};

struct Stu2
{
	char a;
	char b;
	int c;
};

int main()
{
	printf("%d\n", sizeof(struct Stu1));
	printf("%d\n", sizeof(struct Stu2));
	return 0;
}

我们会看到,两个结构体内存放的虽然都是相同的元素,但是大小却不尽相同,我们用画图的方式来理解一下

如上,蓝色部分代表的是char类型,橙色部分代表的是int类型

先看左边的部分:

我们先存了一个char类型的数据,在偏移量为0的位置。而接着存了一个int类型的数据进去,但是根据第二条规则,这个int类型数据不是第一个,所以要对其到对齐数的整数倍处。默认对齐数为8,int的大小为4,8>4,所以对齐数为4,int就要对齐到4的倍数处,如上

而随后又存了一个char的数据进去,易得其对齐数为1,所以直接跟在int的后面

又根据第3条规则结构体的大小为最大对齐数的整数倍,int的对齐数为4,char的对齐数为1,显然4是最大的对齐数,上面的总大小为9,但9不是4的倍数,所以我们需要浪费3个空间凑到12,12是4的倍数,所以这个结构体的大小为12

再来看右边的部分:

易得char的对齐数为1int的对齐数为4,两个char连在一起放置,而int需要对其到4的倍数处,而总大小为8,最大对齐数为4,8是4的倍数,所以该结构体的总大小为8

看完了上述的情况,相信你对前三条规则已经有了初步的了解,而第四条又是什么意思呢?如下:

#include<stdio.h>

struct Stu1
{
	char a;
	int b;
};

struct Stu2
{
	char a;
	char b;
	int c;
	struct Stu1 d;
};

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

结构体1被嵌套在结构体2中,结构体1的成员里最大对齐数是4(int),又因为默认对齐数8>4,所以该结构体的对齐数就是4

对其到4的倍数的位置之后,其原本的大小为8,所以结构体2的大小就是16

为什么会出现对齐呢?

1. 平台原因

有些平台有特定的搜索数据的方式,某些硬件平台只能在特定的地址处取特定的数据

2. 性能原因

同样的一个数据,以不同的方式存进内存里,第一种编辑器会直接找到数据,但是第二种计算机第一次找只能找到前半段,后半段需要第二次寻找,也就是说效率变低了

所以,内存对齐从本质上来讲就是用空间换时间

结构体传参

首先我们需要知道

int*     是指向 int 的结构体

char*  是指向 char 的结构体

struct Stu*  就是一个指向 struct Stu 的结构体

了解了上述内容,我们接下来来看下面这么一段代码

#include<stdio.h>

struct Stu
{
	char a;
	int b;
};

void Print1(struct Stu* test)//用结构体指针接收
{
	printf("%d\n", test->b);
}

void Print2(struct Stu test)
{
	printf("%d\n", test.b);
}

int main()
{
	struct Stu test = { 'a',1 };

	Print1(&test);//传址
	Print2(test);//传值
	return 0;
}

两个函数的结果相同,那我们用哪一种方式会好一点呢?

https://blog.csdn.net/2302_80023639/article/details/134412692?spm=1001.2014.3001.5501

在这里我推荐各位了解一下函数栈帧的相关知识点

我们都知道,函数的形参是实参的一份临时拷贝,而如果我们将整个结构体传过去的话,倘若结构体过大,那么程序运行的效率就会下降

而如果我们传一个结构体的地址过去,随后用结构体指针来接收的话,指针的大小为 4 / 8 个字节,对程序运行效率的影响远小于传结构体

结构体实现位段

讲完了结构体,我们就需要了解结构体实现位段的能力

注意:位段的成员只能是int,unsigned int,char类型(ASCII码)的数据

struct A
{
	int a : 2;
	int b : 5;
};

如上就是一个位段,位段的成员后面都必须跟一个冒号(:)和一个数字

这个数字表示的是什么呢?

——     一个比特位

也就是说,我们在位段里可以按照我需要的字节分配空间,如上,我们加起来就需要7个比特位

这个位段的大小是多少?

一个int有32个比特位,而内存的开辟是需要按照4个字节(int)或是1个字节(char)的方式来开辟的,我们在位段中用的是int,所以这个位段的大小就为4

也就是说,不足一个int的,用一个int就够了,所以大小就是一个int的大小,没有用到的空间就会被浪费掉

#include<stdio.h>

struct A
{
	int _a : 2;
	int _b : 5;
	int _c : 10;
};

struct B
{
	int _a : 2;
	int _b : 5;
};

struct C
{
	int _a : 2;
	int _b : 5;
	int _c : 10;
	int _d : 30;
};

int main()
{
	printf("%d\n", sizeof(struct A));
	printf("%d\n", sizeof(struct B));
	printf("%d\n", sizeof(struct C));
	return 0;
}

如上,前两个因为大小都没有超过32个比特位,所以只需要一个int的大小就足够了

而第三个需要47个比特位,47>32,一个字int不够用,所以我们就需要2个int,大小就为8

位段的内存分配

我们来看这样一段代码
 

#include<stdio.h>

struct S
{
	char a : 3;
	char b : 4;
	char c : 5;
	char d : 4;
};


int main()
{
	struct S s = { 0 };
	s.a = 10;
	s.b = 12;
	s.c = 3;
	s.d = 4;
	return 0;
}

如上我们会看到,位段会先用第一个字节,但是第一个字节用完了之后,是把剩下的空间用了,还是直接使用下一个字节?这个C语言没有明确规定具体看编辑器

就像上面,一个字节有8个比特位,我的 a 和 b 已经把前 7 个给用了,那我的 c 是把剩下那个比特位用了还是直接开辟一个新的字节,这是不确定的

再看,我们现在是要把 a 改成10,10的二进制是   01010

要把 b 改成 12,12的二进制是  01100

要把 c 改成 3,3 的二进制是  00011

要把 d 改成 4,4 的二进制是  00100

我们 a 只给了3个字节,所以我们只能取到 01010 的前三个 ——  010

同理,b 就取到 1100, c 就取到 00011,d 就取到 0100

在图上就是

而在程序执行时,其会4个4个地划分,如下:

我们在编辑器上看一看效果:

位段的跨平台问题

位段在跨平台方面是存在很大问题的,因为很多C语言没有规定,所以只能看具体的编辑器

  1. int 时有符号 int 还是无符号 int 时未知的,也就是最高位是否是符号位是未知的             
  2. 一些比较远古的机器是只有16位的,当我们在32位的机器上创建了一个位段,放到16位的机器上就会出现问题                                                                                                  
  3. 位段中的成员是从左边开始分配内存还是从右边开始也是不确定的,如上我们是从左向右依次分配 a、b、c、d 还是从右向左,这是不确定的                                                    
  4. 当剩下的字节空间不足下一个位段成员存放时,是先把剩下的用完,还是重新再开辟一个新的字节空间,这是不确定的

综上,位段虽然比之结构体能更加节省空间,但是位段存在跨平台的问题

位段的应用

位段应用在明确知道要使用几个字节的情况

如下是IP数据报

如上,我们明确知道了版本要用 4个比特位,首部长度要用 8个比特位等等,在明确知道了要用多少个比特位的情况下,我们就可以考虑使用位段

使用位段的注意事项

由于位段可能是几个成员共用一个字节,是按比特位来算的,所以位段的起点不一定是字节的起点

另,位段使用的是比特位,但只有字节有地址,单一的比特位是没有地址的

使用我们不能对变量成员使用   取地址符号(&),也不能使用 scanf 对成员进行赋值

如若要改变位段成员的值的话,就需要先将其赋值到一个变量上,然后再将该变量赋值给位段成员

或者就直接给成员赋值

如下:

#include<stdio.h>

struct S
{
	char a : 1;
	char b : 4;
};


int main()
{
	struct S s = { 0 };
	//错误示范
	scanf("%d", % s.b);


	//正确示范1
	s.a = 10;

	//正确示范2
	int d = 0;
	scanf("%d", &d);
	s.b = d;

	return 0;
}

结语

至此,我们的结构体就讲完了,如果对你有帮助的话,希望可以留下一个赞!

  • 22
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值