目录
1.结构体类型
1.1结构
结构:一些值的集合,这些值被称为成员变量,结构中每个成员类型可以不同
对于一个学生,其相关属性有:姓名,年龄
以下为创建一个学生的结构:
struct Stu
{
char name[20];
int age;
}s1,s2;
int main()
{
struct Stu s3;
return 0;
}
其中,struct Stu是结构体类型,s1,s2是我们创建的结构体全局变量 (非必要,可以使用时再创建),main函数中s3是我们创建二点结构体局部变量,s1,s2,s3这三个变量的类型都是struct Stu.
1.2匿名结构体类型
匿名结构体类型,创建方式如下:
struct
{
char name[20];
int age;
}s;
匿名结构体类型只能使用一次,就是在声明时创建变量,后续只能使用这个变量,而不能创建这个结构体类型的其他变量
我们看以下一个例子:
匿名结构体1:
struct
{
int a;
char b;
float c;
}x;
匿名结构体2:
struct
{
int a;
char b;
float c;
}a[10],*p;
匿名结构体2中:a[10]是一个有十个结构体类型元素的数组,p是一个结构体指针,指向这个匿名结构体
可以发现,这两个结构体的结构成员完全相同 ,那是否证明这两个结构体一样呢?
当我们在VS编译器中运行 p = &x 时,发现编译器报警告
从"*"到"*"的类型不兼容,第一个"*"是&x,x的类型匿名结构体1类型,第二个"*"是p,p是一个结构体指针,指向匿名结构体2
报警告即说明编译器把这两个声明当作完全不同的两个类型,所以操作非法
由此可见,即使两个匿名结构体的成员变量完全相同,它们也是完全不同的结构体
1.3结构体自引用
顺序表:顺序表的内存空间连续,例如一位数组,二维数组
链表:链表的内存空间不连续。如下图:
链表是如何实现的呢?我们可以使用结构体模拟其过程
struct Node
{
int data;
struct Node* p;
};
这个结构体中,成员变量有一个int类型的数据data,还有一个结构体指针类型的变量p, 这两个成员变量分别代表一个节点中的数据域和指针域,对于指针p,它指向一个结构体类型,这个结构体类型就是其所属的结构体
以上过程就是结构体的自引用,可以发现自引用时,包含用类型指针,而不是类型本身
1.4结构体类型重定义 typedef
代码1:
typedef struct Node
{
int data;
struct Node* next;
}Node;
int main()
{
Node d;
return 0;
}
代码2:
typedef struct Node
{
int data;
struct Node* next;
}* linklist;
int main()
{
linklist p;
return 0;
}
代码1:
我们把结构体类型struct Node类型重定义为Node,所以后续创建结构体变量时 Node s;和struct Node s;是等价的
代码2:
我们把结构体类型指针struct Node*类型重定义为linklist,所以后续创建结构体变量时 linklist s;和struct Node* s;是等价的
1.5结构体变量定义和初始化
结构体变量创建有两种方式,一种是类型定义是创建,另一种是在需要使用是在函数内创建,如下代码:p1,p2,p3都是我们创建的结构体变量,不同之处在于p1,p2是结构体全局变量,p3是结构体局部变量,作用域是main函数内
struct Point
{
int x;
int y;
}p1,p2;
int main()
{
struct Point p3 = { 2,3 };
return 0;
}
结构体变量初始化也有两种方式,一种是类型定义是创建并初始化,另一种是在需要使用是在函数内创建并初始化,如下代码:
#include<stdio.h>
struct Point
{
int x;
int y;
}p1,p2;
struct Stu
{
char name[10];
int age;
struct Point p;
}s1 = { "zhangsan",18,{2,3} };
int main()
{
//初始化
struct Stu s2 = { "lisi",20,{2,5} };
//d打印
printf("%s %d %d %d\n", s1.name, s1.age, s1.p.x, s1.p.y);
printf("%s %d %d %d\n", s2.name, s2.age, s2.p.x, s2.p.y);
return 0;
}
输出如下:
以上代码表明结构体可以进行嵌套使用
1.6结构体内存对齐
计算结构体的大小需要了解结构体内存对齐规则
结构体内存对齐规则:
- 第一个成员在与结构体变量偏移量为0的地址处
- 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。对齐数=min{编译器默认的对齐数,成员大小}。VS编译器中默认对齐数为8
- 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍
- 嵌套结构体情况:嵌套结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体)的整数倍。
计算以下结构体的大小:
#include<stdio.h>
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;
}
可以发现,这两个结构体的成员变量相同,但成员变量定义的顺序不同
根据结构体内存对齐规则,我们进行以下分析:
S1:
如上图,我们标出了每一块内存空间的相对偏移量
1.第一个成员在与结构体变量偏移量为0的地址处
c1是一个char类型的变量,c1的大小为1byte
2.其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。对齐数=min{编译器默认的对齐数,成员大小}。VS编译器中默认对齐数为8
i是一个int 类型的变量,其大小为4byte,所以变量i对齐数 = min{8,4} = 4,变量i要对齐到4的整数倍的地址处,距离第一个变量最近的地址且为4的整数倍的地址为偏移量为4的地方
c2是一个char 类型的变量,其大小为1byte,所以变量c1对齐数 = min{8,1} = 1 ,变量i要对齐到1的整数倍的地址处,偏移量为8的地方满足要求
3.结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍
结构体的总大小 = n*max{1,4,1} = 4n
距离最后一个成员变量最近的地址且为4的整数倍的地址为偏移量为12的地方
所以结构体总大小为12byte
S2:
如上图,我们标出了每一块内存空间的相对偏移量
1.第一个成员在与结构体变量偏移量为0的地址处
c1是一个char类型的变量,c1的大小为1byte
2.其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。对齐数=min{编译器默认的对齐数,成员大小}。VS编译器中默认对齐数为8
注:其他编译器(GCC)没有对齐数,成员对齐数为自身大小
c2是一个char类型的变量,其大小为1byte,所以变量c2对齐数 = min{8,1} = 1,变量c2要对齐到1的整数倍的地址处,偏移量为1的地方满足要求
i是一个int 类型的变量,其大小为4byte,所以变量i对齐数 = min{8,4} = 4,变量i要对齐到4的整数倍的地址处,距离第一个变量最近的地址且为4的整数倍的地址为偏移量为4的地方
3.结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍
结构体的总大小 = n*max{1,1,4} = 4n
距离最后一个成员变量最近的地址且为4的整数倍的地址为偏移量为8的地方
所以结构体总大小为8byte
C语言中具有函数形式的宏offsetof可以返回结构体成员的偏移量
以上述S1和S2为例:
S1:
S2:
以下我们再计算几个结构体的大小
例1:
struct S3
{
double d;
char c;
int i;
};
如上图,我们标出了每一块内存空间的相对偏移量
1.第一个成员在与结构体变量偏移量为0的地址处
d是一个double类型的变量,d的大小为8byte
2.其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。对齐数=min{编译器默认的对齐数,成员大小}。VS编译器中默认对齐数为8
c是一个char 类型的变量,其大小为1byte,所以变量c1对齐数 = min{8,1} = 1 ,变量i要对齐到1的整数倍的地址处,偏移量为8的地方满足要求
i是一个int类型的变量,其大小为4byte,所以变量i对齐数 = min{8,4} = 4 ,变量i要对齐到4的整数倍的地址处,距离上一个变量最近的地址且为4的整数倍的地址为偏移量为12的地方
3.结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍
结构体的总大小 = n*max{8,1,4} = 8n
距离最后一个成员变量最近的地址且为8的整数倍的地址为偏移量为16的地方
所以结构体总大小为16byte
struct S3
{
double d;
char c;
int i;
};
struct S4
{
char c1;
struct S3;
double d1;
};
1.第一个成员在与结构体变量偏移量为0的地址处
c1是一个char 类型的变量,其大小为1byte
2.其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。对齐数=min{编译器默认的对齐数,成员大小}。VS编译器中默认对齐数为8
S3是一个结构体类型的变量,其大小为16byte,所以变量S3对齐数 = min{8,16} = 8 ,变量S3要对齐到8的整数倍的地址处,距离上一个变量最近的地址且为8的整数倍的地址为偏移量为8的地方
d1是一个double类型的变量,d的大小为8byte,所以变量d对齐数 = min{8,8} = 8 ,变量d1要对齐到的整数倍的地址处,距离上一个变量最近的地址且为8的整数倍的地址为偏移量为24的地方
3.结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍
结构体的总大小 = n*max{1,16,8} = 16n
距离最后一个成员变量最近的地址且为16的整数倍的地址为偏移量为32的地方
所以结构体总大小为32byte
为什么存在内存对齐?
1.平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据,一些硬件平台只能在某地址处取某些特定类型的数据,否则抛出硬件异常
2.性能原因:数据结构(尤其是栈)应该尽可能再自然边界上对齐,因为为了访问为对齐的内存,处理器需要作两次内存访问,而对齐的内存只需要访问一次
可以说:结构体内存对齐是拿空间换取时间的做法
设计结构时,应满足占用空间小+内存对齐
基于所设计的功能,让占用空间小的成员尽量集中在一起
默认对齐数修改:#pragma pack(4)//将默认对齐数修改为4
#pragma pack( )//对齐数恢复原值
#pragma pack(1)//将默认对齐数修改为1,数据可以挨个存放
1.7结构体传参
结构传参有两种方式:传值调用和传址调用
struct S
{
int data[100];
int num;
};
void Print1(struct S ss)
{
int i = 0;
for (i = 0; i < 3; i++)
{
printf("%d ", ss.data[i]);
}
printf("%d\n", ss.num);
}
void Print2(struct S* ps)
{
int i = 0;
for (i = 0; i < 3; i++)
{
printf("%d ", ps->data[i]);
}
printf("%d\n", ps->num);
}
int main()
{
struct S s = { {1,2,3},100 };
Print1(s);
Print2(&s);
return 0;
}
Print1函数和Print2函数都可以实现结构体内容的打印,但是我们应首选Print2函数
原因:函数传参时,参数需要压栈,会有时间和空间上的系统开销,如果传递一个结构体对象时,结构体过大,参数压栈的系统开销比较大,所以会导致性能下降,所以结构体传参应首选结构体地址
2.位段
2.1位段的定义与声明
我们一般说,结构体实现位段的能力
位段的声明和结构体类似,有以下俩个不同:
- 位段的成员必须是整型家族类型的数据,例如int,signed int,unsigned int......
- 位段的成员名后边还有一个冒号和一个数字
struct A
{
int _a : 2;//_a占2bit
int _b : 5;//_b占5bit
int _c : 10;//_c占10bit
int _d : 30;//_d占30bit
};
2.2位段的内存分配
位段的内存分配:
- 位段的成员是整型家族类型的数据,例如int,char,unsigned int......
- 位段在空间上是按照需要以4byte(int)或1byte(char)的方式开辟的
- 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应避免使用位段
我们举例说明位段如何计算大小
例1:
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));
return 0;
}
定义char a : 3;
首先开辟8bit,a占用3bit
位段在使用空间时从低位到高位使用
定义char b : 4;//b占用4bit
定义char c : 5;
开辟的这一个字节仅剩余1bit,不够变量c使用,所以这1bit浪费掉
重新开辟1byte = 8bit
定义char d : 4;
开辟的这一个字节仅剩余3bit,不够变量d使用,所以这3bit浪费掉
重新开辟1byte = 8bit
综上所述:这个位段的大小为3byte
s.a = 10;
s.b = 12;
s.c = 3;
s.d = 4;这3byte的内容为:0 1100 010 000 00011 0000 0100
注:a只使用了3bite,数据会发生丢失
十六进制形式为:62 03 04
在VS编译器下,数据是小端存储模式
2.3位段的跨平台问题
- int位段被当成有符号数/无符号数是不确定的
- 位段中最大位的数目不能确定(16位机器最大16位,32位机器最大32位;int类型的在16位机器上为2byte,在32位机器上为4byte,写成27,16位的机器会出问题)
- 位段中的成员在内存中从左向右分配/从右向左分配标准为定义
- 当一个结构包含俩个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用是不确定的
总:跟结构体相比,位段可以达到同样的效果,可以很好的节省空间,但存在跨平台问题
3.枚举
3.1枚举类型的定义
枚举:就是列举,把可能的取值一一列举
例如:星期,性别,三原色......
和结构体相同,枚举类型不占用空间,只有使用时才开辟空间
enum Day
{
Mon,
Tues,
Wed,
Thur,
Fri,
Sat,
Sun
};
int main()
{
enum Day d = Fri;
return 0;
}
枚举类型中,枚举常量的取值默认从0开始
修改:Mon = 1,可以修改默认值从1开始
3.2枚举的优点
- 枚举可以增加代码的可读性和可维护性
在通讯录中,我们打印菜单函数,用户进行输入选择,可以进行优化如下
enum option
{
EXIT,//默认取值为0
ADD,
DELETE,
SEARCH,
MODIFY,
SHOW,
SORT
};
在switch语句中,我们直接可以使用枚举常量
switch(input)
{
case EXIT:
case ADD:
case DElETE:
... ... ...
}
- 和#define定义的标识符比较,枚举有类型检查,更加严谨
- 防止了命名污染,因为类型有封装
- 便于调试
#define定义的标识符在预处理阶段会用值替换变量,当我们调试时,变量已经被替换,可能与代码的逻辑产生参差
- 使用方便,一次可以定义多个常量
3.3枚举的使用
枚举使用时要注意:只能拿枚举常量给枚举变量赋值,才不会出现类型的差异
例:enum Day d = 5;//cpp语言会报错
4.联合(共同体)
4.1联合的定义与声明
联合:这种类型定义的变量也包含一系列成员,特征是这些成员共用一块空间,所以联合也叫共同体
联合声明
union Un
{
int a;
char c;
};
4.2联合的特点
由上图:可以发现联合所有成员共用一块内存空间,这样一个联合变量的大小,至少是最大成员的大小,因为联合至少有能力保存最大的成员
联合可以节省空间,但也导致其某些成员变量不能同时使用
使用联合可以判断当前机器的存储模式是大端还是小端
创建一个联合
union Un { int a; char b; };
union Un
{
int a;
char b;
};
int check_system()
{
union Un u;
u.a = 1;
return u.b;//只访问第一个字节
}
int main()
{
int ret = check_system();
if (ret == 1)
{
printf("小端存储\n");
}
else
{
printf("大端存储\n");
}
return 0;
}
4.3联合大小的计算
定义一个联合如下:
union Un
{
char arr[5];//对齐数 = min{8,1} = 1
int i;//对齐数 = min{8,4} = 4
};
对齐规则:当联合中最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍
最大成员大小:1byte*5 = 5byte
最大对齐数 = max{1,4} = 4
对齐到偏移量为4n处,且有能力保存最大的成员,所以联合大小为8byte
union Un
{
short arr[5];//对齐数 = min{8,2} = 2
int i;//对齐数 = min{8,4} = 4
};
最大成员大小:2byte*7 = 14byte
最大对齐数 = max{1,4} = 4
对齐到偏移量为4n处,且有能力保存最大的成员,所以联合大小为16byte