1. 指標
指標
是一種C++
資料型別,用來描述記憶體地址。
什麼是記憶體地址?
記憶體
中的每一個儲存單元格都有自己的地址,地址是使用二進位制進行編碼。地址
從形態上看是一個整型資料型別。但是,它的資料含義並不表示數字,而是一個位置標誌,類似於門牌號。
指標型別資料的算術運算:
- 在地址上
加上
或減去
一個正整數,表示向前或向後移動地址。移動地址的意義:可實現從一個儲存位置到達另一個儲存位置。 - 地址與地址之間也可以相減,表示兩個地址之間的差距。
- 地址與地址之間不可以相加、相乘、相除運算。對地址進行相加、相乘、相除類似門牌號門牌號之間相加、相乘、相除,沒有任何意義可言。
2. 指標變數
變數是一個儲存塊,為了能訪問到變數中的資料,開發者需要為變數指定一個名字,即變數名。編譯器會在分配變數後,把變數
和變數名
進行關聯。
變數名和變數地址有什麼關係?
變數名
是變數的邏輯地址,由開發者提供。而變數地址是變數的實體地址,指變數在記憶體中的具體位置。如下宣告語句,在編譯時,編譯器會做一些細碎的底層工作。
int num=20;
- 根據資料型別的約定,在
記憶體
中找到一個可用的記憶體塊。int
一般大小為4B
。 - 獲取到記憶體塊的實體地址,並把實體地址和開發者提供的變數名(邏輯名)進行關聯,並儲存在對映表中。
- 把數字
20
儲存在num
變數中。
在使用 num
訪問變數時,需要藉助對映表,找到變數名對應的記憶體地址,方能訪問變數中的資料。變數名是變數地址的邏輯名。
std::cout<<num;
//輸出結果:20
能不能獲取到變數在記憶體的地址,通過地址訪問變數?
當然可以,前提是需要宣告一個指標變數,儲存變數的實體地址。
用來儲存地址(指標)型別資料的變數稱為
指標變數
。
指標變數
也是記憶體中的一個儲存塊,只是變數中儲存的是另一個變數
在記憶體中的地址。如下程式碼,儲存 num
變數在記憶體的地址。
//整型型別變數
int num=20;
//指標型別的變數
int* num_p=#
程式碼說明:
int *
表示指標型別
。宣告指標變數時,需要指定變數是用來儲存指標型別
資料。
int *
表示指標變數是用來儲存一個int
型別變數的地址,並不是指變數用來儲存一個整型資料。
&
運算子,取地址運算子。&num
表示獲取num
變數的記憶體(物理)地址。
既然是變數,指標變數在記憶體也有屬於自己的儲存位置。如下圖所示,只是指標變數中儲存的是地址資訊。
指標變數實際佔用記憶體大小是多少,由底層編譯器決定。
如何通過指標變數中的地址訪問 num
變數?
如下程式碼,先試著直接輸出指標變數 num_p
中的資料。
std::cout<<num_p;
輸出結果:0x70fe14
。很明顯這是記憶體地址的 16
進位制格式,也證實指標變數中儲存的是地址。
千萬別問我為什麼輸出的不是
1000
。圖片只是一個演示。
有了這個地址後,可以通過這個地址訪問num
變數中的資料。
std::cout<<*num_p;
//輸出:20
需要注意:在宣告
和通過地址
訪問資料時,都要使用 *
符號:
- 宣告時
*
表示指標型別。int* num_p;
- 使用指標變數時,表示通過地址找到變數中的資料。
*num_p
和 num
是訪問同一個變數的兩種方案。前者是使用物理名(記憶體地址)訪問變數的語法,後者是使用邏輯名(變數名)訪問變數。
同樣的也能夠使用指標變數
對其引用的變數進行賦值。
int num=20;
int* num_p=#
//通過指標變數賦值,和 num=30 等同
*num_p=30;
std::cout<<*num_p<<std::endl;
std::cout<<num<<std::endl;
//輸出結果:
30
30
3. 幾個問題
3.1 為什麼要使用指標變數
在使用指標變數時,總會有一個疑問,既然能夠使用變數名
訪問變數
,為什麼還要搞一個指標變數
。指標變數不僅要佔用記憶體空間,且語法繁瑣,是不是有點囉嗦了。
其實,指標變數
是C
系列語言的特色,是演化過程中保留下來的原始特性:
- 訪問速度。
指標訪問是直接硬體訪問,速度較快。
遍歷陣列時,通過指標的加法、減法運演算法則,可以向前或向後快速移動指標。
int nums[4]={1,2,3,4};
int* nums_p=nums;
for(int i=0;i<4;i++){
std::cout<<*(nums_p+i)<<std::endl;
}
//輸出
1
2
3
4
陣列變數
本質是指標變數,儲存著陣列在記憶體中的首地址。所以在把陣列的地址賦值另一個指標變數時,int* nums_p=nums;
是不需要使用&
符號的。
上述程式碼nums_p+i
讓指標變數能加上一個正整數,實現指標的移動,這裡要注意,加上 1
不是表示只移動一個儲存單元格,而是移動int
大小。
如果知道資料在陣列中的位置,可以直接在首指標基礎上加上一個移動單位,便能快速訪問陣列中的資料。
- 訪問
new
建立的記憶體塊。
如下語句:
int *num01=new int;
new
運算子會在堆中開闢一個用來儲存int
型別資料的儲存塊,返回儲存塊的記憶體地址(指標型別資料) ,這時只能使用指標變數儲存,並且通過指標變數使用這個儲存塊 。
指標變數的存在為使用堆
提供了必要條件,C++
稱堆為動態記憶體區域,開發者可隨時根據自己的需求在程式執行時申請、使用。
理論上講,編譯器也可以讓開發者提供變數名,然後把變數名和
new
返回的地址進行對映。顯然,省略對映環節,直接指標訪問,即減輕了編譯器的負擔,又提升了訪問速度。
int *num01=new int;
*num01=40;
std::cout<<*num01<<std::endl;
//輸出:40
- 使用指標變數作為函式的引數,用來影響函式呼叫處變數中的值。
如果現在有一個需求,使用一個函式交換 2
個變數中的資料。先看一下下面的程式碼是否能實現這個效果。
#include <iostream>
//交換函式
void swap(int num1,int num2){
int tmp=num1;
num1=num2;
num2=tmp;
}
int main(int argc, char** argv) {
int num1=20;
int num2=30;
std::cout<<"交換前:"<<num1<<":"<<num2<<std::endl;
swap(num1,num2);
std::cout<<"交換後:"<<num1<<":"<<num2<<std::endl;
return 0;
}
輸出結果:
交換前:20:30
交換後:20:30
主函式中的 num1
和num2
變數中的資料根本沒有交換。
原因在於呼叫函式swap
時,引數是值傳遞。所謂值傳遞,指把主函式中num1
和num2
變數的值傳遞給swap
函式中的 num1
和num2
變數。swap
的交換邏輯僅修改了自身 2
個變數中的值。
如下圖所示,主函式變數中的資料沒有改變。
如果希望通過呼叫swap
後直接修改主函式中num1
和num2
中的值,可以使用指標變數作引數。
#include <iostream>
//形參為指標型別
void swap(int* num1,int* num2){
//*num1 通過地址訪問主函式中的 num1 變數
int tmp=*num1;
//交換的是主函式中變數中的值
*num1=*num2;
*num2=tmp;
}
int main(int argc, char** argv) {
int num1=20;
int num2=30;
std::cout<<"交換前:"<<num1<<":"<<num2<<std::endl;
//主函式把變數的地址傳遞給 swap 函式
swap(&num1,&num2);
std::cout<<"交換後:"<<num1<<":"<<num2<<std::endl;
return 0;
}
輸出結果:
交換前:20:30
交換後:30:20
指標作為引數,傳遞的是變數地址
,意味著,swap
函式中兩個變數引用了主函式中兩個變數的實體地址。可以實現修改主函式中變數值的目的。
相當於主函式把變數房間的鑰匙傳遞給
swap
函式,swap
再使用鑰匙進入主函式中的變數,進行資料維護。
3.2 指標潛在的風險
3.2.1 初始化風險
必須初始化: 如下程式碼,編譯器不會報任何錯誤,但實際上是沒有任何意義的程式碼。
int* p;
std::cout<<p<<std::endl;
std::cout<<*p<<std::endl;
輸出結果:
0x40ebd9
264275272
當宣告指標變數 p
時,如果沒有指定初始值,編譯器會隨意指定一個值。指望把這個值當成一個有效地址,是沒有意義的。如果把這指標變數用於程式碼邏輯,會產生無中生有的資料,顯然是違背資料的準確性和可靠性。
所以,在宣告指標變數後,一定要對其進行初始化。
不能使用整型常量初始化: 使用整型數字
常量初始化指標變數,編譯層面是通不過的。
//語法錯誤
int* p=0x40aed9;
0x40aed9
即使是一個有效的記憶體地址資料,因為型別不同,也不能把整型資料
賦值給一個指標型別變數。
但是,可以強制型別
轉換後再賦值。
地址
形態上是數字,也僅是形態上是,本質上不是數字型別,不具有數字語義,也不具有數字運算操作能力,不能把地址型別與數字型別混淆。
//正確
int* p=(int*)0x44eb99;
雖然,通過強制轉換可以成功初始化指標變數,但是存在潛在風險:
0x44eb99
地址不一定是一個有效的地址。0x44eb99
即使是一個有效地址,有可能此地址正被其它變數使用。如此,你便修改了其它變數的值。誤打誤撞,跑到了別人家裡。
如下程式碼,本意並不是想讓p
儲存score
變數的記憶體地址。如果恰好0x70fddc
就是score
的記憶體地址。則通過*p
對變數的修改最終會導致score
變數中的資料被修改。
int score=89;
//本意是想使用一個空閒的空間,誤打誤撞引用了 score 的地址
int* p=(int*)0x70fddc;
//會修改 score 中的值
*p=56;
std::cout<<score<<std::endl;
std::cout<<*p<<std::endl;
//輸出
56
56
可以認為指標訪問
是變數名
訪問的另一種形式,所以在初始化指標變數時, 需要使用 &
或 new
運算子合理計算出來的地址。指標變數必須是一個已經存在的、合法變數的記憶體地址。
型別一致初始化: 如下程式碼是錯誤的,千萬不要認為會發生自動型別轉換。num_p
只能引用double
型別變數的地址,這是語法層面約定。
int num=34;
//語法錯誤,宣告指標時的資料型別,嚴格規定了指標變數能引用的變數型別
double* num_p=#
3.2.2 越界風險
指標越界: 指指標移動到了非法區域,如下程式碼:
int num=34;
int* num_p=#
std::cout<<"正常輸出:"<<*num_p<<std::endl;
//指標移到了一個沒有宣告的區域
std::cout<<"移動指標輸出:"<<*(num_p+1)<<std::endl;
輸出結果:
正常輸出:34
移動指標輸出:7405060
雖然指標變數可以通過加上一個整型數字進行移動。但是一定要控制合法範圍,否則會發生如上的非法訪問,非法訪問到的資料一旦用於資料邏輯,會存在很大的風險。
3.3 多級指標
指標變數本身也是一個儲存塊,它所在記憶體地址是否還可以儲存在另一個指標變數中?
顯然,這是可以的,如下程式碼:
//宣告常規變數
int num=20;
//一級指標變數:用來儲存 num 變數的地址
int* num_p=#
//二給指標變數,用來儲存 num_p 變數的地址
int** num_p_p=&num_p;
int**
表示二級指標型別,本質還是記憶體地址,是另一個指標變數的記憶體地址。
使用二級指標訪問 num
變數中的資料。
//……
*(*num_p_p)=30;
std::cout<<"輸出:"<<num<<std::endl;
程式碼解釋:
*num_p_p
獲取到num_p
變數中的記憶體地址值1000
。*(*num_p_p)
利用上面返回的1000
地址,找到變數num
位置,並返回變數num
的值。
同理可以使用多維指標,如下是三維指標。
int num=20;
int* num_p=#
int** num_p_p=&num_p;
int*** num_p_p_p=&num_p_p;
*(*(*num_p_p_p))=30;
std::cout<<"輸出:"<<num<<std::endl;
//輸出:30
4. 總結
雖然可以通過使用指標提升記憶體的訪問效能,但也因存在指標的自由性,易出現潛在風險。如JAVA
在語法層面對指標使用做了限制,權衡利弊,雖然消弱了指標的自由性,同時也降低了程式碼的潛在風險。