C语言:结构体入门

目录

1. 基础知识

1.1 结构的声明

1.2 结构体变量创建及初始化

2. 结构体内存对齐

2.1 结构体内存对齐规则

2.2 为什么存在结构体内存对齐

2.3 修改默认对齐数

3. 结构体传参

4. 结构体实现位段

4.1 位段的声明

4.2 位段的跨平台问题

4.3 位段使用注意事项


1. 基础知识

如果说数组是一系列相同类型数据的集合,结构体则定义了不同类型数据的集合。比如我们想要描述学生,包括学生的姓名、学号、年龄、班级等信息,单一的内置类型是不可行的,而结构体这种自定义的数据类型能够让程序员自行创建适合的类型。

结构体的每个成员可以是不同类型的变量,如:标量、数组、指针,甚至其他结构体。那么可以为本身的结构体嘛?不可以。

1.1 结构的声明

结构的声明包括3个要件,结构体类型名、成员变量及结构体变量名。

//描述一个学生
struct student           //结构体类型名
{
    char name;           //成员变量名为name,数据类型为char
    int age;             //成员变量名为age,数据类型为int
    float score;         //成员变量名为score,数据类型为float
}s1,s2,s3                //结构体变量名
  • 结构体类型名。其中struct student为结构体的类型名。声明的结构体类型名就好似日常所见的其他数据类型名,比如整型数据类型名为【int],浮点数据类型名为【float】,字符串数据类型名为【char】,那么这里结构体数据类型名为【struct student】。
  • 成员变量。成员变量包括变量名(比如name,age,score)以及变量对应的数据类型,成员变量可根据实际开发需要创建多个。
  • 结构体变量。如果需要创建一个int类型的变量,我们会这样写:int a;按照同样的方式,需要创建结构体变量,可以这样写:struct student s1;
//整型变量的创建
int a = 0;
//结构体变量的创建
struct student s1; //其中struct student 表示结构体类型名,s1为创建的结构体变量

1.2 结构体变量创建及初始化

刚刚讲到,结构的声明包含结构体类型名、成员变量以及结构体变量,那么在实际使用过程中必须同时包括这3个要件嘛?答案是:不必须。关于结构的声明,有3种形式,不同形式下结构体变量创建及初始化会略有不同。

  • 结构体类型名+成员变量

结构体变量未声明,则在后续使用时创建。可对具体成员变量进行初始化,也可一次性对所有成员变量进行默认顺序初始化及指定顺序初始化。

//描述一个学生
struct student  //结构体类型名
{
    char name;  //成员变量名为name,数据类型为char
    int age;    //成员变量名为age,数据类型为int
    float score; //成员变量名为score,数据类型为float
}

//整型变量的创建及初始化
int a = 0;

//结构体变量的创建和初始化
struct student s1;    //结构体变量创建

//对具体成员变量初始化
s1.name="zhangshan";   //结构体初始化
s1.age=15;
s1.score=98.5;  

//对所有成员变量进行默认顺序初始化及指定顺序初始化
struct student s1 = {"zhangshan",15,98.5};  //按照默认顺序初始化
struct student s2 = {.age=15,.name="zhangshan",.score98.5}; //按照指定顺序初始化
  • 匿名结构体类型+成员变量+结构体变量

结构体类型名未完全声明,在这种情况下,后续不能根据结构体类型名创建变量,因此必须在该声明阶段创建结构体变量,不然结构的声明无法供后续使用,那么也是没有意义的。

匿名的结构体类型,如果未对结构体类型重命名的话,基本只能使用一次,因为结构体变量的创建在结构声明阶段已经完成,且后续不能通过结构体类型名创建变量。

//描述一个学生
struct 
{
    char name;   //成员变量名为name,数据类型为char
    int age;     //成员变量名为age,数据类型为int
    float score; //成员变量名为score,数据类型为float
} s1,s1,s2,s3   //结构体变量可以是多个

//整型变量的创建及初始化
int a = 0;

//对具体成员变量初始化
s1.name="zhangshan"; 
s1.age=15;
s1.score=98.5;  

//对所有成员变量进行默认顺序初始化及指定顺序初始化
s1 = {"zhangshan",15,98.5};  //按照默认顺序初始化
s1 = {.age=15,.name="zhangshan",.score98.5}; //按照指定顺序初始化
  • 结构体类型+成员变量+结构体变量

声明阶段创建的结构体变量,后续可以使用并初始化;声明的结构体类型,后续也可用来创建结构体变量。初始化方式与上述内容一致。

//描述一个学生
struct student
{
    char name;   //成员变量名为name,数据类型为char
    int age;     //成员变量名为age,数据类型为int
    float score; //成员变量名为score,数据类型为float
} s1,s1,s2,s3   /

2. 结构体内存对齐

在讲结构体内存对齐之前,我们先看看下面的一个小例子。直观上看,变量a和变量c为字符串,内存大小为1字节;变量b为整型,内存大小为4个字节,整个结构体的大小应该为6个字节,那为什么会是12个字节呢?

int main()
{
	struct Student
	{
		char a;
		int b;
		char c;
	};

	printf("%d ", sizeof(struct Student));
	return 0;
}
//输出:12

2.1 结构体内存对齐规则

为解决这个问题,首先得掌握结构体的内存对齐规则:

  • 结构体的第一个成员对齐到和结构体变量起始位置偏移量为0的地址处。
  • 其他成员变量对齐到某个数字(对齐数)的整数倍的地址处。对齐数为编译器默认的一个对齐数与该成员变量大小的较小值。VS中默认为8。Linux和gcc没有默认对齐数,对齐数就是成员自身的大小。
  • 结构体整体大小为最大对齐数的整数倍。最大对齐数:结构体中每个成员变量都有一个对齐数,所有对齐数中最大的。
  • 结构体嵌套结构体,结构体整体大小为最大对齐数的整数倍。最大对齐数:结构体和嵌套的结构体中每个成员都有一个对齐数,所有对齐数中最大的。

再回到上面这个例子上来,内存是如何对齐的呢???

练习1:

字符类型的变量内存较小,则对齐数较小值永远为自身变量的大小(1个字节),除0外所有无符号整数均为1的整数倍,那么字符串类型的变量永远可以在其他变量下连续开辟空间。

int main()
{
	struct Student
	{
		char a;
		int b;
		char c;
	};

	printf("%d ", sizeof(struct Student));
	return 0;
}
//输出:12
  • 变量a作为第一个成员变量,其地址为结构体变量偏移量为0的地址处
  • 其他成员变量对齐到某个数字(对齐数)的整数倍的地址处。
    • 变量a,大小为1,系统默认对齐数为8,则对齐数较小值为1
    • 变量b,大小为4,系统默认对齐数为8,则对齐数较小值为4
    • 变量c,大小为1,系统默认对齐数为8,则对齐数较小值为1
  • 结构体整体大小为最大对齐数的整数倍。变量a的对齐数为1,变量b的对齐数为4,变量c的对齐数为1,那么所有变量中对齐数最大的为最大对齐数(这里为4),结构体整体大小为4的倍数,即12。空白处空间完全浪费。

练习2:

跟练习1中同样的3个成员变量,由于在内存中存放的顺序不一致,结构体大小也是不一样的。因此在设计结构体的时候,既要满足对齐,又要节省空间时,尽量让空间小的成员集中在一起

int main()
{
	struct Student
	{
		char a;
		char c;
		int b;
	};

	printf("%d ", sizeof(struct Student));
	return 0;
}
//输出:8

练习3:

int main()
{
	struct S1
	{
		char a;
		char c;
		int b;
	};

	struct S2
	{
		double d;
		int e;
		struct S1 f;
	};

	printf("%d ", sizeof(struct S2));
	return 0;
}
//输出:24

2.2 为什么存在结构体内存对齐

通过几个小练习可以发现结构体内存对齐,会造成空间浪费,那为什么会存在存在结构体对齐呢?

平台原因

不是所有硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。

性能原因

数据结构应该尽可能地在自然边界上对齐,原因在于为访问未对齐地内存,处理器需作两次内存访问;而对齐地内存访问仅需一次访问。总的来说,结构体的内存对齐是拿空间换时间的做法。

2.3 修改默认对齐数

结构体在对齐方式不合适的时候,我们可以自行更改默认对齐数。

#pragma pack(1)//设置默认对⻬数为1
#pragma pack()//取消设置的对⻬数,还原为默认

3. 结构体传参

结构体传参,要传结构体的地址。函数中的形参是实参的临时拷贝,当采用print1的方式调用函数时,会临时开辟与结构体大小一致的空间来存放实参的数据,而采用print2通过地址传参,形参仅需要开辟一个存放指针地址大小的空间来存放结构体指针变量,对内存承载影响较小。

struct S
{
	char name;
	int age;
} s1;

void print1(struct S s1)
{
	printf("%d\n", s1.name);
}

void print2(struct S* ps)
{
	printf("%d\n",ps->name);
}


int main()
{
	s1.name = "zhangshan";
	s1.age = 15;
	print1(s1);
	print2(&s1);
	return 0;
}

4. 结构体实现位段

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

4.1 位段的声明

位段的声明和结构体类似,包括位段类型名,成员变量及位段变量。不同点在于:

  • 位段的成员可以为int,unsigned int, signed int, 或char 等类型
  • 位段成员名后边有一个冒号和一个数字,这个数字为bit位数
struct S
{
	int a : 2;
	int b : 5;
	int c : 10;
    int d : 30;
};

4.2 位段内部空间的开辟

同样的,在讲位段之前,我们先看看下面的一个小例子。直观上看,结构体的内存空间为2+5+10+30=47个比特位,6个字节足以存储,但实际输出结果为8个字节。需要说明的一点是,位段的存在是为了节省空间,但并不是无节制的节省空间。相较于16个字节,已经节省一半了。

struct S
{
	int a : 2;
	int b : 5;
	int c : 10;
    int d : 30;
};

int main()
{
	printf("%d", sizeof(struct S));
	return 0;
}
//输出:8

位段内部空间的分配是按照需要以4个字节(int)或者1个字节(char)的方式来开辟的。

以上述例子来看,位段内部成员为int,那么首先会开辟一块4个字节的空间来使用,变量a,b,c完全存放后,还剩下15个比特,不能将变量b完全存放在同一块4个字节的空间里,这时候内存会再开辟第二块4个字节的空间来存放变量d。

再来看一个例子:

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;
	printf("%d", sizeof(struct S));
	return 0;
}

//输出:3

整型数值10的补码为:00000000 00000000 00000000 00001010 变量a中能存放3个bit的二进制,则进行截断,最终存放010

整型数值12的补码为:00000000 00000000 00000000 00001100 变量b中能存放4个bit的二进制,则进行截断,最终存放1100

整型数值  3的补码为:00000000 00000000 00000000 00000011 变量c中能存放5个bit的二进制,则进行截断,最终存放00011

整型数值 4的补码为:00000000 00000000 00000000 00000100 变量d中能存放4个bit的二进制,则进行截断,最终存放0100

位段内部成员类型为char,那么首先会开辟一块1个字节的空间来使用,变量a,b完全存放后,还剩下1个比特,存不下变量c,内存会再开辟第二块1个字节的空间来存放变量c,还剩下3个比特,不够存放变量d,内存再开辟第三块1个字节的空间来完全存放变量d。

4.2 位段的跨平台问题

  • int位段被当成有符号数还是无符号数是不确定的。
  • 位段中最大位的数目是不能确定的。(32位机器最大32,64位机器最大64)
  • 位段中的成员在内存中从左向右,还是从右向左分配尚未定义。VS中从右向左。
  • 当一个结构包含两个位段时,第二个位段成员比较大,无法容纳于第一个位段剩余的比特位时,是舍弃剩余位还是利用,尚不确定。VS中是舍弃,再开辟新的。

4.3 位段使用注意事项

内存中每个字节分配一个地址,字节中的比特是没有地址的。位段的几个成员共用同一个字节,这样有些成员的起始位置并不是某个字节的起始位置,所以不能对位段的成员使用&操作符,也不能使用scanf直接给位段成员输入值,只能输入存放在一个变量中,然后再给位段成员变量赋值。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值