c語言輸出0.000000或亂碼,深究

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章