1.结构体
结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。
结构的声明:
struct person
{
char name[32];
char sex[4];
int age;
};
这样你对结构体是不是更加了解了呢?
(2).结构的成员
结构的成员可以是标量、数组、指针、甚至是其他结构体。
struct student
{
struct person s;
struct student *next;
};
{
struct person s;
struct student *next;
};
(3).结构体成员的访问
结构体变量访问成员时,结构体变量的成员是通过点操作符(.)访问的。点操作符接受两个操作数(1.结构体变量,2.结构体指针的解引用)。对于结构体指针访问结构体成员时,结构体指针是通过(->)来访问结构体成员的。比如:
struct person ret; struct person *p = (struct person*)malloc(sizeof(struct person)); strcpy(ret.name, "zhangsan"); ret.age = 21;
(*p).age = 22; strcpy(p->sex, '男');
这样你是不是对结构体变量和结构体指针访问结构体成员有了一定的认识呢?
(3).结构体的自引用
如果在结构中包含一个类型为该结构体本身的成员会发生什么情况呢?
struct Node
{
int i;
struct Node next;
};
这样可以吗?不可以,因为你无法知道这个结构体的大小。sizeof(struct Node)无法计算他的大小。所以不要在结构体内包含一个类型为结构体本身的成员。但是你可以包含一个结构体本身的指针。比如:
struct Node
{
int i;
struct Node *next;
};
为什么这样就可以呢?因为此时的next是一个大小为四个字节的指针,它可以容易算出该结构体的大小。
如果你认为每次定义一个结构体变量时类型名太长,这时你可以给类型重命名。比如:
typedf struct Node
{
int i;
struct Node* next;
}Node;
Node s;
就像这样你就不用再写struct Node 了,只用Node也可以达到相同的效果。是不是很快捷呢?
(4).结构变量的定义与初始化:
其实定义在上面已经写过了,比如:
struct Node S;
定义一个结构体变量S。
结构体的初始化其实就是在定义的同时给赋值,比如:
struct Node S = {10, NULL};
接下来的是重点:
(5).结构体的内存对齐:
首先我们来了解一下结构体的对其规则:
1.第一个成员在与结构体变量偏移量为0的地址出开始。
2.其他成员变量要对齐到某个数字的(对其数)的整数倍的地指处,对齐数 = 编译器默认的一个对齐数与该成员大小的较小值。vs中的默认值为8,kinux的默认值为4.
3.结构体总大小为最大对齐数的整数倍。
4.如果嵌套了结构体的情况,嵌套的结构体对其到自己最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数的整数倍。
首先我们来用图和代码来说明一下这四点:
struct student {
char a; int i;
}
如图所示,a变量从开始0变量的时候,然后是i,i是一个整形变量,是四个字节,所以它要对到自然边界的4的整数倍的地方,所以,1-3是不可以的,浪费掉了,所以i从4开始,由4-7存放i这个整形,此时结构体的成员全部存放完毕,一共占了0-7,8个字节,结构的最大成员是i4个字节,与vs的默认8个字节,取较小值。就是4个字节,而结构一共占了8个字节,是4的整数倍,所以不需要在向下对齐了。
你或许想问为什么计算机存在内存对齐?这个问题有两个原因:
1.平台原因:
不是所有的硬件平台都能访问任意地址的任意数据;在某些硬件平台上只能在某些特定的地址出取某些特定类型的数据,否则会出现硬件异常。
2.性能原因:
原因在于,为了访问未对齐的内存,处理器需要做两次内存访问,而对齐的内存访问只需一次。
总的来说内存对齐就是拿空间换时间的做法。
所以经过上面的讲解,我们在设计结构体时如果即要满足内存对齐,又要节省空间,应该把空间小的成员尽量其中在一起。
(6).结构体的传参
struct S
{
int data[1000];
int num;
};
struct S a = {{1,2,3,4}, 1000};
void print(struct S a)
{
printf("%d\n", a.num);
}
void print1(struct S *p)
{
printf("%d\n", p->num);
}
int main()
{
print(a);
print1(&a);
return 0;
}
这就是结构体传参,但是我推荐第二种方法,为什么呢?如果你对函数的栈帧有一定了解的话,你会知道函数传参时,参数需要压栈,而结构体变量传参数时,结构体过大,导致压栈时,系统的开销比较大,所以性能就会下降,但是如果传地址的话,结构体变量的地址只有4个字节,所以压栈时系统开销不大,性能更好。
所以结构体传参时,尽量传结构体的地址。
这就是这次所要讲解的结构体的所有内容。
2.位段:
什么是位段呢?可能有的人没有听过位段,其实位段与结构体类似。
首先我们来了解一下。位段的定义:
1.位段的成员必须是int、unsigned int 或signed int。
2.位段的成员名后边加有一个冒号和数字。
比如:
struct A { int _a:2; int _b:5; int _c:10;
int _d:30; };
这就是一个位段类型,是不是感觉和结构体相似呢?
你可能疑问在成员名后加冒号和数字是什么意思呢?其实他的意思是这个成员名是占用多少个比特位的。这时你是否可以计算一下这个结构体需要几个字节呢?
printf("%d", sizeof(struct A));
位段的内存分配:
1.位段的成员可以是int 、unsigned int、 signed int 或者是char类型。
2.位段的空间上是按照需求以四个字节或1个字节的方式来开辟。
3.位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应避免使用位段。
那么接下来我们就具体的分析一下他在内存中是如何存储的。
如图所示,首先创建变量后,先开辟4个字节,也就是32个比特位,i是16位,j是10位刚好可以放入这32个比特位中,之后z占了24个比特位,而刚开辟的32位不够用,所以再开辟4个字节,由上图可以看出,z不会放在第一次开辟后剩余的地方,而是放在了新开辟的32位中(由第一个图中内存的显示可以看出他开始是从右向左排,z是在4个字节之后)。
位段的跨平台问题:
1.int位段被当成有符号数还是无符号数是不确定的。
2.位段的最大为的数目是不确定的。(16位机器的最大位16,32位机器的最大位是32)
3.位段中的成员在内存中从左向右还是从右向左标准是没有定义的。
4.当一个结构体中包含两个位段,地问个位段成员比较大,无法容纳于第一个的位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的。
位段的与结构体相比,位段可以达到相同的效果,但是可以节省空间,不过位段有跨平台的存在。
3.枚举
什么是枚举呢?
枚举就是把可能的值一一列举。
接下来用代码了解一下枚举的定义
enum Day
{
mon,
tues,
wed,
thur,
fri,
sat,
sun
};
这就是枚举类型。{}中的内容是枚举类型的可能取值,也叫枚举常量。这些都是有值的,默认第一个从0开始,依此递增1,当然再定义的时候也可以赋以初值。
比如:
enum Day
{
mon = 1,
tues = 2
};
枚举的优点:
1.增加代码的可读性和可维护性。
2.和#define定义的标识符比较,枚举有类型检查,更加严谨。
3.放置了命名污染。
4.便于调试。
5.使用方便,一次可以定义多个常量。
警告:只可以拿枚举常量给枚举变量赋值。
enum Day
{
mon = 1,
tues = 2
};
enum Day s = mon;
s = 2;
//这样是不可以的。
4.联合
(1).联合的定义
联合定义的类型变量也包含一系列的成员,特征是这些成员共用同一块空间。
union S
{
char c;
int i;
};
union S ret;
这就是联合和联合的定义。
(2).联合的特点:
联合的成员是共用同一块空间,这样一个联合变量的大小,至少是最大成员的大小(因为联合至少有能力保存那个成员,联合也存在内存对齐)。
union S
{
char c;
int i;
};
union S ret;
printf("%d", &(ret.c));
printf("%d", &(ret.i));
这段代码执行的结果时这两个地址相同,这就说明他们占用的是同一块空间。
(3).联合大小的计算
联合的大小至少是最大成员的大小,
当最大成员的大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。比如:
union S
{
char c[5];
int i;
};
printf("%d", sizeof(union S));
这个计算结果就不是5而是8.