当前位置:首页 » 《随便一记》 » 正文

一篇搞定结构体_罅隙的博客

2 人参与  2021年11月04日 16:43  分类 : 《随便一记》  评论

点击全文阅读


目录

一、是什么

二、三种结构体声明

①普通的声明

 ②自引用

 ③特殊的声明——匿名的结构体

三、三种初始化方法

四、三种解引用方法

五、结构体内存对齐(重点)

六、内存对齐的意义

七、修改默认对齐值

 八、获取偏移值

九、结构体传参


 

一、是什么

        结构体是一种自定义类型,用来存放多种类型的数据,使我们得以轻松描述诸如人、书本这些较为复杂的对象。


二、三种结构体声明

①普通的声明

struct book
{
	int date;
	float price;
	char name[10];
	char*ps;
	struct content c;
}book1;
typedef struct book
{
	int date;
	float price;
	char name[10];
	char*ps;
	struct content c;
}book;

1.struct+后面的tab(标签)组成我们创建的变量名。里面可以放入任何类型的变量,甚至是指针和另一个结构体。图一中book1是我们在定义时顺手定义的一个类型为struct book的变量

2.图二我们将struct book这个类型名重命名为book,这里并没有定义一个变量,注意区分。


 ②自引用

struct book
{
	int date;
	struct book b;
}book1;

但注意结构体不能“调用”自己 ,如上面的例子。我们可以类比递归函数的形式,图中无疑是一个“死循环”,根本无法计算他的大小,当然编译器的语法也不通过。要想实现结构体的自引用,我们可以传入一个指针,实现链表一个指向下一个的效果。如下图:

struct Node
{
	int data;
	struct Node*pn;
};

 ③特殊的声明——匿名的结构体

有时候在使用结构体的时候我们直接省去了tag,如下图,在一定程度上简化了我们的代码

struct content
{
	char name[10];
	char id[20];
	int age;
	struct
	{
		int arr[10];
	};

}s1,s2,s3;

虽然匿名结构体没有名字,但编译器也是坚决把他们看成两个不同的类型,如下图。虽然*px指向的类型看似和x一样,但编译器坚决认为两者是不同的,所以px=&x是错误的书写。

struct
{
	int price;
}x;

struct
{
	int price;
}y,*px;

三、三种初始化方法

//声明结构体
struct content
{
	int page;
	char* character;
};

typedef struct book
{
	float price;
	char name[20];
	struct content c;
}b;

typedef struct book1
{
	float price;
	char* name;
	struct content c;
}b1;

int main()
{
	//方法一:定义时赋值
	b book1 = { 10.0, "c++", {100,"bit"} };
    //方法二:先定义后赋值
	b1 book2 = {0};
	book2.price = 10.0;
	book2.name = "c++";
	book2.c.page = 100;
	book2.c.character = "bit";
	//定义时乱序赋值
	b1 book3 = {
		.name = "c++",
		.c.page = 100,
		.c.character = "bit",
		.price = 10.0
	};
}

值得注意的是我们不能用数组来接收例如“c++”,因为常量字符串实际传入的是首元素的地址,所以我们用指针接收。


四、三种解引用方法

struct book
{
	int price;
	char name[10];
};

int main()
{
	struct book b = { 10, "hello" };
	struct book*ps = &b;
	//方法一:->
	printf("%d\n",ps->price);
	//方法二:.
	printf("%d\n",b.price);
	//方法三:本质等同方法二
	printf("%d\n",(*ps).price);
	return 0;
}

五、结构体内存对齐(重点)

1.第一个成员在结构体变量偏移量为0的地址处。
2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处
   对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
    VS中默认的值为8
    Linux中的默认值为4

     gcc下没有默认值 
3. 结构体总大小最大对齐数(每个成员变量都有一个对齐数)的整数倍
4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的     整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

以下面的例子分析规则一二三。(结果分别为8,12)

struct s1
{
	char a;
	char b;
	int c;

};
struct s2
{
	char a;
	int b;
	char c;

};
int main()
{
	struct s1 s1 = { 0 };
	struct s2 s2 = { 0 };
	printf("%d\n",sizeof(s1));
	printf("%d",sizeof(s2));
}

 先来分析s2吧

①a作为第一个成员,创建在结构体偏移量为0的地址,即结构体的起始地址。占用内存一字节。

 ②b作为第二个成员要考虑偏移量的问题。b的类型int 的大小4<8,所以对齐数为4。对齐在最后未被利用的空间里找一块地址,其偏移量为对齐数的整数倍

 ③c作为第三个成员。c的类型为1,偏移量8当然是1的倍数啦,所以c随即在b的后方开辟空间。但由于规则三,现在的总大小为9,并不是最大对齐数(每个成员的对齐数之间)4的倍数,所以又在后面浪费了3个字节使得总字节为12。

 同样的道理分析s1,我们发现是s1的大小是8字节。同样的内容却占用了不同大的空间?我们由此得出以下结论:

为了减小整体所占空间,应该使占用空间小的成员集中在一起

再分析一个内嵌结构体的例子吧!

struct s1
{
	char a;
	char b;
	int  c;
	
};

struct s2
{
	char a;
	char b;
	struct s1 c;
};

int main()
{
	struct s1 s1 = { 0 };
	struct s2 s2 = { 0 };
	printf("%d\n",sizeof(s1));
	printf("%d",sizeof(s2));
}

 相同的分析不再赘述,分析一下内嵌结构体。内嵌结构体的最大对齐数是4,整个结构体的最大对齐数也是4,由此我们画出以下的内存示意图。

 内嵌结构体第一个成员进行内存对齐(对齐数为内嵌结构体内对齐数的最大值),之后的照123规则对齐。


六、内存对齐的意义

由于官方没有给出明确的规定,我们来学习以下两个比较有说服力的理由

1. 平台原因(移植原因): 不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。

2. 性能原因: 数据结构(尤其是栈)应该尽可能地在自然边界上对齐。 原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。

 想必原因一通俗易懂,我们来解释一下原因二。

 我们知道我们的电脑为32/64根地址线,换算成字节就是4/8字节,以4个字节进行说明。内存每四个字节四个字节进行访问时,若结构体的内存对齐为四个字节,则内存可以顺畅的一次性访问,若没有对齐,为了完整访问,需要两次,这就降低了效率

 总的来说,就是用空间来换取效率


七、修改默认对齐值

我们可以采用预处理指令pragma来修改,如以下的案例:打印结果先后为5  8。

#pragma pack(1)//将默认对齐数改为1;
struct book1
{
	char c;
	int a;
};
#pragma pack()//将默认对齐数改为默认值(8)
struct book2
{
	char c;
	int a;
};
int main()
{
	struct book1 b1 = { 0 };
	struct book2 b2 = { 0 };
	printf("%d\n",sizeof(b1));
	printf("%d\n", sizeof(b2));
	
}

 八、获取偏移值

我们可以采用宏“offsetof”实现,头文件为<stddef.h>

#pragma pack(1)//将默认对齐数改为1;
struct book1
{
	char c;
	int a;
};
#pragma pack()//将默认对齐数改为默认值(8)

int main()
{
	struct book1 b1 = { 0 };
	printf("%d\n",offsetof(struct book1,c));
	printf("%d\n",offsetof(struct book1,a));
}

我们也可以用offsetof来验证修改默认对齐值的情况。


九、结构体传参

  结构体传参有传址操作传参操作之分,但当我们在选择时,如果需要修改结构体,必须选择传址操作;若不用修改,也应该尽可能选择传址操作。

原因一:传址才可以改变结构体的内容

原因二:传参需要压栈,会带来内存和效率上的损耗,若结构体过大,会导致程序性能下降

若用传址的方法,但不想结构体被改变,可以采用const修饰

以下用一个简单的函数分别演示传址和传参操作。

struct book
{
	int page;
	float price;
};

void print1(struct book b1)
{
	printf("%d\n",b1.page);
}

void print2(const struct book*ps)
{
	printf("%d\n", ps->page);
}
int main()
{
	struct book b1 = {100,15.0};
	print1(b1);
	struct book*ps = &b1;
	print2(ps);
	return 0;
}

很厉害嘛,坚持看到这里,希望对你有所帮助!


点击全文阅读


本文链接:http://zhangshiyu.com/post/30480.html

对齐  结构  字节  
<< 上一篇 下一篇 >>

  • 评论(0)
  • 赞助本站

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

关于我们 | 我要投稿 | 免责申明

Copyright © 2020-2022 ZhangShiYu.com Rights Reserved.豫ICP备2022013469号-1