目录
- 前言:
- 一.结构体
- 1.结构体的声明
- 2.结构体特殊的声明
- 3.结构体的自引用
- 4.结构体变量的定义和初始化
- 5.结构体内存对齐
- 6.修改默认对齐数
- 7.结构体传参
- 二.位段
- 1.什么是位段
- 2.位段的内存分配
- 三.枚举
- 1.枚举的定义
- 2.枚举的优点
- 四.联合(共用体)
- 1. 联合类型的定义
- 2. 联合的特点
- 3. 联合大小的计算
前言:
之前学过结构体的初阶知识,在原来的基础上会深入了解结构体的自引用、内存对齐和结构体实现位段;同时在初识C语言时,稍微了解了一点枚举的相关知识,在这里将会更深入学习;还有学习全新的知识—联合
——————————————————————————
一.结构体
先回忆下什么是结构体?
结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。
1.结构体的声明
struct tag
{
member-list;
}variable-list;
struct:结构体关键字
tag:自定义名字
member-list:成员列表
variable-list:变量列表
比如描述一个学生,有名字、年龄、性别和身份证号码。
struct Stu
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
}; //分号不能丢
创建结构体变量时可以这样写:
struct Stu
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
}s1, s2, s3;
也可以这样写:
struct Stu
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
};
struct Stu s1, s2, s3;
2.结构体特殊的声明
在声明结构的时候,可以不完全的声明。
比如:
struct
{
int a;
char b;
float c;
}x;
这叫匿名结构体类型,去掉了结构体的名字。这种写法有个缺点,就是创建的变量只能使用一次(变量x)
再创建一个结构体指针:
struct
{
int a;
char b;
float c;
} *p;
那么能不能写成:
p = &x;
答案是不行,p和x里面的成员虽然一样,但是编译器会把上面的两个声明当成完全不同的两个类型,所以这是非法的。
总结:匿名结构体类型的变量只能使用一次,不常用
3.结构体的自引用
结构体的自引用其实是结构体自己包含自己(同类型),但是要注意,包含的必须是结构体指针,这样才能找到结构体的地址。
struct Node
{
int data;//数据域
struct Node* next;//指针域
};
如果用typedef给结构体类型重新起名字为Node,前面学过匿名可以省略Node,那么这样写是否可行?
typedef struct
{
int data;
Node* next;
}Node;
答案是不行,因为typedef对类型重命名后,在花括号里面提前使用Node了,然后才命名Node,所以顺序混乱了。
正确的写法:
typedef struct Node
{
int data;
struct Node* next;
}Node;
4.结构体变量的定义和初始化
结构体变量的定义有两种方式,一种是直接顺着类型定义结构体变量;另一种是有了类型之后单独创建结构体变量。
struct SN
{
char c;
int a;
}sn1, sn2;//全局变量
int main()
{
struct SN sn3, sn4;//局部变量
return 0;
}
结构体变量的初始化:
struct SN
{
char c;
int a;
}sn1 = { 'w',2 }, sn2 = { .a = 5,.c = 't' };
int main()
{
printf("%c %d\n", sn1.c, sn1.a);
printf("%c %d\n", sn2.c, sn2.a);
return 0;
}
sn2 = { .a = 5,.c = ‘t’ };用点可以找到你想赋值的成员,不需要按顺序来初始化。
5.结构体内存对齐
结构体内存对齐其实是计算结构体大小
我们先看一段代码:
struct S1
{
char c1;
int i;
char c2;
};
struct S2
{
char c1;
char c2;
int i;
};
int main()
{
printf("%d\n", sizeof(struct S1));
printf("%d\n", sizeof(struct S2));
return 0;
}
刚开始的思路:
c1一个字节,i 4个字节,c2一个字节,加起来S1一共6个字节,S2同理
结果是:
我们可以使用宏offsetof来计算结构体成员相较于结构体起始位置的偏移量
#include <stddef.h>
struct S1
{
char c1;
int i;
char c2;
};
struct S2
{
char c1;
char c2;
int i;
};
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;
}
打印出来的结果:
我们发现结构成员不是按照顺序在内存中连续存放的,而是有一定的对齐规则
- 第一个成员在与结构体变量偏移量为0的地址处。
- 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
VS中默认的值为8
Linux中没有默认对齐数,对齐数就是成员自身的大小- 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
- 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
知道了规则后,我们再来分析下struct S1
为什么存在内存对齐?
1. 平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2. 性能原因: 数据结构(尤其是栈)应该尽可能地在自然边界上对齐。 原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访 问。
所以,在设计结构体的时候,我们既要满足对齐,又要节省空间(让占用空间小的成员尽量集中在一起)
6.修改默认对齐数
#pragma 这个预处理指令可以改变默认对齐数
#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()//取消设置的默认对齐数,还原为默认
结构在对齐方式不合适的时候,我们可以自己更改默认对齐数。一般设置默认对齐数都是2的次方,便于使用。
7.结构体传参
struct S
{
int data[100];
int num;
};
struct S s = {{1,2,3,4}, 100};
//结构体数值传参
void print1(struct S s)
{
printf("%d\n", s.num);
}
//结构体地址传参
void print2(struct S* ps)
{
printf("%d\n", ps->num);
}
int main()
{
print1(s); //传结构体的数值
print2(&s); //传地址
return 0;
}
两种传参方式选择传地址更好
原因:
函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。
如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降
结论:
结构体传参的时候,要传结构体的地址。
二.位段
位段与结构体相关联,接下来了解位段
1.什么是位段
位段的声明和结构是类似的,有两个不同:
1.位段的成员必须是 int、unsigned int 或signed int (后来引用了char)
2.位段的成员名后边有一个冒号和一个数字。
比如:
struct A//A就是一个位段类型。
{
int _a:2;
int _b:5;
int _c:10;
int _d:30;
};
位段A的大小:
printf("%d\n", sizeof(struct A));// 8
分析:
位段其实是二进制位,也就是说:
int _a:2:占2个比特位;
int _b:5:占5个比特位;
int _c:10:占10个比特位;
int _d:30:占30个比特位。
假设结构体有一个成员用2个比特位就够了,那么就没必要用分配一个整型,这样就可以节省空间。
2.位段的内存分配
- 位段的成员可以是 int unsigned int signed int 或者是 char (属于整形家族)类型
- 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。
- 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。
位段是如何开辟的?
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;
printf("%d\n", sizeof(s));
return 0;
}
打开调试查看内存:
位段的跨平台问题
- int 位段被当成有符号数还是无符号数是不确定的。
- 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机
器会出问题。 - 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
- 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是
舍弃剩余的位还是利用,这是不确定的
总结:
跟结构相比,位段可以达到同样的效果,并且可以很好的节省空间,但是有跨平台的问题存在。
位段的应用类型于包装的作用,既方便也节省了空间
三.枚举
所谓枚举就是一一列举
比如说星期一到星期五、颜色和月份
1.枚举的定义
列举颜色:
enum Color
{
RED,
GREEN,
BLUE
};
int main()
{
//枚举变量的创建
enum Color c = RED;
// 定义 初始化
return 0;
}
};
这些可能取值都是有值的,默认从0开始,依次递增1,当然在声明枚举类型的时候也可以赋初值。
printf("%d\n", RED);//0
printf("%d\n", GREEN);//1
printf("%d\n", BLUE);//2
自己赋值:
enum Color
{
RED=4,
GREEN=10,
BLUE=2
};
2.枚举的优点
- 增加代码的可读性和可维护性
- 和#define定义的标识符比较枚举有类型检查,更加严谨。
- 便于调试
- 使用方便,一次可以定义多个常量
四.联合(共用体)
1. 联合类型的定义
联合也是一种特殊的自定义类型(关键字:union)
这种类型定义的变量也包含一系列的成员,特征是这些成员公用同一块空间(所以联合也叫共用体)
union Un
{
char c;
int i;
};
int main()
{
union Un un;
printf("%d\n", sizeof(un));
printf("%p\n", &un);
printf("%p\n", &(un.i));
printf("%p\n", &(un.c));
return 0;
}
打印出来的结果:
2. 联合的特点
联合的成员是共用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为联合至少得有能力保存最大的那个成员)
判断当前计算机的大小端存储:
union Un
{
int i;
char c;
};
int main()
{
union Un u = { 0 };
u.i = 1;
if (u.c == 1)
printf("小端\n");
else
printf("大端\n");
return 0;
}
第二种写法:(以函数的方式)
int test()
{
union //匿名只能使用一次
{
int i;
char c;
}un = { .i = 1 };
return un.c;
}
int main()
{
int ret = test();
if (ret == 1)
printf("小端\n");
else
printf("大端\n");
return 0;
}
3. 联合大小的计算
- 联合的大小至少是最大成员的大小。
- 当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。
union Un1
{
char c[5];
int i;
};
int main()
{
printf("%d\n", sizeof(union Un1));// 8
return 0;
}
char 类型自身大小是1,默认是8,所以它的对齐数是1,又因为它是数组,所以是5(最大成员是5);
int 类型自身大小是4,默认是8,所以它的对齐数是4;
5是最大成员,但不是最大对齐数的倍数,所以又要补充3个字节到8,因此结果为8。
~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ 感谢你的观看~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~
~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~