一、概念
结构体是一种经典的构造数据类型,所谓构造即把其它的数据类型通过某种方式放在一起从而形成一种新的数据类型。
结构体使用的某种方式即是把程序员声明的数据类型按其声明的先后顺序直接放在一起,但是同时这里设计一个内存对齐的问题,后续会说到。
二、声明、定义、初始化
1 声明(虽然和函数一样都是使用大括号{}定义,但是函数结尾不带分号;,而结构体结尾必须有分号;)
(1)不声明结构名而直接定义变量:(一般不用)
struct
{
int a;
char b;
double c;
} st; // 这里必须实例化结构体的变量,要不后面没法实例化啊
(2)声明结构名:
struct SIMPLE
{
int a;
char b;
double c;
}; // 结构体的对象可在此定义
(3)使用typedef声明结构:
typedef struct // 结构名写不写都行,一般不写
{
int a;
char b;
double c;
} Simple; // Simple不是一个被定义的结构体变量名,而是程序员声明的该结构的数据类型名
(4)如何声明两个互相包含的结构:
struct B; //对结构体B进行不完整声明
//结构体A中包含指向结构体B的指针
struct A
{
struct B *partner;
//other members;
};
//结构体B中包含指向结构体A的指针,在A声明完后,B也随之进行声明
struct B
{
struct A *partner;
//other members;
};
2 定义
和任何其它数据类型一样:
struct SIMPLE simple; // 第二种声明方式
Simple simple; // 第三种声明方式
除此以外我们还可以定义结构数组、结构指针等等:
Simple simple[100], *psimple;
3 初始化
Simple simple = {1, 'c', 1.23};
二、结构体成员的使用
1 结构体变量使用:.
simple.a = 1;
2 结构体指针使用:->
psimple->a = 2;
三、内存对齐(64位系统按8对齐)
#include <stdio.h>
typedef struct {
int i; // 4
double d; // 8
}Data1;
typedef struct {
double d; // 8
int i; // 4
}Data2;
typedef struct {
int i; // 4
double d; // 8
char c; // 1
}Data3;
typedef struct {
double d; // 8
int i; // 4
char c; // 1
}Data4;
typedef struct {
int i; // 4
double d; // 8
char s[9]; // 9
}Data5;
typedef struct {
int i; // 4
double d; // 8
char s[7]; // 7
}Data6;
typedef struct {
double d; // 8
int i; // 4
char s[9]; // 9
}Data7;
typedef struct {
double d; // 8
int i; // 4
char s[7]; // 7
}Data8;
int main() {
printf("4 8 : %ld\n", sizeof(Data1));
printf("8 4 : %ld\n", sizeof(Data2));
printf("4 8 1 : %ld\n", sizeof(Data3));
printf("8 4 1 : %ld\n", sizeof(Data4));
printf("4 8 9 : %ld\n", sizeof(Data5));
printf("4 8 7 : %ld\n", sizeof(Data6));
printf("8 4 9 : %ld\n", sizeof(Data7));
printf("8 4 7 : %ld\n", sizeof(Data8));
return 0;
}
四、柔性数组——在Linux服务器开发中应用广泛
1 试想这样一个应用场景:
我们希望通过一个结构体来存储客户端传来的协议包,但是协议包的大小却是不定的,这取决于用户,我们一般会先读包头,根据包头的长度字段来确定用户数据的大小,可是问题是结构体的大小一般是固定的啊。
2 解决上述场景中矛盾的初步方法:
这时我们可能会想到在结构中放一个指针,用户数据有多大,就给这个指针malloc多大内存,而指针本身长度永远都是8B,这种方法可以解决问题,但是其劣势明显:
在Linux服务器开发中,我们往往要将一个结构体序列化成协议包,此时会为这个结构malloc内存,而在结构里面又为了一根指针malloc了内存,这样造成了结构体内存空间的不连续——不仅在使得程序编写起来非常麻烦,而且容易造成内存碎片,降低内存的访问速度从而影响服务器性能,更有甚者,如果我们要写一个给其他人用的API呢?你在这个API里面做了结构体内部的内存分配,把这个结构返回给了用户,此时用户并不知道你写的这个API在结构内部做了新的内存分配啊。使用完该结构后用户free它只能释放结构内部的那根指针,而指针指向的内存就漏了,这是我们熟知的内存泄漏。
3 柔性数组——完美解决上述方法的劣势:
(1)概念:
在c99中提到,在一个结构体中,结构体的最后一个元素可以是一个未知大小的数组,这个成员就叫做柔性数组:
struct ST1
{
int a;
char arr[]; // 柔性数组
}
//有些编译器支持这样书写柔性数组
struct ST2
{
int a;
char arr[0]; // 柔性数组
}
(2)特点:
①结构中的柔性数组成员前面必须至少一个其他成员;
②sizeof(含有柔性数组的结构)的返回值不计算柔性数组的内存,或者说柔性数组的内存可以认为成0;
③包含柔性数组成员的结构用malloc()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小,这个预期大小理应有一个最大值控制。
五、共用体
共用体和结构体从形式上看何其类似,但是却根本不一样,结构体是按照声明顺序和内存对齐将用户声明的数据在内存中依次存放,而共用体不管声明顺序,用到哪个数据,共用体的内存大小便是这个数据的内存大小(考虑内存按4对齐,可能会比该数据的内存大一点,比如该数据是30B,sizeof(union)的结果是32),也就是说共用体的内存大小是不一的且所有数据共用一个首地址。