C語言中可移植且可靠的指針運算

來源:文萃谷 2.02W

1C語言是目前世界上使用最為廣泛的計算機語言之一,目前已經成為各大高校主要的計算機教學語言。下面小編為大家介紹C語言中可移植且可靠的指針運算吧!

C語言中可移植且可靠的指針運算

 指針不是整數

指針變量包含 C 語言數據的地址。例如,查看以下幾行代碼。

int a, *p;

/* 為指針賦予某個目標的地址 */

p = &a;

/* 解除引用指針以間接訪問目標 */

*p = 0;

上面的代碼將變量a 的值設置為0。應用到a 的&運算符返回一個表示該變量位置的值(地址)。如果將該值複製到一個指針變量,然後對指針解除引用(使用*運算符),則該表達式表示原始變量a。這很容易讓人認為該地址在數值上等於變量a 所在的計算機存儲器地址,但在C 語言中並沒有此類要求。

以下示例可清楚地説明最後一點:考慮具有多個獨立存儲區的PIC 器件。對位於數據存儲器中器件地址100h 的變量使用地址運算符時應返回什麼值?而對位於程序存儲器中器件地址100h 的另一個變量使用地址運算符時又應返回什麼值?

如果在兩種情況下都回答 100h,那麼在運行時如何得知100h 是數據存儲器中的地址還是程序存儲器中的地址呢?顯然,在這種情況下,如果稍後要解除引用地址,則需要其他方式來確定應訪問哪個存儲器。

“其他方式”可以是對地址運算符返回的值進行特殊編碼(與MPLAB XC8 編譯器配合使用的技術),也可以使用傳達相同信息的特殊指針類型限定符(MPLAB XC16 和XC32 編譯器使用該方法)。

為保持代碼的可移植性,不應假設將整數賦給指針就會使指針能訪問任何對象,即使該整數的值與某個對象的器件地址相同。因此對於上面的示例,為指針賦值立即數100h(或者保留此值的整數變量)並不意味着該指針指向變量a。

/* 我們發現“a”被分配到地址100h

*/int a, *p;

/* 注:這涉及整數到指針的隱式轉換 */

p = 0x100;

/* 沒人知道會發生什麼!*/

*p = 0;

請記住,一種地址空間中的取指和存儲可能不像另一種地址空間中的取指和存儲一樣簡單——編譯器可能需要使用不同的寄存器和指令才能執行訪問。

基於同樣的原因,在定義指針時,必須使用適當的指針類型限定符。由於 MPLAB XC8 對地址進行編碼,因此它不使用特殊地址空間限定符,而MPLAB XC16 和XC32 則使用。但是,兩種情況下都必須適時使用通常的const 和volatile 限定符。限定符在數據定義中指定,如果想要可靠地訪問該數據,則需要使用與引用該數據的指針相匹配的限定符。例如,使用MPLABXC16 時:

__psv__ char buffer[8] __attribute__((space(psv)))

在閃存程序存儲器中放置一個字符數組buffer,可通過“psv”(程序空間可視性)窗口進行訪問。直接訪問buffer 將使編譯器生成可確保psv 窗口(位於處理器地址空間中的特定位置)映射到閃存(包含“buffer”)中適當位置的代碼。buffer 的“地址”是所需窗口設置與“buffer”在整個窗口中的可視區域內的偏移量的組合。

通過指針引用“buffer”中的項時,必須使用如下指針:

__psv__ char *bp;

才能使編譯器生成正確的代碼。不帶__psv__限定符的“普通”指針不起作用。

因此指針不僅僅是一個寬到可以保存“地址”的整數,它還具有關聯的目標類型;C 語言數據地址不僅僅是一個計算機存儲器地址,它可由編譯器修改或優化。C 編譯器還會考慮其他一些事項。

 出問題的位置

如果我們認為指針只是一個值為(計算機存儲器)地址的整數,並且認為我們已瞭解地址的含義以及該存儲器中排列數據的方式,我們可能會想要在所編寫的C 語言代碼中顯式執行各種各樣的地址運算,進而在程序中嵌入底層運行時環境的特定於實現的詳細信息。這樣一來,即使現在程序可以運行,但如果針對其他處理器進行編譯,可能就無法正常工作,或者可能在看起來無關緊要的更改後莫名停止工作。我們該如何避免這類問題呢?

1. 使用正確的指針類型。根據引用的數據選擇適用的指針類型。儘管在你添加一系列轉換後程序會進行編譯,但不要據此認為程序會實際按照你的期望工作。它會按照你告訴它的方式工作,這可能與你的期望有很大不同。

2. 根據你將用來訪問數據的結構來分配數據

3. 不要猜測數據類型的佈局

例如,可以分配一個字符緩衝區,然後將該緩衝區的地址轉換為指向更大類型數據數組或結構數組的指針。隨後你可能會通過不同類型的指針,有時訪問字符型數據,有時訪問其他類型的數據。為此,必須知道更大類型的數據在字符數據上以及彼此之間的排列方式。這非常危險而且容易出錯。如果需要通過多類型“視圖”訪問數據,請將數據分配成聯合數組,然後通過聯合訪問數據。編譯器將清楚你的意圖並幫助你正確實現。

示例

下面的 C 程序建立了一個初始化結構數組,顯示該數組,修改數組的一個元素,最後顯示更新的結果。代碼中針對選擇和更新要更改的元素提供了幾種備選方法。其中一些是常用方法,但實際上是不安全的代碼模式:

1: /* 用於演示指針運算問題的測試程序 */

2: #include

3: #include

4:

5: struct twoints {

6: uint8_t a;

7: uint32_t b;

8: };

9:

10: static struct twoints twointbuf[4] = {

11: {1, 5}, {2, 6}, {3, 7}, {4, 8}

12: };

13:

14: int main(int argc, char *argv[])

15: {

16: struct twoints *p;

17: size_t i;

18:

19: /* 輸出結構數組 */

20: printf(“Before:”);

21: i = 0;

22: p = twointbuf;

23: while (i < 4) {

24: printf(“0x%02x , 0x%08x”, p->a, (*p).b);

25: ++p;

26: ++i;

27: }

28: printf(“”);

29:

30: /* 選擇下標為2 的元素的正確方法 */

31: p = twointbuf + 2;

32:

33: /* 等效且同樣好的方法 */

34: #ifdef ALSORIGHT

35: p = &twointbuf[2];

36: #endif

37:

38: /* 正確,但沒有必要採用的方法 */

39: #ifdef CORRECTBUTWHY

40: p = (struct twoints *)((char *)twointbuf + 2*sizeof(struct twoints));

41: #endif

42:

43: /* 以下是常見錯誤 */

44: #ifdef REALLYWRONG

45: p = (struct twoints *)((char *)twointbuf + 2*(sizeof(uint8_t) + sizeof(uint32_t)));

46: #endif

47: #ifdef NOTSAFE

48: p = (struct twoints *)((size_t)twointbuf + 2*sizeof(struct twoints));

49: #endif

50:

51: /* 修改元素2 */

52: p->b = 0xffffffff;

53:

54: /* 顯示更新的數組 */

55: printf(“After:”);

56: i = 0;

57: p = &twointbuf[0];

58: while (i < 4) {

59: printf(“0x%02x , 0x%08x”, (p + i)->a,(p[i]).b);

60: ++i;

61: }

62: printf(“”);

63:

64: return 0;

65: }

我們討論一下如何訪問要修改的第二個結構元素。在第10 行中聲明的twointbuf 是一個結構數組,相當於指向該數組首地址的'指針。我們可以通過數組或指針語法來訪問該數組中的元素,這兩種編碼風格表示同一個意思。第31 行和第35 行中給出的備選方法均是獲取指向數組中元素2 的指針的安全方法。編譯器不會將“2”解讀成兩個字節或兩個“字”,而是解讀成元素0 和元素1 後面的元素的編號2。

在第 40 行,我們看到了根據數組的字節地址以及前面元素的長度(字節)來計算結構元素地址的示例。如果(char *)上的限定符與數組上的限定符(本示例中沒有)匹配,則這種方法可行——只要字符指針和數組均聲明為引用相同的地址空間,地址和增量映射到底層存儲的規則就會相同,且該代碼有效。但為什麼要這樣做呢?使用C 語言提供的簡潔明瞭的語法,編譯器將生成同樣正確或更有效的代碼。

在第 45 行,此代碼假設結構元素的長度(字節)是兩個成員的長度之和。這是不安全的假設,因為編譯器可能必須對結構進行填充才能使兩個成員在自然字邊界上對齊。是否使用結構填充將取決於目標器件。

第 48 行上的語句一開始沒有將數組指針轉換為字符指針,而是轉換為大到足以保存指針的整數,從而向編譯器隱藏了該值是特定地址空間中的地址的事實。隨後執行與第40 行相同的地址運算,並將結果轉換回指向結構數組的指針。在這種情況下,編譯器沒有機會對添加為特定空間中的指針和下標的數字進行解讀,且無法應用任何映射規則。因此轉換回結構指針的值可能是錯誤的。

結論

使用C 語言的功能時,應依據功能在語言中的含義:

使用地址運算符來獲取要賦給指針的地址。

確保所定義的指針類型在程序執行期間與其可引用的數據相匹配。

決不要假設對象分配到存儲器的方式。

不要假設或迴避規則來使 C語言代碼更“直接”和“有效”,此類代碼不會具有可移植性、可靠性或更有效。

熱門標籤