C语言中的结构体你知多少?

目录

结构体存在的意义是什么?

结构体的概念

结构体的优点

结构体的声明

匿名结构体 

结构的自引用

访问结构体成员的两种方法

 结构变量的初始化

 结构体内存对齐

  为什么存在内存对齐?

对齐规则         

修改默认对齐数

计算结构体成员的偏移量

结构体传参

结构体实现位段 

什么是位段?

位段的声明 

位段的内存分配

位段的应用 

位段使用的注意事项


结构体存在的意义是什么?

因为在实际生活中,我们要描述的对象一般都是比较复杂的。例如,要描述一本书籍,包含有作者的姓名char类型,出版社char类型,价格float类型等信息。此时要用一个基本变量类型的数组来存储是不可能的。因为数组是具有相同数据类型元素的集合,然而这些数据显然具有不同的类型。属于构造类型的结构体,可以很好的解决这一问题。

结构体的概念

结构体是一些值的集合,这些值称为成员变量。结构体的每一个成员可以是不同类型,也可以是相同类型。

结构体的优点

结构体不仅可以记录不同类型的数据,而且使得数据结构是“高内聚,低耦合”的,更利于程序的阅读理解和移植,而且结构体的存储方式可以提高CPU对内存的访问速度。

高内聚,低耦合:在一个项目中,每个模块之间相互联系的紧密程度,模块之间联系越紧密,那么耦合性就越高;模块与模块之间联系不紧密,就说明耦合性低,模块的独立性就越好。一个模块中,各个元素的联系紧密程度越高,则内聚性越高,即高内聚。现在的软件结构设计,都会要求“高内聚,低耦合”,来保证软件的高质量!

结构体的声明

以一名学生为例:

struct student

{

     char name[20];

     int age;

     float grade;

};

其中,struct是声明结构体的关键字,student是结构体的名字,内部的name、age、grade是结构体成员,struct student是一种类型。结构体的声明格式可以总结如下:

struct 结构体名

{

      结构体成员;

      ……

}; 

当然,我们还可能会遇到下面两种写法:

struct student

{

     char name[20];

     int age;

     float grade;

} Stu;                 / /不一样之处在这

这种写法在声明结构体变量student的同时创建了变量Stu。另一种写法是在声明的同时结合关键字typedef进行类型重命名。

typedef struct student

{

     char name[20];

     int age;

     float grade;

} student;           

之后就可以用student创建变量。例如下面给出的一段代码:

typedef struct student
{
	char name[20];
	int age;
	float grade;
}student;

int main()
{
	student S = { "zhangsan",18,85 };
	return 0;
}

匿名结构体 

什么是匿名结构体?匿名结构体就是在声明的时候省略掉了结构体的名字。下面声明的结构体就是匿名结构体。

struct 

{

    int  a;

    char  b;

    float  c;

}x;

个人认为,匿名结构体不怎么常用,平时在写代码时应尽量避免使用,这里只是简单的提了一下这东西,直到有这么一回事就可以了,平时很少用。

结构的自引用

在结构中包含一个类型为该结构本身的成员是否可以呢?例如下面代码。

报了个未定义的错误,也就是说,struct Node这种类型还未产生就提前在内部使用了。其实,以上代码是误解自引用的产物。结构体的自引用是指结构体成员中包含一个指向该结构体的指针。正确的写法应该是这样的:

上述代码其实是在创建节点,学了数据结构的读者就深有体会了。

访问结构体成员的两种方法

方法一:

用 . (点操作符)

结构体变量名 .  成员名

方法二:

用 ->箭头访问

结构体指针->结构体成员

想一想,为什么存在第二种访问结构体成员的方法?

因为在某些函数调用时传的是结构体的地址,也就是结构体指针。我们知道,形参是实参的一份临时拷贝,传地址更加高效,也就用4或8字节的空间,但是如果结构体很大,拷贝一份的开销可不止这么多。有了第二种访问方法,就可以很方便的在被调用函数内访问结构体成员,同时提高了效率。

 结构变量的初始化

  首先得区别初始化和赋值。

struct student
{
	char name[20];
	int age;
	float grade;
};
int main()
{
	//在变量创建时就给值,这个叫初始化
	struct student stu = { "zhangsan",18,85 };
	//变量创建后对成员给值,这个叫赋值
	stu.age = 200;
	return 0;
}

struct student
{
    char name[20];
    int age;
    float grade;
}stu = { "zhangsan", 18, 85 };//初始化

值得注意的是,结构体只能被整体初始化,不能被整体赋值,想要赋值的话只能把成员逐个地取出来再赋值。 请看代码:

struct student
{
	char name[20];
	int age;
	float grade;
}stu;

int main()
{
	stu = { "zhangsan",18,85 };//error
	return 0;
}

这种写法是错误的,因为结构体不能被整体赋值。 下面演示如何把成员逐个取出来赋值,在对字符数组(本例中的name)进行赋值时,隐藏了一个细节。

错误写法:

这种写法在对字符数组进行赋值时是错误的,“zhangsan” ,这个表达式的结果为首元素的地址,类型是char*,name是数组名,也是首元素的地址,类型为char*,但是数组名是常量,不可修改,当然会报错。

 正确写法:

用strcpy函数对字符数组进行赋值。

 结构体内存对齐

  为什么存在内存对齐?

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

   > 性能原因。数据结构(尤其是栈)应该尽可能的在自然边界上对齐。原因在于,为了访问未对    齐内存,处理器需要做两次内存访问,而对齐的内存只需要一次访问即可。举个例子,假设处理    器总是从内存中取8个字节,如果我们能保证将所有的double类型的数据的地址都对齐到8的倍        数,那么就可以一次访问读取我们所需的数据。否则,我们可能需要执行两次访问,才能把一个    double类型的数据取出。

                                                                                                                                                                 红色框为数据块。

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

对齐规则         

说明:以下所说的偏移量都是相对于结构体起始位置而言的。

> 结构体的第一个成员总是对齐到偏移量为0的地址处 

> 其他成员要对齐到对齐数的整数倍处的地址(下面会解释对齐数)

> 结构体总大小为最大对齐数的整数倍

> 如果嵌套了结构体,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处,结构体的整体大小就是最大对齐数(含嵌套结构体成员的对齐数)的整数倍。

对齐数 = 编译器默认对齐数该成员变量大小较小值 

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

下面,举一组例子来计算结构体大小,感受上述方法,同时对比分析这两种不同写法的结果。

第一种写法:

请问程序运行的结果是什么? (VS2019下测试)

首先用上述方法分析结果,再将分析得出的答案与程序运行结果比对,看看此处的分析是否正确。

第一步:计算对齐数

                       自身大小               编译器默认对齐数          对齐数 

char  c1  ——       1          ——              8               ——        1

    int   i   ——       4          ——              8               ——        4

char  c2  ——       1          ——              8               ——        1                最大对齐数为 4

 第二步:画图

从偏移量为0的位置到偏移量为8的位置,总共占9个字节,那该结构体的大小是不是就为9字节呢?答案是否定的,别忘了结构体总大小为最大对齐数的整数倍,很明显,9不是最大对齐数4的整数倍,往上去,最先找到的符合的是12。所以该结构体最终的大小为12字节。以上就是分析的全过程,下面来看看程序运行的结果是否与分析得到的结果相同。

可以看到,分析结果与程序运行结果相同,说明上述的分析是正确的。接下来,举第二个例子。

第二种写法: 

还是按上述方法分析就可以得出结果了,这里就不再分析了,直接看运行结果。

对比这两种写法,只是把结构体成员调了顺序,得出的结果就不一样,从节约空间的角度来看,第二种写法显然更好。对比这两个写法,得出的结论是:占用空间小的成员要尽量集中在一起,这样可以节省空间。

修改默认对齐数

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

用法如下:

计算结构体成员的偏移量

 size_t offsetof( structName, memberName );

函数 offsetof —— 可以计算出结构体成员相较于起始位置的偏移量

需要引头文件 —— <stddef.h>

 用法如下:

#include <stdio.h>
#include <stddef.h>

struct s2
{
	char c1;
	char c2;
	int i;
};

int main()
{
	printf("%d\n", offsetof(struct s2, c1));
	printf("%d\n", offsetof(struct s2, c2));
	printf("%d\n", offsetof(struct s2, i));
	return 0;
}

 运行结果: 

结构体传参

结构体传参的两种方式:

> 直接将结构体传过去

> 将结构体的地址传过去

 下面将给出两种传参方式的代码。

struct S
{
	int data[1000];
	int num;
};

struct S s = { {1,2,3,4,5},999 };//创建变量并初始化

//直接传结构体
void print1(struct S s)
{
	printf("%d\n", s.num);
}

//传结构体的地址
void print2(struct S* ps)
{
	printf("%d\n", ps->num);
}

int main()
{
	print1(s);//直接传结构体
	print2(&s);//传结构体的地址

	return 0;
}

  上面的print1和print2两个函数,你觉得哪一个更好呢?为什么?

   如果是我,我觉得print2更好,理由如下:

函数传参的时候,参数是需要压栈的,会有时间和空间上的系统开销。如果直接传递结构体,结构体较大时,参数压栈的系统开销比较大,会导致性能的下降。如果传的是结构体的地址,无非就是4或8个字节(32位或64位),效率相对来说要好很多,结构体越大,越能体现出这种优势。所以,建议结构体传参的时候尽量传结构体的地址。

结构体实现位段 

我不敢保证每一位读者都听说过位段,但我相信大家一定听说过段位。接下来,介绍一下什么是位段。

什么是位段?

位段是通过结构体来实现的一种以位(bit位)为单位的数据存储结构,它可以把数据以位的形式紧凑的储存,并允许程序员对此结构的位进行操作。

> 位段的成员必须是int、unsigned int、char等整形家族成员。

> 位段的成员一般都是同一数据类型,例如都是int或char类型。

> 位段的出现就是为了节省空间的。

位段的声明 

位段的声明和结构体的声明类似,但也有不同之处。第一点不同之处就是上面提到的,位段的成员一般是int、char等整形家族;第二点不同之处就是位段的成员名后边有一个冒号和一个数字。下面先声明一个位段,再解释位段中的数字到底有什么含义。

//位段的声明
struct A
{
    int a : 2;
    int b : 5;
    int c : 10;
    int d : 30;
};

A就是一个位段类型。那么数字2、5、10、30的含义是什么呢?

这些数字的单位都是比特,以数字2为例,2,说明变量a需要占用两个比特位。其他的数字同理。

位段的内存分配

我相信肯定会有小伙伴好奇,上述的位段A占用多大的内存。下面就让程序跑起来,看看结果何。

//位段的声明
struct A
{
	int a : 2;
	int b : 5;
	int c : 10;
	int d : 30;
};
int main()
{
	printf("%d\n", sizeof(struct A));
	return 0;
}

运行结果为:

可以看到,位段A占用8个字节。 下面分析这一结果的缘由。

位段的空间上是按照需要以4个字节(int)或者1个字节(char)的方式来开辟的。需要注意的是,不能写成这样:int  a : 40; 在32位平台下,int的大小为4个字节,即32个比特位,这里明显超过了32,是错误的写法。可以看到,编译器会报错。

这里插入了一个小细节,下面接着上文讲为什么结果是8字节。

位段A中都时int类型,每次按需开辟4个字节。请看图:

a、b、c和起来占用17个比特位,4个字节的空间完全够用,此时还剩下15个比特位,d要用30个比特位,显然15个比特位放不下,所以又开辟了4个字节。加起来总共8个字节。

其实,这当中还隐藏了一些细节,例如下面的例子。

在一个字节内部,是从左向右分配还是从右向左分配,是标砖尚未定义的。还有,结合上面的,a、b、c使用了17个比特位,还剩15个比特位,这15个比特位是别舍弃还是利用,这也是标准未定义的 ,不同的编译器的实现可能有所不同,在VS2019下,就是舍弃了这15个比特位。可以看出,位段涉及很多不确定的因素,是不跨平台的,注重可移植的程序应该避免使用位段。

位段的应用 

虽然上面把位段说的一无是处,但是,在特定的场景下,使用位段时高效的。例如是⽹络协议中,IP数据报的格式,这里就不过多赘述了。

位段使用的注意事项

位段的几个成员共用同一个字节,这样有些成员的起始位置并不是某个字节的起始位置,那么这些位置处是没有地址的。因为内存中,每个字节分配一个地址,一个字节内部的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);
	return 0;
}

 会报错误:不允许使用位域的地址。

正确示范:

struct A
{
	int a : 2;
	int b : 5;
	int c : 10;
	int d : 30;
};
int main()
{
	struct A sa = { 0 };
	int a = 0;
	scanf("%d", &a);
	sa.a = a;
	return 0;
}

需要注意的是,a只有两个比特位,最大值不能超过3(两个比特位的二进制最大值为11,即3)。 

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值