结构体详解

目录

基本介绍

1.结构体类型的声明

2. 结构体变量的创建和初始化

2.1结构体正常声明

2.2结构体的匿名声明

2.3结构体的自引用

3. 结构成员访问操作符

3.1 直接访问结构体成员

3.2 通过指针访问结构体成员

4. 结构体内存对齐

4.1对齐规则

4.2嵌套结构体

4.3为什么存在内存对齐?

4.4修改默认对齐数

5. 结构体传参

6. 结构体实现位段

6.1什么是位段

6.2位段的声明

6.3位段的内存分配

6.4位段的跨平台问题

6.5位段使用的注意事项


基本介绍

结构体是一些数据的集合,这些数据被称为成员,而每个成员可以是不同类型的变量。

而我们可以用结构体自定义一个类型来使用。

例如:

#include<stdio.h>
struct STU
{
	char name[20];//char数组类型成员
	int age;//int类型成员
	struct STU Deskmate;//结构体类型成员
};

1.结构体类型的声明

#include<stdio.h>
struct STU
{
    char name[20];
    int age;
    char sex[5];
};//👈分号不能少

定义一个结构体,描述好结构体内的成员类型,就是结构体的声明。

2. 结构体变量的创建和初始化

2.1结构体正常声明

创建结构体变量有两种方式:

第一种:在结构体的声明后面直接创建变量。

示例:

#include<stdio.h>
struct STU
{
    char name[20];
    int age;
    char sex[5];
}stu = { "Lihua",18,"boy" };//👈创建一个结构体变量并赋予初始值。

其中,变量名为stu,三个成员:name,age,sex的值分别被赋为"Lihua",18,"boy"。

如果要创建多个变量,在变量后加个逗号再创建即可。

第二种:在函数内创建结构体变量并初始化。

示例:

#include<stdio.h>
struct STU
{
    char name[20];
    int age;
    char sex[5];
};
int main()
{
    struct STU stu = { "Lihua",18,"boy" };//👈创建一个结构体变量并赋予初始值。
    return 0;
}

注意:在函数内创建结构体变量时,struct STU代表类型名(struct是在说明类型为结构体),

stu代表变量名。

2.2结构体的匿名声明

struct 
{
    char name[20];
    int age;
    char sex[5];
}stu = { "Lihua",18,"boy" };//👈只能在这里创建变量


结构体的匿名声明就是把结构体的类型名省去,正常声明结构体的类型名为struct STU,而匿名声明则将STU省去。

如此一来,结构体的类型名就不完整了,于是就不能正常在函数内创建变量(因为类型名不完整)

只能在结构体声明的最后创建变量,因此这类结构体通常只能用一次(创建一次变量)

再来看一个例子:

#include<stdio.h>
struct 
{
	char name[20];
	int age;
	char sex[5];
}stu = { "Lihua",18,"boy" };//创建变量stu

struct
{
	char name[20];
	int age;
	char sex[5];
}*p;//创建指针p

int main()
{
	p = &stu;//指针P指向stu
	return 0;
}

上面我创建了两个成员完全一样的结构体,用这两个结构体分别创建了一个变量stu与指针p,

并让指针P指向变量stu。问题来了,这么做合法吗?

警告:

结构体的类型名不完整,即使两个结构体的成员完全相同,编译器也会将两个结构体当作不同的类型。因此是非法的。

对于匿名结构体,如果没有对结构体进行重命名的话,基本只能使用一次。

2.3结构体的自引用

#include<stdio.h>
struct Node
{
	int Data;
	struct Node* next;//创建结构体指针变量
};

在结构体内创建自己的结构体变量,我们称之为结构体的自引用。

插播一条题外话,每次创建一个结构体变量时都要写struct+类型名+变量名,你会不会觉得这样写很麻烦?有没有一种更简便的写法呢?

有!只要使用typedef对结构体进行重命名即可。

#include<stdio.h>
typedef struct Node
{
	int Data;
	struct Node* next;
}N;//←对结构体重命名


//或者这样也行
//struct Node
//{
//	int Data;
//	struct Node* next;
//};//←对结构体重命名
//typedef struct Node N;

int main()
{
	N node = { 0 };
	return 0;
}

在结构体声明的开头写上typedef进行重命名,并在声明的结尾处写上结构体的新名字(N),就能将结构体类型名从struct Node重命名成N,这样创建结构体变量时就不会那么麻烦。

但是问题又来了,在结构体内进行结构体的自引用时能不能用重命名后的类型名呢?

答案是:不行。

因为声明结构体是先确定好结构体内的成员再对其进行重命名,如果在结构体内使用了重命名后的类型名,编译器会无法识别重命名后的类型名。

3. 结构成员访问操作符

既然我们能定义一个结构体,也能创建结构体变量,那么要如何访问结构体的成员呢?

要访问结构体成员,有两种方式。

3.1 直接访问结构体成员

#include<stdio.h>
struct
{
	char name[20];
	int age;
	char sex[5];
}stu = { "Lihua",18,"boy" };

int main()
{
	printf("%s %d %s", stu.name, stu.age, stu.sex);
	return 0;
}

要直接访问结构体成员,可以使用 .操作符(点操作符),使用格式为:变量名.成员名

3.2 通过指针访问结构体成员

除了直接访问结构体成员,还可以通过创建结构体指针,再用结构体指针来访问结构体成员。

例如:

#include<stdio.h>
struct STU
{
	char name[20];
	int age;
	char sex[5];
}stu = { "Lihua",18,"boy" },*p=&stu;

int main()
{
	printf("%s %d %s", p->name, p->age, p->sex);
	return 0;
}

创建一个同类型的结构体指针,并指向结构体stu,再使用操作符->就可以访问stu的成员了。

使用格式:指针名->成员名

4. 结构体内存对齐

在谈论这个内容之前,先看一个例子:

struct test
{
	int i;//4个字节
	char c1;//1个字节
	char c2;//1个字节
};
int main()
{

	printf("%zd\n", sizeof(struct test));
	return 0;
}

猜猜结构体test占用内存大小为多少?

按理来说,int型占4个字节,两个char型各占一个字节,因此结构体应该占用6个字节。

运行看看结果正不正确。

按理来讲咱算出来的是6,为什么实际占用内存是8呢?

这就涉及到一个知识点——结构体内存对齐。

4.1对齐规则

⾸先得掌握结构体的对⻬规则:
1. 结构体的第⼀个成员对齐到和结构体变量起始位置偏移量为0的地址处
2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数 = 编译器默认的⼀个对齐数 与 该成员变量大小的较小值。
- VS 中默认的值为 8
- Linux中 gcc 没有默认对齐数,对齐数就是成员自身的大小
3. 结构体总大小为最⼤对齐数(结构体中每个成员变量都有⼀个对齐数,所有对齐数中最大的)的整数倍。
4. 如果嵌套了结构体的情况,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍。

用上面的例子来解释一下对齐规则:

牛刀小试:

struct test
{
	char c1;//1个字节
    int i;//4个字节
	char c2;//1个字节
};
int main()
{

	printf("%zd\n", sizeof(struct test));
	return 0;
}

还是上面那个例子,但是将第二个成员int i与第一个成员char c1对调位置,该结构体的内存占用

会有变化吗?如果有,应该如何变化?

将上面这个代码按照刚刚的讲的对齐规则来分析即可得出答案!

据我分析,该结构体的大小应为 12 。

运行程序看看计算结果是否正确。

正是如此!

4.2嵌套结构体

如果结构体内嵌套了结构体呢,又该如何计算其大小?

让咱来测试一下便可知晓!

#include<stdio.h>
struct test_in
{
	double c1;//8个字节
	int i;//4个字节
	char c2;//1个字节
};
struct test
{
	char c1;//1个字节
	int i;//4个字节
	struct test_in t;//占用 ?个字节
};
int main()
{
	printf("%zd\n", sizeof(struct test));
	return 0;
}

看看运行结果:

以下是分析过程:

按照内存对齐规则,先算出结构体struct test_in的大小(过程为左图):

#include<stdio.h>
struct test_in
{
	double c1;//8个字节
	int i;//4个字节
	char c2;//1个字节
};
int main()
{
	printf("%zd\n", sizeof(struct test_in));
	return 0;
}

不难算出struct test_in的大小为16。在计算过程中,咱发现其成员的最大对齐数为8(double的)

根据对齐规则的第④条规则:嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍

可以知道:成员struct test_in的对齐数为8(double)。

因此,在结构体struct test中存储时,它需要存到偏移量为8的整数倍处(即图中下标为8的格子)

而整个结构体的大小为16,则直接存入16字节,此时结构体struct test的大小为8+16=24

根据对齐规则第④条:结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍。

可知:结构体struct test的最大对齐数为8(double),因此该结构体的大小必须为8的整数倍

而此时struct test的大小为24,符合对齐规则。因此可以得出结构体的大小为24。

4.3为什么存在内存对齐?

大部分的参考资料都是这样说的:
1. 平台原因 (移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2. 性能原因:
数据结构(尤其是栈)应该尽可能地在⾃然边界上对⻬。原因在于,为了访问未对⻬的内存,处理器需要作两次内存访问;⽽对⻬的内存访问仅需要⼀次访问。假设⼀个处理器总是从内存中取8个字节,则地址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对⻬成8的倍数,那么就可以⽤⼀个内存操作来读或者写值了。否则,我们可能需要执⾏两次内存访问,因为对象可能被分放在两个8字节内存块中。
总体来说:结构体的内存对⻬是拿空间来换取时间的做法。

那么,在设计结构体时,要如何做到既满足对齐规则又能尽量节省空间呢?

答:让占用空间小的成员尽量集中在一起。

例如:

struct test
{
	int i;//4个字节
	char c1;//1个字节
	char c2;//1个字节
};
int main()
{

	printf("%zd\n", sizeof(struct test));
	return 0;
}
struct test
{
    char c1;//1个字节
	int i;//4个字节
	char c2;//1个字节
};
int main()
{

	printf("%zd\n", sizeof(struct test));
	return 0;
}

就拿上面用的例子来说,第一段代码结构体大小为8字节,第二段代码结构体大小为12字节。

可以发现:即使成员类型完全相同,但是将占用空间小的成员放在一起的结构体大小会更小。

4.4修改默认对齐数

上文的对齐规则中有提到:

VS 中默认的值为 8
- Linux中 gcc 没有默认对齐数,对齐数就是成员自身的大小

既然VS有默认对齐数,那么有没有办法能将VS的默认对齐数修改呢?

有!

#pragma 这个预处理指令,可以改变编译器的默认对⻬数。

请看代码:

#include <stdio.h>
#pragma pack(1)//设置默认对⻬数为1
struct S {
	char c1;//1个字节
	int i;//4个字节
	char c2;//1个字节
};
#pragma pack()//取消设置的对⻬数,还原为默认
int main()
{
	//输出的结果是什么?
	printf("%d\n", sizeof(struct S));
	return 0;
}

将默认对齐数(8)修改为 1 后,三个成员char c1、int i、char c2 的实际对齐数将变为1

任何偏移量(下标)都为1的倍数,因此这三个成员就会紧凑地排在一起,就占用6个字节

用这个方法,结构体在对齐方式不合适的时候,我们可以自己更改默认对⻬数。

5. 结构体传参

先来看一段代码:

#include<stdio.h>
struct test
{
	int data[1000];
	int num;
};

void print1(struct test t)
{
	printf("%d\n", t.num);
}

void print2(struct test* pt)
{
	printf("%d\n", pt->num);
}

int main()
{
	struct test T = { {0},10 };
	print1(T);
	print2(&T);
	return 0;
}

这两个print函数都能打印出成员num的值,哪一个效果更好呢?

先看函数print1,形参为结构体变量。再看print2,形参为结构体指针。

根据学过的知识,我们知道:函数在传参时会创建一个临时变量(形参),并将实参的值拷贝到

该临时变量(形参)中。

也就是说:函数print1被调用时,会新创建一个结构体变量t,并将实参(T)的值赋给形参 t 。

而print2被调用时,会新创建一个结构体指针pt,并将实参(T的地址)赋给形参pt。

从占用内存的角度来看,print1会占用一个结构体的大小(sizeof(struct test)),而print2则只占用

了一个结构体指针的大小(所有指针的大小都统一 4/8个字节)。

综上所述,print2的效果更佳。

结论:结构体传参的时候,要传结构体的地址。

6. 结构体实现位段

6.1什么是位段

我们知道:char占一个字节,int占4个字节。

当我们在int中存储一个比较小的数据时,就会有内存浪费的现象。

例如:int a=5;

5的二进制为: 00000000 00000000 00000000 00000101

可以看到,虽然int占用4个字节,但是实际上存储有效数据的空间只有前三个比特位,

剩下的29个比特位将被浪费。

那么有没有一种方法,可以只给int型变量分配3个比特位的空间,以此来避免内存浪费呢?

有!那就是位段

6.2位段的声明

位段的声明和结构是类似的,有两个不同:
1. 位段的成员必须是 int、unsigned int 或signed int 、char,在C99中位段成员的类型也可以
选择其他类型。
2. 位段的成员名后边有⼀个冒号和⼀个数字。

例如:

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

A就是一个位段类型

我们打印一下,看看位段A所占内存的大小。

思考:结构体A中含有4个int型成员,按理说应当占用16个字节,实际却只占用了8个字节,这是

什么原因?

这就与位段的内存分配有关了。

6.3位段的内存分配

先来看看这个例子:

#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;
}

注意:位段的存储规则在每个编译器中都不同,在VS中是这样存,也许换到其他编译器就不同了

因此,位段的存储不支持跨平台!

根据上图的分析,该结构体一共使用了三个字节来存储各个成员的数据,

且数据为(从低字节到高字节)62 03 04,运行看看是否正确。

运行结果显示:确实占用了三个字节,且数据(低地址到高地址)也的确是62 03 04。

现在再回过头看看之前说的那个例子,是不是就迎刃而解了?

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

6.4位段的跨平台问题

1. int 位段被当成有符号数还是⽆符号数是不确定的。
2. 位段中最大位的数⽬不能确定。(16位机器最⼤16,32位机器最⼤32,写成27,在16位机器会出问题。
3. 位段中的成员在内存中从左向右分配,还是从右向左分配,标准尚未定义。
4. 当⼀个结构包含两个位段,第⼆个位段成员⽐较⼤,⽆法容纳于第⼀个位段剩余的位时,是舍弃剩余的位还是利⽤,这是不确定的。
总结:
跟结构相⽐,位段可以达到同样的效果,并且可以很好的节省空间,但是有跨平台的问题存在。

6.5位段使用的注意事项

位段的⼏个成员共有同⼀个字节,这样有些成员的起始位置并不是某个字节的起始位置,那么这些位置处是没有地址的。内存中每个字节分配⼀个地址,⼀个字节内部的bit位是没有地址的。
所以不能对位段的成员使⽤&操作符,这样就不能使⽤scanf直接给位段的成员输⼊值,只能是先输⼊放在⼀个变量中,然后赋值给位段的成员。
#include<stdio.h>
struct A 
{int _a : 2;
 int _b : 5;
 int _c : 10;
 int _d : 30;
};

int main()
{
 struct A sa = {0};
 scanf("%d", &sa._b);//这是错误的
 
 //正确的⽰范
 int b = 0;
 scanf("%d", &b);
 sa._b = b;
 return 0; 
}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值