C++ 之函數返回局部變量

1. 返回字符串字面量的指針,即存放在常量存儲區的數據,不會因爲函數調用棧被釋放而消失,所以操作可行。

char* const_str()
{
    char* p = "Hello World!"; // 指向常量區字符串的指針
    return p;
}

另外,其實這裏寫的不規範,C++11 標準要求在字面量指針增加 const,以防止猿們隨意改變其內容導致段錯誤。

 

2. 返回局部變量的 reference 引用

首先,不管是基本數據類型還是對象,返回局部引用都是錯誤的。返回的別名引用了函數內局部變量,它的數據區是放在棧空間的,在函數調用結束後,棧空間被釋放之後就不存在了,其數據可能會被覆蓋掉。

2.1 普通變量的引用 

通常編譯器會在 return i; 處放出警告,如 xcode Reference to stack memory associated with local variable 'i' returned

打印出的地址是一樣的,說明返回的引用並沒有真正的重新拷貝複製一份到新的棧空間,即 main 函數棧中,而是直接將它給了引用變量 p。

如果不是引用,則會拷貝一份新的內存,兩者地址是不同的。

int& refer()
{
    int i = 99;
    cout << &i << endl; // 0x7ffeefbff4ac
    return i;
}

void main() 
{
    int& p = refer();
    cout << &p << endl; // 0x7ffeefbff4ac
    cout << p << endl; // 32767(???)
}
int refer()
{
    int i = 99;
    cout << &i << endl; // 0x7ffeefbff4ac
    return i;
}

void main() 
{   
    int p = refer();
    cout << &p << endl; // 0x7ffeefbff4e4
    cout << p << endl; // 99
}

2.2 局部對象的引用

自定義了一個 Tools,data 初始化爲 "Hello, World"

問:什麼情況,A 跟 B 的結果居然不一樣?

答:原因在於編譯器在碰到 “=” 時在背後調用了默認的拷貝構造函數,即 Tools:Tools(const Tools& rhs);

data 指針也默認來自 rhs,即等號右手邊的對象。這時如果把析構函數的註釋去掉,運行時就會報錯 effectiveC++(6837,0x1000f45c0) malloc: *** error for object 0x1005401d0: pointer being freed was not allocated

 問:是不是真的調用了拷貝構造呢?

答:自己增加該函數 cout 一下便可以清楚看到。

注意⚠️,從這裏可以看出:如果對象中包含非普通數據成員變量(int, double...),最好明確給出拷貝構造函數的拷貝方式,保證可靠。

class Tools
{
public:
    const char* data;
    Tools()
    {
        data = new char[20]{'H','e', 'l', 'l', 'o', ','
                            , 'W', 'o', 'r', 'l', 'd'};
    }
    /** 
    ~Tools()
    {
        delete [] data; // release memery
    }
    **/
};

Tools& fun()
{
    Tools tool;
    cout << &tool << endl; // 0x7ffeefbff4b8
    return tool;
}
/**
A. 沒有賦值操作
**/
void main()
{
    cout << &fun() << endl; // 0x7ffeefbff4b8
}

/**
B. 有賦值操作
*/
void main()
{
    Tools tool = fun();
    cout << &tool << endl; // 0x7ffeefbff4e0
}

3. 局部對象

這個是本筆記📒要關注的,返回局部對象並非通常所說的拷貝一個臨時變量作爲返回,即調用拷貝構造函數(不論有無賦值操作)。現代編譯器都進行了優化,即 return value optimization (RVO)。編譯器遇到這樣的函數其實做了不少的更改,以保證最後的運行結果是你預期的,同時避免了拷貝內存這樣費時的工作。

首先來看看下面的代碼,想想會產生什麼樣的輸出:

class C
{
public:
    C() = default;
    C(const C&)
    {
        cout << "copy C" << endl;
    }
};

C F()
{
    return C();
}
void main()
{
    C c = F();
}

是的,什麼都不會輸出。問題又來了,如果按照剛剛的解釋,棧空間被釋放,F 返回的局部對象 C 應該被重新拷貝一份纔對,這時會調用拷貝構造函數。如果用不同的編譯器,你可能會看到三種不同的情況,這裏是一種,下面是另外兩種:

copy C
copy C
copy C

原因都在於編譯器生成了不同的"優化"代碼。

我這裏簡單寫一下大致第三種的優化情況👇

C* F(C* _hiddenAddress)
{
    C local; // local Object in F
    *_hiddenAddress = local;// copy once after assignment
    return _hiddenAddress;
}

void main()
{
    C _c; // 之所以不用指針是因爲最後還要 delete _c;
    C c = *F(&_c); // copy twice after return
}

這個優化真的是太棒了!成功拷貝了兩次,超乎想象👍

所以後來怎麼改的呢,是的,用過庫函數那些字符串處理函數的應該清楚,都是外部先聲明變量開闢空間,再將地址傳入函數體內進行賦值,這樣就避免了所有的拷貝操作。於是,上面的調用就被改成這樣,對的,F 什麼都沒幹。

void F(C* p)
{
}

void main()
{
    C c;
    F(&c);
}

爲了證明這一點,我們可以在 F 中打印地址進行對比,結果是一致的。當然,爲了保證程序的正常執行順序,優化的過程會比這複雜。

C F()
{
    C c;
    cout << "c1: " << &c << endl; // c1: 0x7ffeefbff4e0
    return c;
}

void main()
{
    C c = F();
    cout << "c2: " << &c << endl; // c2: 0x7ffeefbff4e0
}

注意⚠️,還記得剛剛那個析構函數重複釋放 data 的錯誤嗎,爲了說明返回的 c 並不會因爲指向相同的內存區域,被函數返回時釋放第二次出錯,現在再重新驗證。

class C
{
public:
    const char* data;
    C()
    {
        data = new char[20]{'H','e', 'l', 'l', 'o', ','
            , 'W', 'o', 'r', 'l', 'd'};
    }

    C(const C&)
    {
        cout << "copy C" << endl;
    }

    ~C()
    {
        cout << "delete C" << endl;
        delete [] data;
    }
};

C F()
{
    C c;
    return c; // 局部對象 c 並不會被釋放
}

void main()
{
    C c = F(); // ~C 也只會被執行一次
    // ~C()
}

4. 返回的是局部對象的指針,但賦值時是對象 Object 類型

這時候就像[3]所講,要重新拷貝新的內存到等號左邊賦值。

同樣是上邊的例子

class C
{
public:
    C() = default;
    C(const C&)
    {
        cout << "copy C" << endl;
    }
    ~C()
    {
        cout << "delete C" << endl;
    }
};

C* F()
{
    C c;
    return &c; 
    // delete c
}
void main()
{
    C c = *F(); // copy c
    // delete c
}
delete C
copy C
delete C

5. 返回的是局部對象指針,直接賦值指針,不允許❌

原因很清楚,局部對象已經被釋放了。

void main()
{
    C *c = F();
    cout << "c 還要用呢" << endl;
}
delete C
c 還要用呢

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章