文/劉碩
字節(jié)是內(nèi)存的基本單位。一字節(jié)有八位,在內(nèi)存中字節(jié)從上到下按照由低到高的順序編號(如圖1)。
對于內(nèi)存來說,“數(shù)據(jù)”僅僅是每一個字節(jié)中的八個高低電平位的組合;而對于高級語言(如C++)來說,“數(shù)據(jù)”代表的是“對象”。由于對象的“類型”不同,一個對象儲存在一個或多個字節(jié)中。例如在64位系統(tǒng)中,一般情況下char類型對象占1字節(jié),而int類型的對象要占4個字節(jié)。在C++中,讀取一個T類型對象在內(nèi)存中占多少個字節(jié)是通過sizeof(T)完成的(如前所述可以得出sizeof(int)等于4)。
指針是對象在內(nèi)存中的地址,它的值是對應(yīng)字節(jié)的編號。如果我們在C++中定義一個指向T類型對象的指針P(為了避免對指針的操作和對指針值的操作混淆,我們把指針P的值記為valueP)。那么P的含義是“內(nèi)存中第valueP個字節(jié)開始,到第valueP+sizeof(T)個字節(jié),這一段連續(xù)的內(nèi)存中保存著一個T類型的對象”。
由此我們得到第一個結(jié)論:對于內(nèi)存來說,數(shù)據(jù)的長度是統(tǒng)一的,始終是一字節(jié);對于高級語言來說,對象的長度是根據(jù)對象的類型(如char、int)變化的,是sizeof(T)個字節(jié)。
“在某些架構(gòu)下,從一個不被對象大小均勻分割的地址中讀取多字節(jié)對象是不可能(比如從32位整形中讀取4比特)。在像x86這樣的架構(gòu)下,CPU通過多次讀取并從這些讀取中獲取你的值來自動處理這種情況,但代價是顯著降低了性能”——《學(xué)習(xí)OpenCV3(中文版)》為了提高效率,有必要對申請內(nèi)存產(chǎn)生的原始指針進行處理,使指針的值能被對象長度整除(如圖2)。
根據(jù)指針與字節(jié)編號的關(guān)系,我們可以很自然的想到:在解引用指針時,指針?biāo)笇ο蟮念愋鸵?guī)定了本次解引用需要要一次讀取幾個內(nèi)存單元(字節(jié))。比如在解引用char*類型的指針時需要讀取一個字節(jié)的數(shù)據(jù),而解引用int*類型指針時,需要一次讀取四個字節(jié)的數(shù)據(jù)。由此產(chǎn)生了一個不容忽視的問題——指針類型轉(zhuǎn)換時,類型大小的問題。
·較高的指針類型轉(zhuǎn)換成較低的指針類型(int* -> char*):這種轉(zhuǎn)換是安全的。只不過這樣解引用指針時,讀取的字節(jié)數(shù)會由原來的四個轉(zhuǎn)變成一個。
·較低的指針類型轉(zhuǎn)換成較高的指針類型(char* ->int*):這種轉(zhuǎn)換是危險的。因為這樣解引用指針的時候,讀取的字節(jié)個數(shù)會由原來的一個,轉(zhuǎn)變成四個。我們不能保證多被讀取的字節(jié)中是否存儲著有其他用途的數(shù)據(jù),對這樣得到的內(nèi)存加以修改,很可能引發(fā)程序運行異常。
在C/C++環(huán)境下使用動態(tài)內(nèi)存分配時,malloc()函數(shù)返回的值是申請到的內(nèi)存資源中,第一個字節(jié)的編號,即指向一段連續(xù)的內(nèi)存資源首地址的指針P。如果我們用申請得到的內(nèi)存資源來存放n個T類型的對象,不能保證valueP可以被sizeof(T)整除。由此我們需要進行“指針對齊”
指針對齊操作alignPtr()
(T*)(((size_t)P + n-1) & -n);
· P是malloc()返回的指針,即得到的內(nèi)存資源首地址,也是需要進行對齊操作的指針。
· T是我們要存放的數(shù)據(jù)的類型(顯然T*就是ptr的類型)
· n是T類型數(shù)據(jù)所占的字節(jié)數(shù)
圖1
·“(size_t)P”這個表達式的值就是P指針的值,即valueP
·表達式的結(jié)果就是對齊之后的指針
·OpenCV源碼:
template
_Tp* ptr, intn=(int)sizeof(_Tp)){
CV_DbgAssert((n & (n - 1)) == 0); // n is a power of 2
return (_Tp*)(((size_t)ptr + n-1) & -n);}
情況一:分配的內(nèi)存第一個字節(jié)的編號(分配內(nèi)存首地址)是對象長度的整數(shù)倍。
假設(shè)編號(首地址)為0000 0100 B ,要在這段內(nèi)存中存放一些長度為4的對象。則(size_t)P + n-1)等于:
0000 0100 B
+0000 0100 B
-0000 0001 B
=0000 0111 B
當(dāng)n=0000 0100 B時,-n是1111 1100 B(取反加一),于是(size_t)P + n-1)&-n就是低二位置零,使得0000 0111 B變成0000 0100 B.
對比觀察得到在這種情況下,表達式的值即P的值。
情況二:分配的內(nèi)存第一個字節(jié)的編號(分配內(nèi)存首地址)不是是對象長度的整數(shù)倍。
假設(shè)編號(首地址)為0000 0110 B,要在這段內(nèi)存中存放一些長度為4的對象,則(size_t)ptr + n-1)等于:
0000 0110 B
+0000 0100 B
-0000 0001 B
=0000 1001 B
當(dāng)n=0000 0100 B時,-n是1111 1100 B(取反加一),于是(size_t)P + n-1)&-n就是去掉低二位,使得0000 1001 B變成0000 1000 B.
對比觀察可以發(fā)現(xiàn)這種情況下,表達式的值是P的值加一個不大于n的整數(shù),且表達式的值可以被n整除。由“不可整除”到“可以整除”這個過程叫做“對齊”
圖2:一個T類型占4個字節(jié),打√的編號可以被4整除
圖3
通過“對齊表達式”得到的指針?biāo)傅膬?nèi)存,是我們真正寫入數(shù)據(jù)的起點,而我們申請的內(nèi)存是從P開始分配的。釋放內(nèi)存時,也應(yīng)該從P開始釋放,如何保證P和表達式的值之間的字節(jié)能夠被釋放呢?只需要在調(diào)用malloc()函數(shù)時,多申請sizeof(void *)個字節(jié),之后將P存入這幾個字節(jié)即可,銷毀時把P從這幾個字節(jié)中讀取出來供free()函數(shù)使用。
假設(shè)有X個T類型對象需要寫入內(nèi)存,每個對象長度是sizeof(T),那么調(diào)用malloc()時,應(yīng)該是malloc(sizeof(T)*x+sizeof(void*)),這樣得到的字節(jié)數(shù)量正好是x個對象和一個指針需要的空間,但是因為指針對齊時是會舍棄幾個字節(jié)不用的,這幾個字節(jié)的數(shù)量大于零且小于對象的長度,所以還要再多申請存放一個對象所需要的字節(jié),以補充因舍棄而減少的內(nèi)存空間。故調(diào)用malloc()時,參數(shù)為
Sizeof(T)*x+sizeof(void*)+sizeof(T)
*從用”sizeof(void*)”來預(yù)留一個指針?biāo)枰淖止?jié)數(shù)來看,指針的長度始終是固定的,而指針?biāo)赶虻膶ο箝L度是隨著對象的類型變化的。
OpenCV源碼:
·udata是調(diào)用malloc()之后得到的資源中的第一個字節(jié)編號。是一個指向資源首地址的指針,是一個未對齊的指針
·將幾個T類型的對象放入這片連續(xù)的內(nèi)存,每個T類型的對象所在的地址都被一個指針?biāo)?。這些指針組成了一個指針數(shù)組adata。第一個T類型對象的地址是adata[0],第二個T類型對象的地址是adata[1]...以此類推。在我們想用T類型對象的時候可以解引用指針 *adata[index]。此外還可以用adata[-1]這個位置存儲udata的值,以便銷毀時利用。
·adata的值應(yīng)該是用udata對齊后的值。因為udata是uchar*類型,而adata是uchar**類型(C++中數(shù)組名自動轉(zhuǎn)換成指向數(shù)組首地址的指針),直接帶入alignPtr()中進行對齊會有類型錯誤,所以需要強制類型轉(zhuǎn)換以滿足利用udata產(chǎn)生adata。強制轉(zhuǎn)換不會改變udata的值、長度。
執(zhí)行fastMalloc()之后會返回adata,這時內(nèi)存與指針的關(guān)系如圖3所示。
根據(jù)剛剛的分析udata和pdata之間有幾個(或者沒有)字節(jié)被閑置,不能保證是否為adata[-1]提供了足夠的字節(jié)以存放udata。因此在調(diào)用alignPtr()時,傳入的第一個參數(shù)是(uchar**)udata + 1?!?uchar**)”優(yōu)先級比“+”要高,所以這個操作是“對指向指針的指針加一”?!爸羔樇右弧钡牟僮鞅硎尽爸羔槷?dāng)前值+指針?biāo)傅膶ο笏嫉淖止?jié)數(shù)”。就是說指針對齊的時候,給udata預(yù)留了sizeof(void *)個字節(jié),保證adata前面至少有存放一個指針的空間。
根據(jù)計算機CPU的架構(gòu)而定,簡單說,32位計算機的指針是32個bit組成,也就是4字節(jié);64位計算機的指針長度是64bit組成,也就是8字節(jié)。
“指針加一”和“指針的值”加一是兩種情況。假設(shè)有一個指向T類型對象的指針P,P的值是valueP。P+1這個操作等價于valueP+sizeof(T)。而valueP+1是令P指針指向“當(dāng)前所指的字節(jié)的”下一個字節(jié)。