C語言在明面上將數的變數分為兩類,整型變數以及浮點數,對應著現實世界的整數和小數。
首先是整數,使用了這麼多的C語言之後,每當在使用整數之時都會將其想象成二進位的存在,而不是十進位。原因在於,這是程式的本質所在,稍有研究編譯器工作原理的都會發現,在編譯器處理乘法乃至除法的時候,優秀的編譯器總會想方設法的加快程式的速度,毫無疑問在所有運算中移位元運算是最快速的"乘法"以及"除法":
而正常一個乘法相當於十數次的加法運算的時間消耗,移位則不用(除法的消耗更大,但是隨著CPU的進步,這些差距正在逐漸縮小,就目前來看依舊是有著不小的差距但無論如何最佳化,乘法時間都會大於加法)。正如前面所說,C語言設計之初便是給了程式員所有的權利,而程式員要做的就是掌控所有能掌控的,即便是數的計算亦是如此,比如在優秀的編譯器看來:
2*7 ====> (2<<3) - 25*31 ====> (5<<5) - 5
毫無疑問經過編譯器最佳化後的代碼此前者要快許多。這就是為什麼我們要將一個數看作二進位,這不僅僅是表面,而是要在深層次的認為它是二進位,總體來說C語言的整型是非常簡潔明了的總體分為 有符號 和 無符號,很好理解只需要注意不要讓無符號數進行負數的運算,這裡有一個原則,可以很好的規避這種無意之過,不把無符號類型變數和有符號類型變數放於同一運算中,時刻記得保持式子的類型一致是設計時的保障。
浮點數,由於實數域可以看作稠密的,故除了整數以外,還有無數的小數,而小數在電腦中如何表示?一種無限的狀態是無法在電腦中被精確表示,所以有了浮點法,關於浮點法可以參考書籍《深入理解電腦系統》。
這裡介紹的是在C語言中我們應該如何正確使用浮點數?很多人(包括我)在初作之時總是想當然的以為電腦是無所不能的,連人類都無法完全表達出來的小數電腦一定可以,實際上並非如此,在這裡我可以說,電腦只是近似表達,而最大的忌諱的便是將兩個浮點數進行比較,此處介紹一種浮點數常用的比較方法,精確度法:
#define DISTANCE 0.00000001 ... float f_x_1 = 20.5; float f_x_2 = 19.5; if(f_x_1 - f_x_2 < DISTANCE) printf("They are Equal\n"); else printf("Different\n");
所以說,在很大程度上,當你在程式中使用了浮點數,又直接使用浮點數進行比較,卻發現始終無法達到預期效果,那麼你可以檢查一下,是否是這個原因,在這一點上,不得不說是C語言的一個缺憾。
指標變數,是一種比較特別的變數,以至於總是對它進行特別對待。這裡有幾個原則:
- 兩個不相關的指標進行加減操作是無意義的
- 始終確保自己能夠找到分配的記憶體
- 無論何時何地何種情況,都要記住,不使用未初始化的指標,不讓未使用的記憶體持續存在。
指標在不同位的作業系統上的大小是不一樣的,但是在同一個作業系統下,無論什麼類型的指標都是相同大小,這涉及到指標的定址問題,(題外話:C語言的定址實際上使用了組合語言的間接定址,有興趣的可以自行嘗試,方法之一,使用gcc編譯器的彙編選項,產生彙編代碼,進行一一比對),對於定址一個籠統一些的說法便是
所以32位的作業系統下C語言指標:
... size_t what = sizeof(void*); printf("%d", what); ...
輸出:
對於大部分使用者來說,指標主要用來降低記憶體消耗以及提高運算效率的,這裡設計許多學問,我也無法一一展示,比較有意思也常用的兩個東西便是遞增以及文法糖:++, ->
... int dupli_of_me[10] = {0};//也可以使用庫函數memset()進行置0 int *point_to_me = dupli_of_me; int me = 100; while(point_to_me < (dupli_of_me + 10)) *point_to_me++ = me;
其中*point_to_me++ = me;在C語言應用廣泛它相當於是:
*point_to_me = me; point_to_me++;
的文法糖,對於++,在非必要的情況下,請使用首碼遞增,而非尾碼遞增,原因是消耗問題,仔細想想這兩種遞增的區別在何處?
首碼遞增總是在原數上進行遞增操作,然而尾碼遞增呢?它首先拷貝一份原數放於別處,並且遞增這份拷貝,在原數進行的操作完畢後,將這份拷貝再拷貝進原數取代它,此中的操作涉及的更多,所以在非必要的情況下,請使用首碼遞增而不是尾碼遞增(遞減也是同樣的道理)。
->則是在結構體上使用的非常廣泛:
typedef struct data{ int test; struct data* next; }my_struct; ... my_struct temp; my_struct *ptemp = &temp; ptemp->test = 100; ptemp->next = NULL; if(temp.test == 100) printf("Correctly!\n"); else printf("That is impossible!\n"); ...
可以很清楚的看出其實ptemp->test便是(*ptemp).test的文法糖
變數限定
const 是最常用的變數限定符,它的意思是告訴編譯器,這個變數或者對象在初始化以後不能被改變,常用它來保護一些必要的傳回值,參數以及常量的定義。
volatile 這個關鍵字常常被C語言教材所忽略,它很神秘。實際上確實如此,他的作用的確很神秘:一旦使用了,就是告訴編譯器,即使這個變數沒有被使用或修改其他記憶體單元,它的值也可能發生變化。通俗的說就是,告訴編譯器,不要把你的那一套最佳化策略用在我身上。
/* 此時我們將編譯器最佳化等級提高到 -O2 */ int test_num = 100; //測試一個迭代加法 int nor_result = 0; volatile int vol_result = 0; /* 測試無volatile限定下,該程式的耗時 */ for(int i = 0;i < 10000;++i) for(int j = 0;j < 10000;++j) nor_result += test_num;
接下來就是測試volatile限定下的代碼
for(int i = 0;i < 10000;++i) for(int j = 0;j < 10000;++j) vol_result += test_num;
在使用一些手段後,得到已耗用時間,可以很清晰的看出差別,在我的機器上,i5-4CPU,得到的結果是後者比前者慢大概十五倍。 從某一些方向上證明了,volatile的一些作用,比如調試的時候,或者一些特殊用途。涉足不多,故不記錄。
變數說明
extern 用於將不同檔案的,帶有外部連結性的變數引用到本檔案中。所謂外部連結性就是可以被除本檔案外的其他檔案"看見"的變數,如全域變數,使用方法:
/* 以下為一個工程內可見 */ /*file1.c*/ int glo_show;//對於該全域變數來說,它們在聲明時無初始化,則預設初始為0 int glo_print = 10;//聲明定義完成後,自動分配記憶體以儲存資訊 ... /* file2.c */ extern glo_print; //僅僅是引用名字,並不會額外分配空間 //所以,只需要寫正確變數名字即可,後方的初始化無須完全 //因為變數的初始化定義只能有一次。 void print() { printf("The Globle Value is %d \n", glo_print); }
auto 可以姑且忽略,因為沒有什麼實際意義。
變數擷取
格式化輸入輸出在C語言的初學中使用的比較頻繁,但是到後期會發現,由於I/O操作過於消耗資源,換句話來說就是會極大影響程式的執行效率,會漸漸的在發行版程式中消除。
常見格式化輸入標準函數: sacnf, fscanf, sscanf
對於常見的使用不贅述,有兩種比較不常見的格式:`%[]` 和 `%*`,
前者是用於限制讀取類型,常見於字串的過濾(不是真正的過濾)
scanf("%d %[a-z]", &tmp, str); scanf("%d %[^i]", &tmp, str); scanf("%d %[^,]", &tmp, str);
假設輸入的是:22 hello,string to me!
讀取到的分別為:22 hello 和 22 hello,str 和 22 hello
後者則是忽略第一個輸入:
假設輸入的是:22 33
讀取到的則是:33
其中開頭的%*d忽略的輸入,必須和其類型匹配,例如輸入:string 33則會讀取失敗。
也可以將其解讀為檔案寬度,例如在使用printf格式化輸出的時候:
char str[10] = "dir"; printf("%*s%s",4 ,"" , str); /* 輸出: dir */ 四個空白佔位
但是實際上scanf並不太好用,所謂的好用指的是功能上以及設計上的缺陷,總是讓很多人摸不著頭腦的出了錯,往往很難調試。例如它會將每一行輸入的\n保留在輸入資料流裡面,這個缺陷導致如果不明所以得人將其與其他的輸入函數,例如fgets或者gets配合會出現差錯。