文章目录
1,结构体
结构体类型的声明
结构体的基础知识: 结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量
接下来,让我们通过程序来进一步加深我们对结构体的认识
程序一:
#include<stdio.h>
//声明一个结构体类型
//声明一个学生类型,是 想通过 学生类型 来创建 学生变量(对象)
// 描述学生 : 属性 + 名字 + 性别 + 年龄 + 电话
struct student// struct 结构体 关键字 student 结构体 标签
{
char name[10];
char sex[20];
int age;
char telephone[12];// 这四个变量 就是结构体的成员变量
}s4,s5,s6; // 创建 结构体 全局变量
// 以上所有 就是一个 结构体类型
struct student s3; // 创建 结构体 全局变量
int main()
{
//创建 结构体变量 的方式
struct student s1;
struct student s2; // 因为 这两个变量放在 main 函数里,所以是 局部变量
return 0;
}
特殊的声明
程序一:
#include<stdio.h>
struct // 缺少一个标签(名字) 这种结构体类型 称为 匿名结构体类型
{ // 那么问题来了,没有名字 该 如何 创建 结构体 变量 ?
char name[10];
char sex[20];
int age;
char telephone[12];
}x;// 只有一种方式在结构体末尾分号(;)前面 创建 结构体 全局变量
struct // 缺少一个标签(名字) 这种结构体类型 称为 匿名结构体类型
{
char name[10];
char sex[20];
int age;
char telephone[12];
}* px;// 在 匿名结构体 的 全部变量 px 前面加上 * ,该 匿名结构体类型 变成了 匿名结构体指针类型
// 即 * px 是一个结构体指针
int main()
{
px = &x; // 经过 编译器 编译, 程序报警,该表达式 是 不合法的
//因为 编译器, 会把 它们 当做 2 种 不同类型 来处理
// 所以 两种 不同类型 的数据,是无法进行赋值
return 0;
}
结构体自引用
程序一:
#include<stdio.h>
struct node
{
int data;
struct node* next;// 如果 没有 * 号,也就是数 next 是本身的结构体变量,会导致这个结构体所占内存无限大。
// 这里存地址, 存的是下一个数据的地址:
//结构体自引用 就是 结构体 用指针 找到 与自身同类型的 结构体变量
// 而不是说 结构体自己 包含 结构体自己 的 变量 : struct node next (error)
};
int main()
{
return 0;
}
程序二:
#include<stdio.h>
typedef struct node // 这里的 node 是不能省略的, 要不然 下面 没有这 struct node* next 类型,只能写成 node* next
{ // 但是 node 是 [把 省略了 node 从而 变成 匿名结构体 的 重命名]。 是后有的,
// 也就是说 node 还没有生成, 就在结构体 调用它,
// 这种写法 是错误的
int data;//数据域
struct node* next;// 指针域
}node; // node : typedef 把 结构体 struct node 简化成 node
int main()
{
node n;
return 0;
}
结构体变量的定义和初始化
程序一:
#include<stdio.h>
struct s
{
char c;
int a;
double d;
char arr[20];
};
int main()
{
struct s s = { 'c', 100, 3.14, "hellworld" };
printf("%c %d %lf %s\n", s.c, s.a, s.d, s.arr);
return 0;
}
程序二:
#include<stdio.h>
struct t
{
double weight;
short age;
};
struct s
{
char c;
int a;
double d;
char arr[20];
struct t st;
};
int main()
{
struct s s = { 'c', 100, 3.14, "hellworld", {55.6,30} };
printf("%c %d %lf %s %lf %d\n", s.c, s.a, s.d, s.arr, s.st.weight, s.st.age);
return 0;
}
结构体内存对齐: 用来计算结构体 的 内存大小
结构体的对齐规则:
- 第一个成员 在与 结构体变量 偏移量为 0 的 地址处(第一个 结构体成员 存储的地址,即结构体第一个结构体变量 存储地址,地址为 0)。
- 其他成员 变量 要对齐某个数字(对齐数)的 整数倍 的 地址处
对齐数 = 编译器默认 的 一个对齐数 与 成员大小的 较小值
vs 中 默认的值为 8; gcc 没有默认对齐数(成员的大小,就是对齐数)
比如 结构体里 有一个 成员(变量) 为 整形 int 类型 为 4byte
// 而 vs 中 默认值为 8, 4 < 8, 取 4,
// 那么 该成员的 对齐数 为 4
- 结构体总大小为 最大 对齐数(每个成员 的 变量 都有 一个对齐数)的整数倍。
- 如果嵌套了结构体的情况,嵌套的结构体 对齐 到自己的 最大 对齐数 的整数倍处,
// 结构体 的 整体大小 就是 所有 最大对齐数(含嵌套结构体的对齐数)的整数倍。
程序一:
struct s
{
char c1;// 第一个成员 在与 结构体变量 偏移量为 0 的 地址处 (内存所占 1 字节)
int a; // a 对齐数 是 4 ,因为 其他成员 变量 要对齐 对齐数(4) 的 整数倍 的 地址处(地址4)
// 从 c1(0 地址) 后面开始(从地址 4 开始) 地址 4 处 存放 a,就是说 c1 与 a 之间 隔了 3 个 地址(1,2,3) -> 3 字节
// 此时 a 末尾地址 为 地址8 (因为 a 的存储 需要 4 byte 空间)
char c2;// c2 对齐数 1 ; 其他成员 变量 要对齐 对齐数(1)的 整数倍(倍数为 1) 的 地址处 也就是紧跟 a 后面的 地址8,(char 1 byte)存完之后,末尾地址指向 9;
// 无论是地址几,都是 对齐数 1 的倍数
// 至此,1+3+4+1 为 9 字节
// 结构体 总大小 为9 字节,但此时地址,不是 成员中 最大 对齐数(4) 的整数倍,
// 12 满足
// 所以 结构体的大小 最后 为 12 byte
};
struct s2
{
char c1;// 第一个成员 在与 结构体变量 偏移量为 0 的 地址处 (1字节)
char c2;// 对齐数为 1 -> 其他成员 变量 要对齐 对齐数的 整数倍( 倍数为 1 ) 的 地址处(地址1) ,c2 的存储地址 紧跟在 c1 的后面(2 字节)
int a;// 对齐数 4 -> 其他成员 变量 要对齐 对齐数的 整数倍( 倍数为 4 ) 的 地址处(地址4),(c1 是 0 地址,c2 是1 地址,浪费 2,3地址)
// 即 来到 地址4, 也就是 a 的地址,也就是说 a 与 c2 隔了 2 个地址(浪费了2字节空间),a 的 存储 也要 占 4 字节
// 1 + 1 + 2 + 4 == 8 字节
// 成员中 最大对齐数(4) 的整数倍
// 因为 8 == 2*4 > 6 满足条件
// 所以最后 结构体总大小 为 8
};
int main()
{
struct s s = { 0 };
struct s2 s2 = { 0 };
printf("%d\n", sizeof(s));// 12
printf("%d\n", sizeof(s2));// 8
return 0;
}
struct s1附图 :
12 byte ,满足成员中 最大 对齐数(4) 的整数倍, 即 结构体大小 为 12 byte
struct 附图2:
程序二:
#include<stdio.h>
struct s3
{
double d;// 第一个成员 在 与 结构体 偏移量为 0 的地址处(double 8字节,此时地址 指向地址7)
char c;// 对齐数 1 地址8(9字节)
int i;// 对齐数 4 c 后面的 是 地址9,不满足倍数条件,地址 12 满足(浪费 9,10,11地址,即 3字节空间),即 i 的地址 是 地址12
// i 占 4 字节,
// 8 + 1 + 3 +4 == 16
// 16 满足成员中 最大对齐数(8)的整数倍
};
int main()
{
printf("%d\n", sizeof(struct s3));// 16
}
附图:
程序二:
#include<stdio.h>
struct s3
{
double d;// 对齐数 8 地址 7
char c;// 对齐数 1 地址 8
int i;// 对齐数 4 地址 12 i 存储 需要 4byte ,地址 16
// 结构体大小 16 byte 满足 最大 对齐数(8) 的整数倍
// 故结构体 真正大小 为 16字节
};
struct s4
{
char c1; // 地址0, 对齐数 1
// 嵌套了结构体的情况,嵌套的结构体 对齐 到自己的 最大 对齐数(8) 的整数倍处
struct s3 s3; // 对齐地址 8 然后 结构体 s3 内存大小 为 16 byte
// 存完16byte之后,地址 24
double d; // 对齐数 8 ,地址24 满足 对齐数 整数倍 地址
// 地址 24 到 地址 32
// 32 满足 最大 对齐数(8)的整数倍
// 结构体 变量 s4 内存大小 32 字节
};
int main()
{
printf("%d\n", sizeof(struct s4));// 32
}
附图:
为什么存在内存对齐 : 空间(浪费的) 换取 时间
1. 平台原因(移植原因):不是所有的硬件平台都能访问地址上的任意数据的,
某些硬件平台智能在某些地址处取某些特定类型的数据。否则抛出硬件异常。
2.性能原因:数据结构(尤其是栈)应尽可能地在自然边界sang对齐,原因在于,为了访问 未对齐 的内存,
处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问
那么在 设计结构体的时候,我们既要满足对齐,又要节省空间,如何做到?
struct s
{
char c1;// 第一个成员 在与 结构体变量 偏移量为 0 的 地址处 (内存所占 1 字节)
int a; // a 对齐数 是 4 ,因为 其他成员 变量 要对齐 对齐数(4) 的 整数倍 的 地址处(地址4)
// 从 c1(0 地址) 后面开始(从地址 4 开始) 地址 4 处 存放 a,就是说 c1 与 a 之间 隔了 3 个 地址(1,2,3) -> 3 字节
// 此时 a 末尾地址 为 地址8 (因为 a 的存储 需要 4 byte 空间)
char c2;// c2 对齐数 1 ; 其他成员 变量 要对齐 对齐数(1)的 整数倍(倍数为 1) 的 地址处 也就是紧跟 a 后面的 地址8,(char 1 byte)存完之后,末尾地址指向 9;
// 无论是地址几,都是 对齐数 1 的倍数
// 至此,1+3+4+1 为 9 字节
// 结构体 总大小 为9 字节,但此时地址,不是 成员中 最大 对齐数(4) 的整数倍,
// 12 满足
// 所以 结构体的大小 最后 为 12 byte
};
struct s2
{
char c1;// 第一个成员 在与 结构体变量 偏移量为 0 的 地址处 (1字节)
char c2;// 对齐数为 1 -> 其他成员 变量 要对齐 对齐数的 整数倍( 倍数为 1 ) 的 地址处(地址1) ,c2 的存储地址 紧跟在 c1 的后面(2 字节)
int a;// 对齐数 4 -> 其他成员 变量 要对齐 对齐数的 整数倍( 倍数为 4 ) 的 地址处(地址4),(c1 是 0 地址,c2 是1 地址,浪费 2,3地址)
// 即 来到 地址4, 也就是 a 的地址,也就是说 a 与 c2 隔了 2 个地址(浪费了2字节空间),a 的 存储 也要 占 4 字节
// 1 + 1 + 2 + 4 == 8 字节
// 成员中 最大对齐数(4) 的整数倍
// 因为 8 == 2*4 > 6 满足条件
// 所以最后 结构体总大小 为 8
};
根据 剖析, 发现 结构体成员相同,但是 让 占用空间小的的成员尽量集中在一起 ,节省很多的空间(也就是不至于浪费很多空间)
修改默认对齐数
之前我们见过了 #pragma 这个预处理指令,这里让我们再次使用,可以改变我们的默认对齐数
程序一:
#include<stdio.h>
#pragma pack(4) // 设计 默认对齐数 为 4
struct s
{
char c1;// 1
// 浪费 3 个字节(原本要浪费 7 个 字节,现在只需 3 个字节)
double d; // 8 byte 对齐 为 对齐数 4,【4与8 选择较小的】 的整数倍 的地址
// 存储 d 需要 8 个字节 加上前面浪费 3 个 和 c1 1个 字节
// 1+3+8 == 12 字节
// 这样写法,帮我们 避免 了 4 字节 的 空间浪费
};
#pragma pack() // 取消设置的 默认 对齐数
程序二:
那 我们把 默认对齐数 设置为 1 呢?
#include<stdio.h>
#pragma pack(1) // 设计 默认对齐数 为 1
struct s
{
char c1;// 1
// 一个字节的都不会浪费
double d; // 8 byte 对齐 为(对齐数 1,【1与8 选择较小的】) 的整数倍 的地址
// 存储 d 需要 8 个字节
// 1+8 == 9 字节 (9 是 最大 对齐数(1)的 整数倍)
// 这样写法,帮我们 避免 了 7 字节 的 空间浪费
};
#pragma pack() // 取消设置的 默认 对齐数
// #pragma pack() 一般设置 默认 对齐数 为 2,4,8, 16 (2的次方数)
百度面试题
写一个宏,计算结构体中某变量 相对于首地址的偏移,并给出说明 (考察 offsetof 宏的实现)
#include<stdio.h>
#include<stddef.h>
struct s
{
char c;
int i;
double d;
};
int main()
{
// offsetof 其实是一个宏,用来表示 成员 相对于 结构体 的 偏移量
printf("%d\n", offsetof(struct s, c));// 0
printf("%d\n", offsetof(struct s, i));// 4
printf("%d\n", offsetof(struct s, d));// 8
return 0; //而且 offsetof 的 参数 传的是 一个类型,更加说了 offsetof 是一个宏
}
结构体传参
#include<stdio.h>
struct s
{
int a;
char c;
double d;
};
void init(struct s *tmp)
{
tmp->a = 100;
tmp->c = 'w';
tmp->d = 3.14;
}
void print(struct s tmp)// 传值:如果传递的结构体对象的时候,结构体过大(空间过大),容易导致 参数压栈 的 系统 开销比较大,从而导致系统性能下降。
{
printf("%d %c %lf\n", tmp.a, tmp.c, tmp.d);
}
void print2(const struct s* tmp) // 传址 : 最好使用这种方法(节省空间),一个地址在操作系统不改变的情况,永远都是4个字节大小,
{ // const 是为了防止 意外改变 结构体变量地址 指向的 值
printf("%d %c %lf\n", tmp->a, tmp->c, tmp->d);
}
int main()
{
struct s s = { 0 };
init(&s);
print(s);
print2(&s);
return 0;
}
结构体 实现 位段 ( 位段的填充 & 可移植性 )
位段 的 声明 和 结构 是 类似的,有 两个 不同:
1、 位段 的 成员 必须是 int,unsigned int 或 signed int(还可以是 char )
2、 位段 的 成员 后边 有 一个冒号 和一个数字( 数字 <= 32 [有多少位操作系统决定] )
位段 - 位:二进制位
位段的内存分配:
1、位段的成员可以是 int unsigned int,signed int 或者是char( 属于整形家族 )类型
2、位段的空间上是按照需要 以 4个字节(int)或者1个字节(char)的仿古式来开辟
3、位段涉及很多不确定因素,位段是不跨平台的,注重 可移植 的 程序 应该 避免使用 位段
程序一:
#include<stdio.h>
struct a // 按照以下写法写结构体成员, a 已经不是个结构体类型了,而是 一个 位段 类型了
// 位段 看见成员 都是 int 类型 ,所以,它 一开始 就创建了 4 byte的 空间
{
int a : 2;// 2 这里的意思是: a只需要 2 个比特位(bit)
int b : 5;// 5 这里的意思是: b只需要 5 个比特位(bit)
int c : 10;// 10 这里的意思是: c只需要 10 个比特位(bit)
int d : 30;// 30 这里的意思是: d只需要 30 个比特位(bit)
};// 一共 47 个 bit 位,由已经创建的 4byte 空间来分配空间, 很明显 空间不够 大,只能存入 a,b,c,4 byte 空间 还剩 15 bit
d 放不下,怎么办呢? 在vs 环境中 系统 会舍弃(浪费)剩余的 15 bit 空间
然后, 它再向 空间申请 4 byte(32bit) 空间, 存储 d 需要 30 bt 空间,这 30 bit 的数据 就存入 这个向系统第二次申请 4 byte 的空间里
至此,数据全部存完,剩余的空间就浪费掉了,也就是说 这 位段 的 内存大小为 8 byte
int main()
{
struct a a;
printf("%d\n", sizeof(a));// 8 byte
return 0;
}
附图:
&ensp;
位段的意义:
经过上程序,我们发现 原本 a 需要占 4 byte空间,但是 a 只需要 2 bit 的空间, 这样就形成巨大的空间浪费。 所以 我们通过 位段 这种方式 大大减小了 空间上浪费的问题
(虽然位段也有浪费,但是相比 结构体 浪费的空间要少, b c d 也是同理)
程序一:
#include<stdio.h>
// vs 环境
struct s // 因为下面 成员类型 为 char 类型,所以它 一开始 就准备 了 1 byte 空间
{
char a : 3;// a 要个 3 bit 位, 由提前准备 1 byte 空间 来分配,还剩 5 bit
//
char b : 4;// b 要 4 bit 位,由提前准备 1 byte 空间 来分配,还剩 1 bit 空间
char c : 5;// c 要 5 bit 位,由提前准备 1 byte 空间 来分配(剩余 1 bit),不够大,
// 舍弃(浪费)掉,再向内存申请 1 byte 的空间 来 存储 c (剩余 3 bit 空间)
char d : 4;// d 需要 4 bit 位,剩余内存空间(3 bit)不够,把 3 bit 浪费掉(舍弃)
// 再向内存申请 1 byte 空间,来存储 d,(剩余 4 bit 空间)
// 至此 数据全部 存储完毕,剩余的空间 舍弃掉(浪费了)
// 一共向内存 申请了 3 byte 的空间
};
int main()
{
struct s s = { 0 };
s.a = 10;// 1010 因为 a 只有 3 bit 位 所以存入的是 010
s.b = 20;// 10100 因为 b 只有 4 bit 位 所以存入的是 0100
s.c = 3;// 0011 因为 c 只有 5 bit 位 所以存入的是 00011
s.d = 4;// 0100 因为 d 只有 4 bit 位 所以存入的是 0100
return 0;
}
附图:
化作 16 进制 0x 22 03 04 (可以通过调试 -> 内存 -> &s 来观察 )
位段 的 跨平台问题
1、 int 位段 被当成 有符号数 还是 无符号数 是不确定的。
2、 位段中 最大位的数目 不能确定。(16 位 机器最大 16,32位机器最大32。写成 27,在 16 位 机器会出现问题)
3、 位段中 的 成员 在内存中 从左向右分配,还是从右向左 分配标准 尚未定义(由编译器决定)。
4、当一个结构包含两个位段,第二个位段成员比较大,无法容纳与 第一个 位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的(由编译器决定)。
总结:
位段 跟 结构 相比,位段可以达到同样的效果,且可以很好的节省空间,但是 有 跨平台的问题存在。 位段 不能 跨平台 使用
2,枚举: 顾名思义就是 -> 列举(把可能的取值 一 一 列举)
举几个现实生活的例子
1、一周 的 星期一 到 星期日 是 有限的 7天,可以 一 一 列举
2、性别有:男、女、保密,也可以 一 一 列举
3、月份有 12 个月,也可以 一 一 列举
枚举类型的定义
程序一:
enum sex// 性别
{
// 枚举的可能取值(枚举常量)
male,// 0
female,// 1
secret// 2
};
enum color // 颜色
{
// 枚举的可能取值(枚举常量)
red,// 0
green,// 1
blue// 2
};
int main()
{
enum day d = mon;//枚举赋的值,只能是 枚举的 可能取值
enum sex s = male;//枚举赋的值,只能是 枚举的 可能取值
enum color c = blue;//枚举赋的值,只能是 枚举的 可能取值
blue 在枚举的里面 是 2, 那我们 可不可以这样写 enum color c = 2 ?
答案是不行,右边 2 是 int 类型, 左边的 c 是 enum color 类型
printf("%d %d %d\n", male, female, secret);// 0 1 2
printf("%d %d %d\n", red, green, blue);// 0 1 2
printf("%d\n",sizeof(s)); // 枚举大小为 4 byte ,为什么呢?
因为 枚举常量 的类型是 整形,
那 s 就是整形变量,所以输出为 4
return 0;
}
程序二:
#include<stdio.h>
//#define red 0 // 这里的 red 是一个符号, 程序 遇到 red 替换成 0
//#define green 1
//#define blue 2
enum color
{
// 枚举的可能取值(枚举常量)
red = 1,
green = 3,
blue = 5
};
int main()
{
//int color = red;
// 枚举
printf("%d %d %d\n", red, green, blue);//[red = 2 ] 2 3 4
//[green = 2 ] 0 2 3
// [ green = 2 , blue = 9] 0 2 9
// [ red = 1、 greem = 3、blue = 5] 1 3 5
return 0;
}
枚举的优点
为什么要是使用枚举
1. 增加代码 的 可读性 和 可维护性
2. 和 #define 定义 的 标识符 比较 枚举 有类型检查,更加严谨。
3. 防止了 命名污染(命名冲突,互相干扰)
4. 便于调试
5. 使用方便,一次可以定义多个常量
小知识点
C语言的源代码 – 预处理(预编译)-> 编译 -> 链接 -> 可执行程序
在预编译中 就完成了 define 的替换
3. 联合 == 联合体(共用体)
联合类型的定义
联合也是一种特殊的自定义类型,这种类型 定义的变量 也包含一系列的成员,特征是这些成员公用同一块空间
// 所以 联合 也叫 联合体 / 共用体
程序一:
#include<stdio.h>
union un // 创建联合体
{
char c;// 对齐数 1
int i; // 对齐数 4
};
int main()
{
union un u;// 联合体变量 u
printf("%d\n", sizeof(u));// 4
printf("%p\n", &u);// 00D7FBBC
printf("%p\n", &(u.c));// 00D7FBBC
printf("%p\n", &(u.i));// 00D7FBBC
// 你会发现 联合体(共用体)变量 和 成员 都是用的同一块空间
// 这意味着 一次 只能用一个成员,要不然会出问题
return 0;
}
为什么 该联合体 的大小为 4 呢?跟着我往下看,你就会知道为什么为 4 了。
联合的特点:
联合的成员 是 共用 同一块 内存空间的,这样 一个 联合变量 的大小,至少是最大成员的大小
因为 联合 至少得有 能力 保存 最大 的 那个成员
面试题:判断当前计算机大小端存储
#include<stdio.h>
正常方法
int check_system()
{
int a = 1;
return *(char*)&a;// 返回1,表示小端
// 返回0, 表示大端
}
联合体 方法
int check_system()
{
union // 因为 我们只用 一次,所以可以写成 匿名 形式
{
char c; // |-- i --|
int i; // 口 口口口
// c
}u; // |-- i --|
u.i = 1;// 01 00 00 00 小端字节序存储模式
// c
// 我们想从 4 byte 里面取出 第一个字节的内容,
// i 占了 4byte,那么 c 不就是它第一个字节了
// 因为 联合 成员是共用 一块空间的,int 访问 4 byte 的内容。char 访问一个字节内容
// 所以直接 返回 u.c 等于就是把 第一个字节的内容(01) 传回去了
return u.c;// 返回1,表示小端
// // 返回0, 表示大端
}
int main()
{
int a = 1;
//低地址 高地址
// 01(低) 00 00 00(高) 小端字节序存储模式
// 00(高) 00 00 01低) 大端字节序存储模式
int ret = check_system();
if (1 == ret)
{
printf("小端存储模式\n");
}
else
{
printf("大端存储模式\n");
}
}
联合大小的计算
联合的大小至少是最大成员的大小
当最大成员大小不是最大对齐的整数倍的时候,就要对齐到最大对齐数的 整数倍
程序一:
#include<stdio.h>
union un
{
int a; // 自身大小是4 默认对齐数 8 取较小值 4,所以对齐数为 4
char arr[5];// 拿元素的类型,来算对齐数,元素的类型是 char ,大小 为 1
// 默认对齐数 为 8 ,取较小值 1,即 对齐数为 1
//最大 对齐数 是 4,如果我们按照 5 个 byte 就够了(因为数组有 5 个元素)
// 但是 5 不是 最大对齐数 4 的整数倍
// 所以浪费 3 字节,5 + 3 == 8 byte
};
int main()
{
union un u;
printf("%d\n",sizeof(u));// 8
return 0;
}