本文重点(前言)
自定义类型包括结构体,枚举和联合,他们都是自己构造的类型,对于这篇文章我们了解的就是这些自定义类型的用法,以及在内存中占用多少内存。以及这些特点和对我们在编写代码时的帮助。
-
(1) 对于结构体知识的详细介绍
-
<1>结构体类型的声明
<2>结构的自引用
<3>结构体变量的定义和初始化
<4>结构体内存对齐
<5>结构体传参
<6>结构体实现位段(位段的填充&可移植性)
(2)枚举
-
<1>枚举类型的定义
<2>枚举的优点
<3>枚举的使用
(3)联合
-
<1>联合类型的定义
<2>联合的特点
<3>联合大小的计算
为什们要有自定义类型
在C语言中我们为了区分不一样的数据,C语言中自己定义了内置类型
int
,char
,short
,long
, long long
,float
,double
,有了这些我们在一看到这些类型我们就可以知道这是什么数据,让我们一目了然。
但是如果定义一个比较抽象的东西的时候我们怎么办呢。
比如:
在定义一本书,要介绍书的价钱,作者,书号,出版社,书名等等
在定义一个学生,也要介绍学生的年龄,学号,姓名,身高,等等
但是我们C语言中也没有定义这些东西,这些东西太复杂了,对于编译器来说不太现实,所以我们需要自定义类型,用这些来表示这些复杂对象。
结构体
结构体的声明
(1)什么是结构体,结构体是干什么的。
-
结构体的概念
- 结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。
(2)结构体的声明
-
声明
-
请看下图
例子:写一个书的结构体
struct BOOK
{
int ID[20];
char BOOK_name[20];
char BOOK_author[20];
int price[5];
}s3,s4;
int main()
{
struct BOOK s1 = { 0 };
//可以类比一下int,char,这些就相当于struct BOOK一样,而s1就是他的变量
//s1和下面的s2都是局部变量,而在结构体后随手定义的变量就是全局变量
struct BOOK s2 = { 0 };
return 0;
}
(3)匿名结构体类型
-
匿名结构体
-
在声明结构的时候,可以不完全的声明。匿名结构体类型,他不能在主函数中随便用,只能在刚创建就用,并且在下图我们可以看出来,这两个匿名结构体类型完全相同,但是却并不相等,编译器会把下面的两个声明当成完全不同的两个类型。
所以是非法的。
(4) 结构的自引用
-
自引用
- 在结构中包含一个类型为该结构本身的成员是否可以呢?
这个就是相当于一个链表的存在,画图解释
struct stu
{
int data;
struct Node* next;
//结构体指针,能通过这个找到下一个结构体。
};
int main()
{
return 0;
(5) 结构体变量的定义和初始化
-
定义和初始化
-
有了结构体类型,那如何定义变量,其实很简单。
对于结构体变量的定义来说,就是上面所说的全局变量和局部变量的定义。结构体也可以嵌套定义。
在定义完了可以直接将内容初始化,初始化的内容可以跟定义结构体类型变量的顺序相同,也可以不同。这些都是可以的。
struct BOOK
{
char ID[20];
char BOOK_name[40];
char BOOK_author[20];
int price;
}s3 = { "WT10010","自定义类型保姆级教学","梧桐",88 }; s4;
//s3,s4就是在结构体定义完后,直接定义变量,是全局变量。可以直接定义内容
int main()
{
struct BOOK s1 = { "WT1000011","C中常见的字符函数和字符串函数讲解","梧桐", 88 };
//在主函数中定义s1变量,是局部变量
struct BOOK s2 = { .price = 88,.BOOK_author = "梧桐",.BOOK_name = "结构体教学",.ID = "WT10010"};
printf("%s %s %s %d\n", s1.ID, s1.BOOK_name, s1.BOOK_author, s1.price);
printf("%s %s %s %d\n", s2.ID, s2.BOOK_name, s2.BOOK_author, s2.price);
printf("%s %s %s %d\n", s3.ID, s3.BOOK_name, s3.BOOK_author, s3.price);
return 0;
}
(6) 结构体内存对齐
我们已经掌握了结构体的基本使用了。现在我们深入讨论一个问题:计算结构体的大小。
-
计算结构体的大小(内存对齐)
- 先看一道例题
struct S1
{
char c1;
int i;
char c2;
};
struct S2
{
char c1;
char c2;
int i;
};
int main()
{
printf("%d\n", sizeof(struct S1));
printf("%d\n", sizeof(struct S2));
}
答案:12,8 。现在你心里肯定有点疑问,为什们明明结构体的内容都一样,就是顺序不一样,那为什么这个答案还不一样呢,有些没有基础的小白可能想
char
是一个字节,int
是4个字节,那们加起来不应该是6个字节吗?这个就涉及到结构体的内存对齐。等看完下面内存对齐我相信你一定对上面的题目豁然开朗。
-
内存对齐解释
-
首先我们先要了解一个函数(offsetof),能够反应偏移量的参数,所谓偏移量就是计算机汇编语言,是指把存储单元的实际地址与其所在段的段地址之间的距离称为段内偏移,也称为“有效地址或偏移量”。
我们先用这个函数看一下第一个结构体每个数据的偏移量然后再画图解释。
结构体的对齐规则
-
(1)第一个成员在与结构体变量偏移量为0的地址处。
(2)其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
vs的对齐数:8
linux环境默认不设对齐数(对齐数是结构体成员的自身大小)
(3)结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
(4)如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整
体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
下面就用规则解释为什们上面的结构体是这么分配的
-
请看下图
我相信大家应该能举一反三了吧,那么s2就自己动手算算吧,下来给大家算一道结构体嵌套的题目。用一下规则4.
struct S3
{
double d;
char c;
int i;
};
struct S4
{
char c1;
struct S3 s3;
double d;
};
int main()
{
printf("%d\n", sizeof(struct S4));
return 0;
}
我相信大家明白了结构体的内存对齐,那为什么要内存对齐呢?
为什么存在内存对齐?
- 平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。 - 性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
-
总体来说
- 结构体的内存对齐是拿空间来换取时间的做法。
(7)修改默认对齐数
之前我们见过了 #pragma 这个预处理指令,这里我们再次使用,可以改变我们的默认对齐数。
#include <stdio.h>
#pragma pack(1)//设置默认对齐数为8
struct S1
{
char c1;
int i;
char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认
int main()
{
printf("%d\n", sizeof(struct S1));
return 0;
}
位段
什么位段
-
位段的声明和结构是类似的,有两个不同
-
1.位段的成员必须是 int、unsigned int 或signed int ,现在又多了一个char。
2.位段的成员名后边有一个冒号和一个数字。
需要解释一下,位段的位是什么意思,其实是二进制位,看完下面代码就知道了
struct S
{
int a1;
int b1;
int c1;
int d1;
};
struct A
{
int a2 : 2;
//位段的位就是二进制位,2表示int a2占2个字节
int b2 : 5;
int c2 : 10;
int d2 : 30;
};
int main()
{
printf("%d\n", sizeof(struct S));//16
printf("%d\n", sizeof(struct A));//8
//对于为什们是8,我们就看下面的字段内存介绍
return 0;
}
(2) 位段的内存分配
-
规则
- (1). 位段的成员可以是 int unsigned int signed int 或者是 char (属于整形家族)类型
- (2). 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。
- (3). 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。
在使用的位段的时候有好多不确定性,就上上面他应该将剩余的空间用完,还是再开辟一个空间用呢。这都是不确定性的,我们看看再vs中他的内存分配。
struct S
{
char a : 3;
char b : 4;
char c : 5;
char d : 4;
};
//该字段再内存中怎么分配?
int main()
{
struct S s = { 0 };
s.a = 10;
s.b = 12;
s.c = 3;
s.d = 4;
return 0;
}
(3)位段的跨平台问题
- int 位段被当成有符号数还是无符号数是不确定的。
- 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机
器会出问题。 - 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
- 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是
舍弃剩余的位还是利用,这是不确定的
所以字段是不确定性的,就像一句话高风险高回报,
跟结构相比,位段可以达到同样的效果,但是可以很好的节省空间,但是有跨平台的问题存在
结构体的应用
通讯录的实现,请看我的下一篇文章《用C实现通讯录》
枚举
1.什么叫枚举
枚举顾名思义就是一一列举。
把可能的取值一一列举。
比如我们现实生活中:一周的星期一到星期日是有限的7天,可以一一列举。 性别有:男、女、保密,也可以一一列举。月份有12个月,也可以一一列举
2.枚举的定义
-
定义
-
enum
是枚举的关键字,枚举{}中的内容是枚举类型的可能取值,也叫 枚举常量 。它是将所有可能的取值都放在一块,默认从0开始,一次递增1,当然在定义的时候也可以赋初值。之后的值如果没有被定义,那么可能也是直接+1,如果被定义了,那他就是被定义的数。
注意,枚举定义的是常量,不可被修改。
我们可以看到在修改枚举变量时,是不可修改的,说明他是一个常量。
3.枚举的优点
-
为什么使用枚举?我们可以使用 #define 定义常量,为什么非要使用枚举?
-
枚举的优点:
(1). 增加代码的可读性和可维护性
(2). 和#define定义的标识符比较枚举有类型检查,更加严谨。
(3). 防止了命名污染(封装)
(4). 便于调试
(5). 使用方便,一次可以定义多个常量
4.枚举的大小
enum color { red, blue, green, };
可能有的人认为这是12个字节,因为这是3整型,那么这就是大错特错了,枚举的定义是什么,是将所有可能的值列出来,但是最后的结果只有一个呀,所以他的大小就是4个字节,一个整型的大小。
联合体(共同体)
(1)联合类型的定义
联合也是一种特殊的自定义类型,他的关键字是union
,这种类型定义的变量也包含一系列的成员,特征是这些成员公用同一块空间(所以联合也叫共用体)。
如果像下面这个代码,c,i,d的第一个二进制位都是一样的。看图。union Un
{
char c;
int i;
double;
};
union Un
{
char c;
int i;
double d;
};
(2) 联合的特点
联合的成员是共用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为联
合至少得有能力保存最大的那个成员)。
因为其共同使用一个空间可以用这个特点判断大小端
如果不知道什么是大小端,请点击这个,是我写的大小端的解释。
int check_sys()
{
union Un
{
int i;
char c;
}u;
u.i = 1;
//利用联合体的特点将这四个字节变为1
return u.c;
//返回第一个字节,也就是c所在的字节,
//如果是1,就是小端
//如果是0,就是大端
}
int main()
{
int ret = check_sys();
if (ret == 1)
{
printf("小端\n");
}
else
{
printf("大端\n");
}
return 0;
}
(3) 联合大小的计算
-
联合体大小
-
联合的大小至少是最大成员的大小。
当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。
例子:
对于这个联合体来说,c的对齐数应该是1,int的对齐数是4,
我们知道c[5],占5个字节,而int只占4个字节,但是最大成员大小是最大对齐数的整数倍
所以应该是8个字节。
总结
对于这些知识点,关键的还是怎么使用,不是你看一遍你就可以知道怎么用了,你需要多多的练习,掌握他的用法,更加熟悉在内存中的存储,我相信大家一定可以克服难关,将这些问题都解决,一起加油!!!😊😊