【C语言进阶】自定义类型:结构体、枚举、联合

前言: 

     c语言本身携带了一些内置类型(char、short、int、long、float等)可以直接使用的类型,与内置类型相对的一种,叫自定义类型。生活中一些类型不能用内置类型进行简单的表示的(如:复杂对象书、人等),所以C语言就给用户一定权力,可以自己构造类型。这就是自         定义类型,自定义类型有结构体(struct)、枚举(enum)、联合体(union)。

目录

前言: 

1、结构体

1.1、结构体的基础知识

1.2、结构的声明 

1.3、特殊的声明

1.4、结构体的自引用

1.5、结构体变量的初始化

1.6、结构体内存对齐

1.6.1、【问题】计算结构体的大小

1.6.2、【解决方法】

1.7、为什么存在内存对齐?

1.8、修改对齐数

1.9、结构体传参

2、位段

2.1、什么是位段

2.2、位段的内存分配

2.3、位段的跨平台问题 

 3、枚举

3.1、枚举类型的定义

3.2、枚举类型使用

给枚举常量赋一个初始值

3.3、枚举的优点

4、联合体(共用体)

4.1、联合体类型的定义

4.2、联合体的特点 

4.3、联合体的使用

4.4、联合体大小的计算


1、结构体

1.1、结构体的基础知识

结构是一些值得集合,这些值称为成员变量,结构的每个成员可以是不同类型的变量。C语言提供了关键字struct来标识所定义的结构体类型。

1.2、结构的声明 

图解: 

 代码演示:描述一本书

//书名+作者+定价+书号
struct Book//struct是结构体关键字,Book是这个结构体标签,结构体标签是根据自己的需要定义的
{
    char book_name[20];
    char author[20];
    int price;
    char id[15];
};//结构体大括号后的;不能丢

在上述结构体类型中,并没有variable-list,在这里就引出了一个知识点,结构体变量的创建

结构体变量的创建有两种情况:

1>、像图解中variable-list是结构体成员变量列表,可以在这里创建变量,在这里创建的变量          是全局变量。

2>、是结构体类型构造好之后,需要使用到该结构体类型时,再创建变量。这种变量的创建          是局部变量。

//1>、情景一
struct Book
{
    char book_name[20];
    char author[20];
    int price;
    char id[15];
}a1,a2,a3;//这里创建的变量是全局变量,因为这里的变量在所有的大括号之外
//在这里创建变量是可选的,需要时创建,不需要可以不创建

//2>、情景二
struct Book
{
    char book_name[20];
    char author[20];
    int price;
    char id[15];
};

int main()
{
    struct Book a1;//局部变量
    struct Book a2;//局部变量
    return 0;
}

注释:构造结构体类型不开辟空间,只有在创建结构体变量时,才开辟空间。


1.3、特殊的声明

   1、匿名结构体类型(在声明结构的时候,可以不完全的声明。)

struct     //当把结构体标签去掉,若在后边不直接创建变量,在C语言的语法中是不通的。
{      //像要构造的结构体可以使用,就需要在后边直接创建变量,构成匿名结构体类型
    char book_name[20];
    char author[20];
    int price;
    char id[15];
}a1,a2,a3;//匿名结构体类型

注释:匿名结构体类型在创建变量的时候只能使用一次,一次用完之后就不能再创建变量。

   2、匿名结构体类型指针和匿名结构体

struct     
{
    char book_name[20];
    char author[20];
    int price;
    char id[15];
}a1 = {"cyuyan","xxxx",20,"1234"};


struct     
{
    char book_name[20];
    char author[20];
    int price;
    char id[15];
}*p;//在这里创建的变量,为结构体类型的指针变量,
//这个指针变量并不能指向上边的结构体变量

int main()
{
    p = &a1;
    return 0;
}

  虽然类型成员名相同,但是在编译器看来是两个不同的类型。


1.4、结构体的自引用

数据在内存中的存储结构有

1>、线性数据结构:顺序表,链表

2>、树形数据结构:二叉树

 在这里浅浅的了解一下以链表的方式,一组数据在内存中的存储方式

 通过找到结点一,再通过指针,指向下一个结点,依次找到在内存中存储的数据。

这里可以通过结构体的自引用来实现。

struct Node
{
    int data;
    struct Node* next;//通过指针找到下一个结点
};

在结构体创建好之后,每次使用该结构体时都要输入struct Node ,如若感到麻烦可以通过typedef重命名结构体

typedef struct Node
{
    int data;
    struct Node* next;
}Node(重命名);//这里的Node表示struct Node(结构体类型),不能认为是结构体变量。

int main()
{
    struct Node n;//创建结点n
    Node n;//这里的Node就相当于struct Node
    return 0;
}

在使用该结构体时只有重命名之后才能使用Node。


1.5、结构体变量的初始化

1>、定义变量时初始化

//1>、情景一
struct book
{
    char name[10];
    int price;
}s1={"cyuyan",20};


//2>、情景二
struct book
{
    char name[10];
    int price;
};

int main()
{
    struct book s1={"cyuyan",20};
    return 0;
}

2>、结构体中包含结构体成员的初始化

1>、情景一
struct Stu
{
    char name[20];
    int age;
    char id[12];
};

struct Book
{
    char book_name[20];
    char author[20];
    int price;
    char id[15];
    struct Stu s;
}sb3 = {"C语言","xxxx",54,"PG1001",{"lisi",30,"20220101"}};
2>、情景二
//与第一种的情景二相似

1.6、结构体内存对齐

1.6.1、【问题】计算结构体的大小

#include<stdio.h>
struct S1
{
	char c1;
	int i;
	char c2;
};
struct S2
{
	char c1;
	char c2;
    int i;
};

int main()
{
	struct S1 s1;
	printf("%d\n", sizeof(struct S1));//计算结构体(struct S1)类型的大小
    printf("%d\n", sizeof(struct S2));
	return 0;
}

 结构体struct S1和struct S2的成员只是位置不相同,为什么结构体大小就不相同。


1.6.2、【解决方法】

1、 在解决这个问题之前先来了解一个宏

 offsetof ( type , member )

1>、功能

offsetof  用来计算结构体成员对于起始位置的偏移量

2>、参数

type    告诉结构体成员类型(告诉offsetof是那个结构体类型)

member   类型的成员名

3>、 头文件为<stddef.h>

2、再了解结构体内存对齐规则

1、第一个成员在于结构体变量偏移量为0的地址处。

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

    对齐数 = 编辑器默认的一个对齐数与该成员大小的较小值。

     vs编译器默认对齐数为8;linux环境下默认不设对齐数(对齐数是结构体的自身大小)。

3、结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍

4、如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

1、举例一 

1>、 代码演示:

#include<stdio.h>
#include<stddef.h>
struct S1
{
	char c1;
	int i;
	char c2;
};
int main()
{
	printf("%d\n", offsetof(struct S1, c1));//计算结构体struct S1成员中c1的偏移量
	printf("%d\n", offsetof(struct S1, i));
	printf("%d\n", offsetof(struct S1, c2));
	return 0;
}

 2>、画图解释

2、举例二:结构体中嵌套结构体

1>、代码演示:

#include<stdio.h>
#include<stddef.h>
struct S1
{
	char c1;
	int i;
	char c2;
};
struct S2
{
	char c1;
	struct S1 s1;
	int i;
};

int main()
{
	printf("%d\n", sizeof(struct S1));
	printf("%d\n", sizeof(struct S2));
    return 0;
}

 2>、画图解释


1.7、为什么存在内存对齐?

大部分参考资料给了两个点

1、平台原因(移植原因)

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

2、性能原因

      数据结构(尤其是栈)应该尽可能地在自然边界上对齐。

      原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅          需要一次访问。

 图例讲解:

总结:

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

那在设计结构体的时候,我们既要满足对齐,又要节省空间,就应该让占用空间小的成员尽量集中在一起。

 举例:

#include<stdio.h>
struct S1
{
	char c1;
	int i;
	char c2;//12
};
struct S2
{
	char c1;
	char c2;
    int i;//8
};
相较而言,第二种构建方式更节省空间。

1.8、修改对齐数

  •   更改默认对齐数
#pragma pack(1)//将默认对齐数修改为1
  •   恢复默认对齐数
#pragma pack()//取消设置的默认对齐数,恢复到编辑器默认的对齐数

一般在设置默认对齐数时,是以2^x来设置,一般来说都是偶数,很少设置为奇数,因为计算机在读取时,要不读取4个字节,要不读取8个字节。

1.9、结构体传参

  • 结构体传参
struct S
{
    int data[1000];
    int num;
};
void print1(struct S s)
{
    printf("%d %d %d %d\n",s.data[0],s.data[1],s.data[2],s.num);
}
int main()
{
    struct S ss = {{1,2,3,4,5},100};
    print1(ss);
}
  • 结构体地址传参
struct S
{
    int data[1000];
    int num;
};
void print(struct S* ps)
{
    printf("%d %d %d %d\n",(*ps).data[0],(*ps).data[1],(*ps).data[2],(*ps).num);
    printf("%d %d %d %d\n",ps->data[0],ps->data[1],ps->data[2],ps->num);
}
int main()
{
    struct S ss = {{1,2,3,4,5},100};
    print2(&ss);
}

上面两种传参方式,那个更好?

      函数传参时,参数是需要压栈的,会有空间上的系统开销。

      如果传递一个结构体对象的时候,结构体过大,参数压栈的系数开销比较大,所以会导          致性能的下降。

结论:

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


2、位段

2.1、什么是位段

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

1、位段的成员类型必须是int、unsigned int或signde int。

2、位段的成员后边有一个冒号和一个数字。 

下面俩看一个代码:

提示:位段其中的位表示的是二进制位。

#include<stdio.h>
struct A
{
    int _a:2;//:2表示成员_a只需要2个比特位
    int _b:5;//:5表示成员_b只需要5个比特位
    int _c:10;//:10表示成员_c只需要10个比特位
    int _d:30;//:30表示成员_d只需要30个比特位
};
int main()
{
    printf("%d\n",sizeof(struct A));
    return 0;
}

 

 _a占2个比特位,_b占5个比特位,_c占10个比特位,_d占30个比特位,将其相加位47个比特位,1字节 = 8bit,那么占6个字节的大小。与编译器显示结果不同,那么位段是有自己的内存分配规则。接下来了解一下位段的内存如何分配。


2.2、位段的内存分配

1、位段的成员可以是 int ,  unsigned  int , signed int 或者是char(属于整型家族) 类型

2、位段的空间上是按照需要以4个字节 (int) 或者1个字节 (char) 的方式来开辟的。

3、位段涉及很多不确定因素,位段是不跨平台的,注意可移植的程序应该避免使用位段。

 下面我们通一段代码来了解位段的内存如何分配

struct S
{
	char a : 3;//位段S的成员类型是char,先来申请1个字节的大小,
	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;
	return 0;
}

 在vs2019的环境下

 上述代码中位段s,先开辟一个字节的空间(8bit),位段S规定成员a占用3bit空间,当a= 10 时,二进制为1010只取三位,成员b占4个字节的位段,到这时第一次申请的空间只剩余一个bit的大小,成员c占5个bit的空间,剩余空间不满足,第二次开辟一个字节的空间,在一个字节的空间中,从低位到高位存储,剩余空间抛弃,重新开辟新的空间,按照这样的规律,将所有的成员都放下。

 注释:由于位段的不确定性,位段不支持跨平台使用


2.3、位段的跨平台问题 

1、int位段被当成有符号数还是无符号数是不确定的。

2、位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机器会出问题)。

3、位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。

4、当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的。

总结:

跟结构体相比,位段可以达到同样的效果,但是可以很好的节省空间,但是有跨平台的问题存在。


 3、枚举

枚举顾名思义就是一一列举。

把可能的取值一一列举。

比如现实生活中:

一周星期一到星期日是有限的7天,可以一一列举。

3.1、枚举类型的定义


3.2、枚举类型使用

#include<stdio.h>
enum Color
{
//枚举的这些可能取值就是常量————枚举常量
//枚举的可能取值都是有值的。
    RED,//在这里枚举的默认值为0;他的特点就是,不论起始值是多少,后面都是增加一的。
    GREEN,
    BLUE
};
int main()
{
    enum Color color = BLUE;//创建枚举类型的变量color
    //RED = 2;这句代码是运行不了的,因为RED是常量,常量不可修改。
    printf("%d\n",RED);
    printf("%d\n",GREEN);
    printf("%d\n",BLUE);
    return 0;
    
}

 枚举常量的值分别是

给枚举常量赋一个初始值

//1.第一种赋值方式
enum Color
{
    RED = 5,//即使RED是常量,但是在刚开始是可以赋一个值的,可以凭自己的需要赋值。
    GREEN = 9,
    BLUE = 10
};//这样赋值是可行的

//2.第二种赋值方式
enum Color
{
    RED = 5,//给第一个可能取值赋值,不论起始值是多少,后面的都加一
    GREEN ,
    BLUE 
};

3.3、枚举的优点

我们可以使用#define定义常量,为什么非要使用枚举?

枚举的优点:

1、增加代码的可读性和可维护性

2、和#define定义的表示符比较枚举有类型检查,更加严谨。

3、防止了命名污染(封装)

4、便于调试

5、便于使用,一次可以定义多个变量

 代码演示:将通讯录代码中的菜单通过枚举进行改造,可以使写的时候更方便也可以增强代码的                       可读性和可维护性。

//1.增加代码的可读性和可维护性
void menu()
{
    printf("************************************\n");
    printf("******    1.add      2.del     *****\n");
    printf("******    3.search   4.modify  *****\n");
    printf("******    5.show     6.sort    *****\n");
    printf("******    0.exit               *****\n");
    printf("************************************\n");
}

enum Option//将上述菜单中的操作全部枚举出来
{
    EXIT,
    ADD,
    DEL,
    SEARCH,
    MODIFY,
    SHOW,
    SORT
};
在接下来的代码中,实现上述功能是不需要再看,相应的共能对应的序号,这样就增加了代码的可维护性和可读性


4、联合体(共用体)

联合体也是一种特殊的自定义类型                                                                                                      这种类型定义的变量也包含了一系列的成员,特征是这些成员公用同一块空间(所以联合也叫共用体)。union为联合体的关键字

4.1、联合体类型的定义

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

4.2、联合体的特点 

联合体的成员是共用同一块内存空间,这样一个联合体变量的大小,至少是最大成员的大小(因为联合至少得有能力保存最大的那个成员)。

#include<stdio.h>
union Un
{
	char c;
	int i;
	double d;
};
int main()
{
	union Un un;//创建联合体变量
	printf("%p\n",&un);
    printf("%p\n",&(un.c));
    printf("%p\n",&(un.i));
    printf("%p\n",&(un.d));
	return 0;
}

 看到上述代码运行的结果,四个地址都是相同的,这就是联合体共用同一块空间。在取出联合体地址时,取出的都是第一个字节的地址,与结构体不同,结构体成员都有自己的空间。

画图理解:


4.3、联合体的使用

联合体的一个使用场景:

在前面讲过判断当前机器的大小段字节序,在这里用联合体的知识点进行改造。

int check_sys()
{
	union Un
	{
		char c;
		int i;
	}u;
	u.i = 1;
	return u.c;
}
int main()
{
	int ret = check_sys();
	if (ret == 1)
		printf("小端\n");
	else
		printf("大端\n");
	return 0;
}

注释:通过给联合体成员i赋值,在内存空间中i占4个字节空间大小,再输出c,在同一块内存空间中,c只占一个内存空间的大小,所以通过输出c的的结果1或0来判断当前机器的大小端字节序。

图片解释:


4.4、联合体大小的计算

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


union Un
{
	char arr[5];//5   1  8  1 虽然是数组,但是成员大小是按char来算的
	int i;//4  8  4
//共用体的最大对齐数为4;成员占用最大空间为5,所以共用体占用空间为最大对齐数的整数倍,至少是最大 
  //成员的大小.
};
int main()
{
	printf("%d\n", sizeof(union Un));//结果为8
	return 0;
}

 

  • 联合体大小至少为最大成员的大小我为5 
  • 成员一虽然为数组,但是计算对齐数的时候还是以char来计算,本身大小为1,默认对齐数为8,对齐数为1
  • int自身大小为4,默认对齐数为8,对齐数为1
  • 显然5不是最大对齐数4的整数倍,所以输出结果为8。

 在看一个例子

union Un
{
	short arr[7];//14 2  8  2  
	int i;//4  8  4   
};
int main()
{
	printf("%d\n", sizeof(union Un));//输出结果为16
	return 0;
}

 

注释:

第一个成员的对齐师叔为2,占用空间为14

第二个成员的对齐数为4,最大成员为14,不是最大对齐数的整数倍,所以共用体的空间大小为16

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值