这篇文章是对结构体、位段、联合体以及枚举四种自定义类型的学习分享,文章有些长但还是希望你能够耐心看完,我相信你一定能够在这里加深对这部分知识的理解~
那我们废话不多说,直接开始吧!
1. 结构体
1.1 结构体的含义与用途
与我们所熟知的整型 int、浮点型 float、高精度浮点型 double类似,结构体struct也是一种数据类型,特殊的是它是由我们用户自行定义的。如果要描述天数、人数等整型量,我们便可使用int类型的数据,如果要描述体温、或一些需要精度较高的数据,便可使用float类型来表示。但如果我们是要描述一个人呢?ta有年龄、身高、体重、生日等等信息,这时单⼀的内置类型便无法再满足相关需求,我们的结构体也随之诞生。
结构体是多种数值的集合,这些值称为成员变量。每个成员可以是不同类型的变量,如:整型、数组、指针,甚至是另一个结构体。
1.2 结构体的声明、定义以及初始化
1.2.1 声明、定义、初始化
//结构体声明---------------------------------------------------------
struct point
{
int x;
int y;
};//分号不能少
//结构体变量定义------------------------------------------------------
struct stu2
{
int age;
float weight;
}s1;//结构体声明的同时定义一个变量s1
//结构体初始化--------------------------------------------------------
struct stu3
{
int age;
float weight;
char name[10];
}s1={19,77.8,"Lisi"};
struct stu3 s2={19,88.6};
struct stu3 s3={.weight=77.3,.age=19,.name="Wangwu"};//自定义顺序初始化
//结构体嵌套初始化-----------------------------------------------------
struct Node
{
int data;
struct point p;
struct Node *next;
}n1={19,{12,13},NULL};
struct Node n2={20,{2,5},NULL};
1.2.2 不完全声明
还有一种比较特殊的声明方式——不完全声明
//匿名结构体类型
struct
{
int a;
char b;
float c;
}x;
struct
{
int a;
char b;
float c;
}a[20], *p;
可见,两种声明都省略了结构体的标签。
此时,如果我们用第二个结构体的指针去接收第一个结构体能实现吗?
p = &x;
警告:
此时编译器会把两个声明看成完全不同的两个类型,是非法的!
匿名的结构体类型,如果没有结构体类型重命名,基本上只能使用一次!
1.2.3 结构体的自引用
在 C 语言中,结构体的自引用指的是在一个结构体的定义中包含一个指向该结构体类型的指针。
在上面2.1中的嵌套结构体中我们便已经见到了这种正确的自引用方法
struct Node
{
int data;
struct Node *next;
}
上面的定义方式在数据结构的链表部分十分常见,定义节点时通过对自身的引用创建一个指针以达成节点间链接的效果。
那如果是这样自引用呢?
struct Node
{
int data;
struct Node next;
};
这种写法对吗?如果是对的,那么sizeof(struct Node)是多少呢?
我们会发现这种写法是完全错误的,因为一个结构体中再应用一个同类型的结构体变量,会导致引用无法终止使得结构体变量大小无限大,因此是不合理的。
不妨再看看下面这种用法是否正确
typedef struct
{
int data;
Node* next;
}Node;
这种也是错误的,因为Node是结构体类型重命名后才有的,但上述代码中在这一步之前就已经上手Node来创建成员变量了,这当然是不正确的。
正确的使用typedef来定义结构体应该是像这样
typedef struct Node
{
int data;
struct Node* next;
}Node;
1.3 结构体成员访问符
1.3.1 结构体成员的直接访问
对结构体内成员的直接访问是通过“ . ”来实现的
struct stu
{
int age;
ffloat weight;
int name[10];
}s1={20,77.8,"Wangwu"};
int main()
{
printf("%d ",s1.age);//打印年龄
printf("%f ",s1.weight);//打印体重
printf("%s ",s1.name);//打印名字
return 0;
}
1.3.2 结构体成员的间接访问
有时我们得到的不是一个结构体变量,而是一个结构体指针时,我们便是通过这个指针来对结构体内的成员进行访问。
#include <stdio.h>
struct stu
{
int x;
int y;
float z;
};
int main()
{
struct stu s1={1,2,3.14};
struct stu *p=s1;
//间接访问 修改成员值
p->x=4;
p->y=9;
p->z=6.66;
//间接访问 打印成员值
printf("%d %d %f ",p->x,p->y,p->z);
return 0;
}
1.3.3 直接间接访问的综合运用
#include <stdio.h>
struct stu
{
int x;
int y;
char name[10];
};
//直接访问打印成员值
void Printl(struct stu s)
{
printf("%d %d %s", s.x, s.y,s.name);
}
//间接访问修改成员值
void Set_stu(struct stu *ptr)
{
strcpy( ptr->name,"Wangwu");
ptr->x = 8;
ptr->y = 6;
}
int main()
{
struct stu s1 = { 2,4,"lisi"};
Printl(s1);
printf("\n");
Set_stu(&s1);
Printl(s1);
return 0;
}
1.4 结构体内存对齐
在掌握了结构体的基本时候过后,让我们更深入地探讨一个问题:如何计算结构体的大小?
这便涉及到一个十分热门的考点:结构体的内存对齐。
1.4.1 内存对齐规则
1. 结构体的第⼀个成员对⻬到和结构体变量起始位置偏移量为0的地址处 2. 其他成员变量要对⻬到某个数字(对⻬数)的整数倍的地址处 对齐数 = 编译器默认的⼀个对⻬数 与 该成员变量⼤⼩的较⼩值 -VisualStudio中的默认值为 8 -Linux中gcc没有默认对齐数,对齐数就是成员自身的大小 3. 结构体总⼤⼩为最⼤对⻬数(结构体中每个成员变量都有⼀个对⻬数,所有对⻬数中最⼤的)的 整数倍。 4. 如果嵌套了结构体的情况,嵌套的结构体成员对⻬到⾃⼰的成员中最⼤对⻬数的整数倍处,结构 体的整体⼤⼩就是所有最⼤对⻬数(含嵌套结构体中成员的对⻬数)的整数倍。 在了解了规则之后,让我们一起来看几个例子:struct stu { char x; int y; char z; }; 的大小是多少?有点感觉了吗?接下来再来个难一点的:
struct p1
{
char a;
int b;
}m1;struct p2
{
char c;
struct p1 m1;
char d;
int e;
}; 的大小是多少?
1.4.2 内存对齐存在的原因
1. 平台原因(移植原因)
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据(如整型数据都只能在4的倍数上取得),否则抛出硬件异常。
2. 性能原因: 数据结构(尤其是栈)应该尽可能地在⾃然边界上对⻬。原因在于,为了访问未对⻬的内存,处理器需要做两次内存访问;⽽对⻬的内存访问仅需要⼀次访问。假设⼀个处理器总是从内存中取8个字节,则地 址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对⻬成8的倍数,那么就可以 ⽤⼀个内存操作来读或者写值了。否则,我们可能需要执⾏两次内存访问,因为对象可能被分放在两 个8字节内存块中。 如图,在32位机器上,操作一次需要访问四个字节,像第一行那样,如果我们想要访问int b的话直接跳过四个字节便可一次性访问完成;但如果是第二个不考虑对齐的情况,我们就需要访问两次才能够将b所占空间访问完。 不难看出,内存对齐事实上是一种 用空间换取时间的做法! 但一味地使用空间来换取时间上的效率也不是最佳的方法,究竟又该如何处理才能够兼顾空间与时间呢? 在做出回答前不妨先算算以下两个结构体的大小分别为多少吧struct S1
{
char c1;
int i;
char c2;
};
struct S2
{
char c1;
char c2;
int i;
};
两个大小分别为12、8,你算对了吗?
十分神奇的是,这两个结构体的成员一模一样,唯一不同的便是成员的创建顺序,因此我们可以得出结论:
让占⽤空间⼩的成员尽量集中在⼀起便可既满足对齐又节省空间。1.4.3 修改默认对齐数
还记得在Visual Studio上的默认对齐数是多少吗?没错,就是8,但其实这个数值是可以根据我们的实际需求进行修改的。
通过#pragma这个预处理指令便可实现改变编译器的默认对⻬数。
#pragma pack(1)//修改默认对齐数为1
struct S
{
char a;
int b;
char c;
};
#pragma pack()//取消修改的默认对齐数,恢复为默认情况
int main()
{
//输出的结果是什么?
printf("%d\n", sizeof(struct S));
return 0;
}
运行代码我们能够发现,原本当默认对齐数为8的时候,这个结构体的大小应该为8,但当我们将默认对齐数修改为1过后,得出的大小就变成了6。
因此我们可以知道,结构体在对⻬⽅式不合适的时候,我们可以⾃⼰更改默认对⻬数。
1.5 结构体传参
struct pp
{
char a;
int b;
char c;
};
void print1(struct pp p1)
{
printf("%c %d %c", p1.a,p1.b,p1.c);
}
void print2(struct pp* p1)
{
p1->a = 'c';
p1->c = 'e';
printf("%c %d %c", p1->a, p1->b, p1->c);
}
int main()
{
struct pp p1 = { 'a',2,'b' };
print1(p1);//传结构体
printf("\n");
print2(&p1);//传地址
return 0;
}
上述是两种print函数的实现方法,你觉得那种要好一些呢?
事实上第二种会好一些,因为:
- 函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。
- 如果传递⼀个结构体对象的时候,结构体过⼤,参数压栈的的系统开销⽐较⼤,所以会导致性能的下降。
因此在传结构体的时候需要传结构体的地址。
2. 位段
在学完结构体的知识后,我们便可实现用结构体来表示位段了
2.1 什么是位段
位段的声明与结构体十分类似,唯二不同的是;
1. 位段的成员必须是 int、unsigned int 或signed int ,在C99中位段成员的类型也可以选择其他类型。
2. 位段的成员名后边有⼀个冒号和⼀个数字。
如下面代码表示:
struct A
{
int a:2;
int b:3;
int c:5;
int d:10;
};
此处的A便是一个位段类型。
那问题又来了,位段A的大小是多少呢?
printf("%d\n", sizeof(struct A));
运行完我们会发现,这个位段A占了4个bits的内存,但是怎么算出来的呢?
2.2 位段的内存分配
首先我们需要了解冒号后面的数字表示的是指这个成员所占的bit位,不难看出在这个位段中a,b,c,d分别占2,3,5,10个bit位。
在让我们看下面一段代码:
struct S
{
char a:3;
char b:4;
char c:5;
char d:4;
};
struct S s = {0};
s.a = 10;
s.b = 12;
s.c = 3;
s.d = 4;
在上面我们已经知道,冒号后面的是该成员所占的bit位数量,那么在这一段代码中我们为各个成员赋了数值,但在这种情况下,系统又是如何存储这个位段的呢?
在看具体实现操作前我们需要再回想起来,冒号后面的数字只是该成员所占内存的比特位的数量,就相当于int a中的a一样,我们在给它赋值时并不是将冒号后面的数字改了,而仅仅是赋了个数值。
让我们在Visual Studio中开启调试验证一下它在内存中究竟是不是这样的吧
可见,的确如我们刚刚所推导的那样
2.3 位段的跨平台问题
位段虽好,但跨平台问题是它跨不过去的一道沟
什么意思?原因是在不同编译器上
1.int 位段被当成有符号数还是⽆符号数是不确定的。
2. 位段中最⼤位的数⽬不能确定。(16位机器最⼤16,32位机器最⼤32,若我们写27,在16位机器上就会出问题。
3. 位段中的成员在内存中从左向右分配,还是从右向左分配,标准尚未定义。
4. 当⼀个结构包含两个位段,第⼆个位段成员⽐较⼤,⽆法容纳于第⼀个位段剩余的位时,是舍弃剩余的位还是利⽤,这是不确定的。
因此跟结构相⽐,位段可以达到同样的效果,并且可以很好的节省空间,但是有跨平台的问题存在。
2.3 位段的应用
上图是⽹络协议中,IP数据报的格式,我们可以看到其中很多的属性只需要⼏个bit位就能描述,这⾥使⽤位段将各种必要的信息用最小的空间封装在一起,节省了空间,⽹络传输的数据报⼤⼩也会较⼩⼀些,对⽹络 的畅通是有帮助的。 就好比在这个图中,将网络上传输的信息看做是一个个的方块,下面的信息要比上面的小很多,那么传输起来当然就会比上面的要高效很多2.3 位段使用的注意事项
在对位段成员值进行修改的时候需要注意, 位段的⼏个成员是会有共有同⼀个字节的情况的,内存中每个字节分配⼀个地址,⼀个字节内部的bit位是没有地址的,这样有些成员的起始位置并不是某个字节的起始位置,那么这位置处是没有地址的。 所以不能对位段的成员使⽤&操作符,这样就不能使⽤scanf直接给位段的成员输⼊值,只能是先输⼊放在⼀个变量中,然后赋值给位段的成员。struct A {
int a : 2;
int b : 5;
int c : 10;
int d : 30;
};
int main()
{
struct A sa = {0};
//错误的示范
scanf("%d", &sa._b);
//正确的⽰范
int b = 0;
scanf("%d", &b);
sa._b = b;
return 0;
}
3. 联合体
3.1 联合体类型的声明
与结构体类似,联合体也是由⼀个或者多个相同或不同类型的成员构成。union Un
{
char c;
int i;
};
3.2 联合体的特点
联合体的特点是:
1.编译器只为最⼤的成员分配⾜够的内存空间
2.其他所有成员共⽤这⼀块内存空间
运行下面这段代码,观察打印出来的内存大小是多少
#include <stdio.h>
union Un
{
char c;
int i;
};
int main()
{
//联合变量的定义
union Un un = {0};
printf("%d\n", sizeof(un));
return 0;
}
运行结束我们会发现大小为4,刚好是int所占内存的大小,这正好印证了第一个特点。
再运行下面这段代码观察出现了什么现象:
#include <stdio.h>
//联合类型的声明
union Un
{
char c;
int i;
};
int main()
{
//联合变量的定义
union Un un = {0};
// 下⾯输出的结果是⼀样的吗?
printf("%p\n", &(un.i));
printf("%p\n", &(un.c));
printf("%p\n", &un);
return 0;
}
运行完后我们会发现无论是整个联合体的地址,还是内部的两个成员的地址都是一样的,这也验证了第二个特点。
3.3 联合体内存分布的特点
在上面结构体部分的内容中我们已经知晓了结构体的内存是如何分布的,那么联合体的呢?
事实上,联合体的内存分布就如下图所示:
与结构体不同,因为联合体中所有成员都用同一块内存,因此内存浪费的情况相对不那么严重。
你说这哪里有内存浪费的情况?别急,先看下面这段代码:
#include <stdio.h>
union Un1
{
char c[5];
int i;
};
union Un2
{
short c[7];
int i;
};
int main()
{
//下⾯输出的结果是什么?
printf("%d\n", sizeof(union Un1));
printf("%d\n", sizeof(union Un2));
return 0;
}
一看似乎很简单啊,Un1不就是5吗?Un2不就是14吗?但在我们运行后会发现并不是这么简单的问题:
我们发现两个都和我们预想中的不一样,那为什么会这样呢?
因为:
联合的⼤⼩⾄少是最⼤成员的⼤⼩。但当最⼤成员⼤⼩不是最⼤对⻬数的整数倍的时候,就要对⻬到最⼤对⻬数的整数倍。
在这两个联合体中虽然包含了两个数组,但在计算内存的时候只会对比前面的char、short与int的对齐数大小,因此内存分配应该如下图所示:
当然,上述情况只是说后面需要浪费多少内存,前面各个成员的共用一块地址的特点依旧没有变化。
也正是由于这个特点,使得我们在给联合体其中⼀个成员赋值时,其他成员的值也会跟着变化。
#include <stdio.h>
union Un
{
char c;
int i;
};
int main()
{
//联合变量的定义
union Un un = {0};
un.i = 0x11223344;
un.c = 0x55;
printf("%x\n", un.i);
return 0;
}
运行完之后我们会发现原本为i赋的11223344在我们为c也赋完值后发生了改变,变成了11223355
3.4 联合体的应用
联合体大概是怎么事儿我们都差不多知晓了,那么它可以用在哪里呢?
我们来看个例子:
我们要搞⼀个活动,要上线⼀个礼品兑换单,礼品兑换单中有三种商品:图书、杯⼦、衬衫。 每⼀种商品都有:库存量、价格、商品类型和商品类型相关的其他信息。
其中: 图书:书名、作者、⻚数
杯⼦:设计 衬衫:设计、可选颜⾊、可选尺⼨
如果我们才学完结构体,当然会直接力大砖飞地写出这么一段代码:
struct gift_list
{
//公共属性
int stock_number;//库存量
double price; //定价
int item_type;//商品类型
//特殊属性
char title[20];//书名
char author[20];//作者
int num_pages;//⻚数
char design[30];//设计
int colors;//颜⾊
int sizes;//尺⼨
};
看到这是不是娇羞一笑:这不就是我嘛~
但仔细一想我们会发现,这样写虽然确实是包含了我们所需要的所有变量,但是如果我们在只描述杯子的时候,其他两件商品的特殊变量的内存就直接浪费掉了,非常不环保!
那我们只不是可以设计一块空间,用到哪个礼品就开辟那个礼品所需要的空间,这样就解决了空间浪费的问题了呀!
正确的,我们就用刚学的联合体:
struct gift_list
{
//常规变量
int stock_number;
float price;
int item_type;
//特殊变量
union {
struct
{
char title[20];
char author[20];
int num_pages;
}book;
struct
{
char design[20];
}mug;//杯子
struct
{
char design[20];
int colors;
int sizes;
}shirt;
}item;
};
int main()
{
struct gift_list gift;
// 假设修改书的属性
gift.item_type = 0; // 表示书类型
strcpy(gift.item.book.title, "New Book Title");
strcpy(gift.item.book.author, "New Author");
gift.item.book.num_pages = 250;
gift.stock_number = 8;
gift.price = 19.99;
// 假设修改杯子的属性
gift.item_type = 1; // 表示杯子类型
strcpy(gift.item.mug.design, "New Mug Design");
gift.stock_number = 15;
gift.price = 10.5;
// 假设修改衬衫的属性
gift.item_type = 2; // 表示衬衫类型
strcpy(gift.item.shirt.design, "New Shirt Design");
gift.item.shirt.colors = 3;
gift.item.shirt.sizes = 42;
gift.stock_number = 12;
gift.price = 25.0;
return 0;
}
如上面这段代码所示,三种礼品都用得到的常规变量我们就直接写出来就好了,其他特殊的属性就将三个结构体封装到一个联合体中便可实现我们的目标。
后续如果我们需要修改或者打印各个商品的属性,直接像上面的代码中现实的那样进行操作就行啦!
3.5 联合体的小练习
接下来再让我们来个小练习来结束联合体部分的内容分享吧!
写⼀个程序,判断当前机器是⼤端还是⼩端你先等等,啥是大小端啊? 如图,在存储一个整形变量a的数值1时,如果是大端机器,进行大端存储则是由低地址向高地址存储的位权则是递减的;若是小端机器,则相反。 那这么一看,似乎只需要看第一位数值为不为零就行了,若为零则是大端,反之则为小端,再结合联合体的特点,我们班可写出下面这段程序:
int check_sys()
{
union
{
int i;
char c;
}un;
un.i = 1;
return un.c;//返回1是⼩端,返回0是⼤端
}
int main()
{
int ret = check_sys();
if (ret == 1)
printf("小端");
else if(ret == 0)
printf("大端");
return 0;
}
运行效果
4. 枚举
4.1 什么是枚举
顾名思义就是一一列举,将所有可能的取值都一一列举。 在我们日常生活中,一周七天、一年十二个月、一个班的所有同学等等,都可以通过枚举类型来一一列举表示。4.2 枚举类型的声明
enum Day {
Mon,
Tues,
Wed,
Thur,
Fri,
Sat,
Sun
};//星期
enum Sex
{
MALE,
FEMALE,
WALMART SHOPPING BAG,
SECRET
};//性别
上面的代码便是对星期和性别的枚举声明,所定义的enum Day 、enum Sex都是枚举类型
{}中的内容是枚举类型的可能取值,也叫枚举常量 。
这些可能取值都是有值的,默认从0开始,依次递增1,当然在声明枚举类型的时候也可以赋初值。enum Color
{
RED=2,
GREEN=4,
BLUE=8
};//颜⾊
当然如果在上面这个颜色的枚举中我们没有给RED赋初值,它的值依旧是默认的0
那这样呢?
enum Color
{
RED,
GREEN=4,
BLUE
};//颜⾊
运行结果:
这下我们就知道了,如果前面的没被赋值,枚举常量就为系统的默认数值;若是中间赋了一个数值但后面的没有,那么就按照往下递增1的规则
4.3 枚举类型的优点
我们能够知道,虽然看起来枚举类型是一个一个的词,但他们自身是枚举常量,是有数值的。
既然是这样,我们直接用#define来定义不也一样吗?
1.增加代码的可读性和可维护性
-拿性别的枚举类型来举个例子
enum Sex
{
MALE,
FEMALE,
WALMART SHOPPING BAG,
SECRET
};//性别
通过这个枚举类型,我们既能够知道都有哪些性别,还能够很自然的了解每个性别所对应的枚举常量。
但如果我们不这样写,而是将int i=1define成MALE之类的,当别人看到这段代码可能就会觉得有些不理解,不能够立马知道这样定义的意义。
2. 和#define定义的标识符⽐较枚举有类型检查,更加严谨
-打个比方:
#define MALE 0
这里的MALE是没有类型的,但如果我们像上面那样通过枚举来定义,那里的MALE就具有枚举类型。
3. 便于调试,预处理阶段会删除 #define 定义的符号(后续学完预处理将做进一步的解释)。
4. 使⽤⽅便,⼀次可以定义多个常量
-这个就很好理解了,如果想用define来定义一周七天,我们就需要写七行代码来完成这个定义,但通过枚举我们就只需要在一个{}内输入七个词,当然要方便高效很多了
5. 枚举常量是遵循作⽤域规则的,枚举声明在函数内,只能在函数内使⽤
-define是一种宏定义方法,作用域是全局覆盖的,但枚举常量就相对而言作用域比较小
但其实具体有什么作用以我目前的水平确实是无法再做过多的解释,如果大家有什么见解都可以打在评论区,我们一起讨论进步~
4.4 枚举类型的使用
enum Color
{
RED=1,
GREEN=2,
BLUE=4
};
int main()
{
enum Color clr = GREEN;
return 0;
}
要注意的是,在C语⾔中是可以拿整数给枚举变量赋值的,但在C++是不⾏的,C++的类型检查⽐
较严格。 好了,本次的学习分享到这里就结束了,十分感谢你能看到这里,相信帅气聪明的你看到这里也对结构体、位段、联合体以及枚举有了更全面的认识。 当然后续我依旧会继续分享我所学到的知识,敬请期待吧~ 让我们下次再见~ 标签:char,struct,int,联合体,位段,枚举,结构 From: https://blog.csdn.net/2301_80029060/article/details/142476860