当前位置:网站首页>C语言自定义类型(结构体,位段,枚举,联合体)
C语言自定义类型(结构体,位段,枚举,联合体)
2022-07-15 19:49:00 【KissKernel】
对于自定义类型之前了解了,自定义类型有、结构体、枚举类型、共用体
结构体
结构体的声明
struct person//结构体标签
{
char name[20];//结构体成员
char tele[12];
int age;
}a1,a2;//使用这个结构体创建出来的全局变量
结构体就是不同类型元素的集合,结构体的成员可以是各种类型。也可以是结构体本身。我们创建一个结构体相当于创建了一个类型。通过这个类型创建出来变量,就是结构体的实例化。
结构体的特殊声明
这里可以省略结构体标签,也就是省略到person这个结构体就变成了匿名结构体。
struct
{
char name[20];
char tele[12];
int age;
}q1,*pp;
struct
{
char name[20];
char tele[12];
int age;
}q2;//变量列表
因为匿名结构体没有标签,所以匿名结构体的变量只能再下面的变量列表来定义
什么类型结构体可以定义成为匿名结构体呢?就是你只会再这里使用一次,并且后面不会使用也不想让别人使用就可以定义成匿名结构体变量。
*pp = &q2;//这句代码是错误的
因为两个匿名结构体,就算成员完全一样,他们也是不一样的结构体,所以上面这句代码就会出现类型不兼容的错误。
结构体的自引用
就是结构体里面包含了自己的结构体。
typedef struct ListNode
{
int val;
struct ListNode* next;
}ListNode;
这里需要注意的第一个点,自引用的一定得使用自己类型的指针,如果不是指针,就会无线包含套娃,所以是错误的。
第二个点:使用typedef重命名后的结构体在自引用的使用一定要使用重命名之前的名字来定义指针类型。否在也会报错。
因为:逻辑上看,如果你想要重命名那么这个类型必须定义完了,而自引用是在定义的时候进行自引用,这时候重命名还没有进行,所以自然不可以使用重命名之后的名字来定义结构体的指针。
结构体变量的定义和初始化
struct point
{
int x;
int y;
}p1;//定义了全局的结构体变量
struct point p2 = {
2,4 };//定义全局变量并且初始化
struct test
{
struct point p;
double b;
}t1 = {
{
4,3},9.0 };//初始化列表定义变量,并且嵌套初始化。
struct test t2 = {
{
33,22},11 };//定义全局变量且初始化。
int main()
{
struct test t1 = {
{
3,1},4 };
p1.x = 3;//如果不初始化,后面的赋值只能一个一个的赋值
p1.y = 5;
return 0;
}
结构体的内存对齐
首先在计算结构体大小的时候并不是简单的将所有的成员大小加在一起。
结构体的大小计算符合内存对齐规则
1.结构体的第一个成员放在相对于起始地址0偏移量的位置。
2.其他成员对齐到对齐数的整数倍处(vs有默认对齐数8,对齐数就是成员的类型大小和默认对齐数的较小的那个)其他的编译器没有默认对齐数,成员的类型大小就是对齐数。
3.结构体的总大小是最大对齐数的整数倍,最大对齐数就是所有成员对齐数的最大的那个
4.嵌套结构体,那么这个嵌套结构体成员的对齐数,就是这个嵌套结构体内的最大对齐数。也需要对齐到他的对齐数的整数倍。
struct test
{
char arr[5];
int a;
};
int main()
{
printf("%zu", sizeof(struct test));
return 0;
}
这段代码的结果是12,也就说明了数组的对齐数是他的类型的对齐数,arr的对齐数也就是1,最大对齐数是4,最后结果就是12啦。
为什么要内存对齐
有两个原因:
1.平台原因
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特
定类型的数据,否则抛出硬件异常。
2.性能原因
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。
原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访
问。
所以总体来说就是,用空间换时间。
那么如何让我们既满足内存对齐,又能尽可能的减少内存消耗呢?
就是尽可能的让占内存小的变量靠在一起。
修改默认对齐数
既然vs有默认对齐数,那么我们如何修改默认对齐数呢?
#pragma pack(1)
struct test
{
char arr[5];
int a;
};
#pragma pack()
在这两个#pragma之间的结构体默认对齐数被修改成了1。所以我们可以用这种方式修改默认对齐数。
如何计算结构体成员的偏移量
这里我们可以使用宏offsetof,这个宏需要引用头文件<stddef.h>
struct test
{
int a;
char b;
int c;
};
int main()
{
printf("%d\n", offsetof(struct test, a));
printf("%d\n", offsetof(struct test, b));
printf("%d\n", offsetof(struct test, c));
return 0;
}
这个就是使用方法,第一个参数是结构体,第二个参数就是这个结构体里面的成员。
结构体传参
结构体传参,一般使用传地址,因为我们传值的时候,会先在内存中拷贝一份。如果结构体过大,那么传参的时候参数压栈的性能消耗就大。
struct S
{
int data[1000];
int num;
};
struct S s = {
{
1,2,3,4}, 1000 };
//结构体传参
void print1(struct S s)
{
printf("%d\n", s.num);
}
//结构体地址传参
void print2(struct S* ps)
{
printf("%d\n", ps->num);
}
int main()
{
print1(s); //传结构体
print2(&s); //传地址
return 0;
}
这两个就是结构体传参和传地址的使用。
位段
位段是通过结构体来实现的
注意点:1,位段的成员必须是int,unsigned int,signed int,或者是char
2,位段成员后面有一个冒号+数字
struct test
{
int a : 2;
int b : 4;
int c : 10;
int d : 30;
};
上面的代码就是位段的使用形式,他们后面跟的数字叫做位数,要注意位数不能大于位域,位域就是变量类型的最大位数,int就是32位。所以如果后面跟的数字大于32就报错。
这里的位数代表了他们在内存中占用几位bit位。
使用位段可以在一定程度上节省内存
比如上面这个结构体如果不用位段就是16个字节,使用位段就是8个字节。
位段在内存中的存储

从这个内存来看,位段在vs2019中是先使用了低地址处的空间,然后再使用高地址空间,当一个int剩余的位数不足以容纳下一个对象的时候就会舍弃这些剩余的位数,重新开辟一个新的整形来存储。
位段的跨平台问题
上面也说了这个结论只是再vs2019这个平台下的结论。位段再不同的平台下,规则也是不同的。
1.int位段被当作是有符号数还是无符号数是不确定的。
2.位段中的最大位数不能确定。比如int再32位机上是4字节。但是在16位机上就是2字节。
3.位段成员是从左向右使用内存还是从右向左是不确定的。
4.当一个第一个位段使用后,剩余的位数不足以容纳下一个位段成员的时候,这些位数是浪费还是继续使用也是不确定的。
位段的应用

位段一般用在网络这一块比较多。如上,我们在网络上面发送的数据都会加上一个这样的数据包,为了尽可能的减小数据包的大小,显然使用位段更合适。网络数据包越小那么就会减少网络上的拥堵,使得数据传送的效率更高。
枚举
枚举的意思就是一一列举,在我们生活中又很多东西都是可以列举的,比如星期,比如人的性别。这些
要注意枚举和穷举是不一样的。穷举是就算又很多的数据,只要能列举出来,就一个一个的列举,枚举如果是情况太多的时候就不可以枚举了。
枚举类型的定义和使用
enum Sex
{
MALE,
FEMALE,
SECRET
};
上述代码就是枚举性别,有三种,男,女和保密。
enum Sex
{
MALE,
FEMALE,
SECRET
};
int main()
{
enum Sex s1 = MALE;
return 0;
}
这就是枚举类型的使用,当然枚举类型还可以用于项目中来替换switch中的1,2,3,。。。。
enum Sex
{
MALE = 1,
FEMALE = 4,
SECRET = 3
};//可以给每个枚举常量任意赋值
因为枚举类型里面定义的成员又叫枚举常量,他们都是有一个常量值的,如果不进行赋值,那么第一个成员就是0,依次递增,
enum Sex
{
MALE,//0
FEMALE,//1
SECRET//2
};
当然可以自己给每个枚举常量赋值,而且如果给第一个赋值为4,那么剩下的就会在4的基础上面递增,4,5,6.
关于枚举在项目中的应用有以下这种场景
enum selete
{
EXIT,//0
ADD,
DELETE,
CHECK,
MODIFY,
SHOW,
SORT
};
int main()
{
int input;
contact p;
ConInit(&p);
do
{
menu();
scanf("%d", &input);
switch (input)
{
case ADD:
ConAdd(&p);
break;
case DELETE:
ConDelete(&p);
break;
case CHECK:
ConCheck(&p);
break;
case MODIFY:
ConModify(&p);
break;
case SHOW:
ConShow(&p);
break;
case SORT:
ConSort_name(&p);
break;
case EXIT:
printf("退出程序\n");
break;
default:
printf("选择错误\n");
}
} while (input);
return 0;
}
可以看到,使用了枚举常量替换了原来的数字,使得这些分支他们的含义变得非常明了,增强了代码的可读性。
枚举的优点:
1.增强了代码的可读性和可维护性
2.和#define相比具有类型检查,更安全
3.防止命名污染(将枚举常量封装了起来)
4.便于调试(相对于#define)
5.一次可以定义多个常量
关于枚举和#define
可以看上面的代码,我们其实就是将数字替换成了单词,这个功能使用#define宏定义一下,也可以做到,那么为什么还要使用枚举呢?
因为首先,枚举写起来比#define更加方便简单。#define需要一个一个的写。
其次,在代码的编译过程中,预处理阶段,#define的单词就会被替换成对应的数字,这个就是宏替换。调试的时候就不能看到宏定义了,所以调试的时候宏定义的单词就没有了。
类型检查也就是,我们定义enum实际上是定义了一个新的类型。在给这个类型赋值的时候,最好使用枚举常量进行赋值,否则就会出现类型不兼容。#define没有类型检查,就是直接替换。
联合体(共用体)
联合体看名字就是共用,也就是所以的成员共用同一块内存空间。
联合体的定义
union un
{
char a;
int b;
};
union un u;
我们可以尝试使用sizeof计算共用体的大小,上面这个是4个字节。
联合体大小的计算
1.首先联合体的大小至少是最大的对象的大小。
2.联合体也符合内存对齐规则。
union un
{
char arr[5];
int a;
};
int main()
{
union un u;
printf("%d", sizeof(u));
}
这段代码,他的大小就是8,因为arr的对齐数是1,a的对齐数是4,所以最大对齐数就是4,arr是最大成员占用5个字节,但是联合体的总大小是最大对齐数的整数倍,所以,这个联合体的大小就8个字节。
联合体的特点
联合体的所有成员的起始地址是相同的。
其次,公用体的成员不可以同时使用,因为改变一个成员,其他的成员也会跟着改变
union Un
{
int i;
char c;
};
union Un un;
// 下面输出的结果是一样的吗?
printf("%d\n", &(un.i));
printf("%d\n", &(un.c));
//这个两个的地址是相同的
//下面输出的结果是什么?
un.i = 0x11223344;
un.c = 0x55;
printf("%x\n", un.i);
//这里使用的是vs2019,小端存储,所以结果是0x11223355;
使用联合体判断大小端
我们知道,小端存储就是将数据的低位,放在内存的低地址处,相反就是大端
union un
{
char a;
int i;
};
int main()
{
union un u;
u.i = 1;//0x00 00 00 01;
if (u.a == 1)
{
printf("小端\n");
}
else
{
printf("大端\n");
}
return 0;
}

因为各个成员的起始地址相同,所以如果是小端那么低地址处放的肯定是1,所以只需要判断char a是不是1就可以了。
边栏推荐
- 网络空间资产探测关键技术研究
- Codeforces Round #802 D. River Locks
- Codeforces Round #802 D. River Locks
- Comprehensive explanation of e-commerce crawler API
- internship:移动端源码的分析
- Can you use redis? Then come and learn about redis protocol
- 浅析电子签章应用安全与技术
- Codeforces Round #802 B. Palindromic Numbers
- LeetCode-128-最长连续序列
- Research on Key Technologies of asset detection in Cyberspace
猜你喜欢

开机按F11 选择 one-shot再选U盘启动

下班前几分钟,我彻底弄懂了JSON.stringify()

Codeforces Round #804 C The Third Problem

Codeforces Round #804 B Almost Ternary Matrix

华为云Stack南向开放框架,帮助生态伙伴高效入云

Unity Shader——CGInclude文件cginc

Codeforces Round #802 C. Helping the Nature

电商爬虫API全面详解

nacos win10单机启动命令

User login and registration function with verification code
随机推荐
Codeforces Round #804 B Almost Ternary Matrix
脚本编写规则和变量定义
Exness: crude oil stopped falling and rebounded. Pay attention to the performance of US terrorist data in the evening
Ubuntu 22.04 LTS 是目前最安全的版本的七大原因
Hcip day 8 notes
CSDN博客专家专属荣耀奖励来了
5-Redis架构设计到使用场景-存储原理-数据类型底层结构
网络信息查看及配置
Easy gene encode histone chip SEQ and transcription factor chip SEQ data standard and processing flow
7-Redis架构设计到使用场景-缓存穿透、缓存雪崩、缓存预热、缓存降级
Codeforces Round #804 C The Third Problem
下班前几分钟,我彻底弄懂了JSON.stringify()
基于角色的云环境下虚拟机安全访问控制策略
【开源可信隐私计算框架 “隐语”】蚂蚁宣布面向全球开发者正式开源
Role based security access control strategy for virtual machine in cloud environment
请教下MySQL Source并行度的问题,我的Source下游并行度为1,我发现不开并行的话,和下
请教一个问题, FLinkCDC同步mysql的数据,必须要root权限吗?
Cloud document management software docuware cloud how to solve five it problems
Codeforces Round #802 A. Optimal Path
Power on, press F11, select one shot, and then select U disk to start