1、结构体
1.1基础知识
结构体的成员可以是不同的类型的变量,从而构成一个集合
1.2结构体声明
struct Node
{
char name[20];
int age;
};
- 在代码中,成员变量不需要初始化
#include <stdio.h>
struct Node
{
char name[20];
int age;
}n1, n2 = {"zhangsan",20};
int main()
{
struct Node n3;
struct Node n4 = {"lisi",18};
printf("%d\n", sizeof(struct Node));
return 0;
}
- 在上述代码,n1和n2是全局变量,n3和n4是局部变量
- 创建变量时可以初始化也可以不初始化
struct Node
{
char name[20];
struct Node next;
};
int main()
{
printf("%d\n", sizeof(struct Node));
return 0;
}
- 在上述代码中,该样嵌套创建是不行的,因为我们无法计算结构体变量的大小
#include <stdio.h>
struct Node
{
char name[20];
struct Node * next;
};
int main()
{
printf("%d\n", sizeof(struct Node));
return 0;
}
- 将结构体变量改成结构体指针变量即可
typedef struct Node
{
char name[20];
Node * next;
}Node;
int main()
{
printf("%d\n", sizeof(Node));
return 0;
}
- typedef:为复杂的定义声明一个简单的别名
- 这段结构体声明也是不行的,因为在创建结构体时,我们还没有将struct Node声明为Node,
- 在VS上运行会报这样的警告: error C2065: “Node”: 未声明的标识符
typedef struct Node
{
char name[20];
struct Node * next;
}Node;
int main()
{
printf("%d\n", sizeof(Node));
return 0;
}
- 正确写法是在Node*next前面加上struct
struct
{
char name[20];
int age;
}Node;
int main()
{
printf("%d\n", sizeof(Node));
return 0;
}
- 该处struct后面省略了标签,即不完全的声明,这种叫做匿名结构体
struct
{
char name[20];
int age;
}Node;
struct
{
char name[20];
int age;
}*p;
p = &Node;//?
- 上述代码是非法的,因为编译器会将两个结构体类型当做不同的结构体类型
那么在上述代码中出现的那么多的sizeof(结构体变量类型)的值是多大呢,在内存中是如何存储的呢?
1.3结构体内存对齐规则
我们先来看下面这段代码
struct S1
{
char c1;
int i;
char c2;
};
struct S2
{
char c1;
char c2;
int i;
};
struct S3
{
double d;
char c;
int i;
};
struct S4
{
char c1;
struct S3 s3;
double d;
};
int main()
{
printf("%d\n", sizeof(struct S1));
printf("%d\n", sizeof(struct S2));
printf("%d\n", sizeof(struct S3));
printf("%d\n", sizeof(struct S4));
return 0;
}
你们认为结果是多少呢?
当然我们可以打印出来看一看,但是我们还需要了解为什么,数据在内存里面究竟是如何存储的
那么如何计算?
首先得掌握结构体的对齐规则:
1.第一个成员在与结构体变量偏移量为0的地址处
首先,什么是结构体变量的偏移量?
结构体变量的偏移量是指结构体中各个成员相对于结构体起始位置的偏移量
我们可以借助一个宏,你可以暂时用函数来理解,因为宏的内部是可以传类型的,函数传参是不允许传类型的
offsetof (type,member)
#include <stdio.h>
#include <stddef.h>
struct S1
{
char c1;
int i;
char c2;
}s = {'A',4,'B'};
int main()
{
printf("%d\n", offsetof(struct S1, c1));
printf("%d\n", offsetof(struct S1, i));
printf("%d\n", offsetof(struct S1, c2));
return 0;
}
- 我们可以看一下打印结果和数据在内存中的位置,的确如第一条所说第一个成员变量的地址是0x0031A000,结构体的地址也是0x0031A000(这里的41是十六进制的,转换成十进制就是65,对应了'A'的ASCII值)
2.其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。 对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。VS中默认的值为8 Linux中没有默认对齐数,对齐数就是成员自身的大小
- 自身的大小可以用sizeof计算
3.结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍
4.如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍
struct S3
{
double d;
char c;
int i;
};
struct S4
{
char c1;
struct S3 s3;
double d;
};
int main()
{
printf("%d\n", sizeof(struct S4));
return 0;
}
上述代码中结构体struct S4的最大对齐数就是double类型的8,所以结构体的大小需要是8的倍数
s1的大小是:
struct S1
{ //自身的大小 默认对齐数的大小 二者取小
char c1;//1 8 1
int i; //4 8 4
char c2;//1 8 1
};
根据结构体内存对齐规则我们可以画出在内存里面的图
struct S2
{ //自身的大小 默认对齐数的大小 二者取小
char c1;//1 8 1
char c2;//1 8 1
int i; //4 8 4
};
struct S3
{
//自身的大小 默认对齐数的大小 二者取小
double d;//8 8 8
char c; //1 8 1
int i; //4 8 4
};
从上图我们知道struct s3的最大对齐数就是其成员的最大对齐数,也就是double d的最大对齐数8
大小是16个字节
struct S4
{ //自身的大小 默认对齐数的大小 二者取小
char c1; // 1 8 1
struct S3 s3;// 8 8 8
double d; // 8 8 8
};
1.4结构体对齐存在原因
使用结构体对齐主要有两个原因
1.兼容性的问题,也就是平台移植问题,在某些硬件设备只能在特定位置取得数据
2.性能原因,设备读取数据时,尽量需要靠近自然边界,就是牺牲空间换取时间的做法
如图所示,系统想获取double类型的这个变量,需要抓取两次,才能得到所需要的数据,如果我们使用结构体内存对齐规则,只需要一次抓取
当然,如果我们又想要快,又尽量不浪费空间,就尽可能把小的变量放到一起,参考S1和S2
1.5修改默认对齐数
#pragma pack(1) //将默认对齐数改为1
struct S4
{ //自身的大小 默认对齐数 二者取小
char c1; // 1 1 1
struct S3 s3;// 8 1 1
double d; // 8 1 1
};
#pragma pack() //恢复默认对齐数
int main()
{
printf("%d\n", sizeof(struct S4));
return 0;
}
2、位段
2.1位段是什么
1.1位段的成员必须是 int、unsigned int 或signed int
1.2位段的成员变量后面跟着一个冒号和数字
struct Node
{
char a : 3;
char b : 1;
char c;
char d : 2;
};
int main()
{
printf("%d\n", sizeof(struct Node));
return 0;
}
上述结果是什么呢?
位段在内存中开辟内存是根据成员变量类型的,比如char类型开辟一个字节,int 开辟4个字节
使用位段应注意,位段具有不确定性,是不跨平台的
位段在计算机网络上有一定的应用
struct Node
{
char a : 3;
char b : 1;
char c;
char d : 2;
};
int main()
{
struct Node n = {0}; //成员变量的值 被切割后的值(取冒号后面的数字)
n.a = 10; //00000000000000000000000000001010 010
n.b = 2; //00000000000000000000000000000010 0
n.c = 5; //00000000000000000000000000000101 00000101
n.d = 'A';//00000000000000000000000001000001 01
printf("%d\n", sizeof(n));
return 0;
}
我们可以画出模拟在内存中的存放
可以看出24是最大对齐数的整数倍,也就是3个字节,将24位二进制转化为16进制存放在内存里面就应该是
我们直接打开内存可以看到
内存里面也确实如此
3.枚举
3.1枚举,就是将可能得取值一一列举
enum Color
{
red,
green,
blue
};
enum Day
{
mon,
tues,
wed,
thur,
fri,
sat,
sun
};
enum Sex
{
male,
female,
secret
};
3.2枚举的优点
1. 增加代码的可读性和可维护性
int main()
{
int num = 0;
switch (num)
{
//定义加法
case 1:
//具体实现
//定义减法
case 2:
//具体实现
//定义乘法
case 3:
//具体实现
//定义除法
case 4:
//具体实现
}
return 0;
}
不难看出,1234分别代表什么不够直观,但是如果我们使用枚举
enum Function
{
Add = 1,
Sub,
Mul,
Div
};
int main()
{
int num = 0;
switch (num)
{
//定义加法
case Add:
//具体实现
//定义减法
case Sub:
//具体实现
//定义乘法
case Mul:
//具体实现
//定义除法
case Div:
//具体实现
}
return 0;
}
代码就会变得非常直观
2. 和#define定义的标识符比较枚举有类型检查,更加严谨。
#define 定义的全局变量是没有类型的,枚举常量是有类型的
3. 便于调试
#define 定义的全局变量在预处理时就已经被处理掉了,在我们debug时,极有可能找不到错误的原因出现在哪里
4. 使用方便,一次可以定义多个常量
4.联合(共用体)
4.1共用体的定义
- 共用体的定义和结构体类似,只不过它的成员变量共用同一块空间
- 大小为最大成员变量的大小
4.2共用体大小的计算
union Un
{
char c;
int i;
};
int main()
{
//联合变量的定义
union Un un;
//计算连个变量的大小
printf("%d\n", sizeof(un));
return 0;
}
所以上述代码结果是4
union Un1
{
char c[5];
int i;
};
union Un2
{
short c[7];
int i;
};
//下面输出的结果是什么?
printf("%d\n", sizeof(union Un1));
printf("%d\n", sizeof(union Un2));
- 结构体的的大小应该是成员变量中最大对齐数的整数倍
- 数组就在计算时当做几个单独的变量就行