2015-08-17 92 views
-6

正在關注another question which caused much confusion,這裏是關於指針語義的問題,希望能夠解決這些問題:指針的memcpy與賦值相同嗎?

這個程序是否在所有情況下都有效?唯一有趣的部分是在「pa1 == pb」分支。

#include <stdio.h> 
#include <string.h> 

int main() { 
    int a[1] = { 0 }, *pa1 = &a[0] + 1, b = 1, *pb = &b; 
    if (memcmp (&pa1, &pb, sizeof pa1) == 0) { 
     int *p; 
     printf ("pa1 == pb\n"); // interesting part 
     memcpy (&p, &pa1, sizeof p); // make a copy of the representation 
     memcpy (&pa1, &p, sizeof p); // pa1 is a copy of the bytes of pa1 now 
     // and the bytes of pa1 happens to be the bytes of pb 
     *pa1 = 2; // does pa1 legally point to b? 
    } 
    else { 
     printf ("pa1 != pb\n"); // failed experiment, nothing to see 
     pa1 = &a[0]; // ensure well defined behavior in printf 
    } 
    printf ("b = %d *pa1 = %d\n", b, *pa1); 
    return 0; 
} 

我想要一個基於標準報價的答案。

編輯

應廣大用戶要求,這裏是我想知道:

  • 是一個指針的語義「價值」(根據規範其行爲)只能由它的數值確定(在它包含的數字地址),給定類型的指針?
  • 如果不是,有可能只複製包含在指針中的物理地址,而忽略相關的語義?

這裏我們假設有一個過去的結束指針碰巧意外指向另一個對象;我怎麼能使用這樣一個結束指針訪問另一個對象?

我有權做任何事情,除了使用其他對象的地址副本。 (這是一個瞭解C中指針的遊戲)

IOW,我試圖像黑手黨一樣回收髒錢。但是我通過提取其值表示來回收髒指針。然後它看起來像乾淨的錢,我的意思是指針。沒有人能分辨出來,不是嗎?

+0

是的,它是一樣的 –

+1

什麼是'memcpy(&p,&pa1,sizeof p)'和'memcpy(&pa1,&p,sizeof p)'應該用你自己的話來做?另外,你真的應該**用'memcmp'添加一些關於你的意圖的內容(參見我對Sourav Ghosh的評論)。 – DevSolar

+0

@DevSolar複製物理值,就像賦值int一樣;不轉移語義 – curiousguy

回答

4

指針只是一個無符號整數,其值是存儲器中某個位置的地址。覆蓋指針變量的內容與覆蓋正常的int變量的內容沒有區別。

所以,是的,例如, memcpy (&p, &pa1, sizeof p)相當於作業p = pa1,但可能效率較低。


讓我們嘗試有點不同,而不是:

你有pa1指向某個對象(或者更確切地說,一個超越一些對象),那麼你有指針&pa1指向變量pa1(即變量pa1在存儲器中的位置)。

圖形它會是這個樣子:

 
+------+  +-----+  +-------+ 
| &pa1 | --> | pa1 | --> | &a[1] | 
+------+  +-----+  +-------+ 

[注:&a[0] + 1相同&a[1]]

+0

「但效率較低」 - 大多數編譯器將這些函數視爲內置函數,因此可能會在兩種情況下都發出相同的代碼。 –

+0

好吧,那麼在解引用過去指向某個對象的結束指針時,是否存在未定義的行爲? – curiousguy

+0

@curiousguy但是沒有取消引用任何一個指針。如果你寫過例如'pa1'或'p'(沒有地址運算符),那麼你會有未定義的行爲。 –

2
*pa1 = 2; // does pa1 legally point to b? 

不,那pa1b,純屬巧合。請注意,程序必須在編譯時符合指針在運行時碰巧具有相同的值並不重要。

沒人能分辨出來,不是嗎?

編譯器優化器可以區分不同! 編譯器優化器可以看到(通過代碼的靜態分析)b並且永遠不會通過「合法」指針訪問,因此假設將b保存在寄存器中是安全的。這個決定是在彙編時做出的。

底線:

「法律」指針是通過分配或通過複製存儲從法律的指針得到指針。您也可以使用指針算術來獲得「合法」指針,前提是生成的指針位於從其分配/複製的數組/內存塊的合法範圍內。如果指針算術的結果恰好指向另一個存儲塊中的有效地址,那麼使用這樣的指針仍然是UB。

另請注意,只有兩個指針指向相同的數組/內存塊時,指針比較纔有效。

+0

「_還要注意,只有當兩個指針指向同一個數組/內存塊_時,指針比較纔有效。」你是什麼意思? – curiousguy

+1

@curiousguy:'int x = 1,y = 2,* px =&x,* py =&y;' - 因爲'x'和'y'不在同一個數組中,'if(px DevSolar

+1

如果將[i]與&b進行比較,結果不是由標準定義的。該標準允許使用魔法,因此即使它們相等,也不必指向相同的內存。 –

2

你已經證明,它似乎在特定的實現工作。這並不意味着它的工作原理一般。事實上,在一個可能的結果完全「似乎有效」的情況下,這是未定義的行爲。

如果我們回到MS-DOS時代,我們接近指針(相對於特定段)和遠指針(包含段和偏移量)。

大型數組通常分配在它們自己的段中,只有偏移量被用作指針。編譯器已經知道哪個段包含一個特定的數組,因此它可以將指針與正確的段寄存器結合起來。

在這種情況下,您可以使用兩個具有相同位模式的指針,其中一個指針指向一個數組段(pa),另一個指針指向堆段(pb)。指針比較相同,但仍指向不同的東西。

更糟糕的是,具有段:offset對的遠指針可能會形成重疊段,因此位模式仍指向相同的物理內存地址。例如,0100:02100120:0010的地址相同。

C和C++語言被設計爲可以工作。這就是爲什麼我們有規則,比較指針只能在同一個數組中運行(提供總順序),並且指針可能不指向相同的事物,即使它們包含相同的位模式。

+0

因此,編譯器將不得不跟蹤指向哪個數組的指針? – curiousguy

+0

@curiousguy:在小型和中型內存模型中,帶有「far」限定符的指針需要4個字節來存儲並可以訪問任何內容;那些沒有限定符的代碼需要兩個字節來存儲,並且可以通過除了特殊的「遠malloc」調用以外的任何方式訪問代碼內創建的所有對象;通過近指針訪問通常比通過「遠」指針訪問快2-3倍;而有一些地方,指定遠指針是一個有點討厭的,使用小型或中型模型來代替大型號的性能優勢往往非常巨大。 – supercat

2

未定義的行爲:玩n部分。

編譯器1和編譯器2進入階段。

int a[1] = { 0 }, *pa1 = &a[0] + 1, b = 1, *pb = &b; 

[Compiler1]你好,apa1bpb。很高興認識你。現在你就坐在那裏,我們將查看代碼的其餘部分,看看我們是否可以爲你分配一些不錯的堆棧空間。

Compiler1查看代碼的其餘部分,偶爾皺眉,並在紙上做一些標記。編譯器2摘下鼻子並凝視窗外。

[編譯器1]那麼,我恐怕,b,我決定優化你。我根本找不到修改你記憶的地方。也許你的程序員用Undefined Behavior做了一些技巧來解決這個問題,但我可以假設沒有這樣的UB存在。對不起。

退出b,熊追求。

[編譯器2]等等!等一下,b。我無法優化這些代碼,所以我決定在堆棧上給你一個很好的舒適空間。

b在歡樂中跳躍,但只要他通過未定義的行爲被修改,就會被鼻魔所殺。

[旁白]因此,結束變量b的悲傷,悲傷的故事。這個故事的寓意是,永遠不能依靠未定義的行爲

+0

那麼,他需要'b'的地址,然後將其饋送給函數,否則這確實是一個明確的情況。 ;-) – DevSolar

+0

我不相信這裏有UB! – curiousguy

2

在C99之前,預計實現的行爲就好像任何類型的每個變量的值都存儲了一個unsigned char值的序列;如果檢查相同類型的兩個變量的基本表示並且發現它們相等,則這意味着,除非發生未定義行爲,否則它們的值通常是相等且可互換的。在一些地方有一些模棱兩可的情況,例如鑑於

char *p,*q; 
p = malloc(1); 
free(p); 
q = malloc(1); 
if (!memcmp(&p, &q, sizeof p)) 
    p[0] = 1; 

的C每一個版本已經非常清楚地表明q可能會或可能不等於p,如果q不等於p代碼應該想到,當p[0]寫入任何可能發生的事情。雖然C89標準沒有明確指出,如果寫入p將等於寫入q,則實現可能僅具有按位等於q的比較,這樣的行爲通常會被完全封裝在序列中的變量模型暗示的值爲unsigned char

C99增加了很多情況,其中變量可能會按比特相等但不相等。舉個例子:

extern int doSomething(char *p1, char *p2); 
int act1(char * restrict p1, char * restrict p2) 
    { return doSomething(p1,p2); } 
int act2(char * restrict p) 
    { return doSomething(p,p); } 
int x[4]; 
int act3a(void) { return act1(x,x); } 
int act3b(void) { return act2(x); } 
int act3c(void) { return doSomething(x,x); } 

調用act3aact3b,或act3c會導致doSomething()與兩個指針是比較等於x被調用,但如果通過act3a調用的x任何元件,其doSomething必須在寫專門使用x,專門使用p1,或專門使用p2。如果通過act3b調用,該方法將獲得使用p1寫入元素的自由,並通過p2訪問它們,反之亦然。如果通過act3c訪問,則該方法可以交替使用p1,p2x。在p1p2二進制表示沒有什麼能表明是否可以與x交替使用,但是編譯器將允許在聯機中act1act2擴大doSomething,並將這些擴展的行爲根據不同是什麼指針訪問是允許和禁止。