作為C++語言的亮點與精髓,指針一直是備受人們追捧和病詬的東西。不知道有多少精巧的代碼是通過它實現的,也不知道有多少難纏的bug是由它引發的,這篇文章先對指針做一個全局的總結,從聲明、賦值、調用和實現機制上對所有指針做一個說明,希望對大家有所幫助。在后續的文章中,我將給出那些使用指針的技巧。
首先是一個列表,列出我們今天討論的內容:
- 指向基本數據類型的指針
- 指向指針的指針
- 指向數組的指針
- 指向函數的指針
- 指向結構體和對象的指針
- 指向類成員變量的指針
- 指向類成員函數的指針
1. 指向基本數據類型的指針
定義 | 示例 | |
聲明 | [數據類型] * [指針名]; | int* pa; |
賦值 | [指針名] = &[變量]; | int a; pa = &a; |
取值 | *[指針名] | int b = *pa; |
實現機制:
對於指向基本數據類型的指針,它包含兩個信息:數據所在的地址、取數據時的長度(類型)。
其中地址信息存放在指針pa中,如果是一個32-bit的計算機,pa的長度應為4 bytes,即地址的長度是32位(unsigned int)。那么怎樣獲取一個變量的地址呢?
13: int* pa = &a; 00411A5A lea eax,[a] 00411A5D mov dword ptr [pa],eax
可以使用取地址運算實現,即lea:Load Effective Address。
不過這不是唯一一種對基本數據類型指針的賦值方式,其他方式還有:
(1) 強制把其他類型的指針轉換為該類型的指針:
double *px = &x; pa = (int *) px; (2) 直接使用常數或者變量值對指針賦值:
pa = (int *) 0; 我就經常把指向對象的指針強制轉換為unsigned char *以便研究對象內部結構,不過強制轉換面臨的最大問題是,如果指針指向了不可訪問的內存空間,取值的時候就會引發異常,相信大家都見過這個:
它是怎么引發的呢?
我們先來看一下取值的原理:
14: int b = *pa; 00412250 mov eax,dword ptr [pa] 00412253 mov ecx,dword ptr [eax] 00412255 mov dword ptr [b],ecx
現獲取pa的值,放到eax中,然后再把eax當做地址,取出4個字節(int型)的值。“取出多少字節的數據”就是指針所表示的第二個信息(指針的類型),這個信息由編譯器自動轉換為目標代碼(即dword ptr, word ptr等)。
(注:dword ptr [pa]表示從pa開始,取出4個byte的數據,組成一個32位的整數。在asm32中,訪問修飾符byte表示1個字節,word:2個字節,dword:4個字節,qword:8個字節。而我們學過一個字(word)表示一台計算機一次處理的數據長度,按理說word應該是4個字節,這里卻是兩個,原因是微軟為了兼容它16位程序代碼而導致的,同樣,在windows API中,我們看到的DWORD也被定義為16位,即typedef short DWORD)
圖中的訪問錯誤異常就產生在mov ecx, dword ptr[eax]這一句,如果eax里面放的地址,映射到不存在、不可讀或者沒有權限訪問的頁面,就會產生CPU內部錯誤,由於我們的程序沒有捕獲錯誤,所以把錯誤拋給了windows,windows就彈出了一個很難看的對話框,然后終止進程的執行。
這里還要說一下指針的運算,如果把指針加1,表示地址向后移動了指針類型那么長的字節數,也就是說,pa+1表示pa的地址加4個字節,可使用下面的偽代碼表示:
pa + 1 == (int *)((char *)pa + sizeof(a))
指針不單可以做加減運算(不能做乘除),還可以做[]運算,pa[i]表示*(pa + i),但這並不能表示指針和數組是同一類型。
如果用指針做乘法,則會產生這樣的編譯錯誤:
error C2296: '*' : illegal, left operand has type 'int *'
2. 指向指針的指針
指向指針的指針,是存儲指針地址的變量,而且每次都取出4個byte的數據(因為地址是統一的)。定義方式和原理與指向基本數據變量的指針一模一樣,這里再寫一下:
定義 | 示例 | |
聲明 | [數據類型] *[*…] [指針名]; | int** ppa; |
賦值 | [指針名] = &[指針]; | ppa = &pa; |
取值 | *[*…][指針名] | int b = **pa; |
指向指針的指針沒有什么特別的地方,把指針看做基本數據類型來對待就行了。
[int*] * ppa = &pa;
[數據類型] * [指針名] = &[變量名];
3. 指向數組的指針
還記得前文說過數組和指針根本不同嗎,要理解指向數組的指針,我們先來看什么是數組。
指針是一個存放地址的變量,而數組是一個地址。
或者說,數組是一個指針常量,數組只能指向確定的數據。數組聲明時一定要賦值,這是為了它肯定能指向一個有用的地址空間(數組變量一旦確定了地址,就不能變了,因為編譯器已經把它定死了(或者說把它替換為一個常量了))。
定義 | 示例 | |
聲明和賦值 | [數據類型] [數組名][數組長度]; [數據類型] [數組名][]; |
int arr_a[10]; int arr_b[] = {1,2,3}; |
取值 | [數組名][索引號] | int a = arr_a[1]; |
當使用int arr_a[10]聲明數組時,會發生兩種情況:如果arr_a在函數中申請,那么編譯器就會自動計算這個函數所使用的棧空間,然后把arr_a安插到棧中,轉換為ebp-常數;而如果arr_a在全局數據區申請(函數外),那么編譯器就在未初始化數據段(.bss = Block Started by Symbol)中申請一塊可以放10個int的符號,然后將所有使用arr_a的地方,都轉換為那個地址。
局部數組:
24: int arr_c[3]; 26: int d = arr_c[1]; 00412F47 mov eax,dword ptr [ebp-2Ch] 00412F4A mov dword ptr [ebp-38h],eax
全局數組:
9: int arr_d[4]; 27: int e = arr_d[1]; 00412F4D mov eax,dword ptr [arr_d+4 (42E2ACh)] 00412F52 mov dword ptr [ebp-3Ch],eax
而使用int arr_b[] = {1,2,3}這種形式,則更為麻煩一點,因為除了給數組分配空間外,還要賦初值。如果聲明在函數中,那么在聲明這條語句的代碼塊里,編譯器會寫入賦值語句,來初始化這個數組;如果聲明在函數外,編譯器就會把它放入到數據段中(.data),在程序加載時,由loader賦值。
局部數組:
25: int arr_a[] = {1,2}; 00412F36 mov dword ptr [ebp-24h],1 00412F3D mov dword ptr [ebp-20h],2
當然,如果使用static int arr_c[10];也可以將數組放入數據段中,並且還能將初值設為全0。
另外一種將數組元素設為全0的方法是int arr_a[10] = {0},不過這種形式只是int arr_a[10] = {0, 0, …, 0}的簡化版罷了,如果在給數組賦初值時,大括號里面的元素不足數組的長度,就用0替代。即int arr_a[10] = {1, 2}會被轉換為int arr_a[10] = {1, 2, 0, 0,…, 0}。
為什么初學者會認為數組就是指針呢,大概是數組類型變量可以直接賦值給指針類型變量吧,即:
int* pArr_a = arr_a;
但這只是一種賦值,就像int a = 1,反過來就不行了(1 = a),就連使用強制轉換都不行。
// x:\工程\deepintocpp\objectmodel\04_data\pointer.cpp(35) : error C2440: 'initializing' : cannot convert from 'int *' to 'int []' // There are no conversions to array types, although there are conversions to references or pointers to arrays /*35:*/ int arr_p[] = pArr_a;
這里還要說的是,數組也不能賦值給數組(就像不能寫1=1一樣,常量是不能賦值給常量的):
int arr_f[10]; // wrong: // x:\工程\deepintocpp\objectmodel\04_data\pointer.cpp(39) : error C2440: 'initializing' : cannot convert from 'int [10]' to 'int []' // There is no context in which this conversion is possible int arr_g[] = arr_f; // wrong: // x:\工程\deepintocpp\objectmodel\04_data\pointer.cpp(41) : error C2075: 'arr_i' : array initialization needs curly braces int arr_i[10] = arr_f;
但是有一個例外:在傳遞函數參數時,可以使用int []表示一個數組。
void func(int arr[]) { return; } // correct. func(arr_a); // also correct. func(pArr_a);
但是這種數組其實就是指針,只是給人看上去好看一點,對於編譯器來說,它和指針是一樣的:
// passing array. 13: void func(int arr[]) 14: { 00412540 push ebp 00412541 mov ebp,esp 00412543 sub esp,40h 00412546 push ebx 00412547 push esi 00412548 push edi 15: arr[0] = 1; 00412549 mov eax,dword ptr [arr] 0041254C mov dword ptr [eax],1 16: return; 17: } // passing pointer. 19: void func_p(int* pArr) 20: { 00412560 push ebp 00412561 mov ebp,esp 00412563 sub esp,40h 00412566 push ebx 00412567 push esi 00412568 push edi 21: pArr[0] = 1; 00412569 mov eax,dword ptr [pArr] 0041256C mov dword ptr [eax],1 22: return; 23: }
指針和數組的另一個相同之處是,指針+1表示指針所指向的地址+sizeof(指針的類型),而數組+1同樣表示表示數組首地址+數組元素類型長度:
printf("arr_a size = %d, arr_a = 0x%08x, arr_a + 1 = 0x%08x\n", sizeof(arr_a), arr_a, arr_a + 1); printf("pArr_a size = %d, pArr_a = 0x%08x, pArr_a + 1 = 0x%08x\n", sizeof(pArr_a), pArr_a, pArr_a + 1); // output: // arr_a size = 40, arr_a = 0x0012ff34, arr_a + 1 = 0x0012ff38 // pArr_a size = 4, pArr_a = 0x0012ff34, pArr_a + 1 = 0x0012ff38
arr_a的長度是10個int。因此可以用sizeof(arr_a)/sizeof(arr_a[0])來計算一個數組的長度(當然這個長度本來就是編譯時已知的)。
弄清了數組和指針的關系,我們來看什么是指向數組的指針:
內部存放數組首地址的變量。
對於定義來說,比較容易理解,可是形式就比較難記了:
定義 | 示例 | |
聲明 | [數據類型] (* [指針名])[數組長度]; | int (*pArr)[10]; |
賦值 | [指針名] = &[數組名]; | pArr = &arr; |
取值 | (*[指針名])[下標] | int b = (*pa)[1]; |
由於operator[]的優先級高於*,因此要在指針名和*之上加一個括號,表示聲明的是指針。若不加,則表示的是存放指針的數組。
// pointer, point to an array which has 10 int elements. int (*pArr)[10] = &arr_a; // array, stores 10 pointers. int *arrP[10]; arrP[0] = pa;
現在的問題是:pArr+1表示的是什么?
printf("sizeof(pArr) = %d, sizeof(*pArr) = %d, pArr = 0x%08x, pArr + 1 = 0x%08x\n", sizeof(pArr), sizeof(*pArr), pArr, pArr + 1); // output: // sizeof(pArr) = 4, sizeof(*pArr) = 40, pArr = 0x0012ff34, pArr + 1 = 0x0012ff5c
從代碼中,可以看出:
首先pArr是一個指針,因此它的長度是4個字節,而*pArr是一個有10個int元素的數組, 因此它的長度是40。使用第一節的指針+1公式:
pArr + 1 == (int [10])((char *)pArr + sizeof(int [10])) == pArr的地址 + pArr指向的數組長度 == pArr指向的地址+40個字節
由此可知,pArr+1表示下一個數組。
由於*pArr取出的是數組,那么就要使用一個可以指向數組的變量來存放它的值了。int []表示常量,肯定不能作為左值,因此要想取出pArr指向的內容,只能使用指針或者operator[]了:
61: int (*pArr)[10] = &arr_a; 00413CE1 lea eax,[ebp-44h] 00413CE4 mov dword ptr [ebp-6Ch],eax 65: b = (*pArr)[0]; 00413CF0 mov eax,dword ptr [ebp-6Ch] 00413CF3 mov ecx,dword ptr [eax] 00413CF5 mov dword ptr [ebp-10h],ecx
注意:上面代碼中lea eax,[ebp - 44h]表示“把ebp-44放入eax中”,這是一種聰明的地址運算的做法,否則就需要用兩條語句來實現:
mov eax, ebp
sub eax, 44h
(lea表示取某個變量的地址,而[…]表示取某個地址指向變量的值,兩者疊加就變為“取某個地址所指向變量的值的地址”,相互抵消,就變成了計算括號中的值,然后把它放入到前面那個寄存器中。)
ebp-44h就是數組名arr_a所轉換為的地址常量。