结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。
比如我们要怎么描述一本书呢?在描述一本书的时候,往往会关心这本书的属性,作者,价格,书名,书号等等。那么在c语言中中就存在一种方法能够将一类物品的属性包含起来,这类属性的集合就是c语言中的自定义类型,结构体。
代码如下(示例):
struct tag //结构体的标签
{
member-list;//结构体的成员变量
}variable-list;//结构体的变量
例如:对一本书的描述
struct Book
{
char author[20];//作者
char name[20];//书名
int price;//价格
int num;//书号
};
代码如下(示例):
//匿名结构体类型
struct
{
int a;
char b;
float c;
}x;
struct
{
int a;
char b;
float c;
}* p;
了解了匿名结构体类型,让我们来思考以下几个问题:
这两个结构体都是匿名结构体类型,两个结构体的成员变量也是一模一样的。那么这两个结构体到底是不是同一个结构体呢?在VS的编译器下,编译器会不会将它们当作同一个结构体呢?能不能将结构体变量x的地址赋值给结构体指针p呢(p = &x;)?
在VS2019的编译器下进行调试,我们发现编译器警告"="两边的类型不兼容,说明编译器认为这两个匿名结构体是不一样的,所以不能将一个匿名结构体变量的地址赋值给另外一个匿名结构体的指针变量,由此我们得出结论,虽然这两个匿名结构体的类型和成员变量是一模一样的,但是这是两个完全不同的匿名结构体,它们两个之间没有任何的关系,除此之外,如果想要使用匿名结构体,那么需要在进行匿名结构体声明的同时创建匿名结构体变量。
我们先思考下面这个代码:
struct Node
{
int data;
struct Node next;
};
这个是不是结构体的自引用呢?
如果是,那么结构体Node的大小是多少呢?
我们发现结构体内部嵌套一个结构体,这样是根本无法计算结构体的大小的,说明这是一个错误的代码。
那究竟怎么实现结构体的自引用呢?
让我们先来了解一下结构体自引用的定义:
结构体的 自引用 (self reference) ,就是在结构体内部,包含指向自身类型结构体的指针。
我们发现要实现结构体的自引用需要使用指针,通过指针指向结构体本身,就可以实现结构体的自引用,而且也解决了结构体嵌套无法解决的计算大小的问题。
正确的自引用代码如下:
struct Node
{
int data;
struct Node* next;
};
注意:
在进行结构体类型名的重定义前,是不允许在结构体的成员变量内使用定义后的结构体名的。
例如:
typedef struct
{
int data;
Node* next;
}Node;
因为在进行编译的时候,编译器对成员变量的编译发生在结构体的重定义之前,所以编译器在编译到成员变量Node的位置的时候,无法对Node进行识别,所以编译无法通过。
正确的重定义的方法如下:
typedef struct Node
{
int data;
struct Node* next;
}Node;
struct Point
{
int x;
int y;
}p1; //声明类型的同时定义变量p1
struct Point p2; //定义结构体变量p2
//初始化:定义变量的同时赋初值。
struct Point p3 = {x, y};
struct Stu //类型声明
{
char name[15];//名字
int age; //年龄
};
struct Stu s = {"zhangsan", 20};//初始化
struct Node
{
int data;
struct Point p;
struct Node* next;
}n1 = {10, {4,5}, NULL}; //结构体嵌套初始化
struct Node n2 = {20, {5, 6}, NULL};//结构体嵌套初始化
上面代码中的变量都是全局变量。
结构体变量的定义可以在进行结构体声明的同时进行,也可以先进行结构体的声明,再进行结构体变量的定义,这里完全取决于个人的习惯。
注意:
需要注意的是,如果在进行结构体声明的同时进行结构体变量的定义,那么这个结构体变量是全局变量,如果是先进行结构体的声明,然后在函数内部进行结构体变量的定义,那么这个结构体变量是局部变量,这是很关键的一点。
#include
struct Student
{
char name[20];
int age;
char num[10];
char class[20];
};
int main()
{
struct Student stu = { "张三",18,"2022100101","软件工程2201"};
//方法一
printf("%s %d %s %s\n", stu.name, stu.age, stu.num, stu.class);
//方法二 指针法
struct Student* ps = &stu;
printf("%s %d %s %s\n", (*ps).name, (*ps).age, (*ps).num, (*ps).class);
printf("%s %d %s %s\n", ps->name, ps->age, ps->num, ps->class);
return 0;
}
结构体内的内存对齐规则:
1.第一个成员在与结构体变量为0的地址处;
2.其他成员变量要对其到某个数字(对齐数)的整数倍处;
对齐数 = 编译器默认的一个对齐数与该成员变量大小的较小值,VS中默认对齐数是8;
3.结构体总大小是最大对齐数(每个成员变量都有一个对齐数)的整数倍;
4.如果嵌套了结构体的情况,嵌套的结构体对齐到自己最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
struct S1
{
char c1;
int i;
char c2;
};
printf("%d\n", sizeof(struct S1));
按上面的规则,c1是第一个成员变量,它应该放在相对于结构体地址偏移处为0的位置,i是第二个成员变量,它要放在对齐数的整数倍出,i 的大小是4个字节,VS 默认对齐数是8字节,那么 i 的对齐数是两者之中的较小值,就应该是4字节,所以 i 应该放在相对于结构体地址偏移量为4的整数倍处,i 应该放相对于结构体地址偏移量为4的位置处,c2是第三个成员变量,它的大小是1个字节,VS默认对齐数是8字节,c2的对齐数就是两者之中的较小值,所以c2的对齐数是1字节,c2要放在结构体地址为1的整数倍的位置处,(注意:每一个成员变量存放在的地址必须在前一个成员变量的后面),所以c2存放在 i 的地址后面,c2存放在相对于结构体地址偏移量为8的位置处。根据第三条规则,结构体的总大小是为最大对齐数的整数倍,c2存放在偏移量为8的位置,内存总大小是9,最大对齐数是4,9不是4的倍数,所以9并不是结构体的总大小,结构体内存的总大小应该是4*3=12。红色部分的空间虽然分配给了结构体,但是没有被利用,被浪费掉了。
如何测试计算的结果对不对呢?
在C语言中提供了一个函数,可以计算成员变量相对于结构体起始地址的偏移量,这个函数就是offsetof(),它包含在
它的第一个参数是结构体类型,第二个参数是成员变量,返回值是一个无符号的整型,返回一个相对于结构体起始地址的偏移量。
嵌套结构体的类型计算:
#include
#include
struct S1
{
char c1;
int i;
char c2;
};
struct S2
{
char a1;
char a2;
int j;
struct S1 s1;
};
int main()
{
struct S2 s2;
printf("a1相对于结构体起始地址的偏移量=%d\n", offsetof(struct S2, a1));
printf("j相对于结构体起始地址的偏移量=%d\n", offsetof(struct S2, j));
printf("a2相对于结构体起始地址的偏移量=%d\n", offsetof(struct S2, a2));
printf("c1相对于结构体起始地址的偏移量=%d\n", offsetof(struct S2 , s1.c1));
printf("i相对于结构体起始地址的偏移量=%d\n", offsetof(struct S2, s1.i));
printf("c2相对于结构体起始地址的偏移量=%d\n", offsetof(struct S2, s1.c2));
printf("结构体的内存的总大小=%d\n", sizeof(struct S2));
return 0;
}
a1是第一个结构体成员变量,直接对齐到结构体起始地址偏移量为0的地址处,a2是第二个结构体成员变量,a2大小为1字节,默认对齐数是8字节,a2的对齐数就是1,a2存放在相对于结构体起始地址偏移量为1的整数倍的位置处,所以a2存放在1的位置处,j是第三个成员变量,j 的大小为4字节,默认对齐数是8字节,所以 j 的对齐数是4,所以j存放在偏移量为4的整数倍处,j 的大小为4,所以 j 存放在4,5 ,6, 7的位置,s1的最大对齐数是4,所以应该存放在最大对齐数4的整数倍处,s1的总大小为12,所以s1存放在8~19的位置处。红色部分的空间虽然分配给了结构体s2,但是没有被利用,浪费掉了。
为什么存在内存对齐? 大部分的参考资料都是如是说的:
- 平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。- 性能原因: 数据结构(尤其是栈)应该尽可能地在自然边界上对齐。 原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访 问。
总体来说:
结构体的内存对齐是拿空间来换取时间的做法
#include
#pragma pack(8)//设置默认对齐数为8
struct S1
{
char c1;
int i;
char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认
#pragma pack(1)//设置默认对齐数为1
struct S2
{
char c1;
int i;
char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认
int main()
{
//输出的结果是什么?
printf("%d\n", sizeof(struct S1));
printf("%d\n", sizeof(struct S2));
return 0;
}
实际上修改默认对齐数和不修改默认对齐数的结构体所占内存空间大小的计算方法是一样的。
值得注意的是,一般情况下不建议修改默认对齐数,一是系统在进行内存空间分配的时候有自己的规则,二是系统在读取数据的时候,访问的地址也有自己的规则,如果贸然修改了默认对其数,可能会造成空间和时间上的浪费,三是修改的默认对齐数一般都是2^n。
#include
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;
}
上面的 print1() 和 print2() 哪个更好一些?
首选print2函数。
原因:
函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。
位段的声明和结构体是类似的,有两个不同:
1.位段的成员必须是int、unsigned int 、signed int或者是char(属于整型家族)类型。
2.位段的成员名后面有一个冒号和一个数字。
比如:
struct A
{
int _a:2;
int _b:5;
int _c:10;
int _d:30;
};
A就是一个位段类型。
位段类型的成员变量一般是同一个类型的;
整形类型有自己的内存分配方式,结构体也有自己的内存分配方式,既然位段的声明方式和结构体那么相似,
那么它的内存分配方式是不是和结构体的内存分配分配方式一样相似呢?
位段的内存分配方式:
- 位段的成员可以是 int unsigned int signed int 或者是 char (属于整形家族)类型
- 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。
- 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。
比如:
//一个例子
struct S
{
char a:3;
char b:4;
char c:5;
char d:4;
};
int main()
{
struct S s = {0};
s.a = 10;
s.b = 12;
s.c = 3;
s.d = 4;
return 0;
}
根据VS2019调试,我们可以得出位段类型的成员变量赋值后在内存中存放的方式如上图所示。
1.int 位段被当成有符号数还是无符号数是不确定的。
2.位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机器下会出问题)
3.位段中的成员在内存中从左向右分配,还是从右向左分配,标准尚未定义。
4.当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的。
本文中,所举的例子,都是在VS2019的环境下进行调试的,位段的内存分配方式也只是VS2019下的分配方式,位段存在的最大的问题就是跨平台问题,它不支持跨平台,所以在使用时,要格外的注意。
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;
}
第一个成员变量的大小是一个字节,所以内存中的第一个字节的内存空间是分配给ucPim1的,3的二进制位为00000011,但是ucData0是位段,长度为1个bit,所以第二个字节的第一个bit存放的是1,
4的二进制数为00000100,ucData1是位段,长度为2,所以内存中第二个字节的第2个和第3个bit存放的都是0,5的二进制数位00000101,ucData2是位段,长度为3,所以内存中第二个字节的第4、第5、第6个bit分别存放着1、0、1,内存中存放的数据为 00000010 00101001 00000000 00000000,
转换为十六进制就是 0X02290000
1.如果要使用匿名结构体,需要在进行匿名结构体声明的同时创建匿名结构体变量,匿名结构体一般只使用一次
2.结构体的自引用是通过结构体指针访问自身来实现的,并不是在结构体内部嵌套结构体本身。
3.结构体和位段区别,结构体中的成员变量可以是任意的,而使用位段时,位段的成员变量一般是同一种类型,结构体可以实现跨平台,而位段不能实现跨平台。
4.结构体和结构体是两个极端,结构体考虑内存对齐,它是以牺牲内存空间为代价来获取高效率,而位段是不考虑对齐的,位段本身存在的意义就是为了节省内存空间,所以位段不考虑内存对齐。