1. 程式人生 > >C語言知識深度彙總(本文僅談語言,且不適合初學者閱讀)

C語言知識深度彙總(本文僅談語言,且不適合初學者閱讀)

修正的部分內容的索引放在這裡進行說明:
第一次修正:關於自定義型別那裡進行了部分內容的修正
第二次修正:
1.對語句部分進行了大程度的修正
2.對陣列部分進行了大程度的修正
3.補上了位段的一系列操作
4.對文章中的一些地方進行了小幅度修改(使之更嚴謹,更容易閱讀理解)
第三次修正:
1.增加了檔案操作部分的內容
2.對文章中的一些地方進行了小幅度修改。
第四次修正:
1. 對關鍵字部分進行了調整,這裡感謝網友“gsfdjgji”在評論區指出我的錯誤!
2.對柔性陣列部分進行了詳細說明
第五次修正:
1.記憶體管理
2.對文章中的一些地方進行了小幅度修改

一、資料型別

我在開頭這裡提一下c語言識別符號和關鍵字

c語言的識別符號不能和標準庫中的關鍵字重名,除此外,它由字母、數字、以及下劃線組成,只能以字母或者下劃線開頭。這裡兩種命名方法,各舉一個例子(這兩種沒有優劣之別,使用完全看個人習慣):

  • 駝峰命名法:StringLength

  • 下劃線命名法: string_length

c89規定c語言的關鍵字一共有32個,我在這裡講一下typedef和volatile
  • typedef
    這個關鍵字常用於給型別重新命名,eg:typedef char* p_char
    這就代表以後可以用p_char 來代表指向字元的指標型別了。
    它的好處

    有這兩點:
    1、使複雜的宣告變得簡單,避免出錯和提高程式碼的可閱讀性
    2、可以隱藏指標,陣列語法
    這裡把typedef和#define做一個區別對比:
    他們兩個都可以給型別名更改一個名字,但是不同於typedef的是,#define只是簡單的符號替換,並沒有使一個識別符號根本上成為一種已知的型別名( 我這麼說你可能聽不明白,沒關係,我寫一段程式碼,結合我的文字你絕對就理解了 )
    1、

        define p_char char *
        p_char a,b;
    

2、

        typedef p_char char *;
        p_char a,b;

第一段程式碼中的a是一個指向字元的指標,而b只是一個字元變數
第二段程式碼中的a、b都是指向字元的指標

還有,#define重新命名的型別支援擴充套件,而typedef重新命名的型別不支援擴充套件,看以下程式碼:

#define INT int
unsigned INT a,b;//a、b均為無符號整形變數

typedef INT int
unsigned INT a,b;//錯誤,這就相當於是兩個關鍵字套在一起:char int a,b
  • volatile【定義】:被這個關鍵字修飾的變數,當編譯程式需要獲取或者儲存這個變數的值時,都從它的地址中來獲取,而不要在某些情況下為了優化速度,從臨時暫存器中獲取(因為若是此時剛好別的程式通過地址更新了這個變數的值,暫存器中的那個值就會過時,從而出現錯誤)
    我在這裡舉一個例子:
#include<stdio.h>
int main()
{
    const int num=10;
    int *p=(int *)&num;
    printf("%d ",num);
    *p=20;
    printf("%d\n",num);
    return 0;
}

在c++的編譯器上這個輸出結果是:

10 10

但是如果把定義num的那行程式碼改成這種

volatile const int num=10;

那麼輸出結果就會是我們想要的

10 20

注:c89規定了32個關鍵字,c99又新增了“_Bool ”,“_Complex ”,“ _Imaginary ”,
“restrict ”,“inline ”五個關鍵字。

一、基本資料型別(以下描述均在32位cpu環境下)

下面的書寫的格式為: 所佔位數->所表示範圍

  • <字元型>(char):1->(-2^7 ~ 2^7-1)

  • <整形>
    短整形(short)
    signed short: 2->(-2^15 ~2^15-1)
    unsigned short: 2->(0 ~2^16-1 )
    整形(int)
    signed int: 4->(-2^31~2^31-1)//少的那一位是符號位
    unsigned int:4-(0~2^32-1 )
    長整型(long)
    signed long: 4->(-2^31~2^31-1)
    unsigned long: 4->(0~2^32-1 )
    long long(c99新加的型別)
    signed long long: 8->( -2^63 ~ 2^63-1)
    unsigned long long: 8->(0~2^64-1)

  • <浮點型>
    單精度浮點型(float):4-> +/-3.40282e+038 (7~8位有效數字)
    雙精度浮點型(double):8-> +/-1.79769e+308 (15~16位有效數字)

    下面我特地講一下,浮點型和整形在記憶體中的儲存方式

1>整形

這裡我們要知道什麼是原碼、反碼和補碼:
1)原碼:將一個整數轉換成二進位制形式就是它的原碼(即正數和負數的原碼相同)
2)反碼:負整數的反碼就是它的原碼除首位的符號位外其他位按位取反,正數的反碼就是它的原碼
3)補碼:負整數的補碼就是它的反碼加一,正整數的補碼就是它的原碼

一切整數在記憶體中都是以它的補碼的形式儲存的,而讀取的時候則轉換回原碼。這樣的儲存方式有兩個好處
- 簡化了硬體電路
- 把加法和減法合併為一種運算

2>浮點型資料在記憶體中的儲存方式

對於32位的浮點數來說(float):最高的一個bit位(示意圖如下)是符號位,用1來表示負數,用0來表示正數。接下來的8個bit位用來表示指數,剩下來的23位均用來表示數值。

這裡寫圖片描述

對於64位的浮點數來說(double),大體和32位相同

這裡寫圖片描述

指數位的儲存規則

比方說數字12.125,它首先會轉換成二進位制的數字:1100.001。用指數表示法就是1.100001 x 2^3,又因為所有的數字轉換成二進位制指數表示法小數點前的數都是1,所以這個數字和小數點就都可以省略了(若是0.1xx怎麼辦?當然是轉換成1.xxx *2^-xx的形式了。。。),然後將擷取後的尾數放到尾數位,不滿的則補0。指數位置的儲存方式是取能表示的數的範圍中的中間值(8位範圍為0~255,所以中間值就是127),然後給2的指數減去這個數字(讀取的時候再加上),然後轉換成二進位制的形式,然後放到指數的位置。

二、自定義資料型別

struct定義的結構體型別、union定義的聯合體(共用體)型別、enum定義的列舉型別

這裡提一下關鍵字typedef,typedef的用法就是給原有的資料型別起一個名字,方便以後的使用。例如:
typedef struct Node
{
int _data;
struct SList *_next;
}Node;
以後再用到上面封裝的那個結構體型別時,就不用使用struct Node來定義了,直接用Node來定義可以了,這樣一方面時為了減少因為手誤而出現的錯誤,一方面也可以使程式碼變得簡潔。

二、基本語句

1、判斷語句

  • if……else
    這裡有一個易錯的問題:”懸空else”,下面舉一個例子說明:下面這段程式碼給我們由於縮排的原因給我們的直觀感覺就是第一個if和下面的else對應為一對,但是實際上是第二個if和else相對應,因為else總是和上面緊接著的if相對應,但是這裡的錯誤往往不易察覺!最有效的避免這種錯誤的一個方法就是給每一個if和else後面都加上花括號,將要要執行的程式碼括起來,這樣即使出現縮排錯誤,也不會再出現上面的問題。
if(judge)
    if(judge)
      code block1;
else
    code block2;
/*下面是修改後的程式碼,當然也不推薦這樣子寫,縮排最好還是要按照一定的
  原則對齊,下面只是起到一個對照的作用*/
if(judge){
    if(judge){
        code block1;
    }
else{
    code block2;
    }
}
  • switch……case
    這裡我強調一下,最好不要用default來表示最後一種情況,否則你既喪失了case語句的自說明功能,又失去了default語句處理意外事件的功能。
    switch(表示式/變數)
    {
        case 常量:程式碼塊或者語句;break;
        ……
        default:程式碼塊或語句;break;                                   
    }   
switch case語句除了上述用法外,還有一個fall though的用法,下面說明這種用法

可以用好幾個情況對應一種事件的發生,例如1~5都代表工作日,而如下程式碼就表示了這種用法的使用方式。事實證明,fall though在某些特定情況下還是非常有用的。。。

switch(day)
{
case '1':
case '2':
case '3':
case '4':
case '5':printf("今天是工作日!\n");break;
case '6':
case '7':printf("今天是休息日!\n");break;
default:
    break;

迴圈語句

  • while(條件表示式)……
  • do……while(條件表示式)
  • for(零條或多條語句;判斷語句;零條或多條語句)

for語句相對於while語句的好處在於,它的使用可以使程式碼變得緊湊,且部分變數的增減位於頭部,一目瞭然,不易出錯。
do_while語句的作用和while語句類似,不同之處在於它至少要執行一次花括號內的程式碼塊。

三、操作符

對於操作符來說,主要就三點內容:優先順序(網上有詳細的15級)、結合性、是否控制求值順序(&&,||……)

這裡提一下左值和右值

左值:標識了一個可以儲存結果值的地點。
右值:即一個指定的值
在使用右值的地方都可以使用左值,但是相反則不一定

複雜表示式的求值是由三個因素決定的:操作符的優先順序操作符的結合性,以及操作符是否控制求值順序,相鄰兩個操作符究竟哪個先執行取決於他們的優先順序,若是優先順序相同,則再看結合性。這裡我強調一下,優先順序的那個表最好還是背過,因為雖然自己可以在所有要執行的運算兩邊加上括號,以避免優先順序的問題,但是不代表別人也會那樣,而背過那個表,就會避免很多不必要的麻煩。

PS:筆者是見得多了,用得多了就記住了,這個記憶方法網上還是有很多的,覺得吃力的讀者可以去學習學習

四、陣列

1.概念:一組型別相同的資料的集合,在記憶體地址上從低到高依次排列(也就是說,定義陣列時在記憶體上的棧空間申請一段連續的空間)

2.分類:一維陣列和多維陣列

(注意:這個維數可不是空間上的維數,所有型別的陣列在記憶體上都是呈線性排列的,讀者千萬別被和譚書上類似的說法給誤導了)

3.初始化

  • 一維陣列的初始化
    1.在定義陣列時對陣列元素賦以初值;
    2.可以只給一部分元素賦值
    3.想使一個數組中全部元素值為0,可以寫成:a[10]={0};
    需要注意 int a[10] = {1}; 並不能把陣列初始化為全1,只是將第一位初始化為1,後面全部都是0滴.
4.在對全部陣列元素賦初值時,可以不指定陣列長度。
  • 多維陣列的初始化(以二維陣列示例)

1.分行給陣列元素賦值:int a[3][4]={{1,2,3,4},{5,6,7,8},{,9,10,11,12}};
2.將所有元素按照順序寫在一個花括號內:int a[3][4]={1,2,3,4,5,6,7,8,9,10,11,12};
3.可以給部分元素賦值(c11規定)
4.如果給全部元素賦值,第一個方括號內可以省略數值,但是第二個必須寫上數值:int a[][4]={{1,2,3,4},{5,6,7,8},{,9,10,11,12}};

  • 字元陣列的初始化(舉栗子)
    第一種:char arr[常量可寫可不寫,看需求]=”I am a bug”//g後面會自動加上\0
    第二種:char arr[同上]={‘a’,’b’}
    ( 若是中括號內的數值等於(或者沒有數值)後 面賦值的元素個數,則b後面不會自動加上\0,若是大於賦值的元素個數,則其他的元素預設為\0 )
    注意:陣列只能在定義的時候一次給多個元素賦值,以後一次只能給一個元素賦值

4.定義:型別+識別符號+[常量]/[(常量)][常量]……

5.使用

這裡需要注意的地方有以下幾點: 1)陣列名只有在:sizeof()裡面,還有取地址的時候代表整個陣列的長度,其他時候都代表陣列首元素的首地址 2)陣列在使用的時候可以採用下標的方式(eg:arr[3]),也可以採用解引用的方式(eg:*(arr+3))

6.(和指標的區別)

指標和陣列是什麼關係呢?答案是:一點關係都沒有
這裡主要論述一下二者的不同

1>在多檔案程式設計中的宣告上來說,在A檔案中定義為陣列,在B檔案中宣告為指標是會出錯的,定義為指標,宣告……也一樣會出錯。 2>從舉藕法來說,兩個指標指向同一個字串,更改一個指標的內容是會改變另一個指標指向的內容,但是兩個陣列若是都存放同一個字串,更改一個數組的內容對另一個數組的內容並沒有影響。 這是因為兩個指標指向的字串內容儲存在字元常量區,而陣列中存放的內容則儲存在記憶體上的棧區。 3>看程式碼
char arr[10];
char *arr_n=NULL;
arr_n=arr;
printf("%d %d",sizeof(arr),sizeof(arr_n));

輸出結果為:10,4

因為陣列名放在sizeof裡面代表整個陣列,而指標的名字永遠代表一塊地址,而地址在記憶體中永遠佔4個位元組(windows32位平臺下)

7.(柔性陣列(和結構體))

柔性陣列是什麼呢?它定義在結構體的最後一個成員,但是它的前面至少要有一個成員。使用方法如下:
#define NUM 10
int main()
{
struct text{
    int arr;
    int sum[0];//c99也可以這樣定義int sum[];
}ce;

struct text *p = (struct text *)malloc((sizeof(ce)+10 * sizeof(int)));
for(int i = 0; i < NUM; i++)
{
    p->sum[i] = i;
}
for (int i = 0; i < NUM; i++)
{
    printf("%d\t", p->sum[i]);
}

free(p);    
有些人在看了上面柔性陣列的用法後,就覺得這樣做感覺完全是多此一舉呀!我明明可以直接用下面這種方式做出和上面看起來“ 一樣 ”的效果:
{
    struct text{
        int arr;
        int _length;
        int *sum;
    }ce;

int main(){
    int length=10;
    ce *text_array = (ce *)malloc (sizeof (ce));
    ce->sum = (int *) malloc( sizeof(int) * length );
    ce->_length = length;
    memset(ce->sum, 1, length);
    return 0;
}
這樣看起來似乎更清楚易懂一些,並且也可以做到像柔性陣列那樣的用法,那為什麼搞出一個柔性陣列呢?**如果你這樣想了,顯然你沒有注意到下面的這兩點**
  • 方便記憶體釋放。如果我們的程式碼是在一個給別人用的函式中,你在裡面做了二次記憶體分配,並把整個結構體返回給使用者。使用者呼叫free可以釋放結構體,但是使用者並不知道這個結構體內的成員也需要free,所以你不能指望使用者來發現這個事。所以,如果我們把結構體的記憶體以及其成員要的記憶體一次性分配好了,並返回給使用者一個結構體指標,使用者做一次free就可以把所有的記憶體也給釋放掉。

  • 有利於減少記憶體碎片,提高訪問速度。這樣做會給結構體內的資料分配一段連續的記憶體,而由於CPU定址的緣故,連續的記憶體有益於提高訪問速度。

八.鋸齒形陣列

在實際操作中,我們常常需要將一些長度不等的字串儲存在一個二維陣列中,你大概會這麼做:

char dst[3][10]={"I","love","knowledge"};

這樣做確實達成了我們的要求,但是這樣操作明顯有兩個缺點:
1.你為了能夠存放下所有的字串,定義的二維陣列第二維大於最大的那個字串長度,但是對於相對很短的字串的儲存來說就浪費了大量的空間,比方上例中的儲存 I 的那塊空間
2.我們在實際操作中有時候並不知道需要多大的空間來儲存要儲存的資料,這樣你這隻能儘可能的分配更大的空間來達成我們的需求,這樣以來,既有極大的不安全性,又有可能浪費大量的空間
為了避免這種問題,我們可以這樣做>

char *dst[3]={"I","love","knowledge"};

這種操作就被稱為“鋸齒形陣列”,由於 [] 的優先順序高於 *,所以dst先和 [] 結合形成一個數組,然後數組裡面存放的元素就是指標,指向要儲存的字串。

五、函式(function)

1.概念:實現某一個功能的一段程式碼塊

2.原型(我把一些包括返回值類的注意事項都寫在這個條目下)

作用:給編譯器宣告函式的資訊,使函式不用重複定義就可以直接使用
模板: 返回值型別 函式名{引數列表}
注意事項:
1、返回值型別若是沒有則寫void,否則會預設為整形型別。這裡若是遺忘顯式註明返回值型別,而恰好函式返回值不是整形(eg:float)則會出現問題
2、函式名的命名規則見開始的識別符號命名規則
3、引數列表的格式:型別名稱+變數名稱(可寫可不寫,但是建議寫上,因為這可以起到幫助程式設計師理解函式的作用!),……
若是沒有引數傳遞,則註明void

3.函式呼叫原理(引出棧幀)

由於在CPU中,計算機沒有辦法知道一個函式呼叫需要多少個、什麼樣的引數,也沒有硬體可以儲存這些引數。也就是說,計算機不知道怎麼給這個函式傳遞引數,傳遞引數的工作必須由函式呼叫者和函式本身來協調。為此,計算機提供了一種被稱為棧(在呼叫的時候形成棧幀)的資料結構來支援引數傳遞。
棧是一種先進後出的資料結構,它由棧頂、棧底、以及一個指標構成。棧底通常不變,變的都是棧頂的位置。函式呼叫在傳入引數的過程中,會按照一定的順序將引數壓入棧中(push),棧頂指標隨之移動,指向最新的棧頂,當呼叫結束時,又將引數從棧頂彈出(pop)。
函式呼叫時,呼叫者依次把引數壓棧,然後呼叫函式,函式被呼叫以後,在堆疊中取得資料,並進行計算。函式計算結束以後,或者呼叫者、或者函式本身修改堆疊,使堆疊恢復原裝。

棧溢位的原理:函式在呼叫的時候就會給定一塊棧幀空間,而如果操作使得陣列的大小超出了給定的空間大小,從而覆蓋掉函式的返回值地址,函式返回錯誤就會出現段錯誤。eg:你定義了一個具有10個元素的陣列,但是在賦值的時候卻超過了10個元素,這時候,多餘的資料就會覆蓋函式的返回值地址,這就是所謂的棧溢位。

4.呼叫約定

常見的呼叫約定有:stdcall(pascal呼叫約定)cdecl(c呼叫約定)

在引數傳遞中,有兩個很重要的問題必須得到明確說明:

  • 當引數個數多於一個時,按照什麼順序把引數壓入堆疊
  • 函式呼叫後,由誰來把堆疊恢復原裝

而呼叫約定就是用來解釋說明這兩個問題的。

5.庫函式相關(常見的函式)

這裡我簡單的說明一下這些函式的作用,具體的實現網上都有,需要的朋友可以去查

1)str家族

  • 函式原型:int strcmp(char *string1,char *string2);
    函式說明:這個函式用於比較兩個字串的大小,若string1>string2,函式返回一個大於零的數;若string1=string2,函式返回零,若string1 < string2,函式返回一個小於零的數。 ( 若第一個字串的長度小於第二個字串的長度,也認為第一個string1 < string2)

    這裡要注意兩個字串相等的時候

  • 函式原型:char *strcat(char *target,char *source)
    函式說明:這個函式用於將字串source連線在字串target的後面,

  • 函式原型:char *strcpy(char *target,char *source)
    函式說明: 這個函式用於將字串source複製在字串target的後面,若是字串target的長度比source的長度長,則在複製過去的source後面自動加上\0,即覆蓋以前的字串,但是相對應的,要是……比……短,則不會在後面加上\0,這樣用%s輸出的時候就會出錯!!!

  • 函式原型:size_t strlen(char *arr)
    函式說明:這個函式用於求字串的長度,需要注意的是函式的返回值是一個無符號型別的整數,也就是說,需要注意向下面這種情況:

    if (strlen(dst) - strlen(src) < 0)
    {
        printf("OK!\n");
    }
    

    恐怕這句“OK!”哪種情況都是無法輸出的,因為strlen()的返回值是一個無符號型別整數,而兩個無符號型別整數運算的結果也是一個正整數,所以……所以如果有需要,則要將strlen的返回值強制型別轉換成int型

  • 函式原型:char *strchr(const char *dst,int cc)
    函式說明:從字串dst中查詢字元cc(注意這裡是整形,用的時候要用單引號括起來),函式返回第一次找到cc的位置,若未找到,則返回NULL,類似的還有strrchr(這個函式是從後向前找)

  • 函式原型:char *strstr(const char *dst,const char *src)
    函式說明:從字串dst中查詢字串src若找到,則返回第一次找到src的起始位置

  • 函式原型:char *strpbrk(const char *dst,const char *src)
    函式說明:從字串dst中查詢字串src中任意一個字元,函式返回第一次找到的任意一個字元在dst中的位置

  • 函式原型:char *strncpy(char *dst,const char *src,size_t num)
    函式說明:將字串src中的num個字元拷貝到dst中,覆蓋其原來的字串,若是num>sizeof(src),則其他位補\0,若是小於,則將num位拷貝過去(注意,沒有在結尾加上\0哦)

  • 函式原型:char *strncat(char *dst,const char *src,size_t num)
    函式說明:將字串src的前num個字元新增到dst字串的末尾,並且自動在結尾加上一個\0,(注意:它不管有沒有超出dst的範圍,這點不同於strncpy)

  • 函式原型:int strncmp(const char * str1,const char *str2,size_t num)
    函式說明:比較兩個字串前num個字元的大小

  • 函式原型: size_t strspn(const char *src,const char *dst)
    函式說明:函式返回字串src前面和字串dst中相同字元的個數,與此類似,還有一個函式strcspn(返回不相同的個數)

  • *函式原型:char *strtok(char *str1,const char *str2)
    函式說明:它從字串str1中隔離各個單獨的稱為標記的部分,並且丟棄分隔符。
    這個函式還是相當重要的,我在這裡寫一段程式碼來說明一下它的使用方法:

    char target[] = "I Lo\nve you Ch\nina for\fever!";
    char *token;
    for (token=strtok(target, arr);\ 
    token != NULL;token = strtok(NULL, arr))
    {
        printf("%s", token);
    }
    

結果:I Love you China forever!

這個函式若用於將一串字元中想要去掉的一些字元拿走,將這些字元前後的字元連起來,則可以封裝成一個函式,需要用的時候直接將字串作為引數傳進去還是很方便的!

2)mem家族

  • 函式原型:void *memcpy(void *dst,void const *src,size_t length)
    函式說明:大體和strncpy作用相同,不同的是這個函式會連\0一起拷貝過去,而不會終止於\0,(需要注意的是函式未定義當dst和src在記憶體中重疊的情況)

  • 函式原型:void *memmove(void *dst,void const *src,size_t length)
    函式說明:將src中的內容移動到 dst中,此函式可以處理當dst和src在記憶體中重疊的情況

  • 函式原型:void *memcat(void *dst,void const *src,size_t length)

  • 函式原型:void *memcmp(void *str1,void *str2,size_t length)

  • 函式原型:extern void *memset(void *buffer, int c, int count)
    函式說明:這個函式用於將某塊記憶體全部置c,一般常用於清空陣列。buffer多為指標或者陣列,c為要設定的數,count為所置記憶體的位元組數。
    例如:char buffer[100];
    memset(buffer,0.sizeof(buffer);

3) 字元分類函式和字元轉換函式

這裡我列舉幾個常用的函式

  • isdigit:判斷是否為數字(十進位制),是則返回真
  • isspace:判斷是否為空白字元,同上
  • isalpha:判斷是否為字母“a~z””A~Z”,同上
  • isupper:判斷是否為大寫字母,同上
  • islower:判斷是否為小寫字母,同上
    • isalnum:判斷是否為數字或字母,同上
    • tolower:把大寫字母轉換為小寫字母
    • toupper:與上相反

4)I/O家族(具體放到檔案那裡講)

printf,scanf……

7.不定引數(它其實是由巨集實現的,但是它的用法又體現在函式這裡)

詳情參見我的另一篇部落格:函式可變引數

六、預處理

1.被編譯器隱藏的過程

  1. 預編譯階段(^.i):這個階段主要是處理預處理指令,包括#difine定義的巨集常量等
  2. 編譯階段(^.s):這個階段就是對程式碼進行詞法分析,語法分析,語義分析,符號彙總
  3. 彙編階段(^.o):形成符號表,將c程式碼轉變成彙編程式碼,再轉換成二進位制機器碼,從而形成可執行程式。
  4. 連結階段:合併段表,符號表的合併以及符號表的重定位
    (如果想要深入研究連結的底層原理和實現方法,請參見《程式設計師的自我修養_連結裝載、庫》

2.巨集

這裡易出現的問題有:帶歧義的巨集、巨集的副作用(i++和i+1)

按照命名約定,定義巨集名時通常為大寫字母。

下面做一下函式和巨集的優缺對比

1、從程式碼量上來說,函式是將一段程式碼塊封裝成一條程式碼(即函式呼叫),所以它的使用可以減少一個專案中的程式碼量。但是巨集替換每一次都會將一段程式碼插入到專案程式碼中,從而增加專案程式碼量。所以除非巨集非常短,否則就使用函式來代替巨集的功能
2、從執行速度上來說,由於函式的使用存在額外的返回/呼叫開銷,所以在這點上,巨集普遍比函式快
3、從引數型別上來說,函式的引數必須宣告為已知型別,也就是說,它的使用一定程度上受到了引數型別的限制。還有函式的引數不可以傳遞型別。而巨集是與型別無關的,而這個特性既是優點,也是缺點(不嚴謹)。
4、從操作符優先順序上來說,由於函式只在函式呼叫時求值一次,所以它不會存在由於鄰近操作符優先順序的問題而產生不可預料的結果。
5、函式沒有巨集的歧義性
6、巨集無法除錯,因為它在預編譯階段就已經完成了操作

這裡說明一下#和##的作用

“#”:把一巨集引數變成對應的字串
    #define PRINT(FORMAT,NUMBER)\
    printf(" the value of "  #NUMBER  "is"  FORMAT "\n",NUMBER);
    //這裡利用了相鄰的字串自動連線在一起的特性(c99)    
    int i=0;
    PRINT("%d",i+3);

result:the value of i+3 is 3

“##”:將兩個位於它兩邊的識別符號連線在一起形成一個新的符號(當然,連線後的新識別符號必須合法),它還允許巨集定義從分離的文字片段建立識別符號(示例如下)
define  ADD_NUM(NUM,TAG)\
sum##TAG+=NUM
ADD_NUM(10,1);

上面這段程式碼的結果是給sum1加上10

3.條件編譯

條件編譯的作用

  • 原來除了註釋掉程式碼外,還可以這樣!!!(除錯性程式設計)
    eg:有些除錯性的語句只要進行選擇性的編譯便能夠實現它的功能而且不用刪除這些語句

這是一種做法:

    #include<stdio.h>
    #define DEBUG 
    main()
    {
        int arr[10];
        for(int i = 0;i < 10;i++)
        {
            arr[i]=i;
            #ifdef DEBUG
            printf("arr[%d]=%d\n",i,arr[i]);
            #endif
        }
        return 0;
    }

這是另一種做法:

    #include<stdio.h>
    #define DEBUG 1   //不需要除錯的時候就把DEBUG設定為0
    main(){
        int arr[10];
        for(int i=0;i <10;i++){
            arr[i]=i;
            if(DEBUG){
            printf("arr[%d]=%d",i,arr[i]);
            }
    }
        return 0;
    }
  • 防止標頭檔案重複引入

在檔案的開頭加條件編譯指令:

#ifndef FUNCTION
#define FUNCTION

程式碼塊(函式宣告,定義的巨集,定義的型別……)

#endif

七、指標

1.概念:記憶體中位元組的編號即為指標

2.分類:

  • 指向基本型別的指標(eg:int*, float *……)

  • 指向自定義型別的指標(eg:結構體指標……)

  • 指向函式的指標

  • 指向指標的指標

3.運算

兩個指向相同陣列的指標相減的值是他們相差的元素個數,指標不可以相加。
指標變數名+n代表指標指向向前跳n個指向元素型別的偏移量

4.深度剖析

我在這裡舉幾個複雜宣告的例子來說明問題(附解析)

<1>

void (* signal ( int ,void( *) (int) ) )( int );
這是一個什麼東西!?別暈,聽我講。
看到這種複雜的宣告,我們首先從識別符號入手,它的識別符號是signal,左邊是星號,右邊是括號,但是由於括號的優先順序高於星號的優先順序,所以signal首先被解釋為一個函式,既然是函式,那麼signal後面緊接著的那個括號內肯定就是引數了,第二個引數是一個指標,指向一個返回值為空,具有一個整形引數的函式,然後再來看signal的返回值。首先我們可以看到它的返回值是一個指標,指向什麼呢?這時你可以把signal左邊的括號去掉,就剩下了 void 空缺(int),這時就一目瞭然了,指標的指向就是一個返回值為空,有一個整形引數的函式。
這時我們總結一下,signal就是一個函式,這個函式有兩個引數,一個整形,一個指向(返回值為空,具有一個整形引數的函式)的函式指標,返回值為一個(返回值為空,具有一個整形引數的函式)!
一般碰到這種情況我們可以這樣來簡化宣告:將重複出現的型別用typedef關鍵字進行重新命名,然後再宣告就會變得簡單多了,比方說上面這個:
typedef void(*p_fun)(int);//更改名稱
p_fun signal(int p_fun)l;//重新宣告

<2>

int main()
{
    int arr[2][5]={1,2,3,4,5,6,7,8,9,10};
    int *ptr1=(int *)(&arr+1);
    int *ptr2=(int *)( *(arr+1));
    printf("%d    %d\n",*(ptr-1), *(ptr2-1));
    return 0;
}
//輸出什麼?

我們來看,取arr的地址加1代表將陣列的地址整個向後偏移一個數組長度,然後再強轉成int * ,此時的地址型別由陣列變成了整形,再賦值給ptr1。ptr-1,根據指標運算,減一就代表減一個型別的步長,它的型別是整形,也就是此時ptr1由指向arr陣列後面那陣列大小的地址的開始向前偏移一個整形大小,那麼它此時就指向arr陣列的末尾元素,解引用的結果就是10。第二個,(arr+1),從陣列那裡我們可以知道,陣列名大多數情況下都代表陣列首元素的地址,這裡陣列arr的首元素是一個一維陣列,給它加一,就相當於偏移了一個一維陣列的長度,此時它的地址就是陣列arr第二個元素,對它解引用,翻譯成另一種你容易理解的形式就是arr[1],而這代表第二個元素的地址,再將其轉換成int *,輸出的時候減一就相當於向前偏移一個整形的長度,所以第二個輸出結果為5

3>

//這個例子貌似在網上廣為流傳,我在這裡做一個解析
int main()
{
char *c[]={"ENTER","NEW","POINT","FIRST"};
char **cp[]={c+3,c+2,c+1,c};
char ***cpp=cp;
printf("%s\n",**++cpp);
printf("%s\n",* -- *++cpp+3);
printf("%s\n",*cpp[-2]+3);
printf("%s\n",cpp[-1][-1]+1);
return 0;
}

c是一個指標陣列,cp是一個二級指標陣列,cpp是一個三級指標。第一個輸出:先對cpp進行自加一,此時它指向cp[1],而cp[1]又指向c+2,所以對其兩次解引用的結果是:POINT。第二個輸出:cpp前面的操作符的優先順序都高於加法操作符的優先順序,所以我們先看前面,先對cpp自加1,此時它指向cp[2],對cpp一次解引用得到c+1,然後再自減1得到的是c,對其解引用再加3,它的輸出是:ER。第三個輸出:cpp[-2]存放的是c+3的地址,對其解引用得到的結果加三也就是:ST。第四個輸出:cpp[-1][-1]的意思翻譯成另一種形式就是 * ( *(cpp-1)-1 ),也就是c+1,然後對解引用結果再加1,所以輸出結果就是:EW。

5.高階應用:

轉換表
回撥函式(參見qsort()函式的使用)

八、結構體

1.概念:將一堆型別相同的資料或者型別不相同的資料放在一(注意不能放函式,這裡不同於c++)

2.結構體大小(記憶體對齊->原因?方法?)

說到結構體的大小,就不得不提一下記憶體對齊

記憶體對齊(效能原因):資料結構中記憶體需要對齊到自然邊界上,原因在於由於CPU訪問未對齊的記憶體需要兩次訪問,而訪問對齊的記憶體只需要一次,提高了效率
記憶體對齊的規則
1。結構體中的第一個成員實際操作(後面有解釋為什麼是實際操作)上不需要記憶體對齊,也就是說它在與結構體偏移量為0的地址處。

2。其他成員需要對齊到對齊數的整數倍的地址處。(對其數:編譯器預設的一個對齊數字和該成員本身大小的較小值< Linux下:4 Windows下:8>)

3。結構體的總大小等於成員最大對齊數的整數倍(包括第一個成員,這就是前面為什麼是實際操作上)

4。如果是巢狀結構體的結構體求大小,內部結構體對齊到它成員的最大對齊數的整數倍處,而外部結構體的大小則是所有成員的最大對其數的整數倍(包括內部的結構體的最大對其數)。

3.聯合體和位段

  • 1>原理及注意:聯合體和結構體的定義很相似,不同之處在於聯合體內部的成員是共享一段記憶體的,而結構體是分別享有一段記憶體,( 這裡聯合的大小至少要大於最大的一個成員的位元組數)這裡需要注意:如果聯合內部的成員位元組數相差太大的話,就會造成一定程度上的記憶體浪費,所以我在這裡建議將聯合體內部的成員宣告為指標,這樣就能最大程度上的節省記憶體。(如果你的程式裡面有1萬個聯合體……)
    2>用法:如果你需要使指定記憶體區域在不同時刻具有不同型別的值,則使用聯合會是一個好的方法
    3>聯合還可以用於判斷大小端

當數值的低位段儲存在記憶體的低地址處,這種計算機模式被稱為小端模式,反之則被稱為大端模式。(這裡列舉一種用聯合體判斷大小端的方法)

int main()
{
    union{ int i=1; char a; }text;
    text.i=1;
    if(text.a==1){
    printf("小端模式\n");
    }else{
    printf("大段模式\n");
    }
    return 0;
}
  • 位段

位段的概念:
位段的宣告和普通的結構型別相同,但是它的成員是一個或者多個bit位的欄位。這些不同長度的欄位實際上儲存在一個或者多個整形變數中。(概念看不懂不要緊,往下看,最後學會位段的使用就行)

位段的宣告:
從它的概念中我們就可以看出,位段必須宣告為int,signed int或unsigned int型別,其次,在成員的後面加上冒號,冒號後面為位段所佔的位數。(這裡我強調一下,最好將位段顯式宣告為signed /unsigned ,不要為了省事而只寫一個int,因為只宣告為一個int,它被解釋為unsigned 還是signed事要根據編譯器決定的!)
下面我舉一個栗子:

struct Text{
    unsigned data1: 6;
    unsigned data2: 7;
    unsigned data3: 19;
}

使用位段的理由:
1.最大的好處在於,它可以節省儲存空間,尤其是需要成千上萬個這種結構的時候!在使用點陣圖處理大資料的時候,這種結構就起到了一個很好的作用。
2.位段可以很方便的操作一個整數的部分內容。(常見於作業系統的設計程式碼)

4.列舉

定義示例1:enum num{a,b,c};
a=0,b=1,c=2 //這裡預設第一個成員的值為 常量0,後面依次遞增
定義示例2:emnm num{a,b=12,c}
a=0,c=13 //相信不說你也應該看明白了吧

5.注意事項

  • 向函式傳遞結構引數的效率非常低下,通常可以採用傳遞指向結構的指標的方法

  • 位段在本質上是不可移植的

  • 聯合在實現變體記錄(記憶體中的某個特定區域在不同時刻具有不同型別的值)的時候非常有用

  • 聯合變數初始化的時候必須與第一個變數的型別相匹配

九、記憶體管理(分配的記憶體都在堆區)

1.malloc

函式原型:void *malloc(size_t size);
常見使用示例:
//給p分配了一段10個位元組大小的空間
int *p=NULL;
p=(int *)malloc(10 *sizeof(int));

如果不瞭解malloc的底層實現原理,那麼在以後使用這個函式難免會寫出效率低下的程式碼,這裡附上malloc底層實現原理

2.calloc

函式原型:void *calloc(size_t num_elements,size_t element_size)
區別與malloc的是,它將分配的位元組中的內容全部初始化為0;

3.realloc

函式原型:void *realloc(void *pointer,size_t new_size);
作用:更改已經分配的記憶體空間大小

4.free

函式原型:void free(void *pointer)
作用:釋放已經分配的空間

5.野指標

所謂野指標即沒有確定指向的指標,這時候若是對其解引用操作就會出現操作異常失敗。野指標通常出現在使用free釋放一塊記憶體後,忘記將指標指向NULL,而後面卻對其解引用操作。
附:常見記憶體洩漏問題
要想規避野指標也很簡單,養成良好的程式設計習慣即可:

  • 指標變數如果暫時不需要賦值,就一定要初始化為NULL,因為指標變數初始化時不會自動指向NULL,它的初始值是隨機的,如果不初始化為NULL,出現了問題就會是各種奇奇怪怪錯誤。

  • 當指標指向的記憶體被釋放掉後,將指標的指向改為NULL,因為free只是釋放掉了記憶體,並沒有改變指標的指向。

6.注意事項

  • 注意每一段申請的記憶體在使用完畢後都要使用free來釋放掉,否則,就會導致記憶體洩露的問題

  • 進行記憶體分配的時候要對函式的返回值進行檢查,eg:

    int *p=NULL;
    if(p=(int *)malloc(10 *sizeof(int))==NULL)
    {
        perror("error");
        exit(1);
    }
    

    這裡要注意realloc的返回值有三種可能:其一為原記憶體後面空閒記憶體的大小足夠新分配的記憶體,所以直接返回原記憶體的首地址;其二為原記憶體後的閒置記憶體不夠分配,所以重新找了一塊空間分配,而原來的記憶體則丟失掉;其三為新找了一塊記憶體還放不下,所以返回NULL,而原來的記憶體而也會丟失掉,所以在使用realloc的時候,事先準備一個指標來接受新分配的記憶體,若是分配成功,則賦值給原來的指標,否則就報錯,這樣原來分配的記憶體就不至於丟失。

十、檔案操作

1.開啟檔案和關閉檔案
“開啟”函式原型:FILE *fopen(char *filename, char *mode);
“開啟”函式說明:第一個引數filename是要開啟的檔案路徑(包含檔名稱),第二個引數mode是指對檔案操作的許可權,一般常用的許可權有:

r: 以只讀的形式開啟檔案,檔案必須存在
w: 以只寫的形式開啟檔案,若是檔案已經包含內容,則覆蓋其內容,若是檔案不存在,則在該路徑建立目標檔案並寫入內容。
a: 以追加的形式開啟檔案,若是檔案已經包含內容,則在其後面繼續新增。若是檔案不存在,則在該路徑建立目標檔案並寫入內容。
b: 二進位制檔案,和上述三個操作聯合使用,eg:rb,wb。
t: 文字檔案,可省略不寫。

“關閉”函式原型:int fclose(FILE *fp);
“關閉”函式說明:檔案正常關閉時,fclose() 的返回值為0,如果返回非零值則表示有錯誤發生。
注意:基於防禦性程式設計的原則,通常要對開啟檔案檔案操作進行檢驗,一般的格式可以為下(開啟當前目錄下的檔案filename):

FILE *fp;
if(fp=fopen("filename","rb") == NULL)
{
    printf("Error on open D:\\demo.txt file!");
    getch();
    exit(1);
}

最後一定要記得對開啟的檔案使用fclose()進行關閉。

2.以字元形式進行檔案操作
涉及的兩個函式的原型
讀:int fgetc(FILE *fp)
寫:int fputc(int c,FILE *fp)
說明:兩個函式若是讀取/寫入成功,則返回讀取的字元數目。若是失敗,返回EOF(具體是哪個數值要看編譯器是怎麼規定的,一般為-1)
在檔案內部有一個位置指標,用來指向當前讀寫到的位置,也就是讀寫到第幾個位元組。在檔案開啟時,該指標總是指向檔案的第一個位元組。使用fgetc(fuptc) 函式後,該指標會向後移動一個位元組,所以可以連續多次使用fgetc(fputc)讀取(輸出)多個字元。
注意:還有兩個常見的檔案操作函式:feof(FILE *fp)ferror(FILE *fp)
feof函式用來判斷檔案操作指標是否到達檔案的末尾,若是,則返回非零值,否則返回零。ferror函式用來判斷檔案操作是否出錯,是則返回非零值,否則返回零值。
3.以字串的形式檔案操作
讀取函式原型:char *fgets(char *dststr,int n,FILE *fp)
函式說明: dststr 為字元陣列,n 為要讀取的字元數目,fp 為檔案指標。
函式返回值,讀取成功時返回字元陣列首地址,也即 str;讀取失敗時返回 NULL;如果開始讀取時檔案內部指標已經指向了檔案末尾,那麼將讀取不到任何字元,也返回 NULL。
注意:
1.fgets() 遇到換行時,會將換行符一併讀取到當前字串,然後結束本次讀取操作。這點和gets()不同,gets()會自動忽略換行符。還有,在設定fgets的接收引數時,要將其空間設定為需要接收的檔案位元組數加一,這是因為fgets每次讀取的字元個數為n-1,最後一個字元預設置為NULL。假如目標檔案中目標行的字元數目大於n-1,則fgets函式讀取n-1個字元後結束本次操作,下次讀取繼續從上次未讀完的行讀取字元。

2.fgets函式不能用於讀取二進位制檔案的內容!

輸出函式原型:int fputs(char *str,FILE *fp)
str 為要寫入的字串,fp 為檔案指標。寫入成功返回非負數,失敗返回EOF。

4.格式化讀寫檔案操作
讀取函式:int fscanf(FILE *fp,char *format,…)
寫入函式:int printf(FILE *fp,char *format,…)

函式說明:這兩個函式和scanf和print唯一的區別就是多了一個檔案指標,可以對檔案進行操作,值得一提的是,如果如下這樣修改fscnaf和fprintf的引數,則這兩個函式將變為scanf和printf。
fscanf(stdin,char *format,…) | fprintf(stout,char *format,…)

5.隨機讀寫檔案操作
什麼是隨機讀寫檔案呢?顧名思義,就是可以在檔案的任意位置進行檔案讀寫。要實現這個操作的關鍵就在於怎樣按照想法移動檔案內部的位置指標,稱為檔案的定位
實現檔案定位函式有:rewind()和fseek()
void rewind(FILE * fp);
說明: 將檔案內部位置指標放到檔案的開始位置
void fseek(FILE *fp,long offset,int orign);
說明: offset為移動的距離
orign為開始移動的位置,規定有三種:
SEEK_SET (0): 檔案開始的位置
SEEK_CUR(1): 當前位置
SEEK_END(2): 檔案結尾的位置
**注意:**fseek函式通常用於二進位制檔案的操作

PS:這是作者的腦力勞動成果,希望廣大網友轉載可以註明出處