·前言
在C语言中有诸如整型家族、浮点型等多种数据类型。丰富的数据类型使C语言的表达性更强,但对于一些复杂的对象只有这些基本类型还不足以描述到位。为了表达一些较复杂的数据,就需要用到构造类型。
下面涉及到的自定义类型主要包括结构体、位段、枚举和联合。
·结构体
结构体是一种自定义的复合数据类型。结构体可以将不同类型的数据成员组织到统一的名字之下,适合对关系紧密、逻辑相关的数据进行处理。比如要描述一个学生,则至少包含姓名、年龄、性别等基本数据,将这些数据统筹在结构体中,更有利于表述和编码。
结构体的声明
·声明结构体类型是为了创建结构体变量
-结构体变量包括局部结构体变量和全局结构体变量
·成员变量
-结构体是一些值的集合,这些值被称为成员变量
-成员变量可以是不同类型的
#include<stdio.h>
struct Stu//创建一个结构体类型Stu
{
char name[20];
int age;//int类型的成员变量
char sex[7];//char类型的成员变量
}s1;//定义一个全局结构体变量s1
struct Stu s2;//创建一个全局结构体变量s2
int main()
{
struct Stu s3={"jojo",28,"man"};//初始化一个局部结构体变量s3
return 0;
}
结构体的自引用
·结构体中不能包含自己本身
-若一个结构体中包含自己本身,即一个结构体中包含一个结构体,该结构体中又包含另一个结构体,就会类似死递归一样不断循环下去,不停地利用栈区空间,最终导致程序崩溃。(实际上很多编译器会直接报错)
·结构体中可以存放本结构体指针类型
-虽然结构体中不允许出现自己本身,但是可以包含自身结构体指针类型。这个用法常常用于链表的构建。通过其中的指针变量,可以找到下一个关联的数据。
struct node
{
char item;
struct node* next;//结构体中定义指向自己的指针
};
可以通过此结构体中的指针找到下一个数据:
匿名结构体
在C语言中,可以在结构体中声明某个结构体而不用指出它的名字,如此之后就可以像使用结构体成员一样直接使用其中结构体的成员。
struct//这是一个匿名结构体
{
int age;
char sex[5];
}p;
若要使用该匿名结构体,必须要在声明结束的同时进行定义。匿名结构体只能使用一次,这对特定的应用场景来说比较方便。
结构体的重命名
在某些情况下,例如当结构体名称较长时,简化的结构体往往给我们带来很多便利。在实际操作中,常常用typedef关键字对结构体进行重命名。
typedef struct person
{
char* name;
char gender;
int age;
int weight;
}person;//将struct person简化为person,方便书写
int main()
{
person p;
return 0;
}
需要注意的是,由于匿名结构体的特殊性,建议不要对其进行重命名,否则该结构体类型将无法使用。
结构体的内存对齐
类似int的大小是4个字节,short类型的数据大小是2个字节(32位机器及以上),结构体也具有一定大小。计算结构体的大小需要了解结构体的内存对齐。
struct MyStruct
{
int n;
char c;
double d;
}s;
int main()
{
printf("%d\n", sizeof(s));
return 0;
}
若对内存对齐一无所知,大概很多人都会直接把三个数据的大小暴力相加。实际上,运行上面的代码,得到的结果是16。
先抛出内存的对其规则:
- 第一个成员在相对于结构体变量偏移量为0的地址处
- 其他成员变量要对齐到该变量对齐数的整数倍的地址处
- 对齐数=编译器默认的一个对齐数 与 该成员大小之中的的较小值
- 结构体的总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍
- 如果嵌套了结构体的情况,可以认为嵌套的结构体的对齐数为 自己成员变量中最大的对齐数
编译器的默认对齐数由编译器决定,v.s.的默认对齐数是8,gcc没有默认对齐数。
上面结构体的大小计算过程如下:
为什么存在内存对齐
·平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据,某些硬件平台只能在某些地址处取某些特定类型的数据,比如读取int数据时只能在偏移量为4的倍数处读取,否则抛出硬件异常。
·性能原因:为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问,这就提高了访问效率。
如何尽量节省内存
内存对齐的过程中难免会出现内存的浪费现象。为了尽量减少内存的浪费,可以让占用内存空间小的成员变量尽量集中在一起。
修改编译器的默认对齐数
一般设有默认对齐数的编译器都会支持修改默认对齐数。v.s.中的修改方法为#pragma pack(num)
,设置默认对齐数为num;#pragma pack()
取消上面对默认对齐数的设置。
计算结构体成员变量的偏移量
offsetof()
宏可以计算某个结构体中某个成员变量相对于起始位置的偏移量。
size_t offsetof( structName, memberName
);
该宏需要stddef.h
头文件。
结构体的传参
结构体的传参可以传值也可以传址。
传值调用时,形参是实参的一份临时拷贝,对形参的操作不会影响实参;
传址调用时,会在形参和实参之间建立起真正的联系。
在实际操作时,往往使用传址调用,原因是传地址不会再对实参进行拷贝,减少了内存的使用,有利于效率的提高。若在传址调动时不希望结构体被修改,则可以加const关键字避免此问题。
·位段
在一个结构体/联合体中以位为单位来指定其成员所占内存长度,这种以位为单位的成员称为“位段”或称“ 位域 ” (bit field)。含有位段的结构体/联合体称为位段结构。
- 位段的成员必须是整型家族
- 位段名后有一个冒号和一个数字
struct data
{
unsigned int a : 7;//位段a,占7个比特位
int b : 3;//位段b,占3个比特位
char c : 4;//位段c,占4个比特位
int : 0;//占0个比特位的无名位段
};
位段的内存分配
- 位段成员属于整形家族
- 位段结构是按照需要以4个字节(int)或1个字节(char)的方式开辟的
#include<stdio.h>
struct data
{
char a : 4;
char b : 3;
char c : 7;
char d : 5;
};
int main()
{
struct data d = { 0 };
d.a = 10;
d.b = 12;
d.c = 3;
d.d = 4;
printf("%d\n", sizeof(d));//3
return 0;
}
运行以上程序,计算d的大小为3个字节。
其内存分配如下(假设内存从右到左分配):
为什么存在位段?
位段可以节省内存空间
位段可以节省空间。见上述案例,若是普通的结构体下,分别为a,b,c,d开辟一个字节,则该结构体就要占4个字节,实际上该位段结构只占用了3个字节。通过使用位段来手动规划空间,可以达到节省内存的目的。
位段不支持跨平台性
- int位段被当成有符号数还是无符号数不确定
- 位段的最大位的数目不能确定(16位机器、32位机器不确定)
- 位段成员是内存中从左向右还是从右向左分配标准未定义
- 无法容纳位段的剩余位时,舍弃剩余位还是利用剩余位不确定
位段的一个常用场景为网络数据传输
·枚举
枚举的规则和使用
-枚举的可能取值是常量——枚举常量
-枚举的可能取值默认从0开始
-枚举可以赋初始值
enum COLOR
{
RED,//0
GREEN = 2,
BLUE
};
int main()
{
printf("%d\n", RED);//枚举的可能取值默认从0开始
printf("%d\n", BLUE);//可以手动为枚举常量赋值
return 0;
}
为什么存在枚举?
既然枚举常量的本质是整型,那为什么还要存在枚举类型,这不是多此一举吗?
以上述为例,若不用枚举,实现上面代码的功能,则我们需要进行下面的操作:
#define RED 0
#define GREEN 2
#define BLUE 3
这就增加了代码量,且不易维护。这个例子也许不明显,若要列举出一周中的周一到周日,则就需要7个#define进行定义。
另一个例子体现在可读性上。想象一下,你要构建一个通讯录的程序,这个通讯录有增删查改、排序等功能可供用户选择,难不成你要将这些功能在switch语句中用0、1、2、3...表示吗?这样即不利于自己写代码,在后续的维护中也会很麻烦。若使用枚举,则可以避免此问题。
enum option//将用户选项写成枚举形式
{
EXIT,
ADD,
DEL,
SEARCH,
MODIFY,
SHOW,
SORT
};
现在可以回答问题:为什么使用枚举?
- 枚举增加了代码的可读性和可维护性
- 枚举存在类型检查,使代码更严谨
- 枚举可以实现数据封装,一定程度上防止命名污染
- 相比
#define
更加便于调试
尝试在适当的情况下使用枚举,你会感受到他的魅力。
·联合
联合体也被称为共用体,联合变量的成员共用同一块内存空间。联合体的这种特性决定了联合体变量的成员不能同时存在,否则会发生内存占用情况。
联合体的内存分配
union U
{
char c;
int d;
};
int main()
{
union U u = { 0 };
u.c = 3;
u.d = 7;
printf("%d\n", sizeof(u));//4
return 0;
}
该联合体的内存分配如下:
扩展到更复杂的结构体也是如此,所有成员的地址都从0偏移量的位置开始,内存重叠部分进行共用。
联合体的大小
联合体的大小要满足两个条件:
- 联合变量的大小至少是最大成员的大小
- 当最大成员大小如果不是最大对齐数的整数倍时,就要对齐到最大对齐数的整数倍,即联合体的大小能被其包含的所有基本数据类型的大小整除。
例:计算u的大小
union U
{
char s[9];//9
int n;//4
double d;//8
};
int main()
{
union U u = { 0 };
printf("%d\n", sizeof(u));
return 0;
}
其中s[]占9个字节,n占4个字节,d占8个字节,则该联合变量最少是9个字节,但运行以上程序发现输出的是16个字节。这是因为9不是1、4或8的整数倍,因此补充到16字节,这就符合所有成员的自身对齐了。
为什么存在联合体
联合体的存在,使只需单独调用其中一个成员的情况变得方便,节省了内存空间。
一个使用联合体的简单场景:
判断机器的存储模式是大端存储还是小端存储
union U
{
char c;
int d;
};
int main()
{
union U u = { 0 };
u.d = 1;
if (u.c == 1)
{
printf("小端\n");
}
else
{
printf("大端\n");
}
return 0;
}