【C語言】java
由於之前學過C語言,只不過太長時間不用,已經忘得差很少了… 因此這篇文章的性質是把C語言中一些對於如今的我不是很符合預期的知識點記錄一下。python
■ HelloWorld程序c++
HelloWorld以下數組
#include <stdio.h> int main(int argc, char *argv[]){ int i = 0; printf("Hello,World\n"); printf("i is %d\n",i); return 0; }
若是是在Linux上而且安裝了gcc,那麼將上述代碼寫入test.c以後,直接使用gcc -o test test.c命令就能夠將其編譯爲可執行程序的test,隨後直接./test便可執行。數據結構
● 注意點app
字符串必定要雙引號了。printf格式化字符串末尾有必要的話記得加上\n。要指明main函數的返回類型,在最新的C語言標準C11中,規定main函數只能返回int類型,因此必定要寫int main而且在main函數體的最後返回一個int型數如0。argc和argv用於接受來自命令行的參數。ide
不要忘了#include <stdio.h>這個加入頭文件的預處理指令。函數
■ 數據類型學習
用於表達整數的類型有char, short, int, long。沒錯,char算是表達整數的。這四種類型的大小(即每個這個類型的變量佔用多少字節的空間)是1, 2, 4(部分老平臺多是2), 8。知道了某個類型佔據的字節數,那麼就能夠知道這個類型的最大最小值了。與這四者相對的,是unsigned char/short/int/long,這四種類型佔據大小和前面四個一致,只不過區別在於他們將全部空間都用來表示正整數。所以,一個unsigned類型的整數能夠表達的最小的數是0,最大的是將其字節所有空間都用1填充以後獲得的那個二進制數。好比unsigned char最大值是255,unsigned short最大值是65535。相對的不帶unsigned的類型的話,所能表示的值的範圍是從(-n,n-1],左開右閉,其中2n+1等於以前unsigned類型時的最大值。好比char是(-128,127],int是(-32768,32767]。spa
用於表達實數的包括了float, double, long double三種類型。具體精度不太好記了…
void類型是指沒有可用的值。一般能夠用在函數返回值聲明、明確函數不接受參數、指針不指向任何地址等狀況。
■ 變量聲明
聲明時能夠直接賦值,用逗號能夠鏈接多個同類型變量的賦值。
如 int a = 3,b = 5;
在C語言中,要使用某個變量,要經歷聲明、定義、初始化三個階段。習慣了python這類動態類型的腳本語言,很容易就忘記了一箇中間使用,最終不返回出函數的變量的聲明。
一般,聲明和定義是一塊兒進行的好比main函數中的int i;這就是聲明而且定義了變量i。若是是int i = 0; 那麼就是聲明、定義、初始化都一塊兒作掉了。對於有些內容好比外部文件或者庫中的變量,咱們可使用extern關鍵字聲明,此時僅僅是聲明而不涉及到定義。如下面代碼爲例:
#include <stdio.h> extern int i; // 聲明變量i int main(){ int i; // 定義變量i i = 10; // 初始化變量i printf("%d",i); return 0; }
● 關於賦值
C語言中的表達式(變量、常量或者經過基本運算符號將它們結合後的東西),大致能夠分爲左值表達式和右值表達式兩種。其中左值表達式指的是指向一個實際存在的內存地址的表達式,好比一個變量等;右值表達式則指那些指向內存中保存着的具體的值的表達式。
在賦值的時候,等號左邊只能是左值表達式而右邊只能是右值表達式。
int g = 10; 這個賦值操做是合法的,可是10 = 20; 就是不合法的了。 固然關於賦值這個操做,即使某一天真的反了這種錯誤,也是很容易看出來的。何況從人類邏輯上來講並不太會可能寫出10 = 20這樣的語句來…
■ 常量
常量即在程序運行過程當中不會改變的量。一般咱們常提到的「值」就是一種常量。然而在C語言中很是不一樣的一點在於,常量也能夠有一個alias來表示。說到常量,Python中也偶爾會有相似於BASEDIR = xxx這種形式,將變量名大寫來提示這個量是一個「常量」,不該該隨意修改其值。可是它本質上仍是一個變量,若是我想修改它的值仍是能夠修改。
可是C裏面的常量機制限定死了,這個常量不能修改。
常量自己的類型以及寫法(好比0x4b,科學計數法,字符串的特殊轉義字符等)就不說了,主要說下常量的這個「alias」是如何定義的。簡單定義常量的方式大概有兩種,分別是#define宏以及const關鍵字。
#include <stdio.h> #define LENGTH 10 #define WIDTH 5 int main(int argc, char *argv[]){ int area; area = LENGTH * WIDTH; printf("area is: %d\n",area); return 0; }
#include <stdio.h> int main(int argc, char *argv[]){ int area; const int LENGTH = 10; const int WIDTH = 5; area = LENGTH * WIDTH; printf("area is: %d\n",area); return 0; }
上面兩個代碼中,若是嘗試修改LENGTH或者WIDTH的值那麼就會報錯,賦值給只讀變量。
■ 存儲類
存儲類這個東西以前學習C語言的時候老師沒有怎麼仔細教。一個變量,除了能夠按照數據類型對其進行分類以外,每每還能夠根據其存儲類型進行分類。存儲類有點像java裏面的從public到private的權限關鍵字。C語言中的存儲類影響變量/函數的做用範圍以及生命週期。C語言中的存儲類包括:
auto, register, static 和 extern。
auto是通常局部變量的默認的存儲類,能夠不顯式地聲明。auto只能用在函數中,用於聲明某個局部變量的存儲性質。
register聲明的變量不存儲在RAM中而是存儲在寄存器中。
static聲明的變量存在於整個程序的聲明週期中,不會隨着進出做用域而建立或銷燬。全局變量的默認存儲類就是static。
extern聲明的變量/函數不只在本文件中可用,在其餘相關文件中也可用。在多文件共享變量/函數時經常使用
■ 運算符
通常的算數運算符,邏輯運算符就很少說了。主要說下不太常見的幾個運算符
位運算符: &,|,~,^ 這四個運算符分別是與運算,或運算,非運算,異或運算。除了非運算其他都是二元運算符。異或運算是指先後的值不一樣時返回真,不然返回假。如0^0=0,1^1=0,1^0=1
與或非運算自己沒什麼可說的。這些運算符一般直接操做數字,數字按照二進制寫出來以後一位一位地比較。
<<,>> 這兩個是左移右移運算符,左邊是待處理值,右邊是一個數字表示移幾位。好比60在8bit環境中的二進制表示是0011 1100。若是進行60 >> 2 運算,那麼向右移動兩位,左邊用0補齊(若是原值是負數那麼就要用1補齊),獲得的是0000 1111,即15。相反,若是是 60 << 2,那麼獲得的就是1111 0000,右端用0補齊,即240。
上面全部位運算符若是是二元運算符,那麼就都會有相應的賦值運算,如A &= B 就是 A = A & B, A <<= B 就是 A = A << B
除了位運算符,還有如下幾個運算符須要指出:
& 和 *。 & 在這裏是一個一元運算符,因此不是與運算的意思。用於得到某個變量的實際地址。好比&a; 能夠得到a變量具體保存在內存中的地址。*也不是乘號,而是一個指向某個變量的指針,某些場合下能夠認爲其運算的值是內存中的地址,經過*運算符的處理返回的是相關內存地址中的實際數據值。這個符號在C語言中和稍微高級點的數據結構打交道的時候就會用到。
對比一下,&的運算對象是變量(或者說變量名?),計算得出的是變量在內存中的存儲位置。
C語言中的運算符有個比較重要的概念就是它的優先度。固然在通常的程序當中可能不太會遇到須要很細緻地去辨別一堆運算符的優先度。就不單獨寫了。參考一下各類資料便可。
■ 判斷、循環、函數
寫久了Python就以爲switch語句實在是太棒了…
for循環,while循環都很熟悉了。do...while...循環其實也很棒,其寫法是:
do{ // do something }while(xxx);
能夠保證循環體至少被執行一次,判斷條件放到每次執行結束後進行而不是開始前。
● 函數
聲明C語言函數時不要忘了聲明函數的返回值類型或者是void。另外每一個參數也要明確地指出類型。
函數的參數有形參和實參的區別。能夠認爲形參就是該函數的局部變量,在執行完函數體以後就被銷燬。須要注意的一點是,和Python中的可變類型,不可變類型相似,若是函數體對形參作出改變,就要清晰地意識到這種改變是否對實參有影響。在C語言中術語是傳值調用和引用調用。
傳值調用,相似於不可變類型作參數,其過程是實際參數將本身的值傳遞給形式參數,而後形式參數在函數體中被作出改變,這種改變並不影響實際參數那個變量自己,由於形式參數只是實際參數的副本。
引用調用的標誌是 形參是指針,至關因而說應該傳入地址,因此實參應該是一個變量的存儲地址了。好比最經典的例子,編寫一個swap函數交換兩個變量存儲的值。若是在Python中,咱們能夠直接 a,b = b,a這樣的方式來交換值,由於a,b變量從更加低層的角度來看自己就是兩個指針(引用)。
下面是兩個例子:
#include <stdio.h> void swap(int a, int b){ // 因爲只是傳值調用,局部變量a,b會被交換值,可是不影響外部調用時的實參x和y。 int tmp; tmp = a; a = b; b = tmp; } void swap_p(int *a, int *b){ // 因爲是引用調用,指向ab的兩個指針直接交換了值,原來的實參也就交換了值 int tmp; tmp = *a; *a = *b; *b = tmp; }
■ 做用域
局部變量是語句塊(包括函數,循環,if/else結構等任何出現大括號對的地方)內的變量,一旦程序運行出了大括號則變量銷燬
全局變量是在函數外聲明的變量,通常而言全局可用。
變量名之間若是出現衝突,則以當前所在區域較小較局部的變量爲準。一旦走出這個區域則回到高一級的同名變量。
正如上面所說,函數的形式參數是函數體範圍內的局部變量。
對於局部變量,聲明時不會自動賦值。對於全局變量,若是聲明時沒有初始化值,那麼程序會自動給他賦值爲0(全部數字類型)或者'\0'(char)或者NULL(指針類型)之類的空值。
■ 數組
C語言中的數組是一個連續表數據結構。默認組中每一個元素的類型都一致。經過type name[size]的方式聲明。如 double numbers[100]。注意這個100是len(numbers)而不是最後一個元素的下標。
數組在聲明的時候能夠進行初始化賦值,賦值等號右邊是一個大括號,如double numbers[5] = {1.1,2.2,3.3,4.4,5.0}; 固然此時大括號中元素個數不能超過聲明的數字,不然會報越界錯誤。相反的,初始化數組長度沒有到達聲明長度,那麼按照順序,已經給出初始值的元素按照初始值初始化,未給出初始值的元素則按照默認的初始值,好比int就是0,char就是'\0'初始化。若根本沒有初始化的時候,若是數組是一個全局變量,那麼符合全局變量的自動初始化慣例。好比int數組的未初始化元素會被賦值爲0或者0.0,另外好比NULL等等;若是數組是一個局部變量,此時數組中元素的值隨編譯器不一樣而不一樣。有些編譯器會比較友好地進行0值初始化,有些則不會,會給爲初始化的元素賦值隨機值。此時的數組中的就是一些垃圾值。
對於有初始化值的狀況,也能夠不寫明具體的數組長度。好比 int numbers[] = {1,2,3,4,5},這樣這個numebrs數組天然就被聲明成長度是5的數組了。可是沒有初始值的話,決不能不寫明數組長度。
C語言中天然是支持進行多維數組的處理的。多維數組只要在聲明的時候多加幾個中括號表示維度的增長便可。好比double numbers[3][4]就表明了三行四列的二維數組。對於多維數組的初始化,等號右邊有兩種方案:
int a[2][1] = { {10}, {20} }; int a[2][1] = {10,20};
這兩中方案均可以構建出一個2*1的二維數組,第一行的是10,第二行的是20。換言之,若是用一個一維數組去賦值一個高維數組,那麼編譯器會從高維數組的第一個元素開始,優先填滿一行,而後再去填下一行。按照如此規則主鍵賦值。
數組的取值,能夠經過經典的a[1]的形式,也能夠經過*(a+1)這種指針的形式。須要說明的是,後者實際上是前者的本質,並且在C語言中,程序不會對數組是否越界作出判斷或者警告。所以在寫C代碼的時候應該時刻警戒當前下標是否已經越界,要否則可能會讀寫到垃圾值。
關於數組,C語言中數組的坑還有不少不少,待把指針給說明完畢以後再繼續細說。
■ 指針
廣義上來講,指針是一個變量類型的總稱,這類變量的值是另外一個(指定類型的)變量的地址。C語言中的常見的類型都有相對應的一種指針類型,和使用其餘變量同樣,須要在使用以前進行聲明。聲明形式是type *pointer_name。其中type就是上面說的指定的類型。好比int *p就是指一個名爲p的 int型指針。它的值就應該是一個int型變量的地址。(可是實際上C語言中並無從語法上嚴格要求指針類型必定要被賦值成相應基本類型的地址,好比int *p;char c; 那麼也能夠作p=&c;,有的編譯器可能會給出警告,可是這不影響正常編譯和運行。可是應該要儘可能保證指針和變量類型的一致)
根據以前運算符的介紹,&var能夠將變量var的地址給得到到。所以,若是有int *p = &var; 那麼合理的狀況下,var應該是一個int類型的變量。
打印內存地址時可使用的格式化字符串是%p,因此有:
int main(){ int *p; int a = 1; p = &a; printf("%p\n",p); printf("%p", &a); return 0; }
這裏打印出來的兩個地址是相同的。
指針類型的變量除了直接調用其變量名獲得的是一個地址值外,還可使用*p的方式得到到其所對應的地址值對應的存儲塊中,到底存儲着什麼數據。
對於指針來講,其空值是C語言中的特殊值NULL。表明目前指針沒有指向任何其餘變量的地址。當指針是全局的變量時NULL也是初始化的默認值。當指針是局部變量時,若是沒有指定的初始化值,那麼比較好的一個習慣就是用NULL給予它初始化。如 int *p = NULL; 當指針沒有被給予初始化值,那麼指針會隨機指向一塊內存空間,而這個指針稱爲野指針。此時若是認爲 *p是對應某個變量的值,因此作了 *p = 1; 之類的操做,那麼這個操做顯然是非法的。正確的操做應該是 p = &a;。
● 指針的賦值以及合理的賦值形式
若是咱們有指針int *p,變量int a=1。那麼若是要讓p指向a而且可以經過p也能得到到a的值即1,那麼能夠 p = &a;。事實上這塊蠻麻煩的,下面是一些圍繞a, p, 1三個要素的一些賦值以及狀況說明:
int *p; p = &a; 正確,標準的作法。
int *p; *p = a; 編譯無措,可是危險操做,不該該作
int *p; *p = 0; 編譯無錯,運行報錯segmental fault
int *p; p = 0; 編譯無錯,可是是危險操做,不該該作
在解釋上面現象的時候首先應該要明確幾點(都是我本身YY的,有可能有錯)
一、 C語言中一個變量究竟是什麼。粗淺來講,咱們能夠認爲變量就是一塊有結構的內存塊。其內部內容大概包括了 變量名,變量地址和變量值,咱們分別以v.name, v.addr和v.value表示吧。對於普通的變量int a = 1;,這三者分別是'a',addr:1000(編的,下同),1。對於int *p=&a;這個指針變量而言這三者是'p', addr:1500, addr:1000。其中全部地址值加上了addr:前綴。
二、 再回頭看看&和*兩個運算符的意義。&運算符毫無疑問是取一個變量的地址。而*運算符後面若是是一個指針的時候,代碼中和表達式*p對等的,並非a.value,而是a這個總體。相應的,在代碼中和 p 對等的不是a的總體或者其餘的什麼東西,而是&a。
三、 指針被聲明以後若是沒有給予初始化值,那麼指針的.value是隨機的一塊內存空間的地址,姑且稱爲野空間,處於這個狀態的指針也叫野指針。這塊野空間的具體結構和規格由指針的類型指定。好比int *p對應的野空間天然是int規格的。對於野空間的直接使用是應該避免的。
如此上面的幾條就好理解了一些了。*p = 0報錯,由於0是一個常量,不具有0.value以及0.name等屬性。雖然能賦值,可是賦值以後*p對應的野空間是不合法的一個空間(何況仍是一個野空間); p = 0 屬於危險操做,由於至關因而將addr:0賦值給了p.value,可能會讀寫一些意想不到的數據;*p = a看似沒有問題,可是是將a複製了一份到了野空間,仍是使用了野空間,並且*p和a是互相獨立的,若是此時另a++,嘗試print *p值仍是不變的。
下面繼續說一下NULL:
NULL是一個常量,其值通常是內存中的addr:0x00000000。 另外,通常系統中都把addr:0x00000000附近的空間列爲系統級的空間,即用戶程序是沒法對其進行修改的。
同時注意一個小點,指針若是在聲明的時候初始化,那麼此時的int *p至關於通常代碼中的p而不是*p。
好了,看到 int *p = NULL; 這個初始化。因爲Int *p至關於p,等於說是將NULL表明的這個地址寫到了p.value中去。 此時若是進行 *p = a; 或者 *p = 0; 會由於0x00附近空間不可寫致使錯誤。整體而言,當使用了NULL對某個指針進行初始化,那麼必定要按照規範的 p = &a,來將p.value直接修改爲另外一個合法可寫的地址,避免對0x0空間作出修改而報錯。
另外再強調一下,&和*都是運算符,並不必定只能運算變量,還能夠算常量。因此對於int a[5]; *(a+1)這樣的操做也是可行的。
● 指針的運算
指針能夠進行+,-,++,--之類的運算。
上文說到指針聲明的時候要指明其指向的是哪一類變量的地址。而這個說明就決定了指針進行自增自減運算時的步長。好比int *p的地址位置是1000的話,那麼p++以後,p的值(或者說p指向的地址)就是1004了。由於一個int類型是4個字節的大小。對應的,char *c若是進行c++,那麼獲得的是1001號地址。
和它統一的還有通常的加減操做。好比仍是上面的int *p,若是p原先指向的地址是1000號地址。那麼p+10指向的地址不是1010而是1040。在地址加減計算中,程序爲咱們自動將步長乘了進去。
● 指向指針的指針
開始麻煩起來了… 剛纔說了指針在聲明的時候須要指出其指向地址的變量類型。若是這個類型自己也是一個指針的話,那麼本指針就是指針的指針了,姑且叫他二階指針。
表示方法是相似於 int **p;。 而*p在此時就是一個指針,**p纔是一個int型值。 關於二階指針的賦值稍微有點容易混淆的地方。其實咱們能夠把指針當作一種實現了賦值魔法方法的類,當一個地址值被賦值給指針時,它只是將這個地址值做爲一個重要屬性保存下來,而不是說指針就是一個單純保存地址值的變量。瞭解了這一點後咱們就能夠知道,對於二階指針p,進行 *p = &a; 這樣的操做是不對的。*p指向的是一塊野空間,沒有進行適配指針的初始化,此時將變量a的地址賦值給這樣一塊野空間是不會自動把*p這麼一個東西變成一個指針的。 正確的賦值能夠是 int *tmp = &a; 而後再 int **p = &tmp; 。經過明確聲明一個指針做爲中間人,將變量a和二階指針p聯繫起來。
對於int **p, int *t和int a,咱們考慮以下事實:
最標準的作法,顯然是 t = &a; 而後 p = &t;。這樣能夠作到一級一級指針準確指向到數據。可是若是奇葩一點,好比作一個 *p = &a,基於上面對於指針賦值機理的YY,能夠分析以下。首先int **p將開闢一片規格是int *的野空間,而開闢int *型空間的時候又會附帶開闢一個int型空間。這樣至關於int **p自己有兩級的野空間。 代碼中的*p對應的,是第一級野空間的總體,而&a顯然是一個addr:常量,因此賦值完以後並不合法。因此若是在這以後經過**p調用數值會報錯segmental fault。
而後來考慮另外一種奇葩賦值,*p = t(假設此時t已經=&a)。這種狀況下,**p能夠取到a的正確值。可是問題在於,此時*p和t並非同一個指針。只不過二者都指向了a。若是此時將t = &b換一個指向,再去看**p看倒的仍是a的值。這種狀況仍是用了野空間,因此仍是不規範。
● 指針做爲參數
上面提到過,好比swap交換兩個變量的值,在定義形參時只有用指針做爲參數才能順利交換二者。指針做爲形參時其實參能夠是一個指針對象或者一個地址值,若是是後者能夠認爲程序自動將基於所給的地址值建立一個指針對象。
因此能夠有下列程序:
#include <stdio.h> #include <time.h> void getSec(int *timestamp){ *timestamp = time(NULL); return ; } int main(){ int a; int *ap; ap = &a; getSec(&a); // 或者 getSec(ap); printf("%d\n",a); return 0; }
在加入了頭文件time.h以後,就可使用time函數獲取當前的時間戳,而後賦值給指針指向的變量。所以在main函數中,咱們能夠經過&a直接傳值給int *timestamp形參,也能夠構造一個int *ap,讓ap指向變量a,再直接把ap做爲實參傳遞給int *timestamp形參。不管是哪一種方式,getSec函數均可以將目前的時間的時間戳整型數賦值給變量a。
另外值得一提的是因爲數組名作變量名的變量其實是數組頭指針,因此「數組」也能夠做爲實參傳遞給 「以指針作形參」的函數。數組和指針之間千絲萬縷的聯繫下面再談。
● 指針做爲返回值
int * function_name(param)就是一個返回地址的函數。其實能夠發現,不論聲明的函數返回是哪一種基本類型的指針,因爲函數返回的是值而不是變量,至關於最後返回出來的都是p.value即一個地址值。固然,若是返回的地址值,所對應存儲快中的數據類型和函數聲明時的不同,編譯器有時會給出警告。
值得注意的是,返回函數中的局部變量的地址沒有什麼意義,由於函數一旦執行結束,這些地址中存儲的數據都將會被銷燬。因此若是是返回函數體中某個變量的地址的話,最好能保證它有static修飾(這樣函數執行結束以後仍然能夠保持存在)。在返回局部變量的地址的時候,編譯器不會報錯可是會給出警告。
■ 數組和指針
C語言中的數組名,它所表明的實體並非像Python中同樣的一個列表,而是數組第一項的地址。所以對於一個數組有如下事實:
a == &a[0]
a[i] == *(a + i)
固然上述a[i]的i是要小於數組長度的。另外值得注意的是,若是數組沒有進行初始化,取a[0]並不會報錯,雖然這是危險的。由於數組變量名本質是一個指針,a[0]的本質是也就是*(a+0),也就是說直接取存儲在a地址內的值。
聲明一個數組時,同時也聲明瞭一個指針,並且這個指針所指的空間(即數組第一個元素的空間)並非野空間,而是能夠直接拿來用的。好比咱們能夠直接a[0] = xxx對此空間賦值。
在定義函數的時候,若是定義了一個形參是好比int a[],那麼這個形參和int *a的效果是同樣同樣的。函數體內的用法也徹底同樣。
上面說了不少數組和指針同質化的提示,可是二者在一些場合也不一樣。好比在接觸數組的時候,咱們常常會碰到的一個問題就是數組的長度。在C語言中沒有像java中.length那樣這麼方便的接口。一般計算一個數組的長度要經過sizeof(a)/sizeof(a[0])這樣的操做來作。假設咱們有int a[5]; int *p = a;。那麼sizeof(a)和sizeof(p)分別是多少?實際上是20和8。8是統一的指針這一大類變量的size,這無關指針具體指向什麼類型的變量。20則是4 * 5,即一個int是4,5個int的大小。
下面看一段程序:
#include <stdio.h> double getAvg(double a[],int size){ // 徹底能夠改爲double *a int i; double sum = 0.0; for(i=0;i<size;i++){ sum += a[i]; } return sum / size; } int main(){ double a[5] = {1,4,6,3,3}; double *p = a; int size = sizeof(a)/sizeof(a[0]); // 不能改爲sizeof(p)/sizeof(p[0]) printf("Average: %f",getAvg(p,size)); // 能夠改爲getAvg(a,size) return 0; }
總的來講,數組名基本上表明瞭第一個元素的存儲地址,除了少數狀況(好比sizeof計算的時候)。
● 高維數組和高階指針(實驗環境的編譯器是gcc4.8.5,對於不一樣編譯器可能會有不一樣的結果)
試着考慮一個二維數組 int a[2][3] = {{0,1,2},{3,4,5}};
在這裏,數組名a其實表明的是一個二階指針。至關於int **a。若是將0,1,2和3,4,5看作是一個數組的兩行的話,那麼其實*a就是第一行數組,*(a+1)顯然是第二行數組。而*(a+2)就越界了,可能取到垃圾值。
之因此說*a是第一行數組,而不說是第一行數組的指針,是由於sizeof(a)顯然是24,可是sizeof(*a)不是8而是12,說明*a實質上等價於 int b[3]這個b。這點還須要記住。
下面要說一個高維(二維)數組和一維數組很不一樣的一個地方。上面對於一維數組的說明其實能夠看到,一個數組的變量名a剛聲明的時候,含義是一個完整的數組。而對於一個二維數組int a[M][N]而言,常數N實際上是數組a的一個固有屬性。也就是說,若是C語言中有個type函數用來提示類型,那麼int a[]的type(a)返回的多是int[],而int a[M][N]的type(a)返回多是int [][N]。
這麼說的證據之一就是在聲明二維數組的時候在有初始值的狀況下能夠寫int a[][N],可是絕對不能寫int a[M][]。第一維參數省略的時候,程序會按照給出的第二維數據去套初值,套完第一行再套第二行,直到初值用完爲止。可是後一種狀況,不符合正常邏輯,編譯器就無從得知如何初始化這個數組。
另一方面,假如咱們編寫一個函數要接收一個二維數組做爲參數。在一維數組的場合,這個參數能夠寫成int *p或者int p[]之類的。可是二維數組,雖然它是一個二階指針,可是不能把參數寫成相似於 int **p,也不能寫成int a[][]。正如上面所說,第二維長度是二維數組的固有屬性。所以必須寫成相似於int a[][5]或者int a[3][5]也行,可是int a[3][]不行。看下面的程序:
#include <stdio.h> void printMatrix(int p[][5],int row,int col){ int i,j; for(i=0;i<row;i++){ for(j=0;j<col;j++){ if(p[i][j] == 1){ printf("%s ","■"); } else{ printf("%s ","□"); } } printf("\n"); } } int main(){ int matrix[][5] = { {0,1,0,0,0}, {0,0,0,1,0}, {1,0,0,0,0}, {0,0,0,0,1}, {0,0,1,0,0} }; int row = sizeof(matrix) / sizeof(matrix[0]); int col = sizeof(matrix[0]) / sizeof(matrix[0][0]); printMatrix((int **)matrix,row,col); return 0; }
* 通過檢查,參數不能寫int **p主要是由於gcc這個編譯器將數組和指針是作了比較明確的區分的。可是若是必定要用二階指針做爲參數也是能夠的 ,這主要就是須要一個強制類型轉換:
(int **)p。然而在這麼轉換過以後,對於取數方法也就不能簡單地p[i][j]這樣作了(會報錯),由於p[i]等價於*(p+i),而此時的p仍然須要作強制的類型轉換,所以應該寫成*((int *)p + i)。關於強制轉換類型的更多信息下面會提到的。對於數組、指針、二維數組的應用也能夠隨着使用慢慢熟悉起來,確實比較複雜…
■ 字符串
C語言中沒有想Java或者Python那樣直接的字符串類型。字符串被當作是一個結尾爲\0的char類型的一維數組。所以C語言中的字符串是如此定義的:
char s[] = {'a','b','c'}; 這類集合賦值形式只能出如今初始化語句中。
固然,每一個字符串要都拆成這樣的字符寫也太不友好,所以也支持更加方便一點的字符串操做方式:
char s[] = "abc"; //注意雙引號的區別
● 字符數組和字符指針的聯繫與區別
在C語言中並不存在一種字符串數據類型。通常而言,一個邏輯上的字符串能夠經過字符數組or字符指針兩種形式來表明。正如通常意義上的數組的數組名和(數組首)元素指針同樣,二者存在着很大共性。可是因爲代碼中字符數組除了{'a','b'}這種形式以外還有"ab"這種形式存在,使得字符數組在初始化完成以後還有機會做爲一個總體對其做出改變,因此區別於其餘類型的數組&數組首元素指針,字符數組和字符指針之間還存在一些應用上的差異。
首先咱們須要明確,形類於 char s[10] = "abc"; 的聲明獲得的s是一個字符數組。其長度能夠指出或不指出。不指出時必定要帶上初始值不然沒法判斷數組該申請多大的內存空間。這個聲明出現的時候程序會申請一塊指定大小的內存空間,並以s指代這一整塊空間。
對應的,形如 char *s = "abc"; 的聲明獲得的s是一個字符指針。這個聲明的操做是在內存中申請一塊能夠保存"abc"這個值的內存空間,而後將內存空間的首地址賦值給s這個指針變量。
基於上述認識,咱們能夠說出二者存在如下區別(部分一樣適用於其餘類型的數組):
1. char s[20]和char *s二者自己大小不一樣,用sizeof函數能夠看出。天然他們佔用的內存空間也是不一樣的,由於都不是同一種東西
2. 對於char s[20],s的本質是一塊存儲空間首地址,能夠視爲一個常量。在s的生命週期中,這個值是不可改變的。所以作相似於s + 1之類的操做是錯誤的。
相反,char *s是一個地址指針,能夠輕鬆地作s + 1。
3. 未給出初始化數據時,若s是一個數組,是個地址常量,因此能夠作s[1] = 'a'這樣的操做。相反,char *s做爲一個指針,s[1] = 'a'是危險的。
4. 因爲char s[20]做爲一個數組時,s是一個地址常量。因此除了初始化的時候,以後就不能再進行s = "hello";之類的賦值。至關於對常量賦值。
上面兩個聲明我都沒有顯式地指出char數組的長度,所以能夠寫任意長度的字符串賦初值。若是採用顯式的帶有數組長度的聲明,那麼和普通數組同樣,對於未被賦予初始值的元素會被賦值爲\0。
C語言中的字符串還有一點很麻煩的就是結尾的處理。在賦值的時候,好比char s[] = "Hello"或者char s[] = {'H','e','l','l','o'};, 賦值完成後若是使用sizeof函數查看s的長度,看到的每每是6,也就是比字符串自己長1個單位。這是由於字符串在聲明的時候會自動將'\0'這個字符串終止符做爲尾巴添加在字符串末尾(對於第二種聲明方式,有些編譯器不會自動加'\0'。此時就比較危險)。
保證一個字符串的末尾始終是'\0'字符有不少意義,其中一個就是打印字符串時,使用%s這個格式化字符串的時候。printf的格式化字符串中若是有%s,那麼其行爲就是以給出的char *s指針做爲起始點,不斷地一個個讀取字符,直到讀取到'\0'。若是不在咱們的字符串末尾加上'\0',就頗有可能會看到一些垃圾值。
C語言內置了一些字符串處理函數,只須要引入頭文件string.h即可使用:
strcpy(s1,s2) 將s2字符串整個賦值給s1。這種賦值是一種深拷貝。因爲C語言中字符串是char類型的數組,因此其應該有一個固定的聲明長度。若是s1長度自己就大於s2倒還好說,若是反過來,那麼就可能會出現s2賦值超出s1的範圍?
strlen(s) 返回字符串s的有效長度。注意這個有效長度的意思是說s從頭開始,一直掃描到第一個\0時的長度。
strcat(s1,s2) 將字符串s2接續到s1的末尾。一樣須要注意長度、越界等問題
strcmp(s1,s2) 字符串比較函數。若是s1和s2相同則返回0。根據ascii碼大小逐字符比較並返回1或-1表示s1>s2或s1<s2。
strstr(s1,s2) 返回s2做爲子字符串第一次出如今s1中的位置指針。
strchr(s1,char) 返回char在字符串中第一次出現的位置指針。
■ 結構體
都說C語言是一種面向過程的語言,但其實在其身上已經能夠看到OOP的雛形。這就是結構體這種東西了。以OOP經常使用的例子,書本類做爲例子。在C語言中定義一個書本結構體:
struct Books{ char title[50]; char author[20]; char comment[100]; int book_id; };
在這個語境下,struct Books和int, char同樣,屬於一種數據類型。能夠調用sizeof(Books)查看一個結構體的實例佔據多少空間。而使用這個實例(包括實例中的各個屬性)就是和其餘語言同樣使用小圓點就能夠了。下面是一段使用實例的程序:
#include <stdio.h> #include <string.h> struct Book{ char title[50]; char author[20]; char comment[100]; int book_id; }; void show_bookinfo(struct Book); int main(){ struct Book book; strcpy(book.title,"To kill a mocking bird."); strcpy(book.author,"Happer Lee"); strcpy(book.comment,"A very good book."); book.book_id = 1022; show_bookinfo(book); return 0; } void show_bookinfo(struct Book book){ printf("[%d]title: %s\n",book.book_id,book.title); printf("Author: %s\n",book.author); printf("Comment: %s\n",book.comment); }
*注意到C語言中對於字符串不能直接賦值。由於字符串變量本質是char數組,於是其變量名是一個char *類型地址。
和其餘基本類型同樣,全部自定義的結構體(or類)也有各自對應的指針。聲明方式就是相似於struct Book *bookp這樣子。除了通常的指針的用法,自定義結構體的指針還有一個比較方便的做用,就是能夠直接調取其指向的實例的各個屬性。使用的運算符是 ->。
好比將上面代碼修改爲:
#include <stdio.h> #include <string.h> struct Book{ char title[50]; char author[20]; char comment[100]; int book_id; }; void show_bookinfo(struct Book *); int main(){ struct Book *bookp; struct Book book; bookp = &book; strcpy(book.title,"To kill a mocking bird."); strcpy(book.author,"Happer Lee"); strcpy(book.comment,"A very good book."); book.book_id = 1022; show_bookinfo(bookp); return 0; } void show_bookinfo(struct Book *p){ printf("[%d]title: %s\n",p->book_id,p->title); printf("Author: %s\n",p->author); printf("Comment: %s\n",p->comment); }
● 位域結構體
剛纔上面的結構體中,全部成員屬性的類型都是通常的數據類型。好比int,char []等。固然也能夠在結構體的屬性中包含結構體。好比struct Class中包含struct Student。
同時,C語言還支持在結構體中設置位域的成員屬性。所謂位域,就是指把C語言中對內存空間最小操做單位由C語言封裝好的各個類型,下降爲一個個字節。好比我在struct Book結構體中想要加入一個是不是中文書的字段is_chinese。顯然這個字段取值通常只會有0和1,若是將其規定爲int類型,顯然會浪費比較大的空間(int有4個字節32位大小)。
此時能夠將這個字段設置爲 int is_chinese:1;。經過冒號加上一個數字,指出這個字段只佔據一位便可。這樣能夠節省大部分空間。固然做爲一個基本類型仍是int的數據類型,冒號後面的數字必須小於等於32(4*8),不然編譯器會報錯。
關於位域,還有一個比較有意思的用法,就是在結構體中固定空出一片空間。好比下面這個位域:
struct s{ int a:1; int :2; int b:1; }
存在一個無名的位域,所以咱們沒法調用。可是這片兩位的空間卻又是實實在在存在的,所以這個結構體中就固定會有一片空出來的空間。
這麼作的目的多是防止屬性間跨屬性地修改數據或者其餘一些考慮。
● 配合typedef關鍵字
typedef能夠理解爲一種整合複雜C語言類型描述的辦法。好比咱們常用unsigned int這類類型來表達字節,因此能夠在C語言文件最開始的時候進行 typedef unsigned int BYTE。在以後的程序中,咱們就能夠直接使用BYTE來聲明unsigned int類型的變量。爲了提示typedef出來的類型是咱們人爲自定義的,一般會將類型名全大寫。
上面的程序中,咱們定義了struct Book。可是實際使用過程當中,聲明Book類型變量時仍是寫了struct Book,有點使人費解。因此在定義的時候,能夠採用:
typedef struct Book{ char title[50]; char author[20]; char comment[100]; int book_id; int is_chinese:1; } Book;
這樣在後續的程序中就能夠直接將Book做爲類型名直接使用。須要指出的是上面的typedef struct Book能夠直接改爲typedef struct,至關於指出這個結構體自己是個匿名結構體。但大括號後面的Book是它的alias名,不能省略。
■ 輸入輸出
● printf和scanf
printf函數上面用了不少了。scanf函數用於從stdin(鍵盤)獲取數據並進行格式化。用法如scanf("%d",&a); 其中a是一個特定變量的地址。至關因而說,scanf的做用是接受輸入流的一個片斷(以空白字符爲界),將這個片斷以第一個參數格式化字符串給出的格式轉化成相應格式的數據。而後再將這些數據給存入到第二個參數給出的地址中。
因爲是以空白字符爲界的,因此當輸入是"abc def"的時候,若是是scanf("%s",s);,那麼字符串變量s的值就會是"abc",而不是"abc def"。另外一方面,scanf("%s %s",s1,s2),若是輸入仍是"abc def",此時能夠將abc和def分別賦值給s1和s2。此時先讀取到了buffer中的第一片斷abc,賦值給第一個%s對應的變量。因爲還存在第二個%s,因此繼續向後掃描,掃描到def後賦值給第二個%s對應的變量。
另外scanf是掃描stdin的buffer,只有當buffer中沒有內容時纔會阻塞stdin,達到python中相似於raw_input那樣的效果。若是buffer中還殘存內容,那麼scanf就會自動讀取那些內容,即便這些內容是上一次掃描剩下的。
● putchar和getchar
若是你試圖從stdin中獲取到一個字符,使用scanf函數能夠是scanf("%c",&c);。c是一個聲明爲char類型的變量。這樣一個過程能夠用getchar一個函數來表示。
如 char ch; ch = getchar(); 這樣能夠將stdin中下一個字符讀入而且賦值給ch變量。
相似的,putchar函數接受一個字符類型數據(變量或常量),而且將其輸出到stdout中。定義是 void putchar(int);
因爲C語言中字符本質上都是經過ascii碼規則映射的整型數,因此上述函數傳值or取值符合規定的整型數在語法和邏輯上也是正確的。
● puts和gets
這兩個函數的聲明分別是 int puts(char *); char *gets(char *);
其做用和putchar,getchar相似,只不過針對的操做對象是字符串。gets在一些Linux環境編譯器中被認爲是危險操做,編譯時會給出一些警告。
Linux中可使用fgets和fputs函數代替這兩個。這兩個函數的用法是:
fgets(string, 10, stdin);
fputs(string, stdout);
前者的10表示只從stdin中讀取10個字符,藉此能夠控制不越界賦值字符串。通常能夠fgets(string,sizeof(string),stdin);