深度剖析C语言结构体
深度剖析C语言结构体
- 1.什么是结构
- 2.结构体的声明
- 3.结构体变量的定义
- 4.结构成员变量的访问:
- 5.结构体变量的初始化:
- 6.嵌套的结构体:
- 7.结构体数组:
- 8.typedef
- 9.结构的自引用
- 10.结构体传参
- 11.结构体内存对齐
- (1)结构体内存对齐的规则
- (2)结构体内存对齐练习
- (3)为什么需要内存对齐
- (4)如何设计结构体
- (5)修改默认对齐数
- (6)用宏来计算结构体成员的偏移量
- 12.结构的一些注意事项:
- 13.位段
- (1)什么是位段?
- (2)如何求位段的大小?
- (3)位段成员变量具体的空间分配
- (4)位段的跨平台问题
- (5)位段的应用
1.什么是结构
结构是一些值的集合,这些值称为成员变量,结构的每个成员可以是不同类型的变量。生活中很多事情我们无法用一个变量来表达,这时候我们可以用到结构,比如学生就是一个结构,它包含姓名,学号,性别,年纪这些变量,即结构体是用来描述复杂对象的。
结构的成员可以是变量、数组、指针,甚至是其他结构体。
2.结构体的声明
第一种形式:
//声明一个结构体
struct point{int x;char y;
};//注意这里的分号
第二种形式:
//声明一个匿名结构体
struct{int x;char y;
}p1,p2;
//声明一个匿名结构体和定义一个结构体变量用一步完成,不太常见
匿名结构体省略掉了结构体标签,即匿名结构体没有名字,因此只能使用一次,也就是在声明该结构体的时候使用,所以我们必须在声明匿名结构体的同时定义匿名结构体变量。
我们再看看下面这两段匿名结构体的声明以及结构体变量的定义:
//匿名结构体类型
struct
{int a;char b;float c;
}x;
struct
{int a;char b;float c;
}*p;
那么问题来了,在上面代码的基础上,下面的代码合法吗?
p = &x;
事实上编译器会把上面的两个声明当成完全不同的两个类型。 所以是非法的,编译器会报警告如下:
3.结构体变量的定义
第一种形式,先声明一个结构体,然后再定义一个结构体变量。
struct point{int x;char y;
};struct point p1;//定义了1个结构体变量,类型为struct point,变量名为p1
第二种形式,声明结构体的同时定义结构体变量。
struct point{int x;char y;
}p1,p2;
// p1,p2都是struct point类型的结构体变量。
注意前面所提到的,匿名结构体因为省略了标签,因此我们只能在声明它的时候使用它,即我们只能在声明它的时候同时定义匿名结构体变量。也就是匿名结构体变量的定义只能采用第二种形式。另外,我们也不要随便定义匿名结构体变量。
4.结构成员变量的访问:
.和->是成员访问操作符,通过.操作符和->操作符访问:
#include <stdio.h>struct date{int month;int day;int year;
};
//1:通过.操作符访问
struct date today;
today.month = 1;
today.day = 1;
today.year =1;struct date * p = &today;
(*p).month = 1;//2:通过->操作符访问
p->month = 1;
.操作符优先级比*的优先级高,因此(*p).month要加括号。
5.结构体变量的初始化:
第一种,定义结构体变量的同时直接赋值。
struct Point
{int x;int y;
} struct Point p3 = {x, y};struct Node
{int data;struct Point p;struct Node* next;
}n1 = {10, {4,5}, NULL}; //结构体嵌套初始化struct Node n2 = {20, {5, 6}, NULL};//结构体嵌套初始化
第二种,定义结构体变量的时候指定成员变量赋值,没有指定的默认为0
struct Point
{int x;int y;int z;
} struct point p = {.x =7,.y=2014};//没指定的默认为0,故p.z = 0
第三种,访问成员变量的同时赋值
struct Point
{int x;int y;int z;
} struct point p;struct point* p1 = &p;p.x = 12;
(*p1).y =10;//注意要加括号
p->z = 9;
6.嵌套的结构体:
struct point{int x;int y;
};//这是一个嵌套的结构体
struct rectangle{struct point pt1;struct point pt2;
};//嵌套的结构体变量
struct rectangle r;//嵌套的结构体成员变量的访问
//r.pt1.x、r.pt1.y
//r.pt2.x、r.pt2.y//如果有下列变量定义
struct rectangle r,*rp;
rp = &r;//那么下面的四种形式是等价的
r.pt1.x
rp->pt1.x
(r.pt1).x
(rp->pt1).x//没有rp->pt1->x(因为pt1不是指针)
7.结构体数组:
struct date dates[100];
struct date dates[] = {{4,5,2005},//dates[0]的值{2,4,2005}//dates[1]的值
};
嵌套的结构体数组:
struct point{int x;int y;
};
struct rectangle{struct point p1;struct point p2;
};
int main(int argc,char const *argv[])
{struct rectangle rects[]={{{1,2},{3,4}},{{5,6},{7,8}}};return 0;
}
8.typedef
C语⾔提供了⼀个叫做 typedef 的功能来声明⼀个类型的新名字。比如:
typedef int Length;
使得 Length 成为 int 类型的别名。这样,Length 这个名字就可以代替int出现在变量定义和参数声明的地方了:
Length a, b, len ;
Length numbers[10] ;
那typedef声明一个类型的新名字有什么意义呢?
简化了复杂的名字,改善了程序的可读性,且新名字的含义更加清晰,具有可移植性。
typedef long int64_t;//新名字的含义更清晰,具有可移植性typedef struct ADate{int month;int day;int year;
}Date;//简化了复杂的名字,此后Date即表示struct ADate,改善了程序的可读性Date d = {9,1,32};
9.结构的自引用
我们思考一个问题,在结构中包含一个类型为该结构本身的成员是否可以呢?比如下面这段代码:
//代码1
struct Node
{int data;
struct Node next;
};
假设我们要求该结构体struct Node的大小,因为struct Node包含一个struct Node的成员变量,该成员变量又包含一个struct Node的成员变量,相当于无限套娃,我们永远无法求出该结构体的大小,因此要想在结构中包含一个类型为该结构本身的成员,代码1是不行的。
正确的自引用方式为:
//代码2
struct Node
{int data;struct Node* next;
};
即只能自引用指针变量
但是,问题又来了,当我们使用typedef对结构体进行重命名时,下面这段代码的自引用方式可行吗?
//代码3
typedef struct Node
{int data;Node* next;
}Node;
答案是不可行的,因为当编译器读到Node* next
这段代码的时候,编译器还不知道Node是什么,所以这个时候编译器会报错如下:
当我们使用typedef对结构体进行重命名时,正确的自引用方式如下:
typedef struct Node
{int data;struct Node* next;//用原名进行自引用
}Node;
10.结构体传参
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* ps)
{printf("%d\n", ps->num);
}
int main()
{print1(s); //传结构体print2(&s); //传地址return 0;
}
显然print2函数更好,因为函数传参的时候,参数是需要压栈的。 如果传递一个结构体变量的时候,结构体变量过大,参数压栈的系统开销比较大,会导致性能下降。因此结构体传参的时候,要传结构体的地址。
换一种理解方式:同变量一样,我们在传结构体变量的时候并不是直接将该结构体变量本身传递过去,而是在函数的变量空间中新建一个结构体变量来接收传进来的结构体变量的值,一个结构体变量可能有32字节,64字节甚至更多,如果新建一个结构体变量这就造成了时间和空间资源的浪费,而传地址仅仅只需要4字节或者8字节,这就大大节省了内存空间,因此结构体传参的时候,要传结构体的地址。
所以我们在设计函数的时候,如果有结构体参数,要把参数设计为结构体指针,这样我们就可以传结构体地址了。
11.结构体内存对齐
(1)结构体内存对齐的规则
我们已经掌握了结构体的基本使用了。现在我们深入讨论一个问题:计算结构体的大小,这也是一个特别热门的考点: 结构体内存对齐。
首先我们得掌握以下结构体内存对齐的规则:
- 第一个成员始终在与结构体变量偏移量为0的地址处。
- 其他成员变量要对齐到偏移量为对齐数的整数倍的地址处。
对齐数 = 编译器默认的一个对齐数与该成员大小的较小值。VS中默认的值为8。 - 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
- 如果嵌套了结构体的情况,嵌套的结构体作为成员变量对齐到偏移量为自己成员变量的最大对齐数的整数倍地址处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
注: Linux环境下没有默认对齐数,对齐数就是成员自身大小
(2)结构体内存对齐练习
空谈误国,实干兴邦,了解了结构体内存对齐规则后,我们通过实际的练习来深刻掌握结构体内存对齐。
//练习1
struct S1
{char c1;int i;char c2;
};printf("%d\n", sizeof(struct S1));//12
//练习2
struct S2
{char c1;char c2;int i;
};printf("%d\n", sizeof(struct S2));//8
//练习3
struct S3
{double d;char c;int i;
};printf("%d\n", sizeof(struct S3));//16
//练习4-结构体嵌套问题
struct S4
{char c1;struct S3 s3;double d;
};printf("%d\n", sizeof(struct S4));//32
(3)为什么需要内存对齐
做了这些题后,相信我们对结构体内存对齐这个概念有了更加深入的理解,那么这个时候问题又来了,为什么存在内存对齐?
大部分的参考资料都是这样说的:
- 平台原因(移植原因): 不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能
在某些地址处取某些特定类型的数据,否则抛出硬件异常。 - 性能原因: 数据结构(尤其是栈)应该尽可能地在自然边界上对齐。 原因在于,为了访问未对齐的
内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。也就是说结构体的内存对齐是拿空间来换取时间的做法。比如:
struct Node
{char c;int i;
}
我们先来看看内存不对齐的情况:
内存不对齐的情况,节省了空间,但是处理器获取一个int型变量需要做两次访问。
再来看看内存对齐的情况:
内存对齐的情况,虽然浪费了一些空间,但是处理器获取一个int型变量只需进行一次访问,这就是空间换时间。
从上述的例子来进行分析,我们就能深刻的了解到内存对齐对于处理器性能的提升有多么重要了。
(4)如何设计结构体
学到现在我们知道结构体有内存对齐这种通过空间换时间的性质,如果在设计结构体的时候,我们既要满足内存对齐来节省处理器对内存的访问时间,又要节省空间,那我们该如何设计结构体呢?
我们再回到练习题的第1题和第2题:
//练习1
struct S1
{char c1;int i;char c2;
};printf("%d\n", sizeof(struct S1));//12
//练习2
struct S2
{char c1;char c2;int i;
};printf("%d\n", sizeof(struct S2));//8
我们发现练习1和练习2的结构体成员完全相同,只是顺序不同,可是练习2的结构体只占8字节,仔细观察练习1和练习2的结构体成员我们就可以得出结论:
让占用空间小的成员变量尽量集中在一起可以节省结构体所占内存空间。
(5)修改默认对齐数
之前我们见过了 #pragma 这个预处理指令,这里我们再次使用,可以改变我们的默认对齐数。
#pragma pack(8)//设置默认对齐数为8
#pragma pack(1)//设置默认对齐数为1
#pragma pack()//取消设置的默认对齐数,还原为默认
因此如果结构体在对齐方式不合适的时候,我们可以自己修改默认对齐数,而且我们一般修改的默认对齐数是2^n。
(6)用宏来计算结构体成员的偏移量
思想: 我们先将0地址转化为结构体类型的地址,那么此时0地址处存储着一个结构体,第一个成员变量的地址为0,且此时它的偏移量也为0,我们假设第二个成员变量的地址为4,那么它的偏移量也就为4,故当0地址为结构体类型的地址时,成员变量的地址即为成员变量的偏移量,根据这个我们可以写出宏:
#define OFFSETOF(struct_name,member_name) ((int)&(((struct_name*)0)->member_name))
12.结构的一些注意事项:
结构体变量和普通变量一样,可以做赋值、取地址,也可以传递给函数参数,也可以返回一个结构变量
p1 = p2;// 相当于p1.x = p2.x; p1.y = p2.y;
p1 = (struct point){5, 10};把这样的两个值强制类型转换为struct point,相当于p1.x = 5;p1.y = 10;
和数组不同,结构变量的名字不是结构变量的地址,必须使用&运算符
struct date *pdate = &today
13.位段
结构体学完后,我们就得学习下结构体实现位段的能力。
(1)什么是位段?
位段的声明和结构是类似的,有两个不同:
- 位段的成员可以是 int unsigned int signed int 或者是 char (属于整形家族)类型。
- 位段的成员名后边有一个冒号和一个数字,数字用来表示该成员需要几个bit。
大致了解了位段的概念后,我们先来看一下位段的一个简单的示例:
struct A
{int a:2;int b:5;int c:10;int d:30;
};
A就是一个位段类型。成员变量a需要2bit,成员变量b需要5bit,成员变量c需要10bit,成员变量d需要30bit。
(2)如何求位段的大小?
那位段A的大小是多少?
在开始探究这个问题之前,我们需要先了解一下位段内存空间的分配规则:
位段的空间上是按照成员类型以4个字节( int )或者1个字节( char )的方式来开辟的。
了解了位段内存空间的分配后,我们就可以开始计算位段A的大小。
首先位段A的成员变量都是int类型,那我们先开辟4字节,32bit的空间,成员变量a占据了2bit,还剩30bit,成员变量b占据5bit,还剩25bit,成员变量c占据10bit,此时还剩15bit,由于成员变量d需要30bit,此时剩余空间已经不够了,因此再开辟一片4字节,32bit的空间用于存放成员变量d,但是至于到底是直接用新开辟的32bit的空间存储成员变量d,还是结合着剩余的15bit的空间一起存储的成员变量d,不同的编译器有着不同的处理方式。
综合上述分析,我们一共开辟了8字节的空间,因此位段A的大小是8字节。
(3)位段成员变量具体的空间分配
我们再来看看下一个例子,这个位段的大小是多大呢?每个成员变量具体的空间又是如何分配的呢?
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;
首先位段S的成员变量都是char类型,因此首先会开辟1字节,8bit的空间,成员变量a占据了3bit,还剩5bit,成员变量b占据了4bit,还剩1bit,此时已经不够存储成员变量c了,所以就会再开辟1字节,8bit的空间,到底是结合之前剩下的空间来存放变量c还是直接使用新开辟的1字节空间来存放成员变量c,这取决于编译器。
如果是结合之前剩下的1bit,那么此时就会剩下4bit,刚好存放成员变量d,所以位段s的大小就是2字节。
但是如果不结合之前剩下的1bit,而是直接用新开辟的1字节空间来存放c,那么只剩下3bit,无法存放成员变量d,所以又会开辟1字节的空间来存放成员变量d。此时位段的大小就是3字节。同理编译器到底是直接使用新开辟的1字节空间来存放成员变量d,还是结合着之前剩下的空间来存放成员变量d,这取决于编译器,不同的编译器有着不同的处理方式。
接下来我们就来更加深入,更加仔细地分析一下每个位段成员变量具体的空间分配是怎样的。
通过我们前面的分析,以第二种情况为例,编译器会为位段S开辟3字节的空间:
这个时候第一个问题来了,位段成员占据的比特位究竟是从低地址向高地址依次占据空间,还是从高地址向低地址依次占据空间呢?
C语言标准也没有对此进行规定,这完全取决于编译器,既然我们对此一无所知,不妨假设位段成员从高地址向低地址依次占据空间。
那么成员变量a和b的所占据内存空间如下:
这个时候第二个问题又来了,此时剩下的1bit空间已经无法存储成员变量c,那到底是直接使用后面新开辟的1字节空间还是把前面剩下的空间结合起来使用呢?
同样,C标准没有对此进行规定,这完全取决于编译器,我们假设编译器浪费掉剩下的空间,直接使用后面新开辟的1字节空间。
那么这时成员变量a,b,c,d所占据的内存空间如下:
我们接着下面的代码继续进行分析,struct S s = {0};
表示将位段S3个字节的空间全部初始化为0,此时位段s的内存空间是这样的:
接着下面就是开始给每个位段成员进行赋值了,s.a = 10;
表示将10存储在a的空间中,10所对应的二进制是1010,而a只占据3bit,无法存储4bit的1010,因此会发生高位截断,将010存储至a的空间中。
此时第3个问题又来了,数据的存储是大端存储模式还是小端存储模式呢?
我们假设数据的存储是大端存储模式。
此时位段s的内存空间如下:
那我们接着往下继续分析s.b = 12;
这表示把12即1100存储至b的内存空间中,b占据了4bit的内存空间完全可以存储的下,此时位段S的内存空间如下:
最后s.c = 3;s.d = 4;
表示将00011和0100分别存储至c和d的空间中,此时位段s的内存空间如下:
将上述二进制转化为16进制,S位段的三字节空间存储的内容应该是0x62 03 04。
而实际编译器下存储的内容是什么呢?
我们通过调试来观察位段s的内存空间如下图所示:
通过这个图我们可以发现实际的运行结果和我们通过3次假设后得到的结果是一致的,这说明实际位段s的内存空间和我们通过3次假设后得到的内存空间一模一样,即在VS2022环境下:位段成员从高地址向低地址依次占据空间,如果之前的空间不够存储位段成员而新开辟了空间,那么编译器会浪费掉之前剩下的空间,将位段成员存储在新开辟的空间中,数据在内存中的存储是大端存储模式。
(4)位段的跨平台问题
- int 位段成员被当成有符号数还是无符号数是不确定的。
- 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,如果位段成员写成27,那么在16位机器就会出问题。)
- 位段成员空间的分配在内存中从高地址向低地址分配,还是从低地址向高地址分配,标准尚未定义。
- 如果剩下的空间无法存储位段的某个成员,是直接使用新开辟的空间还是结合前面剩余的空间一起使用,标准也是没有定义的。
总结:
跟结构相比,位段可以达到同样的效果,但是可以很好的节省空间,但是有跨平台的问题存在,位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。
(5)位段的应用
我们可以通过位段来定义数据包的格式:
通过位段我们可以精确地给每个字段定义它们所需要的比特位,从而减少数据包的大小,进而可以减少网络拥塞的概率。
深度剖析C语言结构体相关推荐
- 【Go】Go语言结构体
文章目录 一.前言 二.结构体 三.定义结构体 四.结构体初始化 1. 结构体默认初始化 2. 使用值或键值对初始化结构体 3. 用访问成员的方式初始化结构体 五.访问结构体成员 六.匿名结构体 与 ...
- ARM汇编语言实现peek()_ARM汇编之访问C语言结构体数据
前言 本文的写作目的在于装逼,没有要产生实际价值的意思. 前几天在做编译器的项目,有一个项目团队成员一直在问我ARM汇编能不能读C语言的结构体.我心想,我这生成ARM汇编的代码是用C++写的呀,又不是 ...
- C语言结构体和结构体数组示例 - Win32窗口程序演示
C语言结构体和结构体数组的使用: /* C结构体和结构体数组示例,by bobo */#include <windows.h>LRESULT CALLBACK WndProc (HWND, ...
- C语言结构体-大小,对齐,填充,使用及其他
C语言结构体-大小,对齐 C语言中的结构体(struct)的定义 在C语言中,最常用的数据结构就是结构体了,结构体也是其它数据结构(比如链表等)的基础,结构体的使用非常简单. 比如,定义一个结构体: ...
- 关于c语言结构体偏移的一点思考
注:此处只是利用了编译器的特性来计算结构体偏移 这句话就一笔带过,说得有点牵强附会.以后有时间自己再详细了解一下编译器的特性... more exceptional c++ 中文版 26页 https ...
- C语言结构体指针的使用方法
1.首先定义一个结构体,给它取别名: typedef struct node{ struct node * next://指向下一节点 int data://数据域 }pnode,*linklist; ...
- C语言结构体与联合体
c语言结构体与联合体 结构类型定义和结构变量说明 一.结构的定义 二.结构类型变量的说明 结构变量的赋值 结构变量的初始化 结构数组 结构指针变量 其访问的一般形式为: (*结构指针变量).成员名 结 ...
- C语言结构体对齐的不足
该博文为原创文章,未经博主同意不得转载,如同意转载请注明博文出处 本文章博客地址:https://cplusplus.blog.csdn.net/article/details/105065657 C ...
- C语言结构体占用内存总结
C语言结构体占用内存总结 前几天有个小朋友问了我一下,关于C语言结构体占用空间的问题.觉得以后会对小可爱有点帮助,就打算先写一下. struct Test {int a;char b;int c; } ...
最新文章
- 深度思考:从头开始训练目标检测
- 【SSH网上商城项目实战20】在线支付平台的介绍
- javaone_JavaOne 2015 –又一年,又向前迈进了一步
- python update skeleton 不自动_python编程笔记(1)-数据类型
- 没有显示屏怎么启动服务器,中关村xp系统提示“没有启动服务器服务”如何解决...
- 学微信,抖音也上线PC版
- SQL ORDER BY Clause
- 为什么现在很小的孩子都会玩游戏,他们真的看得懂吗?
- cocos2d-x中实现不规则按钮的点击效果
- 嵌入式开发的职业前景分析
- 逃逸分析、栈上分配、标量替换、同步消除、锁消除
- 龙威PS305D维修案例收集
- html文件超链接打不开,Excel中出现超链接打不开的解决方法
- Chrome浏览器默认新标签页空白怎么办
- vue图片懒加载插件vue-lazyload监听加载失败事件的解决方案
- linux安装GPU显卡驱动、CUDA和cuDNN库
- Egret的eui的使用
- WebRTC系列<四> 全面了解客户端-服务器网页游戏的WebRTC
- 『Python基础』函数
- 组合逻辑电路一——数字逻辑实验
热门文章
- 【算力网络】算力网络的发展趋势
- css中的BFC、IFC、GFC、FFC
- 我的世界天空之城服务器位置,我的世界天空之城建筑地图详解(附存档)
- 我的世界Java版怎么做tnt,我的世界全自动刷TNT机教程
- 电化学传感器电路设计
- ASUS Vivobook archlinux声卡驱动
- 球差电镜测试常见的问题及解答(二)
- 计算机桌面上的软件是内存上吗,电脑软件运行提示内存不足,占内存小的游戏-...
- 宇宙为什么没有单独存在的夸克,强行将夸克拉出来会怎样?
- 【金猿产品展】Sensingtech便携式人脸识别一体机:让罪犯无处遁寻