文章目录
前言
C语言的数据类型包括基本类型(内置类型)、构造类型(自定义类型)、指针类型和空类型(void),其中基本类型就是我们常见的整形、浮点型等,而自定义类型则包括数组、结构体、字段、枚举、联合(共用体),数组我们已经非常熟悉了,今天我们主要学习自定义类型中其他几种类型:结构体、字段、枚举以及联合。
一. 结构体
- 结构体基础知识
C语言提供了两种聚合数据类型用来存储超过一个的单独数据:数组和结构,数组是相同类型元素的集合,每个元素通过下标访问或指针间接访问来选择。结构是不同类型数据的集合,这些数据称为它的成员,由于一个结构的成员可能长度不同,所以不能用下标访问它们,相反,每个结构成员都有它的名字,它们是通过名字访问的。
1. 结构体类型的声明
- 一般声明
在声明结构时,必须列出它包含的全部成员的类型和名字
struct tag//标签
{
member-list//成员列表
}varible-list;//变量列表(可省略)
struct human
{
char name[10];
int age;
char sex[5];
};
- 特殊声明
结构体声明的时候,省略结构体标签,这种结构体被称为匿名结构体
struct //匿名结构体
{
int a;
char b;
float c;
}x;
由于匿名结构体没有名字,所以不能在程序的其他位置使用该结构体创建结构体变量,而只能在结构体声明的同时定义结构体变量,也就是说,匿名结构体只能使用一次。
以下是不正确使用
struct //匿名结构体
{
int a;
char b;
float c;
};
struct x;//error
声明匿名结构时可以使用的另一种良好技巧是用typedef创建一种新的类型
typedef struct
{
int a;
char b;
float c
}simple;
simple x;
这个技巧和声明一个结构体标签的效果几乎相同,区别在于simple现在是个类型名而不是个结构标签。
2. 结构的自引用
在一个结构体内部包含一个类型为该结构本身的成员是否合法呢?
struct s1
{
int a;
struct s1 b;
int c;
};
这种类型的自应用是非法的,因为成员b是另一个完整的结构,其内部还将包含自己的成员b,这样重复下去永无止境,像是一个永远不会终止的递归程序。下面的声明才是合法的:
struct s1
{
int a;
struct s1* b;
int c;
};
这个声明和前面那个声明的区别在于b现在是一个指针而不是结构,编译器在结构的长度确定之前就已经知道指针的长度,所以这种类型的引用是合法的。
注意警惕下面这个陷阱:
typedef struct
{
int a;
simple* b;
int c;
}simple;
这个声明的目的是为结构创建类型名simple,但是是非法的,类型名直到声明的末尾才定义,所以在声明的内部是无法使用的。
3. 结构体变量的定义和初始化
结构体定义变量一共有两种方式,一种是在进行结构体声明的同时定义结构体变量,另一种是利用结构体类型来定义结构体变量。
struct S
{
int x;
int y;
}s1; //声明类型的同时定义变量s1
struct S s2; //利用结构体类型来定义变量p2
结构体变量的初始化和数组变量的初始化一样,在定义结构体变量的同时赋初值即可。
struct Stu
{
char name[10];
int age;
}s1 = { "wei", 20 };
struct Stu s2 = { "yj", 21 };
struct Node
{
int data;
struct Stu s1;
struct Stu* next;
}n1 = {10, {"wei",19}, NULL}; //结构体嵌套初始化
struct Node n2 = {20, {"yj", 22}, NULL}; //结构体嵌套初始化
4. 结构体内存对齐
结构体内存对齐是结构体大小的计算规则,经常出现在校招笔试和面试,对于下面的题,如果你没有了解过内存对齐规则,只凭借自身朴素的猜想,是很难做对的
//计算结构体大小
#include <stdio.h>
struct S1
{
char c1; //1
int i; //4
char c2; //1
};
struct S2
{
char c1; //1
char c2; //1
int i; //4
};
int main()
{
printf("%d\n", sizeof(struct S1));
printf("%d\n", sizeof(struct S2));
return 0;
}
- 结构体内存对齐规则
第一个成员在与结构体变量起始地址偏移量为0的地址处
其他成员变量要对齐到偏移量为它的对齐数的整数倍的地址处
对齐数 = 编译器默认的对齐数与该成员变量大小的较小值
VS的默认对齐数是8.
只有VS编译器下才有默认对齐数的概念,其他编译器下变量的对齐数 = 变量的大小
结构体总大小为最大对齐数的整数倍。(最大对齐数为所有成员的对齐数的最大值)
如果嵌套了结构体的情况,嵌套的结构体的对齐数是自己的最大对齐数,结构体的整体大小为所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
struct S1
{
char c1; //变量大小为1,默认对齐数为8 -> 对齐数为1
int i; //变量大小为4,默认对齐数为8 -> 对齐数为4
char c2; //变量大小为1,默认对齐数为8 -> 对齐数为1
}; //最大对齐数为4
struct S2
{
char c1; //变量大小为1,默认对齐数为8 -> 对齐数为1
char c2; //变量大小为1,默认对齐数为8 -> 对齐数为1
int i; //变量大小为4,默认对齐数为8 -> 对齐数为4
}; //最大对齐数为4
- 为什么存在内存对齐
- 平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常
- 性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。
原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
总体来说:结构体的内存对齐是拿空间来换取时间的做法。
- 减少空间损失
- 系统禁止编译器在一个结构的起始位置跳过几个字节来满足边界对齐要求,因此你可以在声明中对结构的成员重新排列,让那些对齐数大、对边界要求最严格的成员首先出现,这样可以最大程度减少因边界对齐而带来的空间损失,例如,下面这个结构
struct S1
{
int i; //4
char c1; //1
char c2; //1
};
所包含的成员与上面那个结构一样,但是只占了8个字节,节省了很多空间。
- 修改默认对齐数
我们可以使用 #pragma 这个预处理指令,改变我们的默认对齐数
#include <stdio.h>
#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));
printf("%d\n", sizeof(struct S2));
return 0;
}
sizeof操作符能够得出一个结构的整体大小,包括因边界对齐而跳过的那些字节,如果你必须确定结构某个成员的具体位置(偏移量),应该考虑边界对齐因素,可以使用offsetof宏(定义于stddef.h)
size_t offset = offsetof(struct s1, i) //参数分别是结构体类型和成员名, 结果是size_t类型
5. 结构体传参
结构体传参有两种方式:一种是传递整个结构体,这时函数形参需要创建一个与原结构体同等大小的空间来接收,也就是把结构体拷贝一份压入堆栈,结构体过大浪费空间的同时会十分影响效率,并且这是传值调用,函数处理的是原结构体的一份拷贝,不会改变原结构体;
另一种是传递结构体的地址,这时无论原结构体有多大,形参都只需要用一个结构体指针来接收,把它压到堆栈上效率能提高很多,节省空间的同时提高效率。缺陷在于函数现在可以通过指针对原结构体进行修改,如果我们不希望如此,可以在函数中使用const关键字来防止这类修改。
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 和 char(整型家族)
-
位段的成员名后边有一个冒号和一个数字,这个整数指定该成员所占bit的数目
2. 位段的声明
如果把位段声明为int类型,它究竟被解释为有符号数还是无符号数是有编译器决定的
struct A
{
int _a:2;
int _b:5;
int _c:10;
int _d:30;
};
3. 位段的内存分配
printf("%d\n", sizeof(struct A));
- 内存分配规则
位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。
位段涉及很多不确定因素,无法总结统一的内存分配规则,位段是不跨平台的,注重可移植的程序应该避免使用位段。
以下是VS2013环境下测试数据:
4. 位段的跨平台问题
下面这些与实现有关的依赖性,位段在不同的系统中可能有不同的结果
int 位段被当成有符号数还是无符号数是不确定的。
位段中最大位的数目不能确定(16位机器最大16,32位机器最大32,写成27,在16位机
器会出问题)。
位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是
舍弃剩余的位还是利用,这是不确定的。
在VS编译器下,位段的使用习惯是这样的:
- 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,直接舍弃剩余的位;
- 位段中的成员在内存中是从右向左分配的;
5. 位段的应用
struct CHAR
{
unsigned int ch:7;
unsigned int font:6;
unsigned int size:19;
};
这个声明取自一个文本格式化程序,说明位段能够把长度为奇数的数据包装在一起
位段的另一个常见的用途就是用于ip数据报,如下图:
如图:在 ip数据报中,版本只占4个比特,头部长度只占4个比特,服务类型只占8个比特,如果这些数据我们都用一个整型大小,即32个比特位来存储的话,那么就会在一定程度上增加数据报的大小,从而增加网络负载,降低传输效率,所以在这里,位段的作用就得到了很好的体现。
总结:
跟结构相比,位段可以达到同样的效果,并且可以很好的节省空间,但是有跨平台的问题存在。
三. 枚举
枚举顾名思义就是一一列举,把可能的取值一一列举。
比如我们现实生活中:一周的星期一到星期日是有限的7天,可以一一列举,
性别有:男、女、保密,也可以一一列举。
月份有12个月,也可以一一列举
表示以上数据就可以使用枚举了
1. 枚举类型的定义
enum Day//星期
{
Mon,
Tues,
Wed,
Thur,
Fri,
Sat,
Sun
};
enum Sex//性别
{
MALE,
FEMALE,
SECRET
};
enum Color//颜色
{
RED,
GREEN,
BLUE
};
这些大括号内部的枚举常量都是有值的,默认从0开始,依次递增1,当然在声明枚举类型的时候也可以赋初值。
enum Color//颜色
{
RED=1,
GREEN=2,
BLUE
};
部分没有赋初值的也是由前一个成员递增1
2. 枚举的优点
我们可以使用 #define 定义常量,为什么非要使用枚举?
枚举的优点:
增加代码的可读性和可维护性
和#define定义的标识符比较,枚举有类型检查,更加严谨。
便于调试 :用 #define 定义的常量在程序的预处理阶段就会被替换掉,不便于调试观察
使用方便,一次可以定义多个常量
类型检查:枚举变量必须用枚举常量来赋值,而不能使用普通常量来赋值
3. 枚举的使用
enum Color//颜色
{
RED = 1,
GREEN = 2,
BLUE = 4
};
enum Color clr = GREEN; //使用枚举类型定义枚举变量并初始化
//只能拿枚举常量给枚举变量赋值,才不会出现类型的差异
四. 联合
1. 联合类型的定义
联合也是一种特殊的自定义类型,与结构相比,联合可以说是另一种生物了。
这种类型定义的变量也包含一系列的成员,特征是这些成员共用同一块空间(所以联合也叫共用体)
比如:
//联合类型的声明
union Un
{
char c;
int i;
};
//联合变量的定义
union Un un;
//计算联合变量的大小
printf("%d\n", sizeof(un));
2. 联合的特点
联合的所有成员引用的都是内存中的相同位置,当你想在不同时刻把不同的东西存入同一空间时,就可以使用联合,一个联合变量的大小,至少是最大成员的大小(因为联合至少得有能力保存最大的那个成员)
union Un
{
int i;
char c;
};
union Un un;
// 下面输出的结果是一样的吗?
printf("%d\n", &(un.i));
printf("%d\n", &(un.c));
//下面输出的结果是什么?
un.i = 0x11223344;
un.c = 0x55;
printf("%x\n", un.i);
因为联合体成员共用同一块内存空间,所以联合变量的地址与每个联合成员变量的地址都是相同的
3. 联合大小的计算
联合大小的计算规则如下:
- 联合的大小至少是最大成员的大小。
- 当最大成员大小不是最大对齐数的整数倍的时候,要对齐到最大对齐数的整数倍。
union Un1
{
char c[5]; //1
int i; //4
};//最大对齐数:4
union Un2
{
short c[7]; //2
int i; //4
};//最大对齐数:4
//下面输出的结果是什么?
printf("%d\n", sizeof(union Un1));
printf("%d\n", sizeof(union Un2));