C/C++函數返回局部變量相關問題

C/C++函數返回局部變量相關問題

函數返回局部變量的時候會遇到各種各樣的情況,涉及到內存相關的東西時一定要小心是否會出錯。

一般來說,在函數內對於存在棧上的局部變量的作用域只在函數內部,在函數返回後,局部變量的內存已經釋放。因此,如果函數返回的是局部變量的值,不涉及地址,程序不會出錯;但是如果返回的是局部變量的地址(指針)的話,就造成了野指針,程序運行會出錯,因爲函數只是把指針複製後返回了,但是指針指向的內容已經被釋放了,這樣指針指向的內容就是不可預料的內容,調用就會出錯。

  1. #include <iostream>
  2. using namespace std;
  3. int fun1() {
  4. int i = 1;
  5. return i; // OK.
  6. }
  7. int* fun2() {
  8. int i = 2;
  9. int* ip = &i;
  10. return ip; // Wrong!
  11. }
  12. int main() {
  13. int r1 = fun1();
  14. cout << r1 << endl; // 1
  15. int* r2 = fun2();
  16. cout << *r2 << endl; // 這裏有可能可以打印出結果:2,看似正確的,但其實是有問題的。這是因爲相應的內存還未被覆蓋,但這塊內存已經是自由的、不被保護的了。
  17. return 0;
  18. }

先看代碼:

  1. #include <iostream>
  2. using namespace std;
  3. char* fun3() {
  4. char* s = "Hello";
  5. return s; // OK.
  6. }
  7. char* fun4() {
  8. char s[] = "Hello";
  9. return s; // Wrong!
  10. }
  11. int main() {
  12. char* r3 = fun3();
  13. cout << r3 << endl; // Hello
  14. char* r4 = fun4();
  15. cout << r4 << endl; // 內存已經無效的了。打印出亂碼。
  16. return 0;
  17. }

通過 char* s = "Hello"; 的方式得到的是一個字符串常量 Hello,存放在只讀數據段(.rodata section),把該字符串常量的只讀數據段的首地址賦值給了指針 s,所以函數返回時,該字符串常量所在的內存不會被回收,所以能正確地通過指針訪問。 通過 char s[] = "Hello"; 的方式得到的是局部變量,字符串直接量作爲基於棧的字符數組的初始值,這裏得到的字符數組 s 實際的數據存儲: H e l l o \0。增加了一個終結符 \0。所以函數返回時,棧被清空,局部變量內存清空,返回的是一個已被釋放的內存的地址,打印出來的就會是亂碼。

如果函數的返回值非要是一個局部變量地址,可以把局部變量聲明爲static靜態變量。這樣變量存儲在靜態存儲區,程序運行過程中一直存在。

  1. #include <iostream>
  2. using namespace std;
  3. int* fun5() {
  4. static int i = 5;
  5. return &i; // OK.
  6. }
  7. char* fun6() {
  8. static char s[] = "Hello";
  9. return s; // OK.
  10. }
  11. int main() {
  12. int* r5 = fun5();
  13. cout << *r5 << endl; // 5
  14. char* r6 = fun6();
  15. cout << r6 << endl; // Hello
  16. return 0;
  17. }

數組是不能作爲函數的返回值的。因爲編譯器會把數組名認爲是局部變量(數組)的地址。返回一個數組,實際上是返回指向這個數組首地址的指針。函數結束後,數組作爲局部變量被釋放,這個指針則變成了野指針。同1的fun2()及2的fun4()(字符數組)

但是聲明數組是靜態的,然後返回是可以的,同3。

  1. #include <iostream>
  2. using namespace std;
  3. int* fun7() {
  4. int a[3] = {1, 2, 3};
  5. return a; // Wrong!
  6. }
  7. int* fun8() {
  8. static int a[3] = {1, 2, 3};
  9. return a; // OK.
  10. }
  11. int main() {
  12. int* r7 = fun7();
  13. cout << *r7 << endl; // 內存已經是無效的了。
  14. int* r8 = fun8();
  15. cout << *r8 <<endl; // 1
  16. return 0;
  17. }

函數返回指向存儲在堆上的變量的指針是可以的。但是,程序員要自己負責在函數外釋放(free/delete)分配(malloc/new)在堆上的內存。

  1. #include <iostream>
  2. using namespace std;
  3. char* fun9() {
  4. char* s = (char*) malloc(sizeof(char) * 100);
  5. return s; // OK. 但需要程序員自己釋放內存。
  6. }
  7. int main() {
  8. char* r9 = NULL;
  9. r9 = fun9();
  10. strcpy(r9, "Hello");
  11. cout << r9 << endl; // Hello
  12. free(r9); // 要記得自己釋放內存。
  13. return 0;
  14. }

函數內局部對象的引用是不能作爲函數返回值的。函數內的局部對象在離開函數作用域後會被自動析構(自動調用其析構函數),則它的引用不再指向一個有效對象,結果無法預測。就類似1的fun2()返回局部變量地址後,這個地址指向的內存其實已經不再有效了。

函數內用new創建的堆上的對象的指針是可以作爲函數返回值的,但是類似5要程序員自己負責在函數外釋放對象內存。

classa.h

  1. #ifndef CLASSA_H
  2. #define CLASSA_H
  3. class ClassA
  4. {
  5. public:
  6. int x;
  7. int y;
  8. int z;
  9. public:
  10. ClassA(int xx, int yy, int zz);
  11. ~ClassA();
  12. void printXYZ();
  13. };
  14. #endif // CLASSA_H

classa.cpp

  1. #include "classa.h"
  2. #include <iostream>
  3. using namespace std;
  4. ClassA::ClassA(int xx, int yy, int zz) : x(xx), y(yy), z(zz) {
  5. cout << "Construct a ClassA" << endl;
  6. }
  7. ClassA::~ClassA() {
  8. cout << "Deconstruct a ClassA" << endl;
  9. }
  10. void ClassA::printXYZ() {
  11. cout << x << " " << y << " " << z << endl;
  12. }

main.cpp

  1. #include <iostream>
  2. using namespace std;
  3. ClassA& fun10() {
  4. ClassA ca(1, 2, 3);
  5. return ca; // Wrong!
  6. }
  7. ClassA* fun11() {
  8. ClassA* ca = new ClassA(1, 2. 3);
  9. return ca; // OK. 但需要程序員自己釋放內存。
  10. }
  11. int main() {
  12. ClassA r10 = fun10();
  13. r10.printXYZ(); // 對象在函數返回時已經被析構了,對應的內存已經無效。
  14. ClassA* r11 = fun11();
  15. r11->printXYZ(); // 1 2 3
  16. delete r11; // 要記得自己釋放內存。
  17. return 0;
  18. }

總結上面的幾個例子,我們只要搞清楚函數返回的東西是什麼、其對應的內存存在哪一般就可以搞清楚它能不能被返回,其實有的例子印證的結果是重疊的。

一個完整的程序,內存分佈情況:program-memory-distribution

上面的幾個例子中,我們返回的東西包括:

  • 棧上變量值(基本類型)可以作爲函數返回值;因爲只涉及到傳值,不會有什麼問題;
  • 棧上變量地址(基本類型指針;數組;對象引用)不能在作爲函數返回值;因爲它們的作用域只在函數內,函數返回後它們的內存都不再有效;
  • 堆上變量地址(malloc或new得到的指針)可以作爲函數返回值;變量存在堆上,其內存從分配後一直有效直到程序員自己釋放(free或delete);
  • 靜態變量地址(static)可以作爲函數返回值;因爲它們存儲在靜態變量區,其內存整個程序運行期間一直有效;
  • 只讀數據段(.rodata)地址(字符串常量)可以作爲函數返回值;因爲它們存儲在只讀數據段,其內存整個程序運行期間一直有效;

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