在C/C++中有個叫指針的玩意存在感極其強烈,而說到指針又不得不提到記憶體管理。現在時不時能聽到一些朋友說指針很難,實際上說的是記憶體操作和管理方面的難。(這篇筆記咱也會結合自己的理解簡述一些相關的記憶體知識) 最近在寫C程式使用指針的時候遇到了幾個讓我印象深刻的地方,這裡記錄一下,以便今後回顧。 “經一 ...
在C/C++中有個叫指針的玩意存在感極其強烈,而說到指針又不得不提到記憶體管理。現在時不時能聽到一些朋友說指針很難,實際上說的是記憶體操作和管理方面的難。(這篇筆記咱也會結合自己的理解簡述一些相關的記憶體知識)
最近在寫C程式使用指針的時候遇到了幾個讓我印象深刻的地方,這裡記錄一下,以便今後回顧。
“經一蹶者長一智,今日之失,未必不為後日之得。” - 王陽明《與薛尚謙書》
指針和二級指針
簡述下指針的概念。
指針
一個指針可以理解為一條記憶體地址。
這裡先定義了一個整型變數
test
,接著用取址運算符&
取得這個變數的記憶體地址並列印出來。
可以看到該變數的記憶體地址是000000000061FE1C
指針變數
指針變數就是存放指針(也就是存放記憶體地址)的變數,使用數據類型* 變數名
進行定義。
值得註意的是指針變數內儲存的指針(記憶體地址)所代表的變數的數據類型,比如int*
定義的指針變數就只能指向int
類型的變數。
int test = 233;
int* ptr = &test;
test
變數的類型是整型int
,所以test
存放的就是一個整形數據。
而ptr
變數的類型是整型指針類型int*
,存放則的是整性變數test
的指針(記憶體地址)。
二級指針
二級指針指的是一級指針變數的地址。
int main() {
int test = 233;
printf("%p\n", &test);
int *ptr = &test;
printf("%p", &ptr);
return 0;
}
/* stdout
000000000061FE1C
000000000061FE10
*/
這個例子中二級指針就是
ptr
變數的地址000000000061FE10
。
二級指針變數
二級指針變數就是存放二級指針(二級指針的地址)的變數,使用數據類型** 變數名
進行定義。
int main() {
int test = 233;
int *ptr = &test;
int **ptr2 = &ptr;
return 0;
}
ptr
變數的類型是整型指針類型int*
,存放的是整性(int
)變數test
的指針(記憶體地址),
ptr2
變數的類型是二級整型指針類型int**
,存放的是整性指針(int*
)變數ptr
的記憶體地址。
多級指針變數
雖然二級以上的指針變數相對來說不太常用,但我覺得基本的辨別方法還是得會的:
通過觀察發現,指針變數的數據類型定義其實就是在其所指向的數據類型名後加一個星號,
比如說:
-
指針
ptr
指向整型變數int test
,那麼它的定義寫法就是int* ptr
。(數據類型在int
後加了一個星號) -
指針
ptr2
指向一級指針變數int* ptr
,那麼它的定義寫法就是int** ptr2
。(數據類型在int*
後加了一個星號)
再三級指針變數int*** ptr3
,乍一看星號這麼多,實際上“剝”一層下來就真相大白了:
(int**)*
實際上三級指針變數指向的就是二級指針變數的地址。
其他更多級的指針變數可以依此類推。
棧記憶體和堆記憶體
指針和記憶體操作關係緊密,提到指針總是令人情不自禁地想起記憶體。
程式運行時占用的記憶體空間會被劃分為幾個區域,其中和這篇筆記息息相關的便是棧區(Stack)和堆區(Heap)。
棧區 (Stack)
棧區的操作方式正如數據結構中的棧,是LIFO後進先出的。這種操作模式的一個很經典的應用就是遞歸函數了。
每個函數被調用時需要從棧區劃分出一塊棧記憶體用來存放調用相關的信息,這塊棧記憶體被稱為函數的棧幀。
棧幀存放的內容主要是(按入棧次序由先至後):
-
返回地址,也就是函數被調用處的下一條指令的記憶體地址(記憶體中專門有代碼區用於存放),用於函數調用結束返回時能接著原來的位置執行下去。
-
函數調用時的參數值。
-
函數調用過程中定義的局部變數的值。
-
and so on...
由LIFO後進先出可知一次函數調用完畢後相較而言局部變數先出棧,接著是參數值,最後棧頂指針指向返回地址,函數返回,接著下一條指令執行下去。
棧區的特性:
-
交由系統(C語言這兒就是編譯器參與實現)自動分配和釋放,這點在函數調用中體現的很明顯。
-
分配速度較快,但並不受程式員控制。
-
相對來說空間較小,如果申請的空間大於棧剩餘的記憶體空間,會引發棧溢出問題。(棧記憶體大小限制因操作系統而異)
比如遞歸函數控制不當就會導致棧溢出問題,因為每層函數調用都會形成新的棧幀“壓到”棧上,如果遞歸函數層數過高,棧幀遲遲得不到“彈出”,就很容易擠爆棧記憶體。
-
棧記憶體占用大小隨著函數調用層級升高而增大,隨著函數調用結束逐層返回而減小;也隨著局部變數的定義而增大,隨著局部變數的銷毀而減小。
棧記憶體中儲存的數據的生命周期很清晰明確。
-
棧區是一片連續的記憶體區域。
堆區 (Heap)
堆記憶體就真的是“一堆”記憶體,值得一提的是,這裡的堆和數據結構中的堆沒有關係。
相對棧區來說,堆區可以說是一個更加靈活的大記憶體區,支持按需進行動態分配。
堆區的特性:
-
交由程式員或者垃圾回收機制進行管理,如果不加以回收,在整個程式沒有運行完前,分配的堆記憶體會一直存在。(這也是容易造成記憶體泄漏的地方)
在C/C++中,堆記憶體需要程式員手動申請分配和回收。
-
分配速度較慢,系統需要依照演算法搜索(鏈表)足夠的記憶體區域以分配。
-
堆區空間比較大,只要還有可用的物理記憶體就可以持續申請。
-
堆區是不連續(離散)的記憶體區域。(大概是依賴鏈表來進行分配操作的)
-
現代操作系統中,在程式運行完後會回收掉所有的堆記憶體。
要養成不用就釋放的習慣,不然運行過程中進程占用記憶體可能越來越大。
簡述C中堆記憶體的分配與釋放
分配
這裡咱就直接報菜名吧!
這一部分的函數的原型都定義在頭文件stdlib.h
中。
-
void* malloc(size_t size)
用於請求系統從堆區中分配一段連續的記憶體塊。
-
void* calloc(size_t n, size_t size);
在和
malloc
一樣申請到連續的記憶體塊後,將所有分配的記憶體全部初始化為0。 -
void* realloc(void* block, size_t size)
修改已經分配的記憶體塊的大小(具體實現是重新分配),可以放大也可以縮小。
malloc
可以記成Memory Allocate 分配記憶體
;
calloc
可以記成Clear and Allocate 分配並設置記憶體為0
;
realloc
可以記成Re-Allocate 重分配記憶體
。
簡單來說原理大概是這樣:
-
malloc
記憶體分配依賴的數據結構是鏈表。簡單說來就是所有空閑的記憶體塊會被組織成一個空閑記憶體塊鏈表。 -
當要使用
malloc
分配記憶體時,它首先會依據演算法掃描這個鏈表,直到找到一個大小滿足需求的空閑記憶體塊為止,然後將這個空閑記憶體塊傳遞給用戶(通過指針)。
(如果這塊的大小大於用戶所請求的記憶體大小,則將多餘部分“切出來”接回鏈表中)。 -
在不斷的分配與釋放過程中,由於記憶體塊的“切割”,大塊的記憶體可能逐漸被切成許多小塊記憶體存在鏈表中,這些便是記憶體碎片。當
malloc
找不到合適大小的記憶體塊時便會嘗試合併這些記憶體碎片以獲得大塊空閑的記憶體。 -
實在找不到空閑記憶體塊的情況下,
malloc
會返回NULL
指針。
釋放
釋放手動分配的堆記憶體需要用到free
函數:
void free(void* block)
只需要傳入指向分配記憶體始址的指針變數作為實參傳入即可。
在
C/C++
中,對於手動申請分配的堆記憶體在使用完後一定要及時釋放,
不然在運行過程中進程占用記憶體可能會越來越大,也就是所謂的記憶體泄漏。
不過在現代操作系統中,程式運行完畢後OS會自動回收對應進程的記憶體,包括泄露的記憶體。記憶體泄露指的是在程式運行過程中無法操作的記憶體。
free
為什麼知道申請的記憶體塊大小?
簡單來說,就是在malloc
進行記憶體分配時會把記憶體大小分配地略大一點,多餘的記憶體部分用於儲存一些頭部數據(這塊記憶體塊的信息),這塊頭部數據內就包括分配的記憶體的長度。
但是在返回指針的時候,malloc
會將其往後移動,使得指針代表的是用戶請求的記憶體塊的起始地址。
頭部數據占用的大小通常是固定的(網上查了一下有一種說法是16
位元組,也有說是sizeof(size_t)
的),在將指針傳入free
後,free
會將指針向前移動指定長度以獲得頭部數據,讀取到分配的記憶體長度,然後連同頭部數據和所分配長度的記憶體一併釋放掉。
記憶體釋放可以理解為這塊記憶體被重新接到了空閑鏈表上,以備後面的分配。
(實際上記憶體釋放後的情況其實挺複雜的,得要看具體的演算法實現和運行環境)
二維數組
定義和初始化
C語言中二維數組的定義:
數據類型 數組名[行數][列數];
初始化則可以使用大括弧:
int a[3][4]={
{1,2,3,4},
{5,6,7,8},
{9,10,11,12}
};
int b[3][4]={ // 內層不要大括弧也是可以的,具體為什麼後面再說
1,2,3,4,
5,6,7,8,
9,10,11,12
};
char str[2][6]={
"Hello",
"World"
};
此外,在有初始化值的情況下,定義二維數組時的一維長度(行數)是可以省略的:
int a[][4]={ // 如果沒有初始化,則一維長度不可省略
1,2,3,4,
5,6,7,8,
9,10,11,12
}
在記憶體中
按上述語句定義的數組,在進程記憶體中一般儲存於:
-
棧區 - 在函數內部定義的局部數組變數。
-
靜態儲存區 - 當用
static
修飾數組變數或者在全局作用域中定義數組。
數組在記憶體中是連續且呈線性儲存的,二維數組也是不例外的。
雖然在使用過程中二維數組發揮的是“二維”的功能,但其在記憶體中是被映射為一維線性結構進行儲存的。
實踐驗證一下:
int i, j;
int a[][4] = { // 如果沒有初始化,則一維長度不可省略
1, 2, 3, 4,
5, 6, 7, 8,
9, 10, 11, 12
};
size_t len1 = sizeof(a) / sizeof(a[0]);
size_t len2 = sizeof(a[0]) / sizeof(a[0][0]);
for (i = 0; i < len1; i++) {
for (j = 0; j < len2; j++)
printf(" [%d]%p ", a[i][j], &a[i][j]);
printf("\n");
}
輸出:
第一維有3行,第二維有4列。
一個int
類型數據占用4
個位元組,從上面的圖可以看出來:
-
[1]000000000061FDD0
->[2]000000000061FDD4
相隔4位元組,說明這兩個數組元素相鄰,同一行中數組元素儲存連續。 -
[4]000000000061FDDC
->[5]000000000061FDE0
同樣相隔4位元組,這兩個數組元素在記憶體中也是相鄰的。 -
從
[1]000000000061FDD0
到[12]000000000061FDFC
正好相差44
個位元組,整個二維數組元素在記憶體中是連續儲存的。
這樣一看,為什麼定義並初始化的時候二維數組的第一維可以省略已經不言而喻了:
在初始化的時候編譯器通過數組第二維的大小對元素進行“分組”,每一組可以看作是一個一維數組,這些一維數組在記憶體中從低地址到高地址連續排列儲存形成二維數組:
在上面例子中大括弧中的元素
{1,2,3,4,5,6,7,8,9,10,11,12}
被按第二維長度4
劃分成了{1,2,3,4}
,{5,6,7,8}
,{9,10,11,12}
三組,這樣程式也能知道第一維數組長度為3
了。
二維數組名代表的地址
一維數組名代表的是數組的起始地址(也是第一個元素的地址)。
二維數組在記憶體中也是映射為一維進行連續儲存的,
既然如此,二維數組名代表的地址其實也是整個二維數組的起始地址,在上面的例子中相當於a[0][0]
的地址。
在上面的示例最後加一行:
printf("Arr address: %p", a);
列印出來的地址和a[0][0]
的地址完全一致,是000000000061FDD0
。
二維數組和二級指針
二維數組不等於二級指針
首先要明確一點:二維數組 ≠ 二級指針
剛接觸C語言時我總是想當然地把這兩個搞混了,實際上根本不是一回事兒。
-
二級指針變數儲存的是一級指針變數的地址。
-
二維數組是記憶體中連續儲存的一組數據,二維數組名相當於一個一級指針(二維數組的起始地址)。
int arr[][4]={
{1,2},{1},{3},{4,5}
};
int** ptr=arr; // 這樣寫肯定是不行的!,ptr儲存的是一級指針變數的地址
int* ptr=arr; // 這樣寫是可以的,但是不建議
int* ptr=&arr[0][0]; // 這樣非常ok, ptr儲存的是數組起始地址(也就是首個變數的地址)
可以把之前二維數組的例子改一下:
int i;
int a[][4] = { // 如果沒有初始化,則一維長度不可省略
1, 2, 3, 4,
5, 6, 7, 8,
9, 10, 11, 12
};
size_t len1 = sizeof(a) / sizeof(a[0]);
size_t len2 = sizeof(a[0]) / sizeof(a[0][0]);
size_t totalLen = len1 * len2; // 整個二維數組的長度
int *ptr = &a[0][0]; // ptr指向二維數組首地址
for (i = 0; i < totalLen; i++) {
// 一維指針操作就是基於一維的,所以整個二維數組此時會被當作一條連續的記憶體
printf(" [%d]%p ", ptr[i], &ptr[i]);
// printf(" [%d]%p ", *(ptr + i), ptr + i);
if (i % len2 == 3) // 換行
printf("\n");
}
printf("Arr address: %p", ptr);
輸出結果和之前遍歷二維數組的是一模一樣的。
指針數組
實現“二維數組”
既然二級指針變數不能直接指向二維數組,那能不能依賴二級指針來實現一個類似的結構呢?當然是可以的啦!
整型變數存放著整型int
數據,整型數組int a[]
中存放了整型數據;
如果是用申請堆記憶體來實現的整型數組:
int* arr = (int*)malloc(sizeof(int) * 3);
指針int*
變數arr
此時指向的是連續存放整型(int
)數據的記憶體的起始地址,相當於一個一維數組的起始地址。
代碼實現
二級指針int**
變數存放著一級指針變數的地址,那麼就可以構建二級指針數組來存放二級指針數據(也就是每個元素都是一級指針變數的地址)。
具體代碼實現:
int rows = 3; // 行數/一維長度
int cols = 4; // 列數/二維長度
int **ptr = (int **) malloc(rows * sizeof(int *));
// 分配一段連續的記憶體,儲存int*類型的數據
int i, j, num = 1;
for (i = 0; i < rows; i++) {
ptr[i] = (int *) malloc(cols * sizeof(int));
// 再分配一段連續的記憶體,儲存int類型的數據
for (j = 0; j < cols; j++)
ptr[i][j] = num++; // 儲存一個整型數據1-12
}
其中
ptr[i] = (int *) malloc(cols * sizeof(int));
這一行,等同於
*(ptr+i) = ...
也就是利用間接訪問符*
讓一級指針變數指向在堆記憶體中分配的一段連續整形數據,這裡相當於初始化了第二維。
而在給整型元素賦值時和二維數組一樣用了中括弧進行訪問:
ptr[i][j] = i * j;
其實就等同於:
*(*(ptr+i)+j) = i * j;
-
第一次訪問第一維元素,用第一維起始地址
ptr
加上第一維下標i
,取出對應的一級指針變數中存放的地址:*(ptr+i)
這個地址是第二維中一段連續記憶體的起始地址。 -
第二次訪問第二維元素,用1中取到的地址
*(ptr+i)
加上第二維下標j
,再用間接訪問符*
訪問對應的元素,並賦值。
在記憶體中的存放
指針數組在記憶體中的存放不同於普通定義的二維數組,它的每一個維度是連續儲存的,但是維度和維度之間在記憶體中的存放是離散的。
用一個迴圈列印一下每個元素的地址:
for (i = 0; i < rows; i++) {
for (j = 0; j < cols; j++)
printf(" [%d]%p ", ptr[i][j], *(ptr + i) + j);
printf("\n");
}
輸出:
可以看到第二維度的地址是連續的,但是第二維度“數組”之間並不是連續的。比如元素4
和元素5
的地址相差了20
個位元組,並不是四個位元組。
其在記憶體中的存放結構大致如上,並無法保證*(ptr+0)+3
和*(ptr+1)
的地址相鄰,也無法保證*(ptr+1)+3
和*(ptr+2)
的地址相鄰。
這種非連續的存放方式可以說是和二維數組相比很大的一個不同點了。
釋放對應的堆記憶體
通常指針數組實現的“二維數組”是在堆記憶體中進行存放的,既然申請了堆記憶體,咱也應該養成好習慣,使用完畢後將其釋放掉:
for (i = 0; i < rows; i++)
free(ptr[i]);
free(ptr);
先利用一個迴圈釋放掉每一個一級指針變數指向的連續記憶體塊(儲存整型數據),最後再把二級指針變數指向的連續記憶體塊(儲存的是一級指針變數的地址)釋放掉。
sizeof的事兒
sizeof()
是C語言中非常常用的一個運算符,而二級指針和二維數組的區別在這裡也可以很好地展現出來。
對於直接定義的數組
對於非變數長度定義的數組,sizeof
在編譯階段就會完成求值運算,被替換為對應數據的大小的常量值。
int arr[n];
這種定義時數組長度為變數的即為變數長度數組(C99標準開始支持),不過還是不太推薦這種寫法。
直接固定長度定義二維數組時,編譯器是知道這個變數是數組的,比如:
int arr[3][4];
size_t arrSize = sizeof(arr);
在編譯階段,編譯器知道數組arr
是一個整型int
二維數組:
-
每個第二維數組包含四個
int
數據,長度為sizeof(int)*4=16
個位元組。 -
第一維數組包含三個第二維數組,每個第二維數組長度為
16
位元組,整個二維數組總長度為16*3=48
個位元組。
即sizeof(arr) = 48
。
對於指針數組
指針變數儲存的是指針,也就是一個地址。記憶體地址在運算的時候會存放在CPU的整數寄存器中。
64位電腦中整數寄存器寬度有64
bit(位),而指針數據要能存放在這裡。
目前來說 1
位元組(Byte) = 8
位(bit),那麼64
位就是8
個位元組,
所以64位系統中指針變數的長度是8
位元組。
int rows = 3; // 行數/一維長度
int **ptr = (int **) malloc(rows * sizeof(int *));
size_t ptrSize = sizeof(ptr); // 8 Bytes
size_t ptrSize2 = sizeof(int **); // 8 Bytes
size_t ptrSize3 = sizeof(int *); // 8 Bytes
size_t ptrSize4 = sizeof(char *); // 8 Bytes
雖然上面咱通過申請分配堆記憶體實現了二維數組(用二級指針變數ptr
指向了指針數組起址),
但其實在編譯器眼中,ptr
就單純是一個二級指針變數,占用位元組數為8 Bytes
(64位),儲存著一個地址,因此在這裡是無法通過sizeof獲得這塊連續記憶體的長度的。
通過上面的例子很容易能觀察出來:
sizeof(指針變數) = 8 Bytes
(64位電腦)
無論指針變數指向的是什麼數據的地址,它儲存的單純只是一個記憶體地址,所以所有指針變數的占用位元組數是一樣的。
函數傳參與返回
得先明確一點:C語言中不存在所謂的數組參數,通常讓函數接受一個數組的數據需要通過指針變數參數傳遞。
傳參時數組發生退化
int test(int newArr[2]) {
printf(" %d ", sizeof(newArr)); // 8
return 0;
}
int main() {
int arr[5] = {1, 2, 3, 4, 5};
test(arr);
return 0;
}
在上面這個例子中test
函數的定義中聲明瞭“看上去像數組的”形參newArr
,然而sizeof
的運算結果是8
。
實際上這裡的形參聲明是等同於int* newArr
的,因為把數組作為參數進行傳遞的時候,實際上傳遞的是數組的首地址(因為數組名就代表數組的首地址)。
這種情況下就發生了數組到指針的退化。
在編譯器的眼中,newArr
此時就被當作了一個指針變數,指向arr
數組的首地址,因此聲明中數組的長度怎麼寫都行:int newArr[5]
,int newArr[]
都可以。
為了讓代碼更加清晰,我覺得最好還是聲明為int* newArr
,這樣一目瞭然能知道這是一個指針變數!
函數內運算涉及到數組長度時
當函數內運算涉及到數組長度時,就需要在函數定義的時候另聲明一個形參來接受數組長度:
int test(int *arr, size_t rowLen, size_t colLen) {
int i;
size_t totalLen = rowLen * colLen;
for (i = 0; i < totalLen; i++) {
printf(" %d ", arr[i]);
if (i % colLen == colLen - 1) // 每個第二維數組元素列印完後換行
printf("\n");
}
return 0;
}
int main() {
int arr[3][3] = {
1, 2, 3,
4, 5, 6,
7, 8, 9
};
test(arr, sizeof(arr) / sizeof(arr[0]), sizeof(arr[0]) / sizeof(arr[0][0]));
return 0;
}
輸出:
這個例子中test
函數就多接受了二維數組的一維長度rowLen
和二維長度colLen
,以對二維數組元素進行遍歷列印。
返回“數組”
經常有應用場景需要函數返回一個“數組”,說是數組,實際上函數並無法返回一個局部定義的數組,哪怕是其指針(在下麵一節有寫為什麼)。
取而代之地,常常會返回一個指針指向分配好的一塊連續的堆記憶體。
(在演算法題中就經常能遇到要求返回指針的情況)
int *test(size_t len) {
int i;
int *arr = (int *) malloc(len * sizeof(int));
for (i = 0; i < len; i++)
arr[i] = i + 1;
return arr;
}
int main() {
int i = 0;
int *allocated = test(5);
for (; i < 5; i++)
printf(" %d ", allocated[i]);
free(allocated); // 一定要記得釋放!
return 0;
}
這個示例中,test
函數的返回類型是整型指針。當調用了test
函數,傳入要分配的連續記憶體長度後,其在函數內部定義了一個局部指針變數,指向分配好的記憶體,在記憶體中存放數據後將該指針返回。
在主函數中,test
返回的整型指針被賦給了指針變數allocated
,所以接下來可以通過一個迴圈列印出這塊連續記憶體中的數據。
再次提醒,申請堆記憶體並使用完後,一定要記得使用free
進行釋放!
生疏易犯-函數返回局部變數
錯誤示例
記得初學C語言的時候,我曾經犯過一個錯誤:將函數內定義的數組的數組名作為返回值:
int *test() {
int arr[4] = {1, 2, 3, 4};
return arr;
}
int main() {
int i = 0;
int *allocated = test();
for (; i < 4; i++)
printf(" %d ", *(allocated + i));
return 0;
}
這個例子中直到for迴圈前進程仍然正常運行,但是一旦嘗試使用*
運算符取出記憶體中的數據*(allocated + i)
,進程立馬接收到了系統發來的異常信號SIGSEGV
,進而終止執行。
原因簡述
SIGSEGV
是比較常見的一種異常信號,代表Signal Segmentation Violation
,也就是記憶體分段衝突
造成異常的原因通常是進程 試圖訪問一段沒有分配給它的記憶體,“野指針”總是伴隨著這個異常出現。
上面簡述棧區的時候提到了棧幀,每次調用函數時會在棧上給函數分配一個棧幀用來儲存函數調用相關信息。
函數調用完成後,先把運算出來的返回值存入寄存器中,接著會在棧幀上進行彈棧操作,在這個過程中分配的局部變數就會被回收。
最後,程式在棧頂中取到函數的返回地址,返回上層函數繼續執行餘下的指令。棧幀銷毀,此時局部變數相關的棧記憶體已經被回收了。
然而此時寄存器中仍存著函數的返回值,是一個記憶體地址,但是記憶體地址代表的記憶體部分已經被回收了。
當將返回值賦給一個指針變數時,野指針就產生了——此時這個指針變數指向一片未知的記憶體。
所以當進程試圖訪問這一片不確定的記憶體時,就容易引用到無效的記憶體,此時系統就會發送SIGSEGV
信號讓進程終止執行。
教訓
教訓總結成一句話就是:
- 程式中請不要讓函數返回代表棧記憶體的局部變數的地址。
延伸:返回靜態局部變數是可以的,因為靜態局部變數是儲存在靜態儲存區的。
int *test() {
static int arr[4] = {1, 2, 3, 4};
return arr;
}