解析编程中不可或缺的基础:深入了解结构体类型

精琢博客,希望可以给大家带来收获~

博主主页:17_Kevin-CSDN博客

收录专栏:《C语言》


引言

在编程中,结构体是一种自定义的数据类型,它允许开发人员将不同类型的数据组合在一起,并为其定义相关属性和行为。结构体提供了一种灵活的方式来表示复杂的数据结构,使得程序设计更加模块化和可读性更高。

结构体类型的声明 

结构的声明
 

声明格式如下:

struct 结构体类型名
{
	成员名-list;

}直接声明变量-list;

结构体变量的声明和使用

下面是一个程序。首先创建了一个结构体类型Stu,里面包括了成员变量name、age、sex和 id。在主函数中创建了结构体变量s,并打印。

结构体变量创建格式:

① 按成员顺序初始化:结构体类型名 + 自定义变量名(+ 初始化内容);

② 按指定顺序初始化:结构体变量名 + 自定义变量名 (+ .成员名);

 变量的使用:结构体变量名 . 成员名

#include <stdio.h>

struct Stu
{
	char name[20];//名字
	int age;//年龄
	char sex[5];//性别
	char id[20];//学号
};

int main()
{
	//按照结构体成员的顺序初始化
	struct Stu s = { "张三", 20, "男", "20230818001" };
	printf("name: %s\n", s.name);
	printf("age : %d\n", s.age);
	printf("sex : %s\n", s.sex);
	printf("id : %s\n", s.id);
	//按照指定的顺序初始化
	struct Stu s2 = { .age = 18, .name = "lisi", .id = "20230818002", .sex = "⼥" };
	printf("name: %s\n", s2.name);
	printf("age : %d\n", s2.age);
	printf("sex : %s\n", s2.sex);
	printf("id : %s\n", s2.id);
	return 0;

}

结构的不完全声明

在声明结构体类型的时候可以不完全声明,直接在结构体类型后声明变量,这样创建的变量就是一次性变量,之后只能一次性使用。

声明如下:

struct
{
	int a;
	char b;
	float c;
}x;

struct
{
	int a;
	char b;
	float c;
}a[20], * p;

结构的⾃引⽤

结构的自引用典型例子就是链表中对节点的定义,用于连续节点连接,具体有关链表的知识可以点击这段蓝字阅读博主另一篇博客

自引用结构声明格式如下:

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

自定义结构体

声明格式如下:

格式:typedef + struct 结构体类型名 

        {

                成员变量;

        }自定义类型名;

 示例声明如下:

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

结构体内存对⻬(热门考点)

引子

我们经常会用sizeof运算符计算各个变量的字节大小,例如:

#include<stdio.h>

int main()
{
	printf("%d\n", sizeof(int));
	printf("%d\n", sizeof(short));
	printf("%d\n", sizeof(long long));

	return 0;
}

得到结果:

如文所示,我们可以用sizeof来计算各个类型的大小,那么结构体变量计算会得到什么结果呢?

对⻬规则

结构体变量在内存中会遵循结构体对齐规则,对齐规则如下:

1.结构体的第⼀个成员对⻬到和结构体变量起始位置偏移量为0的地址处

2.其他成员变量要对⻬到某个数字(对⻬数)的整数倍的地址处。

   对⻬数=编译器默认的⼀个对⻬数与该成员变量⼤⼩的较⼩值。

    VS 中默认的值为
    Linux中gcc没有默认对⻬数,对⻬数就是成员⾃⾝的⼤⼩

3.结构体总⼤⼩为最⼤对⻬数(结构体中每个成员变量都有⼀个对⻬数,所有对⻬数中最⼤的)的整数倍。

4.如果嵌套了结构体的情况,嵌套的结构体成员对⻬到⾃⼰的成员中最⼤对⻬数的整数倍处,结构体的整体⼤⼩就是所有最⼤对⻬数(含嵌套结构体中成员的对⻬数)的整数倍。

结构体内存对齐练习

1.非嵌套结构体

#include<stdio.h>

int main()
{
	struct S1
	{
		char c1;
		int i;
		char c2;
	};

	printf("%d\n", sizeof(struct S1));

	return 0;
}

按照内存对齐规则,从编译器行数从上到下进行内存存储。逐个对比各个成员和VS的默认对齐数8对比,取二者最小对齐数作为对齐数。

根据对齐数从0开始偏移计算每个变量开始存储的内存地址,成员变量要对⻬到对⻬数的整数倍的地址处,如图所示。

最后计算结构体总共大小是需要按照结构体中成员变量的最大对齐数进行对齐,最终结构体大小是最大对齐数的整数倍,产生的浪费空间也要计入总大小。

内存存储图示如下:

2.嵌套结构体

#include<stdio.h>

struct S3
{
	double d;
	char c;
	int i;
};

int main()
{
	struct S4
	{
		char c1;
		struct S3 s3;
		double d;
	};
	printf("%d\n", sizeof(struct S4));

	return 0;
}

计算嵌套结构体的字节大小时对待被嵌套的结构体时,就相当于把嵌套结构体当做结构体的一个成员进行内存对齐。最终计算字节总大小的时候用所有成员中最大对齐数(包括被嵌套结构体中的成员)进行整数倍的计算。

下图即为上述代码的演示图例:

为什么存在内存对⻬?

1. 硬件访问要求: 计算机硬件对于访问内存通常有一定的要求,例如某些硬件可能只能从特定地址开始读取数据,或者只能按照特定的字节长度进行读取。通过内存对齐,可以保证结构体中的字段在内存中按照一定的规则排列,满足硬件访问的要求。

2. 性能优化: 在数据结构中,尤其是在涉及栈这种数据结构时,我们应该尽可能使数据在自然边界上对齐。这样做的原因在于,处理器访问未对齐的内存时需要进行两次内存访问,而对齐的内存访问只需要一次访问。举例来说,如果一个处理器总是从内存中取8个字节,则地址必须是8的倍数。如果我们能够保证所有的double类型的数据的地址都对齐成8的倍数,那么就可以用一个内存操作来读或写值了。否则,我们可能需要执行两次内存访问,因为对象可能被分放在两个8字节内存块中。因此,通过合理地对数据进行内存对齐,我们可以提高程序的执行效率和性能表现。

3. 内存空间利用: 内存对齐可以使数据结构更加紧凑,减少内存空间的浪费。如果结构体中的字段按照对齐规则排列,编译器可以合理地利用内存空间,避免由于未对齐而导致的内存浪费。

4. 平台移植性: 不同的计算机架构可能对内存对齐有不同的要求。合理地处理结构体的内存对齐可以增强程序在不同平台上的移植性,使程序更容易地在不同平台上移植和运行

针对于性能优化,我们可以了解到结构体对齐是为了优化性能,用空间换时间,那么有没有什么办法让我们尽量的减少浪费的空间呢?

我们可以利用结构体对齐的规则,将小的结构体尽量的凑在一起,这样他们会在空间上连续存储,因为对齐数小的和大的之间会存在大对齐数所造成的空间浪费,,所以将小的放一块这样就可以将其中的浪费空间给合理利用起来。

 具体如下代码示例及图示:

struct S1
{
	char c1;
	int i;
	char c2;
};
struct S2
{
	char c1;
	char c2;
	int i;
};

修改默认对齐数

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

例如,我们要将编译器的默认对齐数修改为1,那么勇以下代码实现:

#pragma pack(1)

如果需要取消修改的默认对齐数,使用以下代码即可实现:

#pragma pack()


 

位段结构体

当我们需要在C或C++中表示一些具有特定位长度的数据时,位段(bit fields)结构体就成为了一种非常有用的工具。位段结构体允许我们将数据按位组织,并且可以更加高效地使用内存空间。

什么是位段结构体?

位段结构体是C和C++中的一种特殊结构,它允许我们定义结构体的成员为特定位长度的字段,从而更为灵活地管理数据。通过位段结构体,我们可以精确地控制每个字段的位数,从而在内存中节约空间。

如何定义位段结构体?

在C和C++中,我们可以使用结构体来定义位段。

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

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

下面是一个简单的例子:

struct BitFieldStruct 
{
    unsigned int flag1 : 1;
    unsigned int flag2 : 2;
    unsigned int flag3 : 3;
};

位段的内存分配
 

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

通过上文已经得知位段结构体如何创建,下面请通过示例代码和图示来了解位段结构体再内存中的分配原理。

代码如下:

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

s.a = 10;
s.b = 12;
s.c = 3;
s.d = 4;

代码中定义了一个结构体类型S,在main函数中创建S型变量s并初始化为0。重点在于,在已经规定的位段情况下,后面的a,b,c,d赋值后在内存中是如何存储的呢?

图示操作如下:

最后的d由于在第二个字节段中无法存储,所以会直接存到下一个字节中,大小位4比特。


使用位段的注意事项

位段的⼏个成员共有同⼀个字节,这样有些成员的起始位置并不是某个字节的起始位置,那么这些位
置处是没有地址的。内存中每个字节分配⼀个地址,⼀个字节内部的bit位是没有地址的。
所以不能对位段的成员使⽤&操作符,这样就不能使⽤scanf直接给位段的成员输⼊值,只能是先⼊放在⼀个变量中,然后赋值给位段的成员。

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. 节省内存空间:位段结构体可以将多个字段压缩到一个字节中(或者更少),这样可以减少内存使用量。在一些嵌入式系统或需要高效利用内存的场景中,位段结构体可以发挥重要作用。

  2. 更好的可移植性:位段结构体可以帮助开发者更好地处理不同机器上的字节顺序问题和对齐方式。因为位段结构体的字段是按照位来处理的,所以不受机器的字节顺序和对齐方式的影响。

  3. 方便地操作位数据:位段结构体可以方便地处理二进制数据,例如一些硬件寄存器中的位标志。使用位段结构体可以使得代码更加简洁和易读,降低出错的风险。

  4. 更好的兼容性:位段结构体的语法与普通结构体非常相似,因此可以很容易地与其他代码进行交互和集成。此外,C++11标准中还引入了新的标准化位字段类型,称为 std::bitset,可以更加方便地处理位数据。


我的博客即将同步至腾讯云开发者社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=1g0z83rjgi6dj

I'm Kevin, and we'll see you in the next blog

  • 63
    点赞
  • 46
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 32
    评论
结构体是由多个不同类型的变量组成的数据类型,每个变量在内存占用不同的空间。结构体类型地址偏移量指的是结构体每个变量的地址相对于结构体起始地址的偏移量。 在C语言,可以使用指针来访问结构体的变量。偏移量可以通过指向结构体的指针加上变量偏移量来计算。例如,假设有以下结构体: ```c struct student { char name[20]; int age; float score; }; ``` 可以定义一个指向该结构体的指针,并使用指针来访问结构体的变量: ```c struct student stu; struct student *p = &stu; ``` 访问结构体的变量可以使用点运算符或箭头运算符,例如访问name变量的偏移量可以使用以下代码: ```c int name_offset = (int)&((struct student *)0)->name; ``` 这个表达式,(struct student *)0将0强制转换为指向结构体类型的指针,然后取出name成员的地址,最后将其转换为int类型。这个偏移量可以用来计算结构体name变量的地址。 类似地,可以计算age和score变量的偏移量: ```c int age_offset = (int)&((struct student *)0)->age; int score_offset = (int)&((struct student *)0)->score; ``` 使用偏移量可以在不知道结构体起始地址的情况下访问结构体的变量。例如,可以使用以下代码访问结构体的name变量: ```c char *name_ptr = (char *)((int)p + name_offset); ``` 这个表达式,将指向结构体的指针p转换为int类型,然后加上name变量的偏移量,最后将结果转换为指向char类型的指针。这个指针可以用来访问结构体的name变量。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 32
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

17_Kevin

你们的点赞收藏是对我最大的鼓励

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值