C语言学习之路
第一章 初识C语言
第二章 变量
第三章 常量
第四章 字符串与转义字符
第五章 数组
第六章 操作符
第七章 指针
第八章 结构体
第九章 控制语句之条件语句
第十章 控制语句之循环语句
第十一章 控制语句之转向语句
第十二章 函数基础
第十三章 函数进阶(一)(嵌套使用与链式访问)
第十四章 函数进阶(二)(函数递归)
第十五章 数组进阶
第十六章 操作符(详解及注意事项)
第十七章 指针进阶(1)
第十八章 指针进阶(2)
第十九章 指针进阶(3)
第二十章 指针进阶(4)
第二十一章 数据的存储(秒懂原、反、补码)
第二十二章 指针和数组笔试题详解(1)
第二十三章 指针和数组笔试题详解(2)
第二十四章 字符串函数(1)
第二十五章 字符串函数(2)
第二十六章 内存函数
第二十七章 自定义数据类型之结构体进阶(1)
第二十八章 自定义数据类型之结构体进阶(2)
文章目录
前言
通过上一章节的学习,我们了解到了结构体的声明初始化等基本操作,同时还弄懂了结构体中的难题——内存对齐。今天,我们将继续对自定义数据类型进行深刻讲解,那么本章讲解的数据类型包括:位段、枚举、联合体。
一、位段
1、位段的语法:
可能很多人都没有听说过这个概念。所以在解释什么是位段之前,我们先来了解一下位段的语法,然后通过具体的例子来解释位段。
struct A
{
int _a:2;
int _b:5;
int _c:10;
int _d:30;
};
我们发现这个位段的声明和结构体的声明是非常相似的。唯一的不同出现在了中间的成员变量的部分,数据类型 数据名 :(该数据所占的bit位);
。
除此之外,我们需要明白的是,并不是所有数据类型都能用来声明位段。
位段的成员必须是int、unsigned int 、 signed int或者是char
。
2、位段的内存分配:
结构体的内存在分配时,需要遵循内存对齐的原则,那么位段的内存分配又需要满足什么规则呢?
- 位段的成员可以是
int、unsigned int、signed int、char
(属于整型家族的数据类型)。 - 位段在开辟空间时是以
int
或者char
为基本单位来开辟的。 - 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。
我们以下面的例子来具体讲解位段的内存分配:
//声明一个位段:
struct S
{
char a:3;
char b:4;
char c:5;
char d:4;
};
//利用位段来创建一个变量:
struct S s= {0};
s.a=10;
s.b=12;
s.c=3;
s.d=4;
我们根据上述的规则画出了位段的存储图示。
这个位段中的成员变量是char类型,所以每次开辟的最小单位都是一个字节。因为计算机在内部存储的时候,存储的都是一个数据的二进制补码,而正数的源码、反码和补码是相同的。基于此,我们写出了a的二进制为:1010。这个二进制数共占有4个比特位。但是在进行位段的声明时,我们仅仅分配给a了三个比特位。所以,我们仅将a数据的三位二进制位存储进空间的右边。同理,我们存储了b数据。但是,当我们存储c数据的时候,出现了一些问题,因为在声明的过程中,我们分配给c了5个比特位。但是这一个字节在存储了a和B后,仅仅剩余1个比特位,那么这个位置是使用还是舍弃呢?目前还没有统一的标准,因此我们这里先假设舍弃这个剩余的位置(VS中的确如此)。
既然这个字节空间无法存储c,那么我们就需要再次开辟一段空间来继续存储,前面也讲到了,我们在创建的时候,每次开辟的都是char或者int的大小,由于这里的成员是char,所以每次都是开辟char类型的大小(即一个字节)。因此,我们开辟出第二块空间。接着按照刚才的逻辑继续存储,直到存储完毕。最终我们发现这个结构体仅仅使用了3个字节。
经过验证,我们即可验证我们的分析。
3、位段的跨平台问题
我们发现,位段的使用使得空间的利用率达到了最高,其对内存的占用远远小于结构体创建的变量。但是任何东西都有好坏两面。由于C语言标准中对于位段内存的占用规则并没有详细的说明,这就导致不同的平台处理位段的方式不相同。
在刚刚讲解内存分配的时候,我们以刚才的那个题目为例,当我们开辟一个char类型的内存空间时(1个字节),这个时候就会有8个比特位来存储具体的数据。但是,我们将这个数据放入的时候,是从右向左存放数据,还是从左向右存放数据,多余的空间不够存储下一个数据时,是会舍弃,还是继续使用等等细碎的问题均没有一个明确的规定。
由此,我们就能总结一下位段可能造成的跨平台问题:
- int位段被当成有符号数还是无符号数是不确定的。
- 位段中最大位的数目是不确定的,16位机器最大是16,32位机器最大32。
- 位段中的成员在内存中是从左向右分配,还是从右向左分配,标准并未定义。
- 当一个结构体包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余还是利用,这是不确定的。
跟结构体相比,位段可以达到同样的效果,但是位段可以很好的节省空间,但是有跨平台的问题存在。
二、枚举:
1、什么是枚举?
提到枚举,我们可能首先想到的是枚举法,即将所有可能一一列举,这里也是相同的含义,即将所有的可能取值一一列举。比如一周的7天,就能够从周一到周日一一列举出来。
2、枚举类型的定义
enum Day
{
Mon,
Tues,
Wed,
Thur,
Fri,
Sat,
Sun
};
上面就是枚举的语法,与结构体在形式上有一些类似,但是在大括号中的成员变量之间,结构体是利用;
将各个成员变量分割开的,而枚举中的成员变量是利用,
隔开的。
二者的共同点是,在声明结束后,都要加上一个分号。
{}内的内容是枚举类型的可能取值,也叫枚举常量
。而这些枚举常量也是有默认值的,默认从0开始,由上到下依次递增1。当然,这个默认取值是可以进行更改的,倘若我们只更改其中的一个枚举常量的值,那么这个值下面的枚举常量值会再次基础上依次递增1。
注意!!!
枚举常量是常量,所以无法在声明以外的部分进行修改,枚举常量只能在声明的过程中对其进行修改。
我们以下面的代码为例:
enum Sex
{
MALE,
FEMALE,
SECRET
};
enum Color
{
red = 1,
green = 5,
blue = 2
};
enum Day
{
mon = 1,
tue,
wed,
thur = 5,
fri,
sat,
sun
};
int main()
{
printf("%d\n", MALE);
printf("%d\n", FEMALE);
printf("%d\n", SECRET);
printf("\n");
printf("%d\n", red);
printf("%d\n", green);
printf("%d\n", blue);
printf("\n");
printf("%d\n", mon);
printf("%d\n", tue);
printf("%d\n", wed);
printf("%d\n", thur);
printf("%d\n", fri);
printf("%d\n", sat);
printf("%d\n", sun);
return 0;
}
3、枚举常量的使用
我们发现枚举常量在定义的时候,非常类似于结构体。那么我们完全可以把enum Color
想象成一个自定义的变量,只不过这个变量所有的取值都是有限的。
enum Color
{
RED,
BLUE,
GREEN
};
int main()
{
enum Color col = RED;
return 0;
}
枚举变量的创建和初始化如下:
enum 枚举名 变量名 = 某个枚举常量
4、枚举常量的优点
- 增加代码的可读性和可维护性
- 和#define定义的标识符比较枚举有类型检查,更加严谨。
- 防止了命名污染(封装)
- 便于调试
- 使用方便,一次可以定义多个常量
三、联合体(共用体)
1、联合的定义:
我们发现联合体的声明和结构体的声明高度类似,仅仅是关键字不同。
union UN
{
int a;
char b;
double c;
};
2、什么是联合体?
顾名思义,联合体又称共用体,共用的是同一块内存空间。怎么理解呢?我们可以创建一个联合体的变量,然后关注一下其成员的内存地址:
union UN
{
int a;
char b;
double c;
};
int main()
{
union UN u;
printf("%p\n",&(u.a));
printf("%p\n",&(u.b));
printf("%p\n",&(u.c));
return 0;
}
我们发现,三个成员变量的地址是相同的,说明三者内存空间是从同一个位置开始开辟的,只是长度不同。那么联合体在内存中又是如何存储的呢?
3、联合体的内部存储
- 联合的大小至少是最大成员的大小。
- 当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。
//例子1:
#include<stdio.h>
union Un1
{
char c[5];
int i;
};
int main()
{
union Un1 u;
printf("%d", sizeof(u));
return 0;
}
//例子2:
union Un
{
char c;
int i;
};
int main()
{
union Un u;
printf("%d",sizeof(u));
return 0;
}
4、联合体使用原理:
我们以下面这个联合体为例子:
union Un
{
char c;
int i;
};
这个联合体的内存大小是4个字节。其中这两个成员变量的地址是相同的,不同的仅仅是所占的内存和访问的空间大小。
我们以下面的例子为例:
union Un
{
char c;
int i;
};
int main()
{
union Un u1;
u1.i=0x11223344;
return 0;
}
上述的代码在内存中的存储形式如下:
当我们再添加一行代码:
u1.c=0;
那么就会对44所占的字节空间进行修改。得到如下的结果:
那么联合体有什么用呢?
5、联合体的用途
我们以一道题为例,曾经我们学习过大小端的概念,那么如何判断大小端呢?
我们利于图示的方式讲解一下大体的思路:
代码实现:
#include<stdio.h>
union UN
{
int a;
char b;
};
int main()
{
union UN u;
u.a=1;
printf("%d",(int)u.b);
return 0;
}
因为我们的VS编译器是小端存储模式,所以最终的输出结果应该是1。我们运行上述代码:
最终的运行结果恰恰是我们所预料的,即验证了我们的方法。
以上仅仅是联合体的一个简单应用。