赞
踩
第一种是最常见的数组,数组也是自定义类型,前面文章有介绍过。这里就只详细总结后面的四种自定义类型。
结构体里面存的可以是不同类型的成员变量。
结构体有两种声明方式:一是普通声明;二是特殊声明(也叫不完全声明)。
- 1. 结构体声明
- 1.1 普通声明
- struct tag 结构体标签
- {
- member_list; 存放成员列表
- }variable_list; 存放变量列表
- 1.2 特殊声明/不完全声明(输出即是匿名结构体类型)
- struct
- {
- int a;
- char b;
- }x;
普通声明是中规中矩地声明,也是用得比较多的一种;而特殊声明,也就是匿名结构体类型是会把struct后面的结构体标签给省略掉,这也意味着匿名结构体类型声明后就只能使用一次,就算是两个一模一样的结构体类型,其中一个存放指针变量并指向另一个结构体变量,再将这个指针存放某个东西,也会造成非法访问的,所以说匿名结构体类型在实际编写代码中会用的比较少。
错误示范1:
- struct Node
- {
- int data;
- struct Node next;
- };
写出这样的代码是看着好像没问题,跟递归有点像,但是这样写是错的,不能在结构体中直接把其中一个成员写成与本结构体相同的类型,这样只会造成死循环使程序崩溃,正确的修改如下:
- struct Node
- {
- int data;
- struct Node* next;
- };
应该将结构体struct Node类型改成struct Node*类型,这就类似于数据结构中的链表,都是通过指针指向的位置来进行寻找的。
那如果是用typedef来定义结构体类型进而来实现自引用应该怎么办呢?
错误示范二:
- typedef struct
- {
- int data;
- Node* next;
- }Node;
在现实中会有很多初学者的代码是这样子的,其实这样的代码是不正确的。虽然注意到了应该用指针来进行访问,但因为是用typedef来定义结构体的,而该代码是在整个结构体的结尾才声明成Node的,这样的话中间部分(也就是结构体内部)的Node是未定义的。正确的修改应该如下:
- typedef struct Node
- {
- int data;
- struct Node* next;
- }Node;
把整个结构体类型定义成struct Node,结构体内部就可以使用这个结构体类型指针来调用,最后再声明成Node(也就是将结构体类型变为Node,以方便下面使用时的调用等)。
其实这方面的知识之前的文章有提到过,比较简单,这里再做一下的整理:
结构体定义情况如下:
- struct Point
- {
- int x;
- int y;
- }p1;
-
- struct Point p2;
一是声明类型的同时定义变量,如p1;二是在外面定义结构体变量,如p2。这两种定义的都是全局变量,而如果是在一个函数中定义的结构体变量,那么这个结构体变量就是局部的。
结构体初始化最常见的有两种:
定义结构体变量的同时初始化,如:
- struct Point
- {
- int x;
- int y;
- };
-
- struct Point p = { 1,2 };
也有一种是结构体嵌套初始化,就像下面的n1和n2:
- struct Point
- {
- int x;
- int y;
- };
-
- struct Point p = { 1,2 };
-
- struct Node
- {
- int data;
- struct Point pp;
- struct Node* next;
- }n1 = { 10,{4,5},NULL };
-
- struct Node n2 = { 20,{7,8},NULL };
首先我们要清楚的一点是:在了解完结构体内存对齐后的最终目标是能够计算结构体的大小。
接下来就是结构体对齐规则:
1. 结构体的第一个成员,存放在结构体变量开始位置的0偏移处
2. 从第二个成员开始,都要对齐到对齐数的整数倍的地址处
对齐数:成员自身大小和默认对齐数相比,取较小值(VS环境的一个默认对齐数是8;Linux环境没有默认对齐数->对齐数就是成员自身大小)
3. 结构体的总大小,必须是最大对齐数的整数倍(最大对齐数是指所有成员的对齐数中最大的那个)
4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍
那么这时候很多人就有一个疑问了:为什么会存在内存对齐呢?
1. 平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则会跑出硬件异常。
2. 性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,CPU处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
总的来说:结构体的内存对齐是拿空间来换取时间的做法。
那么,这种拿空间来换取时间的方法随着运行时间的缩短,有时候务必会浪费很大的一块空间。所以,这时候就引出了一个设计结构体既满足对齐又节省空间的方法,那就是让占用内存小的成员尽量集中在一起。巧妙地利用结构体对齐规则,在内存中未存放内容的位置塞进一些占用内存小的成员,大大节省了空间。就比如下面的例子:
- struct s1
- {
- char c1;
- int i;
- char c2;
- };
-
- struct s2
- {
- char c1;
- char c2;
- int i;
- };
这两个结构体s1和s2类型成员是一样的,但是通过对齐规则来计算,就会很快地发现结构体s1的大小是12字节,而结构体s2的大小是8字节,又再次证明了让占用内存小的成员尽量集中在一起可以节省空间。
对于修改默认对齐数,可以使用#pragma预处理指令来实现。
例如:
1.
#pragma pack(6) //设置默认对齐数为6
2.
#pragma pack() //取消默认对齐数,还原为默认
先说结论:结构体传参的时候,要传结构体的地址。
那么为什么呢?接下来通过一个简单的代码来解释。
- 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* s)
- {
- printf("%d\n", s->num);
- }
-
- int main()
- {
- print1(s); //传结构体
- print2(&s); //传地址
- }
对于print1来说,因为函数传参的时候,参数是需要压栈的,会有时间和空间上的系统开销。那么,从长远来看,如果传递一个结构体对象的时候,结构体过大,参数压栈的系统开销比较大,所以会导致性能的下降。
而对于print2来说,因为它传递的是地址,就没有这个问题。
所以总的来说,结构体传参首选还是传地址。
什么是位段?
其实位段说到底就是结构体的一种特殊实现,位段的声明和结构体是类似的,但有两个不同点:一是位段的成员必须是int、unsigned int、char类型;二是位段的成员名后边有一个冒号和一个数字。
知道了什么是位段, 位段又是如何进行内存分配的呢?又有哪些值得注意的点呢?
1. 位段的空间上是按照需要以4个字节(int)或者1个字节(char)的方式来开辟的(易遗忘!!!)
2. 位段的成员应是整型家族(int, unsigned int, signed int, char)
3. 位段涉及很多不确定因素,位段与结构体的最大区别是:位段节省了内存空间,但不跨平台,因此注重可移植的程序应该避免使用位段
关于位段跨平台问题应该注意的一些点:
1. int位段被当成有符号数还是无符号数是不确定的
2. 位段中最大位的数目不能确定(16位机器最大是16,32位机器最大是32,如果冒号后面跟的数字是25,在32位机器跨到16位机器上的时候是会出问题的)
3. 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义(在VS环境下是从右向左分配的)
4. 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不能确定的(在VS环境下是舍弃的)
总结:位段相较结构体来说,虽然也是可以达到同样的效果,也比结构体更能节省空间,但是有跨平台问题存在。
对于位段来说,有跨平台问题就真的一无是处吗?其实不是的,下面就列举一个用位段实现的例子(数据包):
数据包在两个用户之间传递数据的时候,是会将4位版本号、8位服务类型、8位协议这一系列的东西附加在要发送的数据上面,形成数据包并一起传输过去的。那么就可以试想,一些只有4个比特位的如果用char类型存储起来或者一些是16个比特位的用int类型存储起来的时候,就会照成大量的空间浪费,那么传输起来的速度将会比较慢,所以这时候就会用位段来对这些东西进行存储,节省空间,就可以提高传输效率等。
- enum Sex
- {
- MALE,
- FEMALE,
- SECRET
- };
-
- enum Color
- {
- RED,
- GREEN,
- BLUE
- };
例如上面的两个例子,定义的Sex和Color都是属于枚举类型。 { }中的内容是枚举类型的可能取值,也称为枚举常量。
其中,这些枚举常量都是有值的,默认是从0开始,一次递增1,当然在定义的时候也可以赋初值,就比如下面的例子:
- enum Color
- {
- RED=1,
- GREEN=2,
- BLUE=4
- };
还有三点值得注意的是:
1. 枚举类型中存的是可能取值,一次调用时只能够取出其中的一个,所以计算该枚举类型的大小的时候会发现它的大小只是一个int类型的大小
2. 枚举类型和结构体类型的定义形式是相似的,都是可以用typedef来进行定义并声明
3. 给枚举变量赋值的时候,只能拿枚举常量来赋给它;如果是直接用一个整数来对枚举变量来进行赋值,则会警告说出现类型差异,就像下面这个例子:
- #include <stdio.h>
- typedef enum Color
- {
- RED=1,
- GREEN=2,
- BLUE=4
- }Color;
-
- int main()
- {
- Color clr = GREEN; //正确
- //clr = 5; //错误,出现类型差异
- printf("%d", clr);
- return 0;
- }
那么,使用枚举类型又有什么优势呢?不可以直接全部都用#define来定义全局变量吗?
这里就总结了几个枚举的优点:
1. 增加代码的可读性和可维护性
2. 和#define定义的标识符比较枚举有类型检查,更加严谨
3. 防止了命名污染(封装)
4. 便于调试
5. 使用方便,一次可以定义多个常量
联合也是一种特殊的自定义类型,这种类型也包含一系列的成员,最明显的特征是这些成员共用同一块空间(所以联合体也叫共用体)。
其次,联合体大小也有自己的计算规则:
1. 联合的成员是共用一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为联合至少得有能力保存最大的那个成员)
2. 当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍
拓展:判断当前机器的大小端存储
其实这道题在之前已经有做过了,但是之前的方法是将整型地址强制类型转换为字符地址,再访问该地址的内容判断是否与原来的数相等,若相等,则是小端字节序存储;若不相等,则是大端字节序存储。(如果还不了解大小端是什么可以看看之前的文章)
- //判断大小端
- #include <stdio.h>
-
- //方法一
- int cheak_sys()
- {
- int a = 1;
- if (*(char*)&a == 1)
- {
- return 1;
- }
- else
- {
- return 0;
- }
- }
-
- //方法二
- int cheak_sys()
- {
- int a = 1;
- return *(char*)&a;
- }
-
- int main()
- {
- int ret = cheak_sys();
- if (ret == 1)
- {
- printf("小端\n");
- }
- else
- {
- printf("大端\n");
- }
- return 0;
- }
之后也知道了有一个库函数:memcmp这个库函数是对内存进行比较的,也是同样可以实现大小端判断的。
但是今天介绍一种新的方法,这种方法比较巧妙,是运用到了联合体的知识:
- #include <stdio.h>
-
- int cheak_sys()
- {
- union Un
- {
- char c;
- int i;
- }u;
- u.i = 1;
- return u.c;
- }
-
- int main()
- {
- int ret = cheak_sys();
- if (ret == 1)
- {
- printf("小端\n");
- }
- else
- {
- printf("大端\n");
- }
- return 0;
- }
这种联合体的方法巧妙之处就在于联合的成员c和i是共用一块内存空间的,我们先让内存中存的是i,由于i是int类型,所以会开辟4个字节,将它存入一个值,但是我们最后只是返回c(也就是char类型),其只在同一块内存中占用一个字节的内存(开始的第一个字节),就可以判断是大端还是小端了。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。