c语言自定义类型
前言
c语言中除了有整型、字符型等基本类型外,还有自定义类型:结构体类型、枚举类型、联合类型、数组,今天主要讲前三种。
1、结构体
结构是一些值的集合,这些值称为成员变量。结构体的每个成员可以是不同类型的变量,如标量、数组、指针、也可以是结构体。
1.1 结构体的声明
struct Stu { char name[20]; int age; double score; };
- 声明一个结构体类型的一般形式为:
struct 结构体名
{成员列表};
struct是结构体关键字、struct Stu 是结构体类型、Stu是结构体名、name、 age、 score是结构体成员变量名。- 上述结构体描述了学生的基本情况。
1.2 特殊的声明
在声明结构体的时候可以不用完全声明
struct //匿名结构体变量,只能用一次,如创建ss { char name[20]; float price; }ss;
- 这种结构体的不完全声明称为结构体的匿名结构体
- 这种结构体声明只能使用一次,即在结构体声明后,立即定义一个结构体全局变量,如代码中的结构体变量 ss,后面再主函数中不能再被使用来定义结构体变量了。
1.3 结构体的自引用
struct Node { int data;//数据域 struct Node* next;//指针域 };
- 结构体的自引用,一部分成员是自己储存的数据,另一部分成员储存的是指向下一个结构体的指针。
- 为什么自引用要储存指向下一个结构图体的指针?
因为如果是直接储存下一个结构体的所用成员变量,需要的储存空降过于庞大,并且下一个结构体有可能同时存储着下下一个结构体,实在是无法分配储存空间,所以结构体要自引用结构体的话,储存一个指向下一个结构体的指针更为方便,也更节省空间。
1.4 结构体变量的定义和初始化
struct Book { char name[20]; float price; char id[12]; }s1 = { "C语言",55.5f,"c001" }; //创建的时候同时初始化 struct Book s2 = { "算法",55.5f,"c005" }; struct Node { struct Book b; //结构体中包含另一个结构体,这里的结构体成员是一个结构体 struct Node* next; //结构体的自引用,存储指向下一个结构体的指针 }; int main() { struct Book s3 = { "数据结构",66.6f,"c002" }; //可以在主函数中创建变量再初始化 struct Node n = { {"操作系统",66.8f,"c003"},NULL }; return 0; }
- 声明一个结构体类型struct Book的时候也定义一个结构体变量s1,并且同时初始化了。
- 二是在结构体类型声明完后,在定义变量 struct Book s2;,同时将它初始化。
- s1与s2都是结构体全局变量,也可以在主函数内部定义结构体变量,如s3 与 n。此时的结构体变量是局部变量。
- 结构体变量 n进行了嵌套初始化,因为结构体类型 struct Node 中的成员有结构体类型。
1.5 结构体内存对齐
#define _CRT_SECURE_NO_WARNINGS 1 #include <stdio.h> struct S1 { char c1; //1 int i; //4 char c2; //1 }; int main() { struct S1 s; printf("%d\n", sizeof(s)); //结果为12个字节 也可以写成sizeof(struct S1); return 0; }
打印结果
- 结构体中的三个变量的大小分别为1个字节、4个字节、1个字节,为什么计算结构体的大小为12个自己?
答:结构体的储存方式要按照结构体对齐的方式储存。计算结构体的大小也可以写成 sizeof(struct S1);- 结构体对齐的规则是什么?
- 第一个成员在与结构体变量偏移量为0的地址处。
- 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数=编译器默认的一个对齐数与该成员大小的较小值。vs中默认的对齐数的值为8。
那么对于一个整型来说,它的大小为4个字节,取默认对齐数与整型大小的较小值,所以整型要对齐数为 4,整型要对齐到 4的整数倍的位置。
- 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
- 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
- 图解
第一个成员c1在偏移量为0的位置处。
第二个成员 i的大小为4个字节,默认对齐数为8,所以第二个成员的对齐数为4,偏移量1、2、3都不是4的倍数,第二个成员要对齐到偏移量 4的位置,依次往下对齐4 5 6 7。
第三个成员c2的大小为1个字节,默认对齐数为8,所以第三个成员的对齐数为1,偏移量8是1的倍数,对齐到偏移量8的位置。
此时结构体的大小为0~8共九个字节,第一个成员的对齐数为1、第二个成员的对齐数为4、第三个成员的对齐数为1,所以最大对齐数位4,9不是最大对齐数4的整数倍,继续往下占用,直到偏移量11的位置,此时结构体的大小为12,12为最大对齐数4的整数倍,满足条件。
c1 i c2 的偏移量是否真的如上图分析的那样,在0 4 8的位置处,为了验证,我们借助如下代码
#define _CRT_SECURE_NO_WARNINGS 1 #include <stdio.h> #include <stddef.h> struct S1 { char c1; //1 int i; //4 char c2; //1 }; int main() { struct S1 s; printf("%d\n", sizeof(s)); //结果为12个字节 也可以写成sizeof(struct S1); printf("---\n"); printf("%u\n", offsetof(struct S1, c1)); printf("%u\n", offsetof(struct S1, i)); printf("%u\n", offsetof(struct S1, c2)); return 0; }
打印结果
- offsetof - 宏,用来计算结构体成员起始位置的偏移量的,头文件为 stddef.h
- 结果显示的各成员起始位置的偏移量与分析相吻合。
结构体嵌套了结构体的情况分析
#define _CRT_SECURE_NO_WARNINGS 1 #include <stdio.h> struct S3 { double d; //8 char c; //1 int i; //4 }; struct S4 { char c1; //1 struct S3 s3; //16 double d; //8 }; int main() { struct S3 s3; struct S4 s4; printf("%d\n", sizeof(s3)); //结果为16个字节 printf("%d\n", sizeof(s4)); //结果为32个字节 return 0; }
打印结果
- 结构体 struct s3 的大小为16个字节,结构体 struct s4 嵌套了结构体 struct s3,其大小为32个字节。
- 结构体struct s4 图解
第一个成员c1在偏移量为0的位置处
第二个成员为一个结构体,大小经过计算为16个字节,默认对齐数为8,所以第二个成员的对齐数为8,偏移量1 2 3 4 5 6 7都不是8的倍数,所以第二个成员对齐到偏移量为8的位置处。依次往后对齐16个字节到偏移量为23的位置处。
第三个成员d 的大小为8个字节,默认对齐数为8,所以第三个成员的对齐数为8。偏移量24刚好为8的倍数,所以第三个成员对齐到偏移量为24的位置处,依次往下对齐8个字节到偏移量为31的位置处。
此时结构体的总大小为32个字节,第一个成员的对齐数为1、第二个成员的对齐数为8、第三个成员的对齐数8,所以最大对齐数为8,32为最大对齐数8的整数倍,满足条件,所以结构体 struct s4的大小为32个字节。- 结构体 struct s3在图解中看似占满了内存,他其实也是按照对齐规则来对齐的,按照对齐规则占满了16个字节,所以在 struct s4 的图解中为了方便,直接将16个字节画满了。
1.6 为什么存在内存对齐?
- 平台原因(移植原因)
不是所有的硬件都能访问任意地址上的任意数据;某些硬件平台只能在某些地址处某些特定类型的数据,否则抛出硬件异常。- 性能原因
数据结构(尤其是栈)应该尽可能在自然边界上对齐。
原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的的内存访问仅需要一次。如图
对于32位机器来说,一次访问四个字节。
如上图,结构体内存不对齐的情况下,访问成员整型数据 i,要先访问 0 1 2 3四个字节,再访问 4 5 6 7四个字节,一共访问了两次,才将数据 i 访问完毕;
结构体内存对齐,访问成员整型数据 i,直接访问 4 5 6 7四个字节,一次就访问完毕。
总的来说:
内存对齐就是拿空间换取时间的做法。
所以我们在设计结构体的时候,既要满足对齐,又要节省空间,为了满足此条件,需要做到:让占用空间小的成员尽量集中在一起。如下
struct S1 { char c1; //1 int i; //4 char c2; //1 }; struct S2 { char c1; //1 char c2; //1 int i; //4 }; int main() { struct S1 s; struct S2 s2; printf("%d\n", sizeof(s)); //结果为12个字节 也可以写成sizeof(struct S1); printf("%d\n", sizeof(s2)); //结果为8个字节 return 0; }
打印结果
- S1与S2类型成员一模一样,但是S1与S2所占空间的大小不一样。
- S2中占用空间小的成员集中在一起,所占内存空间比S1要小。
1.7 修改默认对齐数
#define _CRT_SECURE_NO_WARNINGS 1 #include <stdio.h> #pragma pack(4) //修改默认对齐数为4,如果修改默认对齐数为1,则不按对齐数的方式存储 struct S { char c; double d; }; #pragma pack() //取消设置的默认对齐数,表示改回来,这个后面写的结构体的默认对齐数又回到8 int main() { printf("%d\n", sizeof(struct S));//在默认对齐数为4后,大小变为12,本来是16 return 0; }
打印结果
- 利用预处理指令 #pragma 这个指令,可以修改默认对齐方式,修改方式为:
#pragma pack(修改的默认对齐数)- #pragma pack() ,取消设置的默认对齐数,将默认对齐数还原为8 ,这个指令后面的结构体的默认对齐数全为8,除非再次用预处理指令修改。
- 上述代码将结构体的默认对齐数修改为4,计算结构体大小得到的结果为12个字节,如果在结构体的默认对齐数为8的情况下,结构体大小为16个字节。
结论:结构在对齐方式不合适的时候,我们可以自己更改默认对齐数。
1.8 结构体传参
#define _CRT_SECURE_NO_WARNINGS 1 #include <stdio.h> 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); int i = 0; for (i = 0; i < 4; i++) { printf("%d ", s.data[i]); } printf("\n"); } void print2(struct S* ps) //形参用结构体指针接收结构体地址 { printf("%d\n", ps->num); int i = 0; for (i = 0; i < 4; i++) { printf("%d ", ps->data[i]); } printf("\n"); } int main() { print1(s); //传结构体 print2(&s); //传地址 return 0; }
打印结果
- 函数传参可以传结构体本身,形参用结构体类型接收
- 函数传参可以传结构体的地址,形参用结构体指针类型接收
- 结构体访问操作符:
. 结构体变量.结构体成员
-> 结构体指针变量->成员名 <===> * (结构体指针变量).成员名- 上述传参最好选函数 print2的方式,因为函数传参的时候,参数是需要压栈的,会有时间和空间上的系统开销。如果传递一个结构体对象的时候,结构体过大,参数压栈的系统开销比较大,所以会导致性能的下降。
2、位段
位段是通过结构体来实现的。
2.1 什么是位段
位段的声明和结构体是类似的,有两个不同
- 位段的成员必须是 int 、unsigned int 或signed int。
- 位段的成员名后面有一个冒号和一个数字。
位段代码展示
#define _CRT_SECURE_NO_WARNINGS 1 #include <stdio.h> struct AA { int _a; //大小为Min~Max int _b; int _c; int _d; }; struct A { int _a : 2; //_a这个成员只占2个bit位,两个bit位能组成的就是 00 01 10 11 四种值,所以int _a的大小为0~3 int _b : 5; //_b这个成员只占5个bit位 int _c : 10; int _d : 30; }; int main() { printf("%d\n", sizeof(struct A)); //结果为8个字节 printf("%d\n", sizeof(struct AA)); //结果为16个字节 return 0; }
打印结果
- 结构体的大小为16个字节,通过结构体实现的位段的大小为8个字节。
- 在位段后面分配的比特位 2+5+10+30=47,小于八个字节(8×8=64个比特位),为什么?
答:位段也存在内存对齐。
2.2 位段的内存分配
位段的内存对齐方式跟结构体是一样,只不过位段中的每个成员占据的不是成员类型本身该有的大小,而是冒号后面的数字,即分配的比特位数。
- 位段的成员可以是 int 、unsigned int 、signed int 或者是 char(属于整型家族)类型
- 位段的空间是按照需要以4个字节(int)或者一个字节(char)的方式来开辟的。
- 位段涉及很多不确定因素,位段是不夸平台的,注重可移植的程序应该避免使用位段。
以 2.1节中的代码为例,图解
根据结构内存对齐,结构体 struct AA 占据了16个字节
位段第一个成员是int 所以上来先开辟四个字节,四个字节共有32个比特位,第一个成员占据2个比特位;
第二个成员与第一个成员类型一致,且没有超过该成员类型的最大位数,不用重新分配空间(即不用重新开辟四个字节,如果第二个成员是char类型,则需要重新开辟一个字节,到偏移量位5的位置),占据5个比特位;
第三个成员与第二个成员类型一致,且没有超过该成员类型的最大位数,不用重新开辟空间,占据10个比特位;
第四个成员共有30个比特位,此时按照图上所示,还剩32-17=15个比特位,超过该成员类型的剩余位数,不够了,所以另开辟四个字节,占据30个比特位。综上,此位段共占据8个字节。- 此案例中位段的所有成员都是同类型,如果第一个成员是char类型,第二个成员是int类型,那么第二个成员要在对齐数为4的整数倍的位置,即偏移量为4的位置开辟四个字节。与结构体的内存对齐规则是一样。
结论:根据实际情况,用位段可以节省空间。
2.3 位段的跨平台问题
- int 位段被当成有符号数还是无符号数是不确定的
- 位段中最大位数的数目不能确定。16位机器最大16,32位机器最大32,如果在十六位机器中给int类型的变量分配30个比特位,会出问题。
- 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。在vs平台中,从右向左分配。
- 当一个结构包含两个位段,第二个位段的成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的。以2.2节中举例,第三个成员有10个比特位,而第一个字节还剩1个比特位(总的剩余位数是够的,不用重新开辟空间),第一个字节中剩下的那个比特位用还是不用,这是不确定的。
3、枚举
枚举即一一举例
enum是枚举的关键字
3.1 枚举的定义
#define _CRT_SECURE_NO_WARNINGS 1 #include <stdio.h> enum Sex { MALE, FEMALE, SECRET }; enum Day { Mon = 4, //初始化为4,本来是默认为0的 Tues, Wed, Thur, Fri, Sat, Sun }; enum Color { RED, GREEN, BLUE }; int main() { enum Sex s = MALE; //枚举类型创建变量,并且赋值 enum Sex s2 = FEMALE; enum Day d = Fri; enum Color c = GREEN; printf("%d\n", RED); //0 默认为从0开始 printf("%d\n", GREEN);//1 printf("%d\n", BLUE); //2 printf("---\n"); printf("%d\n", Mon); //4 可以通过初始化,将默认的0修改为4,后面的跟着变 printf("%d\n", Tues); //5 return 0; }
打印结果
- 以上定义的enum Sex 、enum Day 、enum Color都是枚举类型。
- {}中的内容是枚举类型的可能取值,也叫枚举常量。
- 这些可能取值都是有值的,默认从0开始,一次递增1,当然在定义的时候也可以初始化为其他值。如
enum Color
{
RED=1,
GREEN=2,
BLUE=4
};
3.2 枚举的有点
我们可以使用 #define定义常量,为什么非要使用枚举?
枚举的有点
- 增加代码的可读性和可维护性;
- 和 #define定义的标识符比较,枚举有类型检查,更加严谨。
- 防止了命名污染(封装)
- 便于调试。如 #define color 6 ,程序在预编译后,color其实已经是6了但是显示出来的还是color,不利于调试。
- 使用方便,一次可以定义多个变量。
3.3 枚举的使用
enum Color { RED=1, GREEN=2, BLUE=4 };
- 只能拿枚举常量给枚举常量赋值,才不会出现类型差异,如:enum Color clr = GREEN;
- clr = 2;这种写法是不被允许的,回出现编译错误,因为左边是枚举类型,右边是整型,有类型差异。
- 更准确说法叫初始化,因为枚举类型是常量,常量没有赋值,这里的 = 符号都是初始化。
4、联合体
4.1 联合类型的定义
#define _CRT_SECURE_NO_WARNINGS 1 #include <stdio.h> union Un { char c; int i; }; int main() { union Un u; printf("%d\n", sizeof(u)); //结果为4个字节 }
打印结果
- union是联合体的关键字
- 联合变量的定义 union Un u;
- 这个联合体大小为4个字节。
4.2联合的特点
#define _CRT_SECURE_NO_WARNINGS 1 #include <stdio.h> union Un { char c; int i; }; int main() { union Un u; printf("%p\n", &u); printf("%p\n", &(u.c)); printf("%p\n", &(u.i)); //发现共同体的地址与共同体中成员的地址是同一个, //说明他们是共用同一块内存空间,也表明了内存大小是4个字节 //共同体中的成员在同一时间只能操作访问其中一个 }
打印结果
- 发现共同体的地址与共同体中所有成员的地址是同一个,说明他们是共用同一块内存空间。当改变其中一个成员变量的地址时,所有成员的地址都会跟着变。
- 共同体中的成员在同一时间只能操作访问其中一个。类似于一个高校教师的身份,是正教授就不是副教授,是副教授就不是正教授。
根据这种特性,判断当前计算机的大小端存储,之前在数据存储中写过
#define _CRT_SECURE_NO_WARNINGS 1 #include <stdio.h> check_sys() { union Un { char c; int i; }u; u.i = 1; //01 00 00 00 四个字节 return u.c; // c与i占用的是同一个地址,返回c,即返回一个i的字节 } int main() { if (1 == check_sys()) { printf("小端\n"); } else { printf("大端\n"); } return 0; }
打印结果
- 图解
c与i是同一块内存,i是int类型,c是char类型,所以给i赋值一个 数字1,但是打印c,就只会打印一个字节,如果这个字节中存储的是01,说明是小段存储,存储的是00,则是大端存储。
4.3 联合体大小的计算
#define _CRT_SECURE_NO_WARNINGS 1 #include <stdio.h> union Un { char arr[5];//5 因为是字符型,对齐数是1 int i;//4 对齐数是4 }; int main() { printf("%d\n", sizeof(union Un)); //结果为8个字节,而不是5个字节,8是最大对齐数4的整数倍 return 0; }
打印结果
- 打印结果为8个字节,为什么?
联合体的大小至少是最大成员的大小,
当最大成员大小不是最大对齐数的整数倍的时候,这就要对齐到最大对齐数的整数倍。
这里的最大成员是数组char[5],5个字节,char类型的对齐数是1,int类型的对齐数是4,所以最大对齐数是4。5不是最大对齐数4的整数倍,所以对齐到8,即联合体的大小是8个字节。