目錄
1.內存和地址
2.指針變量和地址
2.1 取地址操作符(&)
2.2 指針變量
2.3?解引用操作符(*)
2.4 指針變量的大小
3.指針變量類型的意義?
3.1 指針的解引用
3.2 指針 + - 整數
3.3 void*指針?
4.指針運算
4.1 指針 + - 整數?
4.2 指針 - 指針
4.3 指針的關系運算
正文開始
1.內存和地址
? ? ? ? 在這里,我們先提一下存儲器的概念。在計算機中,存儲器可以分為:
- 主存儲器:又稱主存、內存。用來存放計算機運行期間所需的程序和數據,CPU可以直接隨機地對其進行訪問。特點是容量小、存取速度快、造價高。
- 輔助存儲器:又稱輔存、外存。用來存放當前暫時不用的程序和數據,以及一些需要永久性保存的信息。外存的內容需要調入主存后才能被CPU訪問。特點是容量大、存取速度慢、造價低。我們常見的硬盤就屬于外存。
- 高速緩沖存儲器:又稱cache。位于主存和CPU之間,用來存放當前CPU經常使用的指令和數據,以便CPU能夠高速地訪問它們。特點是容量小、價格高。現代計算機一般將cache制作到CPU中。
? ? ? ? 可知,內存其實是電腦上的存儲設備,程序在運行的過程中,需要向內存申請空間來使用。
? ? ? ? 我們可以舉一個生活中的例子,在大學里,同一個專業的同學,一般是在同一棟宿舍樓中的。而在一棟宿舍樓中,有不同的房間號,比如:1樓有101、102、103...,2樓有201、202、203...,3樓有301、302、303...。如果你想找到你班上的某個同學,要是一層層樓,挨個房間去找,效率會非常低。但是,如果我們知道某個同學的地址,也就是所在宿舍的門牌號,就可以根據地址直接找到這個同學了。
? ? ? ? 如果把上面的例子推廣到計算機中,是怎么樣的呢?
? ? ? ? 我們知道,在計算機中,CPU處理數據的時候,所需的數據是從內存中讀取的,處理后的數據也會放回到內存中。我們日常使用的電腦,有8G、16G、32G......,這些內存空間是如何高效管理的呢?
? ? ? ? 其實就是把內存劃分為一個個的內存單元,每個單元的大小為1個字節,1個字節含有8個比特位,每個比特位可以存放1個二進制的0或1。
? ? ? ? 計算機中常見的單位如下:
- bit:比特位,簡稱位,縮寫為b。是計算機中最小的存儲單位,1bit表示1個二進制位(0或1)。
- Byte:字節,縮寫為B。1Byte = 8bit。
- KB:千字節。1KB =? 1024B,? ?即2的10次方B。
- MB:兆字節。1MB = 1024KB,即2的20次方B。
- GB:吉字節。1GB = 1024MB,即2的30次方B。
- TB:太字節。1TB? = 1024GB,即2的40次方B。
- PB:拍字節。1PB = 1024TB, 即2的50次方B。
? ? ? ? 如上圖,從下往上,給每一個內存單元編了號:0,1,2,3...,用16進制來表示。
? ? ? ? 每個內存單元,都有1個字節的空間,也就是8個比特位的空間,相當于1個宿舍里,有8個床位的空間。
? ? ? ? 每個內存單元,都有一個編號,相當于宿舍的門牌號。有了這個門牌號,就可以快速找到一個同學。有了這個內存單元的編號,CPU就可以快速找到一個內存空間。
? ? ? ? 生活中, 我們把門牌號也叫作地址。在計算機中,我們把內存單元的編號也叫地址。在C語言中,給地址起了新的名字:指針。
? ? ? ? 可以這樣理解:在C語言中, 內存單元的編號 = 地址 = 指針,三種說法是一樣的。
? ? ? ? 那么,我們怎么理解編址呢?
? ? ? ? 如上圖,我們知道, CPU和內存之間,有大量的數據交互,為了實現這種交互,兩者之間是用線連接起來的。
? ? ? ? CPU是完成計算工作的,首先要從內存中讀數據,計算完畢后,又將結果的數據寫入內存。
? ? ? ? 這個過程是:首先,控制總線發出Read指令,然后CPU會給出一個地址,通過地址總線向內存傳遞一個地址信號,從內存中找到該空間,再將該空間中的數據通過數據總線傳遞給CPU。在CPU利用所讀的數據進行計算,得出結果之后,控制總線發出Write指令,然后CPU給出一個寫入地址,通過地址總線向內存傳遞有一個地址信號,在內存中找到該空間,把結果的數據寫入這個空間中。
? ? ? ? 我們再來討論一下編址:
? ? ? ? CPU要訪問內存中的某個字節空間,必須知道這個字節空間在內存中的什么位置, 而內存中有很多個字節,就需要給內存進行編址了。就好比,一棟樓中有很多個宿舍,需要給每個宿舍進行編號。
? ? ? ? 計算機中的編址,并不是把每個字節的地址記錄下來,而是通過硬件設計完成的。
? ? ? ? 就好比,吉他上面沒有寫:哆、來、咪、發、梭、拉、西。但是演奏者也能準確找到每一個音所在的位置,這是為什么呢?因為制造商已經在樂器的硬件層面上設計好了,并且所有的演奏者也知道,本質上是一種約定的共識。硬件編址也是如此。
? ? ? ? 我們可以簡單理解為:32位機器有32根地址總線,每根線只有2種狀態,電脈沖的有無代表0和1。那么1根線能表達2種含義,2根線能表達4種含義,......,32根線能表達2的32次方種含義。每一種含義都代表一個地址。
? ? ? ? 地址信息被下達給內存,在內存中,通過硬件的設計,就可以直接找到該地址中對應的數據,這個數據再通過數據總線傳給CPU內的寄存器。
2.指針變量和地址
2.1 取地址操作符(&)
? ? ? ? 在了解了內存和地址之后,我們回到C語言。在C語言中,創建1個變量,其實就是在向內存申請空間。比如:對于int a = 10;,實質就是,向內存申請4個字節的空間,把10存放進去。如下圖:
? ? ? ? 如上,創建了整型變量a,向內存申請了4個字節的空間,用于存放整數10。
? ? ? ? 我們使用取地址操作符(&),取出了4個字節中地址較小的那個地址。在printf打印時,對應的占位符是%p。
? ? ? ? int變量占4個字節,我們只要知道了第一個字節的地址,就可以順藤摸瓜訪問到剩下3個字節的數據,因為相鄰地址之間相差1。
2.2 指針變量
? ? ? ? 剛剛,我們使用取地址操作符(&),取出了變量a的地址,如果我們想把這個地址存到一個變量里面去,該怎么操作呢?如下:
? ? ? ? 指針就是地址,指針變量是專門用來存放地址的變量。應該知道的是,上面的pa,在日常生活中,指針變量有時會被口頭語叫做指針,而指針實際上是地址,而非變量。
? ? ? ? 對于int類型的變量a,它的地址存放在int*類型的pa中。那么舉一反三,如果有一個char類型的變量c,它的地址應該存放在什么類型的指針變量中呢?如下:
2.3?解引用操作符(*)
? ? ? ? 指針變量是用來存放地址的,存起來的地址有啥用呢?當然是用來使用的,怎么使用呢?我們可以看下面的例子:
? ? ? ? 如上,變量pa中存放了變量a的地址。*pa的意思就是 ,通過pa中存放的地址,找到這個地址所指向的空間,也就是找到變量a。其實,*pa就是a,*pa=20這個操作就是把a改成了20。我們可以直接通過a=20來修改a的值,但是用指針來修改,就多了一種途徑,寫起代碼來會更靈活,而且在某些情況下,指針是會起到它的作用的。
2.4 指針變量的大小
? ? ? ? 我們知道,各種類型的變量,都是有其大小的,比如:int的大小是4個字節,char的大小是1個字節。那么,指針變量的大小是多少呢?我們來看如下代碼:
? ? ? ? 如上圖,在x64環境,即64位環境下,int*和char*的長度都是8。在x86環境,即32位環境下,int*和char*的長度都是4。這是為什么呢?
? ? ? ? 在前面,我們提到過,對于32位機器,一般有32根地址總線,每根線傳遞的電信號轉換成數字信號,是0或1。我們把32根地址線產生的二進制序列當做一個地址,那么地址在總線上傳輸的時候,長度就是32個bit位,即4個字節,在內存中存儲時,和它傳遞時的長度是一樣的,也是4個字節。
? ? ? ? 同理,對于64位機器,一般有64根地址總線,存儲的地址就有64個bit位的空間, 也就是8個字節,在總線上傳輸和內存中存儲的長度就是4個字節了。
? ? ? ? 結論:
- 32位平臺下,地址是32個bit位,指針變量的大小是4個字節。
- 64位平臺下,地址是64個bit位,指針變量的大小是8個字節。
- 指針變量的大小,與指針變量的類型無關。
3.指針變量類型的意義?
? ? ? ? 剛剛我們發現,無論什么類型的指針變量,在相同的平臺上,它的大小都是相同的,和類型沒有關系。那么,指針變量的類型有什么意義呢?
3.1 指針的解引用
? ? ? ? 請看代碼:
? ? ? ? 如上,在創建變量a之后,我們取a的地址,此時4個字節中依次存放著:44、33、22、11。
? ? ? ? 如上,在使用*pa修改a的值為0之后,內存中的4個字節全部被修改為0,呈現出:00、00、00、00、00。
? ? ? ? 在上面,我們是用int* pa來接受a的地址的,如果換成char*類型會怎么樣呢?
? ? ? ? 如上,我們發現,如果指針變量pa的類型為char*,那么在執行*pa=0后,內存中只有第1個字節被修改為00,呈現出:00、33、22、11。
? ? ? ? 如上,運行結果顯示,變量a的值確實沒有修改為0,而是為第1個字節修改為0之后的結果。
? ? ? ? 其實,指針的類型決定了,在指針解引用的時候,一次能操作幾個字節。
? ? ? ? 比如:char*的指針解引用就只能訪問1個字節,而int*的指針解引用能訪問4個字節。
? ? ? ? 我們還可以用下面的方式驗證一下,pa由于是char*的類型,所以在打印的時候,也只能打印出第1個字節的內容,只有使用int*才能完整地打印出4個字節所表示的值。如下:
3.2 指針 + - 整數
? ? ? ? 請看代碼:
? ? ? ? 如上,a、pa、pc的地址都是一樣的,這當然應該一樣。
? ? ? ? 但是,pa+1后, 地址增加了4,而pc+1后,地址只增加了1。
? ? ? ? 我們來看看圖示:
? ? ? ? 結論:
- 指針的類型決定了指針向前或向后走一步,有多大(距離)。
- 指針+n,其實就是跳過n個指針所指向元素的長度。
- 指針可以+n,也可以-n。
3.3 void*指針?
? ? ? ? 在前面我們已經了解到, 指針變量是有很多種類型的,比如:int*表示指針指向的是int類型的變量、char*表示指向char類型的變量、short*表示指向short類型的變量、double*表示指向double類型的變量......
? ? ? ? 其實,在指針類型中,有一種特殊的類型叫void*類型,可以理解為,無具體類型的指針(或泛型指針)。
? ? ? ? void*指針可以接收任意類型的地址。
? ? ? ? void*指針的局限性在于,不能直接進行指針的解引用操作和+-整數操作。
? ? ? ? 我們可以看到,void*指針可以接收不同類型的地址,但是無法直接進行指針運算。那么,void*指針到底有什么用呢?
? ? ? ? 一般,void*指針是在函數的參數部分使用的,用來接收不同類型的數據,這樣的設計可以實現泛型編程的效果,使得一個函數可以處理多種類型的數據。
4.指針運算
? ? ? ? 指針的基本運算有3種,如下:
- 指針+-整數
- 指針-指針
- 指針的關系運算
4.1 指針 + - 整數?
? ? ? ? 假如要遍歷一個數組,打印出每一個元素,我們首先想到的可能是如下方法:
? ? ? ? 其實,我們還可以使用指針的方式來遍歷數組。
? ? ? ? 我們知道,數組在內存中是連續存放的,只要知道了第一個元素的地址,就能順藤摸瓜找到后面的所有元素。我們可以將首元素的地址存放在變量p中,*p訪問的是第1個元素,*(p+1)訪問的是第2個元素,*(p+2)訪問的是第3個元素......循環下去,我們就能遍歷數組了,請看下圖:
? ? ? ? 當然了,我們也可以用指針實現倒序打印數組,把數組的尾元素地址交給指針變量p,*p訪問的是倒數第一個元素,*(p-1)訪問的是倒數第二個元素......循環下去即可,請看代碼:
4.2 指針 - 指針
? ? ? ? 關于指針減去指針,我們要注意的是:
- 指針 - 指針的前提是:2個指針指向同一塊空間。
- 指針 - 指針得到的是:2個指針之間的元素個數。?
? ? ? ? 請看代碼:
? ? ? ? 我們再來看看圖示:
? ? ? ? 我們可以聯想到前面所提到的指針 + - 整數:
? ? ? ? 那么,指針 - 指針有什么用呢?我們來看看它的應用:
? ? ? ? 如上,為了統計字符串中,\0之前的字符個數,我們使用了strlen這個庫函數。那么,我們能不能不使用庫函數,而是自己寫一個函數,來統計字符個數呢?
? ? ? ? 我們先來看看指針+1的方式:
? ? ? ? 我們再來思考一下,用指針 - 指針的方式來解決這個問題。如果我們能夠找到'\0'的指針,用'\0'的指針減去首元素的指針,就可以得到元素個數了。請看代碼:
4.3 指針的關系運算
? ? ? ? 指針的關系運算,其實就是地址比較大小。我們知道,地址其實就是一種編號,是數值,當然可以比較大小了。
? ? ? ? 我們還是以遍歷數組為例子:
完結
?
????????
????????