写点什么

换个角度带你学 C 语言的基本数据类型

  • 2022 年 6 月 01 日
  • 本文字数:3145 字

    阅读完需:约 10 分钟

本文分享自华为云社区《从深入理解底层的角度学习C语言之基本数据类型》,作者: breakDawn 。


C 语言的基本数据类型,大家从学生时代就开始学习了,但是又有多少人会试图从底层的角度去学习呢?这篇文章会用一问一答的形式,慢慢解析相关的内容和困惑。


  1. 数据类型位数和符号

  2. 数据类型转换

  3. 浮点数

数据类型位数和符号


Q: C 里的 signed 和 unsigned 类型的区别是什么?


A:拿 unsigned char 无符号 char 和 signed char 有符号 char 举例(因为他们都是 1 字节,比较好举例子)

假设某个局部变量 a,内存里存的都是 0xff(即二进制 11111111)


执行 printf("%d",a)时, 输出的是 255,还是-1 呢?


如果 a 是无符号,那就是 255。


如果 a 是有符号,那就是-1。


Q:为什么有符号的 0xff 输出的是-1?


A:这个就是补码的概念。


正数的补码就是其本身


负数的补码是在其原码的基础上, 符号位不变, 其余各位取反, 最后+1. (即在反码的基础上+1)


  • 补码的计算方式:如果是-1,则负号就是首位的“1”, 而“-1”里的 1 作为二进制是 0000001,取反+1,得到 1111111, 和首位 1 拼接,变成了 11111111.

  • 进行 printf 打印时,C 语言通过变量类型,确认 11111111 的首位是符号位,于是通过补码的反向计算,得到实际真值为-1。

  • 如果是无符号,则 C 语言通过变量类型,确认 11111111 的首位不是符号位,不需要反向计算,于是直接输出 255。


原码、反码、补码对于+1 和-1 的表示如下


[+1] = [00000001]原 = [00000001]反 = [00000001]补


[-1] = [10000001]原 = [11111110]反 = [11111111]补


Q: 已知正负数默认都是补码的形式,为什么不能用原码表示数字呢?即只用第一个标识符号位,后面 7 位就是代表真实绝对值


A:计算机 CPU 做计算时,无法区别符号位,只会死板的将 8 位数字进行加法计算。


假设做减法,就和下面那样


1 - 1 = 1 + (-1) = [00000001]原 + [10000001]原 = [10000010]原 = -2


可以看到符号位的信息会误导减法的计算。


Q: 那为什么不用能反码呢


A:因为反码对于 0 的表示有两种情况,11111111 可以代表-0,而 00000000 代表+0,相当于浪费了。

而补码不存在这个情况。11111111 代表-1,00000000 代表 0。


Q: 为什么要有补码?补码有什么好处?


A:当计算机执行 1 - 1 时, 希望都是用加法的动作来做,且不希望做 if-else 判断,根据符号位去判断正负再做加减,对计算机的消耗是很大的。


使用补码的机制,则可以将 1-1 转成变成 1+(-1)


那么-1 就是补码 0xff,和 0x01 相加,变成了 0,即不需要做真正的减法即可


Q: 刚才提到 CPU 希望都是位加法,不肯做减法,为什么?


A:因为 CPU 的减、乘、除都是基于加法、移位等操作实现的。


加法过程依赖 CPU 的 ALU 累加器,累加器背后的电路是数字电路异或门和与门的组合。



Q: 为什么补码表示的情况下,范围是-128 到 127?为什么补码会比原码和反码多一位?


A:就是上面提到的 0 的问题。原码的 10000000、00000000 都表示 0,补码的 11111111 和 00000000 都表示 0,而补码只有 1 个 0 的表示


同时补码有一个 100000000, 把后 7 位取反+1,等同于-128。


原码、反码、补码知识详细讲解(此作者是我找到的讲的最细最明白的一个)


Q: 计算机在 CPU 做计算时,怎么识别是无符号还是有符号?


A:CPU 所处理的寄存器、内存中的数本身无符号信息。CPU 做加减法时会一起做无符号数的进位/有符号数的溢出标志,并不专门对待有符号数和无符号数。


有无符号的区别是只属于(中)高级语言的概念,反映到机器语言上,是跟运算及与其结果相关的指令上的区别,而不会反映到 CPU 所处理的数本身。


即 CPU 处理时,统一用加法处理,但是否要做求补等操作,取决于提供的运算指令。


Q: C 语言的 char 是 signed char 还是 unsigned char?


A:

  • 当你定义为 char 时, 可能是 signed char,也可能是 unsigned char。

    这个取决于你编译器的实现。

    -funsigned-char : 设置为 unsigned char

    -fno-signed-char : 设置为 非 signed char

    -fsigned-char : 设置为 signed char

    -fno-unsigned-char : 设置为 非 unsigned char


Q: int 有可能像 char 一样,即可能是 signed int 也可能是 unsigned int 吗?


A:int 一定是有符号 int。不会因为编译器不同而不同。


Q: 为什么 char 可以区分有符号或者无符号,但是 int 只能默认为 signed int ?


A:个人理解和应用场景有关,char 不一定会参与计算,而 int 大部分情况下都是有符号计算,因此默认为 signed int 比较好。


Q: ILP32、LP64、LLP64 分别是什么?


A:指的是这个操作系统中,有哪些类型分别是多少位的意思。


I 指 int


L 指 long


LL 指 long long


P 指 point 指针


32 和 64 就是分别指 32 位和 64 位。


  • 32 位系统一定是 ILP32 模型

  • 64 位系统中,unix 一般是 LP64,而 windows 则是 LLP64

    即 linux 中,long 是 64 位, 而在 windows 中,long 是 32 位,而只有 long long 是 64 位


Q: 为什么 windos 要用 LLP64 这么奇怪的模型?这个模型里, long 是 32 位,long long 才是 64 位。


A:来自知乎陈硕大佬的回答:


我猜,是因为 Windows API 从 16-bit 升级到 32-bit 发生得太晚了——大约是随 1995 年发布的 Windows 95 而普及 。


虽然之前有 Windows NT 3.x 和 Win32s,但似乎比较小众。


而 Unix 从 16-bit 升级到 32-bit 发生在 1980 年前后,当时运行在 VAX 上的 Unix/32V 和 3BSD 都是 32-bit 的。


造成的结果是,两边的程序对 short/int/long 的长度形成了不同的习惯认知:


Unix 程序习惯了 int 是 32-bit,而 long 不一定只有 32-bit。Windows/DOS 习惯了 long 是 32-bit,而 int 有可能是 16-bit 或 32-bit,因为刚刚从 16-bit 升级上来嘛。


当往 64-bit 升级的时候,如果把 Windows 的 long 升级到 64-bit,会破坏原来很多程序的假设,

只好用个新的类型来表示 64-bit 整数了。反正 LONGLONG 在 32-bit 程序中也是 64-bit 整数,干脆用它好了。


详细的数据类型展示:



PS: 从上面可以看到 java 虚拟机的一个优势,就是对开发者而言,屏蔽了各不同系统情况下的数据位数。


Q: 那么又有个问题,java 虚拟机如何实现不同平台可以跑相同的 java 代码,不用担心底层数据类型的?


A:如图所示,class 字节码都是同一份,但是不同的系统,会有不同的虚拟机解释器实现,在解释器实现里处理了不同的数据类型位数情况。

数据类型转换


Q: C 里的隐式类型转换有什么规律?


A:

  • 占用字节数少的类型,向占用字节数多的类型转换;

    int->long

  • 占用字节数相同情况下,有符号向无符号转换;

    int->unsigned int

  • 整数类型向浮点类型转换;

    int -> double

  • 单精度向双精度转换;

    float->double



Q: 下面这个例子输出多少,为什么?


A:


void Test()
{
int a = -1;
unsigned b = 10;
if (a > b)
{
printf("a is greater than b.\n");
}
else
{
printf("a is less than or equal b.\n");
}
}复制
复制代码


输出 a>b 即 a is greater than


因为 a=-1,存储的二进制是 11111111, 强转成 unsigned 时,二进制没有变,但是对编译器而言表示的大小变成了 255 了。

浮点数


float、double、long double 的比特数、有效位数、数值范围如下:



Q: 下面这个代码输出什么?



#include <stdio.h>
int main(void)
{
float a = 9.87654321;
float b = 9.87654322;
if(a > b)
{
printf("a > b\n");
}
else if(a == b)
{
printf("a == b\n");
}
else
{
printf("a < b\n");
}
return 0;
}复制
复制代码


A:输出"a=b", 因为 float 最多 7 位有效小数点位数。


Q: 32 位 float,1bit 为符号位,23bit 为位数,8bit 为指数, 这 3 个划分是如何得到 float 的有效位数以及数值范围的?


A:IEEE754 标准理解。


【计算机组成原理】IEEE754标准


有人问为什么要学习这个?


对于高精度场景下的浮点计算,掌握 IEEE754 的标准很重要,否则无法理解高精度场景时计算过程出现的各种问题, 特别是一些金融场景,对于小数点后面的数字会特别敏感。


Q:java 的 BigDecimal 类可以表示任意精度,原理是啥?


A:BigDecimal 的原理很简单,就是将小数扩大 N 倍,转成整数后再进行计算,同时结合指数,得出没有精度损失的结果。


以 long 型的 intCompact 和 scale 来存储精确的值。


点击关注,第一时间了解华为云新鲜技术~

发布于: 刚刚阅读数: 5
用户头像

提供全面深入的云计算技术干货 2020.07.14 加入

华为云开发者社区,提供全面深入的云计算前景分析、丰富的技术干货、程序样例,分享华为云前沿资讯动态,方便开发者快速成长与发展,欢迎提问、互动,多方位了解云计算! 传送门:https://bbs.huaweicloud.com/

评论

发布
暂无评论
换个角度带你学C语言的基本数据类型_开发_华为云开发者社区_InfoQ写作社区