(被调)函数内的局部变量在函数返回时被释放,不应被外部引用。虽然并非真正的释放,通过内存地址仍可能访问该栈区变量,但其安全性不被保证。后续若还有其他函数调用,则其局部变量可能覆盖该栈区内容。常见情况有两种:前次调用影响当前调用的局部变量取值(函数的"遗产");被调函数返回指向栈内存的指针,主调函数通过该指针访问被调函数已释放的栈区内容(召唤亡灵)。
1 函数的"遗产"
【示例1】先后连续调用Ancestor和Sibling函数,注意函数内的dwLegacy整型变量。
1 void Ancestor(void){ 2 int dwLegacy = 42; 3 } 4 void Sibling(void){ 5 int dwLegacy; 6 printf("%d\n", dwLegacy); 7 } 8 int main(void){ 9 Ancestor();10 Sibling();11 return 0;12 }
若使用普通编译(如gcc test.c),则输出42,因为编译器重用之前函数的调用栈;若打开优化开关(如gcc -O test.c),则输出一个随机的垃圾数,因为Ancestor函数将被优化为空函数,也不会被main函数调用。
因此,为避免这种干扰,建议声明自动局部变量时对其显式赋初值(初始化)。
【示例2】先后调用Ancestor和Sibling函数,注意函数内的aLegacy数组变量。
1 void Ancestor(void){ 2 int aLegacy[10], dwIdx = 0; 3 for(dwIdx = 0; dwIdx < 10; dwIdx++) 4 aLegacy[dwIdx] = dwIdx; 5 } 6 void Sibling(void){ 7 int aLegacy[10], dwIdx = 0; 8 for(dwIdx = 0; dwIdx < 10; dwIdx++) 9 printf("%d ", aLegacy[dwIdx]);10 }
若使用普通编译,则输出0 1 2 3 4 5 6 7 8 9(Ancestor函数内的数组赋值会影响Sibling函数的数组初值);若打开优化开关,则输出一串随机的垃圾数。
【示例3】连续调用两次Func函数。
1 void Func(void){2 char acArr[25];3 printf("%s ", acArr); //注意此句打印结果4 acArr[0]= 'a'; acArr[1] = 'b'; acArr[2] = 'c'; acArr[3]= '\0';5 printf("%s ", acArr);6 }7 void FuncInsert(void){ char acArr[25] = { 0};}
若使用普通编译,则输出(乱码) abc abc abc;若打开优化开关,则输出(空串) abc abc abc。
若在两次调用中间插入其他函数调用(如FuncInsert),则使用普通编译时输出(乱码) abc (空串) abc;若打开优化开关时仍输出(空串) abc abc abc(FuncInsert函数被优化掉)。
2 召唤亡灵
【示例4】Specter函数返回局部变量dwDead的地址,main函数试图打印该地址内容。
1 int *Specter(void){2 int dwDead = 1;3 return &dwDead; //编译器将提出警告,如function returns address of local variable4 }5 int main(void){ 6 int *pAlive = Specter();7 printf("*pAlive = %d\n", *pAlive);8 return 0;9 }
若使用普通编译,则输出* pAlive = 1;若打开优化开关,则Specter函数跳过赋值语句直接返回dwDead变量地址,故输出*p = (随机的垃圾数)。
注意,Specter函数返回值(地址)存放在%eax寄存器内,main函数读取寄存器值,将其作为内存地址访问该地址处的存储内容——该内容很可能并未初始化,或即将被新的调用栈覆盖!
【示例5】GetString函数返回局部字符数组szStr的地址,main函数试图打印该地址内容。
1 char *GetString(void){ 2 char szStr[] = "Hello World"; //此句后增加printf("%s\n", szStr);可防止赋值被优化掉 3 return szStr; //编译器将提出警告,如function returns address of local variable 4 } 5 int main(void){ 6 char *pszStr = GetString(); //pszStr指向"Hello World"的副本 7 8 //GetString函数返回后,尝试输出GetString函数内局部字符数组szStr的内存内容 9 #ifdef LOOP_COPY10 unsigned char ucIdx = 0;11 char szStackStr[sizeof("Hello World")] = { 0};12 for(ucIdx = 0; ucIdx < sizeof("hello world"); ucIdx++)13 szStackStr[ucIdx] = pszStr[ucIdx]; 14 printf("szStackStr = %s\n", szStackStr); //原szStr处的内容,"Hello World"15 #endif16 #ifdef MEMCOPY_CALL //当内存拷贝函数内部无局部或临时变量时,可用该法17 char szStr[sizeof("Hello World")] = { 0};18 memcpy(szStr, pszStr, sizeof(szStr));19 printf("szStr = %s\n", szStr);20 #endif21 #ifdef CHAR_PRINT22 printf("pszStr = %c%c%c%c%c%c%c%c%c%c%c%c\n", \23 pszStr[0],pszStr[1],pszStr[2],pszStr[3],pszStr[4],pszStr[5], \24 pszStr[6],pszStr[7],pszStr[8],pszStr[9],pszStr[10],pszStr[11]);25 #endif26 #ifdef JUNK_PRINT27 printf("pszStr = %s\n", pszStr); //当前pszStr处的内容,垃圾28 #endif29 return 0;30 }
调用GetString函数时,将只读数据段存放的字符串常量"Hello World"拷贝至堆栈临时分配的字符数组szStr,即szStr指向该字符串的可读写副本。函数返回szStr地址,同时栈顶指针下移以保证堆栈指针平衡。此时若有函数调用或单步跟踪(软中断也使用堆栈),则可能覆盖szStr所指向的内存。为保留和查看栈区szStr处的内容,可采用示例中的LOOP_COPY、MEMCOPY_CALL或CHAR_PRINT方法(为避免相互影响,三者中应任选一个)。
若使用普通编译,则三种方法均可输出"Hello World";若打开优化开关且在GetString函数返回前添加输出szStr内容的语句(以防赋值被跳过),则三种方法仍可输出"Hello World"。这也证明GetString函数调用返回后,堆栈内存szStr处的内容并未清除。
注意,JUNK_PRINT无论何种编译方式均输出乱码。
另见下面的代码片段:
测试1 | 测试2 | 测试3 |
//采用return返回动态内存地址 char* GetMemory1(char *p, int size){ p = (char *)malloc(size);* return p; } void Test1(void){ char *str = NULL; str = GetMemory1(str, 100); strcpy(str, "Hello\n"); printf(str); free(str); } | //采用二级指针返回动态内存地址 void GetMemory2(char **p,int size){ *p = (char *)malloc(size); } void Test2(void){ char *str = NULL; GetMemory2(&str, 100); strcpy(str, "Hello"); printf(str); free(str); if(str != NULL)* strcpy(str,"World\n"); printf("%s", str); } | //正确返回只读字符串地址,但无意义(无法修改内容) char* GetMemory3(void){ char *p = "Hello World";* return p; } void Test3(void){ char *str = NULL; str = GetMemory3(); printf(str); } |
Test1输出Hello 【注*】malloc函数返回void*指针,但C++不允许void*隐式转换到任意类型指针(需要static_cast)。故建议如下兼容写法: T* p = (T*)malloc(size * sizeof(*p));或 T* p = (T*)malloc(size * sizeof(T)); | Test2输出Hello World 【注*】进程中内存管理由库函数完成。当释放内存时,通常不会将内存归还给操作系统,故可继续访问该地址。但因其已被”回收”,若输出语句前再次分配内存,则同段空间可能被重新分配给其他变量,造成错误。 | Test3输出Hello World 【注*】此处若写为char p[] = "Hello World";则返回无效指针,输出不确定。 |