1. 程式人生 > >C編程基礎

C編程基礎

自身 自由 二進制 遍歷 ext 說明 gic 維護 cond

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)的行為所有的遞歸算法均可以轉換為叠代算法。遞歸可以使程序簡單,但是函數調用需要大量時間和空間開銷所以應盡量使用叠代而非遞歸算法。

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

C編程基礎