0.引例
刚刚看到一篇帖子,发现我刚学编程时也遇到过,后来问同学要了代码(额)过了就没再管……现在看到了,接触的底层东西也多了,觉得有必要深究一下。
问题链接(已经有大佬解答了):https://fishc.com.cn/thread-147817-1-1.html
这是他的代码:
#include <stdio.h>
int main(void)
{
int a,t,c;
double cup,p;
printf("请输入转换数值:____杯\b\b\b\b\b\b");
scanf("%f",&cup);
p = cup/2;
a = cup*8;
t = a*2;
c = t*3;
printf("%f杯可换算为%f品脱、%d盎司、%d汤勺、%d茶勺\n",cup,p,a,t,c);
return 0;
}
以下是他的截图和我的结果截图,他是GCC编译器,我用的vs应该是vc++的编译器,结果有不一样,但明显都是错的
其实他的代码里只需要把第5行
double cup,p;
改为
float cup,p;
就行了
——原因就是:输入控制符是【%f】,申请的数据内存类型是【double】,输出的又是【%f、%d】,乱用数据类型导致内存写入、读取方式的不匹配,从而导致0或者乱码。
1.错误汇总及解决
一、格式控制符、数据类型不匹配(本篇主要讲的)
double匹配%lf,
float匹配%f,
int匹配%d,
乱码都是因为不匹配搞的鬼。
格式控制符不匹配会导致输入和读取的规则不一致
提供一种解决办法:使用强制类型转化来告诉程序使用哪一种数据类型进行操作(在本篇结尾有详细)
二、其他乱七八糟的错误:
(1)csanf的输入控制符多了个“%”百分号会导致错误。(csdn的富文本编写模式,百分号没法加粗,只能加上汉字阻隔一下……)
(2)csanf的输入控制符出现了“%d,%d”,(难道每次输入必须输入个“,”吗?这会导致你输入的东西自己都不知道该对应哪个)
(3)指针的类型,加没加*等问题……
(4)没有赋值、没有初始化(0或者乱码)
2.试验
下面我就深究一下(用的是vc++编译器,gcc别找我……)
先对int型来个试验
(代码我详细写了注释,新手同学可以仔细看看)
#include <stdio.h>
#include <string.h>//memcpy函数头文件需要
#include <stdlib.h>//malloc函数头文件需要
void ToBin(int n);//声明一下转换二进制的函数
int main(void)
{
printf("请输入数字:");
int a; //声明a是int型变量,按照int型分配一块内存
scanf("%d", &a); //按照%d整型格式,写入到a所在的地址(&是取地址符)
printf("%%d: %d\n", a);
printf("%%f: %f\n", a);
printf("%%lf:%lf\n", a);
printf("%%o:%o\n", a);//8进制
printf("%%x:%x\n", a);//16进制
void *p=malloc(4); //申请4字节地址
memcpy(p,&a,4);
printf("二进制输出:");
ToBin( *((int *)p) ); //“(int *)”强制类型转化成int *,由于之前是void型指针,所以可以不用担心转化会有错误
delete p; //用完及时清理自己分配的p所指向的内存
return 0;
}
//10进制转二进制,代码来自https://blog.csdn.net/qq_41785863/article/details/84101711
void ToBin(int n)
{
char a[1000];
int y = 0, x=2;
char z = 'A';
while (n != 0)
{
y++;
a[y] = n % x;
n = n / x;
a[y] = a[y] + '0';
}
for (int i = y; i > 0; i--)
{
if (i % 4 == 0) printf(" ");
printf("%c", a[i]);
}
}
先试验一个int型最大值2^31-1=2147483647
↑win10自带的计算器程序员模式还是挺好用的,可惜不支持小数。
↑很明显,用浮点输出的两个值都是0。
↑在改了a的内存类型(float)和输入格式控制符(%f)后,%d、%o、%x输出却都变成了0。
↑在改成了“int型变量a,用%f格式输入”后,%f和%lf都仍然是0,而%d、%o、%x都乱了。
由此可见,乱码的原因与:变量类型、输入控制符、输出控制符,都有关系(仔细一想,这不是废话吗……)。
先写代码看一下这3种数字格式在内存中是什么样子的
#include <stdio.h>
#include <string.h>//memcpy函数头文件需要
#include <stdlib.h>//malloc函数头文件需要
void ToBin(int n);//声明一下转换二进制的函数
void ToBin2(long long int n);//内存更宽了,用longlong搞8字节的double,没法用4字节的int了
int main(void)
{
int a; float b; double c;
a = 64; b = 64; c = 64;
printf("所占字节长度:%d,%d,%d,%d\n", sizeof(int), sizeof(float), sizeof(double), sizeof(long long int));
printf("%d\n",a);
void *p = malloc(4); //申请4字节地址(int)
memcpy(p, &a, 4);//拷贝a的数据
printf("int二进制输出:\t\t");
ToBin(*((int *)p)); //“(int *)”强制类型转化成int *,由于之前是void型指针,所以可以不用担心转化会有错误
delete p; //用完及时清理自己分配的p所指向的内存
p = malloc(4); //申请4字节地址(float)
memcpy(p, &b, 4);//拷贝b的数据
printf("float二进制输出:\t");
ToBin(*((int *)p));
delete p;
p = malloc(8); //申请8字节地址(double)
memcpy(p, &c, 8);//拷贝c的数据
printf("double二进制输出:\t");
ToBin2(*((long long int*)p));
delete p;
return 0;
}
void ToBin(int n)//10进制转二进制,代码来自https://blog.csdn.net/qq_41785863/article/details/84101711
{
char a[1000];
int y = 0, x=2;
char z = 'A';
while (n != 0)
{
y++;
a[y] = n % x;
n = n / x;
a[y] = a[y] + '0';
}
for (int i = y; i > 0; i--)
{
if (i % 4 == 0) printf(" ");
printf("%c", a[i]);
}
printf("\n");
}
void ToBin2(long long int n)
{
char a[1000];
int y = 0, x = 2;
char z = 'A';
while (n != 0)
{
y++;
a[y] = n % x;
n = n / x;
a[y] = a[y] + '0';
}
for (int i = y; i > 0; i--)
{
if (i % 4 == 0) printf(" ");
printf("%c", a[i]);
}
printf("\n");
}
↑为了保全内存内的东西不受影响,我用void型指针申请相应大小的内存,再用memcpy函数拷贝进来,最后统一用int或longlongint进行二进制转化。
↑64得出的东西
↑12.5的结果
=1100.1
=1.1001*2的3次方
=0 10000010 1001 0000000000000000000 (浮点数float)
现在大致明了,为什么整型和浮点型不能互相转化(包括:格式读取、格式输出、还有一部分赋值截断可能带来的错误)——由于浮点的表示方式和整型有很大不同。
(现在明白全是1的数据用浮点表示来读取为什么是0了吧~)
3.深究
之前能“看得懂”的int、long int、long long int型存储方式是定点数存储方式,而float、double等的存储方式为浮点数存储方式。
至于 IEEE754浮点数存储标准,就是《计算机组成原理》中讲了一堆我到现在还没记清楚的东西……
IEEE 浮点标准表示: V = (-1)s * M * 2E 。
①、s 是符号位,为0时表示正,为1时表示负。
②、M为尾数,是一个二进制小数,它的范围是0至1-ε,或者1至2-ε(ε的值一般是2-k次方,其中设k > 0)
③、E为阶码,可正可负,作用是给尾数加权。
【12.5的IEEE 浮点标准表示】
(1)首先,十进制转二进制:
整数部分 除二余数倒写:
12: 12/2=6 余0 ;6/2=3 余0 ;3/2=1 余1 ;1/2=0 余1
倒写 也就是:1100
小数部分 乘二取整顺写:
0.5: 0.5×2=1.0
取整 也就是:1
12.5的二进制:1100.1
(2)然后将二进制转化为浮点数:
由于12.5为正数,所以符号位为0;
1100.1=1.1001×2^3 指数为3 ,
则 阶码=3+127=130 ,即:10000010
0 10000010 1001 0000000000000000000
摘自:https://www.cnblogs.com/rosesmall/p/9473126.html
符号位:0
阶码:10000010
尾数:1001 0000000000000000000
再说明一下,尾数为什么不带“1”,因为标准就是将“有效数字”化为整数第一位是1后跟着小数的形式(只能是1因为是二进制,十进制我们可以1到9),故而省去了,只留下小鼠的部分1.1001->1001 0000000000000000000
float的存储格式:
↓现在再看127得出的东西,可以分清float的【符号位、阶码、尾数】了吧。
使用强制类型转换运算符
平时在编译器waring下我们会偷懒地用 隐式类型转换
这里介绍一下强制类型转换运算符
#include <stdio.h>
int main(void)
{
float b;
b = 12.5;
printf("%d\n", b);
printf("%d\n", (int)b);
return 0;
}
↓见证奇迹的时刻到了!!!
不输出“0”了!!!!
这里可以理解为命令(告诉)程序用int类型取读取b变量!
我读《深入理解计算机系统》得到的知识(有点个人理解,不晓得是否正确恰当)——指针的类型(数据类型)实际上是由位数的多少和读取方式区分的,所以数据类型的不同会导致我们不希望出现的bug。
补充:
1.强制类型转化也有c++式的(类似于实例化对象的风格)
printf("%d\n", (int)b); //c 式的强制类型转化
printf("%d\n", int(b)); //c++ 式的强制类型转化
2.void*型的指针由于其未指定具体的数据类型(void型),可以用强制类型转化变成任何你需要的类型,很好用的。(在本篇博客里的“二进制输出”代码就有用到)