c++ 输出二进制_【C语言学习系列二】 C的基本数据类型

0x00 基本数据类型

b95ced6ed6c832e4bcaf07f8b6883df1.png

基本类型字节范围
char1byte
short2bytes0~32767(0~0x7fff)-32768~-1(0x8000~0xffff)
int4bytes0~2147483647(0~0x7fffffff)-2147483648~-1(0x80000000~0xffffffff)
unsigned int4bytes0~4294967295(0~0xffffffff)
long int8bytes正: 0~0x7fffffffffffffff负: 0x8000000000000000~0xffffffffffffffff
unsigned long int8bytes0~0xffffffffffffffff
float4bytes|3.4e-38|~|3.4e+38|
double8bytes|1.7e-308|~|1.7e+308|

下面我们用一个示例代码打印一下这些基本的数据类型,代码如下:

// Function: 打印基本数据类型的字节数#includeint main(){    int i = 0;    printf("The size of char is %d bytes\n",sizeof(char));    printf("The size of short is %d bytes\n",sizeof(short));    printf("The size of int is %d bytes\n",sizeof(int));    printf("The size of long int is %d bytes\n",sizeof(long int));    printf("The size of float is %d bytes\n",sizeof(float));    printf("The size of double is %d bytes\n",sizeof(double));    return 0;}

运行结果如下:

The size of char is 1 bytesThe size of short is 2 bytesThe size of int is 4 bytesThe size of long int is 4 bytesThe size of float is 4 bytesThe size of double is 8 bytes

需要说明的是上述程序的运行环境为32位,所以long的字节数是4bytes,而在64位环境下则为8bytes

1.有符号和无符号
       对于有符号和无符号数据类型这块,我想首先用一个问题来引出这部分的重点内容——计算机怎么知道数值是有符号的还是无符号的呢?众所周知,数值在计算机内部的存储方式是以0 1方式进行存储的,比如数值4在16位环境下的存储形式为0000 0000 0000 0100,很简单就能知道这是一个正数4,那么如果存储的是-4呢?(大家思考一下负数在内存的存储方式),要知道-4在内存是以它的补码形式进行存储的,首先来看-4从原码到补码的转变:1 000 0100 (原码)⇥ 1 111 1011(反码)⇥ 1 111 1100 (补码),那么-4在内存的存储形式为0xfc,下面我们可以看一下32位环境下的无符号和有符号定义下-44的存储形式,是否和我们推导的一致呢?如下图:

e1c0c998a388a7f5acb58811ea400671.png

      从上图可以看出对于有符号数值是以数值的补码形式进行存储的,但是对于无符号下的负数是什么原因呢?这是因为你前面加上符号之后,相当于把变量提升为有符号类型,所以和正常的有符号下的存储是一样的。
知识点一:无论是整数和负数,在计算机内部都是以它们的补码的形式进行存储的。
     这里通过一个简单的程序引出下面的问题,程序如下:
#includeint main(){    unsigned int i = 2147483649, usum;    int j = 1, sum;    usum = i + j;    sum = i + j;    printf("usum = 214783649 + 1 = %d (d)\n",usum);    printf("usum = 214783649 + 1 = %u (u)\n",usum);    printf("sum = 214783649 + 1 = %d (d)\n",sum);    printf("sum = 214783649 + 1 = %u (u)\n",sum);}

在看下方结果之前,大家先思考一下应该是什么结果?

usum = 214783649 + 1 = -2147483646 (d)usum = 214783649 + 1 = 2147483650 (u)sum = 214783649 + 1 = -2147483646 (d)sum = 214783649 + 1 = 2147483650 (u)

看到上面的输出结果,我猜大家可能会有这样的问题:

  • i和j都是正数,两个数相加之后怎么成负数了?

  • 为什么格式符不同,输出的结果也不通呢?

  • usum是unsigned int类型,怎么成为了负数呢?

下面我们一步一步的来进行解析。

      首先我们知道了数值在机器内存是以补码的形式进行存储的,那么对于两个数相加机器是怎么处理的呢?在汇编层次,一般通过add eax,edx进行的,其中将eaxedx中的二进制相加,然后把结果存储到寄存器eax中,所以说214783649 + 1对于机器来说只不过是将这两个的二进制形式相加而已,关于补码加减运算应该要注意的是:负数的求反码和补码过程符号位是不参与计算的,要取出来,只有在进行负数补码的加减法的时候符号位才参与计算[1]

d871e2965c3727e0d2e54768af4a09fe.png

关键的要来了,机器会把1000 0000 0000 0000 0000 0000 0000 0010看做什么呢?
  • 如果是有符号的话,则它的原码为1111 1111 1111 1111 1111 1111 1111 1100,因为是有符号的,所以最高位为符号位,为1表示是负数,那么1111111111111111111111111111110(去掉符号位的二进制)的十进制为2147483646,再加上符号,那么就是-2147483646
  • 如果是无符号的话,则它的原码就是其本身,那么该二进制的十进制为2147483650
之所以出现上述结果,原因在于格式符%d%u
  • %d是输出带符号十进制定点格式,也就是说机器通过%d将本无意义的二进制看作有符号的数值,那么第一位是符号位;
  • %u是输出无符号十进制定点格式,也就是说不管前面是如何定义的,在这里的输出机器就把二进制串解析成无符号的十进制;
小节总结:
  • 数值在内存中都以补码的形式进行存储和计算;(为什么要以补码的形式呢,见参考文献[1])
  • 数值是有符号还是无符号,看用户怎么使用
2.浮点数在内存的存储形式
       首先要说明的是,这里我们不会对浮点数的具体知识点展开,比如精度问题。我们知道了整型数值在内存中是以其补码的形式进行存储的,那浮点数在内存中是以什么形式进行存储的呢?既然不太清楚,那我们就先用一个简单的例子来看一下浮点数的存储方式:
#includeint main(){    float a = 4.5;// 二进制形式为0000 0000 0000 0000 0000 0000 0000 0100    float b = -4.5;// 二进制形式为1000 0000 0000 0000 0000 0000 0000 0100.1    printf("[*] a:%f\n[*] b:%f\n",a,b);    return 0;}
       我们将其程序编译为32位的ELF程序,用GDB看一下a和b的存储形式,如下:

d9cb566e4d37fa9f9ed62ef47a89dc41.png

      我们现在对比一下a和b的二进制形式和在计算机内存存放的形式:
a    

数值自身形式:00000000000000000000000000000100.1

内存中的存储:01000000100100000000000000000000

b

数值自身形式:10000000000000000000000000000100.1

内存中的存储:11000000100100000000000000000000

可以看出数值本身的二进制形式和存放在内存中的形式完全不同,这是因为浮点数与整数的存储方式完全不同,根据IEEE-754标准,我们可以知道浮点数是以如下方式进行存储的:

770d47d827b37e0c1f79783b424dc5c5.png

既然我们知道了相关的存储方式[2],那么机器如何操作的呢?下面我们还是一上面的程序为例,看一下相关的汇编代码:
 ……         0x565561b0 :     add      eax,0x2e50         0x565561b5 :     fld        DWORD PTR [eax-0x1fe4]        0x565561bb :     fstp      DWORD PTR [ebp-0xc]         0x565561be :     fld        DWORD PTR [eax-0x1fe0]         0x565561c4 :     fstp      DWORD PTR [ebp-0x10]         0x565561c7 :     fld        DWORD PTR [ebp-0xc]         0x565561ca :     fld        DWORD PTR [ebp-0x10]         0x565561cd :     sub       esp,0xc   ……
这里的fldfstp指令是没有接触过,那么这里就简单介绍一下:
fld指令
指令格式:FLD STReg/MemReal    (STReg是处理器堆栈寄存器ST(0)~ST(7)) 指令功能:将浮点数据压入协处理器的堆栈中。当进行内存单元内容压栈时,系统会自动决定传送数据的精度。比如:用DD或REAL4定义的内存单元数值是单精度数等,类似于指令push
fst指令
指令格式:FST STReg/MemReal 指令功能:将协处理器堆栈栈顶的数据传送到目标操作数中。在进行数据传送时,系统自动根据控制寄存器中舍入控制位的设置把栈顶浮点数舍入成相应精度的数据。
fstp指令
指令格式:FSTP STReg/MemReal 指令功能:与FST相类似,所不同的是:指令FST执行完后,不进行堆栈的弹出操作,即:堆栈不发生变化,而指令FSTP执行完后,则需要进行堆栈的弹出操作,堆栈将发生变化。

0x01 数据常量

1.字符和字符字面值

      从一个程序开始我们的学习:

#includeint main(){    int a = 'a';    char b = 'b';    char c = 0x63;    int d = 100;    printf("[*] a = %d(d) = %c(c)\n",a,a);    printf("[*] b = %d(d) = %c(c)\n",b,b);    printf("[*] c = %d(d) = %c(c)\n",c,c);    printf("[*] d = %d(d) = %c(c)\n",d,d);    b--;    printf("[*] b-1 = %d(d) = %c(c)\n",b,b);}

结果如下

[*] a = 97(d) = a(c)[*] b = 98(d) = b(c)[*] c = 99(d) = c(c)[*] d = 100(d) = d(c)[*] b-1 = 97(d) = a(c)
       字面值就是一种记号而已,比如3,100,a,c,3.14。因为我们不能修改它(你能说把3改成5吗?)所以有时候又叫它常量。比如 int a = 100;a是一个int型变量,100就是一个字面值。字面值100只能用来作右值,不能作左值。C语言中字符字面值是由一对单引号括起来的单个字符,比如’a’、‘b’。实际上字符字面值和整型字面值是一样的。编译器把字符字面值都当做整型字面值处理。比如:
  • 定义一个字符变量并用字符字面值来初始化:char ch = ‘a’;或者 char ch = 0x61;
  • 定义一个int型变量并用字符字面值来初始化:int a = ‘a’;也是可以的,printf("%d",a);输出结果为97
总结
  • 字符型可以和整型可以相互转化,在C语言中字符型就是一种整型,因为在内存中存储的是二进制形式,可以被解析成字符和字符字面值两个含义;
  • 字符字面值是由单引号括起来的单个字符;

0x02 数据类型转换

1.类型提升

      首先举一个简单的例子:

#include #include int main(){    short a= -1;    unsigned short b;    unsigned int c;    b = a;    c = a;    printf("[*]          short a = %d(d) = %u(u)\n",a,a);    printf("[*] unsigned short b = %d(d) = %u(u)\n",b,b);    printf("[*] unsigned int c = %d(d) = %u(u)\n",c,b);    return 0;}

结果如下:

[*]                 short a = -1(d) = 4294967295(u)[*] unsigned short b = 65535(d) = 65535(u)[*] unsigned int c = -1(d) = 4294967295(u)

      分析printf的输出,如下图:

ad64d4f14459154246d434472af7a91e.png

总结
  • 有符号数按照有符号数的扩展规则(高位补符号位)扩展,无符号数按照无符号数的扩展规则(高位补0)扩展
  • 整型提升都解释为int
2.类型降格
       关于类型降格的相关知识点我只简单总结一下,大家可以自己写示例代码进行验证
  • 当实数(浮点数)转换为整数时,实数的小数部分被全被舍去,只保留整数部分
  • 当double类型转换为float类型,将去掉多余的数字,但按照四舍五入进行处理,这种会降低精度
3.显示转换

      显示转换的方法有:强制类型转换

4.隐式转换

      隐式转换的方法有:

赋值转换

通过赋值语句使符号右边表达式的值的类型自动转换为其左边变量的类型,如我们在类型提升中程序里面有关赋值的语句,如
#includeint main(){    int a = 3,b;    short i = 4,j;    j = a;// int => short 类型降格    b = i;// short => int 类型提升}

一元转换

将短型数扩展成机器处理的长度
二元转换
按照优先级顺序将各二元运算符的操作数提升为同一类型,长类型的优先级大于短类型的优先级
输出转换
比如我们前面讲到的printf()的格式符%d,是将数据类型提升/降格为signed int类型

0x03 Reference

[1] 计算机补码运算背后的数学原理是什么? [2] 浮点数的二进制表示

d8f383b6e5bdc5e29e0c41d32d242661.png

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值