目录
一:结构体基础概念
1.什么是结构体?
结构体是由一批数据组合而成的结构型数据。组成结构型数据的每个数据称为结构型数据的“成员” ,其描述了一块内存区间的大小及解释意义 。
2.结构体的定义与声明
定义:结构体的定义如下所示,struct为结构体关键字,tag为结构体的名字,member-list为结构体成员列表,其必须列出其所有成员;variable-list为此结构体声明的变量。
struct tag {
member-list
} variable-list ;
声明:
//此声明声明了拥有3个成员的结构体,分别为整型的a,字符型的b和双精度的c
//同时又声明了结构体变量s1
//这个结构体并没有标明其标签
struct {
int a;
char b;
double c;
} s1;
//同上声明了拥有3个成员的结构体,分别为整型的a,字符型的b和双精度的c
//结构体的标签被命名为SIMPLE,没有声明变量
struct SIMPLE{
int a;
char b;
double c;
};
在上面的声明中,第一个和第二个声明被编译器当作两个完全不同的类型,一个是struct一个struct SIMPLE,因此即使他们的成员列表是一样的,也是不同的。如果令t3=&s1,则是非法的。
3.结构体的特殊声明:
匿名结构体声明:
* 在声明结构的时候,可以不完全声明,例如:
//匿名结构体类型
struct
{
int a;
char b;
float c;
}x;
struct
{
int a;
char b;
float c;
}a[20], *p;
上⾯的两个结构在声明的时候省略掉了结构体标签(tag)。
此时编译器就会把上面两个结构体当成完全不同的连个类型,p=&x也是非法的。并且匿名的结构体类型,如果没有对结构体类型重命名的话,基本上只能使⽤⼀次。
* 结构体类型的重命名:
typedef struct Node
{
int data;
struct Node* next;
}Node;
typedef可以把原来的结构体名字进行重命名。在这个代码中就是将struct Node重命名为Node,重命名前定义变量a需要写struct Node a,重命名后只需要写Node a即可,更加简便。
4.结构体变量的初始化:
#include <stdio.h>
struct book
{
char name[20];//名字
int prince;//价格
char id[20];//书单号
};
int main()
{
//按照结构体成员的顺序初始化
struct Stu book1 = { "红楼梦", 30, "1234" };
//按照指定的顺序初始化
struct Stu book2= { .price = 18, .name = "红楼梦", .id = "1234"};
return 0;
}
5.结构体成员的访问:
用"点"运算符进行访问(变量名+点+成员名):
#include <stdio.h>
struct book
{
char name[20];//名字
int price;//价格
char id[20];//书单号
};
int main()
{
//按照结构体成员的顺序初始化
struct Stu book1 = { "红楼梦", 30, "1234" };
printf("%s",book1.name);
printf("%d",book1.price);
printf("%s",book1.id);
return 0;
}
用结构体指针进行访问:
#include <stdio.h>
struct book
{
char name[20];//名字
int price;//价格
char id[20];//书单号
};
int main()
{
//按照结构体成员的顺序初始化
struct book a1={"红楼梦",20,"abc12345"};
struct Stu *book1 = &a1;
printf("%s",book1->name);
printf("%d",book1->price);
printf("%s",book1->id);
return 0;
}
二:结构体内存对齐
1.什么是内存对齐?
结构体内存对齐是指在创建结构体变量时,其成员在内存中的存储遵循特定的规则,以确保数据内容正确且高效地存储和访问。
2.内存对齐详解:
结构体内存对齐遵循一定的规则:
- 第一个成员变量总是存储在结构体变量存储起始位置的偏移量为0的地址处。
- 从第二个成员开始,每个成员变量必须存储在其自身对齐数的整数倍地址处。对齐数取决于编译器默认的对齐数和成员大小中的较小值,vs中默认对齐数为8。
- 结构体的总大小必须是其成员中最大对齐数的整数倍。这意味着结构体的大小可能会因为对齐需求而大于所有成员大小的总和。
- 当结构体中嵌套有结构体时,嵌套的结构体及其成员必须按照上述规则对齐。嵌套结构体的对齐到其自身成员最大对齐数的整数倍处,而整个结构体的大小则是所有最大对齐数的整数倍。
这里添加一个代码:
struct S2
{
char c1;
char c2;
int i;
};
printf("%d\n", sizeof(struct S2));
来通过上面的四条规则来计算结构体类型的大小。
如图所示:
图中最上面的横线是结构体内存中的起始位置,也是偏移量为0的地址,偏移量就是结构体内存中变量相对于偏移的大小。
根据规则一,c1应该存在偏移量为0的地址,且c1为char类型因此占一个字节,即第一个格子放c1。
接下来存c2,根据规则二,每个成员变量必须存储在其自身对齐数的整数倍地址处,假设存在vs中,应该存c2大小的整数倍地址处,即1的整数倍地址,已知c1存在偏移量为1的地址处,那么c2应该接着c1存在偏移量为1的地址处。
接着存i,根据规则2,因为i的大小为4个字节,比8小,因此对齐数为4,因此存在4的整数倍地址处,因此接着c2存,跳过6个字节,存在偏移量为8的地址处,因此i在地址中占的是偏移量8—11的位置。
最后,根据规则三,结构体的总大小必须是其成员中最大对齐数的整数倍。即4的整数倍。由于此时结构体内三个成员在内存中占用的是0—11字节的空间,共占用12个字节,是4的整数倍,所以12个字节也是这个结构体的大小。
3.为什么存在内存对齐?
1. 平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定 类型的数据,否则抛出硬件异常。
2. 性能原因: 数据结构(尤其是栈)应该尽可能地在⾃然边界上对⻬。原因在于,为了访问未对⻬的内存,处理器需要 作两次内存访问;⽽对⻬的内存访问仅需要⼀次访问。假设⼀个处理器总是从内存中取8个字节,则地 址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对⻬成8的倍数,那么就可以 ⽤⼀个内存操作来读或者写值了。否则,我们可能需要执⾏两次内存访问,因为对象可能被分放在两 个8字节内存块中。
总体来说:结构体的内存对⻬是拿空间来换取时间的做法。
三.结构体传参
struct S
{
int data[1000];
int num;
};
struct S s = {{1,2,3,4}, 1000};
//结构体传参
void print1(struct S s)
{
printf("%d\n", s.num);
}
//结构体地址传参
void print2(struct S* ps)
{
printf("%d\n", ps->num);
}
int main()
{
print1(s); //传结构体
print2(&s); //传地址
return 0;
}
在上面传参中,printf1传地址的传参形式比printf2传结构体的形式要好。
因为函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。 如果传递⼀个结构体对象的时候,结构体过⼤,参数压栈的的系统开销⽐较⼤,所以会导致性能的下降,而传输地址则避免了这种情况。
四.结构体实现位段
1.什么是结构体位段?
*位段是以位为单位来定义结构体(或联合体)中的成员变量所占的空间。含有位段的结构体(联合体)称为位段结构。采用位段结构既能够节省空间,又方便于操作。
*位段的成员必须是 int、unsigned int 或signed int ,在C99中位段成员的类型也可以 选择其他类型。
*位段的成员名后边有⼀个冒号和⼀个数字
例如:
struct A
{
int a:2;
int b:5;
int c:10;
int d:30;
};
A就是一个位段类型。那位段A所占的空间是多少呢?
2.位段在内存中的存储
*位段的成员可以是 int ,unsigned int, signed int 或者是 char 等类型
*位段的空间上是按照需要以4个字节( int )或者1个字节( char )的⽅式来开辟的。
注意:位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使⽤位段。
这里举一个在vs中位段存储的例子:
struct S
{
char a:3;
char b:4;
char c:5;
char d:4;
};
struct S s = {0};
s.a = 10;
s.b = 12;
s.c = 3;
s.d = 4;
在这个代码中,内存是如何开辟空间来存储数据呢?
由于结构体中都是char类型位段成员,因此位段的空间是一个字节开辟的,一个字节不够用,再开辟一个字节。
10 存入char a中由于字节限制只存入了后8个bit位:00001010,又因为位段限制,只能存入三个bit位,即只存入010。
12存入char a中只存入00001100,因为位段限制,只存入1100。
此时,这一个字节的内存空间存入了7个bit位,还剩一个没有填充,则直接填充0,再开辟一个字节的空间供接下来的位段成员存储。
因此把a、b、c、d全部存储进位段中,需要3个字节的空间。可以用下图来表示:
3.位段的跨平台问题
1. 位段被当成有符号数还是⽆符号数是不确定的。
2. 位段中最⼤位的数⽬不能确定。(16位机器最⼤16,32位机器最⼤32,写成27,在16位机器会 出问题。
3. 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。 4. 当⼀个结构包含两个位段,第⼆个位段成员⽐较⼤,⽆法容纳于第⼀个位段剩余的位时,是舍弃 剩余的位还是利⽤,这是不确定的。
总结: 跟结构相⽐,位段可以达到同样的效果,并且可以很好的节省空间,但是有跨平台的问题存在。