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


Answers

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

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

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

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

Question

我有以下代碼。

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

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

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

產量是58

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




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

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




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




所有答案的補充:

如果你做這樣的事情:

#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佔據的地址輸出內存塊的值”。 另外,如果你將一個新的函數調用添加到一個強製本地int的函數中,那麼a (或者用來指向的內存地址)的值改變a可能性很大。 發生這種情況是因為堆棧將被包含不同數據的新幀覆蓋。

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




這絕對是一個時間問題! 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 ,在返回它的地址後被銷毀。 您可以通過返回的地址訪問修改後的值。

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

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




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

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




您從來不會通過訪問無效內存來拋出C ++異常。 您只是提供了引用任意內存位置的一般想法的示例。 我可以這樣做:

unsigned int q = 123456;

*(double*)(q) = 1.2;

在這裡,我簡單地將123456作為雙精度地址並寫入它。 任何事情都可能發生:

  1. q實際上可能真的是一個double的有效地址,例如double p; q = &p; double p; q = &p;
  2. q可能指向分配的內存中的某個地方,我只是在那裡覆蓋8個字節。
  3. q分配給分配內存以外的操作系統的內存管理器向我的程序發送分段錯誤信號,導致運行時終止它。
  4. 你贏了彩票。

你設置它的方式更合理一點,就是返回的地址指向一個有效的內存區域,因為它可能會稍微靠後一點,但它仍然是一個無效的位置,你無法訪問確定性的時尚。

沒有人會在正常的程序執行過程中自動檢查內存地址的語義有效性。 然而,像valgrind這樣的內存調試器會很高興地做到這一點,所以你應該通過它運行你的程序並見證錯誤。






Links