目录
一.结构体
1.定义
是一种复杂的数据类型,结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。
我们可以先来回顾一下基础的数据类型有:
1、整型
- int:基本整型,存储整数,占4个字节。
- short:短整型,占2个字节。
- long:长整型,占4个字节。
- long long:双长整型,占8个字节。
2、浮点型
- float:单精度浮点型,占4个字节。
- double:双精度浮点型,占8个字节。
3、字符型
- char:字符型,存储单字符,占1个字节。
当我们要描述一个复杂的对象时,他可能有很多特征需要来描述,但是这些特征又不全是一个类型的,我们用基础数据类型就没有办法很好的展示这个对象,这时我们就可以用结构体类型,简单来说,结构体类型就是将多个基础数据类型可以结合到一起,将其组成一个整体的复杂数据类型。
2.初始化
eg:猫类型:包括姓名,年龄,体重,叫声的特征描述。
(1).先声明,后初始化
struct cat{//猫类型
char name[30];//姓名
int age;//年龄
int weight;//体重
char voice[30];//叫声
};//注意分号
实例化:
void main{
struct cat c1={"小白",2,10,"喵喵喵"};//定义结构体变量c1时同时初始化
struct cat *p1 = NULL;//也可以定义结构体指针变量p1
}
(2).使用 typedef 关键字声明,再初始化
typedef 为C语言的关键字,作用是为一种数据类型定义一个新名字。这里的数据类型包括内部数据类型(int,char等)和自定义的数据类型(struct等)。
在编程中使用typedef目的一般有两个,一个是给变量一个易记且意义明确的新名字,另一个是简化一些比较复杂的类型声明 。
typedef struct cat{
char name[30];
int age;
int wegiht;
char voice[30];
};
实例化:
void main{
cat c2={"小白",2,10,"喵喵喵"};//定义结构体变量c2时同时初始化
cat *p2 = NULL;//结构体指针变量p2
}
(3)声明的同时定义
struct cat{
char name[30];
int age;
int wegiht;
char voice[30];
}c3,*p3;//声明的同时定义结构体变量c3和结构体指针变量p3
实例化:
void main(){
c3 = { "小白", 2, 10, "喵喵喵", NULL };
p3 = NULL;
}
也可以在声明定义时直接初始化:
struct cat{
char name[30];
int age;
int wegiht;
char voice[30];
struct cat *next;
}c3={"小白",2,10,"喵喵喵"};
注意:结构体变量中的成员变量,只有在初始化的时候可以通过赋值进行初始化,定义完毕后就不能使用赋值进行整体的成员变量初始化了(此时的操作就是赋值操作,不能称之为初始化)。
比如说如下操作,就是对c3的赋值操作,这时已经不能再叫初始化了。
struct cat{
char name[30];
int age;
int wegiht;
char voice[30];
struct cat *next;
}c3,*p3;
void main(){
c3 = { "小白", 2, 10, "喵喵喵" };
p3 = NULL;
}
3.结构体的自引用
结构体的自引用(self reference),就是在结构体内部,包含指向自身类型结构体的指针。
(1).不使用 typedef 时
错误用法:
typedef struct node{
int data;
struct node next;//错误
};
这种方式是错误的,因为这种声明实际上是一个无限循环,成员 node 是一个结构体,node 的内部还会有成员 next 是结构体,同理next内部还有成员是结构体,我们也不知道结构体node的大小,其实是无法分配内存的,这种方式是非法的。
正确用法:
那我们该如何实现自引用呢?这时我们就可以利用指针,指针的大小是固定的,比如在32位下是4个字节,这时就不会存在内存长度未知的情况了。
typedef struct node{
int data;
struct node *next;//正确
};
(2).使用 typedef 时
错误用法:
typedef struct {
int data;
node *next;//虽然使用了指针 但这里的node尚未被定义
} node;
正确用法:
//方法一
typedef struct node_1{
int data;
struct node_1 *p;
}NODE;
//方法二
struct node_2;
typedef struct node_2 NODE;
struct node_2{
int data;
NODE *p;
};
//方法三
struct node_3{
int daata;
struct node_3 *p;
};
typedef struct node_3 NODE;
4.结构体的内存对齐(计算结构体的大小)
(1).为什么存在内存对齐?
1. 平台原因(移植原因): 不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2. 性能原因: 数据结构(尤其是栈)应该尽可能地在自然边界上对齐。 原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
总体来说:结构体的内存对齐是拿空间来换取时间的做法。
那在设计结构体的时候,我们既要满足对齐,又要节省空间,如何做到:
让占用空间小的成员尽量集中在一起。
(2).结构体的对齐规则
1. 第一个成员在与结构体变量偏移量为0的地址处。
2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
- VS中默认的值为8
- Linux中的默认值为4
3. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
(3).修改默认对齐数
#pragma 这个预处理指令,可以改变我们的默认对齐数。
#pragma pack(n)
//设置默认对齐数为n
#pragma pack()
//还原默认对齐数
为什么要修改默认对齐数?
结构在对齐方式不合适的时候,我们可以自己更改默认对齐数。比如说在进行网络通信的时候,将一个结构体变量(一块连续的内存数据)发送给了另一个主机,如果两个平台对齐数不同,在进行内存中数据解释的时候就会出现问题,因此两个主机都必须使用指定大小的对齐数。
eg:
#include <stdio.h>
#pragma pack(8)
//设置默认对齐数为8
struct S1{
char c1;//1
int i;//4
char c2;//1
}; //16
#pragma pack()
//取消设置的默认对齐数,还原为默认
(4).举例
问:该结构体默认对齐数为8,则结构体总大小为多少?
struct student{
int sno;
char num;
int age;
short score;
};
画图理解:
最后答案为:16。
5.位段
(1).什么是位段
位段,C语言允许在一个结构体中以位为单位来指定其成员所占内存长度,这种以位为单位的成员称为“位段”或称“位域”( bit field) 。利用位段能够用较少的位数存储数据。
(2).举例说明
由以下结构体举例,我们可以看出该结构体的总大小为12,但是我们可以发现,sex和pos属性其实根本占用不了1个字节,sex属性只需1个比特位,pos属性只需两个比特位,所以这样定义还是会造成一些空间的浪费。
struct student{
int sno;//4
char sex;//1
//0-男 1-女
char pos;//1
//0-普通学生 1-班长 2-学习委员 3-生活委员
int score;//4
};//12
那么我们是否可以采取一些措施,使该结构体的大小做到“物尽其用”呢?
这时就出现了位段,通过 “member:n(member为成员变量,n为占据的比特位)” 的形式指定空间中n个比特位存储数据,并且多个位段可以合并占用同一个存储空间,比如sex和pos成员宫廷使用一个字节的空间就够了,从而实现减少空间浪费的目的。
struct student{
int sno;
char sex : 1;//0-男 1-女
char pos : 2;//0-普通学生 1-班长 2-学习委员
int score;
};
sex和pos会合并使用一个字节,在vs平台下从低位开始赋值。
画图理解:
注意:在vs平台下,多个位段,使用同一块内存空间,从低位开始向高位存储数据,如果比特位不够了则申请一个新的空间存储。
(3).总结
- 为什么使用位段:可以节省空间。
- 位段的成员必须是 int、unsigned int 或signed int 。
- 位段的成员名后边有一个冒号和一个数字。
- 位段存储的数据过大会产生截断。
- 第一个位段剩余的空间不够存储第二个位段,则会为第二个位段新开空间存储。
- 使用位段后不存在字节对齐了。
- 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。
二.联合体
1.定义
联合也是一种特殊的自定义类型 这种类型定义的变量也包含一系列的成员,特征是这些成员公用同一块空间(所以联合也叫共用体)。
//联合类型的声明
union Un{
char c;
int i;
};
void main(){
//联合变量的定义
union Un un;
//计算连个变量的大小
printf("%d\n", sizeof(un));
}
c 成员和 i 成员都是从起始地址处,共用同一块空间,只不过 c 成员只可以访问1个字节,a 成员可以访问4个字节。
运行结果如下:
2.联合体的特点
联合的成员是共用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为联合至少得有能力保存最大的那个成员)。
3.联合大小的计算
联合的大小至少是最大成员的大小。
当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。
4.联合体的应用场景
(1).大小端的检测。
关于大小端的介绍可以访问以下链接查看:
union Un{
char c;
int i;
};
void main(){
//联合变量的定义
union Un un;
//大小端判断
un.i = 1;
if (un.c == 1){
printf("小端\n");
}
}
(2).在不同的场景下适用不同的数据。
三.枚举
1.定义
枚举顾名思义就是一一列举。
把可能的取值一一列举。
比如我们现实生活中:
一周的星期一到星期日是有限的7天,可以一一列举。
enum Day//星期
{
Mon,
Tues,
Wed,
Thur,
Fri,
Sat,
Sun
};
{ } 中的内容是枚举类型的可能取值,也叫 枚举常量 。这些可能取值都是有值的,默认从0开始,依次递增1。当然在定义的时候也可以赋初值。
eg:
enum Color//颜色
{
RED=1,
GREEN=2,
BLUE=4
};
enum Color clr = GREEN;//只能拿枚举常量给枚举变量赋值
注意:只能拿枚举常量给枚举变量赋值,才不会出现类型的差异。
2.优点(原因)
我们也可以使用 #define 定义常量,为什么非要使用枚举?
有以下几点:
- 增加代码的可读性和可维护性。
- 和#define定义的标识符比较枚举有类型检查,更加严谨。
- 防止了命名污染(封装)。
- 便于调试。
- 使用方便,一次可以定义多个常量。