读书笔记及拓展
我们的日常生活中有着非常多的复杂对象,单纯的使用一项数据是无法准确的描述这个对象的。例如:人、我们可以描述他的年龄,性别,电话,身份证号……。但是这些值的类型不同,我们如何使用计算机来描述呢? 在C语言中,使用结构可以将不同类型的值存储在一起。
结构体基础知识
什么是聚合数据类型?就是一些数据类型的集合。 C提供了两种类型的聚合数据类型:数组、结构。 数组是相同类型的元素的集合,而结构可以是不相同的数据类型的值的集合。这些值称为结构的成员
结构体类型的声明
在声明结构的时候,必须列出它所有成员的类型和名字。
struct tag
{
member-list;
}variable-list;
tag:是构造这个结构类型时,所取的类型名
member-list:结构成员
variable-list:定义类型为struct tag的变量
例如描述一个学生:
struct Stu
{
char name[15];
char sex[3];
int age;
char ID[20];
};
注意结构体声明也是声明,最后的分号一定不要忘记!!!这个类型的变量可以在分号后面创建,也可以在其他合法的地方创建,毕竟现在它只是创建了一个类型。
特殊的结构声明
struct
{
int a;
char b;
float c; }x;
struct
{
int a;
char b;
float c; }a[20], *p;
这两个声明省略了结构体的标签(tag) 我们来思考一下这个步骤是否正确: *p = &x; 答案是:这条语句是非法的。 由于上面的成员列表完全相同,以至于我们看上去认为那两个声明是一样的,但是编译器却把它们当作两个截然不同的类型。类型不同就不能进行这样的操作。
结构体类型的自引用
在一个结构体的内部包含一个类型为该结构本身的成员是否合法? 例如:
struct Node
{
int a;
struct Node b;
char c;
};
这种类型的自引用是非法的。,因为成员 b 也是一个完整的结构,该结构里又有一个结构……,这样一直重复下去,有点递归的影子,但是却没有终止条件。 正确的自引用如下:
struct Node
{
int a;
struct Node *b;
char c;
}
代码一和代码二的区别在于,代码一的b是结构,代码二中的 b 是一个指针。这个指针所指向的是同一类型的不同结构。编译器在还没有确定结构的大小的之前,就已经知道指针的大小了。 我们需要警惕下面的陷阱
typedef struct
{
int a;
Node* b;
char c;
}Node;
代码3是非法的,它的目的是为这个结构创建类型名Node,但是没有达到目标,因为在声明的内部还没有进行定义,而在声明的快结尾处才进行了创建。
typedef struct Node
{
int a;
struct Node* b;
char c;
}Node;
结构体变量的定义及初始化
我们已经大致知道了结构体变量的声明,那么如何对变量进行定义和初始化呢?
struct Simple
{
char x[10];
int y;
}p1;
struct Simple p2;
struct Simple p3 = { "abcdef", 123 };
struct Simple2
{
char x[10];
int y;
}p4 = {"abcd", 123};
struct Simple3
{
char x[10];
int y;
struct Simple3* z;
}p5 = { "abc", 123,NULL};
结构体的内存对齐
我们知道一个整型类型占4字节,一个字符型类型占1字节。那么一个结构类型占多少内存空间大小呢?
struct S1
{
char c1;
int i;
char c2;
};
你是否这样认为:一个char 占1个字节, 一个int占4字节,那么这个结构的所占内存大小为6字节。 我们不妨编写代码,让计算机来计算。 结果是12,因此刚才的想法是错误的! 这里就不得不提到一个非常重要的知识了:结构体内存对齐 结构体内存对齐的规则
-
- 第一个成员在与结构体变量偏移量为0的地址处。
-
- 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。 (vs中默认的值为8、 Linux中的默认值为4) -
- 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
-
- 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所
有最大对齐数(含嵌套结构体的对齐数)的整数倍。
举例如下(图片可点击放大):
这个时候我们就会非常的纳闷了,为什么结构体会存在内存对齐?老老实实的一个个分配内存它不香吗。 为什么会存在内存对齐? 大部分的资料是从两个角度来看的 一、平台可移植性: 不是所有的硬件平台都能访问任意地址上的数据;某些硬件平台只能在某些地址处取得某些特定类型的数据,否则会出现硬件异常。
二、性能 数据结构应该尽可能地在自然边界对齐。对齐的内存访问时只需一次访问,而未对齐的内存则需要进行两次访问。
总体来说:内存对齐是一种以空间换取时间的做法
上面两个结构体的成员是一样的,只有顺序不同,然而结构体所占空间大小也不一样了。 我们来看看它是如何分配内存的:
在试验了N多次的类似情况下得出结论: 最初设计结构体的时候,我们应该尽量让占用内存空间小一点的值集中在一起,以此来满足即节省空间又能节省时间的想法。
在VS编译器下,结构体对齐数默认是8。 但是在C中我们可以还能手动设置对齐数
# pragma pack(num)
例如:
# pragma pack(4)
struct S1
{
char c1;
int i;
char c2;
};
# pragma pack()
学会了如何去使用# pragma (num) 来更改默认对齐数。当我们遇到对齐数不合适时,就可以自行更改了。 上面一系列的代码中,大部分我们都是使用sizeof( ) 这个函数来得出结构体所占空间大小(包括了因边界对齐而浪费掉的字节)。如果你必须确认结构体中某个成员的实际位置,这个时候就可以使用offsetof 了,这可不是一个函数,它是一个定义于stddef.h文件的宏。你也可以换一种理解,该宏可以计算出结构体某成员在内存中的偏移量(在结构体第一个成员开始存储的位置处偏移了几个字节)
offsetof(struct S1, i)
这个语句的返回值为4。即 i 偏移了4字节
结构体传参和访问
如果我们已经知道了一个结构体类型,我们怎么访问它的每一个成员呢? 方法一:使用. 来访问 方法二:使用-> 来访问
使用方法一来访问 现在我们将主函数内的功能封装成一个打印函数Print1 传结构体的情况: 使用->访问和传变量地址的情况,此时p->name等价于(*p).name : 在这两种方法给出的效果一样的情况下,我们首选哪一个呢?
函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。 如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,性能会比较低
所以将结构体变量传参的时候尽量传地址。
结构体实现位段
什么是位段? 可能很多人就说是王者里的青铜、黄金、铂金、钻石……这些。 计算机里的位段:位段中的位指的是二进制位,段指的是范围。
位段的声明与结构类似,但是它们有两个不同点 一、位段成员必须是整型家族。 二、位段的成员标志是,成员后面有一个冒号,冒号后面接着一个数字。
struct S
{
int _a : 2;
int _b : 5;
int _c : 10;
int _d : 30;
};
位段和我们之前的结构体内存分配一样吗? 如果我们按照之前的方法来计算应该是16字节,不妨编写代码让计算机算一算 发现和我们的预期结果是不一样的,也就是说位段的内存分配与结构体的内存分配不一样。 位段在内存中是以4字节或者1字节逐个开辟的。 根据上面的内存分配来看,一共占用了24个比特位,也就是3字节,因此结构体S的内存大小为3。
我们应该避免使用位段,位段是不跨平台的。 为什么它是不跨平台的?有四个不确定
- 不确定成员是有符号还是无符号整型
- 不确定最大位的数目
- 不确定成员在内存从低到高还是从高到低分配
- 不确定如果第一个成员位段有剩余且第二个成员所占空间比较大时,是舍弃剩余部分,还是继续用。
以上就是本文所要讲解的内容。
|