在进行学习之前,大家可以先看看我之前写的初阶结构体的知识:C语言初阶之结构体_囚徒玩电脑的博客-CSDN博客
1.结构体类型的声明
1.1结构的基础知识
结构是一些值的集合,这些值被称为成员变量。结构的每个成员可以是不同类型的变量。
1.2结构的声明
struct tag //tag : 根据实际情况写名字
{
member-list; //成员列表:1个或多个成员变量
}variable-list; //变量列表
1.结构体类型
struct Stu
{
char name[20];
int age ;
char sex[5];
};
2.根据结构体创建变量
s1、s2、s3、s4都是创建好的变量
//描述一个学生
//名字+年龄+性别
//声明结构体类型
struct Stu
{
//成员变量:是用来描述结构体对象的相关属性的
char name[20];
int age ;
char sex[5];
}s2,s3;
struct Stu s4;
int main()
{
struct Stu s1;
return 0;
}
3.结构体的特殊声明
在声明结构的时候,可以不完全的声明。
//匿名结构体类型
struct
{
int a;
char b;
float c;
}x;
struct
{
int a;
char c;
float f;
}*p;
上面两个结构体类型在声明的时候省略了结构体标签(tag),那么问题来了
//在上面代码的基础上,这个代码合理嘛?
p = &x;
警告:
编译器会把上面的两个声明当成两个完全不一样类型。所以以上代码是非法的。
1.3结构成员的类型
1.3结构的自引用
在结构中包含一个类型为该结构本身的成员
正确的自引用方式
struct Node
{
int data; //数据域
struct Node* next; //指针域
};
1.4结构成员的类型
结构的成员可以是标量、数组、指针,甚至是其他结构体。
struct S
{
int a;
char arr[5];
int* p;
};
struct B
{
char ch[10];
struct S s;
double d;
};
int main()
{
return 0;
}
1.5结构体变量的定义和初始化
struct S
{
int a;
char arr[5];
int* p;
}s1 = { 100,"bit",NULL };
struct S s2 = { .p = NULL,.a = 122,.arr[5]="cha"};//初始化变量
int main()
{
struct S s3 = { 90,"abc",NULL };
return 0;
}
1.6结构体内存对齐
首先我们先来看一段代码
struct S1
{
char c1;
int i;
char c2;
};
struct S2
{
int i;
char c1;
char c2;
};
int main()
{
printf("%d\n", sizeof(struct S1));
printf("%d\n", sizeof(struct S2));
return 0;
}
大家猜猜打印的结果是什么呢?大家看看下面的想法是不是和你的想法不谋而合?
struct S1
{
char c1;//1
int i;//4
char c2;//1
};
//打印6
struct S2
{
int i;//4
char c1;//1
char c2;//1
};
//打印6
让我们来运行一下来验证我们的猜想吧
咦,怎么和我们的猜想不一样呢,这里就涉及到了我们接下来要学习的结构体内存对齐。在进行学习之前,我们先来简单了解一下 offsetof 这个宏。
offsetof :可以计算结构体成员相较于结构体起始位置的偏移量
图解:
这时候,我们就可以用这个宏来看结构体内部是怎么计算大小了。
理解一下:
但是,到底为什么又多了3个字节呢?我们可以分析得出,结构体成员不是按照顺序在内存中连续存放的,而是有一定规则的。
结构体内存对齐的规则:
1.结构体的第一个成员永远放在相较于结构体变量起始位置的偏移量为0的位置。
2.从第二个成员开始,往后的每个成员都要对齐到某个对齐数的整数倍处。
对齐数:结构体成员自身的大小和默认对齐数的较小值。
VS上默认对齐数是8
gcc 没有默认对齐数,对齐数就是结构体成员的自身大小
3.结构体的总大小必须是最大对齐数的整数倍。
最大对齐数是所有成员的对齐数中最大的值
4.如果嵌套了结构体:嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
我们再来看看第二个结构体的 8 怎么算出来的
struct S2
{
int i;//4
char c1;//1
char c2;//1
};
从结构体内存对齐规则,我们也可以得出:尽量使占用空间少的成员集中在一起,防止结构体空间的浪费。
1.7修改默认对齐数
之前我们见过了 #pragma 这个预处理指令,这里我们再次使用,可以改变我们的默认对齐数。
# pragma pack (数字) //设置默认对齐数是该数字
# pragma pack ( ) //取消设置的默认对齐数,还原为默认
1.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;
}
注意: 首选print2函数
原因:
函数传参的时候,参数是需要压栈的。会有时间和空间上的系统开销。
如果传递一个结构体对象的时候,结构体过大,参数压栈的系统开销比较大,所以会导致性能的下降。
结论:结构体传参的时候,要传结构体的地址。
2.位段
2.1什么是位段
位段的声明和结构是类似的,有两个不同:
1.位段的成员必须是 int、unsigned int 或 signed int
2.位段的成员名后边有一个冒号和一个数字。
例如:A就是一个位段类型
struct A
{
int _a : 2;
int _b : 5;
int _c : 10;
int _d : 30;
};
想必大家很想知道这个位段的字节是什么?
解释如下:
struct A
{
int _a : 2;//2个比特位
int _b : 5;//5个比特位
int _c : 10;//10个比特位
int _d : 30;//30个比特位
};
2.2位段的内存分配
1.位段的成员可以是 int、unsigned int、signed int 或者是 char(属于整型家族)类型
2.位段的空间上是按照需要以4个字节()或者1个字节(char)的方式来开辟的。
3.位段涉及很多不确定的因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。