C语言进阶之路:数据类型

231 阅读13分钟

C语言之进阶:数据类型

1.前言

通过一段时间的学习后,现在也是来到了C语言进阶之路了,接下来我将会为大家讲解数据类型的更深层的东西,而不是过去那种表层的皮毛了,像数据类型的种类之类,还有字节数这种东西,而是像数据是如何存储的?还有数据的存储方式这种更底层的东西

2.数据类型

1.整型家族

整型家族我们知道有这些:char(1byte),short(2byte),int(4byte),long(32位系统:4byte/64位系统:8byte),long long(C99)(8byte)

这里的字节数就不在进行证明了,而先讲讲为什么char归纳于整型家族中?

原因如下:字符的本质是ASCII值,是整型,所以划分到整型家族

还有就是为什么long有两种字节大小?

原因如下:long的字节在C语言中被明确规定大于或等于int的字节,所以当long的字节为4/8时,就可以达到这个要求

当然,对于整型家族我们还能够进行细分

char分为signed char,unsigned char short分为signed short,unsigned short int分为signed int,unsigned int long分为signed long,unsigned long long long long分为signed long long,unsigned long long

正常我们的这些直接写出来的都是表示signed,例如:我们写int,实际上是signed int,这些在C语言中都有着明确的规定

但有一个例外,就是char

char在C语言中没有明确地被定义为signed char或是unsigned char,这最终取决于编译器的实现,但是我们常见的编译器大部分都是指signed char

而signed是指有符号,unsigned是指无符号,这里的符号就是我们所说的+ - 号,这种有符号无符号的存在也是有意义的

因为在生活中,有些东西根本不存在所谓的负,例如:身高,体重......而且代码的存在就是为了使生活更加便捷,所以为了使代码能够更加贴近生活,于是有符号无符号诞生了

因为有符号的数据类型的最高位是符号位,而无符号的数据类型因为不存在符号位的说法,所以其所有的位都是有效位

因此这两种类型的范围其实存在着差异,例如:

signed char的范围是-128127,而unsigned char的范围是0255

这里就能够明显的看出来有符号的数据类型和无符号的数据类型的范围不同

事实上,这些类型通常是编译器看变量的一种方式

int main()
{
    char a=50000;
    printf("%d\n",a);
    return 0;
}

这段代码运行的结果如下:

屏幕截图 2024-10-27 004917.png

这里就有三个东西值得我们一谈

首先

我们可以知道整形在放入内存的一开始是以默认的int存储,这个只需要我们将鼠标点到这个数值上就可以知道了

屏幕截图 2024-10-27 005750.png

然后

我们知道这里char的范围是-128~127,但我们可以看到这里的a被赋值了50000,所以这里就存在截断的现象,即当一个高于字节范围的数据要放进去的时候,会只留下其范围的字节量,多于的量就会被舍去,这里很明显就存在截断的现象

最后

我们能够知道上面发生了截断,但是,最后又是怎么输出的呢?这里就可以提到高位提升

int main()
{
    char a=50000;
    //最开始的50000的二进制
    //00000000000000001100001101010000
    //发生截断后
    //01010000-(正数的)原码,补码,反码相同
    //由于打印%d,此时编译器是以32位的默认视角来看这段数字的
    //由于此时只剩8位,所以会发生整形提升
    //而整形提升是用最高位符号位进行补
    //但假如是无符号位的就是高位补0
    //最终得到是000000000000000001010000
    //转化为十进制就是我们的80
    printf("%d\n",a);
    return 0;
}

当然这个例子不足以来向我们证明高位提升的存在,所以在这里再举一个例子

int main()
{
    char a=403;
    printf("%d\n",a);
    return 0;
}

输出的结果:

屏幕截图 2024-10-27 011033.png

这里我们也对它进行分析

int main()
{
    char a=403;
    //最开始403的二进制
    //00000000000000000000000110010011
    //截断后
    //10010011-补码
    //整型提升
    //11111111111111111111111110010011-补码
    //10000000000000000000000001101100-反码
    //10000000000000000000000001101101-原码
    //十进制:-109
    printf("%d\n",a);
    return 0;
}

这里我们就大概能看到高位提升的存在了,除此之外,我们也能切实感受到数据类型是编译器看这些数据的一种方式

有了上面的一些讲解,我们便可以来看看下面的这两个问题

int main()
{
    //判断一下下面的输出的结果
    char a=-1;
    signed char b=-1;
    unsigned char c=-1;
    printf("%d %d %d\n",a,b,c);
    return 0;
}

输出结果:

屏幕截图 2024-10-27 012454.png

这里的我们可以看见a和b的值相同,这也一定程度上证明了上述的我们常见的编译器大部分都是char指的就是signed char

当然这里这个不是重点,重点是c的值,下面我就跟大家解释一遍:

int main()
{
    //判断一下下面的输出的结果
    char a=-1;
    signed char b=-1;
    unsigned char c=-1;
    //c的二进制
    //10000000000000000000000000000001-原码
    //11111111111111111111111111111110-反码
    //11111111111111111111111111111111-补码
    //截断后
    //11111111
    //高位提升
    //00000000000000000000000011111111
    //十进制:255
    printf("%d %d %d\n",a,b,c);
    return 0;
}

所以,这里的c就是255的原因,关键还是在于其范围大于等于0,还有下面这个

int main()
{
    unsigned int i;
    for(i=8;i>=0;i--)
    {
        printf("hello world\n");
    }
    return 0;
}

你或许会认为这个代码打印完9次**"hello world"**就结束了,但事实真的如此吗?

我们将代码运行后得到的结果是:

屏幕截图 2024-10-27 013752.png 没错,进入死循环了,这是为什么呢?

通过分析,我们知道, i 是无符号的,所以 i 一定会大于等于0,所以,当i= - 1时,编译器会自动将其转化为4294967295,然后继续输出**"hello world"**,所以就有了死循环,而导致这个死循环的关键就是unsigned类型的变量都是大于等于0的

2.浮点型家族

浮点型家族:float(4byte),double(8byte)

当我们正常写浮点数时,其默认为double,即使前面用了float定义这个变量,若想要其就是float类型,可以在其后面加上f,这就是在告诉编译器这个变量就是float

如下:

屏幕截图 2024-10-27 014525.png

屏幕截图 2024-10-27 014545.png

这里就能很明显的知道这个道理,至于其在内存中的存放,我们放到下面再讲

3.构造类型

构造类型:数组类型,结构体类型(struct),枚举类型(enum),联合类型(union),这些东西现在不讲,以后会把这些另外放到一起讲

3.字节的存储方式

字节的在内存中的存储分为了两种:

  • 大端字节序存储:高位字节序放在低地址,低位字节序放在高地址,这就是大端字节序存储
  • 小端字节序存储:低位字节序放在低地址,高位字节序放在高地址,这就是小端字节序存储

这两种存储形式会跟编译器,电脑的硬件有关,不同的编译器和电脑硬件会导致这两者的不同

我这里就以vs为例,来看看其内存中的存放方式

int main()
{
    int a=3;
    return 0;
}

屏幕截图 2024-10-27 015609.png

在这里,我们就能看到这个a的低位被放到低地址,高位放到了高地址,所以这是小端字节序存储,当然在这里还得提一嘴,就是虽然在这里看这些数据是在内存中以十六进制存放的,但本质上还是二进制,这里只是为了让程序员更好的看,所以展示的十六进制

当然,除了这种通过调试的方式来看字节的存储方式外,我们也可以通过代码的方式来得到

int main()
{
    int i=1;
    int* p=&i;
    //我们知道假如1按的时小端字节序存储,则p取出来i的地址的低位是1
    //相反的话,低位是0
    //因为char*只能访问一个字节,而为了只访问低位
    //所以使用char*对p进行强制类型转化
    if(*(char*)p)
    {
        printf("小端\n");
    }
    else
    {
        printf("大端\n");
    }
    return 0;
}

这种代码的方式适用于所有的电脑和编译器上,能够一下子就测出字节的存储方式

4.浮点型在内存中的存储方式

在讲浮点型在内存中的存储方式前,我们可以先来看一下这下面的这个问题:

int main()
{
    //请问这几次的输出结果是什么?
    int a = 9;
    float* pa = (float*)&a;
    printf("%d\n", a);
    printf("%f\n", *pa);
    *pa = 9.0;
    printf("%d\n", a);
    printf("%f\n", *pa);
    return 0;
}

我们对于浮点型在内存中的存储方式可能会有种猜想,那就是跟整型在内存中的存储方式相同,所以那么对于这里的输出结果,有人就会认为是:

9

9.0

9

9.0

但事实真的如此吗?

通过编译器运行这串代码,我们知道了正确结果:

屏幕截图 2024-10-27 095738.png 跟我们一开始的猜想,区别有点大,所以显然浮点型在内存中的存储方式跟整型的不一样,那么浮点型在内存中究竟是如何存储的呢?

根据IEEE(电气电子工程师学会) 754规定了所有的浮点数v都能写成:v=(-1)^S*M*2*E

IEEE 754还规定了:

对于32位浮点数(单精度浮点数),最高的1位是符号位S,接着的8位是指数E,剩下的23位位有效数字M

屏幕截图 2024-10-27 100536.png 对于64位的浮点数,最高的1位是符号位S,接着的11位是指数E,剩下的52位为有效数字M

屏幕截图 2024-10-27 100627.png 这两张图片也能很好的帮助我们进行理解

有人在此可能会有问题:E的位也太少了吧,真的放得了数据吗?

这个问题其实很简单:因为E为指数位,其最大是127,我们已知2^32为4294967296,那么这个2^127毋庸置疑会更大,正常放的数据肯定是够的,所以这种问题无需担心

除此之外,还规定了一些关于M和E的

因为1<=M<2,就是说M可以写成1.xxxxxxx的形式,其中xxxxxx表示小数部分

所以IEEE 754规定:

在计算机内部保存M时,默认这个数的第一位总是1,因此这个1可以被舍去,只保留小数点部分,等到读取这个浮点数的时候再将这个1加上去,这样就可以节省一个有效数字,这样就可以保存更多更大的数字

关于E的规定就相对比较复杂了

1.E不全为0或不全为1

这时候,浮点数就采用E-127(或1023,根据浮点数的类型是单精度还是双精度),得到真实值,再将有效数字M前加上第一位的1

原因:因为E我们知道是存在正负的,但在这里面的数据存储中不存在最高位是符号位之类的言语,所以为了能够存储E的正负值,所以就有了+127进行存储,之后读取的时候,再将E-127即可得到真实值,不是-128的原因大概是-128的补码是10000000,这不好对其进行计算,所以选择了-127

注意:当后面的位没有被填上时,会自动向后自动补0,直到23位(或52位)全部被填满

2.E全为0

这时浮点数的指数E等于1-127(或者1-1023,根据浮点数的类型是单精度还是双精度),有效数字M不再加上第一位的1,而是直接还原为0.xxxxxx,这是为了表示+-0,以及表示接近于0的很小的数字

原因:因为当E为-126时,此时的浮点数v=(-1)^S*M/(2^126),2^32已经是40多亿了,更别提2^126,所以此时的浮点数v表示+-0,以及接近于0的很小的数字

3.E全为1

表示一个正负无穷大的数(正负有符号位S决定)

原因:跟上面类似,这就不在阐述

那么现在我们在回过头来看那个问题:

int main()
{
    //请问这几次的输出结果是什么?
    int a = 9;
    //二进制:00000000000000000000000000001001
    float* pa = (float*)&a;
    //二进制:0 00000000 00000000000000000001001
    //      S     E            M
    //因为E为全0类型,所以E=-126
    //所以输出的结果为0.000000
    printf("%d\n", a);
    printf("%f\n", *pa);
    *pa = 9.0;
    //*pa = 9.0
    //其二进制:0 10000010 00100000000000000000000
    //       S     E              M
    //那么以a的视角来看这个数字
    //其十进制就是:1091567616
    //这也就是a最后输出的值了
    printf("%d\n", a);
    printf("%f\n", *pa);
    return 0;
}

因此,我们也了解了浮点型在内存中的存储方式

总结

我们已经在C语言进阶之路已经成功迈了一步,我们现在学会了整型家族的一些深层内容,以及字节序存储的种类,还有浮点型在内存中存储方式,希望我们接下来能够一起走完C语言的进阶之路!!!