C基本知識點拾遺

最近需要學習C語言,找了些C容易犯錯的知識點和重點,以提示自己。

總體上必須清楚的:

 1)程序結構是三種:  順序結構選擇結構(分支結構)、循環結構

 2)讀程序都要從main()入口, 然後從最上面順序往下讀(碰到循環做循環,碰到選擇做選擇),有且只有一個main函數。

 3)計算機的數據在電腦中保存是以二進制的形式. 數據存放的位置就是 他的地址.

 4)bit 是指爲0 或者1 byte 是指字節, 一個字節 = 八個位.

概念常考到的:

1、編譯預處理不是C語言的一部分,不佔運行時間,不要加分號。C語言編譯的程序稱爲源程序,它以ASCII數值存放在文本文件中。

2、#define PI 3.1415926; 這個寫法是錯誤的,一定不能出現分號。 

3、每個C語言程序中main函數有且只有一個

4、在函數中不可以再定義函數。

5、算法:可以沒有輸入,但是一定要有輸出

6、break可用於循環結構和switch語句。

7、逗號運算符的級別最低賦值的級別倒數第二

第一章C語言的基礎知識

第一節、對C語言的基礎認識

1、C語言編寫的程序稱爲源程序又稱爲編譯單位

2、C語言書寫格式是自由的,每行可以寫多個語句,可以寫多行。

3、一個C語言程序有且只有一個main函數,是程序運行的起點

第二節、熟悉vc++

1、VC是軟件,用來運行寫的C語言程序。

2、每個C語言程序寫完後,都是先編譯,後鏈接,最後運行。(.c---à.obj---à.exe)這個過程中注意.c和.obj文件時無法運行的,只有.exe文件纔可以運行。(常考!)

第三節、標識符

1、標識符(必考內容):

合法的要求是由字母數字下劃線組成。有其它元素就錯了。

並且第一個必須爲字母或則是下劃線。第一個爲數字就錯了

2、標識符分爲關鍵字、預定義標識符、用戶標識符。

關鍵字:不可以作爲用戶標識符號main define  scanf  printf 都不是關鍵字。迷惑你的地方If是可以做爲用戶標識符。因爲If中的第一個字母大寫了,所以不是關鍵字。

預定義標識符:背誦define scanf  printf  include。記住預定義標識符可以做爲用戶標識符。

用戶標識符:基本上每年都考,詳細請見書上習題。

第四節:進制的轉換

十進制轉換成二進制、八進制、十六進制。

    二進制、八進制、十六進制轉換成十進制。

第五節:整數與實數

1)C語言只有八、十、十六進制,沒有二進制。但是運行時候,所有的進制都要轉換成二進制來進行處理。(考過兩次)

    a、C語言中的八進制規定要以0開頭。018的數值是非法的,八進制是沒有8的,逢8進1。 

    b、C語言中的十六進制規定要以0x開頭。

2)小數的合法寫法C語言小數點兩邊有一個是零的話可以不用寫

1.0在C語言中可寫成1.

0.1在C語言中可以寫成.1。

3)實型數據的合法形式:

a、2.333e-1 就是合法的,且數據是2.333×10-1

b、考試口訣:ee後必有數,e必爲整數請結合書上的例子。

4) 整型一般是4個字節, 字符型1個字節,雙精度一般是8個字節:

  long int x; 表示x是長整型。

    unsigned int x; 表示x是無符號整型。

第六、七節:算術表達式和賦值表達式

核心:表達式一定有數值

1、算術表達式:+,-,*,/,%

   考試一定要注意:“/” 兩邊都是整型的話,結果就是一個整型。 3/2的結果就是1.

                  “/” 如果有一邊是小數,那麼結果就是小數。 3/2.0的結果就是0.5

                  “%”符號請一定要注意是餘數,考試最容易算成了除號。)%符號兩邊要求是整數。不是整數就錯了。[注意!!!]

2、賦值表達式:表達式數值是最左邊的數值,a=b=5;該表達式爲5,常量不可以賦值

1、int x=y=10: 錯啦,定義時不可以連續賦值。

2、int x,y;

x=y=10;   對滴,定義完成後可以連續賦值。

3、賦值的左邊只能是一個變量

4、int x=7.7;對滴,x就是7

5、float y=7;對滴,x就是7.0

3、複合的賦值表達式:

   int a=2;

   a*=2+3;運行完成後,a的值是12。

一定要注意,首先要在2+3的上面打上括號。變成(2+3)再運算。

4、自加表達式:

自加、自減表達式:假設a=5,++a(是爲6), a++(爲5);

運行的機理:++a 是先把變量的數值加上1,然後把得到的數值放到變量a中,然後再用這個++a表達式的數值爲6,而a++是先用該表達式的數值爲5,然後再把a的數值加上1爲6,

再放到變量a中。 進行了++a和a++後在下面的程序中再用到a的話都是變量a中的6了。

  考試口訣:++在前先加後用++在後先用後加

5、逗號表達式:

優先級別最低。表達式的數值逗號最右邊的那個表達式的數值。

(2,3,4)的表達式的數值就是4。

 z=(2,3,4)(整個是賦值表達式) 這個時候z的值爲4。(有點難度哦!)

  z=  234 (整個是逗號表達式)這個時候z的值爲2。

補充:

1、空語句不可以隨意執行,會導致邏輯錯誤

2、註釋是最近幾年考試的重點,註釋不是C語言,不佔運行時間,沒有分號。不可以嵌套!

3、強制類型轉換

  一定是 (int)a 不是  int(a),注意類型上一定有括號的。

   注意(int)(a+b) 和(int)a+b 的區別。前是把a+b轉型,後是把a轉型再加b。

4、三種取整丟小數的情況

    1、int a =1.6;

              2、(int)a;  

              3、1/2; 3/2;

第八節、字符

1)字符數據的合法形式::

   '1'字符個字節,"1"是字符串佔個字節(含有一個結束符號)。

  '0' 的ASCII數值表示爲48,'a' 的ASCII數值是97,'A'的ASCII數值是65。

一般考試表示單個字符錯誤的形式:'65'    "1"  

字符是可以進行算術運算的,記住: '0'-0=48

大寫字母和小寫字母轉換的方法: 'A'+32='a'  相互之間一般是相差32。

2)轉義字符:

轉義字符分爲一般轉義字符、八進制轉義字符、十六進制轉義字符

一般轉義字符:背誦\0、 \n、 \’、 \”、 \\。

八進制轉義字符:  ‘\141’ 是合法的, 前導的0是不能寫的。

十六進制轉義字符:’\x6d’ 纔是合法的,前導的0不能寫,並且x是小寫

3、字符型和整數是近親:兩個具有很大的相似之處

          char a = 65 ; 

            printf(“%c”,a);  得到的輸出結果:a

printf(“%d”, a); 得到的輸出結果:65                

第九章、位運算 

1)位運算的考查:會有一到二題考試題目。

總的處理方法:幾乎所有的位運算的題目都要按這個流程來處理(先把十進制變成二進制再變成十進制)。

例1: char a = 6,b;

     b = a<<2;  這種題目的計算是先要把a的十進制6化成二進制,再做位運算。

例2: 一定要記住,異或的位運算符號” ^ ”。0 異或 1得到1。

                                         0 異或 0得到0。兩個女的生不出來

考試記憶方法:一男(1)一女(0)纔可以生個小孩(1)。

例3: 在沒有捨去數據的時候,<<左移一位表示乘以2;>>右移一位表示除以2。   

第二章

第一節:數據輸出(一)(二)

1、使用printf和scanf函數時,要在最前面加上#include“stdio.h”

2、printf可以只有一個參數,也可以有兩個參數。(選擇題考過一次)

3、printf(“ 第一部分 ”,第二部分  );把第二部分的變量、表達式、常量以第一部分的形式展現出來!

4、printf(“a=%d,b=%d”,12, 34) 考試重點!

一定要記住是將12和34以第一部分的形式現在在終端也就是黑色的屏幕上。考試核心爲一模一樣在黑色屏幕上面顯示爲  a=12,b=34          

  printf(“a=%d,\n b=%d”,12, 34)那麼輸出的結果就是:a=12,

b=34

   5、int x=017;   一定要弄清楚爲什麼是這個結果!過程很重要

      printf(“%d”, x); 15

printf(“%o”, x); 17

printf(“%#o”,x); 017

printf(“%x”, x); 11 

printf(“%#x”,x); 0x11

   6、int x=12,y=34; 注意這種題型

      char z=‘a’;    

      printf(“%d ”,x,y); 一個格式說明,兩個輸出變量,後面的y不輸出

      printf(“%c”,z);      結果爲:12a

   7、一定要背誦的

格式說明

表示內容

格式說明

表示內容

%d

整型   int

%c

字符  char

%ld

長整型 long int

%s

字符串

%f

浮點型 float

%o

八進制

%lf

double

%#o

帶前導的八進制

%%

輸出一個百分號

%x

十六進制

%5d

 

%#x

帶前導的十六進制

舉例說明:

printf(“%2d”,123 );  第二部分有三位,大於指定的兩位,原樣輸出123

printf(“%5d”,123 );  第二部分有三位,小於指定的五位,左邊補兩個空格  123

printf(“%10f”,1.25 ); 小數要求補足6位的,沒有六位的補0,。結果爲 1.250000

printf(“%5.3f”,125 ); 小數三位,整個五位,結果爲1.250(小數點算一位)

printf(“%3.1f”,1.25 );小數一位,整個三位,結果爲1.3(要進行四捨五入)

第三節 數據輸入

1、scanf(“a=%d,b=%d”,&a,&b) 考試超級重點!

一定要記住是以第一部分的格式在終端輸入數據。考試核心爲:一模一樣

在黑色屏幕上面輸入的爲  a=12,b=34纔可以把12和34正確給a和b 。有一點不同也不行。        

2、scanf(“%d,%d”,x,y);這種寫法絕對錯誤,scanf的第二個部分一定要是地址!

scanf(“%d,%d”,&x,&y);注意寫成這樣纔可以!

3、特別注意指針在scanf的考察

例如: int x=2;int *p=&x;

scanf(“%d”,x);   錯誤          scanf(“%d”,p);正確

scanf(“%d”,&p);  錯誤         scanf(“%d”,*p)錯誤

4、指定輸入的長度 (考試重點)

終端輸入:1234567

scanf(“%2d%4d%d”,&x,&y,&z);x爲12,y爲3456,z爲7

終端輸入:1 234567     由於1和2中間有空格,所以只有1位給x

scanf(“%2d%4d%d”,&x,&y,&z);x爲1,y爲2345,z爲67

5、字符和整型是近親:

intx=97;

printf(“%d”,x);   結果爲97

printf(“%c”,x);   結果爲 a

6、輸入時候字符和整數的區別(考試超級重點

scanf(“%d”,&x);這個時候輸入1,特別注意表示的是整數1

scanf(“%c”,&x);這個時候輸入1,特別注意表示的是字符‘1’ASCII爲整數48。

補充說明:

1)scanf函數的格式考察:

   注意該函數的第二個部分是&a 這樣的地址,不是a; 

   scanf(“%d%d%*d%d”,&a,&b,&c); 跳過輸入的第三個數據。

2)putchar ,getchar 函數的考查:

   char a = getchar() 是沒有參數的,從鍵盤得到你輸入的一個字符給變量a。

   putchar(‘y’)把字符y輸出到屏幕中。

3)如何實現兩個變量x ,y中數值的互換(要求背下來)

   不可以把 x=y ,y=x; 要用中間變量 t=x;x=y;y=t

4)如何實現保留三位小數,第四位四捨五入的程序,(要求背下來)

       y=(int)(x*100+0.5)/100.0   這個保留兩位,對第三位四捨五入

       y=(int)(x*1000+0.5)/1000.0 這個保留三位,對第四位四捨五入

y=(int)(x*10000+0.5)/10000.0 這個保留四位,對第五位四捨五入

   這個有推廣的意義,注意 x = (int)x 這樣是把小數部分去掉。

                                 第三章

特別要注意:C語言中是用非0表示邏輯真的,用0表示邏輯假的。

            C語言構造類型沒有邏輯類型

            關係運算符號:注意<=的寫法,==和=的區別!(考試重點)

            if只管後面一個語句要管多個,請用大括號

1)關係表達式:

   a、表達式的數值只能爲1(表示爲真),或0(表示假)。

如 9>8這個關係表達式是真的,所以9>8這個表達式的數值就是1。

如 7<6這個關係表達式是假的,所以7<6這個表達式的數值就是0

   b、考試最容易錯的:就是int x=1,y=0,z=2;

                          x<y<z是真還是假?帶入爲1<0<2,從數學的角度出發肯定是

錯的,但是如果是C語言那麼就是正確的!因爲要1<0爲假得到0,表達式就變成

了0<2那麼運算結果就是1,稱爲了真的了!

   c、等號和賦值的區別!一定記住“=”就是賦值,“= =”纔是等號。雖然很多人可以背

誦,但我依然要大家一定好好記住,否則,做錯了,我一定會強烈的鄙視你!

2)邏輯表達式:

   核心:表達式的數值只能爲1(表示爲真),或0(表示假)。

a)  共有&&   ||   ! 三種邏輯運算符號。

b) !>&&>||  優先的級別。

c)  注意短路現象。考試比較喜歡考到。詳細請見書上例子,一定要會做例1和例2

d)  表示 x 小於0大於10的方法。

0<x<10是不行的(一定記住)。是先計算0<x 得到的結果爲1或則0;再用0,或1與10比較得到的總是真(爲1)。所以一定要用 (0<x)&&(x<10)表示比0大比10

3)if 語句

  a、else 是與最接近的if且沒有else的語句匹配。

  b、交換的程序寫法:t=x;x=y;y=t;

  c、if(a<b)t=a;a=b;b=t;

     if(a<b){t=a;a=b;b=t;}兩個的區別,考試多次考到了!

  d、單獨的if語句:if(a<b)t=a;

     標準的if語句:if(a<b)min=a;

else  min=b;

     嵌套的if語句:if(a<b)

if(b>c)printf(“ok!”);

     多選一的if語句if(a= =t)printf(“a”);

                   else if(b= =t)printf(“b”);

                   else if(c= =t)printf(“c”);

                   else pritnf(“d”);

     通過習題,要熟悉以上幾種if語句!

經典考題:結合上面四種if語句題型做題,答錯了,請自行了斷!預備,開始!

int  a=1,b=0;

if(!a)b++;

else if(a= =0)

if(a)b+=2;

else b+=3;請問b的值是多少?

如果沒有看懂題目,你千萬不要自行了斷,這樣看得懂不會做的人纔會有理由的活着。

 正確的是b爲3。

int  a=1,b=0;

if(!a)b++;    是假的不執行

elseif(a= =0)    是假的執行

if(a)b+=2; 屬於else if的嵌套if語句,不執行。

elseb+=3;        if-else-if語句沒有一個正確的,就執行else的語句!

4)條件表達式:

     表達式1 ?表達式2 :表達式3

a、考試口訣:真前假後

   b、注意是當表達式1的數值是0時,才採用表達式2的數值做爲整個運算結果,當表達式1的數值0時,就用表達式3的數值做爲整個的結果。

   c、int a=1,b=2,c=3,d=4,e=5;

k=a>b?c:d>e?d:e;求k的數值時多少?  答案爲san

5)switch語句:

a) 執行的流程一定要弄懂!上課時候詳細的過程講了,請自己一定弄懂!

b)注意有break 和沒有break的差別,書上的兩個例子,沒有break時候,只要有一個case匹配了,剩下的都要執行,有break則是直接跳出了swiche語句。break在C語言中就是分手,一刀兩斷的意思。

c) switch只可以和break一起用,不可以和continue用。

d) switch(x)   x:是整型常量,字符型常量,枚舉型數據。

{case 1: ….     不可以是變量。

case 2: ….

}

    e)switch是必考題型,請大家一定要完成書上的課後的switch的習題。

                                       第四章

1)三種循環結構:

   a)for() ; while();  do- while()三種。

   b)for循環當中必須兩個分號,千萬不要忘記。

   c)寫程序的時候一定要注意,循環一定要有結束的條件,否則成了死循環。

   d) do-while()循環的最後一個while();的分號一定不能夠丟。(當心上機改錯),do-while循環是至少執行一次循環。

2) break 和  continue的差別

   記憶方法:

break:是打破的意思,(破了整個循環)所以看見break就退出整個一層循環。

continue: 是繼續的意思,(繼續循環運算),但是結束本次循環,就是循環體內剩下的語句不再執行,跳到循環開始,然後判斷循環條件,進行新一輪的循環

3)嵌套循環

   就是有循環裏面還有循環,這種比較複雜,要一層一層一步一步耐心的計算,一般記住兩層是處理二維數組的。

4)while((c=getchar())!=’\n’) 和

while(c=getchar() !=’\n’)的差別

先看a = 3 != 2  和 (a=3)!=2 的區別:

(!=號的級別高於=號 所以第一個先計算 3!=2) 第一個a的數值是得到的1;第二個a的數值是3。

考試注意點: 括號在這裏的重要性。

5)每行輸出五個的寫法:

for(i=0;i<=100;i++)

{printf(“%d”,i);

 if((i+1)%5==0)printf(“\n”); 如果i是從1開始的話,就是if(i%5==0)printf(“\n”);

}

6)如何整除一個數:i%5==0表示整除5

                   I%2==0表示整除2,同時表示是偶數!

7)輸入123,輸出321逆序輸出數據

int i=123;       

while(i!=0)

{

  printf(“%d”,i%10

i=i/10;}

8)for只管後面一個語句

inti=3;

for(i=3;i<6;i++):

printf(“#”):                請問最終打印幾個#號?答案爲一個!

9)不停的輸入,直到輸入# 停止輸入!      不停的輸入,直到輸入$停止輸入!

    while( (x=getchar())!=’ # ’ )                 while( (x=getchar())!=’$ ’ )

不停的輸入,直到遇到?停止輸入!

while((x=getchar())!=’ ? ’)   解說:一定要注意這種給出了條件,然後如何去寫的方法!  

10)for循環和switch語句的和在一起考題!   

11)多次出現的考題

intk=1                       int k=1;

while(- -k);                while(k--);

printf(“%d”,k);           printf(“%d”,k);    

結果爲0                     結果爲-1

第五章

1、函數:是具有一定功能的一個程序塊,是C語言的基本組成單位。

2、函數不可以嵌套定義。但是可以嵌套調用。

3、函數名缺省返回值類型,默認爲 int。

4、C語言由函數組成,但有且僅有一個main函數!是程序運行的開始!

5、如何判斷a是否爲質數:背誦這個程序!

void  iszhishu( int a )

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

    if(a%i==0) printf(“不是質數”);

   printf(“是質數!”);

}

6、如何求階層n! 背誦這個程序!

   int fun(int n)

{  int p=1;

for(i=1;i<=n;i++) p=p*i;

return p;

}

7、函數的參數可以是常量,變量,表達式,甚至是函數調用

  add(int x,int y){returnx+y;}

  main()

{ int sum;

 sum=add(add(7,8),9);請問sum的結果是多少? 結果爲24

}

8、 函數的參數,返回數值(示意圖):

9、一定要注意參數之間的傳遞

   實參和形參之間 傳數值,和傳地址的差別。(考試的重點)

      傳數值的話,形參的變化不會改變實參的變化。

      傳地址的話,形參的變化就會有可能改變實參的變化。

10、函數聲明的考查:

一定要有:函數名,函數的返回類型,函數的參數類型。不一定要有:形參的名稱

填空題也可能會考到!以下是終極難度的考題。打橫線是函數聲明怎麼寫!

int*fun(int a[] , int b[])             

{

…………..

}已經知道函數是這樣。這個函數的正確的函數聲明怎麼寫?

int *fun(int *a , int *b)               這裏是函數聲明的寫法,注意數組就是指針

int *fun(int a[] , int b[])              這種寫法也是正確的                      

int *fun(int b[] , int c[])              這種寫法也是正確的,參數的名稱可以隨便寫

int *fun(int * , int *)                這種寫法也是正確的,參數的名稱可以不寫 

11、要求掌握的庫函數:

    a、庫函數是已經寫好了函數,放在倉庫中,我們只需要如何去使用就可以了!   

b、以下這些庫函數經常考到,所以要背誦下來。

abs()、 sqrt()、fabs()、pow()、sin()  其中pow(a,b)是重點。23是由pow(2,3)表示的。

 

第六章

指針變量的本質是用來放地址,而一般的變量是放數值的。

1、int *p 中   *p和p的差別:簡單說*p是數值,p是地址!

*p可以當做變量來用;*的作用是取後面地址p裏面的數值

 p是當作地址來使用。可以用在scanf函數中:scanf(“%d”,p);

 

2、*p++ 和 (*p)++的之間的差別:改錯題目中很重要!考試超級重點

         *p++是 地址會變化。      口訣:取當前值,然後再移動地址!

         (*p)++ 是數值會要變化。 口訣:取當前值,然後再使數值增加1。   

例題:int *p,a[]={1,3,5,7,9};

      p=a;

      請問*p++和(*p)++的數值分別爲多少?

      *p++:  這個本身的數值爲1。由於是地址會增加一,所以指針指向數值3了。 

(*p)++ 這個本身的數值爲1。由於有個++表示數值會增加,指針不移動,但數值1由於自加了一次變成了2。      

3、二級指針:

  *p:一級指針:存放變量的地址。

  **q:二級指針:存放一級指針的地址。

  常考題目:   int x=7;

               int*p=&x,**q=p;

               問你:*p爲多少?*q爲多少?**q爲多少?

                       7          p         7

               再問你:**q=&x的寫法可以嗎?  

                      不可以,因爲二級指針只能存放一級指針的地址。

4、三名主義:(考試的重點)

   數組名:表示第一個元素的地址。數組名不可以自加,他是地址常量名。(考了很多次)

   函數名:表示該函數的入口地址。

   字符串常量名:表示第一個字符的地址。

5、移動指針(經常加入到考試中其他題目綜合考試)

  char *s=“meikanshu”  

while(*s){printf(“%c”,*s);s++;}

這個s首先會指向第一個字母m然後通過循環會一次打印出一個字符,s++是地址移動,打印了一個字母后,就會移動到下一個字母!

6指針變量兩種初始化(一定要看懂)

方法一:int a=2,*p=&a;(定義的同時初始化)

方法二:int a=2,*p;  (定義之後初始化)

    p=&a;

7傳數值和傳地址(每年必考好多題目)

void fun(int a,intb)                    void fun(int *a,int *b)            

{ int t ;                                       { int t ;

  t=a;a=b;b=t;                                  t=*a;*a=*b;*b=t;

}                                               }

main()                                     main()

{ int x=1,y=3,                             { int x=1,y=3,

  fun(x,y);                                 fun(&x,&y)

  printf(“%d,%d”,x,y);                    printf(“%d,%d”,x,y);

}                                             }

這個題目答案是1和3。                        這個題目的答案就是3和1。

傳數值,fun是用變量接受,所以fun中     傳地址,fun用指針接受!這個時候fun

的交換不會影響到main中的x和y 。        中的交換,就會影響到main中的x和y。

傳數值,形參的變化不會影響實參。          傳地址形參的變化絕大多數會影響到實參!

 

8、函數返回值是地址,一定注意這個*號(上機考試重點)  

int *fun(int*a,int *b)   可以發現函數前面有個*,這個就說明函數運算結果是地址                            

{ if(*a>*b)returna;     return a 可以知道返回的是a地址。

  else return b;                                                                        

}

main()

{ int x=7,y=8,*max;

  max = fun(&x,&y);      由於fun(&x,&y)的運算結果是地址,所以用max來接收。

  printf(“%d,%d”,)   

}                                             

9、考試重要的話語:

指針變量是存放地址的。並且指向哪個就等價哪個,所有出現*p的地方都可以用它等價的代替。例如:int a=2,*p=&a;

    *p=*p+2;

(由於*p指向變量a,所以指向哪個就等價哪個,這裏*p等價於a,可以相當於是a=a+2) 

 

第七章

數組: 存放的類型是一致的。多個數組元素的地址是連續的。

1、一維數組的初始化:

inta[5]={1,2,3,4,5};  合法

int a[5]={1,2,3,};    合法

inta[]={1,2,3,4,5};   合法,常考,後面決定前面的大小!

inta[5]={1,2,3,4,5,6};不合法,賦值的個數多餘數組的個數了

2、一維數組的定義;

int a[5];注意這個地方有一個重要考點,定義時數組的個數不是變量一定是常量。

int a[5]                 合法,最正常的數組

int a[1+1]               合法,個數是常量2,是個算術表達式

int a[1/2+4]             合法,同樣是算術表達式

int x=5,int a[x];           不合法,因爲個數是x,是個變量,非法的,

define P 5  int a[P]       合法,define 後的的P是符號常量,只是長得像變量

3、二維數組的初始化

inta[2][3]={1,2,3,4,5,6};               合法,很標準的二維的賦值。

inta[2][3]={1,2,3,4,5, };                合法,後面一個默認爲0

inta[2][3]={{1,2,3,} {4,5,6}};           合法,每行三個。

inta[2][3]={{1,2,}{3,4,5}};             合法,第一行最後一個默認爲0

inta[2][3]={1,2,3,4,5,6,7};              不合法,賦值的個數多餘數組的個數了。

inta[][3]={1,2,3,4,5,6};                不合法,不可以缺省行的個數。

inta[2][]={1,2,3,4,5,6};                合法,可以缺省列的個數

補充:

1)一維數組的重要概念:

對a[10]這個數組的討論。

1、a表示數組名,是第一個元素的地址,也就是元素a[0]的地址。(等價於&a

2、a是地址常量,所以只要出現a++,或者是a=a+2賦值的都是錯誤的。

3、a是一維數組名,所以它是列指針,也就是說a+1跳一列。 

對a[3][3]的討論。

1、a表示數組名,是第一個元素的地址,也就是元素a[0][0]的地址。

2、a是地址常量,所以只要出現a++,或者是a=a+2賦值的都是錯誤的。

3、a是二維數組名,所以它是行指針,也就是說a+1跳一行

4、a[0]、a[1]、a[2]也都是地址常量,不可以對它進行賦值操作,同時它們都是列指針,a[0]+1,a[1]+1,a[2]+1都是跳一列。

5、注意a和a[0] 、a[1]、a[2]是不同的,它們的基類型是不同的。前者是一行元素,後三者是一列元素。

2) 二維數組做題目的技巧:

如果有a[3][3]={1,2,3,4,5,6,7,8,9}這樣的題目。

步驟一:把他們寫成:      第一列 第二列 第三列  

a[0]à  1    2    3   ->第一行

a[1]à   4     5    6  —>第二行

a[2]à   7     8    9  ->第三行

步驟二:這樣作題目間很簡單:    

*(a[0]+1)我們就知道是第一行的第一個元素往後面跳一列,那麼這裏就是a[0][1]元素,所以是1。

*(a[1]+2)我們就知道是第二行的第一個元素往後面跳二列。那麼這裏就是a[1][2]元素,所以是6。

一定記住:只要是二維數組的題目,一定是寫成如上的格式,再去做題目,這樣會比較簡單。

3) 數組的初始化,一維和二維的,一維可以不寫,二維第二個一定要寫

     int a[]={1,2} 合法。  int a[][4]={2,3,4}合法。   但inta[4][]={2,3,4}非法。

4) 二維數組中的行指針

 int a[1][2]; 

其中a現在就是一個行指針,a+1跳一行數組元素。  搭配(*)p[2]指針

     a[0],a[1]現在就是一個列指針。a[0]+1 跳一個數組元素。搭配*p[2]指針數組使用

5) 還有記住脫衣服法則:超級無敵重要

   a[2]  變成   *(a+2)  a[2][3]變成 *(a+2)[3]再可以變成  *(*(a+2)+3)

這個思想很重要!

 

 

其它重點

文件的複習方法

把上課時候講的文件這一章的題目要做一遍,一定要做,基本上考試的都會在練習當中。

1)字符串的 strlen() 和 strcat() 和strcmp() 和strcpy()的使用方法一定要記住。他們的參數都是地址。其中strcat()和strcmp()有兩個參數。

 

2)strlen 和 sizeof的區別也是考試的重點;

 

3)define  f(x)(x*x)  和  define   f(x) x*x 之間的差別。一定要好好的注意這寫容易錯的地方,替換的時候有括號和沒有括號是很大的區別。

 

4)int  *p

p= (int *)malloc(4);

p= (int *)malloc(sizeof(int));以上兩個等價

當心填空題目,malloc的返回類型是 void *

 

6)函數的遞歸調用一定要記得有結束的條件,並且要會算簡單的遞歸題目。要會作遞歸的題目

 

7)結構體和共用體以及鏈表要掌握最簡單的。typedef考的很多,而且一定要知道如何引用結構體中的各個變量,鏈表中如何填加和刪除節點,以及何如構成一個簡單的鏈表,一定記住鏈表中的節點是有兩個域,一個放數值,一個放指針。

 

8)函數指針的用法(*f)()記住一個例子:

      int add(int x, int y)

{....}

 main()

{ int  (*f)();

  f=add;

 }

賦值之後:合法的調用形式爲1、add(2,3);

2、f(2,3);

3、(*f)(2,3)

9)兩種重要的數組長度:

char a[]={‘a’,’b’,’c’};  數組長度爲3,字符串長度不定。sizeof(a)爲3。

char a[5]={ ‘a’,’b’,’c’}  數組長度爲5,字符串長度3。sizeof(a)爲5。

10)scanf 和 gets的數據:

如果輸入的是 good  good study!

那麼scanf(“%s”,a); 只會接收 good. 考點:不可以接收空格。

     gets(a); 會接收 good good study! 考點:可以接收空格。

11)共用體的考查:

union TT

{ int a;

charch[2];}

考點一: sizeof (struct TT) = 4;

12)“文件包含”的考查點:

       no1.c                  no2.c

#include”no2.c”

main()

{ add(29 , 33);

 …….

}

 

int add(int a,int b)

{

return a+b;

}

 

 

 

 

 

 


這裏一個C語言程序是有兩個文件組成,分別是no1.c, no2.c。那麼no1.c中最開始有個#include”no2.c”他表示把第二個文件的內容給包含過來,那麼no1.c中調用add()函數的時候就可以了把數值傳到no2.c中的被調用函數add()了。

一個文件必須要有main函數。 這句話錯了。 例如:no2.c就沒有。

頭文件一定是以.h結束的。 這句話錯了。例如:no1.c中就是#include”no2.c”以.c結尾的。

13)指針迷惑的考點:

char ch[]=”iamhandsome”;

char *p=ch;

問你 *(p+2) 和 *p+2的結果是多少?

      ‘m’       ‘k’  結果是這兩個,想不通的同學請作死的想!想通爲止!

14)數組中放數組一定要看懂:

   int a[8]={1,2,3,4,4,3,2,2};

   int b[5]={0};

   b[a[3]]++   這個寫法要看懂,結果要知道是什麼?b[4]++,本身是0,運行完後,b[4]爲1了。

15)字符串的賦值

  C語言中沒有字符串變量,所以用數組和指針存放字符串:

1、char  ch[10]={“abcdefgh”};                       對

2、char  ch[10]=“abcdefgh”;                         對

3、char  ch[10]={‘a’,’b’,’c’,’d’,’e’,’f’,’g’,’h’};           對

4、char  *p=“abcdefgh”;                           對

5、char  *p;                                     對

p=“abcdefgh”;

6、char  ch[10];                    錯了!數組名不可以賦值!

          ch=“abcdefgh”;

7、char  *p={“abcdefgh”};           錯了!不能夠出現大括號!

16)字符串賦值的函數背誦:一定要背誦,當心筆試填空題目。

  把s指針中的字符串複製到t指針中的方法

  1、while( (*t=*s)!=null){s++;t++;}  完整版本

  2、while( *t=*s ){s++;t++;}           簡單版本

  3、while( *t++=*s++);                   高級版本

17)typedef 是取別名,不會產生新的類型,他同時也是關鍵字

考點一:typedef int qq  那麼 int x 就可以寫成 qq x

考點二:typedef int *qq  那麼 int *x就可以寫成 qq x

18)static 考點是一定會考的!複習相關的習題。

static int x;默認值爲0。

int x:默認值爲不定值。

19)函數的遞歸調用


 C精華總結

C++的前世是C,而且C所留下的神祕以及精簡在C++中是青出於藍而勝於藍!C所帶給人的困惑以及靈活太多,即使一個有幾年經驗的高段C程序員仍然有可能在C語言的小水溝裏翻船。不過其實C語言真的不難,下面我想指出C語言中最神祕而又詭譎多變的四個地方,它們也繼續在C++語言中變幻莫測。 
指針,數組,類型的識別,參數可變的函數。

一.指針。


它的本質是地址的類型。在許多語言中根本就沒有這個概念。但是它卻正是C靈活,高效,在面向過程的時代所向披靡的原因所在。因爲C的內存模型基本上對應了現在von Neumann(馮·諾伊曼)計算機的機器模型,很好的達到了對機器的映射。不過有些人似乎永遠也不能理解指針【注1】。 
注1:Joel Spolsky就是這樣認爲的,他認爲對指針的理解是一種aptitude,不是通過訓練就可以達到的 
指針可以指向值、數組、函數,當然它也可以作爲值使用。 
看下面的幾個例子: 
int* p;//p是一個指針,指向一個整數 
int** p;//p是一個指針,它指向第二個指針,然後指向一個整數 
int (*pa)[3];//pa是一個指針,指向一個擁有3個整數的數組 
int (*pf)();//pf是一個指向函數的指針,這個函數返回一個整數 
後面第四節我會詳細講解標識符(identifier)類型的識別。 
1.指針本身的類型是什麼? 
先看下面的例子:int a;//a的類型是什麼? 
對,把a去掉就可以了。因此上面的4個聲明語句中的指針本身的類型爲: 
int* 
int** 
int (*)[3] 
int (*)() 
它們都是複合類型,也就是類型與類型結合而成的類型。意義分別如下: 
point to int(指向一個整數的指針) 
pointer to pointer to int(指向一個指向整數的指針的指針) 
pointer to array of 3 ints(指向一個擁有三個整數的數組的指針) 
pointer to function of parameter is void and return value is int (指向一個函數的指針,這個函數參數爲空,返回值爲整數) 
2.指針所指物的類型是什麼? 
很簡單,指針本身的類型去掉 “*”號就可以了,分別如下: 
int 
int* 
int ()[3] 
int ()() 
3和4有點怪,不是嗎?請擦亮你的眼睛,在那個用來把“*”號包住的“()”是多餘的,所以: 
int ()[3]就是int [3](一個擁有三個整數的數組) 
int ()()就是int ()(一個函數,參數爲空,返回值爲整數)【注2】 
注2:一個小小的提醒,第二個“()”是一個運算符,名字叫函數調用運算符(function call operator)。 
3.指針的算術運算。 
請再次記住:指針不是一個簡單的類型,它是一個和指針所指物的類型複合的類型。因此,它的算術運算與之(指針所指物的類型)密切相關。 
int a[8]; 
int* p = a; 
int* q = p + 3; 
p++; 
指針的加減並不是指針本身的表示加減,要記住,指針是一個元素的地址,它每加一次,就指向下一個元素。所以: 
int* q = p + 3;//q指向從p開始的第三個整數。 
p++;//p指向下一個整數。 
double* pd; 
……//某些計算之後 
double* pother = pd – 2;//pother指向從pd倒數第二個double數。 
4.指針本身的大小。 
在一個現代典型的32位機器上【注3】,機器的內存模型大概是這樣的,想象一下,內存空間就像一個連續的房間羣。每一個房間的大小是一個字節(一般是黑客動畫吧8位)。有些東西大小是一個字節(比如char),一個房間就把它給安置了;但有些東西大小是幾個字節(比如double就是8個字節,int就是4 個字節,我說的是典型的32位),所以它就需要幾個房間才能安置。 
注3:什麼叫32位?就是機器CPU一次處理的數據寬度是32位,機器的寄存器容量是32位,機器的數據,內存地址總線是32位。當然還有一些細節,但大致就是這樣。16位,64位,128位可以以此類推。 
這些房間都應該有編號(也就是地址),32位的機器內存地址空間當然也是32位,所以房間的每一個編號都用32位的數來編碼【注4】。請記住指針也可以作爲值使用,作爲值的時候,它也必須被安置在房間中(存儲在內存中),那麼指向一個值的指針需要一個地址大小來存儲,即32位,4個字節,4個房間來存儲。 
注4:在我們平常用到的32位機器上,絕少有將32位真實內存地址空間全用完的(232 = 4G),即使是服務器也不例外。現代的操作系統一般會實現32位的虛擬地址空間,這樣可以方便運用程序的編制。關於虛擬地址(線性地址)和真實地址的區別以及實現,可以參考《Linux源代碼情景分析》的第二章存儲管理,在互聯網上關於這個主題的文章汗牛充棟,你也可以google一下。 
但請注意,在C++中指向對象成員的指針(pointer to member data or member function)的大小不一定是4個字節。爲此我專門編制了一些程序,發現在我的兩個編譯器(VC7.1.3088和Dev-C++4.9.7.0)上,指向對象成員的指針的大小沒有定值,但都是4的倍數。不同的編譯器還有不同的值。對於一般的普通類(class),指向對象成員的指針大小一般爲4,但在引入多重虛擬繼承以及虛擬函數的時候,指向對象成員的指針會增大,不論是指向成員數據,還是成員函數。【注5】。 
注5:在Andrei Alexandrescu的《Modern C++ Design》的5.13節Page124中提到,成員函數指針實際上是帶標記的(tagged)unions,它們可以對付多重虛擬繼承以及虛擬函數,書上說成員函數指針大小是16,但我的實踐告訴我這個結果不對,而且具體編譯器實現也不同。一直很想看看GCC的源代碼,但由於旁騖太多,而且心不靜,本身難度也比較高(這個倒是不害怕^_^),只有留待以後了。 
還有一點,對一個類的static member來說,指向它的指針只是普通的函數指針,不是pointer to class member,所以它的大小是4。 
5.指針運算符&和* 
它們是一對相反的操作,&取得一個東西的地址(也就是指針),*得到一個地址裏放的東西。這個東西可以是值(對象)、函數、數組、類成員(class member)。 
其實很簡單,房間裏面居住着一個人,&操作只能針對人,取得房間號碼; 
*操作只能針對房間,取得房間裏的人。 
參照指針本身的類型以及指針所指物的類型很好理解。 
小結:其實你只要真正理解了1,2,就相當於掌握了指針的牛鼻子。後面的就不難了,指針的各種變化和C語言中其它普通類型的變化都差不多(比如各種轉型)。

二.數組。

在C語言中,對於數組你只需要理解三件事。 
1.C語言中有且只有一維數組。 
所謂的n維數組只是一個稱呼,一種方便的記法,都是使用一維數組來仿真的。 
C語言中數組的元素可以是任何類型的東西,特別的是數組作爲元素也可以。所以int a[3][4][5]就應該這樣理解:a是一個擁有3個元素的數組,其中每個元素是一個擁有4個元素的數組,進一步其中每個元素是擁有5個整數元素的數組。 
是不是很簡單!數組a的內存模型你應該很容易就想出來了,不是嗎?:) 
2.數組的元素個數,必須作爲整數常量在編譯階段就求出來。 
int i; 
int a[];//不合法,編譯不會通過。 
也許有人會奇怪char str[] = “test”;沒有指定元素個數爲什麼也能通過,因爲編譯器可以根據後面的初始化字符串在編譯階段求出來, 
不信你試試這個:int a[]; 
編譯器無法推斷,所以會判錯說“array size missing in a”之類的信息。不過在最新的C99標準中實現了變長數組【注6】 
注6:如果你是一個好奇心很強烈的人,就像我一樣,那麼可以查看C99標準6.7.5.2。 
3.對於數組,可以獲得數組第一個(即下標爲0)元素的地址(也就是指針),從數組名獲得。 
比如int a[5]; int* p = a;這裏p就得到了數組元素a[0]的地址。 
其餘對於數組的各種操作,其實都是對於指針的相應操作。比如a[3]其實就是*(a+3)的簡單寫法,由於*(a+3)==*(3+a),所以在某些程序的代碼中你會看到類似3[a]的這種奇怪表達式,現在你知道了,它就是a[3]的別名。還有一種奇怪的表達式類似a[-1],現在你也明白了,它就是* (a-1)【注7】。 
注7:你肯定是一個很負責任的人,而且也知道自己到底在幹什麼。你難道不是嗎?:)所以你一定也知道,做一件事是要付出成本的,當然也應該獲得多於成本的回報。 
我很喜歡經濟學,經濟學的一個基礎就是做什麼事情都是要花成本的,即使你什麼事情也不做。時間成本,金錢成本,機會成本,健康成本……可以這樣說,經濟學的根本目的就是用最小的成本獲得最大的回報。 
所以我們在自己的程序中最好避免這種邪惡的寫法,不要讓自己一時的智力過剩帶來以後自己和他人長時間的痛苦。用韋小寶的一句話來說:“賠本的生意老子是不幹的!” 
但是對邪惡的瞭解是非常必要的,這樣當我們真正遇到邪惡的時候,可以免受它對心靈的困擾! 
對於指向同一個數組不同元素的指針,它們可以做減法,比如int* p = q+i;p-q的結果就是這兩個指針之間的元素個數。i可以是負數。但是請記住:對指向不同的數組元素的指針,這樣的做法是無用而且邪惡的! 
對於所謂的n維數組,比如int a[2][3];你可以得到數組第一個元素的地址a和它的大小。*(a+0)(也即a[0]或者*a)就是第一個元素,它又是一個數組int[3],繼續取得它的第一個元素,*(*(a+0)+0)(也即a[0][0]或者*(*a)),也即第一個整數(第一行第一列的第一個整數)。如果採用這種表達式,就非常的笨拙,所以a[0][0]記法上的簡便就非常的有用了!簡單明瞭! 
對於數組,你只能取用在數組有效範圍內的元素和元素地址,不過最後一個元素的下一個元素的地址是個例外。它可以被用來方便數組的各種計算,特別是比較運算。但顯然,它所指向的內容是不能拿來使用和改變的! 
關於數組本身大概就這麼多,下面簡要說一下數組和指針的關係。它們的關係非常曖昧,有時候可以交替使用。 
比如 int main(int args, char* argv[])中,其實參數列表中的char* argv[]就是char** argv的另一種寫法。因爲在C語言中,一個數組是不能作爲函數引數(argument)【注8】直接傳遞的。因爲那樣非常的損失效率,而這點違背了C語言設計時的基本理念——作爲一門高效的系統設計語言。 
注8:這裏我沒有使用函數實參這個大陸術語,而是運用了臺灣術語,它們都是argument這個英文術語的翻譯,但在很多地方中文的實參用的並不恰當,非常的勉強,而引數表示被引用的數,很形象,也很好理解。很快你就可以像我一樣適應引數而不是實參。 
dereferance,也就是*運算符操作。我也用的是提領,而不是解引用。 
我認爲你一定智勇雙全:既有寬容的智慧,也有面對新事物的勇氣!你不願意承認嗎?:) 
所以在函數參數列表(parameter list)中的數組形式的參數聲明,只是爲了方便程序員的閱讀!比如上面的char* argv[]就可以很容易的想到是對一個char*字符串數組進行操作,其實質是傳遞的char*字符串數組的首元素的地址(指針)。其它的元素當然可以由這個指針的加法間接提領(dereferance)【參考注8】得到!從而也就間接得到了整個數組。 
但是數組和指針還是有區別的,比如在一個文件中有下面的定義: 
char myname[] = “wuaihua”; 
而在另一個文件中有下列聲明: 
extern char* myname; 
它們互相是並不認識的,儘管你的本義是這樣希望的。 
它們對內存空間的使用方式不同【注9】。 
對於char myname[] = “wuaihua”如下 
myname 







\0 
對於char* myname;如下表 
myname 
\|/ 







\0 
注9:可以參考Andrew Konig的《C陷阱與缺陷》4.5節。 
改變的方法就是使它們一致就可以了。 
char myname[] = “wuaihua”; 
extern char myname[]; 
或者 
char* myname = “wuaihua”;//C++中最好換成const char* myname = “wuaihua”。 
extern char* myname; 
C之詭譎(下)

三.類型的識別。

基本類型的識別非常簡單: 
int a;//a的類型是a 
char* p;//p的類型是char* 
…… 
那麼請你看看下面幾個: 
int* (*a[5])(int, char*); //#1 
void (*b[10]) (void (*)()); //#2 
doube(*)() (*pa)[9]; //#3 
如果你是第一次看到這種類型聲明的時候,我想肯定跟我的感覺一樣,就如晴天霹靂,五雷轟頂,頭昏目眩,一頭張牙舞爪的猙獰怪獸撲面而來。 
不要緊(Take it easy)!我們慢慢來收拾這幾個面目可憎的紙老虎! 
1.C語言中函數聲明和數組聲明。 
函數聲明一般是這樣int fun(int,double);對應函數指針(pointer to function)的聲明是這樣: 
int (*pf)(int,double),你必須習慣。可以這樣使用: 
pf = &fun;//賦值(assignment)操作 
(*pf)(5, 8.9);//函數調用操作 
也請注意,C語言本身提供了一種簡寫方式如下: 
pf = fun;// 賦值(assignment)操作 
pf(5, 8.9);// 函數調用操作 
不過我本人不是很喜歡這種簡寫,它對初學者帶來了比較多的迷惑。 
數組聲明一般是這樣int a[5];對於數組指針(pointer to array)的聲明是這樣: 
int (*pa)[5]; 你也必須習慣。可以這樣使用: 
pa = &a;// 賦值(assignment)操作 
int i = (*pa)[2]//將a[2]賦值給i; 
2.有了上面的基礎,我們就可以對付開頭的三隻紙老虎了!:) 
這個時候你需要複習一下各種運算符的優先順序和結合順序了,順便找本書看看就夠了。 
#1:int* (*a[5])(int, char*); 
首先看到標識符名a,“[]”優先級大於“*”,a與“[5]”先結合。所以a是一個數組,這個數組有5個元素,每一個元素都是一個指針,指針指向 “(int, char*)”,對,指向一個函數,函數參數是“int, char*”,返回值是“int*”。完畢,我們幹掉了第一個紙老虎。:) 
#2:void (*b[10]) (void (*)()); 
b是一個數組,這個數組有10個元素,每一個元素都是一個指針,指針指向一個函數,函數參數是“void (*)()”【注10】,返回值是“void”。完畢! 
注10:這個參數又是一個指針,指向一個函數,函數參數爲空,返回值是“void”。 
#3. doube(*)() (*pa)[9]; 
pa是一個指針,指針指向一個數組,這個數組有9個元素,每一個元素都是“doube(*)()”【也即一個指針,指向一個函數,函數參數爲空,返回值是“double”】。 
現在是不是覺得要認識它們是易如反掌,工欲善其事,必先利其器!我們對這種表達方式熟悉之後,就可以用“typedef”來簡化這種類型聲明。 
#1:int* (*a[5])(int, char*); 
typedef int* (*PF)(int, char*);//PF是一個類型別名【注11】。 
PF a[5];//跟int* (*a[5])(int, char*);的效果一樣! 
注 11:很多初學者只知道typedef char* pchar;但是對於typedef的其它用法不太瞭解。Stephen Blaha對typedef用法做過一個總結:“建立一個類型別名的方法很簡單,在傳統的變量聲明表達式裏用類型名替代變量名,然後把關鍵字 typedef加在該語句的開頭”。可以參看《程序員》雜誌2001.3期《C++高手技巧20招》。 
#2:void (*b[10]) (void (*)()); 
typedef void (*pfv)(); 
typedef void (*pf_taking_pfv)(pfv); 
pf_taking_pfv b[10]; //跟void (*b[10]) (void (*)());的效果一樣! 
#3. doube(*)() (*pa)[9]; 
typedef double(*PF)(); 
typedef PF (*PA)[9]; 
PA pa; //跟doube(*)() (*pa)[9];的效果一樣! 
3.const和volatile在類型聲明中的位置 
在這裏我只說const,volatile是一樣的【注12】! 
注12:顧名思義,volatile修飾的量就是很容易變化,不穩定的量,它可能被其它線程,操作系統,硬件等等在未知的時間改變,所以它被存儲在內存中,每次取用它的時候都只能在內存中去讀取,它不能被編譯器優化放在內部寄存器中。 
類型聲明中const用來修飾一個常量,我們一般這樣使用:const在前面 
const int;//int是const 
const char*;//char是const 
char* const;//*(指針)是const 
const char* const;//char和*都是const 
對初學者,const char*;和 char* const;是容易混淆的。這需要時間的歷練讓你習慣它。 
上面的聲明有一個對等的寫法:const在後面 
int const;//int是const 
char const*;//char是const 
char* const;//*(指針)是const 
char const* const;//char和*都是const 
第一次你可能不會習慣,但新事物如果是好的,我們爲什麼要拒絕它呢?:)const在後面有兩個好處: 
A. const所修飾的類型是正好在它前面的那一個。如果這個好處還不能讓你動心的話,那請看下一個! 
B.我們很多時候會用到typedef的類型別名定義。比如typedef char* pchar,如果用const來修飾的話,當const在前面的時候,就是const pchar,你會以爲它就是const char* ,但是你錯了,它的真實含義是char* const。是不是讓你大吃一驚!但如果你採用const在後面的寫法,意義就怎麼也不會變,不信你試試! 
不過,在真實項目中的命名一致性更重要。你應該在兩種情況下都能適應,並能自如的轉換,公司習慣,商業利潤不論在什麼時候都應該優先考慮!不過在開始一個新項目的時候,你可以考慮優先使用const在後面的習慣用法。

四.參數可變的函數

C語言中有一種很奇怪的參數“…”,它主要用在引數(argument)個數不定的函數中,最常見的就是printf函數。 
printf(“Enjoy yourself everyday!\n”); 
printf(“The value is %d!\n”, value); 
…… 
你想過它是怎麼實現的嗎? 
1. printf爲什麼叫printf? 
不管是看什麼,我總是一個喜歡刨根問底的人,對事物的源有一種特殊的癖好,一段典故,一個成語,一句行話,我最喜歡的就是找到它的來歷,和當時的意境,一個外文翻譯過來的術語,最低要求我會盡力去找到它原本的外文術語。特別是一個字的命名來歷,我一向是非常在意的,中國有句古話:“名不正,則言不順。 ”printf中的f就是format的意思,即按格式打印【注13】。 
注13:其實還有很多函數,很多變量,很多命名在各種語言中都是非常講究的,你如果細心觀察追溯,一定有很多樂趣和滿足,比如哈希表爲什麼叫hashtable而不叫hashlist?在C++的SGI STL實現中有一個專門用於遞增的函數iota(不是itoa),爲什麼叫這個奇怪的名字,你想過嗎? 
看文章我不喜歡意猶未盡,己所不欲,勿施於人,所以我把這兩個答案告訴你: 
(1)table與list做爲表講的區別: 
table: 
-------|--------------------|------- 
item1 | kadkglasgaldfgl | jkdsfh 
-------|--------------------|------- 
item2 | kjdszhahlka | xcvz 
-------|--------------------|------- 
list: 
**** 
*** 
******* 
***** 
That's the difference! 
如果你還是不明白,可以去看一下hash是如何實現的! 
(2)The name iota is taken from the programming language APL. 
而APL語言主要是做數學計算的,在數學中有很多公式會借用希臘字母, 
希臘字母表中有這樣一個字母,大寫爲Ι,小寫爲ι, 
它的英文拼寫正好是iota,這個字母在θ(theta)和κ(kappa)之間! 
下面有一段是這樣的: 
APL is renowned for using a set of non-ASCII symbols that are an extension of traditional arithmetic and algebraic notation. These cryptic symbols, some have joked, make it possible to construct an entire air traffic control system in two lines of code. Because of its condensed nature and non-standard characters, APL has sometimes been termed a "write-only language", and reading an APL program can feel like decoding an alien tongue. Because of the unusual character-set, many programmers used special APL keyboards in the production of APL code. Nowadays there are various ways to write APL code using only ASCII characters. 
在C++中有函數重載(overload)可以用來區別不同函數參數的調用,但它還是不能表示任意數量的函數參數。 
在標準C語言中定義了一個頭文件專門用來對付可變參數列表,它包含了一組宏,和一個va_list的typedef聲明。一個典型實現如下【注14】: 
typedef char* va_list; 
#define va_start(list) list = (char*)&va_alist 
#define va_end(list) 
#define va_arg(list, mode) 
((mode*) (list += sizeof(mode)))[-1] 
注14:你可以查看C99標準7.15節獲得詳細而權威的說明。也可以參考Andrew Konig的《C陷阱與缺陷》的附錄A。 
ANSI C還提供了vprintf函數,它和對應的printf函數行爲方式上完全相同,只不過用va_list替換了格式字符串後的參數序列。至於它是如何實現的,你在認真讀完《The C Programming Language》後,我相信你一定可以do it yourself! 
使用這些工具,我們就可以實現自己的可變參數函數,比如實現一個系統化的錯誤處理函數error。它和printf函數的使用差不多。只不過將stream重新定向到stderr。在這裏我借鑑了《C陷阱與缺陷》的附錄A的例子。 
實現如下: 
#include 
#include 
void error(char* format, …) 

va_list ap; 
va_start(ap, format); 
fprintf(stderr, “error: “); 
vfprintf(stderr, format, ap); 
va_end(ap); 
fprintf(stderr, “\n”); 
exit(1); 

你還可以自己實現printf: 
#include 
int printf(char* format, …) 

va_list ap; 
va_start(ap, format); 
int n = vprintf(format, ap); 
va_end(ap); 
return n; 

我還專門找到了VC7.1的頭文件看了一下,發現各個宏的具體實現還是有區別的,跟很多預處理(preprocessor)相關。其中va_list就不一定是char*的別名。 
typedef struct { 
char *a0; /* pointer to first homed integer argument */ 
int offset; /* byte offset of next parameter */ 
} va_list; 
其它的定義類似。 
經常在Windows進行系統編程的人一定知道函數調用有好幾種不同的形式,比如__stdcall,__pascal,__cdecl。在Windows下_stdcall,__pascal是一樣的,所以我只說一下__stdcall和__cdecl的區別。 
(1)__stdcall表示被調用端自身負責函數引數的壓棧和出棧。函數參數個數一定的函數都是這種調用形式。 
例如:int fun(char c, double d),我們在main函數中使用它,這個函數就只管本身函數體的運行,參數怎麼來的,怎麼去的,它一概不管。自然有main負責。不過,不同的編譯器的實現可能將參數從右向左壓棧,也可能從左向右壓棧,這個順序我們是不能加於利用的【注15】。 
注15:你可以在Herb Sutter的《More Exceptional C++》中的條款20:An Unmanaged Pointer Problem, Part 1:Parameter Evaluation找到相關的細節論述。 
(2)__cdecl表示調用端負責被調用端引數的壓棧和出棧。參數可變的函數採用的是這種調用形式。 
爲什麼這種函數要採用不同於前面的調用形式呢?那是因爲__stdcall調用形式對它沒有作用,被調用端根本就無法知道調用端的引數個數,它怎麼可能正確工作?所以這種調用方式是必須的,不過由於參數參數可變的函數本身不多,所以用的地方比較少。 
對於這兩種方式,你可以編制一些簡單的程序,然後反彙編,在彙編代碼下面你就可以看到實際的區別,很好理解的! 
重載函數有很多匹配(match)規則調用。參數爲“…”的函數是匹配最低的,這一點在Andrei Alexandrescu的驚才絕豔之作《Modern C++ Design》中就有用到,參看Page34-35,2.7“編譯期間偵測可轉換性和繼承性”。 
後記: 
C語言的細節肯定不會只有這麼多,但是這幾個出現的比較頻繁,而且在C語言中也是很重要的幾個語言特徵。如果把這幾個細節徹底弄清楚了,C語言本身的神祕就不會太多了。 
C 語言本身就像一把異常鋒利的剪刀,你可以用它做出非常精緻優雅的藝術品,也可以剪出一些亂七八糟的廢紙片。能夠將一件武器用到出神入化那是需要時間的,需要多長時間?不多,請你拿出一萬個小時來,英國Exter大學心理學教授麥克.侯威專門研究神童和天才,他的結論很有意思:“一般人以爲天才是自然而生、流暢而不受阻的閃亮才華,其實,天才也必須耗費至少十年光陰來學習他們的特殊技能,絕無例外。要成爲專家,需要擁有頑固的個性和堅持的能力……每一行的專業人士,都投注大量心血,培養自己的專業才能。”【注16】 
注16:臺灣女作家、電視節目主持人吳淡如《拿出一萬個小時來》。《讀者》2003.1期。“不用太努力,只要持續下去。想擁有一輩子的專長或興趣,就像一個人跑馬拉松賽一樣,最重要的是跑完,而不是前頭跑得有多快。” 
推薦兩本書: 
K&R的《The C Programming language》,Second Edition。 
Andrew Konig的《C陷阱與缺陷》。本文從中引用了好幾個例子,一本高段程序員的經驗之談。 
但是對純粹的初學者不太合適,如果你有一點程序設計的基礎知識,花一個月的時間好好看看這兩本書,C語言本身就不用再花更多的精力了

轉自:http://hi.baidu.com/qq421681423/blog/item/0c7292ec533bd0d62e2e2105.html







發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章