c++ what - 局部變量的內存是否可以在其範圍之外訪問?





is dangling (18)


你在這裡做的只是簡單的閱讀和寫入到曾經是地址的內存。 現在你已經超出了foo ,它只是一個隨機存儲區域的指針。 在你的例子中恰好如此,那個存儲區域確實存在,此刻沒有其他的東西在使用它。 你不會因為繼續使用而破壞任何東西,也沒有其他東西會覆蓋它。 因此, 5仍然存在。 在一個真正的程序中,這個記憶幾乎會立即被重複使用,並且你會因為這樣做而破壞某些東西(儘管這些症狀可能在很晚之後才會出現!)

當您從foo返回時,您告訴操作系統您不再使用該內存,並且可以將其重新分配給其他內容。 如果你很幸運,並且它永遠不會被重新分配,並且操作系統不會讓你再次使用它,那麼你就會擺脫謊言。 雖然你最終可能會寫下任何與該地址相關的結果。

現在如果你想知道為什麼編譯器不會抱怨,那可能是因為foo被優化消除了。 它通常會警告你這類事情。 C假定你知道你在做什麼,從技術上說你沒有違反範圍(沒有提到foo以外的本身),只有內存訪問規則,它只觸發警告而不是錯誤。

簡而言之:這通常不會奏效,但有時會偶然。

我有以下代碼。

int * foo()
{
    int a = 5;
    return &a;
}

int main()
{
    int* p = foo();
    cout << *p;
    *p = 8;
    cout << *p;
}

代碼只是在沒有運行時異常的情況下運行!

產量是58

怎麼會這樣? 在其功能之外,是不是本地變量的內存不可訪問?




在典型的編譯器實現中,您可以將代碼想像為“ 用過去由a佔據的地址輸出內存塊的值”。 另外,如果你將一個新的函數調用添加到一個強製本地int的函數中,那麼a (或者用來指向的內存地址)的值改變a可能性很大。 發生這種情況是因為堆棧將被包含不同數據的新幀覆蓋。

但是,這是未定義的行為,你不應該依靠它來工作!




在C ++中,你可以訪問任何地址,但這並不意味著你應該 。 您正在訪問的地址不再有效。 它的工作原理是因為在foo返回之後沒有其他任何內容攪亂了內存,但在許多情況下它可能會崩潰。 嘗試使用Valgrind分析您的程序,或者甚至只是編譯優化過的程序,然後參閱...




這絕對是一個時間問題! p指針指向的p像是“預定的”,當它超出foo的範圍時將被銷毀。 但是,該操作不會立即發生,而是會在幾個CPU週期後發生。 不管這是不確定的行為,還是C ++實際上是在後台執行一些預清理工作,我都不知道。

如果您在調用foocout語句之間插入對您的操作系統sleep函數的調用,在解除引用指針之前使程序等待一秒鐘左右,您會注意到數據在您想讀取它時已經消失! 看看我的例子:

#include <iostream>
#include <unistd.h>
using namespace std;

class myClass {
public:
    myClass() : i{5} {
        cout << "myClass ctor" << endl;
    }

    ~myClass() {
        cout << "myClass dtor" << endl;
    }

    int i;
};

myClass* foo() {
    myClass a;
    return &a;
}

int main() {

    bool doSleep{false};

    auto p = foo();

    if (doSleep) sleep(1);

    cout << p->i << endl;
    p->i = 8;
    cout << p->i << endl;
}

(請注意,我使用了unistd.hsleep函數,它只存在於類Unix系統上,所以如果你使用的是Windows系統,你需要用Sleep(1000)Windows.h替換它。)

我用一個類替換了你的int ,所以我可以確切地看到何時調用析構函數。

此代碼的輸出如下所示:

myClass ctor
myClass dtor
5
8

但是,如果您將doSleep更改為true

myClass ctor
myClass dtor
0
8

正如你所看到的,應該銷毀的對象實際上是被銷毀的,但是我想有一些預清理指令必須在對象(或者變量)被銷毀之前執行,所以直到完成之後,數據仍然可以在很短的時間內訪問(但是當然不能保證,所以請不要編寫依賴於此的代碼)。

這很奇怪,因為在退出範圍時立即調用析構函數,但是實際的破壞會稍微延遲。

我從來沒有真正閱讀指定這種行為的官方ISO C ++標準的一部分,但很可能是,標準只承諾您的數據一旦超出範圍就會被銷毀,但它沒有提到任何有關這是在執行任何其他指令之前立即發生的。 如果是這樣,那麼這種行為就完全沒有問題,人們只是誤解了標準。

或者另一個原因可能是不符合標準的厚臉皮編譯器。 事實上,這不是編譯器為了獲得額外性能而進行一點標準兼容性的唯一情況!

無論這個原因是什麼,很明顯,數據被破壞,而不是立即。




從函數返回後,所有標識符都被銷毀,而不是將值保存在內存位置,我們無法找到沒有標識符的值。但該位置仍包含前一個函數存儲的值。

所以,這裡函數foo()返回的地址是aa ,在返回它的地址後被銷毀。 您可以通過返回的地址訪問修改後的值。

讓我舉一個真實世界的例子:

假設一個人在一個位置隱藏錢並告訴你位置。 過了一段時間,那個告訴你錢的位置的人死了。 但是你仍然可以獲得隱藏的資金。




你只是返回一個內存地址,這是允許的,但可能是一個錯誤。

是的,如果你試圖解引用那個內存地址,你會有未定義的行為。

int * ref () {

 int tmp = 100;
 return &tmp;
}

int main () {

 int * a = ref();
 //Up until this point there is defined results
 //You can even print the address returned
 // but yes probably a bug

 cout << *a << endl;//Undefined results
}



注意所有警告。 不僅要解決錯誤。
GCC顯示此警告

警告:返回局部變量'a'的地址

這是C ++的力量。 你應該關心記憶。 用-Werror標誌,這個警告成了一個錯誤,現在你必須調試它。




它可以,因為a是在其範圍的生命週期( foo函數)中臨時分配的變量。 從foo返回後,內存空閒並可以被覆蓋。

你在做什麼被描述為未定義的行為 。 結果無法預測。




因為存儲空間尚未被踩踏。 不要指望這種行為。




正如Alex所指出的那樣,這種行為是不確定的 - 事實上,大多數編譯器都會警告不要這樣做,因為這是一種容易導致崩潰的方法。

對於您可能會遇到的那種令人毛骨悚然的行為的示例,請嘗試以下示例:

int *a()
{
   int x = 5;
   return &x;
}

void b( int *c )
{
   int y = 29;
   *c = 123;
   cout << "y=" << y << endl;
}

int main()
{
   b( a() );
   return 0;
}

這會打印出“y = 123”,但結果可能會有所不同(真的!)。 你的指針正在破壞其他不相關的局部變量。




所有答案的補充:

如果你做這樣的事情:

#include<stdio.h>
#include <stdlib.h>
int * foo(){
    int a = 5;
    return &a;
}
void boo(){
    int a = 7;

}
int main(){
    int * p = foo();
    boo();
    printf("%d\n",*p);
}

輸出可能會是:7

這是因為從foo()返回後,堆棧被釋放,然後被boo()重用。 如果你反彙編可執行文件,你會清楚地看到它。




你實際上調用了未定義的行為。

返回臨時作品的地址,但臨時作品在函數結束時被銷毀,訪問它們的結果將不確定。

所以你沒有修改a而是修改a曾經的內存位置。 這種差異非常類似於崩潰和不崩潰之間的差異。




你的問題與範圍無關。 在你顯示的代碼中,函數main看不到函數foo的名字,所以你不能直接用foo外的這個名稱訪問foo

您遇到的問題是為什麼程序在引用非法內存時不會發出錯誤信號。 這是因為C ++標準沒有在非法內存和合法內存之間指定一個非常明確的界限。 在彈出的堆棧中引用某些東西有時會導致錯誤,有時不會。 這取決於。 不要指望這種行為。 假設在編程時它總是會導致錯誤,但假設它在調試時不會發出錯誤信號。




這是使用內存地址的'骯髒'的方式。 當你返回一個地址(指針)時,你不知道它是否屬於函數的局部範圍。 這只是一個地址。 現在你調用了'foo'函數,'a'的那個地址(內存位置)已經被分配到了你的應用程序(進程)的(安全地,至少現在)可尋址內存中。 'foo'函數返回後,'a'的地址可以被認為是'臟的',但它在那裡,沒有被清理,也沒有被程序其他部分(至少在這個具體情況下)的表達式乾擾/修改。 AC / C ++編譯器不會阻止你從這種'臟'訪問(如果你在意的話,可能會警告你)。 除非通過某種方式保護地址,否則可以安全地使用(更新)程序實例(進程)數據段中的任何內存位置。




你是否啟用了優化器來編譯你的程序?

foo()函數非常簡單,可能在代碼中被內聯/替換。

但是我同意馬克B的意見,結果行為是不確定的。




它的工作原理是堆棧沒有被改變(但),因為放在那裡。 在再次訪問之前調用一些其他函數(也調用其他函數),您可能不再那麼幸運了...... ;-)




如果您使用:: printf而不是cout,則具有正確(?)控制台輸出的內容可能會發生顯著變化。 您可以在下面的代碼中使用調試器(在x86,32位,MSVisual Studio中測試):

char* foo() 
{
  char buf[10];
  ::strcpy(buf, "TEST”);
  return buf;
}

int main() 
{
  char* s = foo();    //place breakpoint & check 's' varialbe here
  ::printf("%s\n", s); 
}



好問題。 我最近試圖寫類似的代理類,但從來沒有取得好的解決方案。 我發現最好的方式是在代理被要求為r值的每一次使用中調用成員函數:

ORef<T> move() {
    return ORef<T>( this->release() );
}

這改變了將std::move(proxy)的r值聲明為proxy.move() ,但也允許返回不同類型的對象(隱式轉換為您所需的類型)的可能性。

我的編碼習慣是通過代理對像作為rvalues來強製手動指定語義(移動,共享引用,複製或其他),但這當然會導致使用錯誤成為潛在的問題(例如,在調用x.move()之前x )的最終用法。





c++ memory-management local-variables dangling-pointer