c语言本身携带了一些内置类型(char、short、int、long、float等)可以直接使用的类型,与内置类型相对的一种,叫自定义类型。生活中一些类型不能用内置类型进行简单的表示的(如:复杂对象书、人等),所以C语言就给用户一定权力,可以自己构造类型。这就是自 定义类型,自定义类型有结构体(struct)、枚举(enum)、联合体(union)。
目录
前言:
1、结构体
1.1、结构体的基础知识
1.2、结构的声明
1.3、特殊的声明
1.4、结构体的自引用
1.5、结构体变量的初始化
1.6、结构体内存对齐
1.6.1、【问题】计算结构体的大小
1.6.2、【解决方法】
1.7、为什么存在内存对齐?
1.8、修改对齐数
1.9、结构体传参
2、位段
2.1、什么是位段
2.2、位段的内存分配
2.3、位段的跨平台问题
3、枚举
3.1、枚举类型的定义
3.2、枚举类型使用
给枚举常量赋一个初始值
3.3、枚举的优点
4、联合体(共用体)
4.1、联合体类型的定义
4.2、联合体的特点
4.3、联合体的使用
4.4、联合体大小的计算
结构是一些值得集合,这些值称为成员变量,结构的每个成员可以是不同类型的变量。C语言提供了关键字struct来标识所定义的结构体类型。
图解:
代码演示:描述一本书
//书名+作者+定价+书号
struct Book//struct是结构体关键字,Book是这个结构体标签,结构体标签是根据自己的需要定义的
{
char book_name[20];
char author[20];
int price;
char id[15];
};//结构体大括号后的;不能丢
在上述结构体类型中,并没有variable-list,在这里就引出了一个知识点,结构体变量的创建
结构体变量的创建有两种情况:
1>、像图解中variable-list是结构体成员变量列表,可以在这里创建变量,在这里创建的变量 是全局变量。
2>、是结构体类型构造好之后,需要使用到该结构体类型时,再创建变量。这种变量的创建 是局部变量。
//1>、情景一
struct Book
{
char book_name[20];
char author[20];
int price;
char id[15];
}a1,a2,a3;//这里创建的变量是全局变量,因为这里的变量在所有的大括号之外
//在这里创建变量是可选的,需要时创建,不需要可以不创建
//2>、情景二
struct Book
{
char book_name[20];
char author[20];
int price;
char id[15];
};
int main()
{
struct Book a1;//局部变量
struct Book a2;//局部变量
return 0;
}
注释:构造结构体类型不开辟空间,只有在创建结构体变量时,才开辟空间。
1、匿名结构体类型(在声明结构的时候,可以不完全的声明。)
struct //当把结构体标签去掉,若在后边不直接创建变量,在C语言的语法中是不通的。
{ //像要构造的结构体可以使用,就需要在后边直接创建变量,构成匿名结构体类型
char book_name[20];
char author[20];
int price;
char id[15];
}a1,a2,a3;//匿名结构体类型
注释:匿名结构体类型在创建变量的时候只能使用一次,一次用完之后就不能再创建变量。
2、匿名结构体类型指针和匿名结构体
struct
{
char book_name[20];
char author[20];
int price;
char id[15];
}a1 = {"cyuyan","xxxx",20,"1234"};
struct
{
char book_name[20];
char author[20];
int price;
char id[15];
}*p;//在这里创建的变量,为结构体类型的指针变量,
//这个指针变量并不能指向上边的结构体变量
int main()
{
p = &a1;
return 0;
}
虽然类型成员名相同,但是在编译器看来是两个不同的类型。
数据在内存中的存储结构有
1>、线性数据结构:顺序表,链表
2>、树形数据结构:二叉树
在这里浅浅的了解一下以链表的方式,一组数据在内存中的存储方式
通过找到结点一,再通过指针,指向下一个结点,依次找到在内存中存储的数据。
这里可以通过结构体的自引用来实现。
struct Node
{
int data;
struct Node* next;//通过指针找到下一个结点
};
在结构体创建好之后,每次使用该结构体时都要输入struct Node ,如若感到麻烦可以通过typedef重命名结构体
typedef struct Node
{
int data;
struct Node* next;
}Node(重命名);//这里的Node表示struct Node(结构体类型),不能认为是结构体变量。
int main()
{
struct Node n;//创建结点n
Node n;//这里的Node就相当于struct Node
return 0;
}
在使用该结构体时只有重命名之后才能使用Node。
1>、定义变量时初始化
//1>、情景一
struct book
{
char name[10];
int price;
}s1={"cyuyan",20};
//2>、情景二
struct book
{
char name[10];
int price;
};
int main()
{
struct book s1={"cyuyan",20};
return 0;
}
2>、结构体中包含结构体成员的初始化
1>、情景一
struct Stu
{
char name[20];
int age;
char id[12];
};
struct Book
{
char book_name[20];
char author[20];
int price;
char id[15];
struct Stu s;
}sb3 = {"C语言","xxxx",54,"PG1001",{"lisi",30,"20220101"}};
2>、情景二
//与第一种的情景二相似
#include
struct S1
{
char c1;
int i;
char c2;
};
struct S2
{
char c1;
char c2;
int i;
};
int main()
{
struct S1 s1;
printf("%d\n", sizeof(struct S1));//计算结构体(struct S1)类型的大小
printf("%d\n", sizeof(struct S2));
return 0;
}
结构体struct S1和struct S2的成员只是位置不相同,为什么结构体大小就不相同。
1、 在解决这个问题之前先来了解一个宏
offsetof ( type , member )
1>、功能
offsetof 用来计算结构体成员对于起始位置的偏移量
2>、参数
type 告诉结构体成员类型(告诉offsetof是那个结构体类型)
member 类型的成员名
3>、 头文件为
2、再了解结构体内存对齐规则
1、第一个成员在于结构体变量偏移量为0的地址处。
2、其他成员变量对齐到某个数字(对齐数)的整数倍的地址处。
对齐数 = 编辑器默认的一个对齐数与该成员大小的较小值。
vs编译器默认对齐数为8;linux环境下默认不设对齐数(对齐数是结构体的自身大小)。
3、结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
4、如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
1、举例一
1>、 代码演示:
#include
#include
struct S1
{
char c1;
int i;
char c2;
};
int main()
{
printf("%d\n", offsetof(struct S1, c1));//计算结构体struct S1成员中c1的偏移量
printf("%d\n", offsetof(struct S1, i));
printf("%d\n", offsetof(struct S1, c2));
return 0;
}
2>、画图解释
2、举例二:结构体中嵌套结构体
1>、代码演示:
#include
#include
struct S1
{
char c1;
int i;
char c2;
};
struct S2
{
char c1;
struct S1 s1;
int i;
};
int main()
{
printf("%d\n", sizeof(struct S1));
printf("%d\n", sizeof(struct S2));
return 0;
}
2>、画图解释
大部分参考资料给了两个点
1、平台原因(移植原因)
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址 处取某些特定的数据,否则抛出硬件异常。
2、性能原因
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。
原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅 需要一次访问。
图例讲解:
总结:
结构体的内存对齐是拿空间来换取时间的做法。
那在设计结构体的时候,我们既要满足对齐,又要节省空间,就应该让占用空间小的成员尽量集中在一起。
举例:
#include
struct S1
{
char c1;
int i;
char c2;//12
};
struct S2
{
char c1;
char c2;
int i;//8
};
相较而言,第二种构建方式更节省空间。
#pragma pack(1)//将默认对齐数修改为1
#pragma pack()//取消设置的默认对齐数,恢复到编辑器默认的对齐数
一般在设置默认对齐数时,是以2^x来设置,一般来说都是偶数,很少设置为奇数,因为计算机在读取时,要不读取4个字节,要不读取8个字节。
struct S
{
int data[1000];
int num;
};
void print1(struct S s)
{
printf("%d %d %d %d\n",s.data[0],s.data[1],s.data[2],s.num);
}
int main()
{
struct S ss = {{1,2,3,4,5},100};
print1(ss);
}
struct S
{
int data[1000];
int num;
};
void print(struct S* ps)
{
printf("%d %d %d %d\n",(*ps).data[0],(*ps).data[1],(*ps).data[2],(*ps).num);
printf("%d %d %d %d\n",ps->data[0],ps->data[1],ps->data[2],ps->num);
}
int main()
{
struct S ss = {{1,2,3,4,5},100};
print2(&ss);
}
上面两种传参方式,那个更好?
函数传参时,参数是需要压栈的,会有空间上的系统开销。
如果传递一个结构体对象的时候,结构体过大,参数压栈的系数开销比较大,所以会导 致性能的下降。
结论:
结构体传参的时候,要传结构体地址。
位段的声明和结构体时类似的,有两个不同:
1、位段的成员类型必须是int、unsigned int或signde int。
2、位段的成员后边有一个冒号和一个数字。
下面俩看一个代码:
提示:位段其中的位表示的是二进制位。
#include
struct A
{
int _a:2;//:2表示成员_a只需要2个比特位
int _b:5;//:5表示成员_b只需要5个比特位
int _c:10;//:10表示成员_c只需要10个比特位
int _d:30;//:30表示成员_d只需要30个比特位
};
int main()
{
printf("%d\n",sizeof(struct A));
return 0;
}
_a占2个比特位,_b占5个比特位,_c占10个比特位,_d占30个比特位,将其相加位47个比特位,1字节 = 8bit,那么占6个字节的大小。与编译器显示结果不同,那么位段是有自己的内存分配规则。接下来了解一下位段的内存如何分配。
1、位段的成员可以是 int , unsigned int , signed int 或者是char(属于整型家族) 类型
2、位段的空间上是按照需要以4个字节 (int) 或者1个字节 (char) 的方式来开辟的。
3、位段涉及很多不确定因素,位段是不跨平台的,注意可移植的程序应该避免使用位段。
下面我们通一段代码来了解位段的内存如何分配
struct S
{
char a : 3;//位段S的成员类型是char,先来申请1个字节的大小,
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;
}
在vs2019的环境下
上述代码中位段s,先开辟一个字节的空间(8bit),位段S规定成员a占用3bit空间,当a= 10 时,二进制为1010只取三位,成员b占4个字节的位段,到这时第一次申请的空间只剩余一个bit的大小,成员c占5个bit的空间,剩余空间不满足,第二次开辟一个字节的空间,在一个字节的空间中,从低位到高位存储,剩余空间抛弃,重新开辟新的空间,按照这样的规律,将所有的成员都放下。
注释:由于位段的不确定性,位段不支持跨平台使用
1、int位段被当成有符号数还是无符号数是不确定的。
2、位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机器会出问题)。
3、位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
4、当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的。
总结:
跟结构体相比,位段可以达到同样的效果,但是可以很好的节省空间,但是有跨平台的问题存在。
枚举顾名思义就是一一列举。
把可能的取值一一列举。
比如现实生活中:
一周星期一到星期日是有限的7天,可以一一列举。
#include
enum Color
{
//枚举的这些可能取值就是常量————枚举常量
//枚举的可能取值都是有值的。
RED,//在这里枚举的默认值为0;他的特点就是,不论起始值是多少,后面都是增加一的。
GREEN,
BLUE
};
int main()
{
enum Color color = BLUE;//创建枚举类型的变量color
//RED = 2;这句代码是运行不了的,因为RED是常量,常量不可修改。
printf("%d\n",RED);
printf("%d\n",GREEN);
printf("%d\n",BLUE);
return 0;
}
枚举常量的值分别是
//1.第一种赋值方式
enum Color
{
RED = 5,//即使RED是常量,但是在刚开始是可以赋一个值的,可以凭自己的需要赋值。
GREEN = 9,
BLUE = 10
};//这样赋值是可行的
//2.第二种赋值方式
enum Color
{
RED = 5,//给第一个可能取值赋值,不论起始值是多少,后面的都加一
GREEN ,
BLUE
};
我们可以使用#define定义常量,为什么非要使用枚举?
枚举的优点:
1、增加代码的可读性和可维护性
2、和#define定义的表示符比较枚举有类型检查,更加严谨。
3、防止了命名污染(封装)
4、便于调试
5、便于使用,一次可以定义多个变量
代码演示:将通讯录代码中的菜单通过枚举进行改造,可以使写的时候更方便也可以增强代码的 可读性和可维护性。
//1.增加代码的可读性和可维护性
void menu()
{
printf("************************************\n");
printf("****** 1.add 2.del *****\n");
printf("****** 3.search 4.modify *****\n");
printf("****** 5.show 6.sort *****\n");
printf("****** 0.exit *****\n");
printf("************************************\n");
}
enum Option//将上述菜单中的操作全部枚举出来
{
EXIT,
ADD,
DEL,
SEARCH,
MODIFY,
SHOW,
SORT
};
在接下来的代码中,实现上述功能是不需要再看,相应的共能对应的序号,这样就增加了代码的可维护性和可读性
联合体也是一种特殊的自定义类型 这种类型定义的变量也包含了一系列的成员,特征是这些成员公用同一块空间(所以联合也叫共用体)。union为联合体的关键字
//联合体的声明
union Un
{
char c;
int i;
};
//创建联合体变量
union Un un;
联合体的成员是共用同一块内存空间,这样一个联合体变量的大小,至少是最大成员的大小(因为联合至少得有能力保存最大的那个成员)。
#include
union Un
{
char c;
int i;
double d;
};
int main()
{
union Un un;//创建联合体变量
printf("%p\n",&un);
printf("%p\n",&(un.c));
printf("%p\n",&(un.i));
printf("%p\n",&(un.d));
return 0;
}
看到上述代码运行的结果,四个地址都是相同的,这就是联合体共用同一块空间。在取出联合体地址时,取出的都是第一个字节的地址,与结构体不同,结构体成员都有自己的空间。
画图理解:
联合体的一个使用场景:
在前面讲过判断当前机器的大小段字节序,在这里用联合体的知识点进行改造。
int check_sys()
{
union Un
{
char c;
int i;
}u;
u.i = 1;
return u.c;
}
int main()
{
int ret = check_sys();
if (ret == 1)
printf("小端\n");
else
printf("大端\n");
return 0;
}
注释:通过给联合体成员i赋值,在内存空间中i占4个字节空间大小,再输出c,在同一块内存空间中,c只占一个内存空间的大小,所以通过输出c的的结果1或0来判断当前机器的大小端字节序。
图片解释:
- 联合体的大小至少是最大成员的大小。
- 当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。
union Un
{
char arr[5];//5 1 8 1 虽然是数组,但是成员大小是按char来算的
int i;//4 8 4
//共用体的最大对齐数为4;成员占用最大空间为5,所以共用体占用空间为最大对齐数的整数倍,至少是最大
//成员的大小.
};
int main()
{
printf("%d\n", sizeof(union Un));//结果为8
return 0;
}
- 联合体大小至少为最大成员的大小我为5
- 成员一虽然为数组,但是计算对齐数的时候还是以char来计算,本身大小为1,默认对齐数为8,对齐数为1
- int自身大小为4,默认对齐数为8,对齐数为1
- 显然5不是最大对齐数4的整数倍,所以输出结果为8。
在看一个例子
union Un
{
short arr[7];//14 2 8 2
int i;//4 8 4
};
int main()
{
printf("%d\n", sizeof(union Un));//输出结果为16
return 0;
}
注释:
第一个成员的对齐师叔为2,占用空间为14
第二个成员的对齐数为4,最大成员为14,不是最大对齐数的整数倍,所以共用体的空间大小为16