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型),可以用強制類型轉化變成任何你需要的類型,很好用的。(在本篇博客裏的“二進制輸出”代碼就有用到)