感謝@榴蓮小小蘇 的熱心翻譯。如果其他朋友也有不錯的原創或譯文,歡迎投遞到伯樂頭條。
從計算機記憶體的角度思考C語言中的一切東東,是挺有幫助的。我們可以把計算機記憶體想象成一個位元組陣列,記憶體中每一個地址表示 1 位元組。比方說我們的電腦有 4K 記憶體,那這個記憶體陣列將會有 4096 個元素。當我們談論一個儲存地址的指標時,就當相於我們在談論一個儲存著該記憶體陣列某個元素索引的指標。逆向引用某個指標,將會得到陣列中該索引所指向的值。這一切當然都是謊言。作業系統對記憶體的管理要遠比這複雜。記憶體不一定連續,也不一定按順序處理。但前面的類比是一種討論C語言記憶體的簡單方式。
如果對『指標』、『地址』和『逆向引用』感到混亂,請看《C語言指標5分鐘教程》。// 譯註:“dereferencing” 的譯法比較多,本文采用了“逆向引用”。
假設我們的計算機有 4K 的記憶體,下一個開放地址的索引是2048。我們宣告一個新的字元變數i='a'
。當該變數所獲得的記憶體放置了它的值,變數的名字也與記憶體中的該位置關聯,我們的字元i就獲得了一個儲存在2048位置的值。該字元是單位元組的因此它只佔用了索引為 2048 的位置。如果我們對 i 變數使用地址操作符(&),它將返回到索引為2048的位置。如果這個變數是另一種型別,比如是 int,它將佔用4位元組,在陣列中佔用索引為 2048-2051 的位置。使用地址操作符仍將返回索引2048的位置,因為 int 型即便佔用了 4 位元組,但它開始於 2048 位置。我們看一個例子:
1 2 3 4 5 6 7 8 9 10 11 |
// intialize a char variable, print its address and the next address char charvar = '\0'; printf("address of charvar = %p\n", (void *)(&charvar)); printf("address of charvar - 1 = %p\n", (void *)(&charvar - 1)); printf("address of charvar + 1 = %p\n", (void *)(&charvar + 1)); // intialize an int variable, print its address and the next address int intvar = 1; printf("address of intvar = %p\n", (void *)(&intvar)); printf("address of intvar - 1 = %p\n", (void *)(&intvar - 1)); printf("address of intvar + 1 = %p\n", (void *)(&intvar + 1)); |
執行將得到如下的輸出:
1 2 3 4 5 6 |
address of charvar = 0x7fff9575c05f address of charvar - 1 = 0x7fff9575c05e address of charvar + 1 = 0x7fff9575c060 address of intvar = 0x7fff9575c058 address of intvar - 1 = 0x7fff9575c054 address of intvar + 1 = 0x7fff9575c05c |
在第一個例子的1-5行中,我們宣告瞭一個字元變數,並列印輸出該字元的地址,然後列印了記憶體中位於該變數前後的兩個地址。我們是通過使用&操作符並+1或-1來獲取前後兩個地址的。在7-11行的第二個例子中我們做了差不多的事,除了宣告瞭一個int型變數,列印出它的地址以及緊鄰它前後的地址。
在輸出中,我們看到地址是 16 進位制的。更值得注意的是,字元的地址前後相差1位元組。int 型變數地址前後相差四位元組。記憶體地址的演算法、指標的演算法、都是根據所引用的型別的大小的。一個給定的型別的大小是依賴於平臺的,我們這個例子中的char是1位元組,int是四位元組。將字元的地址-1是改地址前的地址,而將int型地址-1是該地址前4個的地址。
在例子中,我們是用地址操作符來獲取變數的地址,這和使用表示變數地址的指標是一樣的效果。
英文原博中評論已經提出:儲存&charvar-1(一個非法的地址因它位於陣列之前)在技術上是未特別指出的行為。C的標準已經宣告,未特別指出的以及在一些平臺儲存一個非法地址都將引起錯誤。
陣列地址
在C語言中,陣列是相鄰的記憶體區域,它儲存了大量相同資料型別的值(int、long、*char等等)。很多程式設計師第一次用C時,會將陣列當做指標。那是不對的。指標儲存一個簡單的記憶體地址,而一個陣列是一塊儲存多個值的連續的記憶體區域。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// initialize an array of ints int numbers[5] = {1,2,3,4,5}; int i = 0; // print the address of the array variable printf("numbers = %p\n", numbers); // print addresses of each array index do { printf("numbers[%u] = %p\n", i, (void *)(&numbers[i])); i++; } while(i < 5); // print the size of the array printf("sizeof(numbers) = %lu\n", sizeof(numbers)); |
執行將得到如下的輸出:
1 2 3 4 5 6 7 |
numbers = 0x7fff0815c0e0 numbers[0] = 0x7fff0815c0e0 numbers[1] = 0x7fff0815c0e4 numbers[2] = 0x7fff0815c0e8 numbers[3] = 0x7fff0815c0ec numbers[4] = 0x7fff0815c0f0 sizeof(numbers) = 20 |
在這個例子中,我們初始化了一個含有 5 個 int 元素的陣列,我們列印了陣列本身的地址,注意我們沒有使用地址操作符 & 。這是因為陣列變數已經代表了陣列首元素的地址。你會看到陣列的地址與陣列首元素的地址是一樣的。然後我們遍歷這個陣列並列印每個元素的記憶體地址。在我們的計算機中 int 是四個位元組的,陣列記憶體是連續的,因此每個int型元素地址之間相差4。
在最後一行,我們列印了陣列的大小,陣列的大小等於sizeof(type)乘上陣列元素的數量。這裡的陣列有5個int型變數,每一個佔用4位元組,因此整個陣列大小為20位元組。
結構體地址
在C語言中,結構體一般是連續的記憶體區域,但也不一定是絕對連續的區域。和陣列類似,它們能儲存多種資料型別,但不同於陣列的是,它們能儲存不同的資料型別。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
struct measure { char category; int width; int height; }; // declare and populate the struct struct measure ball; ball.category = 'C'; ball.width = 5; ball.height = 3; // print the addresses of the struct and its members printf("address of ball = %p\n", (void *)(&ball)); printf("address of ball.category = %p\n", (void *)(&ball.category)); printf("address of ball.width = %p\n", (void *)(&ball.width)); printf("address of ball.height = %p\n", (void *)(&ball.height)); // print the size of the struct printf("sizeof(ball) = %lu\n", sizeof(ball)); |
執行後的輸出結果如下:
1 2 3 4 5 |
address of ball = 0x7fffd1510060 address of ball.category = 0x7fffd1510060 address of ball.width = 0x7fffd1510064 address of ball.height = 0x7fffd1510068 sizeof(ball) = 12 |
在這個例子中我們定義了一個結構體measure,然後宣告瞭該結構體的一個例項ball,我們賦值給它的width、height以及category成員,然後列印出ball的地址。與陣列類似,結構體也代表了它首元素的地址。然後列印了它每一個成員的地址。category是第一個成員,它與ball具有相同的地址。width後面是height,它們都具有比category更高的地址。
你可能會想因為category是一個字元,而字元型變數佔用1位元組,因此width的地址應該比開始出高1個位元組。從輸出來看這不對。 根據C99標準(§6.7.2.1),為邊界對齊,結構體可以給成員增加填充位元組。它不會記錄資料成員,但會增加額外的位元組。在實際中,大多數的編譯器會使結構體中的每個成員與結構體最大的成員有相同大小,
在我們的例子中,你可以看到char實際上佔用4位元組,整個struct佔用12個位元組。都發生了什麼?
- struct變數指向struct首元素的地址
- 不要去假設一個結構體的成員相對於另外一個成員有多少記憶體偏移量,結構體成員之間可能有邊界位元組,或者編譯器也可能將它們放在不連續的記憶體空間中。使用地址操作符&來獲得成員的地址
- 使用sizeof(struct instance)來獲得struct的總大小,不能假設它是各個成員大小的大小總和,也許還有填充位元組。
結論
喜歡這篇博文可以幫你理解更多的在C中如何操作不同的資料型別的地址。在以後的博文中,我們將會繼續研究一下指標和陣列的基礎。