目录
前言
C语言中有:
- 内置类型
- 自定义类型
内置类型有:
char、short、int、long 、long long、float、double。
内置类型是C语言本身就具有的类型,可以直接拿来使用,除了这些类型之外C语言还有自定义类型,自定义类型是自己所创造的一种类型。
自定义类型有:
结构体、枚举、联合体,数组。
1. 结构体的声明
1.1 结构的基础知识
结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。
结构体其实是把一些值组合在一起作为某一个对象的属性。
1.2 结构的声明
结构体的语法形式:
struct tag
{
member-list;
}variable-list;
struct是——关键字;
tag——是结构体标签,类似于结构体的名字,在C语言中使用结构体类型时,应该把struct和tag写在一起;大括号里放的是描述对象所需要的相关属性:
member-list——是成员列表,这样的每一个成员是成员变量;
variable-list——是变量列表。
如定义一个学生的结构体类型:(描述一个学生)
#include <stdio.h>
struct Stu//结构体标签是Stu
{
char name[20];
char sex[5];//数组
int age;
int height;//整型
//成员列表里有多个成员变量
};//这里是结构体类型:struct Stu
int main()
{
struct Stu s1;//s1就是用struct Stu这个结构体类型创建的结构体变量
return 0;
}
#include <stdio.h>
struct Stu
{
char name[20];
char sex[5];
int age;
int height;
}s2, s3, s4;//这里直接用上面的struct Stu结构体类型创建了s1,s2,s3,3个结构体变量
//这里用逗号隔开可以一下子创建多个结构体变量,所以这里叫变量列表
//s2,s3,s4是全局变量
int main()
{
struct Stu s1;//结构体变量,s1是局部变量
return 0;
}
#include <stdio.h>
struct Stu
{
char name[20];
char sex[5];
int age;
int height;
}s2, s3, s4;//全局变量,注意这里有分号
//单独再定义一个全局变量:
struct Stu s5;//全局变量
int main()
{
struct Stu s1;//局部变量
return 0;
}
结构体类型最好放在前面定义。
1.3 特殊的声明
结构体有一些特殊的声明:
在声明结构的时候,可以不完全的声明。
比如:把标签去掉——不完全声明,没有名字是匿名结构体类型
#include <stdio.h>
struct
{
char c;
int a;
double d;
}x;
//匿名结构体类型
//这里用匿名结构体类型创建了一个变量叫x
int main()
{
return 0;
}
匿名结构体类型的使用,必须紧挨着结构体类型创建变量,否则以后就不能定义变量了,没有名字就不能定义了。
匿名结构体类型只能用一次:在声明结构体类型的同时定义变量。
#include <stdio.h>
struct
{
char c;
int a;
double d;
}x;//匿名结构体
struct
{
char c;
int a;
double d;
}* ps;//ps是这样一个匿名结构体的指针
int main()
{
//ps指针存放x的地址
ps = &x;//错误:=两边从*到*的类型不兼容
//即编译器认为这是两种类型,虽然成员一模一样但是编译器
//认为匿名结构体在这里使用会是两种类型,认为这两种结构体类型是不一样的
//此代码非法
return 0;
}
1.4 结构的自引用
在结构中包含一个类型为该结构本身的成员是否可以呢?
数据结构:数据在内存中存储的结构。如:顺序表、链表、栈、队列、二叉树。
顺序表:如在内存中找一块连续的空间存储1,2,3,4,5,像数组一样。对顺序表提供增删查改的功能就是顺序表的数据结构。像一条线
链表:1存2的地址,1能找到2;2存3的地址,2能找到3……最后的数字5存NULL,不存有效地址,它没有任何指向。找到1就可以把2,3,4,5串起来,像一条线。
顺序表和链表都像一条线,所以顺序表和链表是线性数据结构。
两个指针之间的交叉点是结点,怎么描述这样的结点呢?
错误的自引用方式:
#include <stdio.h>
struct Node
{
int data;
struct Node next;
};
int main()
{
return 0;
}
此时struct Node的大小是多少?即计算sizeof(struct Node)?
struct Node
{
int data;//结点处放的整型数据
struct Node next;//一个结点中包含下一个结点
};
//结构体中又包含一个自己结构体类型这种形式写法是绝对不行的,因为
//struct Node这个结构体包含一整型data,又包含一个这样的结构体变量next,则
//这个结构体变量里面还有一个data,还有自己的next……无限包含,则
//这个结构体大小就没办法计算了
int main()
{
return 0;
}
重新分析:一个结点中只要存储下一个结点的地址就可以了:
正确的自引用方式:
#include <stdio.h>
struct Node
{
int data;
struct Node* next;
};
int main()
{
return 0;
}
自己类型的对象找自己类型对象的方法——要存储的是结构体指针而不是结构体对象,这是结构体的自引用。
#include <stdio.h>
struct Node
{
int data;//4个字节
struct Node* next;//存放一个地址(指针),4个字节或8个字节
};
//此时结构体大小就确定了
//此时创建的每个结点即可以保存一个数值data又可以保存一个地址(由next指向的结点)
//——这就实现了自己类型的对象找自己类型对象的方法,是结构体的自引用
int main()
{
return 0;
}
这是链表的实现方式,也是结构体的自引用。
注意:
#include <stdio.h>
typedef struct
{
int data;
Node* next;
}Node;
int main()
{
Node n;//这里定义结点n可以吗?
return 0;
}
此时用typedef对struct和{}中的这个匿名结构体起名是Node,然后在结构体成员中写Node*next
——不可行!
因为运行时编译器显示错误:Node:未声明的标识符,即不认识Node,用typedef对结构体类型重命名时这个类型必须是清晰可见的,必须是存在的,即Node必须在有的前提下才能用typedef重新命名。
把一个结构体重命名,则这个结构体必须是清晰可见的。
#include <stdio.h>
typedef struct Node
{
int data;
//Node* next;//但是这里还没有产生Node,则:
struct Node* next;
}Node;//这里是用typedef把struct Node这个结构体重命名为Node
int main()
{
//重命名完之后就不用struct Node创建结构体变量了,直接用Node
//但也还是可以用struct Node创建变量的
//即struct Node和Node可以同时使用
Node n = { 0 };
struct Node n1 = { 0 };
return 0;
}
typedef是关键字,是重命名结构体的。
1.5 结构体变量的定义和初始化
有了类型就可以创建变量,即有了结构体类型就可以创建结构体变量。
定义结构体的方法:
#include <stdio.h>
struct Point//锁定点
{
int x;//结构体成员x:横坐标
int y;//结构体成员y:纵坐标
}p1;//定义结构体变量p1
//这里声明类型的同时定义了结构体变量p1
//这时就已经有了struct Point这个结构体类型
struct Point p2;//用struct Point这个结构体类型创建p2变量
struct Point p3 = { 1, 2 };//定义变量的同时赋初值
int main()
{
return 0;
}
如一个结点的创建及初始化:
#include <stdio.h>
typedef struct Node
{
int data;
struct Node* next;
}Node;
int main()
{
struct Node n = { 100,NULL };
return 0;
}
如对一个学生类型初始化:
#include <stdio.h>
struct Stu
{
char name[20];
char sex[5];
int age;
int height;
};
int main()
{
struct Stu s = { "zhangsan", "nan", 20, 180 };
return 0;
}
初始化,只要把它的成员放在大括号中就可以了,同一个结构体多个成员初始化都用一个大括号,比如数组,结构体。
如果结构体成员是嵌套的,即一个结构体包含一个结构体,这时怎么进行初始化呢?
#include <stdio.h>
struct Stu
{
char name[20];
char sex[5];
int age;
int height;
};
struct Data
{
struct Stu s;
//这是嵌套:结构体中包含一个结构体类型的数据
char ch;
double d;
};
int main()
{
struct Data d = { {"lisi","nv",30,166},'w',3.14 };
//首先初始化s,因为s又是一个结构体成员所以用大括号括起来,后面在初始化ch、d
return 0;
}
#include <stdio.h>
struct Point
{
int x;
int y;
}p1;
struct Node
{
int data;
struct Point p;
struct Node* next;
}n1 = { 10, {4, 5}, NULL };//结构体嵌套初始化
struct Node n2 = { 20,{5,6},NULL };//结构体嵌套初始化
int main()
{
return 0;
}
结构体成员的访问可以用.和->这两个操作符访问。
1.6 结构体内存对齐
——重点
结构体内存对齐涉及到的最终问题就是计算结构体大小。
#include <stdio.h>
struct s1
{
char c1;//c1占1个字节大小
int i;//4
char c2;//1
};//6个字节空间即可
int main()
{
printf("%d\n", sizeof(struct s1));//12,为什么?
return 0;
}
研究c1、i、c2在内存中是如何存储的?
offsetof(),函数名是谁的偏移量,本身是一个宏,返回的是一个结构体成员相较于其结构体起始位置的偏移量。
size_t offsetof( structName, memberName );
参数是结构体名,结构体成员名;返回的值的类型是size_t,是个整型值。使用它需要包含头文件<stddef.h>。
#include <stdio.h>
#include <stddef.h>
struct s1
{
char c1;
int i;
char c2;
};
int main()
{
//printf("%d\n", sizeof(struct s1));
printf("%d\n", offsetof(struct s1,c1));//计算c1的偏移量:0
printf("%d\n", offsetof(struct s1, i));//计算i的偏移量:4
printf("%d\n", offsetof(struct s1, c2));//计算c2的偏移量:8
return 0;
}
结构体是怎么进行内存对齐的呢?以至于刚刚生成了大小为12字节的布局。
首先掌握结构体的对齐规则:
1. 第一个成员在与结构体变量偏移量为0的地址处存放。
2. 其他成员变量(从第二个成员开始)要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数 = 编译器默认的一个对齐数与该成员大小的较小值。
VS环境中编译器默认的值(对齐数)为8。
如果某一成员的自身大小大于8,则也取编译器默认对齐数和成员自身大小的较小值。
3. 结构体总大小为最大对齐数的整数倍。
每个成员变量都有一个对齐数,最大对齐数就是所有成员的对齐数中最大的那个。
4.如果成员是一个结构体:即嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
注意:linux环境默认没有对齐数,则对齐数就是成员自身大小,没有较小值了;总大小还是最大对齐数的整数倍。这里最大对齐数就是所有成员对齐数自身大小的最大值。
#include <stdio.h>
#include <stddef.h>
struct s1
{
char c1;
int i;
char c2;
};
struct s2
{
char c1;
char c2;
int i;
};
int main()
{
printf("%d\n", sizeof(struct s2));//8
//printf("%d\n", offsetof(struct s1,c1));//计算c1的偏移量:0
//printf("%d\n", offsetof(struct s1, i));//计算i的偏移量:4
//printf("%d\n", offsetof(struct s1, c2));//计算c2的偏移量:8
return 0;
}
发现:结构体成员一模一样,只是换了顺序就节省了4个内存单元的空间。
判断下面的结构体总大小是多少?
struct s3
{
double d;
char c;
int i;
};
int main()
{
printf("%d\n", sizeof(struct s3));
return 0;
}
分析:
运行结果:16
接下来看:结构体嵌套问题:
struct s4
{
char c1;
struct s3 s3;
double d;
};
int main()
{
printf("%d\n", sizeof(struct s4));
return 0;
}
运行结果:32
对齐规则的第4条:
如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整
体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
注意:嵌套的s3结构体对齐到自己的最大对齐数的整数倍处,这里的最大对齐数是8,不是16,因为是s3自己结构体成员的最大对齐数,是自己结构体内部成员的最大对齐数不是看结构体自己大小。
为什么存在内存对齐?
1. 平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特
定类型的数据,否则抛出硬件异常。
只能在某些特定的位置取值,则存这些值的时候也应该最好存到那个特定的位置上,比如只能在对齐的位置上取数据,则就把这些数据对齐到能够取的位置上把数据存起来。
2. 性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。
原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访
问。
总体来说:
结构体的内存对齐是拿空间来换取时间的做法。
在设计结构体的时候,既要满足对齐,又要节省空间,如何做到:
让占用空间小的成员尽量集中在一起,(一起放在前面后一起放在后面都可以)尽量利用空间。
#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));//12
printf("%d\n", sizeof(struct s2));//8
return 0;
}
S1和S2类型的成员一模一样,但是S1和S2所占空间的大小有了一些区别。
1.7 修改默认对齐数
可以使用 #pragma 这个预处理指令改变默认对齐数。
#pragma pack (放数字)和#pragma pack(不放数字)是一对,二者是成对使用的。
#include <stdio.h>
#pragma pack(1)
//设置默认对齐数为1
struct s1
{
char c1;//放在0偏移量处
int i;//i成员自身大小是4,默认对齐数是1,则该成员的对齐数就是1,即把它对齐(放)在1的倍数上就可以了,即紧挨着c1放在1偏移量处占4个字节
char c2;//c2成员自身大小是1,默认对齐数是1,则该成员的对齐数就是1,即把它对齐(放)在1的倍数上就可以了,即紧挨着i放在5偏移量处占1个字节
//由1偏移量到5偏移量共6个字节
//此时结构体大小6又是结构体所有成员中最大对齐数1的整数倍,所以该结构体总大小为6.
};//对于设置和恢复中间的数据就会在内存对齐时按照1字节对齐
#pragma pack()
//恢复默认对齐数
int main()
{
printf("%d\n", sizeof(struct s1));//6
return 0;
}
去掉注释:
#include <stdio.h>
#pragma pack(1)
struct s1
{
char c1;
int i;
char c2;
};
#pragma pack()
int main()
{
printf("%d\n", sizeof(struct s1));//6
return 0;
}
#include <stdio.h>
struct s
{
char c1;//放在0偏移量处
//浪费1-7偏移量的位置
double d;//放在8-15偏移量处
char c2;//放在16偏移量处
//0-16共17个字节大小
//结构体总大小应该是24
//所以又浪费了17-23的偏移量位置
};
int main()
{
printf("%d\n", sizeof(struct s));//24
return 0;
}
若修改默认对齐数:
#include <stdio.h>
#pragma pack (4)
//设置默认对齐数为4
struct s
{
char c1;//放在0偏移量处
//浪费1-3偏移量的位置
double d;//放在4-11偏移量处
char c2;//放在12偏移量处
//0-12共13个字节大小
//结构体总大小应该是16
//所以又浪费了13-15的偏移量位置
};
#pragma pack()
//恢复默认对齐数
int main()
{
printf("%d\n", sizeof(struct s));//16
return 0;
}//在一定程度上节省了空间
结论:
结构体在对齐方式不合适的时候,可以自己更改默认对齐数。一般情况下更改的默认对齐数是2^n次方。注意当更改为1的时候就先相当于取消了对齐,没有对齐就没有优化了,这是用时间换空间了。
1.8 结构体传参
#include <stdio.h>
struct S
{
int data[1000];
int num;
};
struct S s = { {1,2,3,4}, 1000 };
//结构体传参
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;
}
print1 和 print2 函数哪个好些?
分析:
如果是传值调用,实参传给形参的时候,形参将是一块很大的临时拷贝,浪费了很大时间和空间;如果采取的是传址调用,传的只是4个字节或8个字节。
答案:
首选print2函数。
原因:
函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。
如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的
下降。
如果传递一个结构体指针的时候,参数压栈的的系统开销就会比较小,性能就会得到保障。
结论:
结构体传参的时候,要传结构体的地址。
注意:
传结构体变量本身:形参和实参是占有不同的内存空间的,对形参的修改不会影响到实参。
传结构体地址:地址(指针)就有能力改变实参。对指针加const即可不被修改从而保护起来。
2. 位段
结构体具有实现位段的能力(位段的填充&可移植性)。
位段是依赖于结构体的,位段是结构体中一种特殊的实现。
2.1 什么是位段
位段的声明和结构是类似的,但有两个不同:
- 1. 位段的成员必须是 int、unsigned int 或signed int 。(位段的成员也可以写成char)
- 2. 位段的成员名后边有一个冒号和一个数字。
struct A
{
int _a : 2;
int _b : 5;
int _c : 10;
int _d : 30;
};
A就是一个位段。位段A的大小是?
struct A就是一个结构体类型,用sizeof直接计算即可。
#include <stdio.h>
struct A
{
int _a : 2;
int _b : 5;
int _c : 10;
int _d : 30;
};
int main()
{
printf("%d\n", sizeof(struct A));//8
return 0;
}
2,5,10,30的意义是什么?位段的大小是怎么计算的呢?它是怎么开辟空间的呢?
位段的位是指二进制位。
:2的意思是只占2个比特位,即_a只需要2个比特位。
:5的意思是只占5个比特位,即_b只需要5个比特位。
:10的意思是只占10个比特位,即_b只需要10个比特位。
:30的意思是只占30个比特位,即_b只需要30个比特位。、
共47个比特位,6字节是48比特位,但是计算结果是8字节。内存是怎么给位段A分配内存的呢?
2.2 位段的内存分配
1. 位段的成员可以是 int 、unsigned int、 signed int 或者是 char (属于整形家族)类型。
2. 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。
意思是:
如果成员是int类型,一次开辟4个字节,不够了再开辟。
#include <stdio.h>
struct A
{
int _a : 2;//给_a一下子开辟4个字节,_a用了2个比特位还剩30个比特位
int _b : 5;//_b只需要5个比特位,剩下的30个还够给_b,_b用了5个比特位还剩25个比特位
int _c : 10;//_c用了10个比特位还剩15个比特位
int _d : 30;//剩下的15个不够给_d了,则再开辟4个字节
//即共开辟8个字节
};
int main()
{
printf("%d\n", sizeof(struct A));
return 0;
}
_d成员用的是开辟的4个字节32个比特位的空间还是先用掉剩下的15比特位的空间呢?这取决于编译器的实现,C语言标准并没有规定,这就形成歧义,不确定因素,但开辟4个字节的空间是必然的。
3. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。
不跨平台是指在不同编译器下实现方式是有差异的,不同编译器又依赖于不同平台,如Linux用的是gcc、clong这样的编译器;VS是在Windows支持下。
如果成员是char类型:一次开辟1个字节:
方案1:
struct s
{
char a : 3;//给a开辟1个字节,还剩5个比特位
char b : 4;//b用了4个比特位,还剩1个比特位
char c : 5;//剩下的1个比特位不够了,则再给c开辟1字节的空间,如果c没有使用剩下的1个比特位,刚开劈的8比特位还剩3比特位
char d : 4;//剩下的3比特位不够了,则再给d开辟1字节空间
//共开辟了3字节空间
};
int main()
{
printf("%d\n", sizeof(struct s));
return 0;
}
方案2:
struct s
{
char a : 3;//给a开辟1个字节,还剩5个比特位
char b : 4;//b用了4个比特位,还剩1个比特位
char c : 5;//剩下的1个比特位不够了,则再给c开辟1字节的空间,如果c使用了剩下的1个比特位,加上刚开劈的8比特位还剩4比特位
char d : 4;//则此时剩下的4比特位就已经够d使用了,不用再另开辟了
//共开辟了2字节空间
};
int main()
{
printf("%d\n", sizeof(struct s));//3
return 0;
}
运行结果是:3
发现在VS编译器下支持的是方案1。
#include <stdio.h>
struct A
{
int _a : 2;
int _b : 5;
int _c : 10;
int _d : 30;
};
struct s
{
char a : 3;
char b : 4;
char c : 5;
char d : 4;
};
int main()
{
printf("%d\n", sizeof(struct s));
return 0;
}
增加了位段的实现能力后,发现如果不是位段A,即不是:2,:5,:10;:30则4个整型就需要8字节,是位段就仅需要3字节,就在一定程度上节省了空间。
struct A
{
int _a : 2;
int _b : 5;
int _c : 10;
int _d : 30;
};
使用位段实现的前提条件:比如需要明确知道_a的取值,而int _a的取值无非就是:00,01,10,11这4种状态,每一种状态需要2个比特位就能保存,int _a : 2仅需2个比特位就能描述这4种状态;int _b : 5,_b的取值需5个比特位存放,_c的取值需10个比特位存放,不需要很大的空间;_d的取值需30个比特位存放,也不需要很大的空间,这样在一定程度上节省了空间——位段存在的意义。
_a为什么可以用2bit保存?
是根据自己需求设计的,谁去设计声明struct A,就自己决定_a的几个比特位,上述_a、_b等设计的几个比特位是举例,是假设。
假设表示年龄,一个人的年龄不会超过1000,则每次在结构体中写:int age;就会造成浪费,则可以在结构体中写int age : 10; int age : 16; 这样的形式,表示age是10个或16比特位的,若age是10比特位,则10个比特位存放的正数的最大数值是:0111111111,是2^10-1,就是1023,则已经够描述age。即需要存储值的空间需要几个比特位是根据需求设计的。
位段在内存中是如何存储的?
对于:
struct S
{
char a : 3;//a成员占3个比特位
char b : 4;//a成员占4个比特位
char c : 5;//a成员占5个比特位
char d : 4;//a成员占4个比特位
//此结构体共开辟3字节空间
};
#include <stdio.h>
int main()
{
struct S s = { 0 };//首先内存空间中都初始化为0,即3字节的每个比特位都是0
//在内存空间中放数值:
s.a = 10;
s.b = 12;
s.c = 3;
s.d = 4;
return 0;
}
给a开辟的1字节空间中,a占的1字节是左边的3比特位还是右边的3比特位——这取决于编译器的实现。
假设在一个字节内部是从右边低位向左边高位使用,注意这与大小端存储没有关联,因为考虑的是1字节内部的数据。大小端是一个数据如果占多个字节则以字节为单位的顺序,是字节与字节之间的顺序,考虑的不是一个字节内部的数据,大小端强调的是大端字节序,小端字节序,强调的是字节序。
a占3个比特位,现在s.a是给a成员中存放10,10的二进制序列是1010,会占4个比特位,而a的空间大小只有3比特位,所以存不下1010,存的是010;
b占4个比特位,b放12,12的二进制序列是1100,有4个比特位,可以放下则存的就是1100;
假设没有用剩下的1比特位空间,先浪费掉。
再开辟一字节空间:
c占5个比特位,c放3,3的二进制序列是00011,可以存下即放进去;依然是从右边低位至左边高位使用一字节的空间;
剩下的3比特位不够存放,假设没有用剩下的3比特位空间,先浪费掉。
再开辟一字节空间:
d占4个比特位,d放4,4的二进制序列是0100,可以放下即放进去。
现在的3字节存放的二进制序列是:
011000100000001100000100
转换成十六进制是:6 2 0 3 0 4
验证假设:在内存中是否是这样放的?
代码调试看内存窗口:
注意:在地址栏中输入的是&s
发现和假设的一模一样,说明在当前编译器一个字节中剩下的比特位不够使用时是浪费掉的;在一个字节内部是从右边低位到左边高位使用存放数据的。
注意:从右边低位至左边高位使用一字节的空间与栈没有关系,栈是从高地址向低地址使用的,即在栈区申请空间时是从从高地址向低地址的。
创建的a,b,s都是局部变量,局部变量是在栈上开辟空间的;
因为栈空间的使用是从高地址向低地址的,即先用高地址空间,用完高地址后再用的是低地址空间。
为变量s开辟空间时是在占区合适的位置一下子就开辟好了的,而变量内部的空间的如何使用完全取决于变量自己的,s是位段,位段内部使用就是一个字节内部先使用低位再使用高位,注意这是位,在内存划分(内存编制)时一个单位是一个字节,而现在操作的是一个个比特位,比编制的力度低很多。
总结:
位段式的结构体是如何开辟空间的?(位段的基本存储方式)
如果成员是char,就一个字节一个字节地开辟空间,开辟好一个字节之后使用这一个字节时,从右边低位到左边高位的;
如果成员是int,就4个字节4个字节地开辟空间,即一个整形一个整形地开辟,开辟好一个整形之后,在一个整形内部(4个字节)也是从4个字节的右边低位向左边高位使用的。
如果剩下的空间不够给下一个成员时就把剩下的空间浪费掉,再为下一个成员开辟新的空间。
——这是当前VS编译器下使用的细节。
2.3 位段的跨平台问题
1. int 位段被当成有符号数还是无符号数是不确定的,在不同的编译器下实现方式有所差异。
2. 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机
器会出问题。
写成int c = 9是错误的;int a = 33是错误的,已经超出int的最大范围了,必须在32以内,但是写30也有问题,因为在早期的16位机器上:sizeof(int)是16个比特位;在当前32位、64位机器下:sizeof(int)是32个比特位。即所使用的机器不同,int的大小就不一样,则位段的最大取值就不一样,这就导致位段的跨平台问题。
3. 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
即是一个字节内部是从右边低位到左边高位使用还是从左边高位到右边低位使用完全是不确定的,C语言标准并没有做出任何规定,它的实现完全取决于编译器,即会导致跨平台问题。
4. 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是
舍弃剩余的位还是利用,这是不确定的。(开辟新的空间是必然的)
基于以上原因,说明位段本身是不跨平台的,可以针对不同平台写出不同的代码实现。
总结:
跟结构相比,位段可以达到同样的效果,并且相对结构可以很好的节省空间,但是有跨平台的问题存在。
2.4 位段的应用
IP数据包的一种格式:
当在网络上传输一个数据时,如在微信上发“呵呵”,这两个字在内存上是怎么发送的呢?是谁发的?发给谁?即必须知道目标和源头这些信息,“呵呵”是发送的数据,数据之上还有(封装头部)信息:比如包装一些4位版本号,4位首部长度、8位服务类型、16位总长度,8位生存时间,8位协议,16位首部校验和,32位源IP地址(源头),32位目的IP地址(目标)。
这里说的位都是比特位,如果开辟空间时不用位段,用最小的char类型则一次开辟8比特位,直接用结构体方式实现,不仅会造成空间浪费,如在版本号就浪费4比特位,首部长度浪费4比特位等,而且会使得结构体的数据放大,这个数据和真实要发送的数据这个数据包如果过大,网络状态就像高速公路,高速公路上的汽车越多,则容易导致高速公路拥堵,行车体验就差,即如果在网络上传输的数据包特别大,当网络上传输的数据包特别大特别多时就会产生网络拥挤。所以在版本号,首部长度等各种地方适当进行内存空间的节省,让数据包都小小的往网上发(传输),就为网络状态减轻了很多负担从而使网络状态更好,所以这种实现就会使用位段,因为不需要一个整形,一个char类型,只需要几个比特位,即可以用位段式的结构来定义数据包的结构格式,所以位段虽然不跨平台但是在网络上位段是有应用的,能很大程度上节省空间。
3. 枚举
枚举是一一列举,把可能的取值一一列举。
比如现实生活中:一周的星期一到星期日是有限的7天,可以一一列举;性别有:男、女、保密,也可以一一列举;月份有12个月,也可以一一列举,这里就可以使用枚举。
但是如成绩,体重的可能取值(每个人都有差异)有很多,所以很难一一列举。
3.1 枚举类型的定义
枚举类型关键字enum + 枚举类型的标签,大括号中放枚举的可能取值。
#include <stdio.h>
//定义星期的枚举类型:enum Day合在一起是枚举类型
enum Day
{
//枚举的可能取值,即enum Day这个枚举类型的可能取值就是以下7种:
Mom,
Tues,
Wed,
Thir,
Fri,
Sta,
Sun
};
//用枚举的方式定义一个性别:
enum Sex
{
//Sex的可能取值:
MALE,
FEMALE,
SECRET
};
//用枚举的方式定义颜色:
enum Color
{
RED,
GREEN,
BLUE
};
int main()
{
enum Day d = Sun;//d变量
enum Sex s = SECRET;//s变量
return 0;
}
以上定义的 enum Day , enum Sex , enum Color 都是枚举类型。
{}中的内容是枚举类型的可能取值,也叫 枚举常量 。
这些可能取值都是有初始值的,默认从0开始,一次递增1;
#include <stdio.h>
enum Sex
{
MALE,
FEMALE,
SECRET
};
int main()
{
printf("%d\n", MALE);//0
printf("%d\n", FEMALE);//1
printf("%d\n", SECRET);//2
return 0;
}
把MALE理解为0,FEMALE理解为1,SECRET理解为2是有点抽象,但是把MALE表示为0,FEMALE表示为1,SECRET表示为2,则既有值的意思又有一定的可读性,增加了代码的可读性。(这里不能与数组相联系,enum Sex是 一个类型,这里从0开始向下增加是对它可能取值的定义,也不会作为真假的判断)
在定义的时候也可以赋初值。
例如:
#include <stdio.h>
enum Color
{
RED = 1,
GREEN = 2,
BLUE = 4
};
#include <stdio.h>
enum Sex
{
MALE = 4,
FEMALE = 8,
SECRET
};
int main()
{
printf("%d\n", MALE);//4
printf("%d\n", FEMALE);//8
printf("%d\n", SECRET);//9
//即没有明确值时是向下增加1
return 0;
}
#include <stdio.h>
enum Sex
{
MALE = 4,
FEMALE ,
SECRET
};
int main()
{
printf("%d\n", MALE);//4
printf("%d\n", FEMALE);//5
printf("%d\n", SECRET);//6
//即没有明确值时是向下增加1
return 0;
}
枚举类型是常量,这里赋值是给这个常量一个初始值,相当于初始化,若以后想改是改不了的
#include <stdio.h>
enum Sex
{
MALE = 4,
FEMALE,
SECRET
};
int main()
{
MALE = 6;//错误:表达式必须是可修改的左值
return 0;
}
枚举是一种类型,如:enum Sex,那这个枚举类型去创建变量,如创建枚举类型的变量d,枚举类型的变量s,可以对枚举类型进行typedef,是类型就可以对它重命名,如用sex替换enum Sex,用enum Sex的时候直接写Sex即可。
#include <stdio.h>
enum Day
{
Mom,
Tues,
Wed,
Thir,
Fri,
Sta,
Sun
};
typedef enum Sex
{
MALE,
FEMALE,
SECRET
}Sex;
int main()
{
enum Day d = Sun;
enum Sex s = SECRET;
/*Sex x = { 0 };*///是错误的初始化方式,应该拿它的可能取值进行初始化
Sex x = MALE;
return 0;
}
枚举类型的一个变量大小是多少?——是一个整型
变量就放一个可能取值,而这个可能取值又是一个整型,所以变量大小就是一个整型。
typedef enum Sex
{
MALE,
FEMALE,
SECRET
}Sex;
int main()
{
/*Sex x = { 0 };*///是错误的初始化方式,应该拿它的可能取值进行初始化
Sex x = MALE;
printf("%d\n", sizeof(x));//4
return 0;
}
注意:
enum Sex
{
MALE,
FEMALE,
SECRET
};
enum Sex这个枚举类型里面不是放了3个整型,大小不是12,里面放的只是可能取值,enum Sex只是类型而已,是要去创建变量,看这个变量的大小是多少,这个变量只能存可能取值中的一个值(放的可能是MALE、FEMALE、SECRET之一)这个值是整型,所以这个变量的大小是4个字节。
printf("%d\n", sizeof(MALE));
这是错误的代码。
枚举只是一种类型,虽然它的可能取值像是整数,MALE的本质是0,但是下面是不建议的写法:
#include <stdio.h>
typedef enum Sex
{
MALE,
FEMALE,
SECRET
}Sex;
int main()
{
Sex x = 0;
return 0;
}
在C语言中,运行成功;
在C++中,会报错误:“int”类型的值不能用于初始化“Sex”类型的实体。
x是Sex类型,0是整型,这里不能随便转化,不能赋值。
应该更合理的赋值:
Sex x = MALE;
枚举的值默认是从0开始的,如果有初始化,则顺延向下递增1。
#include <stdio.h>
enum ENUM_A
{
X1,
Y1,
Z1 = 255,
A1,
B1,
};
int main()
{
enum ENUM_A enumA = Y1;
enum ENUM_A enumB = B1;
printf("%d %d\n", enumA,enumB);
}//1 257
3.2 枚举的优点
为什么使用枚举?
我们可以使用 #define 定义常量,为什么非要使用枚举?
#include <stdio.h>
#define MALE 4
#define FEMALE 5
#define SECRET 6
enum Sex
{
MALE = 4,
FEMALE,//5
SECRET//6
};
int main()
{
return 0;
}
枚举的优点:
1. 增加代码的可读性和可维护性
2. 和#define定义的标识符比较枚举有类型检查,更加严谨。
#define定义的标识符MALE没有类型,它只是一个完成替换的符号,MALE替换4,FEMALE替换成5,SECRET替换成6。
枚举类型的变量和枚举类型的可能取值都是有确切的类型的。
3. 防止了命名污染(放在大括号中封装起来——进行了保护),而#define定义的标识符是散放在外面。
4. 便于调试,#define定义的符号是不能进行调试的,也不便于调试,因为在调试的时候已经把这些替换了,意思是:
枚举类型定义的变量是可以进行调试的,因为不是替换,调试的代码和看到的代码是一回事。
5. 使用方便,一次可以定义多个常量;而#define定义的常量一次定义一个比较麻烦。
3.3 枚举的使用
#include <stdio.h>
enum Color
{
RED = 1,
GREEN = 2,
BLUE = 4
};
enum Color clr = GREEN;
clr = 5;
int main()
{
return 0;
}
只能拿枚举常量给枚举变量赋值,才不会出现类型的差异。
优化通讯录的实现:
#include "contact.h"
enum Oprion
{
EXIT,//0
ADD,
DEL,
SEARCH,
MODIFY,
SORT,
SHOW
};
void menu()
{
printf("************************************\n");
printf("****** 1.add 2.del ******\n");
printf("****** 3.search 4.modify ******\n");
printf("****** 5.sort 6.show ******\n");
printf("********** 0.exit ************\n");
printf("************************************\n");
}
int main()
{
int input = 0;
Contact con;
InitContact(&con);
do
{
menu();
printf("请选择:>");
scanf("%d", &input);
switch (input)
{
case ADD:
AddContact(&con);
break;
case DEL:
DeleteContact(&con);
break;
case SEARCH:
break;
case MODIFY:
break;
case SORT:
break;
case SHOW:
ShowContact(&con);
break;
case EXIT:
printf("退出通讯录\n");
break;
default:
printf("选择错误\n");
break;
}
} while (input);
return 0;
}
便于理解,增加代码的可读性,不用记住数字所代表的意思,也不用关心顺序问题,名字和功能相匹配。
4. 联合体(共用体)
联合体和共用体是一回事。
4.1 联合类型的定义
联合也是一种特殊的自定义类型;
这种类型定义的变量也包含一系列的成员,特征是这些成员公用同一块空间(所以联合也叫共用体)。
联合体的关键字是:union
//定义一个联合体类型:(联合类型的声明)
#include <stdio.h>
union Un
{
char c;
int i;
};
//看这个联合体大小:直观是5个字节;对齐是8个字节
int main()
{
//联合变量的定义:
union Un u;//用union Un这个联合体类型创建了一个联合体变量叫u
//计算这个变量的大小:
printf("%d\n", sizeof(u));//而实际运行结果:4
printf("%p\n", &u);//002EFB30
printf("%p\n", &(u.c));//002EFB30
printf("%p\n", &(u.i));//002EFB30
return 0;
}
是怎么做到这三个地址是一样的?
因为运行结果u占4个字节,而u,i,c的地址又一样所以:
发现i和c共用了第一个字节,所以联合体也叫共用体——成员会共用同一块空间。
当给c赋值的时候,就会把i的第一个字节改掉了;当给i赋值的时候,整个c就会被改掉。因为是同一块内存,改值的时候就会直接覆盖掉原来的值,所以联合体特殊在:对于联合体的成员在同一时间只能用一个。(用c的时候,i就不能用,因为c已经把i该改掉了;用i的时候,c就不能用)不像结构体、位段,它们的每个成员都有自己独立的空间。
联合体使用的场景:
有时候用它的i成员,有时候用它的c成员,不会同时用这两个成员。
比如设计一个学校的用户系统:
用户<描述>
学生<名字,年龄,身份>
老师<名字,年龄,职称>
则对用户就可以定义一个统一的类型;
对于同一个人只有一种选择:要么选身份要么选职称:
用联合体的方式定义则在一定程度上可以节省空间,同一时间只会有一个成员存在。
#include <stdio.h>
union type//联合体
{
//身份
//职称
};
UserInfo//结构体
{
char name[20];//20个字符
int age;
union type t;
}
若描述一个学生,则不能用联合体,因为一个学生的每一个信息都要有一个独立的空间来存储。
一个类型取值有很多种的时候可以用枚举类来描述,有多种选择但是每次选择只会选一个,共用同一块空间就用联合体类型。(相似的值只会用一个)
(是明确知道有时候用char成员,有时候用int成员,才会把它实现成一个char和int的联合体。
)
#include <stdio.h>
union Un
{
char c;
int i;
};
int main()
{
union Un u = { 0 };
u.c = 'w';
u.i = 0x11223344;//是十六进制的整型
return 0;
}
&u后发现u的值会随着调试而被修改掉。
4.2 联合的特点
联合体的特点:联合体也叫共用体,指的是每个成员互相之间都会共用一块空间,在同一时间点只能用其中一个成员,改变其中一个成员时会发现其他成员的值也改变了。
联合的成员是共用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为联
合至少得有能力保存最大的那个成员)
判断当前计算机的大小端存储:
对于int a = 1;
1的十六进制表示形式:0x 00 00 00 01,把这个十六进制存到内存时:
比较的第一个字节的内容,则拿出第一个字节:
&a找到a的起始地址,把a的类型强制类型转换为char*,从a起始地址用char*的指针向后访问(解引用)1字节的内容:
小端存储第一个字节是1;大端存储第一个字节是0——区分大小端
实现:
#include <stdio.h>
int cheak_sys()
{
int a = 1;
if (*(char*)&a == 1)
{
return 1;
}
else
{
return 0;
}
}
int main()
{
int ret = cheak_sys();
//规定:
//如果返回1,表示小端
//如果返回0,表示大端
if (1 == ret)
{
printf("小端\n");
}
else
{
printf("大端\n");
}
return 0;
}#include <stdio.h>
int cheak_sys()
{
int a = 1;
return (*(char*)&a);
}
int main()
{
int ret = cheak_sys();
if (1 == ret)
{
printf("小端\n");
}
else
{
printf("大端\n");
}
return 0;
}//小端
简化:
#include <stdio.h>
int cheak_sys()
{
int a = 1;
return (*(char*)&a);
}
int main()
{
int ret = cheak_sys();
if (1 == ret)
{
printf("小端\n");
}
else
{
printf("大端\n");
}
return 0;
}//小端
a是4个字节,给a放1,即使用这4个字节,而往外拿用时只拿第一个字节,即有时候在使用1个字节有时候在使用4个字节,即有时候用char,有时候用int——这种场景就可以把二者放在一个联合体中:
当给a赋值放1时,使用的是4个字节;当取出第一个字节使用的是第1个字节。
#include <stdio.h>
int cheak_sys()
{
union Un
{
char c;
int i;
}u;
u.i = 1;//这次赋值是对4字节整体赋值
//放i时c中已经有值了,所以u.c不用赋值了
return u.c;
}
int main()
{
int ret = cheak_sys();
if (1 == ret)
{
printf("小端\n");
}
else
{
printf("大端\n");
}
return 0;
}
对于u.i = 1的理解:给4个字节的变量赋值,要存东西才能看出并判断大小端。
简化代码:
union Un类型只用一次,以后就不用了,则可以该成匿名。(以后这个类型想用也用不了)直接用u类型创建了u这个变量:
#include <stdio.h>
int cheak_sys()
{
union
{
char c;
int i;
}u;
u.i = 1;
return u.c;
}
int main()
{
int ret = cheak_sys();
if (1 == ret)
{
printf("小端\n");
}
else
{
printf("大端\n");
}
return 0;
}
这里用了匿名联合体类型。
4.3 联合大小的计算
1、联合的大小至少是最大成员的大小。
2、当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。
最大对齐数:每个成员都有自己的对齐数,所有成员的对齐数中的最大值就是最大对齐数。与结构体中最大对齐数的计算是一样的。
联合体中也存在适当的对齐。
#include <stdio.h>
union Un1
{
char c[5];//5个元素,5个字节
int i;//4个字节
//因为联合体共用同一块空间,则union Un1这样一个对象应该至少是5个字节
//此时大小是5不是最大对齐数的整数倍
//c数组每个元素是char,所以它的对齐数是按照char来算的,所以该成员自身大小是1,默认对齐数是8,所以该成员的对齐数是1
//对于i:i成员的自身大小是4,默认编译器对齐数是8,所以该成员的对齐数是4
//即union Un1联合体所有成员中最大对齐数是4,
//因为联合体的总大小必须是所有成员最大对齐数的整数倍
//5不是4的倍数,则浪费3字节,使联合体总大小为8
};
int main()
{
printf("%d\n", sizeof(union Un1));//计算union Un1这个联合体类型的大小
return 0;
}
#include <stdio.h>
union Un2
{
short c[7];//14个字节,对齐数是2
int i;//4个字节,对齐数是4
//则联合的大小至少是14字节
//成员中最大对齐数是4,即联合体的总大小必须是4的整数倍
//16
};
int main()
{
printf("%d\n", sizeof(union Un2));//16
return 0;
}
注意:
数组中是按照每个元素的大小算对齐数,对于整个数组是不能算对齐数的,是看数组内部元素。数组本身是不对齐的只有数组这种成员放在结构体,联合体中才会对齐,这不是数组在对齐,算是联合体,结构体在对齐。数组是自定义类型但数组本身创建好之后是不对齐的,其大小就是那个创建的大小。
寄个知识小卡片:
C++相对于C语言对语法的检测更加严格——代码在C中可能会运行成功但是在C++中就会报错。
%x——打印十六进制,%02x是打印2位十六进制。