目录
基本知识框架
课堂笔记
结构体类型
结构体类型的声明、变量的定义和初始化
声明
struct unit
{
char member1; //结构体成员可以属于不同的数据类型
int member2; //
long member3; //
struct member4; //结构体的成员可以是其他结构体,这个结构体必须是已经声明过的
struct unit*; //结构体体甚至可以自己引用自己
}; //不要忘记末尾的分号
struct unit
{
char member1;
int member2;
long member3;
struct member4;
struct unit*;
}X; //声明的同时可以定义一个结构体变量,但是不要忘记分号
struct //匿名声明,是不完整的声明形式,省略掉了结构体标签
{
char member1;
int member2;
long member3;
struct member4;
struct unit*;
};
typedef struct unit //这里的结构体标签unit可以省略,相当于直接typedef匿名结构体
{
char member1;
int member2;
long member3;
struct member4;
struct unit*;
}Unit; //这里定义了一个结构体别名
特殊的声明形式
使用匿名结构体时,可能会出现struct
{
int member1;
int member2;
}X;struct
{
int member1;
int member2;
}*pX;当我们声明了以上两个匿名结构体时,pX=&X这个等式是否成立?
不成立,因为编译器会将这两个匿名结构体当作两个完全不同的结构体类型,类型互不相同的指针和地址不能赋值
结构体的自引用
结构体声明中需要使用结构体自引用时,可能会出现以下用法struct unit
{
int a;
struct unit*;
};
这个声明是正确的struct unit
{
int a;
struct unit;
};
这个声明是错误的根据以上两个声明我们可以分析得到:
- 第一种:结构体声明中有一个结构体指针,结构体指针的大小根据平台是一个已知量,结构体的大小因此可以定下来,这样的声明是有效的
- 第二种:结构体声明中引用了结构体本身,结构体本身又是完全没有被定义下来的,于是便产生无限嵌套的结构体,这样的声明是无效的
注:使用typedef声明结构体的情况也类似
定义
struct unit Student; //没有使用typedef时,定义结构体变量需要以 struct + 结构体标签 + 结构体名称 的形式
Unit Student; //使用typedef后,可以通过 结构体别名+结构体名称 的形式来定义结构体,更为方便
初始化
Student = {1,2,3.5,{1,2,3.5},NULL}; //即使是嵌套结构体,也一样可以进行初始化
结构体成员的引用
Student.member1 = 1; //通过 结构体名称+ . +结构体成员名 的形式可以访问结构体内部的成员
(&Student)->member1 = 1; //通过 结构体指针+ -> +结构体成员名 的形式也可以访问结构体成员
注:其实也可以通过结构体首地址+偏移量的方式去获取结构体成员的地址,但是一般不使用这样的方式。使用时要注意:
- 根据结构体声明来计算偏移量
- 根据结构体成员数据类型来读取数据
结构体内存对齐
结构体的内存对齐指的是在计算结构体大小时,结构体成员的排列需要遵循的一些规则
结构体内存对齐的规则包括:
- 第一条:第一个结构体成员会放置在结构体变量地址偏移为0处
- 第二条:之后的其他结构体成员的会放置在【自身对应 对齐数 整数倍】的偏移量处
-
对齐数:指的是编译器默认对齐数与结构体成员大小中的较小值,编译器默认对齐数在Visual Studio中默认为8,Linux中默认为4
- 第三条:结构体整体的大小是其成员对应最大对齐数的整数倍
- 第四条:如果结构体中有内嵌结构体,内嵌结构体的对齐数按照其成员对应的最大对齐数算,外部结构体整体的大小依旧遵循第三条(将内嵌结构体也当作结构体成员来看)
通过下图可以更直观的看出来
内存对齐的意义
- 平台移植原因:不是所有的硬件平台都能访问任意地址上的任意数据的;有些硬件平台只能读取特定地址上的特定数据,否则会抛出硬件异常
- 性能原因:数据结构(尤其是栈)应该尽可能在自然边界上对齐。对于未对齐的内存,处理器读取数据需要进行两次访问;而对于对齐的内存,处理器读取数据只需进行一次访问
结构体的内存对齐是拿空间来换取时间的做法
修改默认对齐数
通过预处理命令#pragma
可以修改默认对齐数
用法为在文件头部加入如下预处理命令:
#include <stdio.h>
#pragma pack(8) //修改默认对齐数为8
结构体传参
结构体传参分为两种:
- 值传参 使用结构体值传参时,相当于将结构体拷贝了一份
- 指针传参 使用结构体指针访问成员时,注意需要使用->
注意
应尽量使用指针传参。因为函数传参时,是需要对参数进行压栈操作的,会有时间和空间上的开销。当结构体的内容较多时,使用值传参会导致性能下降
结构体-位段
C语言中的数据类型可以表示很多数,但是我们的生活中,很多时候只需要表示有限的状态,例如开关,只有开和关的状态;水,有冷和热的状态等等。体现到C语言中,一个char型数据,在计算机中是由8位二进制数存储的,我们并不想完全使用8位,我们只想使用其中的1位,这时我们就需要位段
位段指的就是可以仅仅使用某个数据类型二进制形式中部分位的能力
结构体拥有实现位段的能力,可以通过结构体声明来使用位段
struct unit
{
char a:1; // 表示仅使用char型数据a的后一位
int b:5; // 表示仅使用int型数据b的后五位
... // 使用 : + 数字 的后缀来指定使用的位数
}
注意
- 位段的使用要注意长度,不要超出对应数据类型可用的位数范围
- 位段的使用仅限于unsigned int,signed int数据类型
- 结构体位段的成员内存分布没有特定标准,但都遵循尽量压缩内存的规则
- 有些结构体位段可能会出现无名成员,如
int :5
,这样的结构体成员是不可以使用的,他们存在的作用了位了调整结构体成员的内存分布- 尽量避免跨平台使用位段,因为不同处理器可处理的字长不同,int类型的大小也因此会不一样,从而导致错误
枚举类型
枚举,顾名思义,一一列举。枚举类型的变量取值是被限定在一定范围内的,例如性别,日期,月份等等这样的数据,都可以使用枚举类型来表示
枚举类型的声明、变量的定义和初始化
声明
enum Date // 声明一个枚举数据类型Date,大括号中有所有枚举常量,代表着枚举变量取值的范围
{
Mon = 1, // 声明和定义枚举枚举常量,大括号中所有的枚举常量都会有值
Tues, // 如果不定义首个枚举常量初值的话,默认从0开始,依次加1
Wed,
Thur,
Fri,
Sat,
Sun
};
注意
枚举常量不能再被重新赋值,不能出现Mon = 5;
的情况
定义
enum Date Today // 定义一个枚举变量Today
初始化
enum Date Today = Mon; //定义一个枚举变量Today,并初始化其值为Mon
枚举类型的强制转换
枚举类型可以对数字常量进行强制类型转换
例如Today = (enum Date)1
,效果和Today = Mon;
等价
使用枚举类型的意义
- 增加代码的可读性和可维护性
- 相对于使用
#define
的形式来宏定义常量,枚举类型可以进行类型检查,更为严谨- 防止了命名污染(封装)
- 使用方便,一次可以定义多个常量
联合体类型
联合体类型的声明、定义,包括成员引用都类似于结构体,它也拥有自己的成员,不同的是这些成员都共用一块内存
联合体类型的声明、变量的定义和初始化
声明
unino Block
{
char a;
int b;
};
定义
union Block A;
初始化
A.a = 'a'; //成员引用类似于结构体
A.b = 255;
联合体的特点
所有的成员变量都共享一块内存,所有的成员变量地址都一样。
联合体的整体大小至少是其最大成员的大小(至少有能力保存最大成员)
当联合体成员的大小不是最大对齐数的整数倍是,要将其对齐到最大对齐数的整数倍
基本知识框架Xmind文件下载
链接: 资源下载