目录
正文开始
1. 结构体类型的声明
1.1 什么是结构体?
C语言为我们提供了基本的数据类型,例如int、char、float等,但我们在实际生活中的对象都是复杂的,不能仅靠一种数据简单的描述。
我们回顾一下数组,数组是一种自定义类型,比如int arr[10],它的类型就为int [10],自定义类型使我们能够更加灵活的解决问题。
而结构体同样是一种自定义类型。而结构体就实现了对一个对象进行多方面描述的功能。
1.2 结构体的声明
struct tag
{
member-list;//成员列表
};
举个栗子,例如要描述一个学生,需要他的名字、年龄、性别、学号,就可以这样定义
struct Student
{
char name[20];//姓名
int age;//年龄
char sex[5];//性别
char id[20];//学号
};//分号不能丢
注:
- struct 代表着这是一个结构体类型
- Student 代表着这个结构体的名字
- 结构体内部的就是成员列表
- 最后分号不能丢
对于结构体的理解:结构体就是一个自定义的类型,也就是说,当我们创建一个结构体后,它就可以类似于int、char这些数据类型一样拿来用。
1.3 结构体的特殊声明
在声明结构体时,可以不完全的声明,例如:
//匿名结构体类型
struct
{
int a;
char b;
float c;
}x;
匿名结构体类型定义时,只能在定义结构体时声明结构体变量,否则是不能声明结构体变量的。
//匿名结构体类型
struct
{
int a;
char b;
float c;
}x;
struct
{
int a;
char b;
float c;
}a[20], *p;
(p == &x)成立吗?答案是不成立,因为这是匿名结构体类型,就算里面的成员变量完全一样,编译器仍会把它们当成完全不同的类型。匿名的结构体类型,如果没有对结构体类型重命名的话,基本上只能使用一次。
1.4 结构体的自引用
在结构体内部包含一个该结构体本身的成员是否可行呢?
比如这样:
struct Node
{
int data;
struct Node next;
}
这样写显然是不行的,因为一个结构体内在包含一个同类型的结构体变量,这样就会形成一个无限的套娃,结构体变量的大小就会变成无穷大,是不合理的。我们可以通过使用结构体指针变量来自引用
例如这样就是合法的:
struct Node
{
int data;
struct Node* next;
//包含了下一个节点的地址
//而不是下一个节点的內容
//这样就避免了无限套娃的情况
}
在结构体自引用使用的过程中,若使用了typedef对匿名结构体类型重命名,也容易产生问题,例如:
typedef struct
{
int data;
Node* next;
}Node;
上述代码是将一个匿名结构体类型重命名为了Node,并且在匿名结构体类型中包含了该结构体类型的变量next,但是这是不合法的,因为在匿名结构体内部提前使用了 Node 类型来创建成员变量。所以,强烈建议定义结构体不要使用匿名结构体
2. 结构体变量的创建和初始化
2.1 结构体变量的创建
既然结构体是一种类型,那么我们就可以使用这种类型来创建变量,创建变量有两种方式:
- 先创建结构体类型,再创建结构体变量
//结构体类型创建
struct Student
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
};
int main()
{
//结构体变量的创建
struct Student a;
//创建了类型为struct Student的变量a
return 0;
}
- 创建结构体类型同时创建结构体变量
//结构体类型创建
struct Student
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
}a;
//结构体变量创建
//这时创建的结构体变量是一次性的
int main()
{
return 0;
}
2.2 结构体变量的初始化
结构体变量的初始化有两种情况,一种是按照结构体成员列表顺序初始化,一种是指定顺序初始化。例如:
struct Student
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
};
int main()
{
//按照结构体成员列表顺序初始化
struct Student a = { "张三", 20, "男", "20232022" };
//指定顺序初始化
struct Student b = {.age = 18, .name = "李四", .sex = "女", .id = "20235600"}
return 0;
}
注:
- 在定义结构体变量时可以将其一起全部初始化,但如果在定义结构体变量的时候没有初始化,那么后面就不能全部一起初始化了,只能单独对其中的成员初始化
struct Student
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
};
int main()
{
struct Student a;
//err 这样是不允许的
//a = { "张三", 20, "男", "20232022" };
//只能单独初始化
a.name = "张三";
a.age = 20;
return 0;
}
- 结构体也可以像数组一样,全部将內容初始化为0,例如:
struct Student
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
};
int main()
{
struct Student a = { 0 };
return 0;
}
3. 结构体成员的访问
3.1 结构体成员的直接访问
结构体变量通过操作符.来访问成员:
结构体变量.成员名
例如:
#include <stdio.h>
struct Text
{
int x;
int y;
};
int main()
{
struct Text p = { 1,2 };
printf("x:%d\ny:%d\n", p.x, p.y);
//p.x访问成员x
//p.y访问成员y
return 0;
}
运行结果:
3.2 结构体成员的间接访问
指针可以指向结构体类型,我们可以通过一个结构体指针变量来间接访问结构体成员
(*结构体指针).成员名
结构体指针->成员名
#include <stdio.h>
struct Text
{
int x;
int y;
};
int main()
{
struct Text p = { 1,2 };
struct Text * pp = &p;
pp->x = 4;
(*pp).y = 5;
printf("x=%d\ny=%d\n", pp->x, pp->y);
return 0;
}
运行结果:
注:当使用 (*结构体指针).成员名时,括号不能省去,因为.的优先级要大于 *
4. 结构体内存对齐
结构体在内存中的存储方式同一般的类型颇有不同,它遵循结构体内存对齐规则
4.1 对齐规则
- 结构体的第一个成员对齐到结构体变量起始位置偏移量为0的地址处
- 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处
- 对齐数 = 编译器默认的一个对齐数 与 该成员变量大小的较小值
- VS 中默认值为8
- Linux 中 gcc 没有默认对齐数,对齐数就是成员自身的大小- 结构体总大小为最大对齐数(结构体中每个成员变量都有一个对齐数,所有对齐数中最大值)的整数倍
- 如果嵌套了结构体,嵌套的结构体成员到自己的成员中最大对齐数的整数倍,结构体的整体大小就是所有最大对齐数(包含嵌套结构体中成员的对齐数)的整数倍
相信大家现在都是一脸懵吧,别急,我来举个例子:
#include <stdio.h>
struct S1
{
char c1;
int i;
char c2;
};
int main()
{
printf("%d\n", sizeof(struct S1));
return 0;
}
我们来探讨一下在VS中运行结果是什么:
-
首先,根据对齐规则第一条,将char c1安置好
-
然后安置第二个结构体成员变量int i,它的大小为4个字节,编译器默认对齐数为8,两者较小值为4。也就是说,它的对齐数为4,需要对齐到4的整数倍的地址处,也就是这样:
-
随后安置第三个结构体成员变量char c2,它的大小为1个字节,编译器默认对齐数为8,两者较小值为1。也就是说,它的对齐数为1,需要对齐到1的整数倍的地址处,也就是这样:
-
最后根据对齐规则第3条,三个成员变量中最大的对齐值为4,所以总大小为四的整数倍,所以整个结构体的内存情况就为:
结构体类型struct S1的大小就为12个字节,我们运行以下代码验证一下:
来几个例子练习一下:
#include <stdio.h>
//练习1
struct S2
{
char c1;
char c2;
int i;
};
//练习2
struct S3
{
double d;
char c;
int i;
};
int main()
{
printf("%d\n", sizeof(struct S2));
printf("%d\n", sizeof(struct S3));
return 0;
}
运行验证一下:
当结构体内部嵌套结构体变量时,嵌套的结构体对齐数为其内部的最大对齐数,整个结构体的对齐数为所有最大对齐数(包含嵌套结构体中成员的对齐数)的整数倍。
例如:
#include <stdio.h>
struct S3
{
double d;
char c;
int i;
};
//练习4-结构体嵌套问题
struct S4
{
char c1;
struct S3 s3;
double d1;
};
int main()
{
printf("%d\n", sizeof(struct S4));
return 0;
}
运行验证一下:
4.2 为什么需要内存对齐
- 平台原因(移植原因):
- 不是所有的硬件平台都能访问任意地址上的任意数据;某些硬件平台只能在某些地址处取某些特定类型的数据,否则会产生硬件异常的错误
- 性能原因:
- 数据结构(尤其是栈)应该尽可能地在自然边界上对齐,这样在访问数据的时候可以减少读取的次数。假设一个机器每次访问八个字节,若没有对齐规则的限制,访问对象可能被分在了两个8字节内存块中,这样就需要读取两次才能完整的读取访问对象;而有了对齐规则,就能够保证访问对象都放在同一块内存块中,减少了访问次数。但弊端也很明显,就是为了对齐,而造成的空间浪费。
总的来说,结构体的内存对齐就是拿空间来换取时间的做法。我们在设计结构体的时候,应该尽可能地将占用空间小的成员集中在一起,这样可以在一定程度上减少空间的使用。
4.3 修改默认对齐数
使用#pragma pack()这个预处理指令,可以修改编译器默认的对齐数
例如:
#pramg pack(1)//设置默认对齐数为1
5. 结构体传参
将结构体变量作为参数传递进函数,同样有传址调用和传值调用:
#include <stdio.h>
struct S
{
int data[100];
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 }, 30};
print1(s);
print2(&s);
}
运行结果:
在我们使用结构体传参时,更推荐使用传址调用,因为函数在传参时,参数需要压栈(即在栈区申请空间来存储参数),会有时间和空间上的系统开销,如果传入的结构体对象过大,那么系统开销就大,就会导致性能的下降。
6. 结构体实现位段
6.1 什么是位段?
在前面我们学习了结构体的对齐规则,实现了用空间换取时间的效果,但如果我们想要节省空间,那该怎么办呢?C语言为我们提供了位段的概念,来实现节省空间的效果。
例如:
struct A
{
int _a:2;
int _b:5;
int _c:10;
int _d:30;
};
其中:
- 位段的成员必须是int、unsigned int、signed int或char类型,在C99中位段成员的类型也可以是其他类型
- 位段的成员名后边有一个冒号和一个数字,数字代表着这个成员所占的比特位
6.2 位段的内存分配
- 位段的成员后面的数字就代表着这个成员在内存中所占的比特位
- 位段的空间以4个字节(int)或者1个字节(char)的方式开辟,会首先开辟一块空间,若不够用则继续开辟,直到将所有成员存储起来
- 位段中有很多C语言没有统一的因素,所以位段是比较依赖环境的,这就意味着位段的移植性是很差的,是不跨平台的
- 位段申请到一块内存中,是从左向右使用,还是从右向左使用,是不确定的
- 一块内存中剩余的空间,不足下一个成员使用的时候,是浪费还是继续使用是不确定的
例如:
struct S
{
char a : 3;
char b : 4;
char c : 5;
char d : 4;
};
int main()
{
struct S s = { 0 };
s.a = 10;//二进制:1010
s.b = 12;//二进制:1100
s.c = 3;//二进制:11
s.d = 4;//二进制:100
return 0;
}
这里我们假设位段申请到一块内存中,是从右向左使用;并且剩余的空间,不足下一个成员使用的时候浪费掉,那么内存分配如下:
-
首先,成员都是char类型,所以每次开辟一个字节的空间,开辟了第一个字节后,存放进成员 a,它共占3bit
-
然后存放成员 b,它共占4bit
-
随后要放成员 c,它共占5bit,但开辟的第一个字节已经不够用了,所以就会开辟第二块内存
-
最后要存放成员 d,它共占4个字节,但开辟的第二个字节已经不够用了,所以就会开辟第三块内存
-
这样,结构体成员的内存就都开辟出来了,共三个字节,接下来是给成员初始化;成员 a 二进制表示为1010、成员 b 二进制表示为1100、成员 c 二进制表示为 11、成员 d 二进制表示为100,按位存储进内存,多余位置为0,超出部分舍去
在对应环境下运行检验一下:
6.3 位段的跨平台问题
跟结构体相比,位段可以达到相同的效果,并且可以很好的节省空间,但是有跨平台的问题存在,原因如下:
- int 位段被当成有符号数还是无符号数没有统一
- 位段中最大位的数目不能确定(16位机器最大16,32位机器最大32,若写成26,在16位机器会出现问题)
- 位段中成员在内存中从左向右分配,还是从右向左分配尚未统一
- 当一个结构包含两个位段,第二个位段成员较大,无法容纳于第一个位段剩余位的时候,是舍弃还是利用剩余的位,尚未统一
6.4 位段使用的注意事项
位段的几个成员可能公用一个字节,由于地址是以字节为单位分配的,一个字节内部的 bit 位是没有地址的,而有些成员的起始位置并不是某个字节的起始位置,所以这些成员是没有地址的。
所以不能对位段成员使用&操作符,也就不能使用 scanf 直接给位段成员输入值,只能先输入一个放在变量里,然后赋值给位段成员
例如:
#include <stdio.h>
struct S
{
char a : 3;
char b : 4;
char c : 5;
char d : 4;
};
int main()
{
struct S s = { 0 };
//scanf("%d", &s.a)//这是错误的
//正确用法
int i = 0;
scanf("%d", &i);
s.a = i;
printf("%d", s.a);
return 0;
}
完