C语言——结构体、联合、枚举

C语言中自定义类型

前言

C语言中有内置类型和自定义类型,内置类型就像int 、double等等,其自带的一些类型,但有时候这些类型满足不了一些特定的要求,所以C语言也提供了一些自定义类型:结构体、联合、枚举


结构体

c 复制代码
struct tag
{//结构体成员变量
 member-list;
}variable-list;//结构体变量名称
//可以在使用时候创建,也可以在创建结构体时创建

例如:创建一个学生的结构体

c 复制代码
struct Stu {
	char name[20];
	int age;
	char sex[5];
}; //这里的分号不可以省略

这上面就相当于一个结构体的声明,创建了一个结构体类型,那如何使用呢?

结构体变量的创建和初始化

可以创建的同时初始化

c 复制代码
struct Stu {
	char name[20];
	int age;
	char sex[5];
};

int main() 
{
//创建结构体类型变量并初始化
	struct Stu s = { "张三",18,"male" };
	struct Stu s1 = {"lisi",18,"male"};
}
c 复制代码
#include<stdio.h>
struct Stu {
	char name[20];
	int age;
	char sex[5];
};

int main() 
{
//创建一个结构体变量并初始化
	struct Stu s = { "张三",18,"male" };
	printf("%s\n", s.name);
	printf("%d\n", s.age);
	printf("%s\n", s.sex);

	//可以使用指针来指向这个结构体,来进行访问
	struct Stu* p = &s;
	printf("%d\n", (*p).age);
	printf("%s\n", p->name);
	
	return 0;
}
复制代码
这里使用变量名.结构体成员来访问
如果是一个指针指向一个结构体的话,有两种访问形式
变量名->成员变量
(*变量名).结构体成员

运行结果如下

结构体传参

结构体传参分为传值调用和传址调用

c 复制代码
#include<stdio.h>
struct Stu {
	int age;
	char name[20];
};
//传值
void print1(struct Stu p)
{
	printf("%d\n", p.age);
}
//传址
void print2(struct Stu* p)
{
	printf("%d\n", (*p).age);
}
int main()
{
	struct Stu s = { 18,"sansan" };
	print1(s);
	print2(&s);
	return 0;
}

运行结果如下

传址调用可以修改结构体变量的值,但是传值调用则不可以

c 复制代码
#include<stdio.h>
struct Stu {
	int age;
	char name[20];
};
//传值
void print1(struct Stu p)
{
	p.age = 19;
}
//传址
void print2(struct Stu* p)
{
	(*p).age = 20;
}
int main()
{
	struct Stu s = { 18,"sansan" };
	print1(s);
	printf("调用传值调用后age:%d\n", s.age);
	print2(&s);
	printf("调用传址调用后age:%d\n", s.age);

	return 0;
}

这里我们通过传值调用和传地址调用,发现这里的传地址调用可以修改结构体的变量的值 ,但是传值调用则不可以

结构体在进行传参的时候,传递地址比较好,因为如果传值调用的话,形参是实参的一份临时拷贝,如果这个结构体变量非常大,这可能在是将和空间上的开销比较大,所以传地址调用更好一点

结构体内存对齐(如何存储)

上面我们已经对结构体有所了解了,但是这有个新问题,结构体存储的时候是占多少字节呢?这就引出了:结构体内存对齐

对齐规则

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

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

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

-VS 编译器中默认的值为 8

-Linux系统中gcc没有默认对⻬数,对⻬数就是成员⾃⾝的⼤⼩

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

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

也就是要考虑结构体变量以及嵌套结构体变量其中的最大对齐数

我们就以下面这两个结构体来举例

c 复制代码
#include<stdio.h>
struct S1
{
	char c1;
	int i;
	char c2;
};
struct S2
{
	char c1;
	char c2;
	int i;
};
int main()
{
	//计算S1与S2分别的字节大小
	printf("%zd\n", sizeof(struct S1));
	printf("%zd\n", sizeof(struct S2));

	return 0;
}

运行结果如下

从运行结果我们发现一个问题这里的结构体变量的大小,并不是简单的成员变量所占字节数相加 ,而是经过内存对齐 得出的结果

S1 和 S2结构体带下分析如下


这里的两个结构体S1大小为12,S2大小8

嵌套类型的结构体总大小计算

c 复制代码
#include<stdio.h>
struct S3
{
	double d;
	char c;
	int i;
};

//结构体嵌套问题 
struct S4
{
	char c1;
	struct S3 s3;
	double d;
};
int main()
{
	printf("%zd\n", sizeof(struct S3));
	printf("%zd\n", sizeof(struct S4));
	return 0;
}

这里我们在VS2022这个编译器下,分析的结果是结构体S3的总大小是16,S4的总大小是32

运行结果如下

和我们推测一样

为什么要有这种对齐规则呢

复制代码
1.平台原因(硬件原因)
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,
否则出现硬件异常
2.性能原因
数据结构(尤其是栈)应该尽可能地在⾃然边界上对⻬,原因是这种访问需要访问两次,假设一个编译器只能8个地
地址进行存储,这样我们每次保证存储是8的倍数,这样就方便访问,提高效率
3.我们可以发现上面结构体就是按照对齐规则存储,这是为了快捷访问,相当于牺牲了一部分空间来换取时间

但是我们可以减少浪费

就像上面的两个结构体S1和S2,虽然成员变量的一样,但位置不同结果也不同,我们可以让占用小的空间尽量集中一起,这样可以节省空间

我们这里的VS2022有自己对齐规则,我们可以修改其 默认的对齐数,来改变其大小
#pragma 这个预处理指令,可以改变编译器的默认对⻬数

c 复制代码
#include<stdio.h>
#pragma pack(1)//设置默认对⻬数为1 
struct S
{
	char c1;
	int i;
	char c2;
};

int main()
{
	//输出的结果是什么? 
	printf("%zd\n", sizeof(struct S));
	return 0;
}

我们如果按照VS编译器的话,这里的大小是12 ,但是我们这里设置默认对齐数为1,也就是每个变量是紧挨着存储,所以这里大小是其相加也就是6

联合体(共用体)

联合体创建和初始化

联合体和结构体类似,都是由多个成员组成,这些成员可以是不同类型

但是与结构体不同的是,联合体是所有成员共用同一块内存 ,所以联合体也叫做共用体

c 复制代码
union tag
{//成员变量
 member-list;
}variable-list;

创建了一个共用体类型

c 复制代码
#include<stdio.h>
union Un 
{
	char c1;
	int i;
	char c2;
};

共用体创建和初始化

以及共用体是所有成员占用一块内存吗?我们以下面这个代码来举例

c 复制代码
union Un 
{
	char c1;
	int i;
	char c2;
};
int main()
{
	union Un un = { 0 };
	printf("%zd\n", sizeof(un));
	return 0;
}

运行结果如下

从结果上可以看出,它并不是连续存储,也不像结构体那样根据对齐原则存储,那他是如何存储的呢
上面的联合体的大小是4个字节,因为这里的最大成员变量是int 占4个字节,并且是最大对齐的数4的整数倍

联合体大小(如何存储)

联合体大小计算

复制代码
联合的⼤⼩⾄少是最⼤成员的⼤⼩。
当最⼤成员⼤⼩不是最⼤对⻬数的整数倍的时候,就要对⻬到最⼤对⻬数的整数倍

那我们来看下面的代码

c 复制代码
#include<stdio.h>
union Un1
{
	char c[5];
	int i;
};
union Un2
{
	short c[7];
	int i;
};
int main()
{
	//下⾯输出的结果是什么? 
	//最大的是5,但又要是最大对齐数4的整数倍数,所以这里是8
	printf("%zd\n", sizeof(union Un1));
	//最大是14,但又要是最大对齐数4的整数倍,所以结果是16
	printf("%zd\n", sizeof(union Un2));
	return 0;
}

运行结果如下

他们真的是占用同一块内存吗?

c 复制代码
#include<stdio.h>
union Un 
{
	char c1;
	int i;
	char c2;
};
int main()
{
	union Un un = { 0 };
	//%p是打印地址的,我们看他们是不是真的存储在同一内存
	printf("%p\n", &(un.c1));
	printf("%p\n", &(un.i));
	printf("%p\n", &(un.c2));
	return 0;
}

运行结果如下

从这个结果可以看出,共用体它们所占用的是同一个内存

这里如果修改一个成员数值有成员的结果就会改变

c 复制代码
#include<stdio.h>
union Un 
{
	char c1;
	int i;
	char c2;
};
int main()
{
	union Un un = { 0 };
	//%p是打印地址的,我们看他们是不是真的存储在同一内存
	un.i = 200;
	printf("%d\n", un.c1);
	printf("%d\n", un.c2);
	printf("%d\n", un.i);
	return 0;
}

char类型的范围是-128 ~ 127,如果超过范围结果是怎样呢

虽然它们占用同一块内存,但所占的字节数不同,这也导致结果不一样,就像这里的char只会占1个字节8个比特位,而int占4个字节,32比特位,所以这里的char类型打印的和int类型数值不同

枚举类型

枚举顾名思义就是将其成员一一列举

就像生活中一年有12个月,一周有7天一样可以一一列举

枚举类型创建

c 复制代码
enum Day//星期
{
	Mon,
	Tues,
	Wed,
	Thur,
	Fri,
	Sat,
	Sun
};
enum Color//三颜⾊
{
	Red,
	Green,
	Blue
};

这里的enum Day 和 enum Color都是枚举类型

{ }下都是可能取值,枚举常量

这些枚举常量都是从0开始,依次增加当然我们也可以给它赋值

枚举类型初始化

默认初始化

我们知道枚举常量都是从0开始,我们那上面的Color枚举来举例

c 复制代码
#include<stdio.h>
enum Color//三颜⾊
{
	Red,//默认存储的是0
	Green,//1
	Blue//2
};
int main()
{
	//创建一个c来接收Red
	enum Color c = Red;
	printf("%d\n", c);//0

	printf("%d\n", Red);//0
	printf("%d\n", Green);//1
	printf("%d\n",Blue);//2
	return 0;
}

运行结果如下

复制代码
从结果中可以看出,枚举常量是都是从0开始的,依次增加,并且创建的变量也是存储其常量值

为什么我们这里是说这是枚举常量

这里我们给枚举常量进行修改,发现是不可以的,因为常量值是不可以修改的,枚举常量是常量

就地初始化
全部初始化

c 复制代码
#include<stdio.h>
enum Color//三颜⾊
{
	Red = 1,
	Green = 3,
	Blue = 7
};
int main()
{
	printf("%d\n", Red);
	printf("%d\n", Green);
	printf("%d\n",Blue);
	return 0;
}

这里我们对Color这个枚举中的枚举常量全部都初始化了,但是不可以在枚举类型外部修改

运行结果如下

那如果我们只是部分初始化

c 复制代码
#include<stdio.h>
enum Color//三颜⾊
{
	Red = 1,
	Green,
	Blue
};
int main()
{
	printf("%d\n", Red);
	printf("%d\n", Green);
	printf("%d\n",Blue);
	return 0;
}

这里原本第一个是从0开始的,但是我们初始化为1,但是依旧满足依次增加的原则,所以结果为

那我们只修改中间的呢,依然满足上面依次增加的规则,如果初始化的话,按照初始化的值

就像下面这个,我们只初始化了前面两个枚举常量,那第三个常量值就是第二个常量值+1

枚举的优点(相较于define)

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

就像上面的代码,我们用#define来写

c 复制代码
#include<stdio.h>
#define Red 1
#define Green 2
#define Blue 3
int main()
{
	printf("%d\n", Red);
	printf("%d\n", Green);
	printf("%d\n", Blue);
	return 0;
}

运行结果如下,可以生成与枚举类型相同的结果

我们可以发现这里的#define每次只能定义一个,而枚举类型一个可以定义多个对象

复制代码
枚举的优点
1.可以增强代码的可读性 ,因为define只是简单的代码替换,无法调试观察
2.和#define使用枚举比较严谨
3.方便使用,可以连续创建多个常量,而#define依次只能定义一个

到这里就结束了,希望大家有所帮助,欲知后事如何,请听下回分解

相关推荐
码上淘金3 小时前
【Python】Python常用控制结构详解:条件判断、遍历与循环控制
开发语言·python
Brilliant Nemo3 小时前
四、SpringMVC实战:构建高效表述层框架
开发语言·python
格林威5 小时前
Baumer工业相机堡盟工业相机的工业视觉中为什么偏爱“黑白相机”
开发语言·c++·人工智能·数码相机·计算机视觉
橙子199110165 小时前
在 Kotlin 中什么是委托属性,简要说说其使用场景和原理
android·开发语言·kotlin
androidwork5 小时前
Kotlin Android LeakCanary内存泄漏检测实战
android·开发语言·kotlin
学地理的小胖砸5 小时前
【Python 基础语法】
开发语言·python
IT专业服务商6 小时前
联想 SR550 服务器,配置 RAID 5教程!
运维·服务器·windows·microsoft·硬件架构
海尔辛6 小时前
学习黑客5 分钟小白弄懂Windows Desktop GUI
windows·学习
gushansanren6 小时前
基于WSL用MSVC编译ffmpeg7.1
windows·ffmpeg
DanB247 小时前
Java笔记4
java·开发语言·笔记