C語言深度解剖讀書筆記

開始本節學習筆記之前,先說幾句題外話。其實對於C語言深度解剖這本書來說,看完了有一段時間了,一直沒有時間來寫這篇博客。正巧還剛剛看完了國嵌唐老師的C語言視頻,覺得兩者是異曲同工,所以就把兩者一起記錄下來。等更新完這七章的學習筆記,再打算粗略的看看剩下的一些C語言的書籍。


本節知識:

1.c語言中一共有32個關鍵字,分別是:auto、int、double、long、char、short、float、unsigned、signed、sizeof、extern、static、goto、if、else、struct、typedef、union、enum、switch、case、break、default、do、while、const、register、volatile、return、void、for、continue。注意:define、include這些帶#號的都不是關鍵字,是預處理指令。
2.定義與聲明
定義   是創建一個對象併爲止分配內存。  如:int   a;
聲明   是告訴編譯器在程序中有這麼一個對象,並沒有分配內存。   如: extern   int    a;
3.對於register這個關鍵字定義的變量,不能進行取地址運算(&),因爲對於x86架構來說,地址都是在內存中的,不是在寄存器中的,所以對寄存器進行取地址是沒有意義的。並且應該注意的是給register定義的變量,應該賦一個比寄存器大小 要小的值。注意:register只是請求寄存器變量,但是不一定申請成功。
4.關鍵字static:=
   對於static有兩種用法:
   a.修飾變量:對於靜態全局變量和靜態局部變量,都有一個特點就是不能被作用域外面,或外文件調用(即使是使用了extern也沒用)。原因就是它是存儲在靜態存儲區中的。對於函數中的靜態局部變量還有一個問題,就是它是存在靜態存儲區的,即使函數結束棧區收回,這個變量的值也不改變。static int i=0;  這是一條初始化語句  而不是一條賦值語句  所以跟i=0不一樣的。
   b.修飾函數 :是定義爲靜態函數,使函數只能在文件內部使用,這樣不同文件中的函數名就不怕重名了。原因也是相同的,就是static修飾的一切都是在靜態存儲區中的。
-
static代碼如下:
  1. #include <stdio.h>  
  2. #include <stdlib.h>  
  3.   
  4. int main(void)   
  5. {  
  6.     static int j=0;  
  7.     int k;  
  8.     void fun1()  
  9.     {  
  10.         j=0;  
  11.         j++;  
  12.         printf("fun1 %d\n",j);  
  13.     }  
  14.     void fun2()  
  15.     {  
  16.   
  17.         static int i=0;  
  18.         //i=0;  
  19.         printf("fun2 %d\n",i);  
  20.         i++;  
  21.     }  
  22.     for(k=0;k<10;k++)  
  23.     {  
  24.             fun1();  
  25.             fun2();  
  26.     }   
  27.     return 1;    
  28. }  


5.關鍵字sizeof:
怎麼說明sizeof是關鍵字不是函數,這裏有兩個例子:
a. int i;    printf("%d\n",sizeof i); 可見 sizeof是關鍵字
b. sizeof(fun());  不調用fun函數 因爲sizeof是在預編譯期間完成的  說明是關鍵字
sizeof的代碼:
  1. #include <stdio.h>  
  2. #include <stdlib.h>  
  3.   
  4. void fun(int b[100])  
  5. {  
  6.     printf("sizeof(b) is %d\n",sizeof(b));  
  7. }  
  8.   
  9. int main(void)   
  10. {  
  11.     int *p=NULL;  
  12.     int a[100];  
  13.     int b[100];  
  14.     printf("sizeof(p) is %d\n",sizeof(p));  
  15.     printf("sizeof(*p) is %d\n",sizeof(*p));  
  16.     printf("sizeof(a[100]) is %d\n",sizeof(a[100]));  
  17.     printf("sizeof(a) is %d\n",sizeof(a));  
  18.     printf("sizeof(&a) is %d\n",sizeof(&a));  
  19.     printf("sizeof(&a[0] is %d\n",sizeof(&a[0]));  
  20.       
  21.     fun(b);  
  22.     return 1;  
  23. }  
6.關鍵字if:
a.對於bool類型的比較:FLASE都是0  TRUE不一定是1   所以應該用if(bool_num);    if(!bool_num);
對於浮點型與0比較要是否注意:不能直接比較,要定義精度,其實浮點型與浮點型比較也要注意這個問題,就是不能直接比較,要設定精度,如圖:
原因跟浮點型的存儲格式有關,因爲float的有效位是6位,超出6位就未知了,所以不能直接進行比較。同樣的原因,也不能用一個很大的浮點數去加一個很小的浮點數。這個加法可能體現不出來。
b.對於if後面的分號問題 ,一定要注意, 會被解析成if後面有一個空語句, 所以使用空語句的時候最好使用NULL;
c.在使用if else的時候,應該if裏面先處理正常情況(出現概率大的情況),else裏面處理異常情況,這是一個好習慣看着代碼舒服。
7.關鍵字switch、case:
注意case後面應該是整型或者字符型的常量及常量表達式,case後面最好是應該安裝字母或數字順序排列,先處理正常情況,後處理異常情況。
8.關鍵字void:
void *的一般用途是, 接收任何類型的指針 ,如當傳入函數的指針類型不確定的時候,一般用 void*接收任何類型的指針。
void* 指針作爲右值賦值給其他指針的時候一定要強制類型轉換,因爲void* 指針類型不定。
GNU中void *p p++跟char *p p++是一樣的 。
注意:strcpy跟memcpy的區別 就是 strcpy是char *   memcpy是void *  。所以說strcpy是給字符串賦值,memset是給整塊內存賦值。
9.關鍵字extern:
 extern就有兩種用法:一種是聲明外部定義的變量或函數、另一種是extern c告訴編譯器以標準c語言方式編譯
10.關鍵字return:
使用return的時候,要注意不能返回棧內指針,因爲在函數體結束後,棧是會被收回的,其實是不能期望返回一個指針,來返回一塊內存。因爲返回一個指針或者地址沒有問題,因爲return是copy然後返回的,但是那個指針指向的內存如果是在函數棧中的話,就很有可能在函數結束後被收回了!!!
return  ; 一般返回的值是1,根據編譯器而定。
11.關鍵字const:
a.const是用來定義只讀變量的,切忌它定義的是變量,不是常量,真的常量是#define的和enum。
b.在陳正衝老師的這本書中的第35頁,有說編譯器不爲普通const只讀變量分配內存空間,而是將它們保存在符號表中,這使得它成爲一個編譯期間的值,沒有了存儲與讀內存的操作,使得它的效率也很高,節省空間。具體的沒怎麼看懂,本次學習也不打算看懂了(因爲它說const修飾的全局只讀變量是在靜態區的,我太認同)~~~嘿嘿
c.其實const就是修飾變量,然後這個變量就不能當作左值了,當作左值,編譯器就報錯!!!
d. 其實const中最不好區分的知識點是,如圖:
其實對於這四個情況的記憶很簡單,就是看const跟誰近,是const *p   ,還是  * const  p,還是const  *  const  p,這樣就很容易看出來const是修飾誰的了吧。
e.但是const修飾的變量可以通過,指針將其改變。
f.const修飾函數參數表示在函數體內不希望改變參數的值,比如說在strcmp等函數中,用的都是const  char*
g.const修飾函數返回值表示返回值不可以改變,多用於返回指針的情況:
  1. cosnt int* func()  
  2. {  
  3.       static int  count  =  0;  
  4.       count++;  
  5.       return &count;  
  6. }  

h.在看const修飾誰,誰不變的問題上,可以把類型去掉再看,代碼如下:

  1. struct student  
  2. {  
  3.           
  4. }*str;  
  5. const str stu3;  
  6. str const stu4;  

str是一個類型 ,所以在去掉類型的時候,應該都變成const stu3和const stu4了,所以說應該是stu4和stu3這個指針不能被賦值。
12.關鍵字volatile:
volatile搞嵌入式的,一定都特別屬性這個關鍵字,記得第一使用這個關鍵字的時候是在韋東山老師的,Arm裸機視頻的時候。volatile是告訴編譯不要對這個變量進行任何優化,直接在內存中進行取值。一般用在對寄存器進行賦值的時候,或修飾可能被多個線程訪問的變量。

注意:const  volatile  int  i;  應該是定義了一個只讀寄存器。
13.關鍵字struct:
a.對於空結構體的大小問題 ,vc和gcc的輸出是不一樣的,vc是1 、gcc是0 ,而且vc對於結構體的定義也和gcc不一樣 ,vc中有c++的標準擴展了struct的作用,而gcc中是純c的標準,就是按照標準c語言來的。
b.struct這裏還有一個很有用的東西,就是柔性數組,這個東西很有意思,我已經在數據結構的靜態鏈表中進行了闡述,這裏就僅僅記錄一下,不詳細說明了。
14.關鍵字union: 
union有一個作用就是判斷,pc是大端存儲還是小端存儲的,x86是小端存儲的,這個東西是有cpu決定的。arm(由存儲器控制器決定)和x86一樣都是小端的。
下面的是一個大端小端的一個例子,代碼如下:
  1. #include <stdio.h>  
  2. #include <stdlib.h>  
  3. int main(void)   
  4. {  
  5.     int a[5]={1,2,3,4,5};  
  6.     int *p=(int *)(&a+1);  //數組指針 加一  進行正常的指針運算 走到數  
  7.   
  8. 組尾   
  9.     int *d=(int *)((int)a+1);//地址加一  不是指針運算  
  10.     //printf("%x\n",*((char *)((int)a+1)-1));  
  11.        
  12.     /*因爲是小端存儲  高地址  0x00  0x00  0x00  0x02  0x00  0x00  0x00  0x01 低地址*/  
  13.     /*變成了 0x02  0x00  0x00  0x00 */   
  14.     printf("%x,%x",p[-1],*d);  /*  第二個值就是這麼存儲的0x02  0x00  0x00  0x00  低地址處  所以就是2000000*/  
  15.     int a=0x11223344;  
  16.     char *p=(char *)((int)&a);  
  17.     printf("%x\n%x\n",*(p+0),p+0);   
  18.     printf("%x\n%x\n",*(p+1),p+1);  
  19.     return 0;  
  20. }  
下面是一個利用union判斷PC是大端小端的例子,代碼如下:
  1. #include <stdio.h>  
  2. #include <stdlib.h>  
  3. union  
  4. {  
  5.     int i;  
  6.     char a[2];  
  7. }*p,u;  
  8.   
  9. int main(void)   
  10. {  
  11.     p=&u;  
  12.     p->i=0x3839;  
  13.     printf("%x\n",p->i);  
  14.     printf("a0p=%x,a1p=%x\n",&(p->a[0]),&(p->a[1]));  
  15.     printf("a0=%x,a1=%x\n",p->a[0],p->a[1]);    
  16.     return 0;  
  17. }  
15.enum關鍵字:
枚舉enum其實就是int類型,用來保存枚舉常量的。enum枚舉類型,這個纔是真正的常量,定義常量一般用enum 。#define是宏定義是在預編譯期間單純的替換。#define宏定義無法調試,枚舉常量是可以調試的。#define宏定義是無類型信息的,枚舉類型是有類型信息的常量,是int型的。
16.typedef關鍵字:
a.typedef用於給一個已經存在的數據類型重新命名。
b.typedef並沒有產生新的數據類型
c.typedef重定義的類型不能進行unsigned和signed進行擴展
原因在於typedef 定義新類型的時候 應該定義全了,unsigned int是一個類型  不能拆開的。
  1. typedef  unsigned  int   int32;  
d.typedef 和 #define的區別:typedef是給已有的類型取別名,而#define只是簡單的字符替換。區別如下圖:
#define PCHAR char*             PCHAR p3,p4;  //p3是char*型 p4是char型
typedef char* PCHAR;             PCHAR p1,p2;    //p1和p2都是 char*型
e.有一個知識點忘記了,嘿嘿,程序如下:
  1. typedef struct student  
  2. {  
  3. }str,*str1;  

str1 abc;  就是定義一個struct student *類型
str abc;   就是定義一個struct student 類型
f.對於const和typedef還有兩個問題遺漏了,在< c++學習筆記(1.c到c++的升級)>這篇文章中的最後 (8.補充) 中進行了闡述。

17.關鍵字for
a.長循環應該在最內層,這樣可以減少各個層直接的切換
b.看看如下兩段代碼有什麼區別:
  1. 程序一:  
  2. for(i=0; i<m; i++)  
  3. {  
  4.     for(j=0; j<n; j++)  
  5.     {  
  6.         for(k=0; k<p; k++)  
  7.         {  
  8.             c[i][j] = a[i][k] * b[k][j];  
  9.         }  
  10.     }  
  11. }  
  12.   
  13. 程序二:  
  14. for(i=0; i<m; i++)  
  15. {  
  16.     for(k=0; k<p; k++)  
  17.     {  
  18.         for(j=0; j<n; j++)  
  19.         {  
  20.             c[i][j] = a[i][k] * b[k][j];  
  21.         }  
  22.     }  
  23. }  
從程序來看,兩者實現了同樣的功能,區別只是第二層和第三層循環交換了位置。但是他們的差距卻是巨大的 ,這個需要從CPU的cache來說了, cpu每次訪問內存的時候都會先從內存將數據讀入cache ,然後以後都從cache取數據。但是cache的大小是有限的 ,因此只會有部分進入cache。我們來看這個程序 c[i][j] = a[i][k] * b[k][j];  我們都知道C中二維數組是在內存中一維排列的,如果我們把k循環放在第三層 ,那麼cache基本沒有用了, 每次都需要重新到內存取數據,交換後每次取到cache的數據都可以複用多次 。所以說第二種寫法效率高。
18.關鍵字char(本節最重要的知識點char越界的問題):
對於char有兩種類型,分別是:unsigned  char(範圍是0~255)和  signed  char(範圍是-128~127)  一個是有符號的,一個是沒有符號的。
在計算機中數據都是以數據的補碼形式進行存儲的,所以如圖:
對於無符號類型(unsigned  int):就是不考慮最高位的問題,都是原碼與補碼相等的情況。
     然後我們說說越界的問題,對於一個unsigned  char  i;  我們給 i = 256;這很明顯越界了,i是0到255的,那256的補碼是什麼再在它補碼中取低八位就是i的值了。256的補碼是1  0000  0000,所以printf ("%d\n",i);的值會是0。如果i = -1;-1的補碼是1111 1111 所以會打印出255。
     對於一個char類型的越界又是什麼樣的呢?
     char  i; 我們給 i  =  129; 129是一個正數,它的補碼就是原碼:是1000 0001,但是它是char型,在char型中1000  0001是什麼,如圖是-127。所以printf("%d\n",i);  得到的是-127。如果i  =  -129,它的補碼是0111  1111,所以它打印出來的是127。如果是i  =  259,我們就把它的補碼取低八位來看。259的補碼是1  0000  0011  所以說打印出來的是3。最後一個例子,如果i  =  385,它的補碼是1   1000    0001  ,取低八位是1000   0001,所以打印的應該是-127。
     其實不管是有符號的還是沒符號的,原則就一個,把數據轉換成爲補碼,取低八位,然後在上面的圖中去比較,就ok了。
給一個練習,代碼如下:
  1. #include <stdio.h>  
  2. #include <stdlib.h>  
  3. #include <string.h>  
  4. int main()  
  5. {  
  6.     char a[1000];  
  7.     int i;  
  8.     for(i=0; i<1000; i++)  
  9.     {  
  10.         a[i] = (-1-i);  
  11.     }     
  12.          while(a[i])  
  13.     {  
  14.         printf("%d\n",a[i]);  
  15.         i++;  
  16.     }  
  17.     printf("%d\n",strlen(a));  
  18.     return 0;  
  19. }  

打印結構是什麼:答案是255   分析步驟跟上面是一樣的,自己算算吧!!!
其實int的越界原理跟char是一樣的。
19.一個關於tab鍵的問題:
不同編輯器的tab鍵的字符數是不一樣的,一般是4個字符,也有兩個字節的,要注意一下,爲了代碼格式的整齊,建議設置一下tab或者使用空格。

本節遺留問題:

1.printf的實現問題,其實就是可變參數的問題,看linux源碼,還有一個問題就是轉移字符的問題,char p = '\'' 這樣一個問題。
2.浮點型的存儲格式,爲什麼有效位是6位,小數是怎麼保存的。

二、

本節接觸了,C語言中的三大蛋疼:符號優先級  ++i順序點  貪心法  (其實這裏面好多都是跟編譯器有關的,而且有好多問題都是可以通過良好的編程習慣避免的)

本節知識點:

1.註釋問題:

    註釋不能把關鍵字弄斷,如:in/*註釋*/t

    註釋不是簡單的剔除,而是使用空格替換

    編譯器認爲雙引號括起來的內容都是字符串,雙斜槓也不例外。如:char *p = "heh//jfeafe"   //不起註釋作用

2.接續符:

    接續符\  ,常用於宏定義中 

  1. #define SWAP(a,b) \  
  2. {                 \  
  3.     int temp = a; \  
  4.     a = b;        \  
  5.     b = temp;     \  
  6. }  

    反斜槓同時有接續符和轉義符兩個用途,當接續符使用的時候,可以直接在程序中出現。當轉義符使用的時候,必須是出現在字符串中。

    接續符,也用與接續一個關鍵字,代碼如下,  注意: 但是直接連接\兩邊不能有空格。

  1. #include <stdio.h>  
  2. #include <stdlib.h>  
  3.   
  4. int main()  
  5. {  
  6.     cha\  
  7. r a = 12;  
  8.     return 0;  
  9. }  

3.邏輯運算符:有一個短路規則
4.最容易忘記規則的兩個運算符:

    三目運算符:(a?b:c)   當a的值爲真的時候   返回b的值,否則返回c的值

    逗號表達式:a,b    表達式的值爲b的值

5.位運算:

    對於左移和右移<<  >>問題 :無符號的,和有符號左移,都是補0 ,對於有符號的在右移動的時候,正數補零,負數補什麼跟編譯器有關係。並且左移和右移的大小不能大於數據的長度,也不能小於0。

    交換兩個數,有一種不借助中間變量的方法,就是異或,代碼如下:

  1. #include <stdio.h>  
  2.   
  3. #define SWAP1(a,b) \  
  4. {                  \  
  5.     int temp = a;  \  
  6.     a = b;         \  
  7.     b = temp;      \  
  8. }  
  9.   
  10. #define SWAP2(a,b) \  
  11. {                  \  
  12.     a = a + b;     \  
  13.     b = a - b;     \  
  14.     a = a - b;     \  
  15. }  
  16.   
  17. #define SWAP3(a,b) \  
  18. {                  \  
  19.     a = a ^ b;     \  
  20.     b = a ^ b;     \  
  21.     a = a ^ b;     \  
  22. }  
  23.   
  24. int main()  
  25. {  
  26.     int a = 1;  
  27.     int b = 2;  
  28.       
  29.     SWAP1(a,b);  
  30.     SWAP2(a,b);  
  31.     SWAP3(a,b);  
  32.       
  33.     return 0;  
  34. }  

6.i++,i--順序點:

        只有 i++ i--纔有順序點  就是什麼時候開始加,什麼時候開始減。真心對於順序點 是搞不懂啊~~~ (++i)+(++i)+(++i) ,在gcc中是5+5+6(DEV C++) ,在vc中是6+6+6(vc++6.0) ,不同編譯器順序點不一樣。這個例子的順序點 在; 前。
        a=((++i),(++i),(++i))  它的順序點在每個逗號前面完成計算。我覺得特殊的順序點 是可以通過合理的順序佈局來避免的。

7.貪心法:

        每一個符號應該儘可能多的包含字符
8.符號運算優先級問題:

        個人覺得優先級不用記,好好的寫括號吧~~~

         給一個易錯優先級表,如圖:

9.c語言中的類型轉換:

    c語言中有兩種轉換類型,分別是:隱式轉換和顯示轉換(強制類型轉換)

    隱式轉換的規則:

    a.算術運算中,低類型轉換爲高類型

    b.賦值運算中,表達式的類型轉換爲左邊變量的類型

    c.函數調用時,實參轉換成形參的類型

    d.函數返回值,return表達式轉換爲返回值的類型

隱式轉換的例子,代碼如下:

  1. #include <stdio.h>  
  2.   
  3. int main()  
  4. {  
  5.     int i = -2;  
  6.     unsigned int j = 1;  
  7.       
  8.     if( (i + j) >= 0 )  
  9.     {  
  10.         printf("i+j>=0\n");  
  11.     }  
  12.     else  
  13.     {  
  14.         printf("i+j<0\n");  
  15.     }  
  16.       
  17.     printf("i+j=%d\n", i + j);  
  18.       
  19.     return 0;  
  20. }  

注意:在使用C語言的時候,應該特別注意數據的類型是否相同,儘量避免隱式轉換帶來的不必要的麻煩~~~



   三、

本節知識點:

1.編譯過程的簡介:

   預編譯:

a.處理所有的註釋,以空格代替。

b.將所以#define刪除,並展開所有的宏定義,字符串替換。

c.處理條件編譯指令#if,#ifdef,#elif,#else,#endif

d.處理#include,並展開被包含的文件,把頭文件中的聲明,全部拷貝到文件中。

e.保留編譯器需要使用的#pragma指令、

怎麼樣觀察這些變化呢?最好的方法就是在GCC中,輸入預處理指令,可以看看不同文件經過預處理後變成什麼樣了,預處理指令:gcc -E file.c -o file.i   注意:-C -E一起使用是預編譯的時候保留註釋。

   編譯:

a.對預處理文件進行一系列詞法分析,語法分析和語義分析

                詞法分析:主要分析關鍵字,標示符,立即數等是否合法

                語法分析:主要分析表達式是否遵循語法規則

                語義分析:在語法分析的基礎上進一步分析表達式是否合法

b.分析結束後進行代碼優化生成相應的彙編代碼文件               編譯指令:gcc -S  file.c  -o  file.s

   彙編:

彙編器將彙編代碼轉變爲機器可以執行的指令,每個彙編語句幾乎都對應一條機器指令,其實機器指令就是機器碼,就是2進制碼。彙編指令:gcc  -c  file.c  -o file.o  注意:-c是編譯彙編不連接。

   鏈接:

再把產生的.o文件,進行鏈接就可以生成可執行文件。連接指令:gcc  file.o  file1.o  -o  file  這句指令是鏈接file.o和file1.o兩個編譯並彙編的文件,並生成可執行文件file。

鏈接分兩種:靜態鏈接和動態鏈接,靜態鏈接是在編譯器完成的,動態鏈接是在運行期完成的。靜態鏈接的指令是:gcc -static file.c -o file對於一些沒有動態庫的嵌入式系統,這是常用的。

一般要想通過一條指令生成可執行文件的指令是:   gcc file.c  -o  file

   資料:這裏面說到了很多關於gcc的使用的問題,我提供一個gcc的學習資料,個人覺得還不錯,也不長,就是一個txt文檔,很全面。資源下載地址http://download.csdn.net/detail/qq418674358/6041183   Ps:嘿嘿,設了一個下載積分,因爲真的是沒分用了!希望大家見諒哈!

 

2.c語言中的預處理指令:#define、#undef(撤銷已定義過的宏名)、#include、#if、#else、#elif、#endif、#ifdef、#ifndef、#line、#error、#pragma。還有一些ANSI標準C定義的宏:__LINE__、__FILE__、__DATA__、__TIME__、__STDC__。這樣使用printf("%s\n",__TIME__);     printf(__DATE__);

一個#undef的例子:

  1. #include <stdio.h>  
  2. #include <stdlib.h>  
  3. #include <string.h>  
  4.   
  5.   
  6. #define X 2  
  7. #define Y X*2  
  8. #undef X  
  9. #define X 3  
  10.   
  11.   
  12. int main()  
  13. {  
  14.     printf("%d\n",Y);  
  15.     return 0;  
  16. }  

這個輸出的是6,說明了#undef的作用

3.宏定義字符串的時候:應該是 #define HELLO "hello world"  記住是雙引號。還有就是一切宏都是不能有分號的,這個一定要切忌!!!

4.宏與函數的比較:

   a.宏表達式在預編譯期被處理,編譯器不知道有宏表達式存在

   b.宏表達式沒有任何的"調用"開銷

   c.宏表達式中不能出現遞歸定義

5.爲什麼不在頭文件中定義全局變量:

如果一個全局變量,想要在兩個文件中,同時使用,那這兩個文件中都應該#include這個頭文件,這樣的話就會出現重複定義的問題。其實是重名的問題,因爲#include是分別在兩個文件中展開的,試想一下,如果在兩個文件中的開始部分,都寫上int  a = 10;  是不是也會報錯。可能你會說那個#ifndef不是防止重複定義嗎?是的 ,那是防止在同一個文件中,同時出現兩次這個頭文件。現在是兩個文件中,所以都要展開的。全局變量就重名了!!!所以 對於全局變量,最好是定義在.c文件中,不要定義在頭文件中。

6.#pargma pack 設置字符對齊,看後面一節專門寫字符對齊問題的!!!

7.#運算符(轉換成字符串):

    假如你希望在字符串中包含宏參數,那我們就用#號,它把語言符號轉換成字符串。

    #define SQR(x) printf("the "#x"lait %d\n",((x)*(x)));
    SQR(8)
    輸出結果是:the 8 lait 64   這個#號必須使用在帶參宏中

有個小例子:

  1. #include <stdio.h>  
  2. #include <stdlib.h>  
  3. #include <string.h>  
  4. /*在字符串中  加入宏參用的*/  
  5. #define SCAN(N,String) scanf("%"#N"s",String);  //N是截取的個數  String是存儲的字符串   
  6. int main()  
  7. {  
  8.     char dd[256];  
  9.     SCAN(3,dd) //記得沒有分號哈  自定義 任意格式輸入的scanf  截取輸入的前三個   
  10.     printf("%s\n",dd);  
  11.     return 1;  
  12. }  

8.##運算符(粘合劑)

    一般用於粘貼兩個東西,一般是用作在給變量或函數命名的時候使用。如#define XNAME(n) x##n

    XNAME(8)爲8n   這個##號可以使用在帶參宏或無參宏中

下面是一個##運算符的小例子,代碼如下:

  1. #include <stdio.h>  
  2. #include <stdlib.h>  
  3. #include <string.h>  
  4.   
  5. #define BL1 bb##ll##1  
  6.   
  7. #define BL(N) bbll##N  
  8. int main()  
  9. {  
  10.     int BL1=10;  
  11.   
  12.     int BL(4)=15;  
  13.     printf("%d\n",bbll1);  
  14.       
  15.     printf("%d\n",bbll4);  
  16.     return 1;  
  17. }  

注意:#號和##號都必須只能在宏定義中使用,不能使用在其他地方
9.其實預編譯這塊還有一些,不常用到的預編譯指令,也是盲點,但是不難理解,用到的時候查查就好。比如說#line、#error、#warning等。




四、



很多人都覺得內存對齊這個問題很難,很不好算,總算錯,其實我想說只要你畫一畫就沒那麼難了。好了,進入正題。

本節知識點:

1.結構體爲什麼要內存對齊(也叫字節對齊):

其實我們都知道,結構體只是一些數據的集合,它本身什麼都沒有。我們所謂的結構體地址,其實就是結構體第一個元素的地址。這樣,如果結構體各個元素之間不存在內存對齊問題,他們都挨着排放的。對於32位機,32位編譯器(這是目前常見的環境,其他環境也會有內存對齊問題),就很可能操作一個問題,就是當你想要去訪問結構體中的一個數據的時候,需要你操作兩次數據總線,因爲這個數據卡在中間,如圖:

在上圖中,對於第2個short數據進行訪問的時候,在32位機器上就要操作兩次數據總線。這樣會非常影響數據讀寫的效率,所以就引入了內存對齊的問題。

另外一層不太重要的原因是:某些硬件平臺只能從規定的地址處取某些特定類型的數據,否則會拋出硬件異常。

2.內存對齊的規則:

    a.第一個成員起始於0偏移處

    b.每個成員按其類型大小和指定對齊參數n中較小的一個進行對齊

    c.結構體總長度必須爲所有對齊參數的整數倍

    d.對於數組,可以拆開看做n個數組元素

3.來幾個小例子,畫畫圖,有助於理解:

第一個例子,代碼如下:

  1. #include <stdio.h>  
  2. struct _tag_str1  
  3. {  
  4.     char a;  
  5.     int b;  
  6.     short c;  
  7. }str1;  
  8.   
  9. struct _tag_str2  
  10. {  
  11.     char a;  
  12.     short c;  
  13.     int b;  
  14. }str2;  
  15.   
  16. int main()  
  17. {  
  18.     printf("sizeof str1 %d\n",sizeof(str1));  
  19.     printf("sizeof str2 %d\n",sizeof(str2));  
  20.     return 0;  
  21. }   
輸出的結果分別是:str1爲12    str2爲8,分析的過程如下圖:

看圖很自然就知道了str1爲12個字節,str2爲8個字節。

第二個例子,上面的那個例子有好多問題還沒有考慮到,比如說上面的那個例子在8字節對齊,和4字節對齊的情況都是一樣的。結構體中嵌套結構體的內存對齊怎麼算,所以就有了這個例子,代碼如下:

  1. #include <stdio.h>  
  2.   
  3. #pragma pack(8)  
  4. //#pragma pack(4)  
  5. struct S1  
  6. {  
  7.     short a;  
  8.     long b;  
  9. };  
  10.   
  11. struct S2  
  12. {  
  13.     char c;  
  14.     struct S1 d;  
  15.     double e;  
  16. };  
  17.   
  18. #pragma pack()  
  19.   
  20. int main()  
  21. {  
  22.     struct S2 s2;  
  23.       
  24.     printf("%d\n"sizeof(struct S1));  
  25.     printf("%d\n"sizeof(struct S2));  
  26.     printf("%d\n", (int)&(s2.d) - (int)&(s2.c));  
  27.   
  28.     return 0;  
  29. }  
在Dev c++中,默認的是8字節對齊。我們分析下在4字節對齊的情況下輸出的是,S2是20,S1是8,分析如圖:


在4字節對齊的情況中,有一個問題值得注意:就是圖中畫1的地方。這裏面本應short是可以上去的。但是對於結構體中的結構體一定要十分警惕,S1是一體的,short已經由於long進行了內存對齊,後面還空了兩個字節的內存,其實此時的short已經變成了4個字節了!!!即結構體不可拆,不管是多少字節對齊,他們都是一體的。所有的圈都變成了叉。所以說結構體只能往前篡位置,不能改變整體。

我們在分析一些8字節對齊的情況,如圖:


同樣,到這裏又有一個字節對齊的原則要好好重申一下:就是以什麼爲對齊參數,首先我們要知道編譯器或者自己定義的是多少字節對齊的,這個數爲n。然後我們要看這個結構體中的各個數據類型,找到所佔字節數最大的類型,爲m。如果n大於m,就以m爲對齊參數,比如說一個4字節對齊的結構體中都是short,那這個結構體以什麼爲對齊參數,當然是2了,如果m大於n,就以n爲對齊參數,比如說在4字節對齊的情況下的double類型。

 以上就是我對內存對齊的小總結,最最想要說明的就是兩大段紅色的部分。    



1.int a=9,b=10,d=9;是可以的。

2.%*d ,在scanf中使用的時候,是1整數但不賦給任何變量,有個小代碼:

  1. #include <stdio.h>  
  2. #include <malloc.h>  
  3.   
  4. int main()  
  5. {  
  6.     int a=23,b=5,c=9;  
  7.     scanf("%*d%d%d",&a,&b,&c);  
  8.     printf("%d,%d,%d",a,b,c);  
  9.     return 0;  
  10. }  

a的值,你是賦值不進去的,僅僅佔位用的。

3.對於冒泡排序,怎麼在不完全執行完循環前就預先判斷,已經排序結束了:

在一次內層循環的時候,一次都沒有進行數據交換,就說名冒泡排序已經排序ok了。

4.不要總記得scanf,同樣還存在getchar()和gets()函數,gets能接收含有空格的字符串,這個是scanf不能做到的。

scanf("%ls",a);  //接收有效字符串的第一個字符

scanf("%ns",a);   //這個是格式化輸入,接收字符串的從頭開始的n個字符

其實我想說,scanf函數真心沒有什麼用,很不好的一個函數。

5.堆區分配內存是從兩頭開始增長的,不是單向增長的。

6.typedef int [10]   其實[10]就是int了,個人覺得這個代碼風格,很不好,千萬不能寫成這樣,可讀性很差!

7.要記住函數在傳遞參數的時候,其實是數據的拷貝,直接對形參進行改變或者賦值,是毫無意義的,實參是不會改變的。對於指針也是一樣的。只有通過指針,取得了當前這個指針指向的內容的時候,改變了這個內容,這樣實參纔會被改變。因爲是直接改變了內存地址中保存的數值。

舉個例子就是:在數據結構那節中的鏈表,creat函數就是一個典型的例子。仔細想想爲什麼不能在main函數中定義一個頭結點,再把這個頭結點的地址傳給creat函數呢?一定要通過creat返回一個頭結點指針呢?再想想,爲什麼在想通過形參獲得子函數中數據的時候,一定要傳入地址或者指針呢?然後再把想要獲得數據,寫入這個地址或者指針中去?

給一段代碼,幫助理解這個問題:

  1. #include <stdio.h>  
  2. #include <malloc.h>  
  3. typedef struct _tag_str  
  4. {  
  5.     int a;  
  6.     int b;  
  7. }str;  
  8. void fun(str* str1)  
  9. {  
  10.     str1 = (str* )malloc(sizeof(str));  
  11.     str1->a = 12;  
  12.     str1->b = 34;  
  13. }  
  14. int main()  
  15. {  
  16.     /*str* strp; 
  17.     fun(strp); 
  18.     printf("%d\n",strp->a); 
  19.     printf("%d\n",strp->b);*/  
  20.     str str1;  
  21.     fun(&str1);  
  22.     printf("%d\n",str1.a);  
  23.     printf("%d\n",str1.b);  
  24. }  

想想,爲什麼子函數中賦值,在main中打印出來是不一樣的!!!
      對於fun(strp)的過程是這樣的:在函數傳遞參數的時候,strp的值 賦值給了子函數的str1,這個過程就是函數參數拷貝的過程,然後str1的值在malloc的時候不幸被malloc改變了,所以在main中打印出來的不一樣。

      對於fun(&str1)的過程是這樣的:在函數傳遞參數的時候,&str1的值  賦值給了子函數的str1,後面的過程跟上面一樣。所以在main中打印的也是不一樣的。

對於這種情況,最好的解決辦法就是利用函數返回值,把str1返回 回來就ok了!!!

注意:可能你會問了,那怎樣通過參數獲得子函數傳遞的值啊,其實很簡單,你在main中開闢好一段內存,然後把這個內存地址傳遞到子函數中去,然後對這個內存進行賦值,不要去改變這個指針的指向(即指針的值),僅僅改變指針指向的內存(即指針指向的內容),自然就獲得了你想要的值!

8.c語言文件操作的一個問題:

   c語言中打開文件有兩種方式,一種是二進制方式,另一種是文本方式(ASCII碼方式)。這兩種方式有什麼區別?(對於Linux這種只有一種文件類型的操作系統來說是沒有區別的)

   我們就以windows爲例說說區別:

   a.以文本方式打開文件,若將數據寫入文件,如果遇到換行符'\n'(ASII 值爲10,0A),則會轉換爲回車—換行'\r\n'(ASCII值爲13,10,0D0A)存入到文件中,同樣讀取的時候,若遇到回車—換行,即連續的ASCII值13,10,則自動轉換爲換行符。

      而以二進制方式打開文件時,不會進行這樣的處理。

   b.還有如果以文本方式打開文件時,若讀取到ASCII碼爲26(^Z)的字符即0x1a,則停止對文件的讀取,會默認爲文件已結束,而以二進制方式讀取時不會發生這樣的情況。由於正常情況下我們手動編輯完成的文件是不可能出現ASCII碼爲26的字符,所以可以用feof函數去檢測文件是否結束。

   所以,由於存在上面的兩個區別,我們在明確文件類型的時候,最好使用相對應的方式對文件進行打開。對於那些不明確文件類型的時候,最好使用二進制方式打開文件。



指針這一節是本書中最難的一節,尤其是二級指針和二維數組直接的關係。

本節知識點:

1.指針基礎,一張圖說明什麼是指針:

2.跨過指針,直接去訪問一塊內存:
    只要你能保證這個地址是有效的 ,就可以這樣去訪問一個地址的內存*((unsigned int *)(0x0022ff4c))=10;  但是前提是 0x0022ff4c是有效地址。對於不同的編譯器這樣的用法還不一樣,一些嚴格的編譯器,當你定義一個指針,把這個指針賦值爲一個這樣的地址的時候,當檢測到地址無效,編譯的時候就會報錯!!!如果一些不太嚴格的編譯器,不管地址有效無效都會編譯通過,但是對於無效地址,當你訪問這塊地址的時候,程序就會運行停止!
3.a     &a    &a[0]三者的區別:
首先說三者的值是一樣的,但是意義是不一樣的。(這裏僅僅粗略的說說,詳細見文章<c語言中數組名a和&a>)
     &a[0]:這個是數組首元素的地址
     a : 的第一個意義是 數組首元素的地址,此時與&a[0]完全相同
                第二個意義是 數組名  sizeof(a)  爲整體數組有多少個字節
    &a :這個是數組的地址 。跟a的區別就是,a是一個 int* 的指針(在第一種意義的時候) ,而&a是一個 int (*p)[5]類型的數組指針,指針運算的結果不一樣。(此處的int* 僅僅是爲了舉例子,具體應該視情況而定)
4.指針運算(本節最重要的知識點,但並不是最難的,所以的問題都來源於這兒):   
   對於指針的運算,首先要清楚的是指針類型(在C語言中,數據的類型決定數據的行爲),然後對於加減其實就是對這個指針的大小加上或者減去,n*sizeof(這個指針指向的數據的類型)。即:一個類型爲T的指針的移動,是以sizeof(T)爲單位移動的。如:int* p;  p+1就是p這個指針的值加上sizeof(int)*1,即:(unsigned int)p + sizeof(int)*1。對於什麼typedef的,struct的,數組的都是一樣的。
這個有一個例子,代碼如下:
  1. #include <stdio.h>  
  2. #include <stdlib.h>  
  3.   
  4. int main(int argc, char *argv[])   
  5. {  
  6. /*  int a[20]={1,2,4}; 
  7.     printf("%d\n",sizeof(a)); 
  8.     printf("%p\n",a); 
  9.     printf("%p\n",&a); 
  10.     printf("%p\n",&a[0]);    
  11. */  
  12.   
  13.   
  14. /*  int a[5]={1,2,3,4,5}; 
  15.     int (*p)[5]=&a; 
  16.     printf("%d\n",*((int *)(p+1)-1)); 
  17. */    
  18.       
  19.     int a[5]={1,2,3,4,5};  
  20.     int* p=(int *)(&a+1);  
  21. //  int *p=&a+1;  //這個條語句是  把&a這個數組指針 進行了指針運算後  的那個地址  強制類型轉換成了 int *指針   
  22.     printf("%d\n",*(p-1));  
  23.     return 0;  
  24.       
  25. }  
5.訪問指針和訪問數組的兩種方式:
    分別是以下標方式訪問和以指針的方式訪問,我覺得沒有任何區別,*(p+4)和p[4]是一樣的 ,其實都可以理解成指針運算。如果非要說出區別,我覺得指針的方式會快些,但是在當前的硬件和編譯器角度看,不會太明顯。同樣下標的方式可讀性可能會高些。
6.切記數組不是指針:

    數組是數組,指針是指針,根本就是兩個完全不一樣的東西。當然要是在宏觀的內存角度看,那一段相同類型的連續空間,可以說的上是數組。但是你可以嘗試下,定義一個指針,在其他地方把他聲明成數組,看看編譯器會不會把兩者混爲一談,反過來也不會。
    但是爲什麼我們會經常弄混呢?第一,我們常常利用指針的方式去訪問數組。第二,數組作爲函數參數的時候,編譯器會把它退化成爲指針,因爲函數的參數是拷貝,如果是一個很大的數組,拷貝是很浪費內存的,所以數組會被退化成指針(這裏一定要理解好,退化的是數組成員的類型指針,不一定是數組指針的哈)。
7.弄清數組的類型:
   數組類型是由數組元素類型數組長度兩個因素決定的,這一點在數組中體現的不明顯,在數組指針的使用中體現的很好。
  1. char a[5]={'a','b','c','d','e'};  
  2. char (*p)[3]=&a;  
   上面的代碼是錯誤的,爲什麼?因爲數組指針和數組不是一個類型,數組指針是指向一個數組元素爲char 長度爲3的類型的數組的,而這個數組的類型是數組元素是char長度是5,類型不匹配,所以是錯的。
8.字符串問題:
   a.C語言中沒有真正的字符串,是用字符數組模擬的,即:字符串就是以'\0'結束的字符數組。
   b.要注意下strlen,strcmp等這個幾個函數的返回值,是有符號的還是無符號的,這裏很容易忽略返回值類型,造成操作錯誤。
   c.使用一條語句實現strlen,代碼如下(此處注意assert函數的使用,安全性檢測很重要):
  1. #include <stdio.h>  
  2. #include <assert.h>  
  3.   
  4. int strlen(const char* s)  
  5. {  
  6.     return ( assert(s), (*s ? (strlen(s+1) + 1) : 0) );  
  7. }  
  8.   
  9. int main()  
  10. {  
  11.     printf("%d\n", strlen( NULL));  
  12.       
  13.     return 0;  
  14. }  
    d.自己動手實現strcpy,代碼如下:
  1. #include <stdio.h>  
  2. #include <assert.h>  
  3.   
  4. char* strcpy(char* dst, const char* src)  
  5. {  
  6.     char* ret = dst;  
  7.       
  8.     assert(dst && src);  
  9.       
  10.     while( (*dst++ = *src++) != '\0' );  
  11.       
  12.     return ret;  
  13. }  
  14.   
  15. int main()  
  16. {  
  17.     char dst[20];  
  18.   
  19.     printf("%s\n", strcpy(dst, "hello!"));  
  20.       
  21.     return 0;  
  22. }  
     e.推薦使用strncpy、strncat、strncmp這類長度受限的函數(這些函數還能在字符串後面自動補充'\0'),不太推薦使用strcpy、strcmpy、strcat等長度不受限僅僅依賴於'\0'進行操作的一系列函數,安全性較低。
     f.補充問題,爲什麼對於字符串char a[256] = "hello";,在printf和scanf函數中,使用a行,使用&a也行?代碼如下:
  1. #include <stdio.h>  
  2. int main()  
  3. {  
  4.     char* p ="phello";  
  5.     char a[256] = "aworld";  
  6.     char b[25] = {'b','b','c','d'};  
  7.     char (*q)[256]=&a;  
  8.       
  9.     printf("%p\n",a);  //0022fe48  
  10.     //printf("%p\n",&a);  
  11.     //printf("%p\n",&a[0]);  
  12.       
  13.       
  14.     printf("tian %s\n",(0x22fe48));   
  15.     printf("%s\n",q);    //q就是&a   
  16.     printf("%s\n",*q);   //q就是a   
  17.       
  18.     printf("%s\n",p);  
  19.       
  20.     printf("%s\n",a);  
  21.     printf("%s\n",&a);  
  22.     printf("%s\n",&a[0]);  
  23.       
  24.     printf("%s\n",b);  
  25.     printf("%s\n",&b);  
  26.     printf("%s\n",&b[0]);     
  27. }  
對於上面的代碼:中的0x22fe48是根據打印a的值獲得的。
printf("tian %s\n",(0x22fe48));這條語句,可以看出來printf真的是不區分類型啊,完全是根據%s來判斷類型。後面只需要一個值,就是字符串的首地址。a、&a、&a[0]三者的值還恰巧相等,所以說三個都行,因爲printf根本就不判斷指針類型。雖然都行但是我覺得要寫有意義的代碼,所以最好使用a和*p。還有一個問題就是,char* p = "hello"這是一個char*指針指向hello字符串。所以對於這種方式只能使用p。因爲*p是hello字符串的第一個元素,即:‘h’,&p是char* 指針的地址,只有p是保存的hello字符串的首地址,所以只有p可以,其他都不可以。scanf同理,因爲&a和a的值相同,且都是數組地址。
9.二維數組(本節最重要的知識點):
      a.對於二維數組來說,二維數組就是一個一維數組 數組,每一個數組成員還是一個數組,比如int a[3][3],可以看做3個一維數組,數組名分別是a[0]  a[1]   a[2]   sizeof(a[0])就是一維數組的大小  ,*a[0]是一維數組首元素的值,&a[0]是 一維數組的數組指針。
      b.也可以通過另一個角度看這個問題。a是二維數組的數組名,數組元素分別是數組名爲a[0]、a[1]、a[2]的三個一維數組。對a[0]這個數組來說,它的數組元素分別是a[0][0]  a[0][1]  、 a[0][2]三個元素。a和a[0]都是數組名,但是是兩個級別的,a作爲數組首元素地址的時候等價於&a[0](最容易出問題的地方在這裏,這裏一定要弄清此時的a[0]是什麼,此時的a[0]是數組名,不是數組首元素的地址,不可以繼續等價下去了,千萬不能這樣想 a是&a[0]    a[0]是&a[0][0]     a就是&&a[0][0] 然後再弄個2級指針出來,自己就蒙了!!!這是一個典型的錯誤,首先&&a[0][0]就沒有任何意義,跟2級指針一點關係都沒有,然後a[0]此時不代表數組首元素地址,所以這個等價是不成立的。Ps:一定要搞清概念,很重要!!! ),a[0]作爲數組首元素地址的時候等價於&a[0][0]。但是二維數組的數組頭有很多講究,就是a(二維數組名)、&a(二維數組的數組地址)、&a[0](二維數組首元素地址  即a[0]一維數組的數組地址 a有的時候也表示這個意思)、a[0](二維數組的第一個元素 即a[0]一維數組的數組名)、&a[0][0](a[0]一維數組的數組首元素的地址 a[0]有的時候也表示這個意思),這些值都是相等,但是他們類型不相同,行爲也就不相同,意義也不相同。分析他們一定要先搞清,他們分別代表什麼。
下面是一個,二維數組中指針運算的練習(指針運算的規則不變,類型決定行爲):
  1. #include <stdio.h>  
  2. #include <stdlib.h>  
  3. #include <stdbool.h>  
  4.   
  5. int main(int argc, char *argv[])   
  6. {  
  7.     int a[3][3]={1,2,3,4,5,6,7,8,9};  
  8.     printf("%d\n",sizeof(a[0]));  
  9.     printf("%d\n",*a[2]);  
  10.     printf("%d\n",*(a[0]+1));  
  11.       
  12.     printf("%p\n",a[0]);  
  13.     printf("%p\n",a[1]);  
  14.     printf("%p\n",&a[0]+1); //&a[0]+1 跟 a[1]不一樣  指針類型不一樣   &a[0]+1這個是數組指針  a[1]是&a[1][0] 是int*指針   
  15.       
  16.     printf("%d\n",*((int *)(&a[0]+1)));  
  17.       
  18.     printf("%d\n",*(a[1]+1));  
  19.       
  20.     printf("%p\n",a);  
  21.     printf("%p\n",&a);  
  22.     printf("%p\n",&a[0]);  
  23.       
  24.     printf("%d\n",sizeof(a));   //這是a當作數組名的時候  
  25.       
  26.     printf("%d\n",*((int *)(a+1))); //此時 a是數組首元素的地址  數組首元素是a[0]    
  27.                  //首元素地址是&a[0]  恰巧a[0]是數組名 &a[0]就變成了數組指針   
  28.     return 0;  
  29. }  
總結:對於a和a[0]、a[1]等這些即當作數組名,又當作數組首元素地址,有時候還當作數組元素(即使當作數組元素,也無非就是當數組名,當數組首元素地址兩種),這種特殊的變量,一定要先搞清它現在是當作什麼用的
      c.二維數組中一定要注意,大括號,還是小括號,意義不一樣的。
10.二維數組和二級指針:
     很多人看到二維數組,都回想到二級指針,首先我要說二級指針跟二維數組毫無關係,真的是一點關係都沒有。通過指針類型的分析,就可以看出來兩者毫無關係。不要在這個問題上糾結。二級指針只跟指針數組有關係,如果這個二維數組是一個二維的指針數組,那自然就跟二級指針有關係了,其他類型的數組則毫無關係。切記!!!還有就是二級指針與數組指針也毫無關係!!
11.二維數組的訪問:
     二維數組有以下的幾種訪問方式:
     int   a[3][3];對於一個這樣的二位數組
     a.方式一:printf("%d\n",a[2][2]); 
     b.方式二:printf("%d\n",*(a[1]+1));
     c.方式三:printf("%d\n",*(*(a+1)+1));
     d.方式四:其實二維數組在內存中也是連續的,這麼看也是一個一維數組,所以就可以使用這個方式,利用數組成員類型的指針。
  1. int *q;  
  2. q = (int *)a;  
  3. printf("%d\n",*(q+6));  
     e.方式五:二維數組中是由多個一維數組組成的,所以就可以利用數組指針來訪問二維數組。
  1. int (*p)[3];  
  2. p = a;  
  3. printf("%d\n",*(*(p+1)+1));  
給一個整體的程序代碼:
  1. #include <stdio.h>  
  2. #include <stdlib.h>  
  3. #include <string.h>  
  4. int main()  
  5. {  
  6.     int a[3][3]={1,2,3,4,5,6,7,8,9};  
  7.     int (*p)[3];  
  8.     int *q;   
  9.     printf("%d\n",*(*(a+1)+1));   //a        *(&a[0]+1)  
  10.     p = a;  
  11.     q = (int *)a;  
  12.     printf("%d\n",*(*(p+1)+1));  
  13.     printf("%d\n",*(a[1]+1));  
  14.     printf("%d\n",a[1][1]);  
  15.     printf("%d\n",*(q+6));  
  16. }  
  17. <span style="font-family:Arial;BACKGROUND-COLOR: #ffffff"></span>  
 
總結:對於二位數組int a[3][3]  要想定義一個指針指向這個二維數組的數組元素(即a[0]等一維數組),就要使用數組指針,這個數組指針要跟數組類型相同。a[0]等數組類型是元素類型是int,長度是3,所以數組指針就要定義成int (*p)[3]。後面的這個維度一定要匹配上,不然的話類型是不相同的。
這裏有一個程序,要記得在c編譯器中編譯,這個程序能看出類型相同的重要性:
  1. <span style="color:#000000;">#include <stdio.h>  
  2.   
  3. int main()  
  4. {  
  5.     int a[5][5];  
  6.     int(*p)[4];  
  7.       
  8.     p = a;  
  9.       
  10.     printf("%d\n", &p[4][2] - &a[4][2]);  
  11. }</span>  
12.二級指針:
    a.因爲指針同樣存在傳值調用和傳址調用,並且還有指針數組這個東西的存在,所以二級指針還是有它的存在價值的。
    b.常使用二級指針的地方:
          (1)函數中想要改變指針指向的情況,其實也就是函數中指針的傳址調用,如:重置動態空間大小,代碼如下:
  1. #include <stdio.h>  
  2. #include <malloc.h>  
  3.   
  4. int reset(char**p, int size, int new_size)  
  5. {  
  6.     int ret = 1;  
  7.     int i = 0;  
  8.     int len = 0;  
  9.     char* pt = NULL;  
  10.     char* tmp = NULL;  
  11.     char* pp = *p;  
  12.       
  13.     if( (p != NULL) && (new_size > 0) )  
  14.     {  
  15.         pt = (char*)malloc(new_size);  
  16.           
  17.         tmp = pt;  
  18.           
  19.         len = (size < new_size) ? size : new_size;  
  20.           
  21.         for(i=0; i<len; i++)  
  22.         {  
  23.             *tmp++ = *pp++;        
  24.         }  
  25.           
  26.         free(*p);  
  27.         *p = pt;  
  28.     }  
  29.     else  
  30.     {  
  31.         ret = 0;  
  32.     }  
  33.       
  34.     return ret;  
  35. }  
  36.   
  37. int main()  
  38. {  
  39.     char* p = (char*)malloc(5);  
  40.       
  41.     printf("%0X\n", p);  
  42.       
  43.     if( reset(&p, 5, 3) )  
  44.     {  
  45.         printf("%0X\n", p);  
  46.     }  
  47.       
  48.     return 0;  
  49. }  

             (2)函數中傳遞指針數組的時候,實參(指針數組)要退化成形參(二級指針)。
             (3)定義一個指針指向指針數組的元素的時候,要使用二級指針。
      c.指針數組:char* p[4]={"afje","bab","ewrw"};  這是一個指針數組,數組中有4個char*型的指針,分別保存的是"afje"、"bab"、"ewrw"3個字符串的地址。p是數組首元素的地址即保存"afje"字符串char*指針的地址。
  1. #include <stdio.h>  
  2. #include <stdlib.h>  
  3. #include <stdbool.h>  
  4.   
  5. int main(int argc, char *argv[])   
  6. {     
  7.     char* p[4]={"afje","bab","ewrw"};  
  8.     char* *d=p;   
  9.     printf("%s\n",*(p+1));    
  10.     printf("%s\n",*(d+1));  //d  &p[0] p[0]是"afje"的地址,所以&p[0]是保存"afje"字符串的char*指針的地址      
  11.     return 0;  
  12. }  

       d.子函數malloc,主函數free,這是可以的(有兩種辦法,第一種是利用return 把malloc的地址返回。第二種是利用二級指針,傳遞一個指針的地址,然後把malloc的地址保存出來)。記住不管函數參數是,指針還是數組, 當改變了指針的指向的時候,就會出問題,因爲子函數中的指針就跟主函數的指針不一樣了,他只是一個複製品,但可以改變指針指向的內容。這個知識點可以看<在某培訓機構的聽課筆記>這篇文章。

13.數組作爲函數參數:數組作爲函數的實參的時候,往往會退化成數組元素類型的指針。如:int a[5],會退化成int*   ;指針數組會退化成二級指針;二維數組會退化成一維數組指針;三維數組會退化成二維數組指針(三維數組的這個是我猜得,如果說錯了,希望大家幫我指出來,謝謝)。如圖:

二維數組作爲實參的例子:

  1. #include <stdio.h>  
  2. #include <stdlib.h>  
  3. #include <stdbool.h>  
  4.   
  5. int fun(int (*b)[3])  //此時的b爲  &a[0]   
  6. {  
  7.     printf("%d\n",*(*(b+1)+0));  
  8.     printf("%d\n",b[2][2]);// b[2][2] 就是  (*(*(b+2)+2))  
  9.     printf("%d\n",*(b[1]+2));  
  10. }  
  11.   
  12. int main(int argc, char *argv[])   
  13. {  
  14.     int a[3][3]={1,2,3,4,5,6,7,8,9};  
  15.      fun(a);//與下句話等價  
  16.      fun(&a[0]);      
  17.     return 0;  
  18. }  

       數組當作實參的時候,會退化成指針。指針當做實參的時候,就是單純的拷貝了!

14.函數指針與指針函數:
      a.對於函數名來說,它是函數的入口,其實函數的入口就是一個地址,這個函數名也就是這個地址。這一點用彙編語言的思想很容易理解。下面一段代碼說明函數名其實就是一個地址,代碼如下:

  1. #include <stdio.h>  
  2. #include <stdlib.h>  
  3. #include <stdbool.h>  
  4.   
  5. void abc()  
  6. {  
  7.     printf("hello fun\n");  
  8. }  
  9. int main(int argc, char *argv[])   
  10. {  
  11.     void (*d)();  
  12.     void (*p)();  
  13.     p = abc;  
  14.     abc();  
  15.     printf("%p\n",abc);  
  16.     printf("%p\n",&abc);//函數abc的地址0x40138c  
  17.     p();  
  18.     (*p)();       
  19.     d = ((unsigned int*)0x40138c);  //其實就算d= 0x40138c這麼給賦值也沒問題   
  20.     d();  
  21.     return 0;  
  22. }     
  23.    

可見函數名就是一個地址,所以函數名abc與&abc沒有區別,所以p和*p也沒有區別。

    b.我覺得函數指針最重要的是它的應用環境,如回調函數(其實就是利用函數指針,把函數當作參數進行傳遞)代碼如下,還有中斷處理函數(同理)詳細見<

ok6410學習筆記(16.按鍵中斷控制led)>中的 中斷註冊函數,request_irq。還有就是函數指針數組,第一次見到函數指針數組是在zigbee協議棧中。

回調函數原理代碼:

  1. #include <stdio.h>  
  2.   
  3. typedef int(*FUNCTION)(int);  
  4.   
  5. int g(int n, FUNCTION f)  
  6. {  
  7.     int i = 0;  
  8.     int ret = 0;  
  9.       
  10.     for(i=1; i<=n; i++)  
  11.     {  
  12.         ret += i*f(i);  
  13.     }  
  14.       
  15.     return ret;  
  16. }  
  17.   
  18. int f1(int x)  
  19. {  
  20.     return x + 1;  
  21. }  
  22.   
  23. int f2(int x)  
  24. {  
  25.     return 2*x - 1;  
  26. }  
  27.   
  28. int f3(int x)  
  29. {  
  30.     return -x;  
  31. }  
  32.   
  33. int main()  
  34. {  
  35.     printf("x * f1(x): %d\n", g(3, f1));  
  36.     printf("x * f2(x): %d\n", g(3, &f2));  
  37.     printf("x * f3(x): %d\n", g(3, f3));  
  38. }  

注意:可以使用函數名f2,函數名取地址&f2都可以,但是不能有括號。

       c.所謂指針函數其實真的沒什麼好說的,就是一個返回值爲指針的函數而已。

15.賦值指針的閱讀:

       a.char* (*p[3])(char* d); 這是定義一個函數指針數組,一個數組,數組元素都是指針,這個指針是指向函數的,什麼樣的函數參數爲char*  返回值爲char*的函數。

分析過程:char (*p)[3] 這是一個數組指針、char* p[3] 這是一個指針數組  char* 是數組元素類型、char* p(char* d) 這個是一個函數返回值類型是char* 、char (*p)(char* d)這個是一個 函數指針。可見char* (*p[3])(char* d)是一個數組  數組中元素類型是 指向函數的指針,char* (* )(char* d) 這是函數指針類型,char* (* )(char* d) p[3] 函數指針數組 這個不好看 就放裏面了。(PS:這個看看就好了~~~當娛樂吧)

      b.函數指針數組的指針:char* (*(*pf)[3])(char* p) //這個就看看吧  我覺得意義也不大 因爲這個邏輯要是一直下去 就遞歸循環了。

分析過程:char* (* )(char *p) 函數指針類型,char* (*)(char *p) (*p)[3]  函數指針 數組指針  也不好看 就放裏面了。



本節知識點:

1.棧的知識(我覺得棧是本節很頭疼的一個問題):
    對於棧的問題,首先我們通過幾個不同的角度來看(因爲思維有些小亂所以我們通過分總的形式進行闡述):
    a.sp堆棧指針,相信學過51單片機,學過arm裸機的人都知道這個堆棧指針。我們現在從51單片機的角度來看這個堆棧指針寄存器。這個堆棧指針的目的是什麼?是用來保護現場(子函數的調用)和保護斷點(中斷的處理)的,所以在處理中斷前,調用子函數前,都應該把現場和返回地址壓入棧中。而且堆棧還會用於一些臨時數據的存放。51中的sp指針再單片機復位的時候初值爲0x07。常常我們會把這個sp指針指向0x30處,因爲0x30~0x7f是用戶RAM區(專門爲堆棧準備的存儲區)。然後要引入一個棧頂棧底的概念。棧操作的一段叫棧頂(這裏是sp指針移動的那個位置,sp也叫棧頂指針)sp指針被賦初值的那個地址叫棧底(這裏是0x30是棧底,因爲棧頂永遠會只在0x30棧底的一側進行移動,不會在兩層移動)。而且51單片機的sp是向上增長的,叫做向上增長型堆棧(棧頂指針sp向高地址處進行增長)。因爲PUSH壓棧操作,是sp指針先加1(指向的地址就增大一個),再壓入一個字節,POP彈出操作,先彈出一個字節,sp再減1(指向的地址就減少一個)。看PUSH和POP的過程,可見是一個滿堆棧(滿堆棧的介紹在後面)。小結一下:51的堆棧是一個向上增長型的滿堆棧
    b.對於arm來說,大量的分析過程都與上面相同。只是堆棧不再僅僅侷限於處理中斷了,而是處理異常。arm的堆棧有四種增長方式(具體見d)注意:在arm寫裸機的時候,那個ldr sp, =8*1024   其實是在初始化棧底。sp是棧頂指針,當沒有使用堆棧的時候,棧頂指針是指向棧底的。當數據來的時候,每次都是從棧頂進入的(因爲棧的操作入口在棧頂),然後sp棧頂指針指向棧頂,慢慢遠離棧底。說這些是想好好理解下什麼是棧頂,什麼是棧底。
    c.對於8086來說,它的棧的生長方向也是從高地址到低地址,每次棧操作都是以字(兩個字節)爲單位的。壓棧的時候,sp先減2,出棧的時候,sp再加2。可見8086的堆棧是一個向下增長型的滿堆棧
    d.總結下:
                    (1).當堆棧指針sp指向,最後一個壓入堆棧的數據的時候,叫滿堆棧。
                    (2).當堆棧指針sp指向,下一個要放入數據的空位置的時候,叫空堆棧。如下圖:

                  (3).當堆棧由低地址向高地址生長的時候,叫向上生長型堆棧即遞增堆棧。
                  (4).當堆棧由高地址向低地址生長的時候,叫向下生長型堆棧即遞減堆棧。如圖:

                 (5). 所以說arm堆棧支持四種增長方式:滿遞減棧(常用的ARM,Thumb c/c++編譯器都使用這個方式,也就是說如果你的程序中不是純彙編寫的,有c語言就得使用這種堆棧形式)、滿遞增棧、空遞減棧、空遞增棧。這四種方式分別有各自的壓棧指令,出棧指針,如下圖:  

           e.對於裸機驅動程序(51、ARM)沒有操作系統的,編譯器(keil、arm-linux-gcc等),會給sp指針寄存器一個地址。然後一切的函數調用,中斷處理,這些需要的現場保護啊,數據啊都壓入這個sp指向的棧空間。(arm的.s文件是自己寫的,sp是自己指定的,編譯器會根據這個sp寄存器的值進行壓棧和出棧,但是壓棧和出棧的規則是滿遞減棧的規則,因爲arm-linux-gcc是這個方式的,所以在彙編調用c函數的時候,彙編代碼必須使用滿遞減棧的那套壓棧出棧指令)。這種沒有操作系統的裸機驅動程序,只有一個棧空間,就是sp指針指向的那個棧空間。
           f.對於在操作系統上面的程序,裏面涉及內存管理、虛擬內存、編譯原理的問題。首先說不管是linux還是windows的進程的內存空間都是獨立的,linux是前3G,windows是4G,這都是虛擬內存的功勞。那編譯器給程序分配的棧空間,在程序運行時也是獨立的。每一個進程中的棧空間,應該都是在使用sp指針(但是在進程切換的過程中,sp指針是怎麼切換的我就不清楚了,這個應該去看看操作系統原理類的書)Ps:對於x86的32位機來說不再是sp和bp指針了,而是esp和ebp兩個指針。有人說程序中的棧是怎麼生長的,是由編譯器決定的,有人說是由操作系統決定的!!!我覺得都不對,應該是由硬件決定的,因爲cpu已經決定了sp指針的壓棧出棧方式。只要你操作系統在進程運行的過程中,使用的這個棧是sp棧指針指向的(即使用了sp指針),而不是自己定義的一塊內存(與sp指針無關的話)  Ps:實際中進程使用的是esp和ebp兩個指針,這裏僅僅用sp是想說明那個意思而已!  操作系統使用的棧空間就必須符合sp指針的壓棧和出棧方式,也就是遵循了cpu決定的棧的生長方式。編譯器要想編譯出能在這個操作系統平臺上使用的程序,也必須要遵守這個規則,所以來看這個棧的生長方式是由cpu決定的。這也是爲什麼我用那麼長的篇幅來解釋sp指針是怎麼工作的原因!
          g.要記住,由於操作系統有虛擬內存這個東東,所以不要再糾結編譯器分配的空間在操作系統中,進程執行的時候空間是怎麼用的了。編譯器分配的是什麼地址,進程中使用這個變量的虛擬地址就是什麼!是對應的。當然有的時候,編譯器也會耍些小聰明。不同編譯器對棧空間上的變量分配的地址可能不一樣,但方向一定是一樣的(因爲這個方向是cpu決定,編譯器是無權決定的,是sp指針壓棧的方向),如圖:

圖1和圖2的共同點是:都是從高地址處到低地址處,因爲sp指針把A、B、C變量壓入棧的方向就是從高到低地址的。這個是什麼編譯器都不會變的。
圖1和圖2的不同點是:圖2進行了編譯器的小聰明,它在給A,B,C開闢空間的時候,不是連續開闢的空間,有空閒(其實依然進行了壓棧操作只是壓入的是0或者是ff),這樣變量直接有間隙就避免了,數組越界,內存越界造成的問題。切記在獲取A、B、C變量的時候,不是通過sp指針,而是通過變量的地址獲得的啊,sp只負責把他們壓入棧中,即給他們分配內存
               h.說了那麼多棧的原理,現在我們說說棧在函數中究竟起到什麼作用:保存活動記錄!!!如圖:

注意:活動記錄是什麼上面的這個圖已經說的很清楚了,如果再調用函數,這個活動記錄會變成什麼樣呢?會在這個活動記錄後面繼續添加活動記錄(這個活動記錄是子函數的活動記錄),增加棧空間,當子函數結束後,子函數的活動記錄清除,棧空間繼續回到上圖狀態!
Ps:活動記錄如下:

         i.函數的調用行爲
函數的調用行爲中有一個很重要的東西,叫做調用約定。調用約定包含兩個約定。
第一個是:參數的傳遞順序(這個不是固定的,是在編譯器中約定好的),從左到右依次入棧:__stdcall、__cdecl、__thiscall   (這些指令,直接寫在函數名的前面就可以,但是跟編譯器有點關係,可能會有的編譯器不支持會報錯)
                                                                                                                       從右到左依次入棧:__pascal、__fastcall
第二個是:堆棧的清理(這段代碼也是編譯器自己添加上的):調用者清理
                                                                                                    被調用者函數返回後清理
注意:一般我們都在同一個編譯器下編譯不會出這個問題。 但是如果是調用動態鏈接庫,恰巧編譯動態鏈接庫的編譯器跟你的編譯器的默認約定不一樣,那就慘了!!!或者說如果動態鏈接庫的編寫語言跟你的語言都不一樣呢?                 
          j.這裏要聲明一個問題:就是棧的增長方向是固定的,是cpu決定的。但是不代表說你定義的局部變量也一定是先定義的在高地址,後定義的在低地址,局部變量之間都是連續的(這個在上面已經說過了是編譯器決定的),還有就是棧的增長方向也決定不了參數的傳遞順序(這個是調用約定,通過編譯器的手處理的)。下面讓我們探索下再dev c++中,局部變量的地址問題。
  1. #include <stdio.h>  
  2.   
  3. void fun()  
  4. {  
  5.     int a;  
  6.     int b;  
  7.     int c;  
  8.     printf("funa  %p\n",&a);  
  9.     printf("funb  %p\n",&b);  
  10.     printf("func  %p\n",&c);  
  11. }  
  12. void main()  
  13. {  
  14.     int a;  
  15.     int b;  
  16.     int c;  
  17.     int d;  
  18.     int e;  
  19.     int f;  
  20.     int p[100];  
  21.       
  22.     printf("a  %p\n",&a);  
  23.     printf("b  %p\n",&b);  
  24.     printf("c  %p\n",&c);  
  25.     printf("d  %p\n",&d);  
  26.     printf("e  %p\n",&e);  
  27.     printf("f  %p\n",&f);  
  28.     printf("p0    %p\n",&p[0]);  
  29.     printf("p1    %p\n",&p[1]);  
  30.     printf("p2    %p\n",&p[2]);  
  31.     printf("p3    %p\n",&p[3]);  
  32.     printf("p4    %p\n",&p[4]);  
  33.                       
  34.     printf("p10    %p\n",&p[10]);  
  35.     printf("p20    %p\n",&p[20]);  
  36.     printf("p30    %p\n",&p[30]);  
  37.     printf("p80    %p\n",&p[80]);  
  38.     printf("p90    %p\n",&p[90]);  
  39.     printf("p100    %p\n",&p[100]);  
  40.                   
  41.       
  42.     fun();  
  43.       
  44. }  
  1. #include <stdio.h>  
  2.   
  3. void fun()  
  4. {  
  5.     int a;  
  6.     int b;  
  7.     int c;  
  8.     printf("funa  %p\n",&a);  
  9.     printf("funb  %p\n",&b);  
  10.     printf("func  %p\n",&c);  
  11. }  
  12. void main()  
  13. {  
  14.     int a;  
  15.     int b;  
  16.     int c;  
  17.     int d;  
  18.     int e;  
  19.     int f;  
  20.     int p[100];  
  21.       
  22.     printf("a  %p\n",&a);  
  23.     printf("b  %p\n",&b);  
  24.     printf("c  %p\n",&c);  
  25.     printf("d  %p\n",&d);  
  26.     printf("e  %p\n",&e);  
  27.     printf("f  %p\n",&f);  
  28.     printf("p0    %p\n",&p[0]);  
  29.     printf("p1    %p\n",&p[1]);  
  30.     printf("p2    %p\n",&p[2]);  
  31.     printf("p3    %p\n",&p[3]);  
  32.     printf("p4    %p\n",&p[4]);  
  33.                       
  34.     printf("p10    %p\n",&p[10]);  
  35.     printf("p20    %p\n",&p[20]);  
  36.     printf("p30    %p\n",&p[30]);  
  37.     printf("p80    %p\n",&p[80]);  
  38.     printf("p90    %p\n",&p[90]);  
  39.     printf("p100    %p\n",&p[100]);  
  40.                   
  41.       
  42.     fun();  
  43.       
  44. }  
運行結果如下(不同編譯器的運行結果是不一樣的):

通過上面的運行結果,可以分析得出:在同一個函數中,先定義的變量在高地址處,後定義的變量在低地址處,且他們的地址是相連的中間沒有空隙定義的數組是下標大的在高地址處,下標小的在低地址處(由此可以推斷出malloc開闢出的推空間,也應該是下標大的在高地址處,下標小的在低地址處)子函數中的變量,跟父函數中的變量的地址之間有很大的一塊空間,這塊空間應該是兩個函數的其他活動記錄,且父函數中變量在高地址處,子函數中的變量在低地址處
             k.下面來一個棧空間數組越界的問題,讓大家理解一下,越界的危害,代碼如下(猜猜輸出結構):
  1. #include<stdio.h>  
  2. /*這是一個死循環*/  
  3. /*這裏面有數組越界的問題*/  
  4. /*有棧空間分配的問題*/  
  5. int main()  
  6. {  
  7.       
  8.     int i;  
  9. //  int c;  
  10.     int a[5];  
  11.     int c;  
  12.     printf("i %p,a[5] %p\n",&i,&a[5]); //觀察棧空間是怎麼分配的  這跟編譯器有關係的  
  13.     printf("c %p,a[0] %p\n",&c,&a[0]);  
  14.     for(i=0;i<=5;i++)  
  15.     {  
  16.         a[i]=-i;  
  17.         printf("%d,%d",a[i],i);  
  18.     }  
  19.     return 1;  
  20. }  
  1. #include<stdio.h>  
  2. /*這是一個死循環*/  
  3. /*這裏面有數組越界的問題*/  
  4. /*有棧空間分配的問題*/  
  5. int main()  
  6. {  
  7.       
  8.     int i;  
  9. //  int c;  
  10.     int a[5];  
  11.     int c;  
  12.     printf("i %p,a[5] %p\n",&i,&a[5]); //觀察棧空間是怎麼分配的  這跟編譯器有關係的  
  13.     printf("c %p,a[0] %p\n",&c,&a[0]);  
  14.     for(i=0;i<=5;i++)  
  15.     {  
  16.         a[i]=-i;  
  17.         printf("%d,%d",a[i],i);  
  18.     }  
  19.     return 1;  
  20. }  
注意:不同編譯器可能結果不一樣,比如說vs2008就不會死循環,那是因爲vs2008耍了我上面說的那個小聰明(就是局部變量和數組直接有間隙不是相連的,就避開了越界問題,但是如果越界多了也不行),建議在vc6和dev c++中編譯看結果。
            l.最後說說數據結構中的棧,其實數據結構中的棧就是一個線性表,且這個線性表只有一個入口和出口叫做棧頂,還是LIFO(後進先出的)結構而已。
            對棧的總結:之前就說過了那麼多種棧的細節,現在在宏觀的角度來看,其實棧就是一種線性的後進先出的結構,只是不同場合用處不同而已!
2.堆空間:堆空間彌補了棧空間在函數返回後,內存就不能使用的缺陷。是需要程序員自行跟操作系統申請的。
3.靜態存儲區:程序在編譯期,靜態存儲區的大小就確定了   
4.對於程序中的內存分佈:請看這篇文章<c語言中的內存佈局>
5.對於內存對齊的問題:請看這篇文章<C語言深度解剖讀書筆記(3.結構體中內存對齊問題)>
6.使用內存的好習慣:
    a.定義指針變量的時候,最好是初始化爲NULL,用完指針後,最好也賦值爲NULL。
    b.在函數中使用指針儘可能的,去檢測指針的有效性
    c.malloc分配的時候,注意判斷是否分配內存成功。
    d.malloc後記得free,防止內存泄漏!
    e.free(p)後應該p=NULL
    f.不要進行多次free
    g.不要使用free後的指針
    h.牢記數組的長度,防止數組越界
7.內存常見的六個問題:
    a.野指針問題 :一個指針沒有指向一個合法的地址
    b.爲指針分配的內存太小
    c.內存分配成功,但忘記初始化,memset的妙用
    e.內存越界
    f.內存泄漏
    g.內存已經被釋放 還仍然在使用(棧返回值問題)





 對於本節的函數內容其實就沒什麼難點了,但是對於函數這節又涉及到了順序點的問題,我覺得可以還是忽略吧。

本節知識點:

1.函數中的順序點:f(k,k++);  這樣的問題大多跟編譯器有關,不要去刻意追求。  這裏給下順序點的定義:順序點是執行過程中修改變量值的最後時刻。在程序到達順序點的時候,之前所做的一切操作都必須反應到後續的訪問中。

2.函數參數:函數的參數是存儲在這個函數的棧上面的(對於棧可以看上篇文章<內存管理的藝術>),是實參的拷貝。

3.函數的可變參數:

      a.對於可變參數要包含starg.h頭文件。需要va_list變量,va_start函數,va_arg函數,va_end函數。對於其他函數沒什麼可說的,只有va_arg函數記得一定是按順序的接收。這裏有一個可變參數使用的小例子,代碼如下:

  1. #include <stdio.h>  
  2. #include <stdarg.h>  
  3.   
  4. float average(char c,int n, ...)  
  5. {  
  6.     va_list args;  
  7.     int i = 0;  
  8.     float sum = 0;  
  9.       
  10.     va_start(args, n);  
  11.       
  12.     for(i=0; i<n; i++)  
  13.     {  
  14.         sum += va_arg(args, int);  
  15.     }  
  16.       
  17.     va_end(args);  
  18.     printf("%c\n",c);  
  19.       
  20.     return sum / n;  
  21. }  
  22.   
  23. int main()  
  24. {  
  25.     char c = 'b';  
  26.     printf("%f\n", average(c,5, 1, 2, 3, 4, 5));  
  27.     printf("%f\n", average(c,4, 1, 2, 3, 4));  
  28.       
  29.     return 0;  
  30. }  

        b.可變參數的缺點:

                 (1).必須要從頭到尾按照順序逐個訪問。

                 (2).參數列表中至少要存在一個確定的命名參數。

                 (3).可變參數宏無法判斷實際存在的參數的數量。

                 (4).可變參數宏無法判斷參數的實際類型。

                 (5).如果函數中想調用除了可變參數以外的參數,一定要放在可變參數前面。

注意:va_arg中如果指定了錯誤的類型,那麼結果是不可預期的。

Ps:可變參數就說到這裏,可變參數最經典的應用就是printf,等分析printf實現的時候,再好好寫寫。
4.函數與宏的比較:

注意:宏有一個函數不可取替的功能,宏的參數可以是類型名,這個是函數做不到的!代碼如下:

  1. #include <stdio.h>  
  2. #include <malloc.h>  
  3.   
  4. #define MALLOC(type, n) (type*)malloc(n * sizeof(type))  
  5.   
  6. int main()  
  7. {  
  8.     int* p = MALLOC(int, 5);  
  9.       
  10.     int i = 0;  
  11.       
  12.     for(i=0; i<5; i++)  
  13.     {  
  14.         p[i] = i + 1;  
  15.           
  16.         printf("%d\n", p[i]);  
  17.     }  
  18.       
  19.     free(p);  
  20.       
  21.     return 0;  
  22. }  

5.函數調用中的活動記錄問題:包含參數入棧、調用約定等問題。見上篇文章<內存管理的藝術>

6.遞歸函數:遞歸函數有兩個組成部分,一是遞歸點(以不同參數調用自身),另一個是出口(不再遞歸的終止條件)。

      對於遞歸函數要有一下幾點注意:

       a.一定要有一個清晰的出口,不然遞歸就無限了。

       b.儘量不要進行太多層次的遞歸,因爲遞歸是在不斷調用函數,要不斷的使用棧空間的,很容易造成棧空間溢出的,然後程序就會崩潰的。比如說:對一個已經排好序的結構進行快速排序(因爲快排需要使用遞歸,且對排好順序的結構排序是最壞情況,遞歸層數最多),就很容易造成棧空間溢出。一般不同的編譯器分配的棧空間大小是不一樣的,所以允許遞歸的層數也是不一樣的!

        c.利用遞歸函數,實現不利用參數的strlen函數。代碼如下:

  1. /*這是自己實現  strlen*/  
  2. /*  
  3. #include <stdio.h> 
  4. #include <stdlib.h> 
  5. #include <assert.h> 
  6.  
  7. int my_strlen(const char *str) 
  8. { 
  9.     int num=0; 
  10.     assert(NULL!=str); 
  11.     while(*str++) 
  12.     { 
  13.         num++; 
  14.     } 
  15.     return num; 
  16. } 
  17. int main(int argc, char *argv[]) 
  18. { 
  19.     char *a="hello world"; 
  20.     printf("%d\n",my_strlen(a)); 
  21.     return 0; 
  22. }*/  
  23.   
  24. /*這是不用變量 實現strlen  使用遞歸*/  
  25. #include <stdio.h>  
  26. #include <stdlib.h>  
  27. #include <assert.h>  
  28.   
  29. int my_strlen(const char *str)  
  30. {  
  31.     assert(NULL!=str);  
  32.     return ('\0'!=*str)?(1+my_strlen(str+1)):0; //這裏之所以 是加1 不是++ 我是擔心順序點的問題   
  33. }  
  34.   
  35. int main(int argc, char *argv[])  
  36. {  
  37.     char *a="hello world";  
  38.     printf("%d\n",my_strlen(a));  
  39.     return 0;  
  40. }  

7.使用函數時應該注意的好習慣:

   a.如果函數參數是指針,且僅作爲輸入參數用的時候,應該加上const防止指針在函數體內被以外改變,如:

  1. void str_copy(char *strDestination,const char *strSource);  

   b.在函數的入口處,應儘可能使用assert宏對指針進行有效性檢查,函數參數的有效性檢查是十分必要的。不用assert也行,if(NULL == p)也可以。

   c.函數不能返回指向棧內存的指針

   d.函數不僅僅要對輸入的參數,進行有效性的檢查 。還要對通過其他途徑進入函數體的數據進行有效性的檢查 ,如全局變量,文件句柄等。

   e.不要在函數中使用全局變量,儘量讓函數從意義上是一個獨立的模塊

   f.儘量避免編寫帶有記憶性的函數。函數的規模要小,控制在80行。函數的參數不要太多,控制在4個以內,過多就使用結構體。

   g.函數名與返回值類型在語言上不可以衝突,這裏有一個經典的例子getchar,getchar的返回值是int型,會隱藏這麼一個問題:

  1.  char c;  
  2.  c=getchar();  
  3.  if(XXX==c)  
  4.  {  
  5. /*code*/  
  6.  }  

      如果XXX的值不在char的範圍之內, 那c中存儲的就是XXX的低8位 ,if就永遠不會成立。但是getchar當然不會惹這個禍了,因爲getchar獲得的值是從鍵盤中的輸入的,是滿足ASCII碼的範圍的,ASCII碼是從0~127的,是在char的範圍裏面的,就算是用char去接getchar的值也不會有問題,getchar還是相對安全的。可是對於fgetc和fgetchar就沒這麼幸運了,他們的返回值類型同樣是int,如果你還用char去接收,那文件中的一些大於127的字符,就會造成越界了,然後導致你從文件中接收的數據錯誤。這裏面就有隱藏的危險了!!!對於字符越界問題可以看看這篇文章<c語言深度解剖讀書筆記(1.關鍵字的祕密)>
8.陳正衝老師還有一個第七章是講文件的我覺得總結不多,就寫在這裏了:

   a.每個頭文件和源文件的頭部 ,都應該包含文件的說明和修改記錄 。

   b.需要對外公開的常量放在頭文件中 ,不需要對外公開的常量放在定義文件的頭部。

9.最終的勝利,進軍c++(唐老師的最後一課,講了些c++的知識,總結如下):

    a.類與對象:

    b.c++中類有三種訪問權限:

           (1).public  類外部可以自由訪問

           (2).protected   類自身和子類中可以訪問

           (3).private     類自身中可以訪問

小例子:

  1. #include <stdio.h>  
  2.   
  3. struct Student  
  4. {  
  5. protected:  
  6.     const char* name;  
  7.     int number;  
  8. public:  
  9.     void set(const char* n, int i)  
  10.     {  
  11.         name = n;  
  12.         number = i;  
  13.     }  
  14.       
  15.     void info()  
  16.     {  
  17.         printf("Name = %s, Number = %d\n", name, number);  
  18.     }  
  19. };  
  20.   
  21. int main()  
  22. {  
  23.     Student s;  
  24.       
  25.     s.set("Delphi", 100);  
  26.     s.info();  
  27.       
  28.     return 0;  
  29. }  

注意:上面這段代碼要在c++的編譯器中進行編譯,在gcc中會報錯的,因爲c標準中是不允許struct中有函數的。
        c.繼承的使用,如圖:

小例子:

  1. #include <stdio.h>  
  2.   
  3. struct Student  
  4. {  
  5. protected:  
  6.     const char* name;  
  7.     int number;  
  8. public:  
  9.     void set(const char* n, int i)  
  10.     {  
  11.         name = n;  
  12.         number = i;  
  13.     }  
  14.       
  15.     void info()  
  16.     {  
  17.         printf("Name = %s, Number = %d\n", name, number);  
  18.     }  
  19. };  
  20.   
  21. class Master : public Student  
  22. {  
  23. protected:  
  24.     const char* domain;  
  25. public:  
  26.     void setDomain(const char* d)  
  27.     {  
  28.         domain = d;  
  29.     }  
  30.       
  31.     const char* getDomain()  
  32.     {  
  33.         return domain;  
  34.     }  
  35. };  
  36.   
  37. int main()  
  38. {  
  39.     Master s;  
  40.       
  41.     s.set("Delphi", 100);  
  42.     s.setDomain("Software");  
  43.     s.info();  
  44.       
  45.     printf("Domain = %s\n", s.getDomain());  
  46.       
  47.     return 0;  
  48. }  

Ps:以上6篇文章終於更新完了,是我對陳正衝老師的<c語言深度解剖>一書和國嵌唐老師c語言課程的一些總結和理解,針對c語言,後面的一點c++僅僅是做個筆記而已,望大牛莫噴~~~



本節知識點:

1.可以利用這個宏 #define OFFSET(type,number)  (int)(&(((type*)0)->number))  求出結構體中成員的偏移量
2.對於assert的使用是:
可以這樣
  1. assert(dst && src);   
也可以這樣
  1. assert((NULL != dst) && (NULL != src));   
上面兩種方式都行!
3.給一個考指針運算的面試題吧:
  1. #include <stdio.h>  
  2.   
  3. void main()  
  4. {  
  5.     int TestArray[5][5] = { {11,12,13,14,15},  
  6.                             {16,17,18,19,20},  
  7.                             {21,22,23,24,25},  
  8.                             {26,27,28,29,30},  
  9.                             {31,32,33,34,35}  
  10.                           };  
  11.     int* p1 = (int*)(&TestArray + 1);  
  12.     int* p2 = (int*)(*(TestArray + 1) + 6);  
  13.   
  14.     printf("Result: %d; %d; %d; %d; %d\n", *(*TestArray), *(*(TestArray + 1)),   
  15.                                            *(*(TestArray + 3) + 3), p1[-8],   
  16.                                            p2[4]);  
  17. }  
自己算算吧,記住一個前提就好,就是在對指針進行運算的時候一定要先弄清這個指針的類型!
4.看看下面的代碼,感受下安全編程的重要性:
  1. #include<stdio.h>   
  2.   
  3. int main(int argc, char *argv[])   
  4. {   
  5.     int flag = 0;   
  6.   
  7.     char passwd[10];   
  8.   
  9.     memset(passwd,0,sizeof(passwd));   
  10.   
  11.     strcpy(passwd, argv[1]);   
  12.   
  13.     if(0 == strcmp("LinuxGeek", passwd))   
  14.     {   
  15.         flag = 1;   
  16.     }   
  17.   
  18.     if( flag )   
  19.     {   
  20.         printf("\n Password cracked \n");   
  21.   
  22.     }   
  23.     else   
  24.     {   
  25.         printf("\n Incorrect passwd \n");   
  26.     }   
  27.   
  28.     return 0;   
  29.   
  30. }  
看看上面的代碼有沒有什麼問題?如果把命令行輸入的文字當作密碼的話,會不會存在問題?答案是會,因爲這裏面有兩個知識點:1.是數組越界   2.是strcpy安全性的問題。
首先如果我輸入11個字符且最後一個字符是大於0的話,就慘了,strcpy是要copy到'/0'的。他會一直把這11個字符都copy到passwd數組中,此時數組越界了,最後一個字符就把flag標誌位個賦值了,if條件就滿足了,密碼就被破解了!
所以應該使用安全性更高的strncpy:
  1. strncpy(passwd,argv[1],9);  


最近對c語言的總結學習可以告一段落了!覺得這種邊學邊思考邊總結的方式,還不錯,還是有一定的進步的!但是對於日後的c語言學習還遠遠沒有停止。所以寫了這篇文章來督促自己對c語言的學習,告訴自己還有很多不錯的書沒有去讀。過一段時間,再回頭看看。

        1.對於c語言描述的數據結構的學習。

        2.林銳老師的<高質量程序設計指南>,聽說他的<大學十年>也很不錯,有時間應該讀一讀。

        3.<c和指針>     <c陷阱與缺陷>    <c專家編程>    <c++沉思錄>

        4.仔細研讀 <c primer plus>這本書,這本書中有很多細節,很多標準(c99標準)值得學習,應該好好看看!

        5.還有就是找一本介紹c 表庫函數的書(像字典一樣),看看c庫函數都有什麼,c庫中有多少頭文件等。

        6.還有就是一些當作補充的書籍:<C語言的科學和藝術>    <你必須知道的495個c語言問題>    <c語言進階:重點、難點與疑點解析>  <攻破C語言筆試與機試難點V0.3>

        7.最後在回頭看看,帶我最初接觸c語言的,譚浩強的<c語言程序設計>

發佈了27 篇原創文章 · 獲贊 19 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章