C\C++|指針詳述及實例分析

指針是C語言中的精華,也是一把雙刃劍,關係到安全和效率。

1 系統內存佈局

2 存儲變量的內存地址

3 指針定義:變量,地址,類型(寬度)

5 &與*運算符

6 定義指針與解引用

7 指針初始化

8 指針指向類型長度計算:sizeof(*p)

9 void* 類型指針

10 指針應用:判斷系統大小端

11 指針加減運算

12 常量指針與指針常量

13 數組名是一const指針

14 指針與數組關係

15 字符指針

16 二級指針

17 函數指針與指針函數

18 數組指針與指針數組

19 函數不要返回局部變量的指針

20 指針與引用

21 指針引用做函數參數

22 返回指針和指針引用

23 指針使用注意事項

1 系統內存佈局

指針其實就是一個變量,和其他類型的變量一樣,在32位機器上,它佔用四字節(64位上佔8個字節),它與其他變量的不同就在於它的值是一個內存地址,指向內存的另外一個地方。

以X86的32位系統為例,如下圖所示,系統的內存虛擬地址範圍為4GB(0x0-0xFFFFFFFF)。 其中低2GB主要為應用程序使用(Ring3級別),而高2GB為系統內核使用(Ring0級別):

C\\C++|指針詳述及實例分析

需要注意的是,程序在執行時,傳遞給CPU的地址是邏輯地址,它由兩部分組成,一部分是段選擇符(比如cs和ds等段寄存器的值), 另一部分為有效地址(即偏移量,比如eip寄存器的值)。邏輯地址必須經過映射轉換變為線性地址, 線性地址再經過一次映射轉為物理地址,才能訪問真正的物理內存。

2 存儲變量的內存地址

變量是存放在內存中的,比如下圖中的變量i和a,分別對應一塊內存單元首地址的一個命名,變量i和a的地址,可以用&取址運算符獲得。

而對於指針p來說,它本身也是一個變量,存放在內存中,只不過它的值是一個內存地址,這個內存地址,可以是其它變量的地址。

如:

<code>int i=10;
int *p = &i; // i的地址賦值給p,也就是指針p指向變量i:p→i
int a=100;/<code>
C\\C++|指針詳述及實例分析

如上圖所示,內存地址空間是一個線性空間。

數據類型除了表示可以執行的操作(可以使用的運算符)、編碼與解碼格式以外,還用來表示需要內存空間的長度,如上面的int就是使用4個字節的內存空間,變量a是這個內存空間地址的命名,在C/C++,變量名做右值取得的是其內存空間的二進制位組成的值按數據類型編碼規則解析出來的值(變量名做左值可以更新其內存空間的值),其地址可以通過“&"加變量解析出來。int*p表示p是指向一個int大小的內存空間。

內存的地址可以分為有效地址,即這個所對應的內存是可訪問的;還有無效地址,訪問無效地址,會導致程序崩潰,比如NULL地址就是一個無效地址。

3 指針定義:變量、地址、類型(寬度)

指針其實就是一個變量,和其他類型的變量一樣。它與其他變量的不同就在於它的值是一個內存地址,指向內存的某一個地方。即指針是一種存放另一個變量的地址的變量。

指針含義可以分為3個方面來理解:

I 它是一個變量,所以也佔用一定的內存空間(在X86上佔用4個字節,X64上佔用8個字節)

II 它的值是一個內存地址。這個地址可以是其它變量的地址。

III 它的地址指向的內存空間具有確定的長度。這是指針與地址的本質區別。如果只告訴你一個內存地址,你不會知道從這個地址開始的內存有多長。但如果告訴你一個指針,你會明確的知道從這個內存地址開始的內存有多長。因為指針都是有類型的。知道了指針的類型,就確定了所指向的內存地址對應的長度。

C\\C++|指針詳述及實例分析

指針聲明如同變量聲明一樣,只是多了一個“*”:

<code>char *pch;
int *pi;
float *pf;
double *pd;/<code>

當然需要類型,表示這個地址可以解析的內存空間長度。

其右值只能是一個有效的內存地址:

<code>char *pch = "abc"; // "abc"存儲在靜態區,對應一個內存地址
int i = 0;
int *p = &i; // “&”可以取得變量的地址/<code>

5 &與*運算符

因為變量是內存地址的一個可以理解的命名標識,這個標識的直接使用是值的訪問與更新。通過這個標識我們可以取得其地址,C/C++中,可以使用“&”(取址運算符)來獲取某個變量的地址,比如:

<code>int i = 1;
int *p = &i; // *這裡是指針聲明,&這裡是取址/<code>

*p中,p必須是有效的地址,否則會引發程序崩潰。比如:

int *p = NULL;

*p = 0;//此時,p無NULL地址,會引發程序異常

在編程語言中,許多情況下一個符號會有多種用途,取決於上下文,如“*”即可用於定義指針,又可用於解引用:

<code>int i = 1;
int *p = &i; //p is reference of i, 這裡把p當做是對i的一個引用(reference)
*p += 1;
// *p is dereference for i ,*p equals i,*p代表的就是i
// 把p所指向的地址的內存(i)的值加1/<code>

“*”既是指針聲明符,又是解引用(dereference)運算符,與&運算符互為逆運算。

“*“寫在數據類型之後時表示聲明指針變量,“*“前沒有數據類型時表示解引用。

7 指針初始化

當定義了一個指針,有多種方法進行初始化(賦值):

<code>int i, *p;//聲明瞭一個整型變量i,一個指針p
p= &i;/<code>

② 聲明與賦值分開進行並先賦NULL值

<code>int c;
int *p = NULL;// 聲明瞭一個指針p,並初始化為NULL
p=&c;//將指針p指向變量c/<code>

③ 聲明與賦值同時進行,也就是聲明即初始化

<code>int  d;
int *p = &d; // 聲明瞭一個指針p,並直接初始化為變量d的地址/<code>
<code>char *p = (char *)malloc(100*sizeof(char));//聲明瞭一個字符指針p,並初始化為堆上的一個地址
char *str = “hello world”;//聲明瞭一個字符指針str,並初始化為字符串的首地址
char c=‘A’;
char *str = &c;//聲明瞭一個指針str並直接初始化為變量c的地址
char *pch = &c;/<code>

用於指針初始化的右值的取值空間為:0x0000FFFF-ox7FFF0000,這個值不能由程序員直接給出,因為操作系統統一管理各程序的內存空間,程序中使用的是虛擬內存並通過虛擬內存地址來訪問數據和代碼的,操作系統再將虛擬內存地址映射成為實際的物理內存的,所以合法的地址只能是通過定義變量或由此產生的偏移來獲取,可直接通過malloc()或new申請動態內存來獲取。

8 指針指向類型長度計算:sizeof(*p)

指針的長度(在32位機器系統上)為4。

<code>double x=3.14;
double* p = &x;
int n = sizeof(p); // 4/<code>

當使用sizeof()和數組名(常量指針)求數組的長度時,求的是這個數組整個空間的長度,等於元素個數乘以單個元素大小,字符串數組的長度必須包含字符串的結束標誌符’\\0’。(關於數組名做函數參數時退化為指針的細節,見後續)

char *p1 = “Hello, word!” // 字面量 “Hello, word!”存儲在靜態區.rdata段

p1為字符串指針,所以sizeof (p1) = 4。

char p2[] = “Hello, world”

p2為字符數組並初始化為”Hello, world”。由於字符串的存儲特點,總是以’\\0’做為結束標誌,因此上面的字符串等價於下面的數組:char p2[] = {‘h’, ‘e’, ‘l’,’l’,’o’, ‘ ‘, ‘w’,’o’,’r’,’l’,’d’,’\\0’},必須包含字符串的結束標誌符’\\0’,所以sizeof (p2) = 13。

char p3[] = {‘h’, ‘e’, ‘l’,’l’,’o’, ‘ ‘, ‘w’,’o’,’r’,’l’,’d’}

p3為字符數組,並由12個字符初始化,所以sizeof (p3) = 12。

注意,strlen(p)計算的是字符串中有效的字符數(不含’\\0’)。所以strlen(p)的值為12。考察下面拷貝字符串的代碼,看看有什麼問題沒呢?

<code>char *str = “Hello, how are you!”;
char *strbak = (char *)malloc(strlen(str)); // ?
if (NULL == strbak)
{
//處理內存分配失敗,返回錯誤
}
strcpy(strbak, str);
....../<code>

顯然,由於strlen()計算的不是str的實際長度(即不包含’\\0’字符的計算),所以strbak沒有結束符’\\0’,而在C語言中,’\\0’是字符串的結束標誌,所以是必須加上的,否則會造成字符串的溢出。所以上面的代碼第二句應該是:

char *strbak = (char *)malloc(strlen(str)+1);

既然在這裡談到了sizeof,現在我們就把sizeof運算在下面做一個系統的總結:

1)參數為數據類型或者為一般變量

例如sizeof(int),sizeof(double)等等。這種情況要注意的是不同系統或者不同編譯器得到的結果可能是不同的。例如int類型在16位系統中佔2個字節,在32位系統中佔4個字節。

2)參數為數組或指針

int a[50]; //sizeof(a)=4*50=200; 數組所佔的空間大小為200字節。

注意數組做函數參數時,在函數體內計算該數組參數則等同於計算指針的長度。

int *a=new int[50]; // sizeof(a)=4; a為一個指針,sizeof(a)是求指針的大小,在32位系統中,當然是佔4個字節。

3)參數為結構或類。

sizeof應用在類和結構的處理情況是相同的。有兩點需要注意,第一、結構或者類中的靜態成員不對結構或者類的大小產生影響,因為靜態變量的存儲位置與結構或者類的實例地址無關。第二、沒有成員變量的結構或類的大小為1,因為必須保證結構或類的每一個實例在內存中都有唯一的地址。關於更多的結構的sizeof大小計算,請考慮數據對齊。

9 void* 類型指針

我們可以使用void*來定義個void *類型的指針:

void *p;

p是void *類型指針,其他類型指針隱式轉換成該類型,不能直接使用*p來取值,必須先轉換為特定類型再做取值

p可以接受任何類型的指針賦值。

p賦值給其它類型的指針,需要強轉。

p不能進行解引用*運算,必須先轉換。

比如:

<code>int i = 10;
char ch = ‘a’;
int *p1 = &i;
char *p2 = &ch;
void *pv1 = p1; // 把p1賦值給pv1,不需強轉,不能使用*pv1
void *pv2 = p2; // 把p2賦值給pv2,不需強轉,不能使用*pv2
int *p3 = (int *)pv1; // 把pv1賦值給p3,需要強轉
char *p4 = (char *)pv2; // 把pv2賦值給p4,需要強轉/<code>

一定條件下,void*可以讓數據類型有一定的泛型特徵,因為C/C++是強類型語言,如標準庫中的很多函數便以void*為參數:

<code>void *memcpy(void *dst, void *src, size_t len);
void qsort (void* base, size_t num, size_t size,

int (*compar)(const void*,const void*));/<code>

10 指針應用:判斷系統大小端

對於int x=0x1; 在小端系統中,低位存放整數的低位,因此低地址的第一個字節的值為01。而大端系統中,低位存放整數的高位,因此低地址的第一個字節為00。如果把這個內存地址所在的第一個字節取出來,就可以區別系統是小端還是大端了。

C\\C++|指針詳述及實例分析

<code>/*return value :0—big-endian ;1—little-endian*/
int get_endian()
{

int x=0x1;
char *p=(char*)&x; // 取x的地址並將這個地址轉換為char*類型,這樣就取出了一個字節的地址
return *p ;
}
int main(void)
{
printf(”The platform %s \\n ”,get_endian() ? ”is little-endian”:”is big-endian”);
return 0;
}/<code>

11 指針加減運算與移動

指針運算一般只有算術和有限的比較運算。通過一個指針與一個整數的加減,形成的內存地址變化或偏移可以視為指針的移動。

指針移動,最常用的就是“++”運算符了,下述表達式注意與解引用區分:

<code>*p++; // *p,p++
(*p)++; // (*p)++,即*p = *p+1或者*p += 1;
b=*p++; // b=*p; p++
b=(*p)++; // b=*p; (*p)+=1;
b=++*p; // (*p)+=1; b=*p;
b=++(*p); // (*p)+=1; b=*p;
b=*++p; // p+=1; b=*p;
b=*(++p); // p+=1; b=*p/<code>

如以下利用指針移動和兩個指針的差值求字符串長度的函數:

<code>int len(char* str)
{
\tchar* pMove=str;
\twhile(*pMove!='\\0')
\t\tpMove++;
\treturn pMove-str;
}/<code>

對於鏈表,其移動稍有區別:

<code>struct node\t\t\t\t\t//定義結點結構類型
{
\tchar data;\t\t\t\t//用於存放字符數據
\tnode *next;\t\t\t\t//用於指向下一個結點(後繼結點)
};

void Showlist(node *head)
{
\tnode *pRead=head;\t\t\t//訪問指針一開始指向表頭
\tcout<\twhile (pRead!=NULL)\t\t\t//當訪問指針存在時(即沒有達到表尾之後)
\t{
\t\tcout<<pread->data;\t\t//輸出當前訪問結點的數據
\t\tpRead=pRead->next;\t\t//訪問指針向後移動(指針偏移)
\t}
\tcout<<endl>}/<endl>/<pread->/<code>

12 常量指針與指針常量

當const修飾指針聲明時,根據const的位置不同,const可以修飾指針本身,也可以修飾指針指向的內容:

1)const int *a; // 指針常量,指針指向的變量不能改變值

2)int const *a; // 指針常量,與const int *a等價

3)int * const a; // 常量指針,指針本身不能改變值

4)const int * const a; // 兩者均為常量

小技巧:以“*”為分界,const靠近哪部分,哪部分為常量

13 數組名是一const指針

數組名所代表的值就是數組的首地址,一旦定義了數組之後,數組名所代表的值就不能再改變。從指針的角度來看,數組名就是一個常量指針,比如:

int a[10];

那麼a就是一個常量指針,即:int *const a。因此,不能再用其它的值賦值給a。因為a是常量。

在計算數組長度的時候,我們需要注意數組作為函數的參數,將退化為指針,所以,其長度大小為指針的長度。現在我們來看下面這段代碼:

<code>int a[10];         // sizeof (a) = 10*sizeof (int) = 40;

int a[10];
void func(int a[], int n)
{
printf(“%d”, sizeof (a)); // 此時數組退化為指針,所以 sizeof (a) = 4
}/<code>

下面來看以下有問題的代碼:

<code>void UpperCase( char str[] ) // 將 str 中的小寫字母轉換成大寫字母
{
for( size_t i=0; i<sizeof> if( 'a'<=str[i] && str[i]<='z' )
str[i] -= ('a'-'A' ); // str[i]^=32;
}

char str[] = "aBcDe";
cout << "str字符長度為: " << sizeof(str)/sizeof(str[0]) << endl;
UpperCase( str );
cout << str << endl;/<sizeof>/<code>

分析:函數內的sizeof有問題。根據語法,sizeof如用於數組,只能測出靜態數組的大小,無法檢測動態分配的或外部數組大小。函數外的str是一個靜態定義的數組,因此其大小為6,函數內的str實際只是一個指向字符串的指針,沒有任何額外的與數組相關的信息,因此sizeof作用於上只將其當指針看,一個指針為4個字節,因此返回4。

數組名雖然代表了數組的首地址,雖然a與&a的值一樣,都是數組的首地址,但是,a與&a在特定的上下文中的含義並不一樣。對於一維數組來說:

int a[10];

&a+1中的1代表的是整個數組的長度10*sizeof(int);

a+1中的1代表的是一個元素的長度sizeof(int)。

&a[0]+1中的1也代表的是一個元素的長度。

再看下面實例:

<code>#include   <stdio.h> 

int main(void)
{
\tint a[5] = {1,2,3,4,5};
\tint *ptr1 = (int *)(&a+1); // &a表示含有長度信息的數組變量,+1的1相當於數組長度

\tint *ptr2 = (int *)((int)a+1); // 首地址a的下一個字節的地址
\t // 數組按小端存儲是這樣的:10002000(1個數字表示一個字節)
\tprintf("%x\\n%x\\n",ptr1[-1],*ptr2);// 0002的十六進制就是02000000
\tprintf("%x\\n%x\\n",a,ptr2);
\tgetchar();
\treturn 0;
}
/*output:
5
2000000
12ff34
12ff35
*//<stdio.h>/<code>

前面已經提到,指針加減法運算,後面的數字表示指針指向的數據類型的大小的倍數。比如&a+1,其中的1就表示指針向前移動1*sizeof(&a)那麼多的字節。而&a表示整個數組,所以ptr1 = (int *)(&a+1),ptr1指到了數組的末尾位置。因為ptr1[-1]即為*((int*)ptr1-1),即指針ptr1向低地址移動sizeof(int)個字節,即向後移動4個字節,正好指到a[4]的位置,所以ptr1[-1]為5。

C\\C++|指針詳述及實例分析

對於語句*ptr2 =(int *)((int)a+1),在這裡,我們已經將指針a(地址值)強制轉換成了整型,a+1不是指針運算了。(int *)((int)a+1)指向了首地址的下一個字節,如上圖所示。

所以,*ptr2所代表的整數(四個字節,小端存儲),我們從上圖可以看出是:2000000。

對於多維數組來說:

int a[5][10];

a和&a都是數組a[5][10]的首地址。那麼它們有什麼不同呢?實際上,它們代表的類型不同。a是int a[10]的類型,而&a則是a[5][10]的類型。大家知道,指針運算中的“1”代表的是指針類型的長度。所以a+1和&a+1中的1代表的長度分別為a的類型a[10]即sizeof (int) * 10 和&a的類型a[5][10]即sizeof (int)*10*5。

看下面實例:

<code>#include <stdio.h>
int main(void)
{
int a[5][10];
printf("%d\\n", int(a+1)-int(a)); // 40
\t printf("%d\\n", int(&a+1)-int(a));// 200
\t getchar();
return 0;
}/<stdio.h>/<code>

大家知道,指針運算中的“1”代表的是指針類型的長度。所以a+1和&a+1中的1代表的長度分別為a的類型a[10],即sizeof (int) * 10 。

&a的類型a[5][10]即sizeof (int)*10*5。

更抽象點的說,如果定義一個數組int a[M1][M2][…][Mn],那麼a + 1 = a首地址+M2*M3*…*Mn *sizeof (int);而&a + 1 = a首地址 + M1*M2*…*Mn*sizeof (int)。

14 指針與數組關係

現在大家已經明白,數組名,其實是一個常量指針:

int a[10];

a的類型為:int * const a;//a是常量指針

因此在訪問數組元素的時候:

a[i], 與*(a+i)都可以訪問第i個元素的值。而&a[i]與a+i都是第i個元素的地址。同樣,我們也可以定義一個整數指針pa指向數組的首地址:

int *pa=&a[0];

int *pa=a;

因此pa+i也是第i個元素的地址,而*(pa+i)和pa[i]引用的也是a[i]的值。

15 字符指針

字符指針的定義是:

char *p;

字符指針,既可以指向字符變量,也可以指向字符串(其實就是字符串中首字符的地址)。比如:

char *str=“hello world”;// 這裡str是一個字符指針,它是”hello world”字符串中首字符’h’的地址。

因為字符串是以’\\0’結尾的,所以可以通過字符指針來遍歷字符串:

<code>while(*str!=‘\\0’)
{
            printf(“%c”, *str);
            str++;
}/<code>

字符指針也可以指向某個字符變量,比如:

<code>char ch=‘a’;
char *pch=&ch;/<code>

C的字符串用一個以'\\0'結尾的字符數組來表示。注意以下初始化方式的細微區別:

<code>#include   <stdio.h> 
char* func1()
{
\tchar arr[] = "abc"; // "abc"等同於{'a','b','c','\\0'}
\treturn arr; // error,arr是一局部變量地址
}
char* func2()
{
\tchar* arr2 = "abcd"; // 存儲於"abcd"靜態區.rdata段
\treturn arr2; // ok,arr2是一靜態區.rdata段的地址
}
int main(void)
{
\tprintf("%s\\n",func1()); // 隨機值
\tprintf("%s\\n",func2());
\tgetchar();
\treturn 0;
}

/*output:
?@
abcd
*//<stdio.h>/<code>

16 二級指針

所謂二級指針,就是指向指針的指針,即該指針的值是另外一個一級指針的地址。與此類似,如果一個指針中存放的是二級指針的地址,那麼該指針就是三級指針,與此類推。

<code>char c;
char *pch = &c;       //pch為一級指針
char **ppch = &pch; //ppch為二級指針,存放這一級指針的地址
printf(“%c”, **ppch);
printf(“%p,%p,%p”, pch, ppch, *ppch);/<code>


C\\C++|指針詳述及實例分析

如上圖所示,pch是一級指針,存放著變量c的地址;ppch是二級指針,存放這一級指針pch的地址。只要畫出了上面的關係圖,那麼一次*運算,就是向右移動一次,兩次*運算,就是往右移動兩次,即*pch即為c,*ppch為pch,**ppch即為c。

17 函數指針與指針函數

函數指針是指指向一個函數的指針,指針函數是指返回一個指針的函數。

17.1 函數指針

函數名,就是函數的首地址。如果一個指針變量,存放的是函數的地址,那麼就把這個指針叫做函數指針。定義函數指針有2中形式:

第一種,首先用typdef定義出函數指針的類型,然後,通過函數指針類型來定義函數指針。

第二種,直接用函數的簽名來定義函數指針。

<code>void print_int(int x)
{
         printf("hello, %d\\n", x);
}
typedef void (*F)(int x);//此處定義了一個函數指針類型F
int main(void)
{
         int a =100;

         void (*f1)(int x);
         f1= print_int;//f1是指針定義出來的函數指針,把函數print_int賦值給f1
         f1(a);
         F f2= print_int; // f2是通過函數指針類型F定義出來的函數指針,把print_int賦值給f2。
         f2(a);
         print_int(a);
         return 0;
}/<code>

聲明函數指針,通常的做法是,先聲明一個函數,然後將函數名改為函數指針名,再加一個“*”號,用括號括起來即可。

函數指針通常用做函數參數:

<code>#include <iostream>
using namespace std;

int ascend(int a,int b)
{
return a>b;
}

int descend(int a,int b)
{
return a}

typedef int (*pfunc)(int,int);
void sort(int arr[], int size,pfunc comp)
{
for(int i=0;i<size> for(int j=0;j<size-i-1> if(comp(arr[j],arr[j+1]))
{
int t=arr[j];
arr[j]=arr[j+1];
arr[j+1]=t;
}
}
void printArr(int arr[],int size)
{
for(int i=0;i<size> cout< cout<<endl>}


int main()
{
int arr[]={3,9,9,2,8,5,3,4};
int size = sizeof(arr)/sizeof(arr[0]);
sort(arr,size,ascend);
printArr(arr,size);
sort(arr,size,descend);
printArr(arr,size);
cin.get();
return 0;
}
/*output:
2 3 3 4 5 8 9 9
9 9 8 5 4 3 3 2
*//<endl>
/<size>/<size-i-1>/<size>
/<iostream>/<code>

17.2 指針函數

指針函數即返回指針的函數。比如下面的代碼中,我們嘗試著調用get_memory()獲取一個內存,用來存放“hello world“這個字符串,那麼就可以將get_memory()設置成為一個返回指針的函數:

<code>char *get_memory();
int main(void)
{
    char *p = NULL;//p是指針,做為實參,初始值為NULL
    p=get_memory();//通過該函數,為p分配一塊內存。如何定義get_memory函數?
    strcpy_s(p, 100,”hello world”);
    printf(“%s\\n”, p);
    free(p);
    p=NULL;
    return 0;
}
char *get_memory()
{
    return (char *)malloc(100);

}
// 上面代碼要注意malloc()與free()匹配的問題,因為分散在不同的函數中/<code>

注意:指針函數不能返回局部變量的指針(地址),只能返回堆上內存的地址,或者函數參數中的內存地址以及全局變量或靜態變量的地址。因為局部變量存放在棧上,當函數運行結束後,局部變量就被銷燬了,這個時候返回一個被銷燬的變量的地址,調用者得到的就是一個野指針。

18 指針數組與數組指針

與“指針數組”和“數組指針”類似的有“函數指針”與“指針函數”,“常量指針”與“指針常量”。這些概念都符是偏正關係,所以指針數組其實就是數組,裡面存放的是指針;數組指針就是指針,這個指針指向的是數組;

函數指針就是指針,這個指針指向的是函數,指針函數就是函數,這個函數返回的是指針;常量指針就是指針,只不過這個指針是常量的,不能再修改值指向別的地方;指針常量,就是指指針本身不是常量指針指向的內存是常量,不能修改。

<code>int *a[10];         //指針數組
int (*a)[10]; //數組指針

int (*a)(int); //函數指針
int *a(int); //指針函數,返回指針的函數
int (*a[10])(int); //函數指針數組。注意:*與[]的優先級來判斷這組的區別/<code>

理解上述聲明的含義,關鍵是要明白[],*,和()(這裡的()不是函數聲明的90)運算符的優先級:() > [] > *。比如int *a[10],由於[]的運算級別高於*。掌握了這一點,再按下面的思路去分析就行了:

I 找括號(函數參數的括號通常寫在最後,除外),做為核心,其它部分是修飾;如char* (*pf) (int i); 核心是指針,其它是修飾,所以是函數指針。

II 優先級高的是核心,其它是修飾,如char* arr[12]; 核心是數組,指針修飾數組,所以是指針數組。

所以現在來分析int (*a[10])(int);就簡單了, 核心是*a[3],然後是[3],是一個數組,一個指針數組,一個函數指針數組。

19 函數不要返回局部變量的指針

函數一定不要返回局部變量的指針或者引用。如下面的代碼:

<code>char *func(void)
{
         char c = ‘A’;
         char *p = &c;
         return p;
}


char &func(void)
{
       char c='A';
       return c;
}
int main(void)
{
         char * pc = NULL;
 
         pc = func();
         printf(“%c”, *pc);
         return 0 ;
}/<code>

在func函數中,我們將局部變量c的地址當做一個指針返回,那麼在main函數中,我們是不能夠再次使用或者訪問這個指針所指的內存的。因為局部變量c的生命週期只存在於函數func運行期間。一旦func結束運行之後,那麼c就被銷燬了,c的地址就是一個無效的內存地址,因此,當在main函數中執行了:

pc=func() ;

pc指向的內存是無效的內存,因此pc是一個野指針,試圖訪問一個野指針,其後果是未定義的,程序有可能崩潰,有可能訪問的是垃圾值。

20 指針與引用

引用是一種沒有指針語法的指針,與指針一樣,引用提供對對象的間接訪問。引用為所指對象的一個別名(alisas)。如下面的例子:

<code>#include <stdio.h> 

int main(void)
{
int a[5][10];
printf("%d\\n", int(a+1)-int(a)); // 40
\t printf("%d\\n", int(&a+1)-int(a));// 200
\t getchar();
return 0;
}/<stdio.h>/<code>

引用必須初始化,而指針沒有這個要求(儘管沒有初始化的指針很危險);引用總是指向它最初獲得的那個對象,而指針可以被重新賦值。

引用可以理解為由編譯器實現了自動解引用的const指針。

如果是主調函數給被調函數傳遞指針,那麼會先複製該指針,在函數內部使用的是複製後的指針,這個指針與原來的指針指向相同的地址,如果在函數內部將複製後的指針指向了另外的新的對象,那麼不會影響原有的指針。所以要想在函數中改變指針,必須傳遞指針的指針或者指針的引用。

使用對象指針作為函數參數要比使用對象作函數參數更普遍一些。因為使用對象指針作函數參數有如下兩點好處:

1)實現傳址調用。可在被調用函數中改變調用函數的參數對象的值,實現函數之間的信息傳遞。

2)使用對象指針實參僅將對象的地址值傳給形參,而不進行副本的拷貝,這樣可以提高運行效率,減少時空開銷。

使用對象引用作函數參數要比使用對象指針作函數參數更普遍,這是因為使用對象引用作函數參數具有用對象指針作函數參數的優點,而用對象引用作函數參數將更簡單,更直接(無須在函數體內解引用即是對主調函數實參的操作)。

21 指針引用做函數參數

在C語言中經常使用指針,指針的指針,指針的引用做函數的參數。那麼它們的區別是什麼呢?

1)指針做參數:

void func( MyClass *pBuildingElement );// 指針,不能修改指針本身

通常要求實參是一個變量的地址,函數體內通過操作*pBuildingElement來改變pBuildingElement指向的值。而如果實參是一個指針,函數體內操作pBuildingElement,通常不是函數設計的初衷,沒有意義。

<code>void GetMemory(char *p, int num) // 指針做參數,本意是改變指針指向的內容
{
p = (char *)malloc(sizeof(char) * num);
}
void Test(void)\t
{\t
char *str = NULL;\t
GetMemory(str, 100);\t// str 仍然為 NULL,是因為p=str, 對p的操作影響不到str
// 被調函數體內的*p才能影響到主調函數str指向的內容;

strcpy(str, "hello");\t// 運行錯誤
}/<code>

2)指針的指針做參數:

void func( MyClass **pBuildingElement );//指針的指針,能修改指針

通常在函數體內操作的是*pBuildingElement

<code>void GetMemory2(char **p, int num) \t\t // p解引用後*p是一個指針, 
{
*p = (char *)malloc(sizeof(char) * num); // 可以被返回地址的函數賦值
}
void Test2(void)\t
{\t
char *str = NULL;\t
GetMemory2(&str, 100);\t// p = &str;
strcpy(str, "hello");\t
cout<< str << endl;\t
free(str);\t
}
/<code>

3)指針引用做參數:

void func(MyClass *&pBuildingElement ); // 指針的引用,能修改指針

pBuildingElemen是MyClass*的別名,既是傳址,又因為是引用(實現了自動解引用),函數體內pBuildingElemen與MyClass*兩者的運算是一致的。

<code>void GetMemory2(char *&p, int num)          //p引用一個指針char*, 
{
p = (char *)malloc(sizeof(char) * num); // 可以被返回地址的函數賦值
}
void Test2(void)
{
char *str = NULL;

GetMemory2(str, 100); // 注意參數是str,而不是&str
strcpy(str, "hello");
cout<< str << endl;
free(str);
}
/<code>

對於一個返回動態內存的函數,另外一種方式就是利用指針函數返回。

22 返回指針和指針引用

對於引用,用做參數時,結合了指針和變量的性質,實參和形參結合時,是傳址的特性,引用用在函數體時,無需解引用即是對主調函數的實參的操作。

看下面實例:

<code>#include <stdio.h>
int g = 0;
int *pg = &g;

int &func(int &i) // 引用做為參數和返回值
{
\ti = - 1;
\treturn g ;
}

int* &func2(int* &i) // 指針引用做為參數和返回值
{
\t*i = - 11;
\treturn i ;
}

void main(void)
{
\tint j=10;
\tfunc(j)=100;
\tprintf("%d\\n",j); // -1

\tprintf("%d\\n",g); // 100
\tint* p = func2(pg);
\tprintf("%d\\n",g); // -11
\tprintf("%d\\n",*p); // -11
\t*func2(pg)=22;
\tprintf("%d\\n",g); // 22
\tgetchar();
}/<stdio.h>/<code>

返回指針和返回引用都可以做為左值,但返回引用做左值時操作更直觀。

總結一下指針、引用用做函數參數和返回值:

首先要理解主調函數和被調函數的關係。如果想保持兩者的獨立性,當然是用傳值的方式進行。如果想讓被調函數能夠修改主調函數的變量,就使用傳址,指針傳值因為需要在函數體內解引用,操作不方便,C++引入了引用的語法機制,被調函數既能修改主調函數的實參,在函數體中有無須解引用。

另外可以從抽取函數的角度去理解。如果是引用傳址,相對於直接抽取一部分代碼封裝為函數,實參與形參的相互影響仍在。而指針傳址呢?需要在函數體內改寫成解引用的形式。如果是傳值呢,抽取的是相互沒有關聯性的代碼(一般是一個值的計算,仍返回一個值)。

23 指針使用注意事項

C語言中最複雜最容易出錯的要數指針了。指針讓一些初級程序員望而卻步,而一些新的開發語言(如Java,C#)乾脆就放棄了指針。

大家已經知道,C語言最適合於底層的開發,一個重要的原因就是因為它支持指針,能夠直接訪問內存和操作底層的數據,可以通過指針直接動態分配與釋放內存:

<code>// 下面是用typedef定義一個新結構最常用的定義形式
// 在微軟的面試中,在考查你某個算法前,一般會讓你先定義一個與算法相關的結構。
// 比如鏈表排序的時候,讓你定義一個鏈表的結構。
typedef struct _node
{
int value;
struct _node * next;
}node, *link;

node *pnode = NULL; // 聲明變量都應該初始化,尤其是指針
pnode = (node *)malloc(sizeof (node)); // 內存分配
// 務必檢測內存分配失敗情況,程序健壯性的考查
// 加上這樣的判斷語句,會讓你留給面試官一個良好的印象
// 不加這樣的判斷,如果分配失敗,會造成程序訪問NULL指針崩潰
if (pnode == NULL)
{
// 出錯處理,返回資源不足錯誤信息
}
memset(pnode, 0, sizeof(node)); // 新分配的內存應該初始化,否則內存中含有無用垃圾信息

pnode->value = 100;
printf(“pnode->value = %d\\n”, pnode->value);
node * ptmp = pnode;
ptmp += 1; // 指針支持加減運算,但須格外小心
free(pnode); // 使用完內存後,務必釋放掉,否則會洩漏。一般採取誰分配誰釋放原則
pnode = NULL;// 釋放內存後,需要將指針置NULL,防止野指針/<code>

上面的這段代碼演示了指針的基本使用方式。在指針聲明的時候,最好將其初始化為NULL,否則指針將隨機指向某個區域,訪問沒有初始化的指針,行為為未定義而為程序帶來預想不到的結果;指針釋放之後,也應該將指針指向NULL,以防止野指針。因為指針所指向的內存雖然釋放了,但是指針依然指向某一內存區域。

指針使用注意事項總結:

指針變量沒有被初始化,任何指針變量剛被創建時不會自動成為NULL指針,它的缺省值是隨機的,它會隨機的指向任何一個地址(即野指針),訪問野指針會造成不可預知的後果。所以,指針變量在創建的同時應當被初始化,要麼將指針設置為NULL,要麼讓它指向合法的內存。

2)指針的加減運算移動的是指針所指類型大小

前面已經提到,指針的加法運算p = p + n中,p向前移動的位置不是n個字節,而是n * sizeof(*p)個字節,指針的減法運算與此類似。

3)當用malloc或new為指針分配內存時應該判斷內存分配是否成功,並對新分配的內存進行初始化。

用malloc或new分配內存,應該判斷內存是否分配成功。如果失敗,會返回NULL,那麼就要防止使用NULL指針。在分配成功時,會返回內存的地址。這個時候內存是一段未被初始化的空間,裡面存在的可能是垃圾數據。因此,需要用memset()等對該段內存進行初始化或直接使用calloc()。


此外,應該防止試圖使用指針作為參數,去分配一塊動態內存。如果非要這麼做,那麼請傳遞指針的指針或指針的引用。

4)如果指針指向的是一塊動態分配的內存,那麼指針在使用完後需要釋放內存,做到誰分配誰釋放的原則,防止內存洩漏。

5)指針在指向的動態內存釋放後應該重新置為NULL,防止野指針。

野指針不是NULL指針,是指向“垃圾”內存的指針。野指針是很危險的,它可能會造成不該訪問的數據或不該改的數據被訪問或者篡改。在應用free或者delete釋放了指針指向的內存之後,應該將指針重新初始化為NULL。這樣可以防止野指針。

分析下面的程序:

<code>void GetMemory(char **p,int num)
{
*p=(char *)malloc(num);
}
int main(void)
{
char *str=NULL;

GetMemory(&str,100);
strcpy(str,"hello");
free(str);

if(str!=NULL)
{
strcpy(str,"world");
}
printf("\\n str is %s",str);
getchar();
}/<code>

分析:上面的代碼經常出現在各大外企的筆試題目裡,它通過指針的指針分配了一段內存,然後將”hello”拷貝到該內存。使用完後再釋放掉。到此為止,代碼沒有任何問題。但是,在釋放之後,程序又試圖去使用str指針。那麼這裡就存在問題了。由於str沒有被重新置為NULL,它的值依然指向了該內存。因此後面的程序依然能夠打印出”world” 字符串。

6)指針操作不要超出變量的作用範圍,防止野指針。

分析下面的代碼:

<code>har *func()
{
char c = ‘A’;

char *p = &c;
return p;
}
void main(void)
{
char * pc = NULL;

p = func();
printf(“%c”, *p);
}/<code>

在上面的代碼中,func()函數試圖返回一個指向局部變量c的指針。然而局部變量的生命期為func()函數執行期,即變量c分配在棧上,func()函數執行完後,c就不存在了。返回的指針就是一個無效的野指針。因此,打印*p時,可能會出現任何一個不可確定的字符。

7) 對於複雜指針的使用,如果做不到“誰分配,誰釋放”,那麼可以使用引用計數來管理這塊內存的使用。 引用計數方式來管理內存,即在類中增加一個引用計數,跟蹤指針的使用情況。當計數為0了,就可以釋放指針了。 此種方法適合於通過一個指針申請內存之後,會經過程序各種複雜引用的情況。

下面是一個實際例子:

<code>class CXData
{
public:
CXData()
{
m_dwRefNum = 1; //引用計數賦初值
}
ULONG AddRef() //增加引用

{
ULONG num = InterlockedIncrement(&m_dwRefNum);
return num;
}
ULONG Release() //減少引用
{
ULONG num = InterlockedDecrement(&m_dwRefNum);
if(num == 0) //當計數為0了,就釋放內存
{
delete this;
}
return num;
}
private: ULONG m_dwRefNum; //引用計數
}
void test()
{
CXData *pXdata = new CXData;
pXdata->AddRef(); //使用前增加計數
pXdata->Release(); //使用後減少計數,如果計數為零,則釋放內存
}/<code>

以上實例的目的就是試圖封裝裸指針以達到指針安全的目的,相當於C++STL的智能指針shared_ptr的雛形。

-End-


分享到:


相關文章: