概述
C/C++語言之所以強大,以及其自由性,很大部分體現在其靈活的指針運用上。因此,說指針是C/C++語言的靈魂一點都不為過。 有好的一面,必然會有壞的一面,指針的靈活導致了它的難以控制,所以C/C++程序員的很多bug是基於指針問題上的。今天就對指針進行詳細的整理。
1、指針是什么?
指針是“指向(point to)”另外一種類型的復合類型。復合類型是指基於其它類型定義的類型。
理解指針,先從內存說起:內存是一個很大的,線性的字節數組。每一個字節都是固定的大小,由8個二進制位組成。最關鍵的是,每一個字節都有一個唯一的編號,編號從0開始,一直到最后一個字節。
程序加載到內存中后,在程序中使用的變量、常量、函數等數據,都有自己唯一的一個編號,這個編號就是這個數據的地址。
指針的值實質是內存單元(即字節)的編號,所以指針單獨從數值上看,也是整數,他們一般用16進制表示。指針的值(虛擬地址值)使用一個機器字的大小來存儲,也就是說,對於一個機器字為w位的電腦而言,它的虛擬地址空間是0~[2的w次冪] - 1,程序最多能訪問2的w次冪個字節。這就是為什么xp這種32位系統最大支持4GB內存的原因了。
因此可以理解為:指針是程序數據在內存中的地址,而指針變量是用來保存這些地址的變量。
2、變量在內存中的存儲
舉一個最簡單的例子 int a = 1,假設計算機使用大端方式存儲:
內存數據有幾點規律:
- 計算機中的所有數據都是以二進制存儲的
- 數據類型決定了占用內存的大小
- 占據內存的地址就是地址值最小的那個字節的地址。
現在就可以理解 a 在內存中為什么占4個字節,而且首地址為0028FF40了。
3、指針對象(變量)
用來保存指針的對象,就是指針對象。如果指針變量p1保存了變量 a 的地址,則就說:p1指向了變量a,也可以說p1指向了a所在的內存塊 ,這種指向關系,在圖中一般用 箭頭表示:
指針對象p1,也有自己的內存空間,32位機器占4個字節,64位機器占8個字節。所以會有指針的指針。
3.1、定義指針對象
定義指針變量時,在變量名前寫一個 * 星號,這個變量就變成了對應變量類型的指針變量。必要時要加( ) 來避免優先級的問題:
int* p_int; //指向int類型變量的指針
double* p_double; //指向double類型變量的指針
Student* p_struct; //類或結構體類型的指針
int** p_pointer; //指向 一個整形變量指針的指針
int(*p_arr)[3]; //指向含有3個int元素的數組的指針
int(*p_func)(int,int); //指向返回類型為int,有2個int形參的函數的指針
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
3.2、獲取對象地址
指針用於存放某個對象的地址,要想獲取該地址,虛使用取地址符(&),如下:
int add(int a , int b)
{
return a + b;
}
int main(void)
{
int num = 97;
float score = 10.00F;
int arr[3] = {1,2,3};
int* p_num = #
int* p_arr1 = arr; //p_arr1意思是指向數組第一個元素的指針
float* p_score = &score;
int (*p_arr)[3] = &arr;
int (*fp_add)(int ,int ) = add; //p_add是指向函數add的函數指針
const char* p_msg = "Hello world";//p_msg是指向字符數組的指針
return 0;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
通過上面可以看到&的使用,但是有幾個例子沒有使用&,因為這是特殊情況:
- 數組名的值就是這個數組的第一個元素的地址。
- 函數名的值就是這個函數的地址。
- 字符串字面值常量作為右值時,就是這個字符串對應的字符數組的名稱,也就是這個字符串在內存中的地址。
3.3、解析地址對象
如果指針指向了一個對象,則允許使用解引用符(*)來訪問該對象,如下:
int age = 19;
int* p_age = &age;
*p_age = 20; //通過指針修改指向的內存數據
printf("age = %d\n",*p_age); //通過指針讀取指向的內存數據
printf("age = %d\n",age);
- 1
- 2
- 3
- 4
- 5
- 6
對於結構體和類,兩者的差別很小,所以幾乎可以等同,則使用->符號訪問內部成員:
struct Student
{
char name[31];
int age;
float score;
};
int main(void)
{
Student stu = {"Bob" , 19, 98.0};
Student* p_s = &stu;
p_s->age = 20;
p_s->score = 99.0;
printf("name:%s age:%d\n",p_s->name,p_s->age);
return 0;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
3.4、指針值的狀態
指針的值(即地址)總會是下列四種狀態之一:
- 指向一個對象的地址
- 指向緊鄰對象所占空間的下一個位置
- 空指針,意味着指針沒有指向任何對象
- 無效指針(野指針),上述情況之外的其他值
第一種狀態很好理解就不說明了,第二種狀態主要用於迭代器和指針的計算,后面介紹指針的計算,迭代器等整理模板的時候在介紹。
空指針:在C語言中,我們讓指針變量賦值為NULL表示一個空指針,而C語言中,NULL實質是 ((void*)0) , 在C++中,NULL實質是0。C++中也可以使用C11標准中的nullpte字面值賦值,意思是一樣的。
任何程序數據都不會存儲在地址為0的內存塊中,它是被操作系統預留的內存塊。
無效指針:指針變量的值是NULL,或者未知的地址值,或者是當前應用程序不可訪問的地址值,這樣的指針就是無效指針,不能對他們做解指針操作,否則程序會出現運行時錯誤,導致程序意外終止。
任何一個指針變量在做解地址操作前,都必須保證它指向的是有效的,可用的內存塊,否則就會出錯。壞指針是造成C語言Bug的最頻繁的原因之一。
未經初始化的指針就是個無效指針,所以在定義指針變量的時候一定要進行初始化。如果實在是不知道指針的指向,則使用nullptr或NULL進行賦值。
3.5、指針之間的賦值
指針賦值和int變量賦值一樣,就是將地址的值拷貝給另外一個。指針之間的賦值是一種淺拷貝,是在多個編程單元之間共享內存數據的高效的方法。
int* p1 = &a;
int* p2 = p1;
- 1
- 2
p1和p2所在的內存地址不同,但是所指向的內存地址相同,都是0028FF40。
4、指針內含信息
通過上面的介紹,我們可以看出指針包含兩部分信息:所指向的值和類型信息。
指針的值:這個就不說了,上面大部分都是這個介紹。
指針的類型信息:類型信息決定了這個指針指向的內存的字節數並如何解釋這些字節信息。一般指針變量的類型要和它指向的數據的類型匹配。
同樣的地址,因為指針的類型不同,對它指向的內存的解釋就不同,得到的就是不同的數據。
char array1[20] = "abcdefghijklmnopqrs";
char* ptr1 = array1;
int* ptr2 = (int*)ptr1;
ptr1++;
ptr2++;
cout << &array1 << endl;
cout << *ptr1 << endl;
cout << ptr2 << endl;
cout << (char)ptr2 << endl;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
運行結果如下:
這里&array1,是數組的地址,其實和數組首地址&array1[0]是一樣的。
ptr1和ptr2都指向了同一塊地址,但是ptr1的類型個ptr2的類型不一致,導致內存解釋不一樣,所以同樣是后++操作,但是結果卻不一樣。
上面我用了C的強制轉換,C++中有另一套轉換方法。其根本,轉換(cast)其實是一種編譯器指令。大部分情況下它並不改變一個指針所含的真正地址,它只影響“被指出內存的大小和其內容”的解釋方式。
4.1、void*指針
void* 指針是一種特殊的指針類型,可用於存放任意對象的地址,但是丟失了類型信息。如果想要完整的提取指向的數據,程序員就必須對這個指針做出正確的類型轉換,然后再解指針。因為,編譯器不允許直接對void*類型的指針做解指針操作(提示非法的間接尋址)。
利用void所做的事兒比較有限:拿它和別的指針比較、作為函數的輸入或輸出,或者賦給另外一個void對象。
5、指針的算數運算
指針可以加上或減去一個整數。指針的這種運算的意義和通常的數值的加減運算的意義是不一樣的,指針的運算是有單位的。
如上面第4節介紹的例子:
ptr1和ptr1都被初始化為數組的地址,並且后續都加1。
指針ptr1的類型是char*,它指向的類型是char,ptr1加1,編譯器在1的后面乘上了單位sizeof(char)。
同理ptr2的類型是int*,它指向的類型是int,ptr2加1,編譯之后乘上了單位sizeof(int)。
所以兩者的地址不一樣,通過打印信息,可以看出兩者差了2個字符。
若ptr2+3,則編譯之后的地址應該是 ptr2的地址加 3 * sizeof(int),打印出的字母應該是m
指針運算最終會變為內存地址的元素,內存又是一個連續空間,所以按理只要沒有超出內存限制就可以一直增加。這樣前面所說的指針值的狀態第二條就很好解釋了。
6、函數和指針
6.1、函數的參數和指針
實參傳遞給形參,是按值傳遞的,也就是說,函數中的形參是實參的拷貝份,形參和實參只是在值上面一樣,而不是同一個內存數據對象。這就意味着:這種數據傳遞是單向的,即從調用者傳遞給被調函數,而被調函數無法修改傳遞的參數達到回傳的效果。
void change(int a)
{
a++; //在函數中改變的只是這個函數的局部變量a,而隨着函數執行結束,a被銷毀。
}
int main(void)
{
int age = 19;
change(age);
printf("age = %d\n",age); // age = 19
return 0;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
有時候我們可以使用函數的返回值來回傳數據,在簡單的情況下是可以的,但是如果返回值有其它用途(例如返回函數的執行狀態量),或者要回傳的數據不止一個,返回值就解決不了了。
傳遞變量的指針可以輕松解決上述問題。
void change(int* pa)
{
(*pa)++; //因為傳遞的是age的地址,因此pa指向內存數據age。當在函數中對指針pa解地址時
//會直接去內存中找到age這個數據,然后把它增1。
}
int main(void)
{
int age = 19;
change(&age);
printf("age = %d\n",age); // age = 20
return 0;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
上述方法,當然也可以用引用的方式。之后會整理引用和指針的區別。
傳遞指針還有另外一個原因:
有時我們會傳遞類或者結構體對象,而類或者結構體占用的內存有時會比較大,通過值傳遞的方式會拷貝完整的數據,降低程序的效率。而指針只是固定大小的空間,效率比較高。當然如果你用C++,使用引用效率比指針更高。
6.2、函數的指針
每一個函數本身也是一種程序數據,一個函數包含了多條執行語句,它被編譯后,實質上是多條機器指令的合集。在程序載入到內存后,函數的機器指令存放在一個特定的邏輯區域:代碼區。既然是存放在內存中,那么函數也是有自己的指針的。
其實函數名單獨進行使用時就是這個函數的指針。
int add(int a,int b) //函數的定義
{
return a + b;
}
int (*function)(int,int); //函數指針的聲明
function = add; //給函數指針賦值
function = &add; //跟上面是一樣的
int c = function(1,2); //跟函數名一樣使用
int d = (*function)(1,2); //跟上面的調用是一樣的
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
6.3、返回值和指針
這里唯一需要注意的是不要把非靜態局部變量的地址返回。我們知道局部變量是在棧中的,由系統創建和銷毀,返回之后的地址有可能有效也有可能無效,這樣會造成bug。
可以返回全局變量、靜態的局部變量、動態內存等的地址返回。
7、const與指針
這里主要的就是指針常量和常量指針了,兩者的區別是看const修飾的誰。
7.1、常量指針
實際是個指針,指針本身是個常量。
int a = 97;
int b = 98;
int* const p = &a;
*p = 98; //正確
p = &b; //編譯出錯
- 1
- 2
- 3
- 4
- 5
常量指針必須初始化,而且一旦初始化完成,則它的值就不能改變了。
7.2、指向常量的指針
int a = 97;
int b = 98;
const int* p = &a;
int const *p = &a; //兩者的含義是一樣的
*p = 98; //編譯出錯
p = &b; //正確
- 1
- 2
- 3
- 4
- 5
- 6
所謂指向常量的指針僅僅要求不能通過該指針改變對象的值,但是對象的值可以通過其它途徑進行改變。
需要注意的是常量變量,必須使用指向常量的指針來指向。但是對於非常量變量,兩者都可以。
8、淺拷貝和深拷貝
如果2個程序單元(例如2個函數)是通過拷貝 他們所共享的數據的 指針來工作的,這就是淺拷貝,因為真正要訪問的數據並沒有被拷貝。如果被訪問的數據被拷貝了,在每個單元中都有自己的一份,對目標數據的操作相互 不受影響,則叫做深拷貝
9、指針和數組
這個之前整理數組的時候整理過了,大家可以看數組的詳解
10、比較經典面試題
1、
void GetMemory(char* p)
{
p = (char*)malloc(100);
}
void Test(void)
{
char* str = NULL;
GetMemory(str);
strcpy(str,"hello world");
printf(str);
}
運行的結果是什么?
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
2、
int array[] = {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15};
int *p = array;
p += 5;
int* q = NULL;
*q = *(p+5);
printf("%d %d",*p,*q);
運行結果是什么?
- 1
- 2
- 3
- 4
- 5
- 6
- 7
3、
int arr[5] = {0,1,2,3,4};
const int *p1 = arr;
int* const p2 = arr;
*p1++;
*p2 = 5;
printf("%d,%d",*p1,*p2);
*p1++ = 6;
*p2++ = 7;
printf("%d %d \n", *p1, *p2);
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
第一道題:我們一看函數傳遞,指針只是淺拷貝,申請的內存在臨時對象p中,並沒有傳遞到函數外面,然后又對str地址進行寫操作,str初始地址為NULL,不能進行書寫,所以系統會崩潰。
第二道題:一看很開心是指針類型的加減法,下標從0開始,但是數字從1開始,所以應該是6 11。但是你忽略了q是一個NULL指針,不能進行書寫,所以會崩潰。
第三道題:指針指向數組,數組退化成指針,前兩個指針操作是對的。但是后面*p1++ = 6; 不可以通過p1進行值的修改,*p2++ = 7;不能對p2進行修改。所以這道題是編譯出錯。
感謝大家,我是假裝很努力的YoungYangD(小羊)。
參考資料:
《C++ primer 第五版》
http://www.cnblogs.com/lulipro/p/7460206.html
https://www.cnblogs.com/ggjucheng/archive/2011/12/13/2286391.html