目录
前言:
一:结构体
1.1:什么是结构体?
1.2:结构体类型的声明
1.3:结构体变量的定义
1.4:结构体的内存对齐
1.5:结构体传参
二:位段
2.1:位段是什么?
2.2:位段的内存分配
2.3:位段在vs编译器上内存的分配和使用
前言:
今天分享的内容是C语言中自定义类型之一的结构体。在C语言中我们知道有很多种数据类型,如 int ,char,float 等,但是我们处于社会中,那么社会中的东西能用数据来表示吗?比如,一本书能用 int 或者 char 类型所表示吗?答案是不能的。因为一本书既包含有书名,还包含有作者名,单价和出版社等信息,那么此时单纯的 int ,char 等数据类型就行不通了,这时就得根据自己的需要来自定义一种结构体来描述这本书,其中以书名,单价,出版社等表示结构体的成员列表。可见自定义结构体给我们很大的遐想空间,表述万事万物。接下来我们一起看看结构体有哪些独特的魅力。
一:结构体
1.1:什么是结构体?
结构就是一些值的集合,这些被称为成员变量。并且结构的每个成员可以是不同类型的变量。
1.2:结构体类型的声明
声明:
struct tag // struct(关键字),tag是自定义事物的名称
{
mem_list; // 成员列表,可以一个/多个
}variable_list; // 变量列表
1.2.1:普通声明
例如描述一个学生:
//结构体定义一个学生
struct Student
{
char name[20]; // 学生姓名
char sid[20]; // 学生学号
char sex[5]; // 学生性别
int age; // 学生年龄
}stu1,stu2; //注意有分号,此时创建了两个struct Student类型的变量stu1和stu2.
// stu1 和 stu2 是全局变量
int main()
{
struct Student stu3,stu4; //创建了两个struct Student类型的全局变量。
reutrn 0;
}
此处的 stu1和stu2是全局变量,是在声明结构体的时候顺带创建的,当然也可以不顺带创建。
//结构体定义一个学生
struct Student
{
char name[20]; // 学生姓名
char sid[20]; // 学生学号
char sex[5]; // 学生性别
int age; // 学生年龄
};
1.2.2:特殊声明
在声明结构体的时候,可以不完全的声明,被称为匿名结构体类型。
例如:
// 匿名结构体
struct
{
char name[20];
int age;
}x; //注意:在创建匿名结构体变量的时候,只能在这里创建(x)
int main()
{
return 0;
}
但是这种结构在声明的时候已经省略了结构体标签(tag),这种结构体变量只能在定义的时候创建,并且这种结构体类型只能在声明的时候用一次(只能声明一次)。
为什么只能用一次?
// 结构体1
struct
{
int a;
char b;
double c;
}x;
// 结构体2
struct
{
int a;
char b;
double c;
}*p;
int main()
{
p = &x;
return 0;
}
发现运行报错:
发现 结构体指针变量p与&结构体x的类型不匹配。
我们知道:
int a = 10;
int* p = &a;
所以 结构体1和结构体2的类型是不同的(即类型不匹配),故此这种匿名结构体在程序中只能使用一次。
1.2.3:结构体的自应用
在结构中包含一个类型为该结构本身的成员是否可以呢?就如同数据结构之中的链表一样:
struct Node
{
int data;
struct Node next;
};
不过这样的结构体形式是否正确呢?答案是错误,这是因为在这一结构体中包含有一个结构体,这样的结构体在实现的时候会一直进行下去,永远没有尽头,也就是说它只有进没有出结构体的条件。那该怎样实现结构体的自引用呢?
正确形式:
struct Node
{
int data; // 数据域
struct Node* next; // 指针域,声明一个同类型的指针
};
就是说:将一个结构体通过其内的结构体指针与另一个同类型的结构体相连接起来,之后继续连,连,连。即将多个同类型的结构体通过内部的结构体指针(next)相连接起来就是结构体的自引用。
1.2.4:结构体的重命名
重命名的关键字:typedef
我们发现结构体的类型写起来实在是太长了:
struct Student;
struct Node;
...
将它们重新命一下名,方便写些。
typedef struct Student
{
char name[20];
int age;
}Stu; //此处的 Stu 不是全局变量了,而是此结构体被重命名之后的新名字
int main()
{
struct Student stu1;
Stu stu2;
return 0;
}
其中,变量 stu1 和变量 stu2 的类型是相同的。
1.3:结构体变量的定义与初始化
1.3.1:声明类型的同时定义变量
struct point
{
int x;
int y;
int z;
}p1; // 在声明类型的同时定义结构体变量
1.3.2:先声明类型,再定义变量
// 先定义一个结构体
struct point
{
int x;
int y;
int z;
};
// 再定义一个结构体变量(全局变量)
struct point p2;
int main()
{
struct point p3; // 局部变量
return 0;
}
1.3.3:结构体变量的初始化
struct point
{
int x;
int y;
int z;
}p1 = {1,2,3}; //对p1初始化
struct point p2 = {2,3,4}; //对p2初始化
int main()
{
struct point p3 = {3,4,5}; //对p3初始化
return 0;
}
1.3.4:结构体嵌套结构体的定义与初始化
struct point
{
int x;
int y;
int z;
};
struct stu
{
char name[20];
int age;
struct point a; //结构体嵌套一个struct point类型的结构体
};
int main()
{
struct stu s1 = { "张三",13,{1,2,3} }; // 赋值初始化
struct stu s2 = { "李四",20,{2,3,4} };
printf("%-20s\t%d\t%d\t%d\t%d\n", s1.name, s1.age, s1.a.x, s1.a.y, s1.a.z);
printf("%-20s\t%d\t%d\t%d\t%d\n", s2.name, s2.age, s2.a.x, s2.a.y, s2.a.z);
return 0;
}
代码运行结果:
1.4:(*)结构体的内存对齐(*)
当我们掌握了关于结构体的使用情况后,接下来我们来深思考虑一下结构体的大小有多大呢?
1.4.1:分析结构体内存大小(内存对齐)
先看一下下面两种结构体代码:
struct Stu1
{
int a;
char b;
char c;
};
struct Stu2
{
char b;
int a;
char c;
};
int main()
{
printf("sizeof(struct Stu1) = %d\n", sizeof(struct Stu1));
printf("sizeof(struct Stu2) = %d\n", sizeof(struct Stu2));
return 0;
}
//运行结果:
sizeof(struct Stu1) = 8
sizeof(struct Stu2) = 12
结果显示 struct Stu1类型的结构体大小是8个字节,而显示 struct Stu2类型的结构体大小是8个字节。为什么它们结构体内部的类型变量都相同,只是排序不同它们的大小就不同呢?这就涉及到结构体内部的内存对齐规则了。
结构体的对齐规则:
- 第一个成员在与结构体偏移量为0的地址处。
- 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
- 对齐数=编译器默认的一个对齐数与该成员大小的较小值。
- VS编译器上的对齐数是8,gcc编译器和Linux编译器上没有默认对齐数,这两种编译器上对齐数就是其成员自身的大小。
- 结构体大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
- 若有嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
该如何计算结构体的大小呢?
结构体struct Stu1 内的成员变量一共占用了6个字节,因为结构体的内存对齐规则,需要2个最大对齐数才能将其存下。所以, 结构体struct Stu1的总大小为8个字节。
同理:结构体 struct Stu2 需要3个最大对齐数才能将其存下。所以, 结构体 struct Stu2 的总大小为12个字节。
1.4.2:为什么存在内存对齐
1. 平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的,某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2. 性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。 原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
结构体的内存对齐是拿空间来换取时间的做法。
设计一个优秀的结构体
让占用空间小的成员尽量集中在一起 。
1.4.3:修改默认对齐数
#pragma // 预处理指令--用来改变默认对齐数
#include<stdio.h>
#pragma pack(8) //设置默认对齐数为8
struct s1
{
char c1;
int i;
char c2;
};
#pragma pack() //取消设置的默认对齐数,还原为默认
#pragma pack(1) //设置默认对齐数为1
struct s2
{
char c1;
int i;
char c2;
};
#pragma pack() //取消设置的默认对齐数,还原为默认
int main()
{
printf("%d\n", sizeof(struct s1));
printf("%d\n", sizeof(struct s2));
return 0;
}
// 运行结果:
12
6
1.5:结构体传参
#include<stdio.h>
struct Stu
{
char name[20];
int age;
};
void Print1(struct Stu s) // 传整个结构体
{
printf("%s\n", s.name);
}
void Print2(struct Stu* ps) // 传结构体的指针
{
printf("%s\n", ps->name);
}
void test2()
{
struct Stu s = { "小明",12 };
Print1(s);
Print2(&s);
}
共有两种结构体传参的方法。
但是哪种方法更好一些呢?
首选Print2函数。因为:
1. 函数传参的时候,参数是需要压栈的,会有时间和空间上的系统开销。
2. 如果传递一个结构体对象的时候,结构体过大,参数压栈的系统开销比较大,故会导致性能的下降。
结论:结构体传参的时候,要传结构体的地址。
二:位段
2.1:位段是什么?
首先:位段的声明和结构体是类似的,但有两个不同:
1. 位段的成员类型必须是 int,unsigned int,signed int。
2. 位段的成员名后面有一个冒号和一个数字。
例如:
#include<stdio.h>
struct A
{
int a : 2;
int b : 5;
int c : 10;
int d : 30;
};
//其中 A 就是一个位段类型。
int main()
{
printf("%d\n", sizeof(struct A));
return 0;
}
//运行结果:8
A 就是一个位段类型,但是为什么运行结果是8呢?位段A内部有4个int类型的数据,不应该是4*4=16个字节吗,这就需要了解了解位段的内存分配问题来。
2.2:位段的内存分配
首先我们应知道 ”位段“ 中的 ”位“ 是二进制位,所以,int a:2 表示给 a 分配 2个二进制位(bit位),同理:int b:5 表示给 b 分配 5个二进制位(bit位),int c:10 表示给 c 分配 10个二进制位(bit位),int d:30 表示给 d 分配 30个二进制位(bit位)。
位段的内存分配规则:
1. 位段的成员可以是 int,unsigned int,signed int,或者是 char(属于整形家族)类型
2. 位段的空间是按照需要以 4个字节( int )或者 1个字节(char)的方式来开辟的。
3. 位段涉及很多不确定性因素,位段是不跨平台的,注重可移值的程序应该避免使用位段。
以 struct A 位段分析:
因为位段的空间是按照以4个字节( int )或者 1个字节(char)的方式来开辟的。此时当4个字节不够的时候,需要再申请4个字节来存储,直到存储完毕。
看下列代码:
#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;
return 0;
}
内存分析:
我们可以知道此位段得到大小是3个字节(一次增加一个字节)。
vs编译器中一个字节内部是按照从右往左的顺序,即从二进制低位向高位使用的。
位段的跨平台问题:
1. int 位段被当成有符号数还是无符号数是不确定的。
2. 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机器会出问题。
3. 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
4. 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的。
结论:
跟结构相比,位段可以达到同样的效果,但是可以很好的节省空间,但是有跨平台的问题存在。
总结:
今天,我们从结构体的声明与定义开始,学习了另外一种特殊声明(匿名结构体),又了解了结构体变量的定义与初始化是怎样描述的,接着学习了本章最最重要的结构体的内存分配对其问题,最后介绍了一种特殊结构体(位段),优点是比结构体更节省空间,但是也不要忘了它的局限性。
以上的内容若是对大家起到帮助的话,大家可以动动小手点赞,评论+收藏。大家的支持就是我进步路上的最大动力!