- 相關(guān)推薦
C語言中可移植且可靠的指針運算
1C語言是目前世界上使用最為廣泛的計算機(jī)語言之一,目前已經(jīng)成為各大高校主要的計算機(jī)教學(xué)語言。下面小編為大家介紹C語言中可移植且可靠的指針運算吧!
指針不是整數(shù)
指針變量包含 C 語言數(shù)據(jù)的地址。例如,查看以下幾行代碼。
int a, *p;
/* 為指針賦予某個目標(biāo)的地址 */
p = &a;
/* 解除引用指針以間接訪問目標(biāo) */
*p = 0;
上面的代碼將變量a 的值設(shè)置為0。應(yīng)用到a 的&運算符返回一個表示該變量位置的值(地址)。如果將該值復(fù)制到一個指針變量,然后對指針解除引用(使用*運算符),則該表達(dá)式表示原始變量a。這很容易讓人認(rèn)為該地址在數(shù)值上等于變量a 所在的計算機(jī)存儲器地址,但在C 語言中并沒有此類要求。
以下示例可清楚地說明最后一點:考慮具有多個獨立存儲區(qū)的PIC 器件。對位于數(shù)據(jù)存儲器中器件地址100h 的變量使用地址運算符時應(yīng)返回什么值?而對位于程序存儲器中器件地址100h 的另一個變量使用地址運算符時又應(yīng)返回什么值?
如果在兩種情況下都回答 100h,那么在運行時如何得知100h 是數(shù)據(jù)存儲器中的地址還是程序存儲器中的地址呢?顯然,在這種情況下,如果稍后要解除引用地址,則需要其他方式來確定應(yīng)訪問哪個存儲器。
“其他方式”可以是對地址運算符返回的值進(jìn)行特殊編碼(與MPLAB XC8 編譯器配合使用的技術(shù)),也可以使用傳達(dá)相同信息的特殊指針類型限定符(MPLAB XC16 和XC32 編譯器使用該方法)。
為保持代碼的可移植性,不應(yīng)假設(shè)將整數(shù)賦給指針就會使指針能訪問任何對象,即使該整數(shù)的值與某個對象的器件地址相同。因此對于上面的示例,為指針賦值立即數(shù)100h(或者保留此值的整數(shù)變量)并不意味著該指針指向變量a。
/* 我們發(fā)現(xiàn)“a”被分配到地址100h
*/int a, *p;
/* 注:這涉及整數(shù)到指針的隱式轉(zhuǎn)換 */
p = 0x100;
/* 沒人知道會發(fā)生什么!*/
*p = 0;
請記住,一種地址空間中的取指和存儲可能不像另一種地址空間中的取指和存儲一樣簡單——編譯器可能需要使用不同的寄存器和指令才能執(zhí)行訪問。
基于同樣的原因,在定義指針時,必須使用適當(dāng)?shù)闹羔橆愋拖薅ǚ。由?MPLAB XC8 對地址進(jìn)行編碼,因此它不使用特殊地址空間限定符,而MPLAB XC16 和XC32 則使用。但是,兩種情況下都必須適時使用通常的const 和volatile 限定符。限定符在數(shù)據(jù)定義中指定,如果想要可靠地訪問該數(shù)據(jù),則需要使用與引用該數(shù)據(jù)的指針相匹配的限定符。例如,使用MPLABXC16 時:
__psv__ char buffer[8] __attribute__((space(psv)))
在閃存程序存儲器中放置一個字符數(shù)組buffer,可通過“psv”(程序空間可視性)窗口進(jìn)行訪問。直接訪問buffer 將使編譯器生成可確保psv 窗口(位于處理器地址空間中的特定位置)映射到閃存(包含“buffer”)中適當(dāng)位置的代碼。buffer 的“地址”是所需窗口設(shè)置與“buffer”在整個窗口中的可視區(qū)域內(nèi)的偏移量的組合。
通過指針引用“buffer”中的項時,必須使用如下指針:
__psv__ char *bp;
才能使編譯器生成正確的代碼。不帶__psv__限定符的“普通”指針不起作用。
因此指針不僅僅是一個寬到可以保存“地址”的整數(shù),它還具有關(guān)聯(lián)的目標(biāo)類型;C 語言數(shù)據(jù)地址不僅僅是一個計算機(jī)存儲器地址,它可由編譯器修改或優(yōu)化。C 編譯器還會考慮其他一些事項。
出問題的位置
如果我們認(rèn)為指針只是一個值為(計算機(jī)存儲器)地址的整數(shù),并且認(rèn)為我們已了解地址的含義以及該存儲器中排列數(shù)據(jù)的方式,我們可能會想要在所編寫的C 語言代碼中顯式執(zhí)行各種各樣的地址運算,進(jìn)而在程序中嵌入底層運行時環(huán)境的特定于實現(xiàn)的詳細(xì)信息。這樣一來,即使現(xiàn)在程序可以運行,但如果針對其他處理器進(jìn)行編譯,可能就無法正常工作,或者可能在看起來無關(guān)緊要的更改后莫名停止工作。我們該如何避免這類問題呢?
1. 使用正確的指針類型。根據(jù)引用的數(shù)據(jù)選擇適用的指針類型。盡管在你添加一系列轉(zhuǎn)換后程序會進(jìn)行編譯,但不要據(jù)此認(rèn)為程序會實際按照你的期望工作。它會按照你告訴它的方式工作,這可能與你的期望有很大不同。
2. 根據(jù)你將用來訪問數(shù)據(jù)的結(jié)構(gòu)來分配數(shù)據(jù)
3. 不要猜測數(shù)據(jù)類型的布局
例如,可以分配一個字符緩沖區(qū),然后將該緩沖區(qū)的地址轉(zhuǎn)換為指向更大類型數(shù)據(jù)數(shù)組或結(jié)構(gòu)數(shù)組的指針。隨后你可能會通過不同類型的指針,有時訪問字符型數(shù)據(jù),有時訪問其他類型的數(shù)據(jù)。為此,必須知道更大類型的數(shù)據(jù)在字符數(shù)據(jù)上以及彼此之間的排列方式。這非常危險而且容易出錯。如果需要通過多類型“視圖”訪問數(shù)據(jù),請將數(shù)據(jù)分配成聯(lián)合數(shù)組,然后通過聯(lián)合訪問數(shù)據(jù)。編譯器將清楚你的意圖并幫助你正確實現(xiàn)。
示例
下面的 C 程序建立了一個初始化結(jié)構(gòu)數(shù)組,顯示該數(shù)組,修改數(shù)組的一個元素,最后顯示更新的結(jié)果。代碼中針對選擇和更新要更改的元素提供了幾種備選方法。其中一些是常用方法,但實際上是不安全的代碼模式:
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: /* 輸出結(jié)構(gòu)數(shù)組 */
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: /* 選擇下標(biāo)為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: /* 顯示更新的數(shù)組 */
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: }
我們討論一下如何訪問要修改的第二個結(jié)構(gòu)元素。在第10 行中聲明的twointbuf 是一個結(jié)構(gòu)數(shù)組,相當(dāng)于指向該數(shù)組首地址的指針。我們可以通過數(shù)組或指針語法來訪問該數(shù)組中的元素,這兩種編碼風(fēng)格表示同一個意思。第31 行和第35 行中給出的備選方法均是獲取指向數(shù)組中元素2 的指針的安全方法。編譯器不會將“2”解讀成兩個字節(jié)或兩個“字”,而是解讀成元素0 和元素1 后面的元素的編號2。
在第 40 行,我們看到了根據(jù)數(shù)組的字節(jié)地址以及前面元素的長度(字節(jié))來計算結(jié)構(gòu)元素地址的示例。如果(char *)上的限定符與數(shù)組上的限定符(本示例中沒有)匹配,則這種方法可行——只要字符指針和數(shù)組均聲明為引用相同的地址空間,地址和增量映射到底層存儲的規(guī)則就會相同,且該代碼有效。但為什么要這樣做呢?使用C 語言提供的簡潔明了的語法,編譯器將生成同樣正確或更有效的代碼。
在第 45 行,此代碼假設(shè)結(jié)構(gòu)元素的長度(字節(jié))是兩個成員的長度之和。這是不安全的假設(shè),因為編譯器可能必須對結(jié)構(gòu)進(jìn)行填充才能使兩個成員在自然字邊界上對齊。是否使用結(jié)構(gòu)填充將取決于目標(biāo)器件。
第 48 行上的語句一開始沒有將數(shù)組指針轉(zhuǎn)換為字符指針,而是轉(zhuǎn)換為大到足以保存指針的整數(shù),從而向編譯器隱藏了該值是特定地址空間中的地址的事實。隨后執(zhí)行與第40 行相同的地址運算,并將結(jié)果轉(zhuǎn)換回指向結(jié)構(gòu)數(shù)組的指針。在這種情況下,編譯器沒有機(jī)會對添加為特定空間中的指針和下標(biāo)的數(shù)字進(jìn)行解讀,且無法應(yīng)用任何映射規(guī)則。因此轉(zhuǎn)換回結(jié)構(gòu)指針的值可能是錯誤的。
結(jié)論
使用C 語言的功能時,應(yīng)依據(jù)功能在語言中的含義:
使用地址運算符來獲取要賦給指針的地址。
確保所定義的指針類型在程序執(zhí)行期間與其可引用的數(shù)據(jù)相匹配。
決不要假設(shè)對象分配到存儲器的方式。
不要假設(shè)或回避規(guī)則來使 C語言代碼更“直接”和“有效”,此類代碼不會具有可移植性、可靠性或更有效。
【C語言中可移植且可靠的指針運算】相關(guān)文章:
C語言中的指針是什么08-08
C語言中指針的概念03-16
C語言中野指針的深入解析08-06
C語言中的指針和內(nèi)存泄漏08-07
C語言中的運算規(guī)則10-14
c語言中指針的使用方法10-12
C語言中指針變量作為函數(shù)參數(shù)詳解07-01
C語言指針的概念08-20