目录
1.结构体类型
1.1 结构体定义与声明
数组为一类相同类型的值的集合,结构体为一些不同类型的值集合,这些值称为成员变量。
1.2 结构体的声明
1.2.1 结构体类型的声明
结构体的声明由标签名,成员列表,变量列表组成,其中变量列表在声明中不是必要的,可以在其后再单独进行结构体变量的声明。
struct tag//标签名
{
member-list;//成员列表
}variable-list;//变量列表
例子(描述一个学生)
struct stu
{
char name[20];//姓名
int age;//年龄
char sex[5];//性别
char id[20];//学号
};//分号不可省略
1.2.2 特殊的声明
在结构体类型的声明时,可以进行不完全声明,即省略掉标签名,称之为匿名结构体类型。此种声明只可在初始的类型声明时创建变量。
示例
//匿名结构体类型
struct
{
int a;
char b;
float c;
}x;
struct
{
int a;
char b;
float c;
}a[20], *p;
//结构体类型的数组,结构体类型的指针
引申思考:
以其上声明为背景,下面的代码是否合法
p = &x;
答:不合法,结构体类型即使内容一致,也不是同一种类型。
1.2.3 结构体的自引用
结构体的成员变量亦可是结构体类型,那么,结构体类型在初始声明时是否可以创建自身类型的变量作为成员变量呢,我们来看看下面的声明方式是否可行呢?
//代码1
struct Node
{
int data;
struct Node next;
};
上面的此种声明方式无疑是错误,其对类型定义的逻辑会导致此声明一直无法完成。接下来,我们来看看正确的自引用声明方式。
//代码2
struct Node
{
int data;
struct Node* next;
//我们来创建一个此类型的结构体指针作为成员变量,其中存储的是结构体变量的地址
//在此后的调用和操作中,我们对此成员变量解引用即可
//注:链表
};
自引用结构体类型的 typedef
//此样写代码是否可行
typedef struct
{
int data;
Node* next;
}Node;
//答:不可对匿名结构体类型typedef
//正确写法:
typedef struct Node
{
int data;
struct Node* next;
}Node;
1.3 结构体变量的定义和初始化
//定义变量的方式
//方法1
//声明类型的同时定义变量p1
struct Point
{
int x;
int y;
}p1;
//方法2
//专门定义结构体变量p2
struct Point p2;
//初始化:定义变量的同时赋值
//方式1
//创建变量时赋值
struct Point p3 = {x, y};
//方式2
//声明类型时定义变量并赋值
struct Node//声明一个数据结点类型
{
int data;
struct Point p;
struct Node* next;//结构体嵌套初始化
}n1 = {10,{4,5},NULL};
1.4 结构体内存对齐
我们已知结构体为多种类型的值得集合,那么它大小是简单的所有所有成员对象的大小之和吗?
我们来看如下例子:(注:IDE:VS2019)
//示例1:
struct s1
{
char c1;
int i;
char c2;
};
//运行结果:12
//对照1:
struct s2
{
char c1;
char c2;
int i;
};
printf("%d\n",sizeof(struct s2));
//运行结果:8
如上可见,我们开始的推测是错误的,那么,结构体内部的成员变量是如何排列的呢?
其中有一套明确的规则(结构体的对齐规则),我们接下来学习此规则。
结构体对齐规则
- 第一个成员与结构体变量偏移量为0的地址处对其。
(注:偏移量:一个字节作为一块空间,后续字节空间与结构体的第一个字节空间的距离)
- 其他成员变量要对其到某个数字(对齐数)的整数倍的地址处(偏移量)
(注:对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值,VS中默认的值为8)- 结构体总大小为最大对齐数(每一个成员变量都有自己的对齐数)的整数倍
- 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
//示例2:
struct s3
//最大对齐数为8
{
double d;//对齐数为8
char c;//对齐数为1
int i;//对齐数为4
};//16
printf("%d\n",sizeof(struct s3));
//示例3:
struct s4
//最大对齐数为8
{
char c1;//对齐数为1
struct s3 s;//对齐数为8
double d;//对齐数为8
};//32
printf("%d\n",sizeof(struct s4));
内存对齐的必要性
- 平台原因:并非所有的硬件的平台都能在任意地址上访问任意数据,某些硬件平台只能在某些地址处取某些特定类型的数据,否则将抛出硬件异常
- 访问效率:数据结构(尤其是栈空间,局部变量)应尽可能在自然边界上的对齐(内存对齐规则),若要访问未对齐的内存,cpu需要作两次内存访问,而对齐的内存只需访问一次。
总而言之,就是以一定的空间来换取更快的访问效率
根据上面的示例1 与 对照1,我们看到两个结构体类型其中的成员变量的类型是相同的,可是两者的大小却是不同的,对照1更小,示例1更大。那么,由此可知,成员变量的排列顺序可以在一定程度上减少空间的浪费。
在设计结构体时,我们既要满足对齐,又要节省空间,我们可以让占空间小的成员集中在一起
1.5 修改默认对齐数
#pragma 此预处理指令可以修改默认对齐数。
示例:
#include <stdio.h>
#pragma pack(8)//设置默认对齐数位 8
struct s1
{
char c1;
int i;
char c2;
};//12
#pragma pack()//取消设置的默认对齐数,还原为默认
#pragma pack(1)//设置默认对齐数1
struct s2
{
char c1;
int i;
char c2;
};//6
#pragma pack()//还原为默认
int main()
{
printf("%d\n",sizeof(struct s1));
printf("%d\n",sizeof(struct s2));
//运行逻辑
return 0;
}
结构在对齐方式不合适的时候(即,未在合适的自然边界处没有达到提高访问效率的目的),可以修改结构体的默认对齐数
引申,写一个宏计算结构体中某成员变量相对于首地址的偏移 offsetof 宏的实现
1.6 结构体传参
struct s
{
int data[1000];
int num;
};
struct s a = {{1,2,3,4},1000};
void Print1(struct s a)
{
printf("%d\n",s.num);
}
void Print2(struct s* pa)
{
printf("%d\n",ps->num);
}
int main()
{
Print1(a);//传结构体
Print2(&a);//传结构体地址
}
问:两种结构体传参方式那种更好?(传址)
Print2函数的传参方式更好,因为函数在传参时需要进行压栈,会有时间和空间上的系统开销。
若在传参时,结构体过大,传参时压栈的系统开销也更大,会导致性能下降。显然地址的大小远小于结构体变量本身。
2. 位段
2.1 什么是位段
在结构体声明中,可以使用字节为单位对成员变量的大小作定义,由此实现节省内存空间的目的,此种声明方式即被称为位段。位段声明方式如下:
struct A
{
int _a:2;
int _b:5;
int _c:10;
int _d:30;
};
//A即为一个位段类型
//位段类型变量名可以_(下划线)开头
注:位段声明与结构体的不同:
- 位段的成员只能是 int,unsigned int,signed int,char(属于整形家族)类型
- 位段成员之后有一个冒号与一个数字
2.2 位段在内存中的空间分配
以char类型为例,若位段成员所占空间大于8个字节(一个char类型空间大小),则再申请一个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(struct s));//3字节
return 0;
}
问:此位段类型的大小,与排列方式?(IDE:VS)
VS中,位段成员的填充规则为,以字节为单位自右向左,若当前空间不够时,则申请新的空间,旧的空间不再利用。
2.3 位段的局限性(跨平台的问题)
- 各个平台中int 位段被当成时有符号还是无符号是未确定的
- 位段的最大数目是不能确定的,(有些平台的int 类型大小是确定的,有些平台的为16位,int类型为16 bit,若在此种情况下,位段元素大小为大于16小于32bit的数时,代码无法运行。有些平台为32位,int为32 bit)
- 位段成员的填充是自左向右,自右向左尚未可知。
- 旧空间是否利用也不确定。
2.4 位段的应用
数据包数据(网络)的排列方式就是采用的位段。可以压缩数据包的大小,节省空间,提高效率。
3. 枚举类型
3.1 什么是枚举类型
枚举类型就是创建一个自己拟类型的类型,其中类型可能的取值都由自己规定与枚举出来。
3.2 枚举类型的定义
enum Day//类型名
{
Mon,//枚举类型可能的取值
Tues,
Wed,
Thur,
Fri,
Sat,
Sun
};
enum Sex
{
MALE,
FEMALE,
SECRET,
};
enum Color
{
RED,
GREEN,
BLUE
};
- 其上定义的enum Day,enum Sex,enum Color都是枚举类型。
- { }之中的内容都是枚举类型的可能取值,亦称枚举常量。
- 这些枚举常量都是有数值大小的,默认自第一个枚举常量起始取得数值从0开始,之后一次加1。同时,每个枚举变量的数值也可以在初始声明中自己拟定。
示例
//默认
enum Color
{
RED, //0
GREEN,//1
BLUE //2
};
//自拟定1
enum Color
{
RED = 1,//自拟定值,此后一次加 1
GREEN, //2
BLUE //3
};
//自拟定2
enum Color
{
RED = 1,//每个枚举常量一一自拟定
GREEN = 4,
BLUE = 6
};
3.3 枚举类型的使用意义
如何增加了代码的可读性?枚举类型使用的意义?
将一些有实际意义的数值在编程时能以有意义的字符的形式出现?
枚举的优点:
- 增加代码的可读性和可维护性
- 和#define定义的标识符比较枚举类型有类型检查,更加严谨。(以常量赋值,.c文件中可运行,.cpp文件中报错)
- 便于调试
- 使用方便,一次可以定义多个常量
3.4 枚举的使用示例:
enum Color
{
RED = 1;
GREEN = 2;
BLUE = 4
};
int main()
{
enum Color clr = GREEN;
//clr = 6;
//int类型的数值不能用于初始化枚举类型的数值
printf("%d\n",clr);
//以十进制数的形式打印
//2
return 0;
}
4. 联合体(共用体)类型
判断数据存储方式是大端还是小端。
小端:低位在低地址,高位在高地址
大端:低位在高地址,高位在低地址
VS2019上为小端存储
4.1 联合体类型的定义
联合类型是将各种数据类型的元素在内部声明,这些成员仍具有其本身的数据类型,但所有这些成员公用一块空间。
4.2 联合的特点
- 联合体的成员公用一块空间,此联合体的大小至少为最大的成员的大小。
- 因为在同一块空间上,所以对一个成员变量的更改也会影响其他成员变量。
示例:
union Un
{
int i;
char c;
};
union Un un;
printf("%p\n",&(un.i));
printf("%p\n",&(un.c));
//两联合体成员地址相同,可得知两联合体成员共用一块空间
un.i = 0X11223344;
un.c = 0X55;
printf("%x\n",un.i);
//成员变量un.i的值为0X11223355
//对成员变量un.c的更改也会影响到un.i
4.3 计算联合体的大小
联合体的大小至少为最大成员的大小
当最大成员的大小不是最大对齐数的整数倍时,需对齐到最大对齐数的整数倍。
示例:
union Un1
{
char c[5];//5字节
int i;
};
//最大对齐数为5
union Un2
{
short c[7];//14字节
int i;
};
//最大对齐数为8
printf("%d\n",sizeof(union Un1));
//6字节(补1字节)
printf("%d\n",sizeof(union Un2));
//14字节(补2字节)