深入理解C語言的指針
起源
之前在知乎上看了一句話,指針是C的精髓,也是初學(xué)者的一個坎。換句話說,內(nèi)存管理是C的精髓,C/C++可以直接跟OS打交道,從性能角度出發(fā),開發(fā)者可以根據(jù)自己的實(shí)際使用場景靈活進(jìn)行內(nèi)存分配和釋放。雖然在C++中自C++11引入了smart pointer,雖然很大程度上能夠避免使用裸指針,但仍然不能完全避免,最重要的一個原因是你不能保證組內(nèi)其他人不適用指針,更不能保證合作部門不使用指針。
那么為什么C/C++中會存在指針呢?
這就得從進(jìn)程的內(nèi)存布局說起。
進(jìn)程內(nèi)存布局
上圖為32位進(jìn)程的內(nèi)存布局,從上圖中主要包含以下幾個塊:
- 內(nèi)核空間:供內(nèi)核使用,存放的是內(nèi)核代碼和數(shù)據(jù)
- stack:這就是我們經(jīng)常所說的棧,用來存儲自動變量(automatic variable)
- mmap:也成為內(nèi)存映射,用來在進(jìn)程虛擬內(nèi)存地址空間中分配地址空間,創(chuàng)建和物理內(nèi)存的映射關(guān)系
- heap:就是我們常說的堆,動態(tài)內(nèi)存的分配都是在堆上
- bss:包含所有未初始化的全局和靜態(tài)變量,此段中的所有變量都由0或者空指針初始化,程序加載器在加載程序時為BSS段分配內(nèi)存
- ds:初始化的數(shù)據(jù)塊
- 包含顯式初始化的全局變量和靜態(tài)變量
- 此段的大小由程序源代碼中值的大小決定,在運(yùn)行時不會更改
- 它具有讀寫權(quán)限,因此可以在運(yùn)行時更改此段的變量值
- 該段可進(jìn)一步分為初始化只讀區(qū)和初始化讀寫區(qū)
- text:也稱為文本段
- 該段包含已編譯程序的二進(jìn)制文件。
- 該段是一個只讀段,用于防止程序被意外修改
- 該段是可共享的,因此對于文本編輯器等頻繁執(zhí)行的程序,內(nèi)存中只需要一個副本
由于本文主要講內(nèi)存分配相關(guān),所以下面的內(nèi)容僅涉及到棧(stack)和堆(heap)。
棧
棧一塊連續(xù)的內(nèi)存塊,棧上的內(nèi)存分配就是在這一塊連續(xù)內(nèi)存塊上進(jìn)行操作的。編譯器在編譯的時候,就已經(jīng)知道要分配的內(nèi)存大小,當(dāng)調(diào)用函數(shù)時候,其內(nèi)部的遍歷都會在棧上分配內(nèi)存;當(dāng)結(jié)束函數(shù)調(diào)用時候,內(nèi)部變量就會被釋放,進(jìn)而將內(nèi)存歸還給棧。
class Object { public: Object() = default; // .... }; void fun() { Object obj; // do sth }
在上述代碼中,obj就是在棧上進(jìn)行分配,當(dāng)出了fun作用域的時候,會自動調(diào)用Object的析構(gòu)函數(shù)對其進(jìn)行釋放。
前面有提到,局部變量會在作用域(如函數(shù)作用域、塊作用域等)結(jié)束后析構(gòu)、釋放內(nèi)存。因?yàn)榉峙浜歪尫诺拇涡蚴莿偤猛耆喾吹?,所以可用到堆棧先進(jìn)后出(first-in-last-out, FILO
)的特性,而 C++ 語言的實(shí)現(xiàn)一般也會使用到調(diào)用堆棧(call stack)來分配局部變量(但非標(biāo)準(zhǔn)的要求)。
因?yàn)闂I蟽?nèi)存分配和釋放,是一個進(jìn)棧和出棧的過程(對于編譯器只是一個移動指針的過程),所以相比于堆上的內(nèi)存分配,棧要快的多。
雖然棧的訪問速度要快于堆,每個線程都有一個自己的棧,棧上的對象是不能跨線程訪問的,這就決定了棧空間大小是有限制的,如果棧空間過大,那么在大型程序中幾十乃至上百個線程,光??臻g就消耗了RAM,這就導(dǎo)致heap的可用空間變小,影響程序正常運(yùn)行。
設(shè)置
在Linux系統(tǒng)上,可用通過如下命令來查看棧大?。?/p>
ulimit -s 10240
在筆者的機(jī)器上,執(zhí)行上述命令輸出結(jié)果是10240(KB)即10m,可以通過shell命令修改棧大小。
ulimit -s 102400
通過如上命令,可以將??臻g臨時修改為100m,可以通過下面的命令:
/etc/security/limits.conf
分配方式
靜態(tài)分配
靜態(tài)分配由編譯器完成,假如局部變量以及函數(shù)參數(shù)等,都在編譯期就分配好了。
void fun() { int a[10]; }
上述代碼中,a占10 * sizeof(int)
個字節(jié),在編譯的時候直接計算好了,運(yùn)行的時候,直接進(jìn)棧出棧。
動態(tài)分配
可能很多人認(rèn)為只有堆上才會存在動態(tài)分配,在棧上只可能是靜態(tài)分配。其實(shí),這個觀點(diǎn)是錯的,棧上也支持動態(tài)分配
,該動態(tài)分配由alloca()函數(shù)進(jìn)行分配。棧的動態(tài)分配和堆是不同的,通過alloca()函數(shù)分配的內(nèi)存由編譯器進(jìn)行釋放,無序手動操作。
特點(diǎn)
- 分配速度快:分配大小由編譯器在編譯器完成
- 不會產(chǎn)生內(nèi)存碎片:棧內(nèi)存分配是連續(xù)的,以FIFO的方式進(jìn)棧和出棧
- 大小受限:棧的大小依賴于操作系統(tǒng)
- 訪問受限:只能在當(dāng)前函數(shù)或者作用域內(nèi)進(jìn)行訪問
堆
堆(heap)是一種內(nèi)存管理方式。內(nèi)存管理對操作系統(tǒng)來說是一件非常復(fù)雜的事情,因?yàn)槭紫葍?nèi)存容量很大,其次就是內(nèi)存需求在時間和大小塊上沒有規(guī)律(操作系統(tǒng)上運(yùn)行著幾十甚至幾百個進(jìn)程,這些進(jìn)程可能隨時都會申請或者是釋放內(nèi)存,并且申請和釋放的內(nèi)存塊大小是隨意的)。
堆這種內(nèi)存管理方式的特點(diǎn)就是自由(隨時申請、隨時釋放、大小塊隨意)。堆內(nèi)存是操作系統(tǒng)劃歸給堆管理器(操作系統(tǒng)中的一段代碼,屬于操作系統(tǒng)的內(nèi)存管理單元)來管理的,堆管理器提供了對應(yīng)的接口_sbrk、mmap_等,只是該接口往往由運(yùn)行時庫進(jìn)行調(diào)用,即也可以說由運(yùn)行時庫進(jìn)行堆內(nèi)存管理,運(yùn)行時庫提供了malloc/free函數(shù)由開發(fā)人員調(diào)用,進(jìn)而使用堆內(nèi)存。
分配方式
正如我們所理解的那樣,由于是在運(yùn)行期進(jìn)行內(nèi)存分配,分配的大小也在運(yùn)行期才會知道,所以堆只支持動態(tài)分配
,內(nèi)存申請和釋放的行為由開發(fā)者自行操作,這就很容易造成我們說的內(nèi)存泄漏。
特點(diǎn)
- 變量可以在進(jìn)程范圍內(nèi)訪問,即進(jìn)程內(nèi)的所有線程都可以訪問該變量
- 沒有內(nèi)存大小限制,這個其實(shí)是相對的,只是相對于棧大小來說沒有限制,其實(shí)最終還是受限于RAM
- 相對棧來說訪問比較慢
- 內(nèi)存碎片
- 由開發(fā)者管理內(nèi)存,即內(nèi)存的申請和釋放都由開發(fā)人員來操作
堆與棧區(qū)別
理解堆和棧的區(qū)別,對我們開發(fā)過程中會非常有用,結(jié)合上面的內(nèi)容,總結(jié)下二者的區(qū)別。
對于棧來講,是由編譯器自動管理,無需我們手工控制;對于堆來說,釋放工作由程序員控制,容易產(chǎn)生memory leak
- 空間大小不同
- 一般來講在 32 位系統(tǒng)下,堆內(nèi)存可以達(dá)到4G的空間,從這個角度來看堆內(nèi)存幾乎是沒有什么限制的。
- 對于棧來講,一般都是有一定的空間大小的,一般依賴于操作系統(tǒng)(也可以人工設(shè)置)
- 能否產(chǎn)生碎片不同
- 對于堆來講,頻繁的內(nèi)存分配和釋放勢必會造成內(nèi)存空間的不連續(xù),從而造成大量的碎片,使程序效率降低。
- 對于棧來講,內(nèi)存都是連續(xù)的,申請和釋放都是指令移動,類似于數(shù)據(jù)結(jié)構(gòu)中的
進(jìn)棧和出棧
- 增長方向不同
- 對于堆來講,生長方向是向上的,也就是向著內(nèi)存地址增加的方向
- 對于棧來講,它的生長方向是向下的,是向著內(nèi)存地址減小的方向增長
- 分配方式不同
- 堆都是動態(tài)分配的,比如我們常見的malloc/new;而棧則有靜態(tài)分配和動態(tài)分配兩種。
- 靜態(tài)分配是編譯器完成的,比如局部變量的分配,而棧的動態(tài)分配則通過alloca()函數(shù)完成
- 二者動態(tài)分配是不同的,棧的動態(tài)分配的內(nèi)存由編譯器進(jìn)行釋放,而堆上的動態(tài)分配的內(nèi)存則必須由開發(fā)人自行釋放
- 分配效率不同
- 棧有操作系統(tǒng)分配專門的寄存器存放棧的地址,壓棧出棧都有專門的指令執(zhí)行,這就決定了棧的效率比較高
- 堆內(nèi)存的申請和釋放專門有運(yùn)行時庫提供的函數(shù),里面涉及復(fù)雜的邏輯,申請和釋放效率低于棧
截止到這里,棧和堆的基本特性以及各自的優(yōu)缺點(diǎn)、使用場景已經(jīng)分析完成,在這里給開發(fā)者一個建議,能使用棧的時候,就盡量使用棧,一方面是因?yàn)樾矢哂诙眩硪环矫鎯?nèi)存的申請和釋放由編譯器完成,這樣就避免了很多問題。
擴(kuò)展
終于到了這一小節(jié),其實(shí),上面講的那么多,都是為這一小節(jié)做鋪墊。
在前面的內(nèi)容中,我們對比了棧和堆,雖然棧效率比較高,且不存在內(nèi)存泄漏、內(nèi)存碎片等,但是由于其本身的局限性(不能多線程、大小受限),所以在很多時候,還是需要在堆上進(jìn)行內(nèi)存。
我們先看一段代碼:
#include <stdio.h> #include <stdlib.h> int main() { int a; int *p; p = (int *)malloc(sizeof(int)); free(p); return 0; }
上述代碼很簡單,有兩個變量a和p,類型分別為int和int *,其中,a和p存儲在棧上,p的值為在堆上的某塊地址(在上述代碼中,p的值為0x1c66010),上述代碼布局如下圖所示:
總結(jié)
本篇文章就到這里了,希望能夠給你帶來幫助,也希望您能夠多多關(guān)注腳本之家的更多內(nèi)容!
相關(guān)文章
C++浮點(diǎn)數(shù)在內(nèi)存中的存儲詳解
大家好,本篇文章主要講的是C++浮點(diǎn)數(shù)在內(nèi)存中的存儲詳解,感興趣的同學(xué)趕快來看一看吧,對你有幫助的話記得收藏一下2022-01-01解析C++的線性表鏈?zhǔn)酱鎯υO(shè)計與相關(guān)的API實(shí)現(xiàn)
這篇文章主要介紹了解析C++中的線性表鏈?zhǔn)酱鎯υO(shè)計與相關(guān)的API實(shí)現(xiàn),文中的實(shí)例很好地體現(xiàn)了如何創(chuàng)建和遍歷鏈表等基本操作,需要的朋友可以參考下2016-03-03利用C++11原子量如何實(shí)現(xiàn)自旋鎖詳解
當(dāng)自旋鎖嘗試獲取鎖時以忙等待(busy waiting)的形式不斷地循環(huán)檢查鎖是否可用,下面這篇文章主要給大家介紹了關(guān)于利用C++11原子量如何實(shí)現(xiàn)自旋鎖的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),需要的朋友可以參考下2018-06-06