【C语言】详解C语言自定义类型:结构体(含位段)、枚举、联合

目录

1 结构体

1.1 结构体的定义

1.2 结构体的声明

1.3 结构体特殊声明

1.4 结构体自引用

1.5 结构体变量的定义和初始化

1.6 结构体内存对齐

1.7 修改默认对齐数

1.8 结构体传参

 2 位段

2.1 位段的定义

2.2 位段的内存分配​​​​​​​

2.3 位段的跨平台问题

3 枚举

3.1 枚举类型的定义

3.2 枚举的优点

3.3 枚举的使用

4 联合(共用体)

4.1 联合类型的定义

4.2 联合的特点

4.3 联合大小的计算


我们知道,在C语言中有很多种类的数据类型,如char、int、double等。其实为了创造更多可能性,C语言是允许使用者自己创造一些类型的。而这些被允许创造的类型就叫做自定义类型

自定义类型分别有:结构体、枚举、联合体(共用体)

现在我们将一起来学习这几种自定义类型的特点以及使用方法。

1 结构体


1.1 结构体的定义

结构体是C语言中一个极其重要的知识点,结构体赋予了C语言能够描述复杂类型的能力。

假如我们要描述一个学生的信息,一个学生的信息大致有姓名、学号、性别、班级等等,那么我们就可以将这些不同类型的数据包含在结构体中,统一用结构体来维护这些信息。

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

1.2 结构体的声明

结构体声明的格式如下:

struct tag
{
    member-list;
}variable-list;

struct——关键字;

tag——结构体的标签名(即此结构体类型的名字);

member-list——结构体成员变量;

variable-list——变量列表(假如需要定义一个结构体变量就在此处声明,非必要)

注意:尾部的分号是不可以舍弃的,因为这是一个声明,相当于一条语句,必须以分号结尾

还是以描述一个学生为例,给出一个结构体声明:

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

1.3 结构体特殊声明

结构体声明可以是不完全声明,但是在使用的时候需要注意一些问题。

比如在结构体声明的时候,可以省略结构体标签(tag),即匿名结构体: 

struct Stu
{
    char name[20];
    int age;
    char sex[5];
    char id[20];
}s1;

使用匿名结构体的时候就需要注意,由于该结构体是没有名字的,所以想要定义一个该结构体类型的变量,必须在声明的时候将变量名一起声明。

再来看如下代码:

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

以上两个结构体在声明的时候都省略了结构体标签,那么在以上代码的基础上,下面这一条语句是否正确呢?

p=&x;

我们来编译看一看:

在这里我们会发现,编译器报了警告,原因是:

即使上面的两个声明中,结构体成员变量是完全相同的,但是编译器也会将其认为两个完全不相同的类型,所以上述语句是非法的。

1.4 结构体自引用

现在我们来像这样一个问题:可不可以在结构体内部包含一个类型为该结构体本身的成员呢?

答案是可以的,这就牵扯到了结构体自引用的问题。但是想要实现结构体自引用也是需要规范使用的。

如何对结构体进行自引用操作呢?我们先来看下面一个示例:

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

以上代码可行吗?

其实我们可以这样来思考,假设以上代码可行,那么这个结构体将会占用多少内存空间呢?

显然,从占用空间的角度来看,以上代码是矛盾的,我们先来想,结构体想要放下其中声明的数据,至少要有sizeof(struct Node)+int的大小,然而结构体本身的大小就是sizeof(struct Node),这是不符合常理的。

因此,为了正确自引用,应该使用指针,因为一个指针占用的内存大小是固定的4/8个字节,这一来从内存空间的角度就可行了。

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

1.5 结构体变量的定义和初始化

前面我们已经介绍了结构体类型的一些基础知识,有了这些知识以后就应该学习如何对结构体变量进行定义和初始化。

结构体变量的定义有两种方式:

1.声明类型的同时定义变量

struct Point
{
    int x;
    int y;
}p1;

2.在类型已声明的情况下直接定义

struct Point p2;

对于结构体变量的初始化,是在定义变量的同时赋初始值。

比如:

struct Point p3={0,1};
struct Stu
{
    char name[20];
    int age;
};
struct Stu s={"zhangsan",18};

可以看出,对于结构体变量的定义和初始化其实还是比较简单的,只需要注意赋初始值的时候肤质类型与结构体成员变量类型相匹配就行了。

1.6 结构体内存对齐

结构体的一些基本使用我们了解了,那么我们再来思考一个问题:怎么样计算一个结构体的大小呢?

先来看一个例子:

struct Node
{
	int a;
	char b;
	char c;
};
int main()
{
	printf("%d", sizeof(struct Node));
	return 0;
}

如果按照以往的思维,这里我们会认为:该结构体包含了一个int数据、两个char数据,它所占用的空间应该是6个字节。

但是实际运行起来:

 实际运行起来却发现是8个字节。

这里假如我们交换一下数据的顺序:

struct Node
{
	char b;
	int a;
	char c;
};
int main()
{
	printf("%d", sizeof(struct Node));
	return 0;
}

 会发现运行结果更不一样了

这是怎么一回事呢? 

想要搞清楚里面的原因,就得深刻探讨一下结构体方面一个极其重要的知识:结构体内存对齐。 

首先我们先来掌握一下结构体对其规则:

1.结构体的第一个成员永远放在偏移量为0的地址处。

2.从第二个成员开始,以后的每个成员都要对齐到某个对齐数的整数倍。

   而这个对齐数是——编译器默认的一个对齐数成员自身大小较小值

   (ps:vs环境下默认对齐数是8;  gcc环境下没有默认对齐数,因此在gcc下默认对齐数就是成员自身的大小)

3.当所有成员都存放好后,结构体的总大小为最大对齐数(每个成员都对应有一个最大对齐数)的整数倍。

   如果空间不足够,则浪费一部分空间来满足此条件。

4.如果嵌套了结构体,嵌套的结构体对齐到自己的最大对齐数的整数倍处,外层结构体的整体大小就是所有成员最大对齐数(含嵌套结构体的对齐数)的整数倍。

那我们就可以来分析一下上面两种情况:

对于第一种情况:

struct Node
{
	int a;
	char b;
	char c;
};
int main()
{
	printf("%d", sizeof(struct Node));
	return 0;
}

(橙色代表int,绿色、蓝色代表char,粉色代表浪费空间)

 这里a占4个字节,和默认对齐数8相比4更小、所以a的对齐数是4;

同理,b和c的默认对齐数都是1;

在依次存放完a、b、c之后,三个数据的最大对齐数是a的4,所以需要对齐到4的整数倍处,也就是8个字节,对应的就是偏移量为7的地址。

对于第二种情况:

struct Node
{
	char b;
	int a;
	char c;
};
int main()
{
	printf("%d", sizeof(struct Node));
	return 0;
}

 在存放好b以后,因为a的对齐数是4,需要对齐到4的整数倍地址处;

存放完所有数据后,三个变量中最大对齐数为4,整体大小需要对齐到4的整数倍,也就是12个字节,对应偏移量为11的地址处。

可能就在这里就会有疑惑了:那为什么会存在内存对齐呢?

在目前大部分的参考资料中都是这样说的:

1.平台原因(移植原因):

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

2.性能原因:

        数据结构(尤其是栈)应该尽可能的在自然边界上对齐。原因在于,为了访问未对其的内存,处理器要经过两次内存访问,而访问对齐的内存只需要进行一次访问。

举一个例子就明白了:

对于如下代码:

struct Node
{
    char c;
    int x;
};

假设在32位机器下(一次读取32个bite的数据,也就是四个字节) 

假设没有内存对齐,读取顺序是这样的

会发现如果想要读取x,会拆成两次读写,这样代码效率就会低很多,而且就容易出问题。

如果有内存对齐:

 不仅仅效率提升了很多,同时也使代码更加严谨。

1.7 修改默认对齐数

修改默认对齐数这个点非常的简单,通过#pragma这个预处理指令就可以做到。

设置默认对齐数为x的语句:

#pragma pack(x)

修改默认对齐数在什么情境下适用呢?

有些时候,当我们发现在当前默认对齐数下,结构的对齐方式不太合适(比如说浪费空间过多等等),那我们就可以自己更改默认对齐数,使对齐方式较为合理。

但更改的时候也不能胡来,比如将其设置为3、11等等,这样做反而会使对齐方式更加不良。最好的办法是尽量修改为偶数(以2、4的倍数为最佳)

1.8 结构体传参

了解过函数栈帧相关知识就知道,函数传参的时候,参数是需要压栈的,这样做会导致时间和空间上的系统开销。

结构体的大小一般都比较大,在这时如果将结构体对象作为参数传递给函数,参数压栈的系统开销是比较大的,这样会导致性能和效率的下降。

因此,在结构体传参的时候,最好是传结构体的地址,这样既能方便对结构体对象进行维护,也降低了压栈的系统开销。

 2 位段


2.1 位段的定义

结构体具有实现位段的能力,什么是位段呢?

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

A就是一个位段类型。

这里的位,指的就是比特位,上面a、b、c后面跟的数字,就是a、b、c分别占用比特位的大小。

位段在一定程度上可以节省空间,比如以上代码,如果不使用位段,存放四个整形数据,就需要占用16个字节的空间,也就是128个比特位。

然而如果使用了位段,我们来跑一下上面代码的大小

 我们发现只占用了8个字节,也就是64个比特位,节省了一半的空间。

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

  1. 位段的成员必须是int、unsigned int 、signed int或者是char(char属于整形家族)。
  2. 位段的成员名后边有一个冒号和一个数字。

2.2 位段的内存分配

了解了位段的基本概念,那么如何来计算一个位段的大小呢?

首先需要了解以下规则

  1. 位段的成员必须是int、unsigned int 、signed int或者是char(char属于整形家族)。
  2. 位段的空间上是按照需要一次开辟4个字节(int)或者1个字节(char)的方式来开辟的。
  3. 位段涉及很多不确定因素,同时位段是不跨平台的,因此注意可移植程序应该避免使用位段。

还是对这一段代码分析一下:

2.3 位段的跨平台问题

 我们在上面计算出了一个位段的大小,但是有一个问题:

在存放d之前,还剩下15个字节,在开辟完新的空间之后,是将d的一部分继续存放在这15个字节,剩余部分存放进新开辟空间,浪费后面的17个字节呢,还是浪费掉这15个空间,将d全部存放在新开辟空间内呢?

其实,在C语言标准中,并没有对这种情况进行明确规定,这也是使用位段时的一大问题。

位段跨平台问题主要有以下四点:

  1. int位段被当成有符号数还是无符号数是不确定的。
  2. 位段中最大位的数目不能确定。(16位机器最大16,3位机器最大32,写成27,在16位机器就会出问题)。
  3. 位段中的成员在内存中从左向右分配,还是从右向左分配尚未定义。
  4. 当一个结构包含两个位段,第二个位段成员比较大,无法容纳第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的。

我们来做个总结:

跟结构相比,位段可以达到同样的效果,且可以很好地节省空间,但是具有跨平台的问题存在。所以需要具体使用具体分析。

3 枚举


 枚举,就是一一列举的意思。在某些特定的情境中,使用枚举更加方便。

比如我们想要描述一周当中的从星期一到星期天的每一天,就可以使用枚举。

或者我们想描述一个人的性别(男、女... ...)也可以使用枚举来一一列举。

3.1 枚举类型的定义

就几个常见的例子来说明:

enum Day
{
	Mon,
	Tue,
	Wed,
	Thu,
	Fri,
	Sat,
	Sun
};

enum Sex
{
	Male,
	Female,
	X1,
	X2,
	X3
};

以上定义的enum Day和enum Sex都是枚举类型。

大括号{}中的内容是枚举类型的可能取值,也叫作枚举常量。

枚举常量都是有值的,默认第一个常量为0,之后依次递增1。

也可以在定义的时候就对其进行赋初始值,从最后一次赋值的常量开始,之后的常量的值依次递增1。

如: 

enum Day
{
	Mon,
	Tue,
	Wed=10,
	Thu,
	Fri,
	Sat=20,
	Sun
};

其中Mon=0,Tue=1,Wed=10,Thu=11,Fri=12,Sat=20,Sun=21

3.2 枚举的优点

也许我们会想:定义常量不是可以用#define吗?为什么要还要使用枚举呢?

这里介绍一下枚举的优点:

  1. 增加代码的可读性和可维护性;
  2. 和#define定义的标识符比较枚举有类型检查,更加严谨;
  3. 便于调试;
  4. 使用方便,一次可以定义多个常量

3.3 枚举的使用

以下面的代码来理解枚举的使用:

enum Day
{
	Mon,
	Tue,
	Wed = 10,
	Thu,
	Fri,
	Sat = 20,
	Sun
};
int main()
{
	enum Day d = Thu;/对枚举变量赋值只能用枚举常量,否则会出现内存的差异
	printf("%d\n", d);
	d = 15;
	printf("%d\n", Thu);
	printf("%d", d);
	return 0;
}

运行结果是这样的: 

 

说明通过改变枚举变量,并不能改变枚举常量的值,这样也能使程序结构更加严谨,减少错误的发生。

4 联合(共用体)


4.1 联合类型的定义

联合也是一种特殊的自定义类型。

联合定义的变量同样包含一系列成员,其特征顾名思义,就是这些成员公用同一块空间(所以联合也叫共用体)。

联合类型的声明和定义如下:

//联合变量的声明
union Un
{
	char c;
	int i;
};

//联合变量的定义
union Un un;

4.2 联合的特点

既然联合成员是共用同一块内存空间的,那么我们可以想到,一个联合的大小,至少是其最大成员的大小,只有这样联合才有能力存储所有成员。

对于如下代码: 

union Un
{
	char c;
	int i;
};

union Un un;
int main()
{
	printf("%p\n", &(un.i));
	printf("%p\n", &(un.c));
	return 0;
}

我们会发现结果是这样的: 

联合成员的地址是相同的,这就说明了联合成员是共用同一块内存空间的。

4.3 联合大小的计算

联合体大小计算法则:

  1. 联合体大小至少是最大成员的大小;
  2. 当最大成员大小不是最大对齐数整数倍的时候,就要对齐到最大对齐数的整数倍。

我们来看几个例子:

union Un1
{
	char c[5];
	int i;
};
union Un2
{
	short c[7];
	int i;
};
//联合变量的定义
union Un1 un1;

union Un2 un2;
int main()
{
	printf("%d\n", sizeof(un1));
	printf("%d\n", sizeof(un2));

	return 0;
}

对于un1,其最大成员的大小是c[5],占用了5个字节,所以大小至少是5。

但是c的最大对齐数是1,i最大对齐数是4,需要对齐到4的整数倍处,所以大小是8。

对于un2,其最大成员大小是c[7],占用了14个字节,所以大小至少是14。

但是c的最大对齐数是2,i最大对齐数是4,需要对齐到4的整数倍处,所以大小是16。

我们来验证一下:

结果符合我们的分析。


至此,关于C语言自定义类型就已介绍完毕。

猜你喜欢

转载自blog.csdn.net/fbzhl/article/details/130164854