C语言学习实践

​https://blog.csdn.net/solomon1558/article/details/40798901​

摘要

本文将从C语言变量的本质,不同类型变量在内存中的存储方式,类型强制转换,格式输出4个方面阐述C语言初学阶段的一些问题。

关键词:内存存储,类型强制转换,反汇编

  1. 变量
    变量来源于数学,是计算机语言中能储存计算结果或能表示值抽象概念。在诸如C语言等高级语言中,变量的使用屏蔽了数据的底层细节,使得高级语言程序员不必像汇编程序员那样关心数据与硬件之间的关系。为了探究C语言中变量在内存中的存储形式,可以借助反汇编查看汇编语言以及内存数据。
  2. 变量在内存中的存储形式
    在内存中,无论哪种数据类型的数据,都是以相应长度的二进制码存取。从内存取数据是,如果不按照定义数据类型的方式取数据,所取数据就会错误。

2.1 用反汇编查看变量内存数据

(1) 实验代码如下。在赋值部分打点后调试,转入反汇编。

(2) 在监视窗口查看变量的内存地址,并在内存窗口中查看数据。

整形变量_4ByteData的数据在以内存地址0x0023FA58起始的四个Byte中存放:【注意】 Intel处理器是小端机,数据高位在高地址,地位在地址。所存数据:4Bytes的十六进制数 0x12345678




单精度浮点型变量fl的数据在内存中的存储:



双精度浮点型变量df的数据在内存中的存储:



字符型变量ch的数据在内存中存储;

结论:

(1)   局部变量存储在函数栈中,且该栈向低地址生长,所以先定义的局部变

量在较高内存地址(比如_4ByteData在0x0023FA58,ch在0x0023FA33),并且局部变量之间并非紧密排布,而是由8个Byte 的cc数据隔开。变量周围塞些CCCCCCCC,这可能是编译器提供的一种保护机制,越界了好出断言。通过网上查找资料,这是VC在Debug时给变量留出空间,用来检查stack overflow。

用release调试,就不会有多余的cc。(但是注意这里char型变量与之前的局部变量之间仍有数据,我猜测这里是应为有变量对齐的缘故,char型变量也占了4byte,只不过多余的3byte由其他数据填充。)




(2) 通过观察不同类型的变量在内存中的存储情况,可以发现:

在32位机器,VS2010 IDE中,一个int型数据占4B,float型变量占4B,double型变量占8B,char型占1B。

2.2 IEEE754 单精度数的格式

单精度浮点数占据4个字节,4个字节的分配如下:

(a)第一位为符号位,0表示正,1表示负;

(b)第2~9位为阶码,采用移码表示;

(c)第10~32位为尾数,采用原码表示。

给定32位串,如何转换成十进制数:

假设内存中存在32位串:00 00 00 3f,因为INTELCPU采用little endian存储方式,所以其真实的值为:

0x 3f 00 00 00。将其写成二进制形式:

(1)第一步,化为二进制

0 01111110 0000000 00000000 00000000

(2)第二步

该浮点数为正数,阶码 01111110,移码表示(126-127) = -1

尾数 0000000 00000000 00000000

因为在IEEE754中,单精度浮点数有规格化处理,所以其真正尾数部分为

1.0000000 00000000 00000000,其中‘.’为小数点

(3) 第三步

根据公式写出实际数值大小

0.10000000 00000000 0000000 化为二进制:0.5

2.3 IEEE754 双精度数的格式

长实数也称双精度数符号位1位,阶码11位,尾数52位

给定32位串,如何转换成十进制数:

假设内存中存在64位串:00 00 00 0000 00 e0 3f,因为INTEL CPU采用littleendian存储方式,所以其真实的值为: 0x 3f e0 00 00 00 00 00 00。将其写成二进制形式:

(1)第一步

0 01111111110 0000 00000000 00000000 00000000 00000000 0000000000000000

(2)第二步

该浮点数为正数,阶码 01111111110,移码表示(1022-1023) = -1 尾数 0

因为在IEEE754中,单精度浮点数有规格化处理,所以其真正尾数部分为

1.0,其中‘.’为小数点

(3) 第三步

根据公式写出实际数值大小

0.10 化为二进制:0.5
  1. 格式化输出

3.1 printf函数调用的一般形式

printf函数是一个标准库函数,它的函数原型在头文件“stdio.h”中。但作为一个特例,不要求在使用 printf 函数之前必须包含stdio.h文件。printf函数调用的一般形式为:printf(“格式控制字符串”, 输出表列)。

其中格式控制字符串用于指定输出格式。格式控制串可由格式字符串和非格式字符串两种组成。格式字符串是以%开头的字符串,在%后面跟有各种格式字符,以说明输出数据的类型、形式、长度、小数位数等。如:

“%d”表示按十进制整型输出;

“%ld”表示按十进制长整型输出;

“%c”表示按字符型输出等

非格式字符串原样输出,在显示中起提示作用。输出表列中给出了各个输出项,要求格式字符串和各输出项在数量和类型上应该一一对应。

3.2 类型不对应下的格式输出

_4ByteData = 0x12345678; //Hexadecimal:0x12345678 对应Decimal:305419896

fl = 0.5;

df = 0.5;

ch = 65;

printf("%d\n",_4ByteData);

printf("%c\n",_4ByteData);

printf("%d\n",ch);

printf("%f\n",&_4ByteData);

printf("%d\n",fl);

printf("%d\n",df);

分析:

可以看到同样是int型变量,printf("%d\n",_4ByteData);与

printf("%c\n",_4ByteData);其结果分别为305419896(0x12345678对应的十进制数),而后者只取了4字节数据的最低一个字节0x78,所以打印出了AISII码0x78对应的字符’x’。

联系C语言中指针的用法,我做出假设:格式输出函数printf()根据类型字符%以及变量名,就可以根据数据首地址+读取长度的方式输出数据。

进一步发现:

但是对字符型变量ch使用类型字符%d输出,得到的是其ASCII码的十进制数,如果按照上述假设,会输出以ch地址起始的4B的数据(这将是一个错误数据)。

但实验结果是正确输出了ch字符的ACSII码的十进制数。

再对浮点数做实验:

用类型字符%d输出单精度数,双精度数,结果均为0。

由上文2.1可知单精度浮点变量fl(十进制0.5)在内存中占4B,机器码是

0x00 00 00 3f(小端机)。现对整数_4ByteData赋值0x3f000000,并使用类型字符%f对该整数输出,查看结果:

结果仍是0。这说明即使内存中数据存储的内容一样,但是使用类型字符%f对整型变量输出,其结果仍然不是浮点数!

综上,原假设值得怀疑!
  1. printf()函数类型不对应下的格式输出进一步研究

4.1用%d输出float类型数据

float fl=0.5;如果用printf("%d",fl);输出的是0。 但float型用%d输出是否一定是0呢,答案肯定不都是0(如下图)。

为什么 0.5 用%d输出的是0?

分析如下:

首先来了解下printf的输出格式,int 和 long int 都是32位的,用%d输出;float 、double都是%f输出,但 float 是32位的,double 是64位的,所以在参数传递的时候C语言统一将 float 类型数值传换为double 类型再传入 printf 函数。如果是32位整型则输出格式为%lld。

下面来讲一下 float fl=0.5f ;printf("%d",fl)输出为0的情况:

 %d只输出低32位的数据,并将这些32位二进制以十进制数输出,编译器首先将 0.5从float类型转换为double类型,0.5在内存中的存放方式是0x3f000000,转换成double类型在内存中的数据就是这个0x3fe0000000000000,这个内存数据可以很明显看出低32位全是0,而%d则只能截取到低32位,所以这个以%d输出0.5的数值当然是 0了。如大家不相信可以用%lld 输出看看,这个%lld就很读到低64位数据,读出的结果就是0x3fe0000000000000,在屏幕上看到一个很大的十进制数。(这里用%llx显示十六进制数更直观)

如果我一定要输出0.5在内存中的存放方法怎么办呢?

可以用printf("%d",*(int *)&fl);这里做了一下处理,不是直接把fl传进来,把fl所在地址里的内容处理了一下,不管fl是什么类型,只对地址进行操作,利用(int *)&lf,将fl所在地址中的内容0x3f000000直接当成 int 类型传给printf,int 的类型数据不会再转成double类型了,所以输出正常,这个只是针对浮点型数据只占低32位,如果输出64位还得用%lld格式控制输出。

如果用printf("%d",(int)fl),输出行不行?

这个强制类型转换只针对fl的数据类型进行转换,0.5转换 int 类型是0,而上面的*(int *)&a,是对内存中的实际存储数据进行操作,蔽开数据类型这一层面,只将这个数据0x3f000000直接转成int类型输出。而(int)fl,要先看fl的类型,C语言会根据所要数据类型,对内存存储的数据进行改变,以便可以用int类型正确解析内存数据。

如果用printf("%d",(float)fl),输出什么,输出的是0,这个只是将fl的float类型还转成float类型,还是要自动转成doube类型,传给printf函数。

为什么float非要转成double类型呢?

因为printf格式控制浮点型输出只有%f,所以统一按doube类型输出,不像整型有32位的%d或%ld,64位的有%lld,这就将32位整型和64位整型用不同的格式控制分开了,而%f则没有,所以printf输出的浮点数其实是统一遍历了64位内存,如果float传入printf没有进行转换,那么printf输出高32位数据将不可预知,printf输出结果也就不正确了,因此传入printf的浮点数都会被编译器隐含转成double类型。

4.2 int类型%f格式输出

如果定义了inta=0x3f000000;用printf("%f",a)输出的结果是多少呢?

答案是0,至少我们看的屏幕上显示的是0.000000,实际值可不是0啊,只是我们显示的精度只能有15位小数,而实际的数据可能很小很小,0.0000....000几百个0后会有几个有效数据。

我们分析一下:

首先C语言把a传进printf,因为a是整型,所以不会自动转成double型数据,直接将0x3f0000000传进printf,而%f寻的是64位内存,也就是把0x000000003f000000这个内存中的数据当成浮点型输出来,那浮点型的数据是多少呢,又是怎么存储的呢?

64位浮点数的存放方式:

    63位                 62~52位                 51~0位

1个符号位 11个阶数 52个尾数

从0x000000003f000000来看:

00000000

1)符号位是0,表示正

2)阶数是0,用移码表示:0-1023 = -1023,

用指数表示:1.#*2^-1023,‘#’是代表尾数。

    3)尾数就是,0x000003f000000

4)浮点二进制表示

1.000000000000000000000011 1111 000000000000000000000000*2(-1023),2-1023次方可想而知有多小!

这就是为什么我们的int型数据用%f输出是0.000000的原因!

如果把0.5的双精度数对应的十六进制数赋给long long类型变量,则可以输出正确的小数:
  1. 总结:
    通过以上实验,我验证了原假设基本正确:
    格式输出函数printf()根据类型字符%以及变量名,就可以根据数据首地址+读取长度的方式输出数据。

但是,还要注意其中的一些细节:

(1)用%d输出float类型数据时,在参数传递的时候C语言统一将 float 类型数值传换为 double 类型再传入 printf 函数。而%d只截取低32位数据,所以得到的数字不是相应浮点数的二进制码。

(2)int类型%f格式输出,%f寻的是64位内存,所以输出的数据可能很小(比如2^-1023),那么结果是0.

综上,无论什么类型的数据,都只是01二进制数据。只要清楚其内存存储机制,拿到数据首址+偏移量,就能正确操作该数据!

最后,我想说C语言非常灵活,高级程序员也要熟悉汇编语言,会使用反汇编这把“手术刀”在底层剖析程序,会有更深刻的认识!

———————————————— 版权声明:本文为CSDN博主「Solomon1588」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:​​https://blog.csdn.net/solomon1558/article/details/40798901​