當前位置:才華齋>計算機>C語言>

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

C語言 閱讀(2.1W)

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語言程式碼更“直接”和“有效”,此類程式碼不會具有可移植性、可靠性或更有效。