今天我来分享我学到的关于结构体的知识:
1.结构体类型声明
我们不妨看一下声明:
struct tag
{
member-list;
}variable-list;
假如我们描述一个人:
struct Stu
{
size_t age;
char name;
int marks;
};(注意:不要漏掉了这个分号)
特殊的声明
我们也可以不要名称,直接匿名声明结构体:
这里虽然两个结构体类型相同,但是由于是匿名声明,是只能使用一次的,我们在声明结构体的时候要带上结构体名称,不然就没什么意义了。
结构体的自引用
我们之前学过什么叫递归,递归就是函数自己调用自己,而结构体其实也是可以自引用的,有点想递归但又不完全相同,下面我们一起来看看错误示例和正确示例吧:
错误示例:
struct Node
{
int data;
struct Node next;
}
这个是模拟链表,虽然数据是分散的,但通过这种手段成功的将这些数据连接在一起,挨个访问,但这里我们打个比方:我们不能在自己的车子里在塞进去一辆一模一样大小的车子吧,所以这个是不行的。(求这个结构体的大小将会是无穷大)
正确示例:
struct Node
{
int data;
struct Node *next;
}
我们传一个指针过去就可以了,达到链式访问的效果。
但是我们有些人喜欢摆弄:
#include
typedef struct
{
int data;
struct Node* next;
}Node;
//我们用这样的方式来重命名,是不行的,因为我们在把这个结构体设置全局变量的名称的时候
//在结构体里面就使用了这个名称。
括号外面这个Node是因为上面的匿名结构体而产生的,但是里面的成员变量优先使用了Node来创建变量,这是不行的。
正确的思路应该是
typedef struct Node
{
int data;
struct Node* next;
}Node;
就不要用匿名结构体了。
2.结构体变量的创建和初始化
这个看我写一段代码就很好理解了:
#include
struct Stu
{
int age;
char name;
float grade;
};//这个是声明;
struct Tea
{
int age;
char name;
float grade;
}p1;//边声明边创造一个结构体变量p;
struct Fam
{
int age;
struct Stu p7;
float grade;
}p2={20,"zhangsan",20.00};//边声明边创建一个全局结构体变量p2;
int main()
{
struct Stu s1={10,"wangwu",70.00};
struct Stu s2={30,"lisi",80.00};//一个名字Stu可以创建多个结构体变量,这样“赋值”算初始化;
struct Fam s9={100,{80,"lanlan",150.00},76.00};//嵌套结构体初始化;
return 0;
}
我们初始化也不一定要按照顺序来,如图:
3.结构体成员访问操作符
我们访问结构体成员的操作符有两个,一个是".",一个是“->”
结构体名称.成员变量名或结构体指针->成员变量名得到相关元素。
我们一起来看一下怎么弄吧:
看图片:
优化:如果我们不想指针对应的内容被改变,用const修饰;另外我们尽量传指针过去,以免造成内存过于占用,效率降低。
4.结构体内存对齐
结构体有以下4点对齐规则:
1 结构体中第一个元素默认在偏移起始点地址0的位置存放;
2 往后的元素存放的位置存放在偏移起始点地址为对齐数整数倍的位置上;
对齐数=编译器默认对齐数与该数据所占字节数的较小值;
3.这个结构体的大小必须等于这个结构体中铺开所有元素中的最大对齐数的整数倍;
4.如果嵌套了结构体,那么里面这个结构体的位置的可能对齐数是里面这个小结构体里面的成员最大对齐数的整数倍,同时考虑编译器的默认对齐数(VS默认为8,linux的对齐数是元素的对齐数),再做打算;整个大结构体的内存大小是全部铺开后的元素的最大对齐数的整数倍最大整数倍。
我们来几道题先实验一下:
//第一题
struct S1
{
char c1;
int i;
char c2;
};
printf("%d\n", sizeof(struct S1));
//第二题
struct S2
{
char c1;
char c2;
int i;
};
printf("%d\n", sizeof(struct S2));
//第三题
struct S3
{
double d;
char c;
int i;
};
printf("%d\n", sizeof(struct S3));
//第四题
struct S4
{
char c1;
struct S3 s3;
double d;
};
printf("%d\n", sizeof(struct S4));
第一题:char int char
第二题:char char int
第三题:double char int
第四题:嵌套结构体类型char struct double
我们挑第四题来证明一下:使用offsetof宏
但是第二个就错了,因为我没有考虑s3里面虽然那个double类型是从起始点开始的,但是s3的最大对齐数其实是8,s3的内存是8的倍数,也是24没毛病,然后画s4的内存布局要更改一下:
s3的布局图:
s4的布局图:
这里我再介绍一下怎么修改编译器的默认值:
#include
#pragma pack(1)//设置默认对⻬数为1
struct S
{
char c1;
int i;
char c2;
};
#pragma pack()//取消设置的默认对⻬数,还原为默认
在默认对齐数不合理的时候,我们可以考虑把它改一下。
结构体为什么要内存对齐?简而言之就是牺牲空间换取效率!
5.结构体传参
我们结构体传参其实和数组传参是差不多的,声明函数的时候,要加上类型名,形参,而在main函数里面使用函数时传递实参就行了。老规矩:分为传值调用和传址调用。
#include
struct Stu {
int age;
char name[20];
};//仅仅是声明,还没有初始化及定义变量
void print1(struct Stu s1)
{
printf("%d %s\n", s1.age, s1.name);
}
void print2(struct Stu* s2)
{
printf("%d %s\n", s2->age, s2->name);
}
int main()
{
struct Stu s = { 20,"lihua" };//这个结构体定义了结构体变量s,
//并给它初始化内容(我们也可以叫"赋值")。
//定义一个print1函数,一个print2函数,前者用来传值调用,后者用来传址调用。
print1(s);
print2(&s);
return 0;
}
代码结果是
但是我这里更推荐使用传址调用,因为如果我选择传值调用,参数是要压栈的,会消耗系统大量的内存和时间,这是不提倡的,而使用传址调用则好的多。
6.结构体实现位段
位段其实是基于结构体的,它其实和结构体的声明等十分相似,但又有一些不同,比如:
1.位段的成员需要是int,unsigned int,signed int或者char类型;
2.位段里面多了一个冒号;
我们现在来写代码举例一下:
这里的下划线可有可无,这里的数字表示为a,b,c,d分别开放2个bit位,3个bit位,4个bit位,5个bit位(注意:我这里讲的位同样也是正儿八经的二进制位),以达到节约空间的目的。
但是,我们这里先提醒一些内容:
1.由于大家使用的编译器不一样,编译器没办法对int类型默认为unsigned int或者是signed int,这样我们在使用的时候就可能会出bug了;
2.有一些编译器是16位的,当我想给一个数据开辟17个bit位上,编译器会报错;
3.我们无法确定从上而下的这些数据是从左往右还是从右往左分配空间的;
4.这些数据占用内存后,剩下的空间我们也不知道会怎么利用;
5.位段的内存是以int(4字节)或char(1字节)开辟空间的;
6.我们无法对这些内容进行取地址观察,因为取地址至少是以一个字节为单位,在这个字节的开头开始读取的,我们不能保证刚好我们可以通过取地址再访问的方式得到我们想要的结果;
现在我通过画图和内存监视的方式讲一下在vs2022下,这些经过位段“处理”的数据可能在内存里这样放:(顺便讲一下在这基础上一个位段的大小如何)
先上代码:
#include
struct B
{
char a : 2;
char _b : 3;
char _c : 4;
char _d : 5;
};
int main()
{
struct B s={0};
s.a=2;
s.b=5;
s.c=8;
s.d=20;
printf("%zd", sizeof(struct B));//答案是3
return 0;
}
我们来画一下图:
转换成16进制后,为1 6 0 8 1 4
我们看一下内存布局图:
这个是仅仅在vs系列下的结果,其他的我们还有待探究。