我们知道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 1int 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 1char c2;//1 8 1int i; //4 8 4
};
struct s2
{char c1; //1 8 1struct 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 1int i; //4 1 1char 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位是没有地址的。
因为没有地址,所以我们也就不能对这些位段的成员使用&操作符,只能是先输入放在一个变量中,然后赋值给位段的成员。
只有在结构体才能使用位段。
整个结构体在这里就基本上写完了,感谢大家的观看,如果有错误,还请大家多多指正。