1. 問題背景
最近有小伙伴對(duì)于 C 語言中指針的運(yùn)算有點(diǎn)疑問:指針變量加 1 之后,到底向后偏移了幾個(gè)字節(jié)呢?
示例代碼如下,這段代碼運(yùn)行在32位CPU平臺(tái)上:
#include#pragmapack(1) structtree { intheight; intage; chartag; }; #pragmapack() intmain() { charbuffer[512]; char*tmp_ptr=NULL; structtree*t_ptr=NULL; char*t_ptr_new=NULL; tmp_ptr=buffer; t_ptr=(structtree*)tmp_ptr; t_ptr_new=(char*)(t_ptr+1); printf("t_ptr_newpointtobuffer[%ld] ",t_ptr_new-tmp_ptr); return0; }
請(qǐng)問,指針變量 t_ptr_new 指向數(shù)組 buffer 的哪個(gè)位置?
如果能快速得出答案,恭喜你,已經(jīng)掌握指針?biāo)阈g(shù)運(yùn)算的原理,以及結(jié)構(gòu)體占用空間大小的計(jì)算方法。如果不能,也不要?dú)怵H,正好可以將這部分欠缺的知識(shí)補(bǔ)充上。下面,讓我們來逐步揭開它的內(nèi)幕。
2. 結(jié)構(gòu)體
C 語言中 struct 聲明創(chuàng)建一個(gè)數(shù)據(jù)類型(結(jié)構(gòu)體),能將不同類型的對(duì)象聚合到一個(gè)對(duì)象中,用名字來引用結(jié)構(gòu)體的各個(gè)組成部分。結(jié)構(gòu)體的所有組成部分都存放在一段連續(xù)的內(nèi)存中。指向結(jié)構(gòu)的指針就是結(jié)構(gòu)體第一個(gè)成員的地址。
示例中結(jié)構(gòu)體類型定義:
#pragmapack(1) structtree { intheight; intage; chartag; }; #pragmapack()
結(jié)構(gòu)體內(nèi)部有三個(gè)成員變量,其中兩個(gè)為 int 型,一個(gè) char 型。編譯器按照成員列表順序挨個(gè)給每個(gè)成員分配內(nèi)存。此結(jié)構(gòu)體占用的內(nèi)存空間是多少個(gè)字節(jié)呢?
height 和 age 各占用4個(gè)字節(jié),tag 占用 1 個(gè)字節(jié)。那結(jié)構(gòu)體占用的空間就是 9 個(gè)字節(jié)唄。是這樣嗎?
讓我們先來了解一個(gè)概念:數(shù)據(jù)對(duì)齊。
數(shù)據(jù)對(duì)齊
許多計(jì)算機(jī)系統(tǒng)對(duì)基本的數(shù)據(jù)類型的合法地址做了一些限制。要求某種類型對(duì)象的地址必須是某個(gè)值(通常為2、4、8)的倍數(shù)。對(duì)齊原則是:任何占用 K 字節(jié)空間大小的基本對(duì)象,其地址必須是 K 的倍數(shù)。
由此,編譯器可能需要在結(jié)構(gòu)體成員內(nèi)存的分配中插入間隙,保證每個(gè)結(jié)構(gòu)成員都滿足它的對(duì)齊要求?;蛘咝枰诮Y(jié)構(gòu)體的末尾加入填充,從而使得結(jié)構(gòu)體數(shù)組中的每個(gè)元素都會(huì)滿足它的對(duì)齊要求。
本例中,結(jié)構(gòu)體的首地址滿足 4 字節(jié)對(duì)齊(第一個(gè)成員類型為 int)要求后,height、age、tag 三個(gè)成員均滿足對(duì)齊原則。不過要考慮下面的聲明:
Structtreea[4];
如果分配 9 個(gè)字節(jié),就不能滿足數(shù)組 a 的每個(gè)元素的對(duì)齊要求。
假設(shè)數(shù)組的起始地址為 x,則每個(gè)元素的地址分別為 x、x+9、x+18、x+27,有三個(gè)元素不滿足對(duì)齊原則。由此,編譯器會(huì)為結(jié)構(gòu) tree 分配 12 個(gè)字節(jié),最后 3 個(gè)字節(jié)是補(bǔ)充的空間(浪費(fèi)的空間)。
pragma pack()
注意編譯指令,#pragma pack(1) 和 #pragma pack()
pragma pack 的主要作用就是改變編譯器的內(nèi)存對(duì)齊方式。
在不使用這條指令的情況下,編譯器采取默認(rèn)方式對(duì)齊。這兩條編譯預(yù)處理指令,使得在這之間定義的結(jié)構(gòu)體按照 1 字節(jié)方式對(duì)齊。在本例中,使用這兩條指令的效果是,編譯器不會(huì)在結(jié)構(gòu)體尾部填充空間了。
結(jié)構(gòu)體大小
最終,這個(gè)結(jié)構(gòu)體占用的內(nèi)存空間大小為 9 個(gè)字節(jié)。
3. 理解指針
指針定義
每個(gè)指針都對(duì)應(yīng)一個(gè)類型。這個(gè)類型表明該指針指向的是哪一類對(duì)象。指針的類型不是機(jī)器碼中的一部分,而是C語言提供的一種抽象,幫助程序員避免尋址錯(cuò)誤。
每個(gè)指針都有一個(gè)值。這個(gè)值是某個(gè)指定類型的對(duì)象的地址。
示例代碼中
structtree*t_ptr=NULL;
這語句是什么意思呢?其含義為:定義一個(gè)指針變量 t_ptr 并賦予了初值 NULL。
詳細(xì)解釋:星號(hào) “*” 說明標(biāo)識(shí)符 t_ptr為 “一個(gè)指向…的指針”;struct tree 為類型說明符;可知,t_ptr 為指向結(jié)構(gòu)體 tree 類型的指針。
指針的類型由指向?qū)ο蟮臄?shù)據(jù)類型和星號(hào) “*” 組合起來表示。例如,指針 t_ptr 的指針類型為 “struct tree *”。
示例代碼中,t_ptr_new 和 tm_ptr 為指向 char 類型的指針,并賦初始值NULL。
NULL 指針
C語言標(biāo)準(zhǔn)中定義了 NULL 指針,作為一種特殊的指針變量,其指向的內(nèi)容為空(即不指向任何東西)。將其賦值給某個(gè)指針變量,表示該指針目前并未指向任何東西。
數(shù)組的名字
一個(gè)數(shù)組的名字也是一種指針,但這個(gè)指針的值是不能改變的。這種指針永遠(yuǎn)指向數(shù)組中的第一個(gè)元素,其指向的類型為數(shù)組元素的數(shù)據(jù)類型。
示例代碼:
char buffer[512];
數(shù)組名字 buffer 為指向 char 數(shù)據(jù)類型的指針,它指向數(shù)組的首個(gè)元素 buffer[0]。
4. 指針轉(zhuǎn)換
通過類型轉(zhuǎn)換,可以將指針從一種類型轉(zhuǎn)換為另一種形式,改變的只是它的類型,值是不會(huì)改變的。
C語言中的類型轉(zhuǎn)換有兩種:隱式類型轉(zhuǎn)換和強(qiáng)制類型轉(zhuǎn)換。
示例代碼:
t_ptr_new=(char*)(t_ptr+1);
通過 “(char *)” 強(qiáng)制將 struct tree * 類型的指針轉(zhuǎn)換為 char * 類型,并將其賦值給一個(gè) char * 類型的指針。如果去掉 “(char *)”,在編譯過程中,編譯器會(huì)根據(jù) “=” 左側(cè)變量的類型自動(dòng)進(jìn)行轉(zhuǎn)換,但會(huì)產(chǎn)生告警信息。告警信息如下:
example.c:Infunction‘main’: example.c12:warning:assignmentfromincompatiblepointertype[-Wincompatible-pointer-types] t_ptr_new=(t_ptr+1);
本例中用強(qiáng)制類型轉(zhuǎn)換,一方面是為了消除編譯過程產(chǎn)生的警告,另一方面是為了使程序便于理解。
5. 指針運(yùn)算
C語言的指針運(yùn)算有兩種形式。
第一種:指針 ± 整數(shù)
這種計(jì)算出來的值,會(huì)根據(jù)該指針指向的某種數(shù)據(jù)類型的大小進(jìn)行伸縮。例如,指針的值為 x,指向的數(shù)據(jù)類型大小為 L,整數(shù)為 n,則計(jì)算出來的結(jié)果值為 x + n * L。
示例代碼,
t_ptr_new=(char*)(t_ptr+1);
此表達(dá)式等價(jià)于(a_ptr 符號(hào)在此處是為了便于理解而添加):
a_ptr=(t_ptr+1); t_ptr_new=(char*)a_ptr;
指針 t_ptr 加 1(t_ptr + 1)的結(jié)果,會(huì)根據(jù)數(shù)據(jù)類型 struct tree 的大小進(jìn)行增加。假設(shè)指針 t_ptr 的值為 x(即地址值為 x),而結(jié)構(gòu)體類型 tree 的大小為 9 字節(jié),則 t_ptr + 1 的值為 x+9。然后,將此結(jié)果進(jìn)行強(qiáng)制類型轉(zhuǎn)換后,賦值給指針變量 t_ptr_new。
第二種:指針 – 指針
只有當(dāng)兩個(gè)指針都指向同一個(gè)數(shù)組中的元素時(shí),計(jì)算才有意義。
減法運(yùn)算的值是兩個(gè)指針在內(nèi)存中的距離(等于兩個(gè)地址之差除以該元素?cái)?shù)據(jù)類型的大?。?/strong>。兩個(gè)指針相減的結(jié)果的類型是 ptrdiff_t,它是一種有符號(hào)整數(shù)類型。
如果兩個(gè)指針值(地址值)的差值為 12 字節(jié),每個(gè)元素占用 4 個(gè)字節(jié),則兩個(gè)指針相減得到的結(jié)果將是 3(兩個(gè)指針的差值 12 將除以每個(gè)元素的長度 4)。
示例代碼
printf("t_ptr_newpointtobuffer[%ld] ",t_ptr_new-tmp_ptr);
由以上分析,兩個(gè)指針相減(t_ptr_new - tmp_ptr),地址差值為 9 字節(jié),而數(shù)組中每個(gè)元素的大小為 1 字節(jié)(char類型數(shù)據(jù)),則指針相減得到結(jié)果為 9(9字節(jié)/1字節(jié))。
6. 綜上分析
有了以上分析的基礎(chǔ),讓我們看看最終答案是如何得出的。
tmp_ptr=buffer;
tmp_ptr 指針指向數(shù)組 buffer 的第 0 個(gè)元素,即 buffer[0]。
t_ptr=(structtree*)tmp_ptr;
將指針tmp_ptr強(qiáng)制轉(zhuǎn)換為 struct tree * 類型的指針后,賦值給指針變量 t_ptr。
t_ptr_new=(char*)(t_ptr+1);
這個(gè)表達(dá)式是問題的關(guān)鍵。t_ptr + 1 運(yùn)算得到的結(jié)果指針,指向下一個(gè)結(jié)構(gòu)體 tree 元素,而結(jié)構(gòu)體占用的空間大小為9個(gè)字節(jié),因此指針加 1 后,實(shí)際偏移了 9 個(gè)字節(jié)。經(jīng)過強(qiáng)制類型轉(zhuǎn)換后,賦值給指針 t_ptr_new。
printf("t_ptr_newpointtobuffer[%ld] ",t_ptr_new-tmp_ptr);
t_ptr_new - tmp_ptr 運(yùn)算得到結(jié)果是 9。由于 tmp_ptr 指向數(shù)組的第 0 個(gè)元素buffer[0],則 t_ptr_new 指向數(shù)組的第 9 個(gè)元素buffer[9]。
最終答案
指針加 1 后,偏移 9 個(gè)字節(jié);t_ptr_new指向buffer數(shù)組的第9個(gè)元素。打印輸出結(jié)果如下
t_ptr_new point to buffer[9]
審核編輯:劉清
-
cpu
+關(guān)注
關(guān)注
68文章
10827瀏覽量
211167 -
C語言
+關(guān)注
關(guān)注
180文章
7600瀏覽量
136229 -
編譯器
+關(guān)注
關(guān)注
1文章
1618瀏覽量
49055
原文標(biāo)題:C語言指針加1引發(fā)的思考
文章出處:【微信號(hào):嵌入式技術(shù)開發(fā),微信公眾號(hào):嵌入式技術(shù)開發(fā)】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論