一 函數不可返回指向棧內存的指針
預備知識:內存的分類
C/C++程序佔用的內存分爲兩大類:靜態存儲區與動態存儲區。其示意圖如下所示:
數據保存在靜態存儲區與動態存儲區的區別就是:靜態存儲區在編譯-鏈接階段已經確定了,程序運行過程中不會變化,只有當程序退出的時候,靜態存儲區的內存纔會被系統回收。動態存儲區是在程序運行過程中動態分配的。
在其它地方我們還可以看到內存分配還有其他分類,那些都是細分的分類,比如文字常量區、全局數據區等,都歸爲靜態存儲區這一個大類。
例子:return返回指向棧內存指針
先看一個return返回指向棧內存指針的例子:
#include <stdio.h>
char *GetStr(void)
{
char ptr[] = "Hello"; /* 保存在棧中 */
return ptr;
}
int main(void)
{
char *str = NULL;
str = GetStr();
printf("%s\n", str);
return 0;
}
程序編譯、運行的結果如下:
可以看到,編譯出現警告:
warning: function returns address of local variable
運行結果並不是我們期望的輸出字符串Hello。
那是因爲GetStr函數返回指向棧內存的指針,這裏的變量ptr是局部變量,而局部變量是分配在棧上的。即Hello保存在棧內存上,棧內存在函數調用結束時會自動銷燬,因此此時的ptr裏的內容是未知的,所以結果無輸出。
下面我們把GetStr函數修改爲:
char *GetStr(void)
{
char *ptr = "Hello"; /* ptr在棧上,Hello在靜態區(常量區) */
return ptr;
}
此時編譯運行的結果是怎樣的呢?結果爲:
可以看到能正常輸出。爲什麼這裏又可以正常輸出呢?因爲這裏的ptr雖然分配在棧上,但是此時的Hello是一個字符串常量,其存儲在靜態存儲區。在調用GetStr函數結束時其也不會被銷燬。
這裏可能有些人會有疑惑,同樣是Hello,爲什麼一個在棧上,一個在靜態區。
char *ptr = “Hello”;
此處首先定義了一個指針變量ptr,編譯器就會爲指針變量開闢了棧空間。而此時並沒有空間來存放Hello,所以Hello只能存儲在靜態區。
char ptr[] = “Hello”;
此處首先定義一個數組ptr,因爲未給出數組大小,所以此時數組大小未確定。然後把Hello保存在這個數組裏,編譯器就會爲數組ptr開閉足夠的棧空間來存儲Hello。
其它替代方法
從上面的例子我們知道,若函數返回指向棧內存的指針,所得到的結果並不是我們想要的。除了上面的方法之外,這裏還有如下幾種解決方法:
1、把ptr定義爲全局變量,因爲全局變量存儲在靜態存儲區,程序結束纔會釋放。但是這樣會導致函數是不可重入的。關於函數的重入與不可重入可查看往期筆記:什麼是可重入函數?
2、在GetStr函數中使用malloc申請動態內存,但使用完一定要記得使用free進行釋放,否則會導致內存泄漏。示例代碼如下:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
char GetStr(void)
{
char ptr = (char)malloc(64sizeof(char));
strcpy(ptr, “Hello”);
return ptr;
}
int main(void)
{
char str = NULL;
str = GetStr();
printf("%s\n", str);
free(str); / 釋放str指向的堆內存 */
return 0;
}
3、可以將變量ptr聲明爲static靜態變量。但這也會導致函數是不可重入的。示例代碼如下:
char *GetStr(void)
{
static char ptr[] = “Hello”;
return ptr;
}
二 指針與數組區別
C語言中沒有特定的字符串類型,常用以下兩種方式定義字符串:一種是字符數組,另一種是指向字符串的指針。如下:
(1)char str[] = "happy";
(2)char *str = "happy";
這種方式有什麼不同呢?
下面看兩個例子:修改字符串中的字符
示例1:
#include <stdio.h>
int main(void)
{
char str[20] = "hello";
str[0] = 'H';
printf("%s\n",str);
return 0;
}
運行結果:
Hello
示例2:
#include <stdio.h>
int main(void)
{
char *str = "hello";
str[0] = 'H';
printf("%s\n",str);
return 0;
}
運行結果:
無打印信息輸出
可見,使用(1)方式定義的字符串其字符是可以修改的,使用(2)方式定義的字符串其字符是不可以修改的。(2)中可以成功編譯和鏈接,但運行時可能會出現錯誤,我編譯與運行的平臺是window10平臺,運行結果是無打印信息輸出,在其他不同的平臺運行可能會出現段錯誤(Segment Fault)或者寫入位置錯誤。
這兩種表示字符串的方式的主要區別是:字符串指針指向的內容是不可修改的,字符數組是可以修改的,即(2)方式定義的字符串保存在常量區,是不可更改的,(1)方式定義的字符串保存在全局數據區或棧區,是可修改的。
內存的分配方式:
內存分配可分爲三種:靜態存儲區、棧區、堆區。
1、靜態存儲區:該內存在程序編譯的時候就已經分配好,這塊內存在程序的整個運行期間都存在,它主要存放靜態數據、全局數據和常量。
2、棧區:它的用途是完成函數的調用。在執行函數時,函數內局部變量及函數參數的存儲單元在棧上創建,函數調用結束時這些存儲單元自動被釋放。
3、堆區:程序在運行時使用庫函數爲變量申請內存,在變量使用結束後再調用庫函數釋放內存。動態內存的生存期是由我們決定的,如果我們不釋放內存,就會導致內存泄漏。