C++ 指针基本概念
Ⅰ.內存和地址
我們已經很熟悉一些基本的存儲單位了,比如一個bit(位)用存儲0或者1.也可以把幾個bit合起來表示更大的數字,比如一個byte(字節)就包含了8個bit.這些都是很基礎很簡單的東西.然后我們可以把計算機的內存想象成一個字節數組,內存中的每一個地址表示一個字節.?
?
每個字節中都能夠存儲一定位數的內容,因此,每個字節都能夠通過一些地址來標識.有時候,一個字節不夠,怎么辦呢?那么就同時用很多個字節來表示,比如一個int在我的系統里面就用了4個字節。?
下面的圖是上面那幅圖片每兩個字節合并在一起之后樣子.這個因為是兩個字節合并,所以它成為比一個字節更大的內存單位(廢話),能夠存儲的信息就更多(廢話).但是雖然一個字是兩個字節合并的,但是它仍然只包含一個地址,也就是說,合并之后只需要一個地址來找到合并之后的內存.?
?
因此,我們可以得到:?
1.內存中的每個位置都由一個獨一無二的地址表示.?
2.內存中的每個位置都包含一個值.?
通俗一點,我們可以通過一個地址,來找到內存中的某個具體位置,然后訪問到(得到)該位置的值(允許的話).這就是內存和地址簡單的思想.
Ⅱ.指針含義與創建方式
指針這個名字確實折磨過很多人,這個名字是個好名字,同時也是一個非常不好的名字.說它好,是因為指針這個東西很形象的體現了它的功能:指針指針,指到某個地方某個位置.非常形象.它不是個好名字是因為她的名字有時候掩蓋了它的真實含義.一般來說,指針是一個其值為地址的變量。(就是一個存儲地址的變量)?
所以,要養成一種條件反射,看到指針首先不是想到他能夠指向哪里,而是想到這個變量存放的是一個地址,是這個地址指向哪里哪里.(比如,char類型的變量中存放的是char類型的數據.int變量中存放的是int類型的數據.指針中存放的是一個地址!!!)?
反復的把上面的概念消化之后,我們就來看兩個基本的運算符:&(取址運算符)和*(間接訪問運算符/解引用指針)
首先是&運算符:當它后面跟一個變量名的時候,給出這個變量名的地址.
#include<iostream>
usingnamespace std;
int main()
{
?? int a=5;
?? double b=10.4;
?? cout<<"Address ofa:"<<&a<<endl;
?? cout<<"Address ofb:"<<&b<<endl;
}
至于*運算符:就是后面跟一個指針的時候,得到指針指向的內存中的內容.
#include<iostream>
usingnamespace std;
int main()
{
?? int a=5;
?? double b=10.4;
?
?? cout<<"Address ofa:"<<&a<<endl;
?? cout<<"Address ofb:"<<&b<<endl;
?
?? cout<<"a:"<<*(&a)<<endl;
}
?
通過上面的例子你會發現,這里輸出的地址是16進制的整形.其實事實上,在大多數系統的內部,指針所存的地址值一般是一個無符號的整數。但是,這并不代表指針和整形有一樣的運算規則。指針類型是一種新的類型,而不是一種整數類型。ANSI專門為指針提供了%p輸出格式
理解上面兩個基本的運算符之后,就可以正式開始講指針的創建了.?
聲明指針的模板:
指向地址的數據類型 * 指針變量名;
- 1
其中,*號必須帶,用以表明現在創建的是一個指針類型的變量.同時,當你看到創建變量的語句中帶有星號*的話,那么說明那個變量必定是一個指針變量!?
是不是很簡單!舉一個例子進一步來理解上面那個的含義.比如我想創建一個指針變量(存放地址的變量),這個指針(地址)是指向一個存儲整形的內存.那么我就可以寫為:int * leo;同理,指向char的我可以寫成char * c;其實是很簡單的.?
這里結合前面的內容,簡單的寫一個例子,并且介紹一些寫法.(最開始讓初學者迷惑的地方就是這里了,因為創建時候的*號,解除指針時候的*號,各種符號混在一起,一般就直接懵逼了.但是要是好好掌握一些經驗結論,這里很容易過去.)
#include<iostream>
usingnamespace std;
int main()
{
? int a=5,b=6,c=7,d=8;
? double e=3.1415;
? //單獨賦值,并且*和p_a緊挨著
? int *p_a=&a;
? //多個賦值(既有指針,又有普通變量)
? int * p_b=&b,* p_c=&c,*p_d=&d,temp=100;
?
? //單獨賦值,double類型
? double * p_e=&e;
?
? cout<<p_a<<endl<<p_b<<endl<<p_c<<endl<<p_d<<endl<<p_e<<endl;
? cout<<temp<<endl;
}
上面這個例子有一些可以提煉的經驗:?
首先,就是創建指向int或者double的指針的創建方式,這個前面都講了很多次了.在這個例子里面也可以很容易的找到,所以這里就不啰嗦了.?
然后就是創建時候的寫法.比如?int *p_a=&a;這句話中,*號是緊挨著p_a的,而在int * p_b=&b,* p_c=&c,*p_d=&d,temp=100;?這句話中,*號是可以不挨著p_b和p_c的.也就是說,創建指針變量的時候,星號的位置是很自由的.只要是出現了星號,而不管中間是不是有空格,我們便認為,這算是創建了一個指針變量.?
在別人寫的代碼中,你會看到很多的寫法,其實本質就是這樣.比如有些人喜歡寫成int* p_a=&a;在這里,*號緊挨著int,因為有人理解為int的指針類型即int*.所以,寫法這么多,初學者肯定是會迷惑的,記住前面的經驗,這樣就見怪不怪了.?
接下來,還是在int * p_b=&b,* p_c=&c,*p_d=&d,temp=100;這句話里面,最后接了一個temp=100,千萬不要也把temp也當做了一個指針變量,他只是一個普通的變量.也就是說,同一句話里面,可以混合多種類型的賦值,指針的帶*號,普通的不帶*號.
Ⅲ.使用指針
使用指針的方式有很多,我們這里談談最基礎的,后面會更加深入的講指針的使用.前面已經講過了指針的創建,接下來主要是講指針的初始化問題和賦值以及解除引用等等問題.?
首先是初始化問題.在前面的那一個例子中,我們在創建指針的時候都順便初始化了,那是不是創建指針就一定要初始化?肯定不是!創建指針的時候也可以不初始化.因為有時候你創建一個指針只是為了后續的工作,根本沒有東西拿來初始化.那么到現在,我們解決了第一個問題,那就是創建指針可以初始化也可以不初始化,.那么你肯定會說,這么簡單為什么要單獨拿出來講?是因為后來的某些操作是要考慮是否初始化的問題的.好了,不繞彎了,這里的操作主要是講間接訪問(解引用指針)帶來的一些問題.不多說,直接上例子.
#include<iostream>
usingnamespace std;
int main()
{
??? int num=5;
?
??? //p_a沒有初始化
??? int *p_a1,*p_a2;
??? cout<<"p_a1:"<<p_a1<<endl;
??? cout<<"p_a2:"<<p_a2<<endl;
?
??? int *p_b=#
??? cout<<"p_b:"<<p_b<<endl;
?
??? //同類型指針可以賦值
??? //沒有初始化的指針依然可以被賦值(地址)
??? p_a1=p_b;
??? p_a2=#
??? cout<<"changedp_a1:"<<p_a1<<endl;
??? cout<<"changedp_a2:"<<p_a2<<endl;
}
?
一句一句來分析這個例子.首先我們創建了一個整形,值為5,然后我們創建了兩個指向int類型的指針p_a1,P_a2,但是我們沒有初始化.從最后的運行結果來看,指針不初始化是可行的.然后創建了一個指針p_b并且初始化了,所以從結果來看,就是num的地址.?
接下來有兩句:p_a1=p_b; p_a2=#?
前面那句是直接把p_b這個指向int類型的指針直接賦給了p_a1這個也是指向int類型的指針,那么從效果上面來看,相當于p_a1和p_b指向同樣的地址…..好了,不裝逼了,因為指針就是存儲地址的嘛,其實就是把這個變量中存儲的地址賦給了另外一個變量…..非常簡單.?
后面第二句就是說,即使不初始化,我還是能夠接收地址.(類比int n;n=5).?
所以總結起來就是即使沒有初始化,我依然能夠兩個指針之間賦值(類型肯定要一樣啦)和接受地址.也沒有什么困難的.那么接著看.
把上面的代碼小小的改動一些,代碼變成下面這樣.
#include<iostream>
usingnamespace std;
int main()
{
??? int num=5;
?
??? //p_a沒有初始化
??? int *p_a1,*p_a2;
??? cout<<"p_a1:"<<p_a1<<endl;
??? cout<<"p_a2:"<<p_a2<<endl;
?
??? *p_a1=12;
??? cout<<*p_a1<<endl;
?
}
這里就加了一句*p_a1=12;這句話是很危險的.我們創建了一個指向int的指針,但是并沒有初始化,也就是說,指針會得到一個隨機的地址(至少大部分系統上面是這樣),可創建指針的過程并不包含對于某個整形內存上空間的開辟.?
*p_a1=12;的過程就是把12這個整形放到p_a1指向的內存的過程.但是空間都沒有開辟,怎么放呢?所以這個語句最終是不是運行成功都取決于運氣.不同的系統上面可以有不同的結果.但是這樣的話就算是運行成功了又有什么意義呢??
所以,其他的都不說,至少在解引用指針的時候,你需要保證你的指針被正確的初始化或者正確的被賦過某個地址.不然,那樣的解引用指針操作無意義且危險.
重要:?
既然前面談到了解引用指針(間接訪問),那就再來說說指針常量與指針的強制轉化.?
假如我們想在一個地址500存放一個整數100,沒錯,我們已經知道這個地址是500了,所以我們就能夠這么賦值
*500=100;
這句話的意思是很明確的,先把地址500解引用然后把100放進這個內存.但是這句話是錯的,因為前面說過指針類型是一種特殊的類型.但是我們這里的500就是一個很普通的整形.他是能夠表示500這個地址沒有錯,但是它的類型不適合.因此,我們要把這個普通的整形強制轉換為指向整形的指針類型.因此可以這樣寫
*(int *)500=100
其實使用這個的機會很少,但是并不意味這個不重要,首先在某些硬件問題里面確實是想訪問某些硬件上面特定的地址的時候我們可以用這個方法.后面講到內存管理的時候,也會回來這里.
Ⅳ.指針運算
指針的運算有算術運算和關系運算(比較大小等等),但是在這里僅僅是提一下,因為這部分的內容是和后面指針與數組有關系的.在后面指針與數組會單獨講這些.
Ⅴ.NULL指針和void*
有很多對于指針不是很熟悉的童鞋在上數據結構課的時候一般是很懵逼的,因為用C講數據結構的時候,涉及到很多的內存開辟回收,以及指針問題(鏈式結構).然后很多人沒有學好不是因為邏輯能力差,而是因為在一些代碼或者是偽代碼中對于指針的認識模棱兩可.以至于本來代碼是學習數據結構思想的好工具,變為了學習數據結構的負擔.
首先先講NULL指針.?
NULL指針是一種非常特殊的指針(廢話,不然單獨說它干嘛?),不指向任何東西.表示不指向任何東西的指針(哦,感覺好厲害的樣子).但是事實上面,這種技巧非常有用.(當你被鏈表虐的時候就知道了).?
我們怎么把一個指針變為NULL指針呢?很簡單,賦給這個指針一個0就行了.不用轉化,就是整形的0.同樣,直接賦NULL也是行的.看下面的例子.
#include<iostream>
usingnamespace std;
int main()
{
??? //測試NULL是不是0
??? if(NULL==0)
??????? cout<<"yes"<<endl;
?
??? //轉化為NULL指針
??? int *p_a=0;
??? int *p_b=NULL;
?
}
?
例子很簡單,就不解釋了.?
有一個非常重要的是,因為NULL不指向任何地方,所以,也就肯定不能夠解引用了.這點一定要注意.因為連地址都沒有,怎么得到不存在的地址中的值呢?所以要是想你的程序健壯,最好是在解引用之前加一個判斷是否為NULL指針的步驟,要是你有足夠的信心以后都不會有問題,那么不加也罷.?
下面這個例子是當我試圖對一個NULL指針解引用之后的程序運行情況.(就是上面那個例子加了一句話而已)
#include<iostream>
usingnamespace std;
int main()
{
??? //測試NULL是不是0
??? if(NULL==0)
??????? cout<<"yes"<<endl;
?
??? //轉化為NULL指針
??? int *p_a=0;
??? int *p_b=NULL;
?
??? //試圖解引用
??? cout<<*p_a<<endl;
}
?
直接崩了,其實也挺好,至少比找不到的隱形錯誤要好.?
前面的例子中有一句是判斷NULL==0的,這里需要注意一下,NULL是一個預處理的變量,值為0,在頭文件cstdlib中定義,(我這里并沒有顯式載入也能夠用),因此用到NULL這個預處理變量的時候,盡量帶上cstdlib頭文件,規范一點,避免不必要的錯誤.?
說到這里,其實你反而應該疑惑了,我們前面已經說過了,指針存放的是一個地址.一般來說,地址是整形沒錯,但是它是一種新的類型來表示地址.和整形并不能夠兼容或者運算.但是當使用0來表示空指針的時候,我們便會疑惑,0到底是整形常量還是一個指針常量??
因此,在C++11中,新引入了一種特殊類型的字面值nullptr來初始化指針為空指針.他能夠被轉換成任何類型的指針.
#include<iostream>
int fun(int num)
{
??? return num+10;
}
int main()
{
??? //表達式0==nullptr為真值
??? if(0==nullptr)
??????? std::cout<<"yes"<<std::endl;
?
??? int a=5;
??? int *p=nullptr;
??? p=&a;
??? std::cout<<"Address ofa:"<<p<<std::endl;
??? return 0;
}
?
講到現在,發現前面很多的錯誤都是由于解引用沒有初始化的指針引起來的.所以,這里提個建議,就是盡量定義了對象之后再定義指向這個對象的指針,對于不清楚的指向哪里的指針,一律初始化為nullptr(C++11)或者NULL(0).之后再判斷是否指向對象再進行相應的操作.
接下來講void*.?
Void*是一種特殊類型的指針,能夠用來存放任何類型對象的地址.通俗來說,就是我不知道這個指針指向的是什么類型的對象.
要是還是理解不了那就上例子:
#include<iostream>
int fun(int num)
{
??? return num+10;
}
int main()
{
??? double x=25.5;
??? //普通指針的話類型要嚴格保證
??? double *p=&x;
?
??? //void* 類型可以接受任意類型對象地址
??? void *p_v=&x;
??? void *p_v2=p;
??? std::cout<<"p_v:"<<p_v<<std::endl;
??? std::cout<<"p_v2:"<<p_v2<<std::endl;
}
?
Void *暫時了解到這里就行了,后面內存分配的時候還會專門和他打交道.
Ⅵ.指針的指針
指針的指針就是指向指針的指針.再多講就繞暈了.直接看定義和例子吧.
#include<iostream>
int main()
{
?? int a=10;
?? int *p_a=&a;
?? int **pp_a=&p_a;
?
?? std::cout<<"p_a:"<<p_a<<std::endl<<"*p_a:"<<*p_a<<std::endl;
?? std::cout<<std::endl;
?
?? std::cout<<"PP_a:"<<pp_a<<std::endl<<"*pp_a:"<<*pp_a<<std::endl<<"**pp_a"<<**pp_a<<std::endl;
?
}
?
通過這個例子,我們發現其創建方式也是和普通指針的創建方式是差不多的,除了創建時候的兩個星號**.那創建指針必定需要指針類型,指針類型怎么選擇呢?通俗一點說,就是跟著前面的走:我們發現創建整形時候使用的是int,比如這里的int a=10;.那我們創建指向a的指針的時候,肯定必須也要是int了,比如這里的int *p_a=&a;.最后創建指針的指針的時候,也就用int了.比如這里的int **pp_a=&p_a;?.并不困難?
另外一個就是解引用的時候,帶上兩個星號,就回到的最開始的那個變量.這么說有點模糊.詳細來說,**pp_a因為*的從右向左的結合性,這個表達式可以寫成*(*pp_a),那么我們知道pp_a存放的是p_a的地址,*pp_a就是表示p_a這個地址中存放的內容即a的地址(不能暈!!!).那么*(*pp_a)就相當于*p_a或者a.?
至此,基本的概念部分就結束啦.
?
總結
以上是生活随笔為你收集整理的C++ 指针基本概念的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: matlab 中括号
- 下一篇: C++ 运算符优先级