C語言指針詳解

指針是一個特殊的變量,它裏面存儲的數值被解釋成爲內存裏的一個地址。 要搞清一個指針需要搞清指針的四方面的內容:指針的類型,指針所指向的 類型,指針的值或者叫指針所指向的內存區,還有指針本身所佔據的內存區。讓我們分別說明。
  先聲明幾個指針放着做例子:
  例一:
  (1)int*ptr;
  (2)char*ptr;
  (3)int**ptr;
  (4)int(*ptr)[3];
  (5)int*(*ptr)[4];
  
  指針的類型
  從語法的角度看,你只要把指針聲明語句裏的指針名字去掉,剩下的部分就是這個指針的類型。這是指針本身所具有的類型。讓我們看看例一中各個指針的類型:
  (1)int*ptr;//指針的類型是int*
  (2)char*ptr;//指針的類型是char*
  (3)int**ptr;//指針的類型是int**
  (4)int(*ptr)[3];//指針的類型是int(*)[3]
  (5)int*(*ptr)[4];//指針的類型是int*(*)[4]
  怎麼樣?找出指針的類型的方法是不是很簡單?
  指針所指向的類型
  當你通過指針來訪問指針所指向的內存區時,指針所指向的類型決定了編譯器將把那片內存區裏的內容當做什麼來看待。
  從語法上看,你只須把指針聲明語句中的指針名字和名字左邊的指針聲明符*去掉,剩下的就是指針所指向的類型。例如:
  (1)int*ptr;//指針所指向的類型是int
  (2)char*ptr;//指針所指向的的類型是char
  (3)int**ptr;//指針所指向的的類型是int*
  (4)int(*ptr)[3];//指針所指向的的類型是int()[3]
  (5)int*(*ptr)[4];//指針所指向的的類型是int*()[4]
  在指針的算術運算中,指針所指向的類型有很大的作用。
  指針的類型(即指針本身的類型)和指針所指向的類型是兩個概念。當你對C越來越熟悉時,你會發現,把與指針攪和在一起的"類型"這個概念分成"指針的類型"和"指針所指向的類型"兩個概念,是精通指針的關鍵點之一。我看了不少書,發現有些寫得差的書中,就把指針的這兩個概念攪在一起了,所以看起書來前後矛盾,越看越糊塗。
指針的值,或者叫指針所指向的內存區或地址
  指針的值是指針本身存儲的數值,這個值將被編譯器當作一個地址,而不是一個一般的數值。在32位程序裏,所有類型的指針的值都是一個32位整數,因爲32位程序裏內存地址全都是32位長。 指針所指向的內存區就是從指針的值所代表的那個內存地址開始,長度爲si zeof(指針所指向的類型)的一片內存區。以後,我們說一個指針的值是XX,就相當於說該指針指向了以XX爲首地址的一片內存區域;我們說一個指針指向了某塊內存區域,就相當於說該指針的值是這塊內存區域的首地址。
  指針所指向的內存區和指針所指向的類型是兩個完全不同的概念。在例一中,指針所指向的類型已經有了,但由於指針還未初始化,所以它所指向的內存區是不存在的,或者說是無意義的。
  以後,每遇到一個指針,都應該問問:這個指針的類型是什麼?指針指的類型是什麼?該指針指向了哪裏?
  指針本身所佔據的內存區
  指針本身佔了多大的內存?你只要用函數sizeof(指針的類型)測一下就知道了。在32位平臺裏,指針本身佔據了4個字節的長度。
  指針本身佔據的內存這個概念在判斷一個指針表達式是否是左值時很有用。
  指針的算術運算
指針可以加上或減去一個整數。指針的這種運算的意義和通常的數值的加減運算的意義是不一樣的。例如:
  例二:
  1、chara[20];
  2、int*ptr=a;
  ...
 ...
  3、ptr++;
  在上例中,指針ptr的類型是int*,它指向的類型是int,它被初始化爲指向整形變量a。接下來的第3句中,指針ptr被加了1,編譯器是這樣處理的:它把指針ptr的值加上了sizeof(int),在32位程序中,是被加上了4。由於地址是用字節做單位的,故ptr所指向的地址由原來的變量a的地址向高地址方向增加了4個字節。
由於char類型的長度是一個字節,所以,原來ptr是指向數組a的第0號單元開始的四個字節,此時指向了數組a中從第4號單元開始的四個字節。
  我們可以用一個指針和一個循環來遍歷一個數組,看例子:
  例三:
intarray[20];
int*ptr=array;
...
//此處略去爲整型數組賦值的代碼。
...
for(i=0;i<20;i++)
{
 (*ptr)++;
 ptr++;
}
  這個例子將整型數組中各個單元的值加1。由於每次循環都將指針ptr加1,所以每次循環都能訪問數組的下一個單元。

  再看例子:

  例四:

  1、chara[20];
  2、int*ptr=a;
  ...
  ...
  3、ptr+=5;
  在這個例子中,ptr被加上了5,編譯器是這樣處理的:將指針ptr的值加上5乘sizeof(int),在32位程序中就是加上了5乘4=20。由於地址的單位是字節,故現在的ptr所指向的地址比起加5後的ptr所指向的地址來說,向高地址方向移動了20個字節。在這個例子中,沒加5前的ptr指向數組a的第0號單元開始的四個字節,加5後,ptr已經指向了數組a的合法範圍之外了。雖然這種情況在應用上會出問題,但在語法上卻是可以的。這也體現出了指針的靈活性。
如果上例中,ptr是被減去5,那麼處理過程大同小異,只不過ptr的值是被減去5乘sizeof(int),新的ptr指向的地址將比原來的ptr所指向的地址向低地址方向移動了20個字節。
  總結一下,一個指針ptrold加上一個整數n後,結果是一個新的指針ptrnew,ptrnew的類型和ptrold的類型相同,ptrnew所指向的類型和ptrold所指向的類型也相同。ptrnew的值將比ptrold的值增加了n乘sizeof(ptrold所指向的類型)個字節。就是說,ptrnew所指向的內存區將比ptrold所指向的內存區向高地址方向移動了n乘sizeof(ptrold所指向的類型)個字節。
  一個指針ptrold減去一個整數n後,結果是一個新的指針ptrnew,ptrnew的類型和ptrold的類型相同,ptrnew所指向的類型和ptrold所指向的類型也相同。ptrnew的值將比ptrold的值減少了n乘sizeof(ptrold所指向的類型)個字節,就是說,ptrnew所指向的內存區將比ptrold所指向的內存區向低地址方向移動了n乘sizeof(ptrold所指向的類型)個字節。
運算符&和*
這裏&是取地址運算符,*是...書上叫做"間接運算符"。
  &a的運算結果是一個指針,指針的類型是a的類型加個*,指針所指向的類型是a的類型,指針所指向的地址嘛,那就是a的地址。
  *p的運算結果就五花八門了。總之*p的結果是p所指向的東西,這個東西有這些特點:它的類型是p指向的類型,它所佔用的地址是p所指向的地址。
  例五:
inta=12;
intb;
int*p;
int**ptr;
p=&a;
//&a的結果是一個指針,類型是int*,指向的類型是int,指向的地址是a的地址。
*p=24;
//*p的結果,在這裏它的類型是int,它所佔用的地址是p所指向的地址,顯然,*p就是變量a。
ptr=&p;
//&p的結果是個指針,該指針的類型是p的類型加個*,在這裏是int **。該指針所指向的類型是p的類型,這裏是int*。該指針所指向的地址就是指針p自己的地址。
*ptr=&b;
//*ptr是個指針,&b的結果也是個指針,且這兩個指針的類型和所指向的類型是一樣的,所以用&b來給*ptr賦值就是毫無問題的了。
**ptr=34;
//*ptr的結果是ptr所指向的東西,在這裏是一個指針,對這個指針再做一次*運算,結果就是一個int類型的變量。
  指針表達式
一個表達式的最後結果如果是一個指針,那麼這個表達式就叫指針表式。
  下面是一些指針表達式的例子:
  例六:
inta,b;
intarray[10];
int*pa;
pa=&a;//&a是一個指針表達式。
int**ptr=&pa;//&pa也是一個指針表達式。
*ptr=&b;//*ptr和&b都是指針表達式。
pa=array;
pa++;//這也是指針表達式。
例七:
char*arr[20];
char**parr=arr;//如果把arr看作指針的話,arr也是指針表達式
char*str;
str=*parr;//*parr是指針表達式
str=*(parr+1);//*(parr+1)是指針表達式
str=*(parr+2);//*(parr+2)是指針表達式
  由於指針表達式的結果是一個指針,所以指針表達式也具有指針所具有的四個要素:指針的類型,指針所指向的類型,指針指向的內存區,指針自身佔據的內存。

  好了,當一個指針表達式的結果指針已經明確地具有了指針自身佔據的內存的話,這個指針表達式就是一個左值,否則就不是一個左值。
  在例七中,&a不是一個左值,因爲它還沒有佔據明確的內存。*ptr是一個左值,因爲*ptr這個指針已經佔據了內存,其實*ptr就是指針pa,既然pa已經在內存中有了自己的位置,那麼*ptr當然也有了自己的位置。
  數組和指針的關係
  數組的數組名其實可以看作一個指針。看下例:
  例八:
intarray[10]={0,1,2,3,4,5,6,7,8,9},value;
...
...
value=array[0];//也可寫成:value=*array;
value=array[3];//也可寫成:value=*(array+3);
value=array[4];//也可寫成:value=*(array+4);
上例中,一般而言數組名array代表數組本身,類型是int[10],但如果把array看做指針的話,它指向數組的第0個單元,類型是int*,所指向的類型是數組單元的類型即int。因此*array等於0就一點也不奇怪了。同理,array+3是一個指向數組第3個單元的指針,所以*(array+3)等於3。其它依此類推。

  例九:
char*str[3]={
 "Hello,thisisasample!",
 "Hi,goodmorning.",
 "Helloworld"
};
chars[80];
strcpy(s,str[0]);//也可寫成strcpy(s,*str);
strcpy(s,str[1]);//也可寫成strcpy(s,*(str+1));
strcpy(s,str[2]);//也可寫成strcpy(s,*(str+2));
上例中,str是一個三單元的數組,該數組的每個單元都是一個指針,這些指針各指向一個字符串。把指針數組名str當作一個指針的話,它指向數組的第0號單元,它的類型是char**,它指向的類型是char*。
*str也是一個指針,它的類型是char*,它所指向的類型是char,它指向的地址是字符串"Hello,thisisasample!"的第一個字符的地址,即'H'的地址。 str+1也是一個指針,它指向數組的第1號單元,它的類型是char**,它指向的類型是char*。

  *(str+1)也是一個指針,它的類型是char*,它所指向的類型是char,它指向 "Hi,goodmorning."的第一個字符'H',等等。

  下面總結一下數組的數組名的問題。聲明瞭一個數組TYPEarray[n],則數組名稱array就有了兩重含義:第一,它代表整個數組,它的類型是TYPE[n];第二 ,它是一個指針,該指針的類型是TYPE*,該指針指向的類型是TYPE,也就是數組單元的類型,該指針指向的內存區就是數組第0號單元,該指針自己佔有單獨的內存區,注意它和數組第0號單元佔據的內存區是不同的。該指針的值是不能修改的,即類似array++的表達式是錯誤的。
  在不同的表達式中數組名array可以扮演不同的角色。
  在表達式sizeof(array)中,數組名array代表數組本身,故這時sizeof函數測出的是整個數組的大小。
在表達式*array中,array扮演的是指針,因此這個表達式的結果就是數組第0號單元的值。sizeof(*array)測出的是數組單元的大小。
  表達式array+n(其中n=0,1,2,....。)中,array扮演的是指針,故array+n的結果是一個指針,它的類型是TYPE*,它指向的類型是TYPE,它指向數組第n號單元。故sizeof(array+n)測出的是指針類型的大小。
例十
intarray[10];
int(*ptr)[10];
ptr=&array;:
上例中ptr是一個指針,它的類型是int(*)[10],他指向的類型是int[10] ,我們用整個數組的首地址來初始化它。在語句ptr=&array中,array代表數組本身。

  本節中提到了函數sizeof(),那麼我來問一問,sizeof(指針名稱)測出的究竟是指針自身類型的大小呢還是指針所指向的類型的大小?答案是前者。例如:
int(*ptr)[10];
  則在32位程序中,有:
sizeof(int(*)[10])==4
sizeof(int[10])==40
sizeof(ptr)==4
實際上,sizeof(對象)測出的都是對象自身的類型的大小,而不是別的什麼類型的大小。
指針和結構類型的關係
可以聲明一個指向結構類型對象的指針。
  例十一:
structMyStruct
{
 inta;
 intb;
 intc;
}
MyStructss={20,30,40};
//聲明瞭結構對象ss,並把ss的三個成員初始化爲20,30和40。
MyStruct*ptr=&ss;
//聲明瞭一個指向結構對象ss的指針。它的類型是MyStruct*,它指向的類型是MyStruct。
int*pstr=(int*)&ss;
//聲明瞭一個指向結構對象ss的指針。但是它的類型和它指向的類型和ptr是不同的。
  請問怎樣通過指針ptr來訪問ss的三個成員變量?
  答案:
ptr->a;
ptr->b;
ptr->c;
  又請問怎樣通過指針pstr來訪問ss的三個成員變量?
  答案:
*pstr;//訪問了ss的成員a。
*(pstr+1);//訪問了ss的成員b。
*(pstr+2)//訪問了ss的成員c。
  雖然我在我的MSVC++6.0上調式過上述代碼,但是要知道,這樣使用pstr來訪問結構成員是不正規的,爲了說明爲什麼不正規,讓我們看看怎樣通過指針來訪問數組的各個單元:
  例十二:
intarray[3]={35,56,37};
int*pa=array;
  通過指針pa訪問數組array的三個單元的方法是:
*pa;//訪問了第0號單元
*(pa+1);//訪問了第1號單元
*(pa+2);//訪問了第2號單元
從格式上看倒是與通過指針訪問結構成員的不正規方法的格式一樣。
  所有的C/C++編譯器在排列數組的單元時,總是把各個數組單元存放在連續的存儲區裏,單元和單元之間沒有空隙。但在存放結構對象的各個成員時,在某種編譯環境下,可能會需要字對齊或雙字對齊或者是別的什麼對齊,需要在相鄰兩個成員之間加若干個"填充字節",這就導致各個成員之間可能會有若干個字節的空隙。
  所以,在例十二中,即使*pstr訪問到了結構對象ss的第一個成員變量a,也不能保證*(pstr+1)就一定能訪問到結構成員b。因爲成員a和成員b之間可能會有若干填充字節,說不定*(pstr+1)就正好訪問到了這些填充字節呢。這也證明了指針的靈活性。要是你的目的就是想看看各個結構成員之間到底有沒有填充字節,嘿,這倒是個不錯的方法。
過指針訪問結構成員的正確方法應該是象例十二中使用指針ptr的方法。
  指針和函數的關係
  可以把一個指針聲明成爲一個指向函數的指針。intfun1(char*,int);
int(*pfun1)(char*,int);
pfun1=fun1;
....
....
inta=(*pfun1)("abcdefg",7);//通過函數指針調用函數。
可以把指針作爲函數的形參。在函數調用語句中,可以用指針表達式來作爲實參。
  例十三:
intfun(char*);
inta;
charstr[]="abcdefghijklmn";
a=fun(str);
...
...
intfun(char*s)
{
intnum=0;
for(inti=0;i{
num+=*s;s++;
}
returnnum;
}
  這個例子中的函數fun統計一個字符串中各個字符的ASCII碼值之和。前面說了,數組的名字也是一個指針。在函數調用中,當把str作爲實參傳遞給形參s後,實際是把str的值傳遞給了s,s所指向的地址就和str所指向的地址一致,但是str和s各自佔用各自的存儲空間。在函數體內對s進行自加1運算,並不意味着同時對str進行了自加1運算。
指針類型轉換
當我們初始化一個指針或給一個指針賦值時,賦值號的左邊是一個指針,賦值號的右邊是一個指針表達式。在我們前面所舉的例子中,絕大多數情況下,指針的類型和指針表達式的類型是一樣的,指針所指向的類型和指針表達式所指向的類型是一樣的。
  例十四:
  1、floatf=12.3;
  2、float*fptr=&f;
  3、int*p;
   在上面的例子中,假如我們想讓指針p指向實數f,應該怎麼搞?是用下面的語句嗎?

  p=&f;

  不對。因爲指針p的類型是int*,它指向的類型是int。表達式&f的結果是一個指針,指針的類型是float*,它指向的類型是float。兩者不一致,直接賦值的方法是不行的。至少在我的MSVC++6.0上,對指針的賦值語句要求賦值號兩邊的類型一致,所指向的類型也一致,其它的編譯器上我沒試過,大家可以試試。爲了實現我們的目的,需要進行"強制類型轉換":
p=(int*)&f;
如果有一個指針p,我們需要把它的類型和所指向的類型改爲TYEP*TYPE, 那麼語法格式是:
  (TYPE*)p;
  這樣強制類型轉換的結果是一個新指針,該新指針的類型是TYPE*,它指向的類型是TYPE,它指向的地址就是原指針指向的地址。而原來的指針p的一切屬性都沒有被修改。
  一個函數如果使用了指針作爲形參,那麼在函數調用語句的實參和形參的結合過程中,也會發生指針類型的轉換。
  例十五:
voidfun(char*);
inta=125,b;
fun((char*)&a);
...
...
voidfun(char*s)
{
charc;
c=*(s+3);*(s+3)=*(s+0);*(s+0)=c;
c=*(s+2);*(s+2)=*(s+1);*(s+1)=c;
}
}
注意這是一個32位程序,故int類型佔了四個字節,char類型佔一個字節。函數fun的作用是把一個整數的四個字節的順序來個顛倒。注意到了嗎?在函數調用語句中,實參&a的結果是一個指針,它的類型是int*,它指向的類型是int。形參這個指針的類型是char*,它指向的類型是char。這樣,在實參和形參的結合過程中,我們必須進行一次從int*類型到char*類型的轉換。結合這個例子,我們可以這樣來想象編譯器進行轉換的過程:編譯器先構造一個臨時指針char*temp, 然後執行temp=(char*)&a,最後再把temp的值傳遞給s。所以最後的結果是:s的類型是char*,它指向的類型是char,它指向的地址就是a的首地址。

  我們已經知道,指針的值就是指針指向的地址,在32位程序中,指針的值其實是一個32位整數。那可不可以把一個整數當作指針的值直接賦給指針呢?就象下面的語句:
unsignedinta;
TYPE*ptr;//TYPE是int,char或結構類型等等類型。
...
...
a=20345686;
ptr=20345686;//我們的目的是要使指針ptr指向地址20345686(十進制

ptr=a;//我們的目的是要使指針ptr指向地址20345686(十進制)
編譯一下吧。結果發現後面兩條語句全是錯的。那麼我們的目的就不能達到了嗎?不,還有辦法:
unsignedinta;
TYPE*ptr;//TYPE是int,char或結構類型等等類型。
...
...
a=某個數,這個數必須代表一個合法的地址;
ptr=(TYPE*)a;//呵呵,這就可以了。
嚴格說來這裏的(TYPE*)和指針類型轉換中的(TYPE*)還不一樣。這裏的(TYPE*)的意思是把無符號整數a的值當作一個地址來看待。上面強調了a的值必須代表一個合法的地址,否則的話,在你使用ptr的時候,就會出現非法操作錯誤。

  想想能不能反過來,把指針指向的地址即指針的值當作一個整數取出來。完 全可以。下面的例子演示了把一個指針的值當作一個整數取出來,然後再把這個整數當作一個地址賦給一個指針:
  例十六:
inta=123,b;
int*ptr=&a;
char*str;
b=(int)ptr;//把指針ptr的值當作一個整數取出來。
str=(char*)b;//把這個整數的值當作一個地址賦給指針str。
  現在我們已經知道了,可以把指針的值當作一個整數取出來,也可以把一個整數值當作地址賦給一個指針。
  指針的安全問題
看下面的例子:
  例十七:
chars='a';
int*ptr;
ptr=(int*)&s;
*ptr=1298;
  指針ptr是一個int*類型的指針,它指向的類型是int。它指向的地址就是s的首地址。在32位程序中,s佔一個字節,int類型佔四個字節。最後一條語句不但改變了s所佔的一個字節,還把和s相臨的高地址方向的三個字節也改變了。這三個字節是幹什麼的?只有編譯程序知道,而寫程序的人是不太可能知道的。也許這三個字節裏存儲了非常重要的數據,也許這三個字節里正好是程序的一條代碼,而由於你對指針的馬虎應用,這三個字節的值被改變了!這會造成崩潰性的錯誤。
  讓我們再來看一例:
  例十八:
  1、chara;
  2、int*ptr=&a;
  ...
  ...
  3、ptr++;
  4、*ptr=115;
  該例子完全可以通過編譯,並能執行。但是看到沒有?第3句對指針ptr進行自加1運算後,ptr指向了和整形變量a相鄰的高地址方向的一塊存儲區。這塊存儲區裏是什麼?我們不知道。有可能它是一個非常重要的數據,甚至可能是一條代碼。而第4句竟然往這片存儲區裏寫入一個數據!這是嚴重的錯誤。所以在使用指針時,程序員心裏必須非常清楚:我的指針究竟指向了哪裏。在用指針訪問數組的時候,也要注意不要超出數組的低端和高端界限,否則也會造成類似的錯誤。
  在指針的強制類型轉換:ptr1=(TYPE*)ptr2中,如果sizeof(ptr2的類型)大於sizeof(ptr1的類型),那麼在使用指針ptr1來訪問ptr2所指向的存儲區時是安全的。如果sizeof(ptr2的類型)小於sizeof(ptr1的類型),那麼在使用指針ptr1來訪問ptr2所指向的存儲區時是不安全的。至於爲什麼,讀者結合例十七來想一想,應該會明白的。


http://embedfans.com/C/2007181016375897.htm
摘錄的別人的:

C語言所有複雜的指針聲明,都是由各種聲明嵌套構成的。如何解讀複雜指針聲明呢?右左法則是一個既著名又常用的方法。不過,右左法則其實並不是C標準裏面的內容,它是從C標準的聲明規定中歸納出來的方法。C標準的聲明規則,是用來解決如何創建聲明的,而右左法則是用來解決如何辯識一個聲明的,兩者可以說是相反的。右左法則的英文原文是這樣說的:

The right-left rule: Start reading the declaration from the innermost parentheses, go right, and then go left. When you encounter parentheses, the direction should be reversed. Once everything in the parentheses has been parsed, jump out of it. Continue till the whole declaration has been parsed.


這段英文的翻譯如下:

右左法則:首先從最裏面的圓括號看起,然後往右看,再往左看。每當遇到圓括號時,就應該掉轉閱讀方向。一旦解析完圓括號裏面所有的東西,就跳出圓括號。重複這個過程直到整個聲明解析完畢。

        筆者要對這個法則進行一個小小的修正,應該是從未定義的標識符開始閱讀,而不是從括號讀起,之所以是未定義的標識符,是因爲一個聲明裏面可能有多個標識符,但未定義的標識符只會有一個。

        現在通過一些例子來討論右左法則的應用,先從最簡單的開始,逐步加深:

int (*func)(int *p);

首先找到那個未定義的標識符,就是func,它的外面有一對圓括號,而且左邊是一個*號,這說明func是一個指針,然後跳出這個圓括號,先看右邊,也是一個圓括號,這說明(*func)是一個函數,而func是一個指向這類函數的指針,就是一個函數指針,這類函數具有int*類型的形參,返回值類型是int。

int (*func)(int *p, int (*f)(int*));

func被一對括號包含,且左邊有一個*號,說明func是一個指針,跳出括號,右邊也有個括號,那麼func是一個指向函數的指針,這類函數具有int *和int (*)(int*)這樣的形參,返回值爲int類型。再來看一看func的形參int (*f)(int*),類似前面的解釋,f也是一個函數指針,指向的函數具有int*類型的形參,返回值爲int。

int (*func[5])(int *p);

func右邊是一個[]運算符,說明func是一個具有5個元素的數組,func的左邊有一個*,說明func的元素是指針,要注意這裏的*不是修飾func的,而是修飾func[5]的,原因是[]運算符優先級比*高,func先跟[]結合,因此*修飾的是func[5]。跳出這個括號,看右邊,也是一對圓括號,說明func數組的元素是函數類型的指針,它所指向的函數具有int*類型的形參,返回值類型爲int。


int (*(*func)[5])(int *p);

func被一個圓括號包含,左邊又有一個*,那麼func是一個指針,跳出括號,右邊是一個[]運算符號,說明func是一個指向數組的指針,現在往左看,左邊有一個*號,說明這個數組的元素是指針,再跳出括號,右邊又有一個括號,說明這個數組的元素是指向函數的指針。總結一下,就是:func是一個指向數組的指針,這個數組的元素是函數指針,這些指針指向具有int*形參,返回值爲int類型的函數。

int (*(*func)(int *p))[5];

func是一個函數指針,這類函數具有int*類型的形參,返回值是指向數組的指針,所指向的數組的元素是具有5個int元素的數組。

要注意有些複雜指針聲明是非法的,例如:

int func(void) [5];

func是一個返回值爲具有5個int元素的數組的函數。但C語言的函數返回值不能爲數組,這是因爲如果允許函數返回值爲數組,那麼接收這個數組的內容的東西,也必須是一個數組,但C語言的數組名是一個右值,它不能作爲左值來接收另一個數組,因此函數返回值不能爲數組。

int func[5](void);

func是一個具有5個元素的數組,這個數組的元素都是函數。這也是非法的,因爲數組的元素除了類型必須一樣外,每個元素所佔用的內存空間也必須相同,顯然函數是無法達到這個要求的,即使函數的類型一樣,但函數所佔用的空間通常是不相同的。

作爲練習,下面列幾個複雜指針聲明給讀者自己來解析,答案放在第十章裏。

int (*(*func)[5][6])[7][8];

int (*(*(*func)(int *))[5])(int *);

int (*(*func[7][8][9])(int*))[5];

        實際當中,需要聲明一個複雜指針時,如果把整個聲明寫成上面所示的形式,對程序可讀性是一大損害。應該用typedef來對聲明逐層分解,增強可讀性,例如對於聲明:

int (*(*func)(int *p))[5];

可以這樣分解:

typedef  int (*PARA)[5];
typedef PARA (*func)(int *);

這樣就容易看得多了。
Unix系統永遠只會越來越多,開發人員就沒必要特意學習它們的安裝、配置和管理了,就全部交給集成人員吧。
    但開發人員行走於Unix之間,依然有四樣東西要熟練。

    一、VI
    雖然Unix上的文本編輯器已經越來越好用,但不在Console前面,網速也不夠連XWindows的時候,還是要依賴VI。
    回想VI的時代背景,發現VI對開發人員已經周到得離譜了,熱鍵多到你雙手不離鍵盤就能完成大半編輯工作。
    建議自己製作一張自己認爲有用,但又經常忘記的命令的sheet,拿出考試的力氣把它背熟。

    二、文本處理
       開發人員在Unix下幹得最多的除了Make和除Bug外,大概就是處理日誌文件、業務文件進行查錯和統計了。
     只會more和grep是不夠的,開發老手會把awk,sed,grep,sort,uniq,wc,head,tail這些文本處理命令,通過管道玩具式的拆卸拼裝,最後完成一件原本以爲非編寫大段代碼不可的工作。周到的參數設定,讓人再一次感嘆那個簡單的年代,這樣複雜到極致的設計.......怪不得《Unix 編程藝術》的作者有那麼驕傲的自覺。

     比如車東的每月訪問TOP10 統計腳本:

awk -F 't' '{print $4}' 2004_2.txt| grep chedong.com/tech/|uniq -c|sort -rn|head -10
awk -F '/t' 將2004_2.txt訪問紀錄文件,用TAB分割,打印第4列
grep chedong.com/tech 只列出chedong.com/tech筆記目錄下的文檔
uniq -c 彙總計數
sort -rn 按數值排序
head -10 TOP 10

    三、Bash Shell 編程
    編程是開發人員的天賦本能,不論什麼語言,看看參考手冊應該就能上手。

    見Bash新手指南中文版,一份寫給新手看的包含很多老手知識的指南。

    四、Make與AutoMake
    用過Java的Ant後,想起Make就覺得很煩,很厭倦。總歸還是會的,見GNU Make 3.8.0 中文手冊    

     不過即使make已經精通到變態,每個人寫出來的MakeFile還是千奇百怪,再看看開源項目們個個都是automake+autoconf了,我們自己也長進一點吧。手工編寫MakeFile.am,讓auotomake變成MakeFile.in,再讓用戶./configure 生成最終的MakeFile。
   
    生成的MakeFile既能跨越平臺,又是標準的寫法,最重要的是,編寫MakeFile.am的工作量比MakeFile少多了,只要簡單的定義目標文件,先要處理的子目錄,需要的源文件,頭文件與庫文件就可以了。如果看完下面兩篇還是不懂,直接看ACE裏的Makefile.am就懂了。

    入門文章:使用AutoMake輕鬆生成Makefile
    進階文章:IBM DW:例解 autoconf 和 automake 生成 Makefile 文件
    完整的免費電子書: GNU Autoconf, Automake and Libtool

    另外,ACE裏還貢獻了一個更厲害的MPC(Makefile, Project, and Workspace Creator ),  自動的生成了MakeFile.am或者VC的項目文件。

 

    附錄A:我的VI易忘命令手冊
    上下左右:
    ctrl+u/d 上下半屏,ctrl+f/b,上下一屏
    H/G屏幕頭/文章末 ,0/$ 行首行末
   
    增刪改:
    yy/dd 複製/刪除 一行,p/P:將yy/dd的內容paste出來
    I/A 在行首/末添加, o/O 開新行,d0/d$ 刪除到行首,行末
    u:undo

    查:
    ? 向前查找, n/N 重複上一次查找

附錄B: 文本處理命令小結
   awk:處理結構化的文本(每行以固定符號分成若干列),提取打印某些字段,如:
    ls -l|awk '{print $1}'  --將ls-l結果的第一列打印出來
    awk -F":" '{print $1"  "$6}' /etc/passwd ,將以:分割的/etc/passwd文件的第1,6列打印出來,中間以空格分開
    詳見IBM DW中國的AWK實例(共3篇) 或 Bash新手指南中文版第6章。

    grep:過濾,大家用得最多的命令,支持正則表達式。參數有:
    -i忽略大小寫,-n顯示line number,-c 統計在每個文件的出現次數,-l只顯示符合的文件的名字。

    sed:流編輯器,主要用於替換,如:
    sed -e '1,10s/foo/bar/g' myfile2.txt 將1到10行的文本中的foo 替換成bar,s代表替換,g代表全局替換
    支持正則的替換字符串,可以只替換某個範圍內的內容。
    用法不算簡單,詳見IBM DW中國的Sed實例(共3篇)或 Bash新手指南中文版第5章。
   
    sort:排序,參數有:
    -r逆序, -n 數字比較 , -M 日曆比較 Feb,Dec, -f 忽略大小寫
    同樣支持結構化文件,如
    sort -t : -k 1,1 /etc/passwd,以: 分割,只按第1列排序
    sort -t : -k 1,1 -k2.2,3.4 /etc/passwd ,以:分割,先按第1列排序,再按第2列的第二個字符到第3列的第4個字符排序。

    uniq:去除重複行。
    除了正常用法外,還有-c統計重複次數,和-u (唯一)和 -d (重複)兩個參數,只顯示唯一的和重複的行。

    wc: 統計。
    -l 行,-m 字符,-w 單詞

PS:以下文字不算字數
     一個多月沒有更新博客了,因爲公司裏調了新部門,很多東西要學習。太久沒試過華麗的在上班時間,在工作中,在同事們身上學到這麼多東西了,很是開心。
     下週開始爆發更新。

 


Trackback: http://tb.blog.csdn.net/TrackBack.aspx?PostId=1495778

一、#include “filename.h”和#include 的區別

#include “filename.h”是指編譯器將從當前工作目錄上開始查找此文件

#include 是指編譯器將從標準庫目錄中開始查找此文件

 

二、頭文件的作用

加強安全檢測

通過頭文件可能方便地調用庫功能,而不必關心其實現方式

 

三、* , &修飾符的位置

對於*和&修飾符,爲了避免誤解,最好將修飾符緊靠變量名

 

四、if語句

不要將布爾變量與任何值進行比較,那會很容易出錯的。

整形變量必須要有類型相同的值進行比較

浮點變量最好少比點,就算要比也要有值進行限制

指針變量要和NULL進行比較,不要和布爾型和整形比較

 

五、const和#define的比較

const有數據類型,#define沒有數據類型

個別編譯器中const可以進行調試,#define不可以進行調試

在類中定義常量有兩種方式

1、 在類在聲明常量,但不賦值,在構造函數初始化表中進行賦值;

2、 用枚舉代替const常量。

 

六、C++函數中值的傳遞方式

有三種方式:值傳遞(Pass by value)、指針傳遞(Pass by pointer)、引用傳遞(Pass by reference)

void fun(char c) //pass by value

void fun(char *str) //pass by pointer

void fun(char &str) //pass by reference

如果輸入參數是以值傳遞的話,最好使用引用傳遞代替,因爲引用傳遞省去了臨時對象的構造和析構

函數的類型不能省略,就算沒有也要加個void

 

七、函數體中的指針或引用常量不能被返回

Char *func(void)

{

char str[]=”Hello Word”;

//這個是不能被返回的,因爲str是個指定變量,不是一般的值,函數結束後會被註銷掉

return str;

}

函數體內的指針變量並不會隨着函數的消亡而自動釋放

 

八、一個內存拷貝函數的實現體

void *memcpy(void *pvTo,const void *pvFrom,size_t size)

{

assert((pvTo!=NULL)&&(pvFrom!=NULL));

byte *pbTo=(byte*)pvTo; //防止地址被改變

byte *pbFrom=(byte*)pvFrom;

while (size-- >0)

pbTo++ = pbForm++;

return pvTo;

}

 

九、內存的分配方式

分配方式有三種,請記住,說不定那天去面試的時候就會有人問你這問題

1、 靜態存儲區,是在程序編譯時就已經分配好的,在整個運行期間都存在,如全局變量、常量。

2、 棧上分配,函數內的局部變量就是從這分配的,但分配的內存容易有限。

3、 堆上分配,也稱動態分配,如我們用new,malloc分配內存,用delete,free來釋放的內存。

 

十、內存分配的注意事項

用new或malloc分配內存時,必須要對此指針賦初值。

用delete 或free釋放內存後,必須要將指針指向NULL

不能修改指向常量的指針數據

 

十一、內容複製與比較

//數組……

char a[]=”Hello Word!”;

char b[10];

strcpy(b,a);

if (strcmp(a,b)==0)

{}

//指針……

char a[]=”Hello Word!”;

char *p;

p=new char[strlen(a)+1];

strcpy(p,a);

if (strcmp(p,a)==0)

{}

 

十二、sizeof的問題

記住一點,C++無法知道指針所指對象的大小,指針的大小永遠爲4字節

char a[]=”Hello World!”

char *p=a;

count<
count<
而且,在函數中,數組參數退化爲指針,所以下面的內容永遠輸出爲4

void fun(char a[1000])

{

count<
}

 

十三、關於指針

1、 指針創建時必須被初始化

2、 指針在free 或delete後必須置爲NULL

3、 指針的長度都爲4字節

4、釋放內存時,如果是數組指針,必須要釋放掉所有的內存,如

char *p=new char[100];

strcpy(p,”Hello World”);

delete []p; //注意前面的[]號

p=NULL;

5、數組指針的內容不能超過數組指針的最大容易。

如:

char *p=new char[5];

strcpy(p,”Hello World”); //報錯 目標容易不夠大

delete []p; //注意前面的[]號

p=NULL;

 

十四、關於malloc/free 和new /delete

l malloc/free 是C/C+的內存分配符,new /delete是C++的內存分配符。

l 注意:malloc/free是庫函數,new/delete是運算符

l malloc/free不能執行構造函數與析構函數,而new/delete可以

l new/delete不能在C上運行,所以malloc/free不能被淘汰

l 兩者都必須要成對使用

l C++中可以使用_set_new_hander函數來定義內存分配異常的處理

 

十五、C++的特性

C++新增加有重載(overload),內聯(inline),Const,Virtual四種機制

重載和內聯:即可用於全局函數,也可用於類的成員函數;

Const和Virtual:只可用於類的成員函數;

重載:在同一類中,函數名相同的函數。由不同的參數決定調用那個函數。函數可要不可要Virtual關鍵字。和全局函數同名的函數不叫重載。如果在類中調用同名的全局函數,必須用全局引用符號::引用。

覆蓋是指派生類函數覆蓋基類函數

函數名相同;

參數相同;

基類函數必須有Virtual關鍵字;

不同的範圍(派生類和基類)。

隱藏是指派生類屏蔽了基類的同名函數相同

1、 函數名相同,但參數不同,此時不論基類有無Virtual關鍵字,基類函數將被隱藏。

2、 函數名相同,參數也相同,但基類無Virtual關鍵字(有就是覆蓋),基類函數將被隱藏。

內聯:inline關鍵字必須與定義體放在一起,而不是單單放在聲明中。

Const:const是constant的縮寫,“恆定不變”的意思。被const修飾的東西都受到強制保護,可以預防意外的變動,能提高程序的健壯性。

1、 參數做輸入用的指針型參數,加上const可防止被意外改動。

2、 按值引用的用戶類型做輸入參數時,最好將按值傳遞的改爲引用傳遞,並加上const關鍵字,目的是爲了提高效率。數據類型爲內部類型的就沒必要做這件事情;如:

將void Func(A a) 改爲void Func(const A &a)。

而void func(int a)就沒必要改成void func(const int &a);

3、 給返回值爲指針類型的函數加上const,會使函數返回值不能被修改,賦給的變量也只能是const型變量。如:函數const char*GetString(void); char *str=GetString()將會出錯。而const char *str=GetString()將是正確的。

4、 Const成員函數是指此函數體內只能調用Const成員變量,提高程序的鍵壯性。如聲明函數 int GetCount(void) const;此函數體內就只能調用Const成員變量。

Virtual:虛函數:派生類可以覆蓋掉的函數,純虛函數:只是個空函數,沒有函數實現體;

 

十六、extern“C”有什麼作用?

Extern “C”是由C++提供的一個連接交換指定符號,用於告訴C++這段代碼是C函數。這是因爲C++編譯後庫中函數名會變得很長,與C生成的不一致,造成C++不能直接調用C函數,加上extren “c”後,C++就能直接調用C函數了。

Extern “C”主要使用正規DLL函數的引用和導出 和 在C++包含C函數或C頭文件時使用。使用時在前面加上extern “c” 關鍵字即可。

 

十七、構造函數與析構函數

派生類的構造函數應在初始化表裏調用基類的構造函數;

派生類和基類的析構函數應加Virtual關鍵字。

不要小看構造函數和析構函數,其實編起來還是不容易。

#include

class Base

{

public:

virtual ~Base() { cout<< "~Base" << endl ; }

};

class Derived : public Base

{

public:

virtual ~Derived() { cout<< "~Derived" << endl ; }

};

void main(void)

{

Base * pB = new Derived; // upcast

delete pB;

}

輸出結果爲:

~Derived

~Base

如果析構函數不爲虛,那麼輸出結果爲

~Base

 

十八、#IFNDEF/#DEFINE/#ENDIF有什麼作用

仿止該頭文件被重複引用


 

 

一、sizeof的概念 
  sizeof是C語言的一種單目操作符,如C語言的其他操作符++、--等。它並不是函數。sizeof操作符以字節形式給出了其操作數的存儲大小。操作數可以是一個表達式或括在括號內的類型名。操作數的存儲大小由操作數的類型決定。 

二、sizeof的使用方法 
  1、用於數據類型 

  sizeof使用形式:sizeof(type) 

  數據類型必須用括號括住。如sizeof(int)。 

  2、用於變量 

  sizeof使用形式:sizeof(var_name)或sizeof var_name 

  變量名可以不用括號括住。如sizeof (var_name),sizeof var_name等都是正確形式。帶括號的用法更普遍,大多數程序員採用這種形式。 

  注意:sizeof操作符不能用於函數類型,不完全類型或位字段。不完全類型指具有未知存儲大小的數據類型,如未知存儲大小的數組類型、未知內容的結構或聯合類型、void類型等。 

  如sizeof(max)若此時變量max定義爲int max(),sizeof(char_v) 若此時char_v定義爲char char_v [MAX]且MAX未知,sizeof(void)都不是正確形式。 

三、sizeof的結果 
  sizeof操作符的結果類型是size_t,它在頭文件中typedef爲unsigned int類型。該類型保證能容納實現所建立的最大對象的字節大小。 

  1、若操作數具有類型char、unsigned char或signed char,其結果等於1。 

  ANSI C正式規定字符類型爲1字節。 

  2、int、unsigned int 、short int、unsigned short 、long int 、unsigned long 、 float、double、long double類型的sizeof 在ANSI C中沒有具體規定,大小依賴於實現,一般可能分別爲2、2、2、2、 4、4、4、8、10。 

  3、當操作數是指針時,sizeof依賴於編譯器。例如Microsoft C/C++7.0中,near類指針字節數爲2,far、huge類指針字節數爲4。一般Unix的指針字節數爲4。 

  4、當操作數具有數組類型時,其結果是數組的總字節數。 

  5、聯合類型操作數的sizeof是其最大字節成員的字節數。結構類型操作數的sizeof是這種類型對象的總字節數,包括任何墊補在內。 

  讓我們看如下結構: 

  struct {char b; double x;} a; 

  在某些機器上sizeof(a)=12,而一般sizeof(char)+ sizeof(double)=9。 

  這是因爲編譯器在考慮對齊問題時,在結構中插入空位以控制各成員對象的地址對齊。如double類型的結構成員x要放在被4整除的地址。 

  6、如果操作數是函數中的數組形參或函數類型的形參,sizeof給出其指針的大小。 

四、sizeof與其他操作符的關係 
  sizeof的優先級爲2級,比/、%等3級運算符優先級高。它可以與其他操作符一起組成表達式。如i*sizeof(int);其中i爲int類型變量。 

五、sizeof的主要用途 
  1、sizeof操作符的一個主要用途是與存儲分配和I/O系統那樣的例程進行通信。例如: 

  void *malloc(size_t size), 

  size_t fread(void * ptr,size_t size,size_t nmemb,FILE * stream)。 

  2、sizeof的另一個的主要用途是計算數組中元素的個數。例如: 

  void * memset(void * s,int c,sizeof(s))。 

六、建議 
  由於操作數的字節數在實現時可能出現變化,建議在涉及到操作數字節大小時用sizeof來代替常量計算。

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