C/C++函數返回局部變量相關問題
函數返回局部變量的時候會遇到各種各樣的情況,涉及到內存相關的東西時一定要小心是否會出錯。
1、常見棧內變量
一般來說,在函數內對於存在棧上的局部變量的作用域只在函數內部,在函數返回後,局部變量的內存已經釋放。因此,如果函數返回的是局部變量的值,不涉及地址,程序不會出錯;但是如果返回的是局部變量的地址(指針)的話,就造成了野指針,程序運行會出錯,因爲函數只是把指針複製後返回了,但是指針指向的內容已經被釋放了,這樣指針指向的內容就是不可預料的內容,調用就會出錯。
#include <iostream>
using namespace std;
int fun1() {
int i = 1;
return i; // OK.
}
int* fun2() {
int i = 2;
int* ip = &i;
return ip; // Wrong!
}
int main() {
int r1 = fun1();
cout << r1 << endl; // 1
int* r2 = fun2();
cout << *r2 << endl; // 這裏有可能可以打印出結果:2,看似正確的,但其實是有問題的。這是因爲相應的內存還未被覆蓋,但這塊內存已經是自由的、不被保護的了。
return 0;
}
2、字符串
先看代碼:
#include <iostream>
using namespace std;
char* fun3() {
char* s = "Hello";
return s; // OK.
}
char* fun4() {
char s[] = "Hello";
return s; // Wrong!
}
int main() {
char* r3 = fun3();
cout << r3 << endl; // Hello
char* r4 = fun4();
cout << r4 << endl; // 內存已經無效的了。打印出亂碼。
return 0;
}
通過 char* s = "Hello";
的方式得到的是一個字符串常量 Hello
,存放在只讀數據段(.rodata
section),把該字符串常量的只讀數據段的首地址賦值給了指針 s
,所以函數返回時,該字符串常量所在的內存不會被回收,所以能正確地通過指針訪問。
通過 char s[] = "Hello";
的方式得到的是局部變量,字符串直接量作爲基於棧的字符數組的初始值,這裏得到的字符數組 s
實際的數據存儲: H
e l l o \0
。增加了一個終結符 \0
。所以函數返回時,棧被清空,局部變量內存清空,返回的是一個已被釋放的內存的地址,打印出來的就會是亂碼。
3、靜態變量
如果函數的返回值非要是一個局部變量地址,可以把局部變量聲明爲static
靜態變量。這樣變量存儲在靜態存儲區,程序運行過程中一直存在。
#include <iostream>
using namespace std;
int* fun5() {
static int i = 5;
return &i; // OK.
}
char* fun6() {
static char s[] = "Hello";
return s; // OK.
}
int main() {
int* r5 = fun5();
cout << *r5 << endl; // 5
char* r6 = fun6();
cout << r6 << endl; // Hello
return 0;
}
4、數組
數組是不能作爲函數的返回值的。因爲編譯器會把數組名認爲是局部變量(數組)的地址。返回一個數組,實際上是返回指向這個數組首地址的指針。函數結束後,數組作爲局部變量被釋放,這個指針則變成了野指針。同1的fun2()
及2的fun4()(字符數組)
。
但是聲明數組是靜態的,然後返回是可以的,同3。
#include <iostream>
using namespace std;
int* fun7() {
int a[3] = {1, 2, 3};
return a; // Wrong!
}
int* fun8() {
static int a[3] = {1, 2, 3};
return a; // OK.
}
int main() {
int* r7 = fun7();
cout << *r7 << endl; // 內存已經是無效的了。
int* r8 = fun8();
cout << *r8 <<endl; // 1
return 0;
}
5、堆內變量
函數返回指向存儲在堆上的變量的指針是可以的。但是,程序員要自己負責在函數外釋放(free/delete)分配(malloc/new)在堆上的內存。
#include <iostream>
using namespace std;
char* fun9() {
char* s = (char*) malloc(sizeof(char) * 100);
return s; // OK. 但需要程序員自己釋放內存。
}
int main() {
char* r9 = NULL;
r9 = fun9();
strcpy(r9, "Hello");
cout << r9 << endl; // Hello
free(r9); // 要記得自己釋放內存。
return 0;
}
6、對象
函數內局部對象的引用是不能作爲函數返回值的。函數內的局部對象在離開函數作用域後會被自動析構(自動調用其析構函數),則它的引用不再指向一個有效對象,結果無法預測。就類似1的fun2()
返回局部變量地址後,這個地址指向的內存其實已經不再有效了。
函數內用new創建的堆上的對象的指針是可以作爲函數返回值的,但是類似5要程序員自己負責在函數外釋放對象內存。
classa.h
#ifndef CLASSA_H
#define CLASSA_H
class ClassA
{
public:
int x;
int y;
int z;
public:
ClassA(int xx, int yy, int zz);
~ClassA();
void printXYZ();
};
#endif // CLASSA_H
classa.cpp
#include "classa.h"
#include <iostream>
using namespace std;
ClassA::ClassA(int xx, int yy, int zz) : x(xx), y(yy), z(zz) {
cout << "Construct a ClassA" << endl;
}
ClassA::~ClassA() {
cout << "Deconstruct a ClassA" << endl;
}
void ClassA::printXYZ() {
cout << x << " " << y << " " << z << endl;
}
main.cpp
#include <iostream>
using namespace std;
ClassA& fun10() {
ClassA ca(1, 2, 3);
return ca; // Wrong!
}
ClassA* fun11() {
ClassA* ca = new ClassA(1, 2. 3);
return ca; // OK. 但需要程序員自己釋放內存。
}
int main() {
ClassA r10 = fun10();
r10.printXYZ(); // 對象在函數返回時已經被析構了,對應的內存已經無效。
ClassA* r11 = fun11();
r11->printXYZ(); // 1 2 3
delete r11; // 要記得自己釋放內存。
return 0;
}
小結
總結上面的幾個例子,我們只要搞清楚函數返回的東西是什麼、其對應的內存存在哪一般就可以搞清楚它能不能被返回,其實有的例子印證的結果是重疊的。
一個完整的程序,內存分佈情況:
上面的幾個例子中,我們返回的東西包括:
棧上變量值
(基本類型)可以作爲函數返回值;因爲只涉及到傳值,不會有什麼問題;棧上變量地址
(基本類型指針;數組;對象引用)不能在作爲函數返回值;因爲它們的作用域只在函數內,函數返回後它們的內存都不再有效;堆上變量地址
(malloc或new得到的指針)可以作爲函數返回值;變量存在堆上,其內存從分配後一直有效直到程序員自己釋放(free或delete);靜態變量地址
(static)可以作爲函數返回值;因爲它們存儲在靜態變量區,其內存整個程序運行期間一直有效;只讀數據段
(.rodata)地址(字符串常量)可以作爲函數返回值;因爲它們存儲在只讀數據段,其內存整個程序運行期間一直有效;