目录
引言
在C语言的数组中,我们得知:数组是同一数据类型的元素的集合。那由不同数据类型组成的又称为什么呢?
这就是本文的主要知识点:结构体。
一、结构体的声明
1.普通声明
声明格式为:
struct tag
{
// 成员变量
};
其中struct为声明结构体类型所必需的C语言关键字,tag为所声明的结构体标签,在大括号内的均为结构体的成员变量。
举例如下:
struct Student
{
char name[20];
int age;
float score;
};
在以上的代码中,我们声明了一个标签为Student(学生)的结构体,其中成员变量为姓名、年龄以及成绩。
2.特殊声明
在C语言中,对于结构体类型,存在一种特殊的结构体类型——匿名结构体类型。
即隐藏(不写)结构体标签的结构体。如下:
struct
{
char name[20];
int age;
}s;
在这种情况下,只能使用已声明的结构体变量s对结构体进行使用,而无法再创建或者使用该种结构体类型的变量。也可以理解为一次性的结构体。
#include <stdio.h>
struct
{
char name[20];
int age;
}s1;
struct
{
char name[20];
int age;
}*ps;
int main()
{
ps = &s1; // err!!!
return 0;
}
这种写法是错误的,虽然两个结构体都是匿名结构体类型,成员变量也都相同,但是对于计算机而言,这两个结构体所指向的内存地址是不同的!!!
二、结构体的自引用
常见的数组中的元素是连续的内存地址,在同一结构体内的成员变量也是,那么怎么把相同结构体类型的不同变量连接起来呢?这就涉及到结构体的自引用。
结构体的自引用即指向下一个同样结构体类型的空间。但是在初学时,我们可能会写为如下样式:
struct Node
{
int data;
struct Node n;
};
typedef struct
{
int data;
Node* next;
}Node;
需要注意的是:这两种写法都是错误的。第一种写法可能会造成结构体无法终止,造成程序异常;第二种写法虽然使用了typedef对结构体进行了自定义名称,但是在访问到第二个成员变量时,会因为发现Node还未创建而报出错误。
正确的写法如下:
struct Node
{
int data;
struct Node* next;
};
int main()
{
struct Node n1;
struct Node n2;
n1.next = &n2;
return 0;
}
谨记:任何变量都必须先创建再使用!!!
三、变量的定义和初始化
在已经声明过结构体类型的基础上进行结构体变量的定义和初始化。
1.结构体变量的定义
有如下几种方式:
struct Student
{
char name[20];
itn age;
float score;
}s1; // 全局变量
struct Student s2; // 全局变量
int main()
{
struct Student s3; // 局部变量
return 0;
}
在声明结构体的大括号后声明结构体变量以及在函数外声明的结构体变量均为全局变量,在整个工程内均可使用(其他源文件使用时可能需要声明);在函数内部声明的结构体变量则为局部变量,只能在该函数内部使用。
2.结构体变量的初始化
主要有两种方式对结构体变量进行初始化:
struct S1
{
int a;
char c;
};
struct S2
{
char n;
int age;
};
struct S3
{
int data;
struct S2 s5;
};
int main()
{
// 对普通的结构体变量的初始化
// 按成员变量的默认排序进行初始化
struct S1 s1 = {10,'a'};
//按指定的结构体成员进行初始化
struct S1
{
int a;
char c;
};
struct S2
{
char n;
int age;
};
struct S3
{
int data;
struct S2 s5;
};
int main()
{
// 对普通的结构体变量的初始化
// 按成员变量的默认排序进行初始化
struct S1 s1 = {10,'a'};
//按指定的结构体成员进行初始化
struct S2 s2 = {.name = 'h', .age = 21};
// 对嵌套结构体的结构体变量进行初始化
// 按成员变量的默认排序进行初始化
struct S3 s3 = {10, {'e', 20}};
//按指定的结构体成员进行初始化
struct S3 s4 = {.s5.n = 'k', .s5.age = 2, .data = 19 };
return 0;
}
四、内存对齐
以如下代码为例:
#include <stdio.h>
struct S1
{
char a;
int b;
};
struct S2
{
char a;
int b;
char c;
};
struct S3
{
char a;
int b;
char c;
char d;
};
int main()
{
printf("%d\n", sizeof(struct S1));
printf("%d\n", sizeof(struct S2));
printf("%d\n", sizeof(struct S3));
return 0;
}
使用VS2022运行后得出如下结果:
S1中int占4字节,char占1字节,输出结果却为8;S2和S3的成员变量数量不同,所得出的结果却又相同。
这里就涉及到了内存对齐的知识。
我们使用VS调试并使用内存窗口观察S1的成员变量的内存地址。如下:
可以发现:成员变量a的地址比b的地址小4,a为char类型,占用1个字节,而占用4个字节的变量b却是从比a大4的地址开始存储。
假设S1从0地址处开始存储,且已知VS环境下默认对齐数为8。模拟出S1在内存中的存储:
可以发现,0偏移量处的地址存放的是char类型的a,存放第二个变量b时,b的对齐数为4,与VS的默认对齐数相比较小的是4,从4的倍数的偏移量地址开始存放,占用4~7共4个字节。而0~7的8个偏移量搞好是得出的S1所占的大小8个字节。中间的1~3偏移量地址则浪费掉了。
我们也可以使用offsetof观察成员变量的偏移量:
这与所画出的模拟图是相符的。
那如果把S1的成员变量的顺序交换一下呢(仅交换类型,不改变名称)?
结构体大小仍旧为8个字节。但这样的S1存储方式却以发生改变。我们将不同的S1放在一起观察:
不难得出:只有5个字节(int+char)空间被实际占用,有3个字节的空间被浪费了。
关于S2以及S3所占用的大小可自行画图查看。
那么关于嵌套结构体的结构体大小又是怎样的呢?
struct S1
{
int data;
char a;
};
struct S2
{
char c;
struct S1 s1;
};
int main()
{
printf("%d\n", sizeof(struct S2));
return 0;
}
将这段代码放在VS的环境下执行:
S1占用空间为8,加上S2中的成员变量c,得出的结果却是12。
同样模拟出S2所占用的内存:
综上,可得出如下结论:
内存对齐规则:
(1)结构体的第一个成员变量存放在偏移量为0的地址处
(2)从第二个成员变量开始,要对齐到对齐数的整数倍的地址处
这里的对齐数 = 编译器默认的一个对齐数与该成员大小的较小值。
VS的默认对齐数为8;gcc环境下无默认对齐数,对齐数即为成员变量本身的大小。
(3)结构体总大小为最大对齐数的整数倍。
(4)嵌套了结构体的结构体,嵌套的结构体对齐到自身成员变量最大对齐数的整数倍处,结构体的大小就是所有对齐数(含嵌套结构体的对齐数)的整数倍。
这里有一个建议:在声明结构体时应尽量将对齐数较小的成员变量写在一起,可以有效节省内存空间。
修改默认对齐数
那么,默认对齐数可以修改吗?又是怎么修改呢?
这里介绍一种比较简单的方法:
#pragma pack()
pack后的括号中即为想要修改的对齐数。为了避免程序出现错误,应避免出现单数以及过大或过小的数字。
五、结构体传参
可分为两种情况。举例如下:
#include <stdio.h>
struct S
{
int num[5];
int data;
};
void print1(struct S s)
{
printf("%d\n", s.data);
}
void print2(struct S* ps)
{
printf("%d\n", ps->data);
}
int main()
{
struct S s = { {1,2,3,4}, 10 };
print1(s); // 传结构体
print2(&s); // 传地址
return 0;
}
在VS的环境下运行:
所得出的结果是相同的。但首选print2函数。在进行结构体传参时优先传递结构体的地址。
注意:函数传参时,参数是需要压栈的,会有时间以及和空间上的系统开销。若传递的结构体过大,则产生的系统开销也会过大,会导致性能下降,执行效率降低。
在传递的结构体不发生改变时,可使用const确保结构体不会发生变化。
六、位段
位段:数据以位的形式紧凑的储存,并允许程序员对此结构的位进行操作。
位段的声明是和结构体类似的。
1.位段的成员必须是int、unsigned int、signed int。
2.位段的成员名后必须有一个冒号和一个数字。
如下:
#include <stdio.h>
struct A
{
char a:3;
char b:4;
char c:5;
char d:4;
};
在位段的声明中,所使用的成员变量类型大小表示每次开辟的字节数;冒号后的数字表示存储该变量可用的比特位位数。以结构体A为例,char类型表示每次开辟一个字节的空间。
int main()
{
struct A a1 = { 0 }; // 将a1初始化为0
a1.a = 10;
a1.b = 12;
a1.c = 3;
a1.d = 4;
return 0;
}
假设分配到内存中的比特位是由右至左使用;且分配的内存剩余比特位不够下一个变量存放时,舍弃。做如下推算:
并在VS环境下使用内存窗口进行观察:
与推算得出的结果相同。
有以下结论:
1.位段的成员可以是int、unsigned int、signed int、char(属于整型家族)
2.位段的空间上是按照4个字节或1个字节的方式来开辟的
3.位段涉及很多不确定因素,位段是不跨平台的,注重可移植性的程序应避免使用位段
关于位段跨平台,主要有一下问题:
1.int被当成有符号数还是无符号数是不确定的
2.位段中最大位的数目不能确定(16位机器最大16,32位机器最大32,若为27,则在16位机器可能出错)
3.位段中的成员在内存中从左向右或从右向左分配是不确定的
4.当一个结构包括两个位段成员时,第一个成员剩余的位无法容纳第二个成员时,舍弃或是使用剩余位是不确定的。
结论:和结构相比,位段可以达成同样的效果,较好的节省空间,但存在跨平台的问题,不推荐使用。