1. 問題背景
最近有小伙伴對于 C 語言中指針的運算有點疑問:指針變量加 1 之后,到底向后偏移了幾個字節呢?
示例代碼如下,這段代碼運行在32位CPU平臺上:
#include
?
#pragma pack(1)
struct tree
{
int height;
int age;
char tag;
};
#pragma pack()
?
int main()
{
char buffer[512];
char *tmp_ptr = NULL;
struct tree *t_ptr = NULL;
char *t_ptr_new = NULL;
?
tmp_ptr = buffer;
t_ptr = (struct tree *) tmp_ptr;
t_ptr_new = (char *)(t_ptr + 1);
?
printf("t_ptr_new point to buffer[%ld]\n", t_ptr_new - tmp_ptr);
?
return 0;
}
請問,指針變量 t_ptr_new 指向數組 buffer 的哪個位置?
如果能快速得出答案,恭喜你,已經掌握指針算術運算的原理,以及結構體占用空間大小的計算方法。如果不能,也不要氣餒,正好可以將這部分欠缺的知識補充上。下面,讓我們來逐步揭開它的內幕。
2. 結構體
C 語言中 struct 聲明創建一個數據類型(結構體),能將不同類型的對象聚合到一個對象中,用名字來引用結構體的各個組成部分。結構體的所有組成部分都存放在一段連續的內存中。指向結構的指針就是結構體第一個成員的地址。
示例中結構體類型定義:
#pragma pack(1)
struct tree
{
int height;
int age;
char tag;
};
#pragma pack()
結構體內部有三個成員變量,其中兩個為 int 型,一個 char 型。編譯器按照成員列表順序挨個給每個成員分配內存。此結構體占用的內存空間是多少個字節呢?
height 和 age 各占用4個字節,tag 占用 1 個字節。那結構體占用的空間就是 9 個字節 唄。是這樣嗎?
讓我們先來了解一個概念:數據對齊。
數據對齊
許多計算機系統對基本的數據類型的合法地址做了一些限制。要求某種類型對象的地址必須是某個值(通常為2、4、8)的倍數。對齊原則是:任何占用 K 字節空間大小的基本對象,其地址必須是 K 的倍數。
由此,編譯器可能需要在結構體成員內存的分配中插入間隙,保證每個結構成員都滿足它的對齊要求。或者需要在結構體的末尾加入填充,從而使得結構體數組中的每個元素都會滿足它的對齊要求。
本例中,結構體的首地址滿足 4 字節對齊(第一個成員類型為 int)要求后,height、age、tag 三個成員均滿足對齊原則。不過要考慮下面的聲明:
Struct tree a[4];
如果分配 9 個字節,就不能滿足數組 a 的每個元素的對齊要求。
假設數組的起始地址為 x,則每個元素的地址分別為 x、x+9、x+18、x+27,有三個元素不滿足對齊原則。由此,編譯器會為結構 tree 分配 12 個字節,最后 3 個字節是補充的空間(浪費的空間)。
pragma pack()
注意編譯指令,#pragma pack(1) 和 #pragma pack()
pragma pack 的主要作用就是改變編譯器的內存對齊方式。
在不使用這條指令的情況下,編譯器采取默認方式對齊。這兩條編譯預處理指令,使得在這之間定義的結構體按照 1 字節方式對齊。在本例中,使用這兩條指令的效果是,編譯器不會在結構體尾部填充空間了。
結構體大小
最終,這個結構體占用的內存空間大小為 9 個字節。
3. 理解指針
指針定義
每個指針都對應一個類型。這個類型表明該指針指向的是哪一類對象。指針的類型不是機器碼中的一部分,而是C語言提供的一種抽象,幫助程序員避免尋址錯誤。
每個指針都有一個值。這個值是某個指定類型的對象的地址。
示例代碼中
struct tree *t_ptr = NULL;
這語句是什么意思呢?其含義為:定義一個指針變量 t_ptr 并賦予了初值 NULL。
詳細解釋:星號 “*” 說明標識符 t_ptr為 “一個指向…的指針”; struct tree 為類型說明符;可知,t_ptr 為指向結構體 tree 類型的指針。
指針的類型由指向對象的數據類型和星號 “*” 組合起來表示。例如,指針 t_ptr 的指針類型為 “struct tree *”。
示例代碼中,t_ptr_new 和 tm_ptr 為指向 char 類型的指針,并賦初始值NULL。
NULL 指針
C語言標準中定義了 NULL 指針,作為一種特殊的指針變量,其指向的內容為空(即不指向任何東西)。將其賦值給某個指針變量,表示該指針目前并未指向任何東西。
數組的名字
一個數組的名字也是一種指針,但這個指針的值是不能改變的。這種指針永遠指向數組中的第一個元素,其指向的類型為數組元素的數據類型。
示例代碼:
char buffer[512];
數組名字 buffer 為指向 char 數據類型的指針,它指向數組的首個元素 buffer[0]。
4. 指針轉換
通過類型轉換,可以將指針從一種類型轉換為另一種形式,改變的只是它的類型,值是不會改變的。
C語言中的類型轉換有兩種:隱式類型轉換和強制類型轉換。
示例代碼:
t_ptr_new = (char *)(t_ptr + 1);
通過 “(char *)” 強制將 struct tree * 類型的指針轉換為 char * 類型,并將其賦值給一個 char * 類型的指針。如果去掉 “(char *)”,在編譯過程中,編譯器會根據 “=” 左側變量的類型自動進行轉換,但會產生告警信息。告警信息如下:
example.c: In function ‘main’:
example.c:21:12: warning: assignment from incompatible pointer type [-Wincompatible-pointer-types]
t_ptr_new = (t_ptr + 1);
本例中用強制類型轉換,一方面是為了消除編譯過程產生的警告,另一方面是為了使程序便于理解。
5. 指針運算
C語言的指針運算有兩種形式。
第一種:指針 ± 整數
這種計算出來的值,會根據該指針指向的某種數據類型的大小進行伸縮。例如,指針的值為 x,指向的數據類型大小為 L,整數為 n,則計算出來的結果值為 x + n * L。
示例代碼,
t_ptr_new = (char *)(t_ptr + 1);
此表達式等價于(a_ptr 符號在此處是為了便于理解而添加):
a_ptr = (t_ptr + 1);
t_ptr_new = (char *)a_ptr;
指針 t_ptr 加 1(t_ptr + 1)的結果,會根據數據類型 struct tree 的大小進行增加。假設指針 t_ptr 的值為 x(即地址值為 x),而結構體類型 tree 的大小為 9 字節,則 t_ptr + 1 的值為 x+9。然后,將此結果進行強制類型轉換后,賦值給指針變量 t_ptr_new。
第二種:指針 – 指針
只有當兩個指針都指向同一個數組中的元素時,計算才有意義。
減法運算的值是兩個指針在內存中的距離(等于兩個地址之差除以該元素數據類型的大小)。兩個指針相減的結果的類型是 ptrdiff_t,它是一種有符號整數類型。
如果兩個指針值(地址值)的差值為 12 字節,每個元素占用 4 個字節,則兩個指針相減得到的結果將是 3(兩個指針的差值 12 將除以每個元素的長度 4)。
示例代碼
printf("t_ptr_new point to buffer[%ld]\n", t_ptr_new - tmp_ptr);
由以上分析,兩個指針相減(t_ptr_new - tmp_ptr),地址差值為 9 字節,而數組中每個元素的大小為 1 字節(char類型數據),則指針相減得到結果為 9(9字節/1字節)。
6. 綜上分析
有了以上分析的基礎,讓我們看看最終答案是如何得出的。
tmp_ptr = buffer;
tmp_ptr 指針指向數組 buffer 的第 0 個元素,即 buffer[0]。
t_ptr = (struct tree *) tmp_ptr;
將指針tmp_ptr強制轉換為 struct tree * 類型的指針后,賦值給指針變量 t_ptr。
t_ptr_new = (char *)(t_ptr + 1);
這個表達式是問題的關鍵。t_ptr + 1 運算得到的結果指針,指向下一個結構體 tree 元素,而結構體占用的空間大小為9個字節,因此指針加 1 后,實際偏移了 9 個字節。經過強制類型轉換后,賦值給指針 t_ptr_new。
printf("t_ptr_new point to buffer[%ld]\n", t_ptr_new - tmp_ptr);
t_ptr_new - tmp_ptr 運算得到結果是 9。由于 tmp_ptr 指向數組的第 0 個元素buffer[0],則 t_ptr_new 指向數組的第 9 個元素buffer[9]。
最終答案
指針加一后,偏移9個字節;t_ptr_new指向buffer數組的第9個元素。打印輸出結果如下
t_ptr_new point to buffer[9]
審核編輯:符乾江
評論
查看更多