自定义类型:【结构体】

我们知道C语言中有许多的类型,比如char,short,int等等类型。像是这些C语言本身就支持的类型叫做内置类型,但是有一些复杂对象,只有这些类型是完全不够的。比如人,或者一本书。那么我们就可以自己定义一些类型来实现。

一.结构体的基础知识

1.结构体的创建与初始化

结构是一些值的集合,这些值被称为成员变量。结构的每个成员可以是不同类型的变量。我们先来创建一个结构体类型,看一下是怎么用struct进行创建的。

struct student//student自己取的名字,是类型
{
	int age;//学生的年龄
	char name[30];//学生名字
	char sex[5];//学生性别。这三个都是成员变量。
}xiaoming;//xiaoming就是我创建的一个变量,这是其中的一种创建变量的方法,全局变量
int main()
{
	struct student xiaowang;//这也是创建变量的方法,是局部变量
	return 0;
}

关于结构体的初始化也是很简单的。比如还是上面我创建的学生类型。

	struct student xiaowang = {18,"小王","男"};//直接输入,按着顺序
	struct student xiaohong = {.age=20,.sex = "女",.name="小红"};//也可以乱序

这里的.是结构体引用操作符。它用于访问结构体的成员变量或成员函数。通过结构体变量名后面加上.,然后跟上成员的名称,就可以访问该成员。

匿名结构体类型

这里我单独介绍一下:匿名结构体类型。它有一个特点就是只能使用一次。

struct //关于匿名结构体,这里是没有名字的,我们想要创建变量就只能在这个结构体后面创建
{
	int age;
	char name[30];
	char sex[5];
}xiaowang = {18,"小王","男"};//就只能在这里创建,如果想在后面的函数里用这个类型,由于没有名字我们是用不了的。

这个东西不能再次使用就是因为没有名字,后面我们再次使用的话就没有办法去使用。

2.结构体自引用

提到结构体自引用就不得不要提到关于链表的内容了。关于链表,它是数据结构里的内容。而数据结构其实是数据在内存中的存储和组织的结构。链表就是其中之一。简单的说假如我要在内存中存储1,2,3,4,5.我们想到的有什么存储方式呢?

简单介绍一下关于链表的知识。像是图上的每一个“方块”,我们叫做节点。每一个节点都包含着数据域和指针域。数据域就是在这里的数字,指针域里面是指针,指向的是下一个节点的地址。

那么我们知道了上一个节点,就可以知道下一个节点地址。这就需要我们的结构体自引用了。

struct Node
{
	int date;//数据
	struct Node* next;//struct Node类型的指针,指向的就是下一个节点的地址
};

在结构体里包含了和自己类型一样的指针类型。这就实现了结构体的自引用,自己引用与自己同类型的对象。

二.结构体内存对齐

这个其实就是来探讨结构体大小的。

1.对齐规则

首先要了解结构体的对齐规则:

1.结构体的第一个成员对齐和结构体变量起始位置偏移量为0的地址处

2.其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处

   对齐数=编译器默认的一个对齐数与该成员变量大小的较小值。

-VS中默认的值为0。

-Linux中gcc没有默认对齐数,对齐数就是成员本身的大小。

3.结构体总大小为最大对齐数(结构体中的每个成员变量都有一个对齐数,所有对齐数中最大的)的整数倍

4.如果嵌套了结构体的情况,嵌套的结构体的成员对齐到自己的成员中最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍。

用代码来更深刻的了解对齐数的规则。

#include<stdio.h>
struct s
{
	char c1;
	int i;
	char c2;
};
int main()
{
	printf("%d", sizeof(struct s));
	return 0;
}

我们知道,c1是char类型,字节为1。i为int类型,字节为4。c2也是char类型,也是一个字节。注意这个可不是直接把他们相加,struct s的大小可不是6。

上面的代码运行出来的结果是12

为什么是12呢?这就是因为对齐数规则。来看这个图。

我们要先知道怎么要判断对齐数:对齐数=编译器默认的一个对齐数与该成员变量大小的较小值。

注意:这里我用的vs默认对齐数是8。而且Linux中gcc没有默认对齐数

struct s
{           //该成员大小    默认对齐数    对齐数
	char c1;//   1             8          1
	int i;  //   4             8          4 
	char c2;//   1             8          1
};

跟着我们步骤走一遍。

(1)根据对齐规则的第一条:第一个成员对齐和结构体变量起始位置偏移量为0的地址处。所以c1直接从0开始(就是黄色所占的地方)

(2)然后根据第二条规则:其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处,因为需要找4的倍数,所以我们就往下找,一直到偏移量为4的时候,找到了4的倍数。所以我们的i就从这里占领空间(深蓝色的部分)。

(3)然后还有一个char类型的变量,我们就继续找1的倍数,直接就是8就可以,c2在这里就开始占领空间(红色的部分)。

(4)到这里已经完成了大部分内容,但还没有结束。再根据第三条:结构体总大小为最大对齐数的整数倍。这里的最大对齐数就是上面我三个变量当中的最大对齐数,是4.所以我们就要找4的倍数。到红色部分的时候总共的数量才9,我们就继续往下找。直到到达11这个位置的时候。总共的数量才到12。所以这个结构体的大小就是12。

这几个步骤我们就成功的找到了结构体的大小,注意:我在上面画×的部分是浪费的内存。

还有一个重要的地方,就是结构体里嵌套结构体。这个要怎么样计算它的大小呢?这时我们就需要了解对齐规则的第四点。

struct s1
{
	char c1;//1   8   1
	char c2;//1   8   1
	int i;  //4   8   4
};
struct s2
{
	char c1;     //1   8   1
	struct s1 m; 
	char c2;     //1   8   1
};
#include <stdio.h>
int main()
{
	printf("%d", sizeof(struct s2));
	return 0;
}

这里我就创建了两个结构体,在s2里嵌套了一个s1。其实这里我们就把struct s1当成一个普通的变量对待就行了。m的大小就是8个字节,vs默认对齐数是8,所以对齐数就是8。然后在根据我前面所说的前三个步骤。

注意一下第四条规则说的:

(1)前半句:嵌套的结构体的成员对齐到自己的成员中最大对齐数的整数倍处。这句话的意思代入到咱们的这个代码,就是找一下s1里成员的最大对齐数。这里就是4。那么我们就找4的倍数。找到了4.就从这里开始往后占领。一直占领struct s1的大小:8个字节。

(2)然后是后半句:结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍。也就是说,我们不仅要找s2里的最大对齐数,还要找s1里的最大对齐数,结合起来找最大的对齐数,就是4。然后找4的倍数就行了。最后的结果是16。

 这就是我们的对齐规则。

2.为什么存在内存对齐

大部分参考资料是这么说的。

1.平台原因

不是所有的硬件平台都能访问任意地址上的任意数的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则输出硬件异常。

2.性能原因

数据结构应该尽可能的在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存仅仅需要一次访问。假设一个处理器总是从内存中取8个字节,则地址必须是8个倍数。如果我们能保证将所有的double类型的数据的地址都对齐成8的倍数,那么就可以用一个内存操作来读或写值了。否则我们可能需要进行两次内存访问,因为对象可能被分在两个8个字节内存块中。

下面我画图来看,为什么要对齐

struct s
{
	char c;
	int i;
};

总的来说:结构体的内存对齐是拿空间来换取时间的做法。

虽然我们是拿空间换时间,但是我们也要尽量的节省空间,我在上面写的时候,其实大家也能发现这两个代码:

struct s
{           
	char c1;
	int i;  
	char c2;
};
struct s1
{
	char c1;
	char c2;
	int i;  
};

这两个结构体的成员虽然都一样,但是它们所占的字节大小确差了不少。所以我们可以总结一下这个

让占用空间小的成员尽量集中到一起

3.修改默认对齐数

这里我们就需要知道一个指令,#pragma,这是一个预处理指令,可以改变编译器默认对齐数。我来使用一下这个指令

#include<stdio.h>
#pragma pack(1)//把默认对齐数修改为1
struct s
{           
	char c1;//1   1   1
	int i;  //4   1   1
	char c2;//1   1   1
};
#pragma pack()//取消设置的对齐数,使对齐数恢复默认
int main()
{
	printf("%d", sizeof(struct s));
	return 0;
}

最终打印出来的结果是6

三.结构体传参

对于同一种功能的实现我用两种方法来写:

#include<stdio.h>
struct S
{
	int arr[100];
	int n;
	double d;
};
void print1(struct S tmp)
{
	int i = 0;
	for (i = 0; i < 5; i++)
	{
		printf("%d ", tmp.arr[i]);
	}
	printf("%d ", tmp.n);
	printf("%lf\n", tmp.d);
}
void print2(struct S* ps)
{
	int i = 0;
	for (i = 0; i < 5; i++)
	{
		printf("%d ", ps->arr[i]);
	}
	printf("%d ", ps->n );
	printf("%lf\n", ps->d );
}
int main()
{
	struct S s = { {1,2,3,4,5},10,1.22 };
	print2(&s);
	return 0;
}

这里我有两个函数,一个是print1一个是print2。一个是值传递的方式,一种是地址传递的方式。我们知道,在给函数传递参数的时候,这个函数会再另外开辟一个空间来存放传递过来的值。那么我们可以思考一下,我们是使用print1好还是print2好。答案当然是print2。因为指针不是4个字节就是8个字节,当我们传递参数的时候,只需要传递地址开辟的空间小。如果传递的是结构体,结构体过大,参数压栈的系统开销比较大,所以会导致性能的下降。

四.结构体实现位段

1.什么是位段

位段,位段。这个位其实指的就是二进制位,关于位段先介绍几点需要注意的地方。

1.位段的成员必须是int,unsigned int或signed int,在C99中位段成员的类型也可以选择其他类型。

2.位段的成员名后面有一个冒号和一个数字(这个数字指的就是有多少个二进制位,也就是多少bit位)。

struct S
{
	int a : 2;
	int b : 5;
	int c : 10;
	int d : 30;
};

我们知道一个整型是有四个字节的,32个比特位。当我们在结构体成员后面加上冒号和一个数字的时候,就相当于是改变了这个整型所占的bit位。比如我们想创建一个成员a=3;把3转化为2进制后是11,这里我们就只需要两个二进制位来表示就可以了。所以我们在这里加一个冒号和2,就只占了2个bit位,就足够来表达出这个3了。

所以,位段是专门来节省内存的。

2.位段的内存分配

1.位段的成员必须是int,unsigned int,signed int或者是char等类型。

2.位段的空间上是按照以4个字节或者1个字节来开辟的。(也就是假如我是int类型,我就一次开辟4个字节,如果是char就开辟1个字节,如果不够了再来开辟)。

3.位段涉及许多的不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。

来看一下位段到底是怎么分配空间的:

struct S
{
	char a : 3;
	char b : 4;
	char c : 5;
	char d : 4;
};

如图所示:

也就是说一次会开辟一个字节的大小,然后根据位段后面的数字来判断出a,b,c,d各自占多少个bit位。比如这里a是三个bit位先占3个bit位,然后b是有4个bit位,紧接着占取空间。然后这八个字节就只剩下了一个bit位,不够下一个c来占用了。所以我就再次开辟一个字节来继续让c占取。后面的d也是同样的原理。

注意:1.申请到同一块内存中,从左向右使用,还是从右向左使用的,不确定。这里我用的vs是从右向左使用的。

2.剩余的空间,不足以下一个成员使用的时候,是直接浪费掉了。

这就是位段的内存分配的知识了。

还有一个代码分享一下:

#include<stdio.h>
struct S
{
	char a : 3;
	char b : 4;
	char c : 5;
	char d : 4;
};
int main()
{
	struct S s = { 0 };
	s.a = 10;
	s.b = 12;
	s.c = 3;
	s.d = 4;
	printf("%zd", sizeof(s));
	return 0;
}

根据我刚才说的那些,大家应该也可以很轻松的就算出来这个值是多少了。但是我想说的点不再这里。这里我给每一个结构体成员都赋予了值,那么我给a赋予的值是10,它的二进制是1010,它足足有4位,但是我只给了a3个bit位。这里也很简单,直接取后面的三位010放进去就行了。也就是上面的黄色区域,蓝色区域放的是12,所以就是1100,刚好4位,直接放进去就好了。根据这个方式把我开辟的所有空间都放满的值就是

 0110 0010 0000 0011 0000 0100 

用16进制存放的话就是     6        2       0       3      0       4

在内存中就是这样:

3.位段的跨平台问题

虽然位段的好处很多,但是关于位段也有很大的弊端

1.int位段被当成有符号数还是无符号数不确定

2.位段中最大位的数目不能确定(比如16位机器最大是16,32位机器最大是32,我写一个17,在16位机器会出现问题)

3.位段中的成员在内存中从左向右还是从右向左分配,标准没有定义

4.当一个结构包含两个位段,第二个位段成员比较大,无法容纳第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的

总结:跟结构相比,位段可以达到同样的效果,并且可以节省空间,但是有跨平台的问题存在。

4.位段的使用事项

位段的结构成员共同使用一个字节,这样有些成员的起始位置不是某个字节的起始位置,那么这些位置也是没有地址的。因为在内存中每个字节分配一个地址,一个字节内部的bit位是没有地址的。

因为没有地址,所以我们也就不能对这些位段的成员使用&操作符,只能是先输入放在一个变量中,然后赋值给位段的成员。

只有在结构体才能使用位段。

整个结构体在这里就基本上写完了,感谢大家的观看,如果有错误,还请大家多多指正。

  • 36
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值