结构体
结构体类型的声明、定义
结构的概念:
结构是一些值的集合,这些值称为成员变量,结构的每个成员可以是不同的数据类型。
(数组也是一些值的集合,这些值称为元素,数组的每个元素是相同的数据类型)
//1.完全声明
struct Stu //关键字 标签
{
char name[20];//成员变量
int age;
char sex[10];
int id[10];
}stu1;//此处定义的结构体变量是全局变量
//分号不能丢
struct Stu stu2; // 全局变量
//2.匿名声明
//值得注意的是匿名声明如果想创建变量,得在声明结构体类型时一同创建,否则再想创建就没机会了
struct
{
int id[10];
char name[20];
int price;
}book1;
struct
{
int id[10];
char name[20];
int price;
}*p;
int main()
{
//创建的局部变量
struct Stu stu3;
struct Stu stu4;
return 0;
}
可以看出,上面两个匿名结构体成员变量相同,那么此处是否可以通过结构体指针*p访问book1呢?
很明显,是不能得,纵使成员变量一样,但实际上编译器会把他们当作不同的类型
结构体的初始化和重命名
重命名
typedef struct Node
{
int age;
char name;
}Node;//新的类型名
Node one;//使用新的类型名定义变量
//那么下面这种定义方法可行吗?
typedef struct Book
{
int price;
Book* next;
}Book;
//注意,是不行的,我们在重定义类型名之前就使用重定义之后的类型名,
//编译器会报错:未定义的标识符
int main()
{
Node two;
return 0;
}
初始化
struct Stu
{
char name[20];
int age;
char sex[10];
int id[10];
};
struct Book
{
int price;
char name[20];
struct Stu stu3; //结构体内可以包含不同的结构体类型
};
struct Stu stu2 = { "maal",33,"male",23232 };
int main()
{
struct Stu stu1 = { "David",22,"male",112233 };
struct Book book1 = { 88,"old house",{"haha",22,"female",32323} };
return 0;
结构体的自引用
在结构中包含一个类型为该结构本身的成员是否可以呢?
首先答案是肯定的。
那么怎么包含呢?
struct Meul
{
int prince;
struct Meul one;
};
struct Sport
{
int time;
struct Sport *next;
};
int main()
{
sizeof(struct Meul);
return 0;
}
像上面两种方式的包含,那种是可行的呢?
第一种结构里包含一个自身结构,自身结构又包含一个自己,就这样一直包含下去.......
即一个节点存储下一个节点,下一个节点再存储下下个节点..........
(俄罗斯套娃可还行?)
如果想要求结构体大小,是无法求出来的。
那么问题来了,第二种不也是自己包含自己吗?
这里我们借助链表(简单的就像Sport结构体,一个存数据,一个存地址)便于理解,但并不作深入讨论。
第二种包含的是结构体指针,(指针在32位系统下4个字节,64位系统下8个字节)
我们不是存储下一个节点,而是存储下一个节点的地址,当我们某一个指针域不需要再指向下一个节点时
,只需要将最后一个节点的指针域指向空就可以。
结构体内存对齐
求出结构体的大小是一个非常常见的问题。那么怎么求出呢?
首先我们先来看一段代码
struct stu
{
char name;
int age;
char sex;
};
struct book
{
char name;
char id;
int price;
};
int main()
{
printf("%d\n", sizeof(struct stu));
printf("%d\n", sizeof(struct book));
return 0;
}
上面的两个不同的结构体类型的成员变量的数据类型一样,那么他们所占的字节数是否也一样呢?如果一样是否都占6个字节呢?
自认而然的,我们想到,数据类型相同,那么这些变量占的字节数也应该相同,都应该占6个字节
但事实并非如此。
如下图所示:
那么为什么会这样呢?
首先得掌握结构体的对齐规则:
1.第一个成员在与结构体变量偏移量为0的地址处。
2.其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处
对齐数 = 编译器默认的一个值与该成员大小的其中的较小值
例如:VS默认对齐数是8,而gcc没有默认对齐数
3.结构体的总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍
4.如果嵌套了结构体 , 嵌套的结构体对齐到自己最大对齐数的整数倍
(成员变量中最大的对齐数作为结构体的对齐数),结构体的整体大小就是所有最大对齐数
(含嵌套结构体的对齐数) 的整数倍。
由此就可以求解了
struct stu
{
char name;
int age;
char sex;
};
如图
假设从箭头处开始存储,箭头处指向的地址偏移量就是0
char变量大小为1,VS默认为8,取较小的,对齐数就是1
同理 int 对齐数为4
存放数据的步骤:
1.箭头指向偏移地址为0的内存,是char的整数倍,放入1个字节的内容。
2.往下走,到达偏移量为1的内存处,不是4的整数倍,继续往下走,直到偏移量为4,放入4个字节的内容。
3.此时箭头指向的偏移量为8,是char的整数倍,放入1个字节的内容。
4.此时所有数据全部放入,整体大小为9,不是4的倍数(最大对齐数的倍数),补充3个字节,变成12个字节
所以此结构体的大小是12
在内存中存放的方式如图
同理,
struct book
{
char name;
char id;
int price;
};
内存分布如图
存放数据的步骤:
1.箭头指向偏移地址为0的内存,是char的整数倍,放入1个字节的内容。
2.往下走,到达偏移量为1的内存处,是char的整数倍,放入1个字节的内容。
3.往下走,到达偏移量为2的内存处,不是4的整数倍,继续往下走,直到偏移量为4,放入4个字节的内容。
4.此时所有数据全部放入,整体大小为8,是4的倍数(最大对齐数的倍数),完成存储。
由此我们便知道了结构体大小的计算。
更进一步,上面我们知道结构体可以嵌套,那么嵌套结构体大小怎么求呢?
struct stu
{
char name;
int age;
char sex;
};
struct book
{
char name;
char id;
int price;
struct stu s;
};
int main()
{
printf("%d\n", sizeof(struct stu));
printf("%d\n", sizeof(struct book));
return 0;
}
首先我们了解了对齐数的概念,自然就能知道book结构体类型
前面两个char和int在内存的排列方式,最后那个结构体的对齐数怎么确定呢?
根据对齐数规则,这个结构体大小是12,VS默认对齐数是8,选较小的8,结果就是
24,但是事实并非如此。如图
这时候我们的规则4就起到了至关重要的作用
4.如果嵌套了结构体 , 嵌套的结构体对齐到自己最大对齐数的整数倍
(成员变量中最大的对齐数作为结构体的对齐数),结构体的整体大小就是所有最大对齐数
(含嵌套结构体的对齐数) 的整数倍。
根据这个规则,可以得到结构体的对齐数是成员变量中最大的对齐数,所以我们可以确定book类型中的
s结构体变量对齐数是4,步骤如下:
1.箭头指向偏移值为0的内存,是char的倍数,放入char数据。
2.往下走,找到偏移量为4的内存处,放入int数据,占用4个字节。
3.往下走,找到偏移量为5的内存处,放入char数据,占用1个字节。
4.此时偏移量为6,往下走,一直到偏移量为8的内存储,放入12个字节。
5.此时结构体总大小为20个字节,是4的倍数(最大对齐数),完成存储。
为什么会存在内存对齐?
大部分参考资料都是如是说的。
1.平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处
取某些特定类型的数据,否则抛出硬件异常;
2.性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要
作两次内存访问;而对齐的内存访问仅需要一次访问。
如图,假设内存从箭头处开始访问,一次可以访问4个字节的内容,由于没有采用内存对齐,所以需要访问
两次内存才能完整读出a的内容
总的来说:
结构体的内存对齐是用空间换时间
拓展知识
另外,我们可以通过预编译命令#pragma pack()来改变VS编译器的默认对齐数
#pragma pack(1)//设置默认对齐数为1
struct Stu
{
char a1;
int a2;
char a3;
int a4;
};
#pragma pack()//取消默认对齐数
int main()
{
struct Stu s1;
printf("%d\n", sizeof(s1));
}
如图
我们也可以通过offsetof宏来得到结构体各个成员变量的偏移量
#include <stddef.h>
#pragma pack(1)//设置默认对齐数为1
struct Stu
{
char a1;
int a2;
char a3;
int a4;
};
#pragma pack()//取消默认对齐数
int main()
{
printf("%d\n", offsetof(struct Stu, a1));
printf("%d\n", offsetof(struct Stu, a2));
printf("%d\n", offsetof(struct Stu, a3));
printf("%d\n", offsetof(struct Stu, a4));
}
结果如图(在改变默认对齐数的情况下)
结构体传参
函数调用时常常要传递一些参数,当我们需要传递结构体时怎么进行传递呢?
struct stu
{
char name;
int age;
char sex;
};
void Init1(struct stu tmp)//值传递
{
tmp.name = 'a';
tmp.age = 12;
tmp.sex = 'm';
}
void Init2(struct stu* p)//地址传递
{
p->name = 'a';
p->age = 19;
p->sex = 'm';
}
void Print1(struct stu tmp)
{
printf("%c %d %c\n", tmp.name, tmp.age, tmp.sex);
}
void Print2(const struct stu* p)
{
printf("%c %d %c\n", p->name, p->age, p->sex);
}
int main()
{
struct stu stu1={'b',22,'w'}, stu2={'q',44,'w'};
Init1(stu1);
Init2(&stu2);
Print1(stu1);
Print2(&stu2);
return 0;
}
问题来了,想要修改结构体中的成员变量值,上面两种方式行不行?
首先,第一种值传递方法:
我们知道,传参时会创建临时变量,临时改变的变量值如果不返回并不会影响主函数中的值。
第二种地址传递方式,可以通过指针访问结构体的内存并进行修改。
如图
这里我们是通过调用Print函数打印结构体成员变量的值的
那么问题就来了,是Print1函数好呢?还是Print2函数好呢?
值传递方式会创建临时变量,占用内存,我们声明的结构体足够小,并不会有多大的影响。
但是当传递的变量足够大时,这样传递就显得尤为尴尬,浪费时间和空间。
而地址传递方式就没有这种顾虑,直接进行访问,当然,如果操作不当会改变原来的值,这并不是我们想要的,
这也就是加const修饰的原因。
所以我们就可以得到结论:结构体进行传参时,传结构体的地址
结构体实现位段
结构体是可以实现位段的
什么是位段?
1.位段的成员必须是整形,例如:int、unsigned int、signed int、short、char等等。
2.位段的成员名后边有一个冒号和数字。
(值得注意的是,冒号后面的数字是不能超过数据类型的大小的,例如:int后面的数字不能超32,char后面不能超8)
struct A
{
int _a : 2;
int _b : 5;
int _c : 10;
int _d : 30;
};
int main()
{
struct A a;
printf("%d", sizeof(a));
}
位段的定义如图。如果我们要输出a的大小,该是多少呢?
自然而然的,我们很容易想到,加起来不就行吗?47!!!
但事实并非如此,如图
顾名思义,位段位段,表示的是二进制位。即_a占2个比特位,_b占6个比特位,_c占10个比特位,
_d占30个比特位,所以后面的数字表示变量所占的比特位,而不是字节数。
表示比特位的话,不应该是47个比特位,就算用字节,也应该是6个字节,怎么是8个呢?
这就需要我们先来了解位段的内存分配
位段的内存分配
1.位段的成员可以是int、unsigned int、signed int或者char(属于整形家族)类型
2.位段的空间上是按照需要以4个字节(int)或1个字节(char)的方式开辟的。
3.位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。
如图
内存分配步骤:
1.一次性先开辟一块整形空间,4个字节,32个比特位。
2.分给 a 两个bit,再分给 b 5个bit,再分给 c 10个bit
3.此时还剩下15个bit,不够 d 的空间了怎么办,那就再来一块空间吧,系统会再开辟一块4字节空间
4.把d放在新开辟的空间中,存储完成,占8个字节
但是再使用过程中可能会出现以下问题:
struct S
{
char _a : 3;
char _b : 4;
char _c : 5;
char _d : 4;
};
int main()
{
struct S s;
s._a = 10;
s._b = 20;
s._c = 3;
s._d = 4;
printf("%d %d %d %d\n", s._a, s._b, s._c, s._d);
}
首先,我们可以得出,总共占3个字节,那么输出的值应该为多少呢?
莫非是10、20、3、4?
然而并没有这么简单,如图
我们知道 a 占3bit,b 占 4 bit, c 占 5 bit,d 占 4 bit,
10、20、3、4转化为二进制分别是1010、10100、0011、0100
所以 a 就截取了后3位010,b 就截取了后4位 0100、c最高位补了个0变成00011、d正好,转化为
十进制就和图中一样了
如图
这里就会有疑问,为什么不是从低地址向高地址写入数据呢?这就牵扯到位段的跨平台问题了,如下
位段的跨平台问题
1.int位段被当成有符号数还是无符号数是不确定的。(C语言并没有规定)
2.位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,例如int a:27;在16位机器中会出问题)
3.位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
4.当一个结构包含两个位段,第二个位段成员比较大,无法容纳第一个位段剩余的位时,是舍弃剩余的位还是利用,
这是不确定的(VS会把剩余的位抛弃掉)
总结:
跟结构相比,位段可以达到同样的效果,但是可以很好的节省空间,但是也有跨平台的问题存在
这里举一个位段的应用例子:
比如我们平常经常上网,给别人聊天发信息什么的,都要进行数据的传输,假如我要给女朋友发一个呵呵(慎用),
当我发这条消息的时候,系统怎么会知道发给我的女朋友的呢?是谁发的呢?因此我们在发消息的时候,并不只是
发消息这么简单,系统会追加许多相关信息,比如谁发的,发给谁等等信息。这里有一张图简单了解下
可以看出,发的消息绑定了许多信息,而且绑定的信息并不是char、int等数据类型的整数倍,如果不使用位段的话,
占的 空间就太多了,而位段却将内存空间用到了极致。
所以位段并不是一无是处