当前位置: 首页 > news >正文

【C语言进阶】自定义类型之结构体,枚举和联合

人一能之,己百之;人十能之,己千之。                             ——《中庸》

 

目录

一.结构体

1.结构的基础知识

2.结构体的声明

3.结构体成员的类型

4.结构体变量的定义和初始化:

5.结构体成员的访问:

6.结构体传参 

7.结构体内存对齐:结构体的大小

8.为什么要有内存对齐?

二.结构体的位段

1.什么是位段:

2.位段的内存分配:

3.位段的跨平台问题

4.位段的应用:

三.枚举 

1.枚举的定义

2.枚举的优点:

四.联合(共用体)

1.联合体等定义:

2.联合体的大小


一.结构体

1.结构的基础知识

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

2.结构体的声明

结构体的定义如下所示,struct为结构体关键字,tag为结构体的标志,member-list为结构体成员列表,其必须列出其所有成员;variable-list为此结构体声明的变量。

struct tag {
 member-list
 } variable-list ; //这个;不能少

在结构体中,必须有struct tag和member-list是必须要有的,而variable-list是可以没有的,variable-list只是一个结构体变量,而且是全局变量,这里可以不写也行。

struct tag {
 member-list
 } ; 

3.结构体成员的类型

结构体成员可以是标量,数组,指针,甚至是其他结构体。当我们想定义一个人时,我们可能会想到他的身高,体重,名字等我们就用float来定义,体重用int,名字用char。

struct people
{
    int weight;//体重
    char name[20];//数组来存名字
    float height;//身高
};

4.结构体变量的定义和初始化:

struct people
{
    int weight;//体重
    char name[20];//名字
    float height;//身高
};
int main()
{
    struct people man = { 55,"小陈",172 };//man就是结构体变量,是局部变量
    struct people max = { .weight = 55,.name = "小陈",.height = 172 };
    //这两种结构体的初始化都可以
    return 0;
}

typedef是在计算机编程语言中用来为复杂的声明定义简单的别名,它与宏定义有些差异。它本身是一种存储类的关键字,与auto、extern、mutable、static、register等关键字不能出现在同一个表达式中。

这里我们写struct people感觉有点麻烦,我们就可以使用typedef来给结构体定义一个简单的别名,之后我们就可以使用别名来代替结构体类型。上述初始化代码我们就可以这样写。

typedef struct people M;
struct people//第一次使用时不能用M来代替
{
    int weight;//体重
    char name[20];//名字
    float height;//身高
};
int main()
{
    M man = { 55,"小陈",172 };
    //这里就是用M来代替struct people
    M max = { .weight = 55,.name = "小陈",.height = 172 };
    //这两种结构体的初始化都可以
    return 0;
}

当结构体的成员有结构体,我们又该如何对结构体初始化呢?

struct book
{
    char name[20];
    struct people;
    int price;
};
struct people
{
    int weight;
    char name[10];
    float height;
};
int main()
{
    struct book b1 = { "活着",{60,"富贵",170},50 };//用大括号括起来
    //从结构体book里面从上往下初始化结构体成员
    return 0;
}

5.结构体成员的访问:

1.结构体变量.结构体成员名

2.结构体指针->结构体成员名

struct people
{
    int weight;
    char name[10];
    float height;
};
void print(struct people* b2)
{
    printf("%d %s %f\n", b2->weight, b2->name, b2->height);
}
int main()
{
    struct people b1 = { 55,"小陈",172};
    printf("%d %s %f\n", b1.weight, b1.name, b1.height);
    print(&b1);
    return 0;
}

6.结构体传参 

结构体传参也有两种,一种是传结构体变量过去,还有一种就是传地址过去,这两者有什么区别吗?

struct people
{
    int weight;
    char name[10];
    float height;
};
void print1(struct people* b2)
{
    printf("%d %s %.1f\n", b2->weight, b2->name, b2->height);
}
void print2(struct people b2)//形参
{
    printf("%d %s %.1f\n", b2.weight, b2.name, b2.height);
}
int main()
{
    struct people b1 = { 55,"小陈",172};
    printf("%d %s %.1f\n", b1.weight, b1.name, b1.height);
    print1(&b1);//传地址
    print2(b1);//传变量,实参
    return 0;
}


当我们传参的时候,如果传的结构体变量,那形参就要开辟结构体成员内存的总空间来接收实参,因为函数传参的时候,参数是需要压栈的。如果传递一个结构体对象的时候,结构体过大,参数压栈的系统开销比较大,所以会导致性能的下降。

所以我们尽量使用传地址的形式,也就是传指针,因为我们知道指针大小就是4或者8个字节。

所以这样就比较节省空间。

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

7.结构体内存对齐:结构体的大小

C语言马上都已经学完了,我们还没学过计算结构体的大小。今天我们就将好好的学习一下,在这里我会详细的讲解如何计算结构体的大小。

struct A
{
	int a;
	char c1;
}s1;
struct B
{
	char c1;
	int a;
	char c2;
}s2;
struct C
{
	char c1;
	int a;
	char c2;
	char c3;
}s3;
int main()
{
	printf("%d\n", sizeof(struct A));
	printf("%d\n", sizeof(struct B));
	printf("%d\n", sizeof(struct C));
	return 0;
}

在这里计算结构体的大小会不会是A结构体有一个int和char,大小是5个字节。B有6个字节,C有7个字节。其实想想也不会这么简单,不然我们也不会系统的学习。

很奇怪,为什么结构体的大小会是8,12,12呢?而且结构体C还比结构体B的变量多一个。

这里就要请出结构体内存对齐这个东西了。

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

2.第二个成员开始,以后每一个成员都要到某个对齐数的整数倍处,这个对齐数是:成员自身大小和默认对齐数的较小值。

注:VS环境下,默认对齐数是8。gcc环境下,没有默认对齐数,对齐数就是成员自身的大小。

3.当成员全部存放进去后,结构体的总大小是所有成员的对齐数中最大对齐数的整数倍。如果不够,则浪费空间对齐。

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

上述的话是什么意思呢?我们使用上面的结构体例子来说明一下,就懂了。

 对于结构体A:

对于结构体B:

 

对于结构体C:

 对于结构体嵌套结构体的:

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

struct A
{
	char c1;
	int a;
	double b;
};
struct B
{
	char c2;
	int a1;
	struct A s1;
};
int main()
{
	printf("%d", sizeof(struct B));
	return 0;
}

8.为什么要有内存对齐?

1. 平台原因:

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

2.性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。
原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。


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

struct A//这个结构体的大小是8
{
	char c1;
	char c2;
	int a;
};
struct B//这个结构体的大小是12
{
	char c1;
	int a;
	char c2;
};

虽然结构体成员都是一样,但是顺序不同也会使结构体的大小不同。 

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

二.结构体的位段

1.什么是位段:

顾名思义位段就是二进制位(比特位)。

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

1.位段的成员必须是整型家族的:int,unsigned int或者signed int,char。

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

2.位段的内存分配:

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

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

struct A
{
	char a : 3;//这些就是位段
	char b : 4;
	char c : 5;
	char d : 4;
}S;
int main()
{
	S.a = 10;
	S.b = 12;
	S.c = 3;
	S.d = 4;
	return 0;
}

1.我们假设分配到内存空间的比特位是从右向左的

2.分配的内存剩余的比特位不够使用时,浪费掉。

VS2022环境测试数据:

结果确实按照我们假设的一样。

3.位段的跨平台问题

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

4.位段的应用:

在网络上传输数据的时候,可以通过位段来节省空间。

三.枚举 

1.枚举的定义

枚举顾名思义就是一一给列举出来。

想我们现实中星期,性别和月份等可以一一列举出来。

枚举的可能取值默认时从0开始的,依次递增1。

enum sex
{
    MALE,
    FEMALE,
    SECRET
};
int main()
{
    enum sex s = MALE;
    printf("%d\n", MALE);
    printf("%d\n", FEMALE);
    printf("%d\n", SECRET);
    return 0;
}

enum sex
{
    MALE,
    FEMALE=5,//也可赋值,后面依次自增1
    SECRET
};
int main()
{
    enum sex s = MALE;
    printf("%d\n", MALE);
    printf("%d\n", FEMALE);
    printf("%d\n", SECRET);
    return 0;
}

 

2.枚举的优点:

为什么使用枚举?
我们可以使用#define定义常量,为什么非要使用枚举?枚举的优点:
1.增加代码的可读性和可维护性。
2.和#define定义的标识符比较枚举有类型检查,更加严谨。

3.便于调试。
4.使用方便, 一次可以定义多个常量。

四.联合(共用体)

1.联合体等定义:

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

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

union un 
{
	char c;
	int i;
};
int main()
{
	union un u;
	printf("%d\n", sizeof(u));
    return 0;
}

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

2.联合体的大小

联合体的大小至少是最大成员的大小

当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。

这里的对齐规则是和结构体的内存对齐是一样的,但是联合体是公用一块空间的。

比如我们举例说明:

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

 

之前我们不是写了一个判断大小端的题吗?我们通过int a=1;(char*)p=&a, 强制类型转换,访问一个字节来判断。但是这里我们可以通过共用体来实现。非常的简单。

union un 
{
	char c;
	int a;
};
int main()
{
	union un s;
	s.a = 1;
	if (s.c == 1)
		printf("小端\n");
	else
		printf("大端\n");
	return 0;
}

全部的内容就结束了,感谢支持。 

 

相关文章:

  • springboot校友社交系统
  • 【Python算法】简单深搜练习
  • ASO优化之应用商店中的A/B测试——改良版
  • Github隐藏功能显示自己的README,个人化你的Github主页
  • 什么是 SSL 证书管理
  • java中如何实现全文搜索
  • Eigen中的SparseMatrix(稀疏矩阵)元素的快速插入
  • 第十四届蓝桥杯三月真题刷题训练——第 21 天
  • JavaScript到底如何存储数据?
  • 结构体详解
  • 【数据结构与算法】设计循环队列
  • go语言gin框架学习
  • 一个超简单的渐变平行四边形进度条
  • 安装好unity后给unity配置及插件和资源相关的事情
  • docker版jxTMS使用指南:勾连python
  • 值得记忆的STL常用算法,分分钟摆脱容器调用的困境,以vector为例,其余容器写法类似
  • 重学Java设计模式-结构型模式-组合模式
  • Android11以上版本使用高德定位,定位成功,卫星数一直为0
  • 【十二天学java】day06之方法详解
  • Revit明细表计算重量和明细表导出功能
  • 电加热油锅炉工作原理_电加热导油
  • 大型电蒸汽锅炉_工业电阻炉
  • 燃气蒸汽锅炉的分类_大连生物质蒸汽锅炉
  • 天津市维修锅炉_锅炉汽化处理方法
  • 蒸汽汽锅炉厂家_延安锅炉厂家
  • 山西热水锅炉厂家_酒店热水 锅炉
  • 蒸汽锅炉生产厂家_燃油蒸汽发生器
  • 燃煤锅炉烧热水_张家口 淘汰取缔燃煤锅炉
  • 生物质锅炉_炉
  • 锅炉天然气_天燃气热风炉