在C语言之中,我们之前学习了一些基本的数据类型,例如int、float等等,但是在现实的生活中我们有很多的事物并不只有一种类型。拿书来举例,作为一个复杂的对象它有书名、作者、出版社、定价、书号等等。这时候我们就需要中新的复合的类型来实例化。
结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。
struct Book
{
char Name[20];
char Author[20];
float Price;
};
上述就是一个结构体的简单地声明。在结构体的声明中还有着一种比较特殊的情况,就是在声明的时候,我们可以不完全声明。
struct
{
char Name[20];
char Author[20];
float Price;
}b2;
struct
{
char Name[20];
char Author[20];
float Price;
}*p;
int main(void)
{
p = &b2;
return 0;
}
我们可以在声明结构体的时候,使用如上面代码一样的方法,在编译上述代码的时候码,虽然p和b2结构里面的内容一样,但是编译器会发出警告。说明这两个变量的类型是不同的。
在结构之中还可以包含该结构本身的成员,但是我们需要使用如下的方式:
struct Node
{
int data;
struct Node* Next;//ok
};
struct Node
{
int data;
struct Node Next;//err
};
当我们使用第二种方式的时候会产生一个问题,就是该去如何计算结构体所占空间的内存?我们会发现如果是这样的结构自引用是无法计算所占内存的。所以我们要使用上面的这一种方法,使用指针的方式。使用这种方式,我们就可以完成我们数据结构之中的链表操作。
struct Point
{
int x;
int y;
}p1;
struct Point p = { x, y };//结构体变量的定义与初始化
struct Node
{
int data;
struct Point p;
struct Node* next;
}n1 = { 10, {4,5}, NULL }; //在定义结构体变量的基础上,进行初始化操作
struct Node n2 = { 20, {5, 6}, NULL };//结构体的嵌套初始化
结构体的基本使用上述已经介绍完了,下面我们来简单介绍一下结构体在内存中所占空间的大小,所谓的结构体对齐。如果你认为结构体所占内存的大小就是其内部所有的变量占有空间的相加那就大错特错了。我们先来看几个跟结构体内存对齐相关的代码。
struct S1
{
char c1;
int i;
char c2;
};
printf("%d\n", sizeof(struct S1));//12
struct S2
{
char c1;
char c2;
int i;
};
printf("%d\n", sizeof(struct S2));//8
struct S3
{
double d;
char c;
int i;
};
printf("%d\n", sizeof(struct S3));//16
struct S4
{
char c1;
struct S3 s3;
double d;
};
printf("%d\n", sizeof(struct S4));//32
再解释上述的结果之前先来了解一下什么是结构体对齐的规则:
结构体对齐的规则:
1. 第一个成员在与结构体变量偏移量为0的地址处。
2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
VS中默认的值为8
3. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
我们假设下面的表格就是内存条:
0 |
c1 | c1 | d |
1 | 空 | c2 | d |
2 | 空 | 空 | d |
3 | 空 | 空 | d |
4 | i | i | d |
5 | i | i | d |
6 | i | i | d |
7 | i | i | d |
8 | c2 | c | |
9 | 空 | 空 | |
10 | 空 | 空 | |
11 | 空 | 空 | |
12 | i | ||
13 | i | ||
14 | i | ||
15 | i |
我们以S1为例子进行说明:
它的第一个成员是char类型占1个字节,遵循第一条原则,把它放在0处;然后是int类型占四个字节对齐数为4和8中的较小数,显然为4,根据第二条规则1号地址处不是4的整数倍,因此空出3个字节,将int放在4号地址处;接下来是char类型对齐数是1和8之中的较小时,显然为1,因此位于第九个字节处;最后是第三条规则结构体的总大小是自己最大对齐数的整数倍。
综上所述,我们就得到了基本的判断结构体大小的规则,最后就是在结构体嵌套结构体时,要注意,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
有着两种说法:
1. 平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。2. 性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。
原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。总的来说,结构体的内存对齐就是以时间换取空间。为了达到这个目的我们就需要将占用空间的小的成员尽量集中在一起。
我们可以通过使用#pragma pack( )这个预处理指令,改变默认对齐数:
#include
#pragma pack(8)//设置默认对齐数为8
struct S1
{
char c1;
int i;
char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认
#pragma pack(1)//设置默认对齐数为1
struct S2
{
char c1;
int i;
char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认
int main()
{
printf("%d\n", sizeof(struct S1));//12
printf("%d\n", sizeof(struct S2));//6
return 0;
}
在之前的文章中我们介绍过结构体传参的方式有两种:
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.位段的成员必须是 int、unsigned int 或signed int ...。
2.位段的成员名后边有一个冒号和一个数字。
来举个例子:
struct A
{
int a : 2;
int b : 5;
int c : 10;
int d : 30;
};
我们可以来猜想下位段的大小是多少?//8 下面我们就来介绍一下位段的内存分配:
1. 位段的成员可以是 int unsigned int signed int 或者是 char (属于整形家族)类型
2. 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。
3. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。
struct S
{
char a:3;
char b:4;
char c:5;
char d:4;
};
struct S s = {0};
s.a = 10;
s.b = 12;
s.c = 3;
s.d = 4;
我们假设从第一个字节的右端开始放入:
10 - > 1010 取三位 - >010;第1个字节剩下5个bit位;
12 - > 1100 取四位 - >1100;放入第1个字节剩下的bit位中,剩下1个bit位;
3 - > 0011 取五位 - > 00011; 第1个字节的1个bit位放不下,放入第2个字节中,剩下3个bit位;
4 - > 0100 取四位 - > 0100;第2个字节的3个bit位放不下,放入第3个字节中;
总的结果如下表:再转化为十六进制得到 0x620304,在VS的编译器下运行时就可以得到这个结果。
原始字节储存 位段储存 |
0 0 0 0 0 0 0 0 0 1 1 0 0 0 1 0 |
0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 |
0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 |
1. int 位段被当成有符号数还是无符号数是不确定的。
2. 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机器会出问题。
3. 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
4. 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的。但是,位段可以很好地节省我们的内存。
位段可以应用在如下的报文之中,来节省内存。
枚举顾名思义就是一一列举。
把可能的取值一一列举。
enum Day//星期
{
Mon,
Tues,
Wed,
Thur,
Fri,
Sat,
Sun
};
enum Sex//性别
{
MALE,
FEMALE,
SECRET
};
enum Color//颜色
{
RED,
GREEN,
BLUE
}
以上定义的 enum Day , enum Sex , enum Color 都是枚举类型。
{ }中的内容是枚举类型的可能取值,也叫 枚举常量 。
这些可能取值都是有值的,默认从0开始,一次递增1,当然在定义的时候也可以赋初值。
enum Color//颜色
{
RED = 5,
GREEN = 4,
BLUE = 3,
YELLOW
}c;
int main(void)
{
c = YELLOW;
printf("%d", c);//4
return 0;
}
枚举的优点:
1. 增加代码的可读性和可维护性
2. 和#define定义的标识符比较枚举有类型检查,更加严谨。
3. 防止了命名污染(封装)
4. 便于调试
5. 使用方便,一次可以定义多个常量
当我们在使用switch语句进行编程时我们可以将case中的值变为枚举中的枚举常量,这样在有些情况下可以让我们直接匹配枚举常量而不是数字,更加简单地理解case语句要做什么。
联合也是一种特殊的自定义类型
这种类型定义的变量也包含一系列的成员,特征是这些成员公用同一块空间(所以联合也叫共用体)。
联合的小例子:
union Un
{
char c;
int i;
};
//联合变量的定义
union Un un;
//计算连个变量的大小
printf("%d\n", sizeof(un));
联合的成员是共用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为联合至少得有能力保存最大的那个成员)。
我们之前再数据的存储那篇文章中介绍过计算机的大小端字节序存储,我们就可以使用联合来进行判断:
#include
union un
{
char c;
int i;
};
int check_sys()
{
union un check;
check.i = 1;
return check.c;
}
int main(void)
{
int ret = 0;
ret = check_sys();
if (ret)
printf("小端");
else
printf("大端");
return 0;
}
以0x000001举例,在计算机中存储时,
若是大端存储的结果是:00 00 00 01;
若是小端存储的结果是:01 00 00 00;
我们以联合体int类型的成员存储1,然后使用char类型的成员读取1,就可得出结果。
联合体在内存中的存储也和结构体类似:
联合的大小至少是最大成员的大小。
当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。
union Un1
{
char c[5];
int i;
};
union Un2
{
short c[7];
int i;
};
printf("%d\n", sizeof(union Un1));//8
printf("%d\n", sizeof(union Un2));//16