目录
在C语言中,常见的数据类型有整型int、浮点型float、字符类型char等,而仅仅用这些简单的数据类型来描述现实世界是远远不够的,如描述一本书时,要想通过定义一个变量涵盖这本书的名字,出版社,作者等信息,只用简单的数据类型是不能实现的,因此,C语言还规定了几个常见的自定义类型:结构体、枚举、联合体。
1、结构体
1.1 结构体的定义
C语言中,可以使用结构体来实现存放一组不同类型的数据。结构体也可以认为是一些值的集合,这些值称为成员变量,结构体的每个成员可以是不同类型的变量。
struct tag
{
member list;//成员列表
}variable list;//变量列表
如对一个学生的姓名、年龄、成绩进行描述时定义的结构体:
#include<stdio.h>
struct Stu
{
char name[20];//成员变量
int age;
double score;
};
int main()
{
struct Stu s1;//定义一个结构体变量s1
return 0;
}
1.2 结构体的自引用
如果有五个节点分别为1 ,2 ,3, 4 ,5 ...... 如果要像顺序表一样,按照一个线性的关系把它存起来,即通过1可以找到2,通过2找到3 ...... ,以此类推。可以把2节点的地址存到1节点,把3节点的地址存到2节点 ...... ,以此类推,在最后一个节点处存放空指针。如下定义了一个链表式的结构体:
#include<stdio.h>
struct Node
{
int data;//数据域
struct Node* next;//指针
};
int main()
{
struct Node n;
return 0;
}
1.3 结构体类型的重命名
与其他数据类型一样,结构体类型的重命名同样可以用typedef来实现:
#include<stdio.h>
typedef struct Node
{
int data;//数据域
struct Node* next;//指针
}Node;
int main()
{
struct Node n1;
Node n2;
//n2和n1的类型是一样的
return 0;
}
1.4 结构体的嵌套
如下列代码,在结构体 struct Node 里又嵌套了结构体 struct Book:
#include<stdio.h>
struct Book
{
char name[20];
float price;
char id[12];
}s={"加油",55.5f,"haha001"};
struct Node
{
struct Book b;
struct Node* next;
};
int main()
{
//struct Book s2={"努力",55.3f,"yeah002"};//也可以这样创建结构体变量
struct Node n={{"gogogo",89.2f,"lyd"},NULL};
return 0;
}
2、结构体大小的计算
有如下两个结构体 S1、S2 :
struct S1
{
char c1;
int i;
char c2;
};struct S2
{
char c1;
char c2;
int i;
};
它们的成员列表相同,而排列顺序不同,由简单数据类型的大小可知,char 类型占一个字节,int 类型占4个字节,这样我们可以简单估算两个结构体的大小为6个字节,实际上真的会是这样吗?下面利用 sizeof 来进行检验计算:
#include<stdio.h>
struct S1
{
char c1;
int i;
char c2;
};
struct S2
{
char c1;
char c2;
int i;
};
int main()
{
struct S1 s;
struct S2 s2;
printf("%d\n",sizeof(s)); //12
printf("%d\n",sizeof(s2)); //8
return 0;
}
可以发现,在内存中S1结构体占12个字节大小,S2结构体占8个字节的大小,那么是什么原因导致这样的结果呢?
2.1 结构体内存对齐
结构体对齐规则:
1、第一个成员在与结构体变量偏移量为0的地址处。
2、其他成员变量要对齐到对齐数的整数倍的地址处。
(对齐数 = 编译器默认的一个对齐数与该成员大小的较小值(VS默认的值为8))
3、结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
4、如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
这样,我们再看上面的S1和S2结构体:
struct S1
{
char c1;
int i;
char c2;
};
对结构体S1:假设这个结构体在一个位置处开辟空间要存储,第一个成员c1在与结构体变量偏移量为0的地址处,即在0处;对第二个成员变量,对齐到对齐数的整数倍的地址处,4与8比较4较小,4是对齐数,所以i放到(4的倍数)与结构体变量偏移量为4的地址处,占4个字节,c2为1个字节,1与8比较1较小,1是对齐数,所以c2放到与结构体变量偏移量为8的地址处。又因为结构体总大小为最大对齐数的整数倍,4与1相比,4为最大对齐数,所以现在总共占了9个字节的大小,不是4的倍数,所以还要继续开辟3个字节的空间,即12个字节的空间,所以这个结构体的大小为12个字节。
struct S2
{
char c1;
char c2;
int i;
};
同理对结构体S2:假设这个结构体在一个位置处开辟空间要存储,第一个成员c1在与结构体变量偏移量为0的地址处,即在0处;对第二个成员变量,对齐到对齐数的整数倍的地址处,1与8比较1较小,1是对齐数,所以c2放到(1的倍数)与结构体变量偏移量为1的地址处,占1个字节,i为4个字节,4与8比较4较小,4是对齐数,所以i放到与结构体变量偏移量为4的地址处。又因为结构体总大小为最大对齐数的整数倍,4与1相比,4为最大对齐数,现在总共占了8个字节的大小,是4的倍数,所以这个结构体的大小为8个字节。
2.2 嵌套结构体大小的计算
看下面代码,在结构体S4中嵌套了结构体S3,求S4结构体的大小:
#include<stdio.h>
#include<stddef.h>
struct S3
{
double d;
char c;
int i;
};
struct S4
{
char c1;
struct S3 s3;
double d;
};
int main()
{
printf("%d\n",sizeof(struct S4));//32
return 0;
}
根据结构体对齐规则第四条,如果有嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
c1大小为1字节,放到与结构体变量偏移量为0的地址处,S3可知为16字节大小,对齐到8(嵌套的结构体对齐到自己的最大对齐数的整数倍处)地址处,d大小为8字节,对齐到24地址处,此时总共占了32字节,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍,32是8的整数倍,所以S4大小即为32字节。
2.3 offsetof函数
——计算结构体成员相对于起始位置的偏移量
size_t offsetof( structName, memberName );
该函数的两个参数分别为结构体名和成员名,头文件为:<stddef.h>。
#include<stdio.h>
#include<stddef.h>
struct S3
{
double d;
char c;
int i;
};
struct S2
{
char c1;//1
char c2;//1
int i;///4
};
int main()
{
printf("%u\n",offsetof(struct S3,d)); //0
printf("%u\n",offsetof(struct S3,c)); //8
printf("%u\n",offsetof(struct S3,i)); //12
//printf("%d\n",sizeof(struct S3)); //16
printf("%u\n",offsetof(struct S2,c1)); //0
printf("%u\n",offsetof(struct S2,c2)); //1
printf("%u\n",offsetof(struct S2,i)); //4
return 0;
}
2.4 修改默认对齐数
上面介绍到,在VS编译器中默认对齐数为8(linux环境下没有默认对齐数,此时自身的大小就是其对齐数),在对齐方式不合适的时候,我们可以更改默认的对齐数。
利用#pragma pack( )修改默认对齐数:
#include<stdio.h>
#pragma pack(4) //设置
//#pragma pack(1)----9 改成1时实际上就是按照不对齐方式存储的
struct S
{
char c;
double d;
};
#pragma pack() //取消
int main()
{
struct S s;
printf("%d\n",sizeof(s));//12
return 0;
}
可知,当修改默认对齐数为1时,结构体大小变为9字节,此时就是按照不对齐方式存储的;当修改默认对齐数为4时,此时结构体大小变为12字节。
2.5 结构体内存对齐的意义
1. 平台原因(移植原因):
并不是所有的硬件平台都能访问任意地址上的任意数据的,某些硬件平台只能在某些地址处(比如说只能在4的倍数地址处访问)取某些特定类型的数据,否则抛出硬件异常。
2、性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。为了访问未对齐的内存,处理器需要作两次内存访问,而对齐的内存访问仅需要一次访问就可以。
总的来说,结构体内存对齐是拿空间来换取时间的做法。在上面的代码中,改了下成员的顺序,开辟的空间就不一样了。所以,在设计结构体的时候,如果考虑既要满足对齐,又要节省空间,可以让占用空间小的成员尽量集中在一起(这样在一定程度上浪费的空间就会更少)。
3、结构体传参
比较下列结构体传参的两种方式:
①结构体传参
#include<stdio.h>
struct S
{
int data[1000];
int num;
};
void print1(struct S s)
{
printf("%d\n",s.num);
}
int main()
{
print1(s);//传结构体
return 0;
}
②结构体地址传参
#include<stdio.h>
struct S
{
int data[1000];
int num;
};
void print2(struct S* ps)
{
printf("%d\n",ps->num);
printf("%d\n",(*ps).num);
}
int main()
{
print1(&s);//传结构体地址
return 0;
}
以上两种方式都可以实现对结构体成员信息的打印。不过,函数在传参的时候,参数是需要压栈的,会有时间和空间上的系统开销。第一种方式把整个结构体传参,如果结构体过大,参数压栈的时候系统开销比较大,会导致系统性能的下降;而第二种方式只需要传一个指针,而指针的大小是固定的,4字节(32位平台)或者8字节(64位平台),所以结构体传参的时候,尽量传结构体的地址。
4、位段
了解了结构体,就不得不提结构体实现位段的能力了。
4.1 什么是位段
位段的声明和结构体是类似的,注意有两处不同:
1. 位段的成员必须是 int 、unsigned int 或 signed int (char 其实也可以)
2. 位段的成员名后边有一个冒号和一个数字,数字代表占用 bit 位的数量
比较结构体和位段的不同:
结构体:
//结构体
struct A
{
int _a;
int _b;
int _c;
int _d;
};
位段:
//位段
struct B
{
int _a : 2;
int _b : 5;
int _c : 10;
int _d : 30;
};
对于结构体A,如果不考虑对齐,结构体A需要4个整型即16个字节的内存;对于位段B,_a这个成员只需要占2个bit位,_b这个成员占5个bit位,_c这个成员占10个bit位,_d这个成员占30个bit位,总共只要47个bit位,所以只用两个整型空间即可存下:
#include<stdio.h>
//结构体
struct A
{
int _a; //一个整型的取值范围为INT_MIN ~ INT_MAX
int _b;
int _c;
int _d;
};
//位段
struct B
{
int _a : 2; //2个比特位 只能放 00 01 10 11 四个数, 如果有一个变量正好只有这几个取值,可以考虑用位段
int _b : 5;
int _c : 10;
int _d : 30;
};
int main()
{
printf("%d\n",sizeof(struct A)); // 16
printf("%d\n",sizeof(struct B)); // 8 ---- 确实只需要两个整型的空间
return 0;
}
注意:
结构体A中的整型成员_a,取值范围为INT_MIN ~ INT_MAX;
位段B中的成员_a由于只占两个bit位的大小,意味着只能放 00 ,01 ,10, 11 四个数。
所以在定义结构体的时候,如果发现某些成员是不需要那么大的空间的,只需要少量的bit位就够了,这个时候使用位段在一定程序上节省了空间。
4.2 位段的内存分配
struct A
{
int _a : 2;
int _b : 5;
int _c : 10;
int _d : 30;
};
对这个位段(均为int),在内存存储的时候,先开辟了一个int类型的空间,即4个字节,先存_a,占了2个bit位,还有30个,再存_b,又占了5个bit位,还剩25个bit位,再存_c,还剩15个bit位,再存_d的时候,空间不够了,考虑再开辟一个int类型的空间,就能放下_d了。
此时考虑一个问题:放_d的时候是接着后面放还是在新开辟的那个int类型里的空间去放?
假设存储的时候从低位开始,空间不够了在新开辟的空间去存,不够的空间浪费掉,最后推算的应该是需要3个字节,下面用一个例子去验证:
#include<stdio.h>
struct S
{
char a : 3;
char b : 4;
char c : 5;
char d : 4;
};
int main()
{
struct S s = {0};
printf("%d\n",sizeof(s)); // 3
return 0;
}
由打印结果可知,我们的推断是正确的,确实是3字节。
4.3 位段的跨平台问题
在使用位段的时候,同时要注意:
1. 由于 int 位段被当成有符号数还是无符号数是不确定的;
2. 位段中最大位的数目也是不确定的 (16 位机器最大16,32 位机器最大 32,如果写成27,在16位机器会出问题);
3. 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义(VS中从右向左分配);
4. 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这也是不确定的。
由于以上的不确定性,造成了位段有跨平台的问题。
4.4 总结:
1. 位段的成员可以是int 、 unsigned int 、 signed int 或是char(属于整型家族)类型
2. 位段的空间上是按照需要以4个字节(int)或者1个字节(char)的方式来开辟的3. 在VS环境下:位段的在内存中的空间开辟与存储规则是:当存不下的时候开辟空间,放的时候直接在新开辟的空间里去存,之前放不下的空间就浪费掉了
4. 与结构相比,位段可以很好的节省空间,但是有跨平台的问题所在,注重可移植的程序应该避免使用位段