一.结构体是什么?
(1)是一种数据类型
首先我们需要知道的是结构体是一种数据类型,它本质上是用于将不同类型的数据组合在一起形成的一个新的数据类型。
(2)是变化的
当不同的类型组合在一起的时候,会产生不同的结构体,例如用char和float组成的与用int和char组成的实际上不是同一种结构体
(3)是自定义的
结构体是用户自定义的一种类型,它使用关键字struct来定义结构体
二.结构体的格式(声明)
代码语言:javascript复制struct tag
{
member-list;
member-2ist;
member-3ist;
...
}variable-list;variable-2ist;variable-3ist;...;
特殊声明
结构体变量的声明可以实名也可以是匿名声明,也就是把结构体的名称(如上方tag)给删除掉,进行匿名声明。但是这个时候这个结构体就只能使用一次。匿名结构体无法被其他代码块引用,也无法定义指向匿名结构体的指针,限制了其在复杂程序中的使用。
三.结构体变量的创建和初始化
首先我们需要说明一下结构体变量的访问操作符
a.
“.”:用于直接访问结构体中的变量
例如:
代码语言:javascript复制struct Person person1;
strcpy(person1.name, "Alice");
person1.age = 25;
person1.height = 1.65;
b.
“->”:用于访问结构体变量的地址
接下来我们就可以说明结构体变量的创建和初始化
代码语言:javascript复制#include <stdio.h>
struct Stu
{
char name[20];//名字
int age;//年龄
char sex[6];//性别
};
int main()
{
//按照结构体成员的顺序初始化
struct Stu s1 = { "小明", "20" ,"男"};
printf("name: %sn", s.name);
printf("age : %dn", s.age);
printf("sex : %sn", s.sex);
//按照指定的顺序初始化
struct Stu s2 = { .age = 16, .name = "xiaohua", .sex = "⼥"};
printf("name: %sn", s2.name);
printf("age : %dn", s2.age);
printf("sex : %sn", s2.sex);
return 0;
}
四.结构体的自引用
错误用法:
代码语言:javascript复制struct Node
{
int data;
struct Node next;
};//这样自引用是错误的,因为⼀个结构体中再包含⼀个同类型的结构体变量,这样结构体变量的⼤
⼩就会⽆穷⼤
正确用法:
代码语言:javascript复制struct code
{
int data;
struct Node* next;
};//这样自引用是正确的,直接对其解引用就可以避免重复包含
五.结构体的内存对齐
结构体既然作为一种类型,那么它肯定也是占有内存的。而且由于结构体不同,其内存也会跟着不同。一般正常的内存存放方式就是按照类型本身所占字节来直接存放,由于它们类型统一,所以在存放的时候也是规律的,一般不会导致内存的溢出等问题。但是结构体包含多种不同的类型,所占的内存大小也就各不相同。要想保证内存的正常占用以及不出现溢出等问题,就需要进行内存对齐。
(1)内存对齐的规则
1. 结构体的第⼀个成员对齐到和结构体变量起始位置偏移量为0的地址处
意思就是第一个成员是从起始位置开始对齐,它与正常类型的存放方式无差别。
2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
这里介绍对齐数的概念:
对齐数:编译器默认的⼀个对齐数与该成员变量大小的较小值。
请注意,对齐数有时候并不是默认的对齐数,当该变量的大小小于默认对齐数时对齐数就是该成员变量的大小。
vs的默认对齐数是8bit。Linux中gcc没有默认对齐数,对齐数就是成员自身的大小。
3. 结构体大小为最大对齐数(结构体中每个成员变量都有⼀个对齐数,所有对齐数中最大的)的 整数倍。
举例:
代码语言:javascript复制struct MyStruct
{
char a;
int b;
char c;
};
//根据内存对齐规则,结构体MyStruct的内存布局如下:
//a的大小为1字节,偏移量为0。
//b的大小为4字节,由于前一个成员a的大小为1字节,所以b的偏移量为4的整数倍,即4。
//c的大小为1字节,由于前一个成员b的大小为4字节,所以c的偏移量为4的整数倍,即8。
//因此,结构体MyStruct的总大小为8字节。
4. 如果嵌套了结构体的情况,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对⻬数)的整数倍。
举例:
代码语言:javascript复制struct InnerStruct
{
char x;
int y;
};
struct OuterStruct
{
char a;
struct InnerStruct inner;
double b;
};
/*根据内存对齐规则,结构体InnerStruct的内存布局如下:
x的大小为1字节,偏移量为0。
y的大小为4字节,由于前一个成员x的大小为1字节,所以y的偏移量为4的整数倍,即4。
因此,结构体InnerStruct的总大小为8字节。
接下来,根据内存对齐规则,结构体OuterStruct的内存布局如下:
a的大小为1字节,偏移量为0。
inner的大小为8字节(由上面计算得出),由于前一个成员a的大小为1字节,所以inner的偏移量为8的整数倍,即8。
b的大小为8字节,由于前一个成员inner的大小为8字节,所以b的偏移量为8的整数倍,即16。
因此,结构体OuterStruct的总大小为24字节。(既是4的整数倍又是8的整数倍)*/
(2)关于内存对齐原因的补充
1.硬件需求: 许多处理器在读取内存时要求数据按照特定的字节对齐方式存储,否则可能导致性能下降甚至错误。例如,某些处理器要求整型数据按4字节对齐,双精度浮点数按8字节对齐。不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2.效率需求:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。如果出现某个变量内存恰好是卡在两个内存块中,同时占用了两个内存块,那么这样实际上就需要访问两次,大大降低了效率和性能;如果我们实现内存对齐就不会出现这种情况,减少了所需要消耗的时间,达到用空间换时间的目的。
(3)内存对齐的优化
既然内存对齐会浪费一定的空间,我们在排序变量的时候就应该尽可能的去减少浪费。
举例:
代码语言:javascript复制struct S1
{
char c1;
int i;
char c2;
};
struct S2
{
char c1;
char c2;
int i;
};
//这样排序s1只占了8个,而s2占了12个,所以s1既节省了时间又没有浪费过多空间
(4)默认对齐数的修改
我们可以通过一个指令来修改默认对齐数满足自己的需要
#pragma 预处理指令
代码语言:javascript复制#include <stdio.h>
#pragma pack(1)//设置默认对⻬数为1
struct S
{
char c1;
int i;
char c2;
};
#pragma pack()//取消设置的对⻬数,还原为默认
int main()
{
printf("%dn", sizeof(struct S));//输出结果还原为默认的对齐数
return 0;
}
六.结构体的传参
来看两种传参的方式
代码语言:javascript复制struct S
{
int data[1000];
int num;
};
struct S s = {{1,2,3,4}, 1000};
//结构体传参
void print1(struct S s)
{
printf("%dn", s.num);
}
//结构体地址传参
void print2(struct S* ps)
{
printf("%dn", ps->num);
}
int main()
{
print1(s); //传结构体
print2(&s); //传地址
return 0;
}
这里我们选择传地址的方式会比直接传结构体的方式更佳。
因为函数传参的时候是会涉及到压栈概念的,形参和实参都有各自的内存空间,如果我们直接传结构体的话,会新开辟一个空间;而乳沟我们传地址的话就不会,所耗的内存空间仅占一个地址的字节。所以我们通常选择传地址。
七.结构体与位段
位段是一种特殊的结构体成员,即在一个字节或多个字节中指定位数的字段。位段的作用是可以有效地利用内存空间,将多个标志位或状态信息存储在一个整数类型(4字节)或一个字符类型(1字节)的变量中,从而节省内存空间。所以它与内存对齐的效果是相反的,它的第一标准是空间的节省。
格式:
代码语言:javascript复制struct
{
unsigned int flag1 : 1; // 1比特位的位段
unsigned int flag2 : 2; // 2位的位段
unsigned int flag3 : 5; // 5位的位段
} flags;
位段相对于结构体格式上的差别就是多了数字,用来表示所占有的比特位。
但是需要注意的是,位段的使用可能会导致代码的可移植性问题,因为位段的存储顺序和字节对齐方式可能在不同的编译器和平台上有所不同。例如,vs的规则是:
1.内存从右向左使用 2.若剩余空间不够下一成员使用就浪费
但是在其他编译器中规则可能就不是这样。
原因在于:
1. int 位段被当成有符号数还是无符号数是不确定的。 2. 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机器会出问题。 3. 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。 4. 当⼀个结构包含两个位段,第二个位段成员比较大,无法容纳于第⼀个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的。
需要注意的第二个点就是位段的成员是无法被取地址的,因为取地址是以字节为单位,从起始位置取,但是位段成员不一定在起始位置,那么根据内存中每个字节分配⼀个地址,⼀个字节内部的bit位就是没有地址的。
要想取到地址只能先将成员放入某个变量中,再赋值给位段成员。