我们知道C语言本身就存在着一些语法类型,也就是内置类型(譬如:char、short、int、long、float、double)。但在实际应用中仅仅只有这些类型是不够用的,因为我们无法单一的用内置类型来描述一个复杂的对象,就譬如:一个人、一本书等等。所以C语言就给出了除内置类型之外的一种类型:自定义类型,有了这种类型我们就能够自己来构建类型了。而比较常用的自定义类型有:结构体、联合体、枚举。
结构体是C语言中一种重要的数据类型,该数据类型由一组称为成员的数据所组成,其中每个成员可以是不同类型的。结构体通常用来表示类型不同但是又相关的若干数据。(注意:数组与结构体一样,也是一种值的集合,只不过数组的每个元素都要是相同类型的,而结构体的每个成员可以是不同类型的)
首先,要使用结构体就必须声明(创建) 一个结构体类型,就必须用到结构体关键字:struct。如何声明一个结构体类型,如下所示。其中st1为结构体标签,用来区分不同的结构体类型。struct st1表示结构体类型,声明完结构体后就可以用该类型来创建变量了。member-list;为成员列表,它是结构体所包含的基本的结构类型。variable-list;表示变量列表,这个列表可写可不写,写了就代表你用上面所创建的结构体类型定义了一个该类型的变量,没写则表示你仅仅只创建了一个结构体类型。
struct st1
{
member-list;
}variable-list;
注意:
1.若结构体声明是在mian函数之外的,那么列表后创建的结构体变量是一个全局变量。
2.结构体成员变量也可以是另一个结构体类型,这种用法被称为:嵌套结构体。
3.在结构体声明结束的时候,千万不要忘记{}后面是有一个;的。
声明完结构体类型,就可以用它来定义和初始化结构体变量了。注意:结构体初始化与数组一样需要用{}。举个例子
struct grade
{
double math;
double english;
};
struct student
{
char name[20];//姓名
int age;//年龄
char sex[5];//性别
char id[20];//学号
struct grade;//成绩(这是一个嵌套结构体类型)
};
int main()
{
//定义、初始化结构体类型
struct student ly = {"张三", 23, "男", "2117305789", {98.5, 66.0}};
return 0;
}
也可以通过typedef来重定义结构体的类型名:
typedef struct student
{
char name[20];//姓名
int age;//年龄
char sex[5];//性别
char id[20];//学号
struct grade;//成绩(这是一个嵌套结构体类型)
}stu;
注意:
stu并不是创建的变量,而是对于struct student的重命名。之后我们既可以用stu来创建变量也可以用struct student来创建。
我们在声明结构的时候,可以不完全的声明,也就是不写结构体标签,所以称为匿名。
struct
{
int a;
char b;
double c;
}x;
这种语法结构C语言是支持的,但不建议使用,因为这种声明出来的结构体类型我们只能用其定义一次变量,之后想用都用不了了。
我们知道在数据在内存中有许多存储的方式,就譬如:顺序表、链表等等。其中顺序表是在内存中连续存放的,而链表是打乱着存放的,其并不连续。
我们在读取数据的时候顺序表只要知道起始位置,就可以依次找到各个数据。但链表却不行啊,因为它在内存中并不是连续的,而是乱序存放的。问题来了,那链表该怎么往外取数据呢? 其实也不难,是不是只要1可以找到2,2可以找到3,3可以找到4,4可以找到5,不就可以把存在不同内存位置中的数据全部依次的取出来了嘛,数据就如同被一个链条串起来了一样,故称为:链表结构存储。那该怎么实现链表呢?
想法一: 在结构中包含一个类型为该结构本身的成员
struct Node
{
int data;
struct Node next;
};
其实我们只需要在结构体中包含一个类型为该结构体本身的成员,这样就可以像套娃一样,在一个数据中找到另一个数据,在另一个数据中又找到另另一个数据,以此下去。这样不就可以实现链表结构了。
想法二: 在结构中包含一个指向类型为该结构自身的指针
struct Node
{
int data;
struct Node* next;
};
不难得出这个想法其实是可行的。如下图所示,我们只要就把2节点的地址赋给1节点,把3节点的地址赋给2节点,把4节点的地址赋给3节点,把5节点的地址赋给4节点,最后给5节点赋上一个NULL(表示不指向任何位置)。如此不就可以通过一个节点能够找到下一个节点这种方式逐个找遍所有节点,不就实现链表了嘛。
而且该想法必不会像先前的想法那样,在计算类型所占内存大小时无法被获知。因为,这样设计的结构体类型的第二个成员变量本质上就是一个指针,而指针无非就是4/8个字节。
构体它在内存中到底是如何存放的,结构体多占内存真的是所有成员变量大小的总和吗?会与数组一样随着下标的增长地址由低到高变化吗?大家先来猜一猜,下边两个结构体S1和S2在内存中会占用多少空间:
#include
struct S1
{
char a;
int b;
char c;
};
struct S2
{
char a;
char b;
int c;
};
int main()
{
printf("%d\n", sizeof(struct S1));
printf("%d\n", sizeof(struct S2));
return 0;
}
结构体对齐规则:
1.第一个成员在相对于结构体变量起始位置偏移量为0的地址处
(通俗点来说,就是第一个成员变量的地址与结构体起始位置的地址是相同的)如下所示:
2.其他成员变量要对齐到<对齐数>的整数倍处
(对齐数 = 编译器默认对齐数与该成员变量大小的较小值),vs编译器默认对齐数为8,gcc没有默认对齐数这一说。
3.结构体的总大小为最大对齐数(每个成员的都有一个对齐数)的整数倍
4.如果是嵌套结构体的情况,嵌套结构体的对齐数就是其自身的最大对齐数。(同理数组的对齐数就是其元素的对齐数)
在VS编译器中存在一个宏offsetof(type, member),可以用来计算一个成员在结构体类型所创建的变量中的偏移量是多少。其中type是指想要被计算的结构体类型,member是该结构体中的成员。注意:在使用offsetof()前应该先引用一个头文件
#include
#include
struct S1
{
char a;
int b;
char c;
};
int main()
{
printf("%d\n", offsetof(struct S1, a));
printf("%d\n", offsetof(struct S1, b));
printf("%d\n", offsetof(struct S1, c));
return 0;
}
1、平台原因(移植原因):
不是所有的硬件平台都能够访问任意地址处的任意数据的,某些硬件平台只能在某些地址处取特定类型的数据,否则就会抛出硬件异常。
2、性能原因:
数据结构(尤其是栈)因该尽可能的在自然边界上对齐。原因在于,为了访问未对齐的数据,处理器需要做两次内存访问,而对齐的内存只需要一次访问就够了。
其实VS编译器中的默认对齐数(也就是8)是可以进行编译修改的,通过#pragma预处理指令来进行修改。
#include
#pragma pack(1)//设置默认对齐数为1
struct S1
{
char a;
int b;
char c;
};
int main()
{
printf("%d\n", sizeof(struct S1));
return 0;
}
#pragma pack()
结构体与普通内置类型一样也有两种传参方式:1. 传值,2. 传址。那我们到底该选择哪一种传参方式更好呢?其实吧,选择传递结构体变量的地址更好,为什么呢?如果是传值,函数在传参的时候参数是需要压栈的,若此时结构体过大,参数压栈时系统的开销也就较大,会导致性能的下降。可传址就不同了,不管你结构体有多大我传递的地址永远是4/8个字节,内存远比传值小的多。而且传值和传址都能够实现对结构体变量的调用,故首选传址调用。下面是两个不同的传参方法:
struct S
{
int data[1000];
int num;
};
//结构体传参
void print1(struct S s)
{
printf("%d\n", s.num);
}
//结构体地址传参
void print2(struct S* ps)
{
printf("%d\n", ps->num);
}
int main()
{
struct S s = {{1,2,3,4}, 1000};
print1(s); //传结构体
print2(&s); //传地址
return 0;
}
结果
注意:如果是结构体变量地址,我们需要用->来访问结构体的成员;而如果是结构体变量名,我们需要用.来访问结构体成员。
注意:相同类型的结构体变量,我们可以直接通过=来进行赋值操作,不需要像数组那样访问到每一个元素然后才能进行赋值,因为结构体是一个类型啊。
位段是通过结构体来实现的一种以位(bit位)为单位的数据存储结构,它可以把数据以位的形式紧凑的储存,并允许程序员对此结构的位进行操作。由此可以看出,位段是一种节省空间的用法。位段的声明与结构体类似,但有两点不同:
1.位段的成员必须是整型家族的成员(char、int、unsigned int、signed int……)
2.位段的成员后面有一个冒号和一个数字
struct A
{
int a:2;
int b:5;
int c:10;
int d:30;
};
注意:位段每一个成员冒号后面的数字,代表该成员在内存中占用的二进制位大小。就如这里的成员a占用2个bit位,成员d占用30个bit位。而且要记住冒号后面数字的大小是不能超过前面成员类型大小的。
1.一般情况下位段的成员是同一类型的,不会夹杂不同类型的成员
因为位段本身就是一个非常不稳定的东西,如果成员类型不同的话,就会使得位段变得非常复杂充满了不确定性。
2.位段的空间是按照需要,以一次4个字节(成员为int)或1个字节(成员为char)的方式来开辟的
3.位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序因该尽量避免使用位段
计算下面这个位段会占用多少内存空间
#include
struct A
{
int a : 2;
int b : 5;
int c : 10;
int d : 30;
};
int main()
{
printf("%d\n", sizeof(struct A));
}
结果
对于strucr A位段其首先会开辟4个字节的空间以供使用,a占2bit、b占5bit、c占10bit,还剩15bit的空间,但d却需要30bit。这时4个字节的空间不够用了,那就再开辟4个字节的空间嘛。问题来了:这里的成员d是先用完之前的15个bit,然后再占用后面新开辟4个字节空间的15个bit呢?还是直接在新开辟的32个bit位中直接占用30个bit?这是不确定的,但我们能确定的是这里的struct A占用了8个字节。至于位段对于内存空间是跳着用,还是接着上一个使用,我们可以通过下面这个例子看出:
struct S
{
char a : 3;
char b : 4;
char c : 5;
char d : 4;
};
int main()
{
struct S s = { 0 };
printf("%d\n", sizeof(s));
return 0;
}
假设位段是跳着使用空间的,那这里的struct S会占用3个字节的空间。假设位段是接着上一个空间使用的,那这里struct S只会占用2个字节。所以只要得出位段S到底占几个字节,就可以知道他在内存中是怎么存放的了。
结果
可以看出当开辟出来的空间不够位段使用的时候,那个剩余的空间会被浪费掉,然后新开辟一个空间重新存放数据。至于位段的那些成员变量在内存中到底是怎么存放的,可以通过调试来得出:
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;
return 0;
}
可见,位段的成员在当前所开辟的空间中从高地址向低地址存储,若所剩空间不够下一个成员存放,则会再次开辟一个空间从右向左分配。
注意:位段的成员并不是直接存入开辟的空间中,而是会有一个中转的过程,先按照位段成员变量的存放规则存入一个模板当中,然后再以大小端字节序存入所开辟的空间当中去。
1.int型位段成员会被当成有符号数还是无符号数是不确定的
2.位段中最大位数目是不确定的(在16位机器上int型最大为16,而在32为机器上int型最大为32,如若写成27,那么16位机器就会出问题)
3.位段的成员在内存中到底是从左向右分配,还是从右向左分配尚未定义
4.当一个结构体包含两个位段,第二个位段比较大,无法容纳于第一个位段剩余的位时, 是舍弃剩余的位还是利用,是不确定的。
位段在网络中应用的比较多,如:IP数据包格式
当我们在网络上给其他人发送消息,这个消息会封装成如上所示的一个数据包。如若不这么干,那这条信息跑到网络上能准确找到接收人吗?这是绝对不可能的。可以看出封装的部分的排列恰好都被设计成了int型宽度。大家想象一下,如若不使用位段而是用结构体来进行封装,我们在网络上传输的数据包将会变得巨大,使得网络状态变差。可如若在设计之初就用位段排列好,是不是就避免了未来所会发生的这种情况,可见位段的用法还是比较重要的。