指針是什么
指針是一個變量,其儲存的是值的地址,而不是值本身。指針提供了另一種訪問內存空間的方法:雖然我們不知道變量的名稱,但我們可以通過變量存放的地址訪問它。
指針的初始化
- 可以直接初始化:
//<數據類型> *指針變量名 = 賦值;
//指針的數據類型,表示指針所指向的數據的數據類型
int a = 1;
int array[] = { 1,2,3,4,5 };
int *p1 = &a;
int *p2 = array; 或者 int *p2 = &array[0]
如果int *p2 = &array;
將不能通過編譯,原因是數據類型不匹配。
- 也可以先定義,再賦值:
//對象指針:
Node node1;
Node *p3 = &node1;
//函數指針:
int add(int x,int y);
int (*p3)(int,int);
p3 = add;
指針的運算
- 算數運算:
指針可以跟 整數 進行加法和減法的運算,但是運算規則比較特殊——對指針進行加減運算的結果,與指針本身的類型密切相關??梢钥闯觯?code>指針+1移動了一種數據類型在內存中存放的字節數。不過,空指針和函數指針是不能進行算數運算的(無法通過編譯),因為無法確定移動多少個字節。
//整數指針:
int *p1 = &a;
cout << p1 << " " << p1+1 <<endl;
//字符指針:
char c[] = "people";
char *p2 = &c;
cout << p2 << " " << p2+1 <<endl;
printf("%p %p\n",p2,p2+1);
//對象指針:
Node *p3 = &node;
cout << p3 << " " << p3+1 <<endl;
//函數指針:
int (*p4)(int,int) = add;
printf("%p\n",p4);
cout << p4 <<endl;
輸出結果:
0x7ffe597f872c 0x7ffe597f8730 people eople 0x7ffe597f8760 0x7ffe597f8761 0x7ffe597f8750 0x7ffe597f8760 0x4009ed 1
1)以上使用cout
輸出字符指針
的時候結果跟想象中不太一樣?
答:這是因為cout
對象認為char
的地址是字符串的地址,因此它打印該地址處的字符,然后繼續打印后面的字符,直到遇到空字符(\0)
為止。
2)為什么用cout
輸出函數指針
會得到1
呢?
答:這里先挖個坑...
- 關系運算&邏輯運算
指針變量的關系運算,指的是 指向相同類型數據的指針之間,進行的關系運算。 如果兩個相同類型的指針相等,就表示兩個指針指向的是同一個地址——不同類型的指針之間,或者指針與非 0 整數之間的比較是沒有意義的。
但是有一種情況是特殊的——指針可以跟 整數 0 之間進行比較,0專門用于表示空指針,即指針變量中保存的地址是空的,不指向任何有效的地址。
關系運算的結果經常用于邏輯運算。
int a = 9,b = 7;
int *p[4] = {&a,&a,&b,NULL};
//判斷前后指針是否相等
cout << "equal?" <<endl;
for(int i=0;i<4;i++){
if(p[i]==p[i+1]) {
cout << "YES ";
}
else cout << "NO ";
}
cout <<endl;
//判斷是否為空指針
cout << "NULL?" <<endl;
for(int i=0;i<4;i++){
if(p[i]==NULL) {
cout << "YES " <<endl;
}
else cout << "NO ";
}
cout << endl;
輸出結果:
equal? YES NO NO YES NULL? NO NO NO YES
值得注意的是,越界的指針數組元素p[4]
是一個空指針。
空指針
為什么我們需要空指針呢?因為有的時候,我們在聲明一個指針的時候,并沒有一個確定的地址值可以賦給它,當程序運行到某個時刻的時候,才會將某個地址賦值給這個指針。這樣,在指針定義但沒有使用的這段時間里,它的值是不確定的——要是誤用了這個不確定的指針的話,就很有可能會造成不可預見的錯誤(比如意外地把某個不該變更的值給改掉了),因此在這種情況下,我們首先應該將地址設置為空。
除了給指針賦值0
或NULL
使其為空,在C++11標準中,我們還可以使用nullptr
關鍵字來表示空指針,用法跟NULL基本相同(需要引用命名空間std
中的對應標識符)。
指針與數組
數組的本質,實際上是一串連續的相同大小的內存空間——比如說,對于整形數組int a[10]
,它在內存中就是連續排列的十個可以容納一個整形變量的內存空間。而數組的名稱,其實就是一個常量指針,即不能被賦值的指針。作為一個指針,數組名指向的是數組的第一個元素。
指針加減運算的特點,使得它可以特別被用于處理存儲在一段連續內存空間中的同類數據。而數組正好是具有一定順序關系的,若干同類型變量的集合體——數組元素的存儲,在物理上與邏輯上都是連續的,數組名就是變量的首地址。如果有數組array[5]
,那么array
和&array[0]
是相同的。
- 要訪問數組元素,下面兩種方法是等效的:
int *p = array;
cout << array[10] <<endl;
cout << *(p+10) <<endl;
- 此外,如果我們要把數組作為函數的形參的話,那么它實際上是等價于把指向數組元素類型的指針作為形參的——例如,下面三個寫法,出現在形參列表中就是等價的:
void f(int p[]);
void f(int p[3]);
void f(int *p);
指針數組
如果一個數組的所有元素都是指針變量,那么這就是一個指針數組。指針數組的每一個元素都必須是同一類型的指針。指針數組有一個神奇的應用:
//創建一個指針數組,其元素分別指向三個數組
int line1[]={1,0,0};
int line2[]={0,1,0};
int line3[]={0,0,1};
int *pLine[3]={line1,line2,line3};
//用類似二維數組的形式訪問三個數組
for(int i=0;i<3;i++){
for(int j=0;j<3;j++){
cout << pLine[i][j] << “ ”;
}
}
輸出結果:
1 0 0 0 1 0 0 0 1
上個例子中的pLine在使用上跟一個二維數組沒有區別,但是在存儲方式上,它跟真正的二維數組并不相同:
二維數組在內存中,是以行優先的方式按照一維順序關系存放的。因此,對于二維數組,可以將其理解成一個一維數組的一維數組,其首地址為數組名,元素個數就是行數——而它的每一個元素,就是一個一維數組。
然而,對于指針數組pLine,它的三個“元素數組”在內存中,并不是連續存放的——訪問line2或者line3的時候,首先要在pLine中找出對應的元素指針,即為指向line2或者line3頭元素的地址,然后再通過指針跳轉到要訪問的數組。
對象指針
跟基本類型的變量一樣,每一個對象在初始化之后,都會在內存中占據一定的空間——所以我們同樣也可以通過地址來訪問一個對象。盡管對象同時包含了數據和函數兩種成員,但是對象所占據的內存空間只用于存放數據成員——函數成員并不在每一個對象的存儲副本之中。對象指針就是用于存放對象地址的變量——對象指針遵循一般變量指針的各種規則。
- 通過對象名,我們可以訪問對象成員——同樣,通過對象指針,我們可以訪問對象的成員,以下三種方法完全等價:
//假設已有Line類
cout << line1.getLength() <<endl;
cout << line_ptr->getLength() <<endl;
cout << (*line_ptr).getLength() <<endl;
-
this
指針
對于類的成員函數來說,我們可以直接在函數體內訪問成員變量——例如,如果對象Line
有一個成員變量length
的話,那么我們就可以直接在成員函數內訪問這個成員:
int getLength(){return length;}
而實際上,C++ 為每一個類的非靜態成員函數(就是沒有static
關鍵字的成員函數),都提供了一個隱含的指針this
,當我們寫下return length;
的時候,編譯器執行的實際上是return this->length;
。
this
指針明確地指出了函數當前所操作的數據所屬的對象——它是成員函數隱藏的一個形參,當我們在成員函數中操作對象的數據成員的時候,我們其實就是在使用this
指針。
然在一般情況下,我們不需要特別把this
指針寫出來——但是如果函數的形參列表中的參數跟成員變量重名的話,那么由于標識符作用域覆蓋,我們將沒法直接通過成員變量名來訪問它。當然我們也可以選擇更改形參名——但是更好的辦法是通過this
指針來訪問成員變量,這樣我們可以讓代碼擁有更好的可讀性:
void setLength(int length){
this->length=length;
}
另一種解決方法是使用初始化列表:
void setLength(int length):length(length){
}
函數指針
以上我們使用的指針都是指向數據的——而在程序運行的時候,不僅數據要占據內存空間,執行程序的代碼也會被存入到內存,并占據一定的空間。每一個函數都有函數名,而實際上這個函數名就表示函數的代碼在內存中的起始地址。在程序中可以像使用函數名一樣,使用指向函數的指針來調用函數——也就是說,一旦函數指針指向了某個函數,那么它與函數名就具有同樣的作用。
函數名在表示函數代碼起始地址的同事,也包括函數的返回值類型,以及參數的個數、類型、排列次序等信息。因此,在通過函數名調用函數的時候,編譯器就可以自動檢查實參與形參是否相符,用函數的返回值參與其他運算時,能夠自動進行類型一致性檢查。而函數指針也具有同樣的效果。
- 聲明:
聲明一個函數指針時,需要提供構造一個函數需要的所有信息——包括函數的返回值和形式參數列表,如下所示:
返回值類型 (* 函數指針名)(形參表)
由于對函數指針的定義在形式上比較復雜,如果在程序中出現多個這樣的定義,那么多次重復這樣的定義會相當繁瑣。這里我們有一種很方便的解決方案——使用typedef
。例如:
typedef int (* DoubleIntFunction)(double);
這里我們聲明了DoubleIntFunction
為“有一個double
形參,返回類型為int
的函數的指針”的類型的別名——接下來,如果我們需要聲明這個類型的變量的時候,我們就可以直接進行使用:
DoubleIntFunction funcPtr;
這樣我們就可以直接使用這個類型的指針funcPtr
了。
- 賦值:
函數指針名=函數名;
注意這里的“函數名”必須是一個已經聲明過的函數,并且必須具有跟函數指針相同返回類型跟相同參數表的函數。賦值之后你就可以像使用函數一樣,使用函數指針了。
- 使用:
int add(int x,int y) {
return x+y;
}
int (*func_ptr)(int,int);
func_ptr = add;
//以下二者完全等價
cout << func_ptr(2,3) <<endl;
cout << add(2,3) <<endl;
- C++ 11 提供的
lambda 表達式
,它可以替代函數指針的作用。
動態內存分配
動態內存解決了諸如“用戶輸入XX個數據,那么我應該開多大的數組?”之類的只能在程序運行時才能確定的問題。那跟指針有什么關系呢?這是因為我們申請的動態內存時,返回的就是指向這個這個動態內存首地址的指針。
在 C++ 中,動態內存分配可以保證程序在運行的過程中,可以按照實際需要申請適量的內存,等到使用結束之后我們還可以將其釋放——這種在程序運行的過程中申請和釋放的存儲單元也稱為堆對象,而動態內存分配所調用的內存空間則稱為堆內存。建立和刪除堆對象使用以下兩個運算符:new
和delete
。
-
new
的功能是動態分配內存,其語法形式如下所示:
new 數據類型(初始化參數列表);
以上語句的作用是在程序運行的過程中,申請分配用于存放指定類型數據的內存空間,然后根據參數列表中給出的值來進行初始化。如果內存申請成功,那么new
運算符就會返回一個指向新分配內存區域首地址的指針——我們可以通過這個指針來訪問堆對象。
- 例如我們申請一個
int
類型的內存空間:
int *point;
point=new int(2);
以上,系統動態分配了用于存放int
類型數據的內存空間,然后用初始值2
賦值,得到的地址返回給point
指針變量。我們也可以這么寫,但注意區別:
int *point = new int;//沒有初值
int *point = new int();//初始值為0
- 還可以解決“開多大數組?”的問題:
cin >> n;
int *a=new int[n];
其中,方括號內的表達式表示數組長度,它可以是任何能夠得到正整數值的式子。
- 除了數組類型跟基本類型之外,
new
運算符還可以建立一個類的實例對象:
//假設已有類Node
Node *node_ptr;
node_ptr = new Node();
如果要建立一個對象的話,那么這里的“參數列表”就要跟對象所屬類的構造函數一一對應:如果不寫括號或者括號里為空的話,那么就會調用類的默認構造函數;而如果寫了對應的參數的話就會調用類所具有的對應的構造函數。
-
delete
的作用是刪除一個用new
建立的對象,回收其申請的內存。
所有用new
分配的內存,都必須使用delete
進行回收,否則會導致動態分配的內存無法回收,造成內存泄露!另外,delete
和new
是一一對應的,不能delete
一個不是用new
建立的對象,否則會出現“段錯誤”之類的問題。
使用方法比較簡單——如果你覺得一個堆對象已經不再被需要,那么你直接將其刪除即可,如下所示:
delete node_ptr;//對于基本類型或者對象的指針
delete[] array_ptr;//對于指向數組的指針
注意如果要刪除的是一個數組的話,那么后面的那對方括號不可省略。