目录
1.结构体
1.1 结构的基础知识
结构是涵盖不同数据类型的值的集合,这些值称为成员变量。结构的每个成员可以是任意的原子类型(int、char、float)类型。
1.2 结构的声明
struct tag
{
member-list;
}variable-list;
描述一个学生的结构类型:
struct Student{
char name[20]; // 名字
int age; // 年龄
char sex[5]; // 性别
char id[20]; // 学号
};
1.3 特殊的结构声明
// 匿名的结构体类型
struct
{
int a;
int b;
int c;
}x; // 这里的x是用这个匿名的结构体创建的对象
struct
{
int a;
int b;
int c;
}a[20],*p;
这两个结构省略了tag的标签,这两个结构的成员都是相同的,那么他们是不是同一个结构体类型呢?
我们做以下测试:
p = &x;
这时候编译器会警告,因为编译器把这两个结构体当成不同类型,建议不要创建匿名的结构体类型。
1.4 结构体包含自己
struct Node
{
int data;
struct Node next;
};
上面这段代码是不允许这样,因为自己包含了自己,自己里面又包含了自己,不知道什么时候才是个头,所以这样的定义会到一直嵌套下去。但是编译器运行我们对自己类型的指针进行调用,正确代码如下:
struct Node
{
int data;
struct Node* next;
};
这里的next是一个指针,在32位机器中占4个字节大小,指针是用来存放地址的,那么其实这里是创建了一个存放struct Node类型的指针,可以存放对应的地址,这样就不会导致一直嵌套下去。如果这段代码可行,那下面这个是否可以呢?
1 typedef struct Node
2 {
3 int data;
4 Node* next;
5 }Node;
typedef 是对类型重命名,将struct Node 重新给了一个新的类型名字Node,但是这段代码仍有问题,我们发现在重命名的时候,是要到第五行才找到这个新的名字,而我们在第四行就要使用,这时就会报错。正确方法如下:
1 typedef struct Node
2 {
3 int data;
4 struct Node* next;
5 }Node;
1.5 结构体变量的定义和初始化
struct Point
{
int x;
int y;
}p1; // 定义一个变量p1,且这是一个全局变量
struct Point p2; // 定义结构体变量p2,且这是一个全局变量
struct Point p3 = {x,y}; // 初始化
int main()
{
struct Point p4 = {1,2}; // 定义一个结构体变量p4,这是一个局部变量,初始化为x=1,y=2
// 也可以通过访问成员变量进行修改成员函数
p4.x = 10;
p4.y = 20;
struct Point *p5 = &p4;
p5->x = 10; // 对指针类型的结构体变量用->
p5->y = 20;
}
1.6 结构体的内存对齐
我们在了解了结构体的基本使用后,我们来考虑一个结构体的大小是多少呢,先抛开内存对齐来看下面的代码,结果会是多少?
struct s1
{
char c1;
int c2;
char c3;
};
printf("%d",sizeof(struct s1));
可能你觉得是6,其实是12,为什么会比原来的大小多出了一倍,这是因为编译器在对结构体类型使用了内存对齐,那么如何进行内存对齐?
1. 第一个成员在于结构体变量偏移量为0的地址处。
2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数 = 编译器默认的一个对齐数与该成员大小的较小值
vs默认为8
3. 结构体总大小为最大对齐数(成员变量对齐数的最大值)的整数倍。
4. 如果结构体嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍,结构体的整体大小就是所有最大对齐数的整数倍。
我们再来分析以下代码:
struct s1
{
char c1;
int c2;
char c3;
};
printf("%d",sizeof(struct s1));
根据规则1,我们的变量是随机在内存的某个地址开辟空间的,我们把要开辟空间的其实位置作为偏移量为0的地方。
到了第二步,按照规则二,将当前要放入到内存的变量与编译器的对齐数进行比较,找到较小值。
第三步,根据规则二,将c3放入到内存中
按照规则三,我们需要根据结构体的最大对齐数的整数倍进行去补给这个结构体一些无用的空间大小。灰色区域的内存都是没有占用任何变量,是被浪费掉的内存空间。
那么为什么会存在内存对齐呢?
- 平台原因(移植问题)
- 性能原因
1. 不是所有硬件平台都能访问任意地址上的数据的,某些硬件平台只能在某些地址处取得某些特定类型的数据。
2.如果不进行内存对齐,为了访问未对齐的内存,处理器需要作两次内存访问,而对齐的内存访问仅需要一次访问。比如说我们一次性拿去4个字节的大小,如果是不对齐的,我们想要拿c2这个变量需要拿去两次。
总的来说:内存对齐是用空间换取时间
我们在设计结构体的时候,尽可能根据内存对齐设计一个内存较小的结构体
struct s1
{
char c1;
int c2;
char c3;
};
// 设计成这种,只占8个字节
struct s2
{
char c1;
char c2;
int c3;
};
我们前面讲了vs默认对齐数是8,其实这个对齐数是可以进行修改的,代码如下:
#pragma pack(4) // 修改默认对齐数4
#pragma pack() // 取消设置的默认对齐数
#pragma pack(1) // 对齐数为1时,不进行内存对齐
1.8 结构体传参
struct S
{
int data[1000];
int num;
};
struct S s = {{1,2,3,4},1000};
// 结构体传参
void print1(struct S s){
printf("%d",s.num);
}
// 结构体地址传参
void print2(struct S* ps){
printf("%d",ps->num);
}
我们在进行传参的时候优先使用print2函数,因为函数传参的事实,参数要进行压栈,会有时间和空间上的开销,如果结构体过大,那么这个开销会导致性能下降。所以通常传参,传结构体的地址。
2.位段
2.1 什么是位段
位段和结构的类型时类似的,位段是会给成员变量进行分配内存,位段需要满足下面要求
1. 位段的成员必须是int 、unsigned int 或者 signed int
2. 位段的成员变量是 类型 变量名:比特大小
比如:
struct A
{
int _a:2;
int _b:5;
int _c:10;
int _d:30;
};
那么sizeof(struct A)的大小是多少呢?
首先,我们赋给成员变量的值是bit大小,1字节=8个bit,所以结构类型的成员变量,我们赋给了总共47个bit,约等于6个字节,但是由于位段是按照4个字节的方式进行开辟的。所以这个结构类型为8.
2.2 位段的内存分配
1. 位段的成员可以是int类型(包括有符号和无符号)也可以是char类型(都属于整型家族)
2. 位段的空间时按照4个字节(int)或者一个字节(char)的方式来开辟的。
3.位段涉及很多不确定因素,所以不跨平台,注意可移植的程序应避免使用位段
那么,这段代码的值为多少?
printf("%d",sizeof(struct A)); // 结果为?
首先,我们赋给成员变量的值是bit大小,1字节=8个bit,所以结构类型的成员变量,我们赋给了总共47个bit,约等于6个字节,但是由于位段是按照4个字节的方式进行开辟的。所以这个结构类型为8.
3. 枚举
枚举的意思就是把可能的取值一一列举
在现实生活中,一周有七天,可以对星期一到星期天进行一一列举。还有性别,月份等等。
3.1 枚举类型的定义
enum Day // 星期
{
Mon,
Tues,
Wed,
Thur,
Fri,
Sat,
Sun
};
这些取值都是有一个明确的值,默认第一个是0,依次递增+1,当然,也可以对其赋初值
enum Color
{
RED = 1,
GREEN = 2,
BLUE = 3
}
3.2 枚举的好处
我们经常使用#define 定义常量,那define与enum枚举有什么区别呢?
- 枚举增加了代码的可读性和可维护性
- 枚举有类型检查,更加严谨
- 防止了命名污染
- 便于调试
- 使用方便,相当于用了很多个define
3.3 枚举的使用
enum Color
{
RED = 1,
GREEN = 2,
BLUE = 3
};
enum Color clr = GREEN;
clr = 5; // 相当于直接把的值改成5,不会报错,但建议不要这样做
4. 联合(共用体)
联合也是一种特殊的自定义类型,这种类型的成员变量公用同一块空间
4.1 联合的声明和定义
声明如下:
// 声明
union Un
{
char c;
int i;
};
union Un un; // 定义
printf("%d",sizeof(un));
un的大小为4,因为这些成员变量所占的空间是同一块,那么只需要成员中占最大自己的成员变量放得下就能够保证这种实现,所以共用体类型大小与最大的成员变量大小一致。
4.2 联合的特点
那么,我们看下面这个例子:
union
{
int i;
char a[2];
}*p,u;
p = &u;
p->a[0] = 0x39;
p->a[1] = 0x38;
p.i 的值为多少呢?
首先,在知道p->i的值为多少之前,必须了解大小端存储模式
大端模式:数据的高字节存储在低地址中,而数据的低字节存储在高地址中。
小端模式:数据的低字节存储在低地址中,而数据的高字节存储在高地址中。
那么,vs编译器默认使用小端存储,我们再看会上述代码。
p 的地址
当调试到第二步,执行完后,我们会发现这个共用体a[0]成员改为了0x39,那么共用体是存放在同一块内存的,使用每个成员变量都是放在地址偏移量为0的位置上,这里因为是小端存储,使用改为39 00 00 00
第三步之后,我们会发现p的对应值改为39 38 00 00
这时候p->i,则是拿去前四个字节,也就是39 38 00 00,转为二进制就是i的值,为14393.
拓展:如何判断当前计算机的大小端存储(面试题)
int checkSystem()
{
union check
{
int i;
char c;
}c;
c.i = 1;
return (c.c == 1);
}
如果是小端存储,c是存放在这个i这个变量的低地址处,如果是大端存储,则存放在高地址处。