1. 结构体
1.1匿名结构体
结构体在声明的时候,可以不加上名字,例如:
struct
{
int a;
char b;
float c;
}x;
struct
{
int a;
char b;
float c;
}a[20], *p;
上面这两种都是匿名结构体。 问题来了:下面这种写法正确吗?
p = &x;
答案是不正确的,虽然两个都是匿名结构体,但很明显它们不是同一个结构体。类型自然也不一样了。两个类型不同的变量不能乱赋值。
1.2结构体自引用
结构体里面包含自己就是结构体的自引用。 在数据结构里这种写法非常非常常见。 如链表:
struct ListNode
{
struct ListNode* next;
int val;
};
注意:下面这种写法是不正确的。这成套娃了。ListNode里面还有一个ListNode肯定是不合理的。
struct ListNode
{
struct ListNode next;
int val;
};
我们都知道在写结构体的时候可以加上typedef来重新命名结构体。 有一种写法初学者经常犯这个错误 下面这种写法是错误的。 原因:程序是自上到下的,ListNode*出现的位置在typedef前面,程序还识别不了这个名字。
typedef struct ListNode
{
ListNode* next;
int val;
}ListNode;
1.3结构体大小计算(内存对齐)
结构体大小计算是有自己的一套规则的。并不是线性相加。
这里先说内存对齐的规则再给题目具体解释。
对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
- 第一个成员在与结构体变量偏移量为0的地址处。
- 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
- 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
- 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所
有最大对齐数(含嵌套结构体的对齐数)的整数倍。
先举几道简单的题目来说明这个规则,后面再给几道难一点的。
第一道:
struct S1
{
char c1;
int i;
char c2;
};
printf("%d\n", sizeof(struct S1));
先文字描述再画图解释。 先算出每一个成员的对齐数
再算每一个成员的内存分布情况
-
第一个成员在偏移量为0的地方存储。 第二个成员由于对齐数是4,不能在偏移量为1,2,3的地方存储数据,因此这几个地方的空间被浪费了。(1,2,3都不是4的整数倍数)从偏移量为4的地方开始存储,直到偏移量为7的地方 第三个成员由于对齐数为1,所以可以直接在偏移量为8的地方存储。(8是1的整数倍) 最后我们可以发现三个成员所占的空间从偏移量0-偏移量8,共9个字节。由于9并不是最大对齐数4的整数倍,所以偏移量为9,10,11三个空间都被浪费了。 因此总大小为12.
答案果然是12. 我们现在画图来说明一下上面的文字: 第二题:把第一题的几个成员的顺序换一下,答案还一样吗?
struct S2
{
char c1;
char c2;
int i;
};
printf("%d\n", sizeof(struct S2));
答案是不一样的。 这次直接上图片解释了。 答案是8个字节。 这两个题目告诉我们:让占空间小的成员集中在一起可以减少一些空间。
第三题:这次是嵌套结构体的题。
struct S3
{
double d;
char c;
int i;
};
struct S4
{
double d;
struct S3 s3;
double e;
};
先算S3的结构体大小。S3的大小是16(算法和前面的一模一样) 重点讲S4怎么算? 嵌套的结构体也需要对齐,不过它的对齐数是它自己所有的成员里面对齐数最大的那一个。S3的最大对齐数是8(double),因此S3需要对齐到8的整数倍位置。 会计算结构体大小后,问题又来了:为什么要存在内存对齐?直接线性的累加大小不就好了吗?
这个没有权威的说法。但大部分资料都阐述了两点原因:
- 平台原因:不是所有平台都能访问任意地址的任意数据的。换句话来说,有些地址是不能让你访问的。如果线性地放数据有可能会把数据放到禁区里。(这个点倒没什么所谓个人认为)
- 性能问题:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。如果没有对齐,可能会导致拿数据的时候出现错误。
举个例子:假如不存在对齐。就会出现以下的情况。 现在我想从偏移量0处拿出一个int,就会拿到一个属于char的字节和三个属于int的字节。这就导致了错误出现。 总的来说:结构体的内存对齐是拿空间来换取时间的做法
1.4offsetof
offsetof是一个宏,用来计算偏移量的。 有个问题:offsetof为啥是个宏呢? 看offsetof的参数是有类型名的,函数参数可不能是类型名。 offsetof的使用:
struct S4
{
double d;
struct S3 s3;
double e;
}s4;
printf("%d\n", offsetof(struct S4, d));
printf("%d\n", offsetof(struct S4, e));
printf("%d\n", offsetof(struct S4,s3));
1.6修改对齐数
这里有个很小的知识。对齐数是可以自己修改的。 用下面这行指令
#pragma pack(你想改的数字)
2.位段(大小计算)
相信很多人都没有听过位段。 位段和结构体是类似的。但有两点不同
- 位段的成员必须是整型家族(char也是整型家族)
- 位段成员后面接一个冒号和数字。
位段的写法如下:
struct A
{
int _a : 2;
int _b : 5;
int _c : 10;
int _d : 30;
};
C语言里面的位通常都说的是二进制位。这里也不例外。这里的每一个数字代表的是一个变量占几个bit(一个bit就是一个二进制数)。 如a就占两个bit,b就占5个bit,依次类推。
2.1位段的计算
位段的内存开辟和结构体还有一些不一样。
位段的开辟是以一个int或者一个char类型来开辟的,当一个int或者char不足以存放下剩余的成员就再开辟一个int或者char。(开辟什么类型取决于成员是什么类型)
举个例子就很好懂了。 下面这个A的大小是多少?
struct A
{
int _a : 2;
int _b : 5;
int _c : 10;
int _d : 30;
};
答:首先创建一个int类型前2个bit放a变量,放完a之后的5个bit放变量b,然后放c的10个bit.由于现在第一个int只剩下15个bit了。无法放下成员d的30个bit,因此再开辟一个int(这里开辟一个int而不是char的原因是成员d是int类型),这个int的30个bit来存放d。
所以总大小是8个字节。 为了方便理解,来画图。 问题来了: 为什么a,b,c,d都是先从右端开始放呢?不可以在左边先放吗?
第二个问题也随之来了。 为什么d不能先用完第一个字节没有用完的bit呢?要直接重新开一个新的字节来存放。
这两个问题确实是存在的。因为C语言标准并没有规定这些规则。这些规则是每一个编译器规定的。至少在msvc上面,位段的存放规则是笔者所说的。
因此位段是不具有可移植性的。
2.2位段的几道题目
题目1:输出多少?
#define MAX_SIZE A+B
struct _Record_Struct
{
unsigned char Env_Alarm_ID : 4;
unsigned char Para1 : 2;
unsigned char state;
unsigned char avail : 1;
}*Env_Alarm_Record;
struct _Record_Struct *pointer = (struct _Record_Struct*)malloc
(sizeof(struct _Record_Struct) * MAX_SIZE);
这个位段的大小不难计算。画个图就好了。(做这种题切记不要不动手,心算容易算错) 大小是3字节。 接下来要用位段的大小乘以宏。这个是经典老坑了。宏的使用要多加括号,这里没有加,本质上是在做下面这个操作。
3 × 2 + 3 == 9
答案是9
题目2:
在内存中s是怎么样的?(16进制)
struct S
{
char a:3;
char b:4;
char c:5;
char d:4;
};
struct S s = {0};
s.a = 10;
s.b = 12;
s.c = 3;
s.d = 4;
位段由于有大小的限制,原先要存放的数据可能无法被完整的存放,因此要发生截断。 画图可以解决一切这种问题。 **因此在内存中应该是62 03 04的内容。**我们验证一下。 题目3: 这道题有点难度。输出什么?
int main()
{
unsigned char puc[4];
struct tagPIM
{
unsigned char ucPim1;
unsigned char ucData0 : 1;
unsigned char ucData1 : 2;
unsigned char ucData2 : 3;
}*pstPimData;
pstPimData = (struct tagPIM*)puc;
memset(puc,0,4);
pstPimData->ucPim1 = 2;
pstPimData->ucData0 = 3;
pstPimData->ucData1 = 4;
pstPimData->ucData2 = 5;
printf("%02x %02x %02x %02x\n",puc[0], puc[1], puc[2], puc[3]);
return 0;
}
这个位段的大小是2个字节。(这个很简单,不画图了) 下面这句代码是这道题的核心。 把字符数组的首元素地址强制转换成了结构体tagPIM类型,然后让pstPimData指向了这个字符数组的首元素地址。
pstPimData = (struct tagPIM*)puc;
赋值过程:最后puc[0],puc[1]等于02,29(16进制) 由于后面的两个元素没有修改过,因此最后的答案就是 02 29 00 00
3.共用体(联合)
联合也是一种特殊的自定义类型 这种类型定义的变量也包含一系列的成员,特征是这些成员公用同一块空间(所以联合也叫共用体)。
如:变量c和变量i是在同一段空间存储的。这段空间的大小是4个字节。
union Un
{
char c;
int i;
};
3.1共用体大小计算
共用体的大小就比之前两个要好计算很多。 共用体的大小至少是最大成员的大小。
但是共用体的大小仍然要进行内存对齐。对齐到对齐数的整数倍。 例如:
union Un
{
char c;
int i;
};
这个联合体的大小就是4.(最大对齐数是4,总大小是最大对齐数的整数倍)
3.2判断机器大小端
有两种方法: 方法1:
int i = 1;
char* pc = &(char)i;
if (*pc == 1)
printf("小端\n");
else
printf("大端\n");
这种方法不要写成
if(*(char*)i)==1;
初看好像没啥问题,其实错的很离谱。把i的值强制转换成一个地址,并对这个地址解引用,这就相当于对一个未知的指针解引用,这就肯定会让程序崩溃的。
第二种方法:共用体 其实判断大小端的方法就是取出整型1的第一个字节。共用体正好可以解决这个问题。
union un
{
char i;
int j;
}un;
un.j = 1;
if (un.i == 1)
printf("小端\n");
else
printf("大端\n");
3.3共用体的一些题目
这些题都比较简单。 题目一:
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);
答案: 前两个的地址是一样的。 11223355
原因:共用体所用的空间是一样的。它的每个成员地址都指向那个空间的开头。
题目二:
union Un1
{
char c[5];
int i;
};
union Un2
{
short c[7];
int i;
};
printf("%d\n", sizeof(union Un1));
printf("%d\n", sizeof(union Un2));
答案: 8 16
注:数组其实可以看成有n个单独的这个类型的元素,因此它的对齐数仍然是数组元素的类型。算共用体大小的时候记住要对齐。
题目3:
int main()
{
union
{
short k;
char i[2];
}*s, a;
s = &a;
s->i[0] = 0x39;
s->i[1] = 0x38;
printf(“%x\n”,a.k);
return 0;
}
这道题有点坑。 答案 3839 原因:把数字放进共用体的时候确实是3938.但是vs是小端储存,当它拿出来的时候,会默认还原成原来的样子,因此打印出来是3839.实际上里面仍然是3938.
|