深入理解C語言的指針
起源
之前在知乎上看了一句話,指針是C的精髓,也是初學(xué)者的一個坎。換句話說,內(nèi)存管理是C的精髓,C/C++可以直接跟OS打交道,從性能角度出發(fā),開發(fā)者可以根據(jù)自己的實際使用場景靈活進行內(nèi)存分配和釋放。雖然在C++中自C++11引入了smart pointer,雖然很大程度上能夠避免使用裸指針,但仍然不能完全避免,最重要的一個原因是你不能保證組內(nèi)其他人不適用指針,更不能保證合作部門不使用指針。
那么為什么C/C++中會存在指針呢?
這就得從進程的內(nèi)存布局說起。
進程內(nèi)存布局
上圖為32位進程的內(nèi)存布局,從上圖中主要包含以下幾個塊:
- 內(nèi)核空間:供內(nèi)核使用,存放的是內(nèi)核代碼和數(shù)據(jù)
- stack:這就是我們經(jīng)常所說的棧,用來存儲自動變量(automatic variable)
- mmap:也成為內(nèi)存映射,用來在進程虛擬內(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)變量
- 此段的大小由程序源代碼中值的大小決定,在運行時不會更改
- 它具有讀寫權(quán)限,因此可以在運行時更改此段的變量值
- 該段可進一步分為初始化只讀區(qū)和初始化讀寫區(qū)
- text:也稱為文本段
- 該段包含已編譯程序的二進制文件。
- 該段是一個只讀段,用于防止程序被意外修改
- 該段是可共享的,因此對于文本編輯器等頻繁執(zhí)行的程序,內(nèi)存中只需要一個副本
由于本文主要講內(nèi)存分配相關(guān),所以下面的內(nèi)容僅涉及到棧(stack)和堆(heap)。
棧
棧一塊連續(xù)的內(nèi)存塊,棧上的內(nèi)存分配就是在這一塊連續(xù)內(nèi)存塊上進行操作的。編譯器在編譯的時候,就已經(jīng)知道要分配的內(nèi)存大小,當(dāng)調(diào)用函數(shù)時候,其內(nèi)部的遍歷都會在棧上分配內(nèi)存;當(dāng)結(jié)束函數(shù)調(diào)用時候,內(nèi)部變量就會被釋放,進而將內(nèi)存歸還給棧。
class Object { public: Object() = default; // .... }; void fun() { Object obj; // do sth }
在上述代碼中,obj就是在棧上進行分配,當(dāng)出了fun作用域的時候,會自動調(diào)用Object的析構(gòu)函數(shù)對其進行釋放。
前面有提到,局部變量會在作用域(如函數(shù)作用域、塊作用域等)結(jié)束后析構(gòu)、釋放內(nèi)存。因為分配和釋放的次序是剛好完全相反的,所以可用到堆棧先進后出(first-in-last-out, FILO
)的特性,而 C++ 語言的實現(xiàn)一般也會使用到調(diào)用堆棧(call stack)來分配局部變量(但非標(biāo)準(zhǔn)的要求)。
因為棧上內(nèi)存分配和釋放,是一個進棧和出棧的過程(對于編譯器只是一個移動指針的過程),所以相比于堆上的內(nèi)存分配,棧要快的多。
雖然棧的訪問速度要快于堆,每個線程都有一個自己的棧,棧上的對象是不能跨線程訪問的,這就決定了??臻g大小是有限制的,如果??臻g過大,那么在大型程序中幾十乃至上百個線程,光??臻g就消耗了RAM,這就導(dǎo)致heap的可用空間變小,影響程序正常運行。
設(shè)置
在Linux系統(tǒng)上,可用通過如下命令來查看棧大?。?/p>
ulimit -s 10240
在筆者的機器上,執(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é),在編譯的時候直接計算好了,運行的時候,直接進棧出棧。
動態(tài)分配
可能很多人認為只有堆上才會存在動態(tài)分配,在棧上只可能是靜態(tài)分配。其實,這個觀點是錯的,棧上也支持動態(tài)分配
,該動態(tài)分配由alloca()函數(shù)進行分配。棧的動態(tài)分配和堆是不同的,通過alloca()函數(shù)分配的內(nèi)存由編譯器進行釋放,無序手動操作。
特點
- 分配速度快:分配大小由編譯器在編譯器完成
- 不會產(chǎn)生內(nèi)存碎片:棧內(nèi)存分配是連續(xù)的,以FIFO的方式進棧和出棧
- 大小受限:棧的大小依賴于操作系統(tǒng)
- 訪問受限:只能在當(dāng)前函數(shù)或者作用域內(nèi)進行訪問
堆
堆(heap)是一種內(nèi)存管理方式。內(nèi)存管理對操作系統(tǒng)來說是一件非常復(fù)雜的事情,因為首先內(nèi)存容量很大,其次就是內(nèi)存需求在時間和大小塊上沒有規(guī)律(操作系統(tǒng)上運行著幾十甚至幾百個進程,這些進程可能隨時都會申請或者是釋放內(nèi)存,并且申請和釋放的內(nèi)存塊大小是隨意的)。
堆這種內(nèi)存管理方式的特點就是自由(隨時申請、隨時釋放、大小塊隨意)。堆內(nèi)存是操作系統(tǒng)劃歸給堆管理器(操作系統(tǒng)中的一段代碼,屬于操作系統(tǒng)的內(nèi)存管理單元)來管理的,堆管理器提供了對應(yīng)的接口_sbrk、mmap_等,只是該接口往往由運行時庫進行調(diào)用,即也可以說由運行時庫進行堆內(nèi)存管理,運行時庫提供了malloc/free函數(shù)由開發(fā)人員調(diào)用,進而使用堆內(nèi)存。
分配方式
正如我們所理解的那樣,由于是在運行期進行內(nèi)存分配,分配的大小也在運行期才會知道,所以堆只支持動態(tài)分配
,內(nèi)存申請和釋放的行為由開發(fā)者自行操作,這就很容易造成我們說的內(nèi)存泄漏。
特點
- 變量可以在進程范圍內(nèi)訪問,即進程內(nèi)的所有線程都可以訪問該變量
- 沒有內(nèi)存大小限制,這個其實是相對的,只是相對于棧大小來說沒有限制,其實最終還是受限于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)存可以達到4G的空間,從這個角度來看堆內(nèi)存幾乎是沒有什么限制的。
- 對于棧來講,一般都是有一定的空間大小的,一般依賴于操作系統(tǒng)(也可以人工設(shè)置)
- 能否產(chǎn)生碎片不同
- 對于堆來講,頻繁的內(nèi)存分配和釋放勢必會造成內(nèi)存空間的不連續(xù),從而造成大量的碎片,使程序效率降低。
- 對于棧來講,內(nèi)存都是連續(xù)的,申請和釋放都是指令移動,類似于數(shù)據(jù)結(jié)構(gòu)中的
進棧和出棧
- 增長方向不同
- 對于堆來講,生長方向是向上的,也就是向著內(nèi)存地址增加的方向
- 對于棧來講,它的生長方向是向下的,是向著內(nèi)存地址減小的方向增長
- 分配方式不同
- 堆都是動態(tài)分配的,比如我們常見的malloc/new;而棧則有靜態(tài)分配和動態(tài)分配兩種。
- 靜態(tài)分配是編譯器完成的,比如局部變量的分配,而棧的動態(tài)分配則通過alloca()函數(shù)完成
- 二者動態(tài)分配是不同的,棧的動態(tài)分配的內(nèi)存由編譯器進行釋放,而堆上的動態(tài)分配的內(nèi)存則必須由開發(fā)人自行釋放
- 分配效率不同
- 棧有操作系統(tǒng)分配專門的寄存器存放棧的地址,壓棧出棧都有專門的指令執(zhí)行,這就決定了棧的效率比較高
- 堆內(nèi)存的申請和釋放專門有運行時庫提供的函數(shù),里面涉及復(fù)雜的邏輯,申請和釋放效率低于棧
截止到這里,棧和堆的基本特性以及各自的優(yōu)缺點、使用場景已經(jīng)分析完成,在這里給開發(fā)者一個建議,能使用棧的時候,就盡量使用棧,一方面是因為效率高于堆,另一方面內(nèi)存的申請和釋放由編譯器完成,這樣就避免了很多問題。
擴展
終于到了這一小節(jié),其實,上面講的那么多,都是為這一小節(jié)做鋪墊。
在前面的內(nèi)容中,我們對比了棧和堆,雖然棧效率比較高,且不存在內(nèi)存泄漏、內(nèi)存碎片等,但是由于其本身的局限性(不能多線程、大小受限),所以在很多時候,還是需要在堆上進行內(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++的線性表鏈?zhǔn)酱鎯υO(shè)計與相關(guān)的API實現(xiàn)
這篇文章主要介紹了解析C++中的線性表鏈?zhǔn)酱鎯υO(shè)計與相關(guān)的API實現(xiàn),文中的實例很好地體現(xiàn)了如何創(chuàng)建和遍歷鏈表等基本操作,需要的朋友可以參考下2016-03-03