C语言本身提供一些基本的内置类型(char,short,int,float,double等)用来定义简单对象,但却难以定义一些复杂对象(人,书等),因此C语言还提供了自定义类型。自定义类型顾名思义就是程序员可以自己定义的类型,它包括结构体,联合体和枚举。
目录
结构体
结构是一些值的集合,结构的每个成员可以是不同类型的值。
1.结构体声明
创建一个包含书名和书本价格的数书的类型如下:
struct book//struct为结构体关键字,book为结构体标签 { char name[10];//书名为字符串 int price;//价格为整数 };//整体就是自定义的书的类型
结构体可以不完全声明,当book标签没写时为匿名结构体类型:
struct { char name[10]; int price; };
2.结构体定义
1.在声明类型的同时定义变量,如:
struct book { char arr[10]; int price; }book1,book2;//定义两个struct book类型的变量book1和book2
2.创建完之后声明变量
struct book book1,book2;//定义两个struct book类型的变量book1和book2
* 匿名结构体只能在创建变量的同时
*下面两个匿名结构体变量a1和a2为两种不同的结构体类型
struct { char name[10]; int price; }a1; struct { char name[10]; int price; }a2;
3.结构体的自引用
在结构中包含一个类型为该结构本身的成员:错误示例1:struct Node { int data; struct Node next; };
这种引用是非法的,因为成员next是另外一个完整的结构,其内部还将包含它自己的成员next,这样重复下去将永无止境。类似一个永远没有出口的递归,无法知道sizeof(struct Node)有多大
错误示例2:
typedef struct { int data; Node* next; }Node;
这种引用也是非法的,结构体声明到末尾端才定义,但其内部却已经使用了
正确示例:
struct Node { int data; struct Node* next;//结构体里包含一个指向下一结构体地址的指针 };
这个声明和前面那个声明的区别在于b现在是一个指针而不是结构,编译器在结构的长度确定之前就已经知道了指针的长度
4.结构体的初始化
struct Point { int x; int y; }p1={2,4};//定义变量的同时进行初始化 struct point p2={2,3}//定义变量的同时进行初始化 struct point p3;//先定义 p3={2,1};//后初始化 struct book { int a; struct Point b; }ch={1,{2,3}};//结构体嵌套初始化
5.结构体内存对齐
结构体所占内存大小并不是简单地将其成员所占大小加起来,而是根据对齐规则进行占据。
1. 第一个成员在与结构体变量偏移量为0的地址处。
(偏移量的大小:起始地址的偏移量为0,下一字节偏移量为1,以此类推)
2. 其他成员变量要对齐到偏移量为某个数字(对齐数)的整数倍的地址处。
(对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。)
3. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体整
体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。C语言还提供了个宏:offsetof,它可以返回一个结构体成员在这个类型创建中的偏移量
offsetof的使用样例
那么为什么结构体会存在内存对齐呢?
一般认为有两个方面的原因:
1. 平台原因 :不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。2. 性能原因:数据结构 ( 尤其是栈 ) 应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需一次访问。总的来说,结构体的内存对齐是拿空间 来换取 时间的做法,我们在设计结构体的时候最好让占用空间小的成员机集中在一起当我们想要修改编译器的默认对齐数时也可以通过以下操作进行修改:#pragma pack(4)//将默认对齐数修改为4 struct S { int i; double d; }; #pragma pack()//恢复默认对齐数
6.结构体传参
结构体传参有两种
1.传结构体
2.传址
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.位段的声明
位段的声明和结构类似,有两个不同:
1. 位段的成员必须是 int 、 unsigned int 或 signed int 。2. 位段的成员名后边有一个冒号和一个数字。struct A { int a:2;//为a分配2字节大小的空间 int b:5;//为b分配5字节大小的空间 int c:10;//为c分配10字节大小的空间 int d:50;//为d分配50字节大小的空间 }
可以看出位段可以节省空间,但位段在内存分配上仍会浪费一定空间
2.位段的内存分配
1. 位段的成员可以是 int unsigned int signed int 或者是 char (属于整形家族)类型2. 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。3. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。下面为vs编译器测试下的位段内存分配状态:3.位段的跨平台问题
1. int 位段被当成有符号数还是无符号数是不确定的。2. 位段中最大位的数目不能确定。( 16 位机器最大 16 , 32 位机器最大 32 ,写成 27 ,在 16 位机器会出问题。3. 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。4. 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的。总的来说跟结构相比,位段可以达到同样的效果,但是可以很好的节省空间,但是有跨平台的问题存在。位段的应用网络封装数据包时使用位段能有效缩短数据包的大小,改善网络状态。
枚举
枚举就是将有限可能的值一一列举。
1.枚举的定义
下面是一个关于星期的枚举定义:
enum Day//星期 { Mon,//星期一 Tues,//星期二 Wed,//星期三 Thur,//星期四 Fri,//星期五 Sat,//星期六 Sun//星期天 };
其中enum为枚举关键字;Day为枚举标签;enum Day为枚举类型;Mon,Tues,Wed,Thur,Fri,Sat,Sun为枚举常量。
2.枚举的取值
枚举常量的值默认从0开始,每个枚举常量比上一个数值多1:
当给某个枚举常量赋值时,这个常量后面的值为前一个值加1
3.枚举的意义
1. 增加代码的可读性和可维护性(如在switc语句中使用枚举类型)2. 和 #define 定义的标识符比较枚举有类型检查,更加严谨。(枚举是一种类型)3. 防止了命名污染(封装)4. 便于调试(在.c文件在预处理阶段中标识符常量会被对应数值替换掉,但枚举类型不会被对应的值替换掉)5. 使用方便,一次可以定义多个常量
联合
联合类型定义的变量包含一系列的成员,特征是这些成员公用同一块空间(所以联合也叫共用体)。1.联合的声明和定义
//联合类型的声明 union Un { char c; int i; }; //联合变量的定义 union Un un;
2.联合的大小
联合也存在内存对齐,大小应满足所以成员中最大对齐数的整数倍
3.联合的妙用---判断当前计算机的大小端存储
关于C语言自定义类型的介绍就到这里了哈,咋们下期不见不散~