目录
1. 结构体
1.1. 结构体类型的声明
1.1.1. 特殊声明
2. 结构的自引用
3. 结构体变量的定义和初始化
4. 结构体内存对齐
4.1. 结构体内存对齐
4.2. 修改默认对齐数
5. 结构体传参
6. 结构体实现位段(位段的填充&可移植性)
6.1. 什么是位段
6.2. 位段的内存分配
2. 枚举
2.1. 枚举
2.2. 枚举常量的优点
3. 联合
3.1. 联合
3.2. 判断大小端
3.3. 联合大小的计算
1. 结构体
首先,为什么要求这些自定义类型呢?在C语言中,其标准已经为我们提供了许多的内置类型,int、char、double等等,但是,有些情况下,人们发现单单靠这些内置类型无法满足现实世界各种复杂的情况,例如,如果我们要描述一本书,我们是不是应该描述它的书名、作者、出版社等等各种信息,我们发现如果此时只有内置类型,是不可能达成类似这种复杂需求的,于是C赋予了程序员们一种权利,可以定义自定义类型。而结构体就是自定义类型中的一种。
1.1. 结构体类型的声明
首先,我们看看结构体的声明是怎样的呢?
// 假如我要描述一本书
struct book
{
char book_name[20];
char author_name[10];
int book_pages;
//... 各种信息
}; // 注意这里的 ; 不可漏掉
上面的struct book就是一个结构体类型的声明。
1.1.1. 特殊声明
在声明结构体类型时,C标准允许可以不完全声明:
struct
{
int x;
int y;
int z;
}target; //并且此时只能在这里定义这个类型的变量
struct
{
int x;
int y;
int z;
}*p; // 定义了一个这个结构体类型指针的变量
上面的类型就是一个匿名结构体类型,有人看到这两个匿名结构体的成员变量完全一样,那能不能这样做呢?
void Test1(void)
{
p = &x;
}
首先,这样做是不好的。编译器对于匿名结构体的处理是:如果匿名结构体的成员变量一样,编译器也认为它们是不同的类型。
2. 结构的自引用
结构体的自引用简单理解就是:结构体类型中的成员变量包含一个结构体类型的指针变量。
struct Node
{
int val;
struct Node next;
};
上面的代码可行吗? 答案是,不可行。为什么呢?struct Node这个自定义类型中包含一个类型为struct Node的成员next,而这个成员也是一个自定义类型struct Node,那它里面也有一个struct Node类型的成员啊,这种无穷包含自己,在编译器看来是一种非法行为,因为此时这个类型的大小无法确定。
正确的自引用是这样的:
struct Node
{
int val;
struct Node* next;
};
而我们直到typedef可以对一种数据类型进行重命名,那么它可以对结构体类型重命名吗?当然可以。例如如下:
typedef struct Node
{
int val;
struct Node* next;
}Node;
上面的代码的意思是什么呢?就是定义了一个struct Node的结构体类型,我们对这个结构体类型重命名为 Node,即 Node 等价于 struct Node,它们代表着同一种类型。
然而,有人看到这里就会突发奇想,他说既然Node和 struct Node代表着同一种类型,那么可不可以这样呢?
typedef struct Node
{
int val;
Node* next;
}Node; // 走到这里才代表对struct Node进行重命名为 Node
首先,说答案,上面这种声明是非法的,因为此时的这个成员变量next的类型还没有完成重命名,也就是先有鸡还是先有蛋的问题,只有走完这个类型重命名语句,才会对struct Node进行重命名为 Node,不可以在重命名之前就使用重命名后的类型。因此正确的命名是如下这种形式:
typedef struct Node
{
int val;
struct Node* next;
}Node;
然而,有时候我们会在书中看到这样的声明风格:
typedef struct Node
{
int val;
struct Node* next;
}Node,*PNode;
其实很简单,
这里的Node等价于struct Node
PNode就相当于 struct Node*
3. 结构体变量的定义和初始化
struct Book
{
char book_name[20];
int book_pages;
double price;
}b1 = {"XqianC语言",531,33.3}; // 声明 + 定义了一个struct Book类型的全局变量
struct Node
{
struct Book b;
struct Node* next;
}n1 = { {"wangwuLinux",843,55.5},NULL }; // 定义一个结构体嵌套类型的全局变量
void Test3(void)
{
struct Book b2; // 定义一个struct Book类型的局部变量
// 给b2的成员变量赋值
b2.book_name[20] = "lisiC++";
b2.book_pages = 632;
b2.price = 44.4;
// 定义 + 初始化
struct Node n2 = { { "cuihuaMySql", 483, 66.6 }, NULL }; // 定义一个结构体嵌套类型的局部变量
}
4. 结构体内存对齐
4.1. 结构体内存对齐
首先,我们看看下面的这两个类型:
struct type1
{
char ch1;
int i;
char ch2;
};
struct type2
{
char ch1;
char ch2;
int i;
};
现在要求我们计算着两个类型分别是多大,即:
void Test4(void)
{
printf("struct type1 size = %d\n", sizeof(struct type1));
printf("struct type2 size = %d\n", sizeof(struct type2));
}
有人一看这两个类型,诶,这两个类型的成员变量除了顺序不一样,其他都是一样的啊,让我算的话,它们的大小难道不应该是 两个 char + 一个 int类型的大小,即等于6吗?OK,让我们看看它的结果是多少呢?
出人意料,诶,怎么回事,不是6就是算了,怎么这两个类型的大小还不一样。为了解决这个问题,我们要引出一个东西,称之为结构体内存对齐规则。
那么结构体内存对齐规则是什么呢?如下:
1. 第一个成员在与结构体变量偏移量为0的地址处。2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。对齐数 = 编译器默认的对齐数 与 该成员变量类型大小 的较小值vs下默认对齐数为83. 结构体总大小为最大对齐数的整数倍。最大对齐数:所有成员变量类型大小中的最大值。4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
有了结构体内存对齐规则,我们就可以分析上面这两个结构体类型的大小为何如此了,分析如下:
声明:为了更好理解下面的图,由于结构体对齐规则导致没有使用的空间用红色表示
对struct type1的分析如下:
对struct type2的分析如下:
有了对上面的理解,我们试着去计算下面这个结构体类型的大小:
struct type3
{
char ch1
struct type2 t2;
char ch2;
int i;
};
为了验证上面结构体成员的偏移量是否与预期正确,我们可以用offsetof,它是一个宏,可以帮助我们计算一个结构体中某个成员变量相对于起始位置的偏移量。
// 原型如下
// 其包含在 #include <stddef.h>
// structName --- 结构体名字
// memberName --- 成员变量名字
size_t offsetof( structName, memberName );
void Test5(void)
{
printf("ch1 offset: %d\n", offsetof(struct type3, ch1));
printf("t2 offset: %d\n", offsetof(struct type3, t2));
printf("ch2 offset: %d\n", offsetof(struct type3, ch2));
printf("i offset: %d\n", offsetof(struct type3, i));
}
结果如下:
很显然,符合我们的预期。
那么为什么要存在内存对齐:
1. 平台原因(移植原因): 不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。2. 性能原因: 数据结构(尤其是栈)应该尽可能地在自然边界上对齐。 原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。总体来说,结构体的内存对齐是拿空间来换取时间的做法。
那在设计结构体的时候,如果我们既要满足对齐,又要节省空间,该如何做呢?
答案是:让占用空间小的结构体成员尽量集中在一起,例如:
struct type1
{
char ch1;
int i;
char ch2;
};
struct type2
{
char ch1;
char ch2;
int i;
};
struct type1和struct type2的成员变量是一样的,但是它们的大小确是不一样的,前者占12个字节,后者占8个字节,原因就在于struct type2中的较小成员集中在了一起,节省了一定的空间。
4.2. 修改默认对齐数
我们知道,VS下的默认对齐数是8,但是我们却可以显式的修改其默认对齐数。
// 编译器的默认对齐数为8
struct type5
{
char ch;
double d;
};
#pragma pack(4) // 将编译器的对齐数修改为4
struct type5
{
char ch;
double d;
};
#pragma pack() // 取消设置的对齐数,还原为默认对齐数
#pragma pack(1) // 将编译器的对齐数修改为1,就意味着不对齐.此时的大小就是9
struct type5
{
char ch;
double d;
};
#pragma pack() // 取消设置的对齐数,还原为默认对齐数
结构体在对齐方式不合适的时候,我们可以自己更改默认对齐数。
5. 结构体传参
struct Info
{
int data[1000];
char* name[1000];
};
// 结构体传参
void print1(struct Info tmp)
{
//...
}
// 结构体的地址传参
void print2(struct Info* p_tmp)
{
//...
}
void Test7(void)
{
struct Info information = { { 1, 2, 3 }, {NULL} };
print1(information); // 不推荐,值传递传参的压栈系统开销过大
print2(&information); // 推荐,址传递传参的压栈系统开销很小
}
对于结构体的传参,我们推荐采用以传结构体地址的方式;因为函数在传参的时候,会将其参数压栈,在时间上和空间上都会有消耗,如果我们传递一个结构体对象,当这个结构体很大的时候,参数压栈的系统开销就会很大,会导致性能的降低,而如果传递一个结构体指针,其大小是固定的(32位下4个字节、64位下8个字节),可以节省系统的开销,一定程度上提高性能。
结论:结构体传参尽量传结构体的地址。
6. 结构体实现位段(位段的填充&可移植性)
6.1. 什么是位段
1. 位段的成员必须是 int 、 unsigned int 或 signed int或者char(char也是属于整形家族) 。2. 位段的成员名后边有一个冒号和一个数字。
struct bit
{
int a : 2;
int b : 5;
int c : 10;
int d : 30;
};
上面就是一个位段,我们可以看到,位段的成员后面有一个冒号和一个数字,这个数字代表着你这个成员占用了几个bit位,例如:a这个成员就会占用2个bit位。那么位段的大小如何计算呢?位段需要进行内存对齐吗?
首先,对于位段我们应该知道,其主要作用是:节省空间;而我们知道结构体的内存对齐是以空间换取时间的一种方式,很显然,这就与位段的初衷相矛盾了,故位段是不会有内存对齐的。
void Test8(void)
{
printf("bit size = %d\n", sizeof(struct bit));
}
那么上面这个位段是多大呢?
可以看到这个位段共占用了8个字节,的确节省了空间。那么位段究竟是如何分配内存的呢?
6.2. 位段的内存分配
1. 位段的成员可以是 int、unsigned int、 signed int、 或者是 char( 属于整形家族)类型2. 位段的空间上是按照需要以 4 个字节( int )或者 1 个字节( char )的方式来开辟的。3. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段
之所以说位段是不跨平台的,是由于当出现上面这种情况时,我们不知道它的内存分配是怎样的,例如:上面剩余了15个bit位,d到底有没有使用它,是不确定的,标准并没有明确规定是否使用这个15个bit。因此,对于位段的使用要小心,如果要求程序具有移植性,那么尽量减少位段的使用。
位段的跨平台问题1. int 位段被当成有符号数还是无符号数是不确定的。2. 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机器会出问题。3. 位段中的成员在内存中从左向右分配,还是从右向左分,配标准尚未定义(vs下是从右向左分配的)。4. 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的(vs下没有利用剩余的空间)
总结:
位段较结构体相比,位段可以达到同样的效果,但是可以很好的节省空间,但由于标准对许多细节并没有明确规定,存在跨平台的问题,其可移植性是有待商榷。
2. 枚举
2.1. 枚举
枚举(enum)是一种用于定义一组相关常量的数据类型。它可以帮助开发人员更清晰地表示某个值的取值范围,并提供更好的可读性和可维护性。
在枚举类型中,我们可以定义一组具体的命名常量,也称为枚举成员。每个枚举成员都有一个与之关联的值,它可以是数字(如整数)或者是其他数据类型(如字符串)。枚举成员之间用逗号隔开。
使用枚举,我们可以通过给定的枚举成员来表示某个特定的取值。这有助于编写更可读的代码,并减少硬编码所带来的错误。此外,枚举还可以作为函数参数、变量和属性的类型,增加代码的类型安全性。
enum color
{
RED,
YELLOW,
BILU
};
上面的enum color 就是一种枚举类型,{}中的内容是枚举类型的可能取值,也称之为枚举常量,这些枚举常量都是有值的,默认从0开始,依次递增1,当然在定义的时候也可以赋初值,例如:
enum color
{
RED = 5,
YELLOW = 3,
BLACK, // 这里的BLACK没有赋初值,那么就是YELLOW + 1,即等于4
BILU = 7 // 注意,最后一项的枚举常量不加,
};
2.2. 枚举常量的优点
为什么要使用枚举常量呢?它与#define定义的符号常量有什么区别呢?
枚举常量的优点:
1. 增加代码的可读性和可维护性2. 和 #define 定义的标识符比较枚举有类型检查,更加严谨。枚举常量是属于一种枚举类型的,它是具有类型检查的,与符号常量相比更为严谨3. 防止了命名污染(封装)4. 便于调试,宏定义的符号常量在预编译阶段就被替换了。5. 使用方便,一次可以定义多个常量
因此,我们推荐使用枚举常量,以减少宏定义的标识符的使用
3. 联合
3.1. 联合
联合(union)是一种特殊的 自定义类型 ,它可用于在同一内存空间中存储不同的数据类型。它允许使用同一块内存来存储多种类型的数据,但同一时间只能使用其中的一种类型数据。联合与结构体非常相似,都可以定义多个成员,但联合只分配给它成员中最大的元素所需要的内存空间(需要考虑内存对齐,当最大成员的大小不是最大对齐数的整数倍时,就需要内存对齐)。因此,联合可以通过不同的成员来表示同一块内存中的数据,这使得它在一定程度上可以节省内存空间。
联合体的特征就是:联合的成员共用同一块空间(所以联合也叫共用体),示例如下:
union Un
{
char ch;
int i;
};
那么上面这个联合体的大小是多少呢?
void Test10(void)
{
printf("Un size: %d\n", sizeof(union Un));
}
联合的大小是由其最大的成员决定的,例如上面的这个联合,其最大成员是一个int类型,又因为此时这个联合体的最大对齐数就是4,因此上面这个联合的大小就是4。
void Test11(void)
{
union Un u;
printf("u address: %p\n", &u);
printf("ch address: %p\n", &(u.ch));
printf("i address: %p\n", &(u.i));
}
可以发现,联合对象和它的成员的地址是一样的,也就是说,其内存分配如图所示:
联合的特定是成员共享同一块空间,但这也限制了在同一时刻只可以使用一种成员。例如:
union Un
{
char ch;
int i;
};
void Test12(void)
{
union Un u;
u.i = 0x11223344;
u.ch = 0x66;
}
在使用联合时,改变其中一个成员变量,另一个成员变量也会被修改,因此一般情况下,联合在某一时刻下是单独使用一个成员变量的 。
3.2. 判断大小端
什么叫大端,什么叫小端呢?
大端(Big Endian)和小端(Little Endian)是用于描述存储和处理多字节数据的方式。
大端字节序(Big Endian)是指将最高有效字节(Most Significant Byte,MSB)存储在最低地址处,最低有效字节(Least Significant Byte,LSB)存储在最高地址处。也就是说,数据的高位字节存储在低位地址,低位字节存储在高位地址。
小端字节序(Little Endian)则相反,它是指将最低有效字节(LSB)存储在最低地址处,最高有效字节(MSB)存储在最高地址处。也就是说,数据的低位字节存储在低位地址,高位字节存储在高位地址。
void Test13(void)
{
int i = 0x12345678;
// 对于0x12345678的大端字节序和小端字节序
// 低地址 <---------------> 高地址
// 大端字节序:
// 0x12 0x34 0x56 0x78
// 小端字节序:
// 0x78 0x56 0x34 0x12
}
利用联合的特性(其成员共有同一块空间),我们就可以判断某个机器下是大段还是小端存储,那么如何判断呢?
union Un
{
char ch;
int i;
};
void Test14(void)
{
union Un u;
u.i = 1;
// 此时如果u.ch == 1,那么就是小端;如果u.ch == 0,那么就是大端
if (u.ch == 1)
printf("小端存储\n");
if (u.ch == 0)
printf("大端存储\n");
}
3.3. 联合大小的计算
union Un1
{
char ch[5];
int i;
};
上面的联合体是多大呢?注意,联合体的的大小首先是要保证能够存放最大成员,其次如果最大成员所占空间大小不是其最大对齐数的整数倍,那么需要内存对齐。例如上面的:
至此,C语言的自定义类型到此结束。