详解C语言中结构体,枚举,联合体易错点补充

目录

一. 前言

二. 正文

2.1结构体

2.11结构的基础知识

2.12 结构体内存对齐(比较热门的考点)

结构体内存对齐规则:

2.13 为啥要结构体内存对齐?

2.14修改默认对齐数

2.15 函数传参

2.16 位段

2.17  位段存在的跨平台问题

2.2 枚举

2. 21枚举的优点

2.3 联合体

三. 结语


一. 前言

    本小节跟小伙伴们分享结构体,枚举,联合体的知识,希望能给小伙伴带来收获。

二. 正文

2.1结构体

2.11结构的基础知识

      结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。
结构的声明
struct tag
{
     member - list ;
} variable - list ;
例如描述一个学生:
struct Stu
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
};//分号不能丢

特殊的声明(匿名声明)
//匿名结构体类型
struct
{
     int a;
     char b;
     float c;
}x;
struct
{
    int a;
   char b;
   float c;
}a[20], *p;

 值得注意的是:匿名的结构体仅创建时的变量有效,如:x, a[20],*p是匿名,后面无法再次使用匿名结构体来创建新的结构体变量。

2.12 结构体内存对齐(比较热门的考点)

      接下来我们来深入了解一下:结构体内存大小 

//计算出结构体内存大小
struct S1
{
   char c1;  //1字节
   int i;        // 4字节
   char c2;  //  1字节
};
// 结果: 12个字节

这是为什么呢?

这里我们来了解一下吧

结构体内存对齐规则:

  • 1. 第一个成员在与结构体变量偏移量为0的地址处。
  • 2. 其他成员变量要对齐到某个数字(对齐数)的整数倍地址处。
对齐数 = 编译器默认的一个对齐数 与 该成员大小的 较小值
VS 中默认的值为 8,linux默认对齐数为本身,如:int  对齐数为4
  • 3. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
  • 4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整 体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

 这里我们以上一个的例子来解析:

 如何进行检测呢?

这里给大家介绍一个宏----offsetof,    他的功能就是返回结构体成员所在内存的偏移量

头文件: #include<stddef.h>

 运用:

#include<stddef.h>

#include<stdio.h>

struct S3
{
    double d;// 对齐数 8
    char c; //  对齐数1
    int i; //   对齐数4
}s3;

int  main()

{

   printf("%u\n", offsetof(struct S4, c1));    //  0
    printf("%u\n", offsetof(struct S4, s3));   //   8
    printf("%u\n", offsetof(struct S4, d));     //    12
return 0;

}

上面这个例子只实践了前面三个原则,我们再来一个嵌套结构体的例子:

//练习4-结构体嵌套问题
struct S3
{
  double d;  // 对齐数 8
  char c;      //  对齐数1
  int i;          //   对齐数4
};
//  S3 的大小: 16个字节
struct S4
{
  char c1;    
  struct S3 s3;  // 对齐数 8 , 16字节
  double d;       //  对齐数8
};
printf("%d\n", sizeof(struct S4));
//  结果:  32
如图:

 2.13 为啥要结构体内存对齐?

其中大部分资料是这样解释的:
  • 1. 平台原因(移植原因)
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能 在某些地址处取某些特定类型的数据,否则抛出硬件异常。
  • 2. 性能原因
数据结构 ( 尤其是栈 ) 应该尽可能地在自然边界上对齐。 原因在于,为了访问未对齐的
内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。

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

 那么我们如何做到减少空间的占用?

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

2.14修改默认对齐数

       之前我们见过了 #pragma 这个预处理指令,这里我们再次使用,可以改变我们的默认对齐数。

如: 


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

 值得注意的是:#pragma pack() 修改的默认对齐数一般是 2 ^ n,很少会有奇数。

2.15 函数传参

     函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。 如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致 性能 的下降 。 
结论:
结构体传参的时候,要传结构体的地址

2.16 位段

      结构体讲完就得讲讲结构体实现 位段 的能力。
什么是位段
      位段的声明和结构是类似的,有两个不同:
1. 位段的成员必须是 int unsigned int signed int
2. 位段的成员名后边有一个冒号和一个数字,表示的是所占bit位。
比如:
struct A
{
   int _a:2;   // 2 bit
   int _b:5;   // 5bit
   int _c:10;
   int _d:30;
};  // 问那  struct A的大小是多少?结果是  8个字节
位段的内存分配
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;
//空间是如何开辟的?

 解析:

思路 :首先会将一个字节放满或者不够时,再创建1个字节空间。

 这里要和之前的大小端字节序进行区分,这次的移动单位是bit,后者单位是byte。

2.17  位段存在的跨平台问题

1. int 位段被当成有符号数还是无符号数是不确定的。
2. 位段中 最大位 的数目不能确定。( 16 位机器最大 16 32 位机器最大 32 ,写成 27 ,在 16 位机 器会出问题。
3. 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
4. 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是 舍弃剩余的位还是利用,这是不确定的。
具体运用在网络协议中,感兴趣的同学可以了解了解。
总结:
     跟结构体相比,位段可以达到同样的效果,但是可以很好的节省空间,但是有跨平台的问题存在。

2.2 枚举

     就是将某种情况可能的结果列举在一起,在使用的时候,直接用情况所对的数据。

如:

12个月就能 一,一列举

性别也可以一, 一列举

运用如下:

enum Color//颜色
{
   RED=1,
   GREEN=2,
   BLUE=4
};
{} 中的内容是枚举类型的可能取值,也叫 枚举常量
    如果未初始化,默认从 0 开始,一次递增 1 ,当然在定义的时候也可以赋初值。(注:同#define 修饰常量一样,数据放在常量区,一旦定义就 不允许修改 。)
如:
enum  a
{
   c,   // 默认值为 0
   b,   //                1
   k = 200, //赋值为200
   z,        //  由前+ 1,为 201
   t          //  同理        202
};

2. 21枚举的优点

  • 为什么使用枚举?
    我们可以使用 #define 定义常量,为什么非要使用枚举?
枚举的优点:
  • 1. 增加代码的可读性和可维护性 (少几个#define)
  • 2. #define定义的标识符比较枚举类型检查,更加严谨。
(在c++里面就会对  枚举常量赋值时  对右侧数据进行枚举判断,是否是枚举成员)
  • 3. 防止了命名污染(可以通过枚举名进行限制范围,避免重命名)
  • 4. 便于调试             (可以调试,找到对应的值,不像#define定义,预编译时就替换了,不好溯源)
  • 5. 使用方便,一次可以定义多个常量

2.3 联合体

联合类型的定义
    联合也是一种特殊的自定义类型 这种类型定义的变量也包含一系列的成员,特征是这些成员 公用同一块 空间 (所以联合也叫共用体)。 比如:
#include<stdio.h>
union p1
{
    char i;
    int z;
}a;
int main()
{
    printf("%p\n", &(a.i));
    printf("%p\n", &(a.z));
    return 0;
}

 结果:

联合的特点
    联合的成员是共用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为
联合至少得有能力保存最大的那个成员)。
  • 联合体内存大小也要遵循结构体内存对齐原则。
    面试题:试想一下如何利用联合的特点判断机器的大端与小端
#include<stdio.h>
union p1
{
	char i;
	int z;
}a;
int main()
{
	a.i = 5;
	a.z = 1;
    a.i == 1 ? printf("小端") : printf("大端");
	return 0;
}

结果是小端,利用char  i 只能读取首字节数据,可以判断出int  z修改的值存在于首字节,所以机器为小端。

三. 结语

    本小节到这里就结束了,感谢小伙伴的浏览,如果有什么建议欢迎在评论区评论。

 

猜你喜欢

转载自blog.csdn.net/qq_72112924/article/details/130005582