所谓自定义类型,就是除去基本类型,用户可以自己定义的一些类型,一般有三种:结构体、枚举、联合。
结构体
首先要了解一种类型,需要依次;了解它的概念、声明、定义以及使用。下面我们就来依次看看结构体的声明、定义和使用。结构体这块还有两个比较陌生的东西:内存对齐和位段。没关系,我们一点一点往下看。
所谓结构体,就是由一系列具有相同类型或不同类型的数据构成的数据集合,也叫做结构。
如果我们想声明一个学生结构体类型,它的声明如下:
struct Stu
{
char name[20];//结构体成员变量
int age;//结构体成员变量
char sex[5];//结构体成员变量
};//注意这里的分号不能丢
像一般的基本类型如int float,声明只是告诉编译器有这个变量,还没有申请空间。并且这里我们要知道,这六行代代码只是声明了一个类型,就像int float 一样。
声明了类型之后,就可以用它来创建变量了。我们看看它是如何创建变量的:
1.可以这样创建在结构体类型的后面直接加上变量的名字
struct Stu
{
char name[20];//结构体成员变量
int age;//结构体成员变量
char sex[5];//结构体成员变量
}stu;
可以看到,这是在结构体后面直接加上了变量的名字,那么这里的这个变量就是一个全局变量,也就是说在全局范围内谁都可以使用,一般不推荐使用。
2.通过类型+变量形式创建,创建形式如int float类型:
struct Stu
{
char name[20];//结构体成员变量
int age;//结构体成员变量
char sex[5];//结构体成员变量
};
struct Stu stu;//通过类型加变量的形式
创建变量也叫实例化对象。我们知道定义变量后,系统就会给该变量分配一定的内存,那么结构体变量的内存是如何分配的呢?我们就用上面的结构体变量来说明一下结构体变量的内存是如何分配的,看下图:
了解了结构体类型的创建和结构体变量的创建的基本知识,下面我们来看看一个特殊的东西:结构体特殊声明, 即不完全声明。看下面这两个代码
struct
{
int a;
char b;
float c;
}x;
struct
{
int a;
char b;
float c;
}a[20],*p;
可以看到,这两个结构体声明都不像我们刚刚说的结构体声明那样,好像少了点东西,这种结构体类型叫匿名结构体类型,也就是省略了名字。那这种方法好不好呢?实际上我们是不推荐的,大家可以自己动手调试一下在上面代码的基础上,下面的代码可以编译成功并且运行成功吗
p=&x;
实际上你会发现,是不能的。编译器会把上面两个声明当成两个完全不同的类型。匿名结构体类型只能在结构体类型的后面直接创建变量,并且此变量只能用一次。
依旧是上面的例子,我们看到结构体内有成员变量name、age、sex。那我们如何对这些成员变量进行访问呢?在之前的操作符与运算符章节我们了解到有.和->运算符,它们的作用就是在这里。使用结构体变量名+.+成员变量名,就成功的访问了结构体的成员变量。当结构体访问指向变量的成员,我们得到的是一个指向结构体的指针,这时候访问结构体成员变量就需要使用结构体变量名+->+成员变量名来进行访问。下面我们看具体的代码,会更加直观一点(分别为一般结构体类型和指向结构体的指针):
struct Stu stu;//创建一个结构体变量
strcpy(stu.name,"zhangsan");//初始化成员变量name
s.age = 20;//初始化成员变量age
struct Stu
{
char name[20];
int age;//结构体成员变量
char sex[5];//结构体成员变量
};
我们知道,结构体中可以定义一些成员变量,那么结构体变量也是一个变量,我们可以在结构体内包含一个类型为该结构体本身的成员吗?答案是可以的。那么具体代码怎么实现呢?大家可能想当然的想出了下面这样的代码:
struct Node
{
int data;
struct Node next;
};
但是大家再仔细想想,这样真的可以吗?如果可以,sizeof(struct Node)是多少呢?可以自己调试一下。这样写的代码,sizeof(struct Node)是无限大的,因为结构体会一直不停的调用自己。那么我们应该怎么实现结构体内部包含自己呢?可以将这个在结构体内部的结构体指向自己:
struct Node
{
int data;
struct Node* next;
};
我们再来看一个代码:
struct A
{
int a;
struct B* pb;
}
struct B
{
int b;
struct A* pa;
}
这样可以编译成功并且运行成功吗?答案是不可以的。在A里面定义结构体成员变量B,B此时还没有定义,怎么指向B?那么这样的问题可以怎么解决呢? 我们可以在代码的最上面先不完整声明一个结构体成员变量B,向下面这样:
struct B;
struct A
{
int a;
struct B* pb;
}
struct B
{
int b;
struct A* pa;
}
了解了有关结构体的基本知识,下面我们来看刚刚说的比较陌生的两个东西:内存对齐和位段。
首先看内存对齐:
我们知道,只要是变量,都会有大小,也都可以计算出来。那么结构体也不例外,也有大小并且可以进行计算,那么它是怎样计算呢?是将里面变量的大小全部加起吗?我们说不是的,它有自己的计算方法:要遵循内存对齐规则。首先我们看下内存对齐规则都什么:
- 第一个成员在于结构体变量偏移量为0的地址处。
- 其他成员变量要对其到某个数字即对齐数的整数倍的地址处。对齐数=编译器默认的一个对齐数与该成员大小的较小值。
- 结构体总大小为最大对齐数即每个成员变量对齐数中的最大值的整数倍。
如果有嵌套结构体的情况,嵌套的结构体对齐到自己最大对齐数的整数倍。结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
只看这些干条条是不是很难理解?我们下面通过一个例子来具体说明一下,看代码:
struct Stu1
{
char c1;
int i;
char c2;
};
printf("%d\n",sizeof(struct S1);
下面的图片很好的说明了问题:
那么嵌套的结构体应该怎么计算呢,我们再来看一个例子:
struct Stu1
{
char c1;
char c2;
};
struct S
{
char c1;
struct Stu stu;
int b;
};
printf("%d\n",sizeof(struct S);
这个怎么计算看下面这幅图:
那么为什么会存在内存对齐呢?主要有两个原因:
- 平台移植原因:不是所有的硬件平台都能访问任意地址上的任意数据的,某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
- 性能原因:为了访问未对齐的内存,处理器需要做两次内存访问;而对齐的访问仅需要一次访问。
我们看到结构体对齐可能会造成一定的空间浪费,是拿时间来换取空间,那么我们在设计结构体的时候,既要满足对齐,又要节省空间,怎么做到呢?我们让占用空间小的成员尽量集中在一起。例如下面这两个代码:
struct Stu1
{
char c1;
int i;
char c2;
};
struct Stu2
{
char c1;
char c2;
int i;
};
大家可以计算一下,Stu1和Stu2的大小,显然,Stu2小的多。所以我们在进行设计结构体的时候,应该让占用空间小的成员尽量集中在一起。
再看位段:
我们首先来看下位段的定义:位段,C语言允许在一个结构体中以位为单位来指定其成员所占内存长度,这种以位为单位的成员称为“位段”或称“位域”( bit field) 。利用位段能够用较少的位数存储数据。
位段的声明和结构体是类似的,有两个不同:
- 位段的成员必须是int,unsigned int 或signed int。
- 位段的成员名后面有一个冒号和一个数字。
同样的,我们来看一个例子
struct A
{
int a:2;
int b:5;
int c:10;
int d:30;
};
printf("%d\n",sizeof(struct A));
上面例子中的A就是一个位段类型,那么这个位段的大小是多少呢?和结构体的计算方法一样吗?
大家可以试着编译并运行一下,看看是多少。
事实上,位段的内存分配也有三个规则:
- 位段的成员可以是int unsigned int、signed int或者是char类型。
- 位段的空间上是按照需要以4个字节或者一个字节的方式来开辟的。
- 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。
我们下面看一下上面那个例子具体是怎样计算位段类型A的大小的:
- 首先,可以看到每个成员的类型都是int,也就是该位段的空间是由4个字节的int方式来开辟的。
- 再看每一个成员,第一个成员a,冒号后面是2,所以占2个bit位,接着成员b,占5个bit位,然后成员c,占10个bit位,可以算出现在一个int(32个bit位)被占了(2+5+10)个bit位,接下来的成员d需要30个bit位来存放,可以看到第一个int已经不足以放这个成员了,所以现在有两种方法,一种是将这30个bit位全部放在新开辟的int中,一种是先将30个bit位的15个bit位放在第一个int,再将剩下的放在新开辟的int中。
- 最后,我们看到,两种方法都是需要开辟两个int的空间。
上面讲述了按照int即4个字节方式开辟空间,大家可以试一1个字节即char开辟空间是怎么计算的。
位段这么节省空间,但凡事都是利害相互的,所以位段还存在一些问题,最大的就是跨平台问题:
- int位段被当成有符号数还是无符号数是不确定的。
- 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32)
- 位段中的成员在内存中从左向右分配还是从右向左分配还没有定义。
当一个结构体包含两个位段,第二个位段成员较大,无法容纳第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的。
枚举
我们首先看一下枚举的官方定义:在数学和计算机科学理论中,一个集的枚举是列出某些有穷序列集的所有成员的程序,或者是一种特定类型对象的计数。是一个被命名的整常数的集合。那么通俗点讲就是一一列举。
比如周一到周天,一月到十二月,这些都可以被一一列举出来, 所以就可以使用枚举。
同样的,枚举是一种类型,那么我们就来依次看看它的定义即使用方法。
枚举的定义,直接看代码,直观易懂。
enmu Day
{
Mon,//0
Tue,//1
Wed,//2
Thu,//3
Fri,//4
Sat,//5
Sun,//6
};
这样就定义了一个枚举类型,其中花括号里面的Mon,Tue,等都是枚举类型的可能取值,也叫枚举常量。像注释里面的,这些常量是有值的,默认从0开始,以后依次+1。也可以在定义的时候赋初值,但不能在定义的外面赋初值。
例如
enmu Day
{
Mon = 1,//1
Tue = 2,//2
Wed,//3
Thu,//4
Fri,//5
Sat,//6
Sun,//7
};
这样是可行的。
了解了枚举的定义即使用之后,我们再来看看为什么要使用枚举?
- 枚举可以增加代码的可读性和可维护性。
- 和#define定义的标识符比较枚举有类型检查,更加严谨。
- 防止了命名污染。即一个变量的多次使用。
- 便于调试,使用方便。
联合(共用体)
联合也是一种特殊的自定义类型,同样的,我们来依次看看它的定义和使用。
联合定义的变量像结构体一样,也包含一系列的成员,但是特征是这些成员共用同一块空间,所以联合也叫共用体。
我们还是来举个例子说明一下,顺便看一下联合是怎样声明和定义的:
/*联合类型的声明*/
union Un
{
char c;
int i;
};
/*联合变量的定义*/
union Un un;//定义之后就会开辟相应的内存空间
/*计算联合变量的大小*/
printf("%d\n",sizeof(un));
我们知道,定义一个变量后,系统就会为其开辟相应的空间,那么联合是怎样开辟空间的呢?以上面这个例子为例,我们看下面这幅图:
可以看到,上面代码中还有计算联合变量大小的一句,那么联合变量的大小是怎么样计算的呢?是直接看最大成员的大小吗?还是将全部成员大小相加?
我们说联合成员的大小至少要是最大成员的大小,因为联合至少得有能力保存最大的那个成语。显然,这不是联合成员大小计算的全部规则:
联合大小的计算:
- 联合的大小至少是最大成员的大小。
- 当最大成员不是最大对齐数的整数倍时,就要对起到最大对齐数的整数倍。
我们还是用例子来具体说明下:
union Un1
{
char c[5];
int i;
};
上面这个例子,最大成员的大小时5,但是不是最大对齐数4的整数倍,所以要对齐到最大对齐数的整数倍上,所以Un1的大小为8。
直至现在,我们已经全部了解了结构体、枚举和联合类型,那么下面我们来看看联合和结构体的巧妙结合使用:
将long类型的IP地址转换为点分十进制的表示形式:
#include<stdio.h>
#include<stdlib.h>
/*定义联合类型ip_addr*/
union ip_addr
{
unsigned long addr;//长整型
/*联合类型内定义结构体类型,结构体类型内的4个char连续存放,和long型变量addr共用一块空间*/
struct
{
unsigned char c1;
unsigned char c2;
unsigned char c3;
unsigned char c4;
};
};
int main()
{
union ip_addr ip;
ip.addr = 177222989;
printf("%d.%d.%d.%d\n", ip.c4, ip.c3, ip.c2, ip.c1);//因为是小端存储,所以要从c4到c1依次存储
system("pause");
return 0;
}
运行结果如下图: