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 還要用呢