C程式設計基礎

發表於2015-12-21

1. Hello World!

依照慣例首先Hello World鎮樓:

1 #include<stdio.h>
2 
3 int main(void) {
4      printf("Hello World!\n");
5         return 0;
6  }

 C原始檔組成:

   (1) 預處理指令(不是c語句)

   (2) 函式和外部變數宣告(c語句)

   (3) 函式定義

       1) 函式頭部

       2) 函式體

2 . 資料型別

C的資料型別分為基本型別和構造型別。其中基本型別包括位元組型(char)、整型(int)、浮點型(float)、雙精度浮點型(double)。構造型別是指在基本型別上構造的陣列,指標和結構體,列舉,聯合等資料型別。

1. 基本型別

(1)整型

1. 整型(int):32bit OS下一般為4 bytes,INT_MAX巨集標記了最大的int數值:2147483647,(2^31 - 1)。

2. 字元型(char):依據C標準定義,在任何環境下sizeof(char)都是1。儲存字元的ASCII碼(二者無條件等價),可以參與算術運算。

3. 短整型(short int):C標準規定short int長度不比int長,具體長度由編譯器自定。

4. 長整型(long int):C標準規定long  int長度不比int短,具體長度由編譯器自定。

包含<stdint.h>標頭檔案後可以使用int32_t、int_64t等型別,可以實現32位,64位整數的運算。

(2)浮點型

1. 單精度浮點型(float):4 bytes,6位有效數字,絕對值範圍3.4E38。在計算時自動提升為double型。

2. 雙精度浮點型(double):8 bytes,15位有效數字,絕對值範圍:1.7E308。用於儲存實數,由小數部分和指數部分組成(均為二進位制),由於小數點的位置可以浮動(調節指數保證恆等)所以稱為浮點數,但在計算機中用規範浮點型別(小數點前為0,小數點右第一位不為0)。

浮點誤差:

因為計算機中採取二進位制指數儲存浮點數,並因為指數計算或者截斷導致存在誤差。通常定義一個很小的正數eps作為浮點誤差限,當浮點數小於它時就認為浮點數為0。

由於浮點誤差的存在,應儘量避免使用浮點數進行流程控制,如果必須使用浮點數則要避免使用相等(==)或不等(!=)關係運算。

2.構造型別

1)結構體

struct StructName {

     type1 member1;

 ...

     typen member;

} object1,...,objectn;
struct Node {

    int val; 

} node_a;

在成員表列中宣告結構體成員,格式同宣告變數。成員可以是任何資料型別(變數,陣列,指標也可以是其它構造型別),但不能是其自身(這樣會無限巢狀)。

 

結構體型別是其成員的集合,成員結構體型別的長度不小於成員長度之和(參見“記憶體對齊”)。

只能訪問或修改結構體中基本型別成員,不能直接修改結構體自身。

結構體是一種重要而靈活的構造型別,對於結構體的擴充產生了類(class)這一偉大的概念。

2)聯合

union UnionName {

     type1 member1;

     //...

     typen member;

} object1,...,objectn;

幾個不同的變數共享同一段記憶體的結構稱為共用體型別(union,聯合)。因為在每一個時段,同一段記憶體只能存放唯一內容,也就是說union變數中只能存放一個值。

可以將union理解為VB中的變體型,通過引用不同的成員而變為不同型別的變數。

 

可以對union初始化,但初始化表中只能用有一個常量。

 union UnionName Object={.member = var}; 

當省略成員名時對第一個成員初始化。

 

同類的struct/union物件可以互相賦值。struct,union同樣也有陣列。

在函式呼叫過程中,對於struct通常傳遞其指標而非物件本身以減少開銷。

3)列舉

enum EnumName {

    member1,member2 = var, ...

} object1,...,objectn;

列舉元素表列由幾個列舉元素名(列舉常量名)組成,中間用逗號分隔(類似初始化表列),每一個元素代表一個整數,按定義順序預設為0,1,2…。也可以用賦值語句進行強制賦值,如{a = 1 , b = 2 }。可以宣告列舉物件,列舉元素也可以直接使用。

#include<stdio.h>

enum Num {zero, one};

int main(void) {

    enum Num n = one;

     printf("%d %d\n",n,one);

     return 0;

}

列舉變數,簡單巨集,常變數(const)是C中常用的使用符號常量的方法。

通常將struct、union和enum的第一個字母用大寫表示以和系統定義的型別名區別(這不是規定只是習慣)。

3. typedef關鍵字

typedef關鍵字用於為一個型別生成一個別名:

typedef Old New;

在使用typedef後Old和New均可以作為型別關鍵字定義變數。Old是在使用typedef之前已經存在的型別關鍵字,它可以是基本型別(如 int),構造型別(如int *)或者自定義型別(如struct node)也可以帶有關鍵字修飾(如const,static)。

typedef可以定義一個型別名代替一個定長陣列 typedef int arr[Size];  ,arr a可以像int  a[Size]一樣定義陣列。

 

因為自定義型別需要struct等關鍵字修飾,通常使用typedef關鍵字簡化,如:

typedef struct Node {

      int val;

} Node;

 使用上述語句後,Node 即可代替struct Node作為型別關鍵字。實際上struct Node連同其定義一起充當了Old型別名。

typedef struct Node {

      int val;

      struct Node *next;

} Node;

這種定義在鏈式儲存結構中常見,第3行中的struct關鍵字不能省略因為此時typedef語句尚未定義Node作為型別別名,而struct Node則在花括號開始處即生效。

typedef關鍵字定義函式指標型別:

typedef (*ptr)(...);

 

呼叫 (*ptr)(...); 

 

簡單巨集也可以實現型別別名的功能,但是typedef定義的功能更為強大。如 typedef int* ptr; ptr p,q;    

 #define int* ptr { ptr p, q; }  則定義int指標變數p,和int變數q。

建議使用typedef關鍵字定義型別別名而不是使用巨集。C++繼承了C中typedef關鍵字的用法,由於類别範本和名稱空間使得名稱複雜typedef起到了更為關鍵的作用。

 

4. 字面值

直接書寫於原始碼中的值稱為字面值,如0,1等。字串也常以字面值的形式出現,如"Hello World!\n"。字串字面值將於字串一節說明,其他型別的字面值往往會使閱讀者無法得知其含義(所謂魔數magic)。除了0,1等含義明確的字面值外,其餘字面值應使用巨集或者常量,以提高程式碼可讀性和可維護性。

3.變數(物件)

在C中,記憶體中的物件一般被稱為變數,而在大多數面嚮物件語言中它們被稱為物件。以物件的觀點理解變數比較容易理解諸如常變數之類的概念。

 

C為靜態型別語言,物件的型別在定義後不能改變。C使用型別關鍵字+識別符號來定義物件,如int a;。識別符號可以由字母、數字或下劃線(_)組成,不能以數字開頭,區分大小寫,不能與關鍵字相同。 定義變數後C不會自動初始化,必須顯式的初始化int a = 0;。

 

作用域是指一個變數有效的範圍,C變數的作用域包括檔案作用域和程式碼塊作用域(函式體也是一個程式碼塊)。變數的生存期則是指物件記憶體空間從開闢到釋放的週期,一般非靜態變數的生存期是其作用域程式碼塊執行期。

 

具體變數的使用方式和特點如下表:

<1>自動變數 / 暫存器變數:

   定義:函式內auto或預設關鍵字宣告;

        函式內register宣告。

   作用域:函式內(空連結)

   生存期:自動(函式呼叫期)

<2>空連結靜態變數

   定義:函式內static關鍵字宣告

   作用域:函式內(空連結)

   生存期:靜態(程式執行期)

<3>外部連結的靜態變數

   定義:函式外extern或空關鍵字宣告

   作用域:所有程式檔案(外部連結)

   生存期:靜態(程式執行期)

<4>內部連結的靜態變數

   定義:函式外static關鍵字宣告

   作用域:本原始檔(內部連結)

   生存期:靜態

 

   暫存器變數位於CPU暫存器中,呼叫較快。編譯器會將呼叫頻繁的變數自動存入暫存器中以提高效率。靜態變數在函式呼叫結束後不釋放,下一次呼叫保持原值,外部變數不需要static生存期即為靜態。

常物件與const關鍵字

    對變數使用const宣告,則此變數只允許呼叫不允許改變它的值。

1)對指標使用const

   位於*左邊任意位置的const使得指標指向的資料成為常量,位於*右邊的const使得指標本身成為常量。靠近變數的使指標變數成為常量,靠近型別的讓指向型別成為常量。

   在函式原型和函式頭部,參量(const 型別名 陣列名[])(const 型別名 *指標名)表明陣列中的元素是不允許改變的。使用const關鍵字可對陣列提供保護(就像傳值對基本型別提供保護一樣),避免陣列被意外修改。

2)對外部變數使用const

   使用外部變數時容易因為變數意外被修改而造成不易察覺的錯誤,使用const將為外部變數提供保護。外部常變數可用於重置變數,特別是重置指標變數。

4. 運算子、表示式和語句

C表示式由運算元(operand)和運算子(operator)組成, 每一個表示式有且只有一個值。表示式可以結合,複雜表示式的求解順序由運算子的優先順序和結合性來確定。

運算子可以粗略地分為初等運算子(() . -> [] ),單目運算子(! ++  -- sizeof  & * cast運算子…),算術運算子(+ - * / %),關係和條件運算子(== != > < ?:…),賦值運算子(= +=…),逗號運算子(,)。優先順序從高到低,除單目運算子,賦值運算子和條件運算子從右向左結合外,其餘運算子都是從左到右結合的。

短路運算子

雙目關係運算子和條件運算子均為短路運算子。以邏輯與(&&)運算為例,表示式0&&(i++),因為左值為假,表示式一定為假,此時右值表示式不求解,i的值不自增。為了避免短路運算子產生的錯誤,應避免將具有副作用的表示式寫入短路運算子的表示式中。

副作用(side effect)與順序點

副作用是對資料物件或檔案的修改。從C的角度來看,主要目的是對錶達式求值。自增(減)運算子和賦值運算子主要因為副作用而被使用。

順序點是程式執行中的一個點,在該點處所有副作用都在進入下一步前被計算。分號和完整的表示式(即該表示式不是更大表示式的一部分)都標記了順序點。

當在一個表示式中存在多個有副作用的運算時,C標準不規定副作用生效的次序只保證在該語句結束後所有副作用均已生效。

 

常用運算子

(1) 賦值運算子(=,+=,…)

 左值: 賦值運算子左側標識物件或表示式

 右值: 可以賦給左值的常量,變數或表示式

 

複合賦值運算子 +=,-=,*=,/=,%=,^=: 對左值和右值進行+,-,*,/,%,^運算,並把結果賦給左值。所有算術運算子均具有對應的賦值運算子。

在C中,賦值是一種運算而不是特殊的指令,賦值表示式的返回值是賦值後的左值。運算的屬性允許更靈活的操作,如連續賦值,利用返回值等。由於副作用順序的不確定性濫用賦值運算將會導致嚴重錯誤,儘量使用簡單、單義的賦值運算,嚴禁賦值運算與自增(減)運算子同時使用。

(2)sizeof

 以位元組為單位返回運算元的大小(在C中,一個位元組被定義為char型別所佔空間的大小)。

運算元可以是一個具體的資料物件(例如變數名),也可以是一個型別名。如果它是一個型別,運算元必須被括在"()"中。

(3)自增(減)運算子(++,--)

使運算元加1(++)或減1(--)。在字首模式下(++a)先改變值再呼叫(即表示式的值為原值),字尾模式下,先呼叫再改變值。何時改變由順序點決定,C只保證在語句執行完時一定。

(4)逗號運算子

用於將多個表示式並列,表示式值為右側表示式的值。優先順序最低,從左向右結合。常用於for迴圈等語句中。

(5)強制型別轉換與指派運算子

完成強制型別轉換的方法稱為指派(cast)。圓括號與型別名組成指派運算子。

       (type)operand

用於臨時轉換型別,不對變數造成影響。降級運算採用截斷(直接捨棄)的方式進行。

 

(6) 函式呼叫運算子

沒錯,函式呼叫時包含參數列的那對圓括號也是運算子(初等運算子,最高優先順序)。

將函式呼叫看作運算子將會便於以後的理解,特別是函式指標。

C標準將函式呼叫視為一種運算,C++標準明確將其作為運算子並允許過載(詳見C++中關於運算子過載的說明。

 

2. C語句

語句是C程式最基本的單位,C語句以分號(;)作為結束標誌,一個語句可以寫多行,一行可以寫多個語句。

為了保證程式可讀性,儘量每行寫一個語句;C語句的巢狀關係與縮排無關。

(1)宣告語句

  宣告語句用於宣告(定義)型別,物件和函式。宣告與定義有所差別,宣告只是告知編譯器物件或函式的存在和識別符號;定義則是指物件和函式已經處於可用狀態,型別的所有成員已定義,物件已開闢記憶體空間並初始化,函式已經實現可以呼叫。

方括號([]),指標(*)這些符號在宣告語句和執行語句中的含義有所不同,但依舊具有運算子的一些特性,這些特性有助於理解一些宣告。這一觀點將在《C指標與記憶體》中說明

(2)執行語句

C的執行語句絕大多數都是表示式語句,即表示式加“;”。

函式呼叫和賦值也可以認為是表示式語句。

(3)流程控制語句

包括條件語句if-else,switch,迴圈語句while,do-while,for輔助語句break,continue,goto語句以及return語句。

 

(4)複合語句

用花括號{}括起來的語句組成一條複合語句(程式碼塊)。在複合語句中宣告的變數的作用域為程式碼塊級,即只在程式碼塊中有效,並遮蔽同名的函式級區域性變數和全域性變數。

 

(5)空語句

只有一個分號的語句一般起佔位的作用,如表示空迴圈體。

5. 流程控制

(1)選擇結構

 1) if語句

    if(condition) 

    statement;

      else if (condition)

    statement;

      else 

          statement;

 

statement表示一條C語句,可以是簡單語句也可以是由花括號{}括起的複合語句。

即使只有一條簡單語句也應儘量使用花括號避免二義性或修改後產生錯誤。

else自動與最近的未配對的if配對,與縮排無關。if與else數量不同時,注意使用花括號保證匹配正確。

2)條件表示式

     condition?true_expr:false_expr

當condition的值為真時,條件表示式的值為true_expr的值;否則為flase_expr的值。條件表示式為短路運算子,當判斷為真時false_expr不計算,為假時true_expr不計算,當表示式中存在有副作用的運算時需多加註意。

3)switch語句

    switch (condition){

      case flag:

    statement

        ...

       case flag:

       statement 

       default:

     statement

     }

    

condition與某一flag相等時,從此case開始向下執行至switch結尾 , 不再進行判斷(包括default後的語句),一般用break語句來跳出switch結構。

當表示式的值與所有case標號都不符時執行default標號後的語句,無default則跳出。

 

2.迴圈結構

1) while迴圈

while (condition) 

  statement

計算condition若為真則執行statement,再次計算condition直至condition為假時結束迴圈。

 

2)do while 迴圈

  do {

   statement

  } while(condition);

  先迴圈後判斷。

3) for迴圈

  for (init;condition;update)

      statement

 

迴圈變數增值常用自增(減)運算子,多用於計次迴圈。

三個語句可以是逗號表示式語句或空語句,但不允許缺失或多個語句且語句順序不允許顛倒。

 

for語句流程說明:

<1>執行init,即賦初值。

<2>計算condition,為真則執行迴圈體,假則結束迴圈。

<3>執行update,返回<2>。

 

3.輔助流程控制 

 

break;

 該語句跳出迴圈或switch語句。

 

continue;

 跳至迴圈體末端,接著執行下一迴圈。

 

goto 標號;

標號: 語句;

儘量避免使用goto語句,唯一可被大多數人接受的goto是用來跳出巢狀的迴圈。

一些必須注意的細節:

(1)迴圈巢狀時內層迴圈每一次進入均需考慮是否對其中變數再次初始化。

(2)在迴圈語句中, 當迴圈變數恰滿足條件時還要執行最後一次迴圈並且更新。 當迴圈結束後,迴圈變數是恰好不滿足條件的值而非恰好滿足條件的值。

 一些有用的小技巧:

(1)while(1)與if(){break;}及更新語句的搭配可以任意的設計迴圈流程而不必拘泥於三種迴圈語句的流程。

(2)在搜尋等的函式中設定多個出口,在迴圈體中設定匹配時出口,迴圈體外設定不匹配時的出口。可以繞過通過返回的迴圈變數判斷搜尋結果這一易錯環節。

示例:判斷一個大於2的整數是否為素數

 

int isPrime(int a) {

         int i;

         for (i=2;i<a;i++) {

            if (a%i == 0) {

               return 0;

            }
  
         return 1;

}

 

(3)邏輯標記:

在搜尋過程中常要求找到目標後就跳出迴圈,若根據返回的迴圈變數判斷則難以區分是未找到還是最後一個成員即為目標。可以設定一個每次進入迴圈都被設定為真(1)的變數,如果發現不符合條件則置其為假(0)利用該標記判斷匹配結果。

     示例:

 int main() {

          int i,n,m,t;

          scanf("%d",&m);

          for (n=3;n<m;n++) {

             t=1;

             for (i=2;i<n-1;i++) {

                if (n%2 == 0) {

                   t=0;

                   break;

              }

            }

            if (t) {

              printf("%d\n",n);

            }

          }

        return 0;
}  

(4)靈活的退出方式

       (1)與if(){break;}及更新語句的搭配可以任意的設計迴圈流程而不必拘泥於三種迴圈語句的流程。

(5)跟蹤變數

       在遍歷單連結串列等問題中存在更新後就無法取得上一個值的變數(ptr=ptr->next;),此時可以設定另一個變數記錄更新前的值:

void traver(Node *head) {
     Node * ptr = head, * prior ;
     while (ptr->next != NULL) {
           prior = ptr;
           ptr = ptr->next;
           use(ptr,prior);
     }  
  
}    

6. 函式

函式是C程式的基本模組,函式可以將功能封裝只通過介面來使用,提高開發效率和程式安全性。

函式需要先宣告(定義)再呼叫,C允許先宣告函式而不同時定義。

 

函式宣告包括返回值型別、函式名、參數列以及修飾關鍵字組成。

如包含於<math.h>中的double pow(double x, double y)接受兩個double值作為引數,返回一個作為結果的double值。

編譯器只關注形參的數量和型別不關注形參的具體名稱,也就是說前置宣告與後置定義的形參名稱允許不同。當然,定義時實參名稱與函式體之間必須是對應的。

馮諾依曼體系結構的計算機中指令與資料同樣以二進位制儲存於儲存器中,C函式在被呼叫前形參和區域性變數沒有開闢相應的記憶體空間,在函式被呼叫後才會開闢空間。

引數及其傳遞

C函式中引數傳遞採用傳值的方式,即形參是實參的一個副本,形參的修改不會影響實參。傳值的方式使得只要型別符合的值均可以作為實參,除了變數外還可以是字面值,表示式,函式返回值;傳址方式下只有存在於記憶體中的物件才有地址,只有物件才可以作為引數。

傳址方式編寫函式更加方便自由,C通過傳遞指標(物件的地址)的方式來實現傳址呼叫。傳址呼叫的說明詳見指標。

void關鍵字置於形參表,表示函式沒有引數int fun(void);;也可以使用一個空括號表示int fun();。在呼叫時只能採用空括號fun();而不能使用void關鍵字fun(void); 。

C函式將陣列作為指標處理,在傳遞陣列(非字串)時一般需要同時傳遞陣列長度等參數列示陣列長度。在將陣列作為引數時可以使用[]或*修飾,如 fun(int *a)與 fun (int a[])是等價的,編譯器將陣列作為指標處理,不關注陣列長度。

在傳遞高維陣列時可以使用指標,也可以用陣列的形式如fun (int a[][8]),編譯器不關注第一維的長度它將自行計算。

函式返回

函式中的return語句將會終止函式執行返回主調函式,返回值型別非void的函式,在return語句後加一個與返回值型別相容的值作為返回值,int fun(void) {return 0;};;返回值型別為void的函式不返回任何值,return ;只起到提前終止函式執行的作用。

修飾關鍵字

 函式可以是外部的(extern或預設關鍵字)或靜態(static)。外部函式可以被所有程式檔案呼叫,靜態(內部)函式只能在定義它的檔案中呼叫,static  void  fun(void)宣告瞭一個靜態函式。

 內聯(inline)函式在呼叫時將函式程式碼嵌入呼叫點,而普通函式在呼叫時需要一系列耗時的操作。行內函數是一種典型的以空間換時間的策略。inline關鍵字是建議性而不是指令性的,函式是否是inline的由編譯器最終決定,沒有inline修飾的函式可能被優化為inline,使用inline修飾的函式可能不會被優化。

 除了使用引數傳遞和返回值外,函式也可以通過外部變數進行通訊。區域性變數與外部變數重名時,區域性變數將遮蔽外部變數。 所以在自定義函式時不再次宣告全域性變數作形參(形參表列中不含函式中呼叫的全域性變數)。必須謹慎使用外部變數,它可能導致程式可移植性降低並增加因為外部變數修改而出錯的概率。

C中函式名在自己的作用域內是唯一的,而在C++或Java等允許函式過載的語言中允許函式名重複,但是函式名+參數列是唯一的。

遞迴過程

 一些書籍將遞迴過程定義為:一個函式呼叫其自身的過程稱為遞迴過程。這個定義……

int Fibo(int n) {

    if (n == 0 || n == 1) {

      return 1;

    }

    else  {
          return Fibo(n-1)+ Fibo(n-2);

    }

}

 我們可以從執行過程的角度理解遞迴,遞迴過程分為迴歸、遞推兩個過程。以上述Fibonacci數列為例,呼叫Fibo(3)時它會呼叫Fibo(1)和Fibo(2),Fibo(2)會呼叫Fibo(0)和Fibo(1)。Fibo(0)和Fibo(1)到達遞迴底部無需繼續遞迴,上述過程稱為迴歸;Fibo(0)與Fibo(1)的返回值使得Fibo(2)取得結果,Fibo(1)和Fibo(2)使得Fibo(3)取得結果,遞推結束這個過程稱為遞推。

通過模仿呼叫棧(call stack)的行為所有的遞迴演算法均可以轉換為迭代演算法。遞迴可以使程式簡單,但是函式呼叫需要大量時間和空間開銷所以應儘量使用迭代而非遞迴演算法。

當遞迴呼叫是整個函式體中最後執行的語句且它的返回值不屬於表示式的一部分時,這個遞迴呼叫就是尾遞迴。大多數編譯器可以將尾遞迴優化為迭代,提高執行效率。

相關文章