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依次只能定义一个

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

相关推荐
字节王德发1 分钟前
如何用Python和Selenium实现表单的自动填充与提交?
开发语言·python·selenium
genispan1 小时前
python基础8 单元测试
开发语言·python·单元测试
钢铁男儿5 小时前
Python 生成数据(随机漫步)
开发语言·python·信息可视化
正经教主5 小时前
【菜鸟飞】在vsCode中安装python的ollama包出错的问题
开发语言·人工智能·vscode·python·ai·编辑器
Dongliner~5 小时前
【QT:多线程、锁】
开发语言·qt
鹏神丶明月天6 小时前
mybatis_plus的乐观锁
java·开发语言·数据库
极客代码6 小时前
Unix 域套接字(本地套接字)
linux·c语言·开发语言·unix·socket·unix域套接字·本地套接字
Zhuai-行淮6 小时前
施磊老师高级c++(一)
开发语言·c++
Muisti6 小时前
TCP 通信流程图
服务器·网络·windows
ylfhpy6 小时前
Java面试黄金宝典1
java·开发语言·算法·面试·职场和发展