资料合集下载链接:
https://pan.quark.cn/s/472bbdfcd014
在C语言编程中,指针和内存管理是两大核心,也是许多新手甚至有经验的开发者容易踩坑的地方。一个经典的问题就是:“为什么我的函数返回一个指针,有时候能用,有时候程序就崩溃了?”
答案往往藏在C语言的内存分区模型中。今天,我们就根据一份课堂笔记,深入探讨一个关键的“禁区”:从函数返回局部变量的地址,并搞清楚为什么有些地址可以安全返回,而另一些则会引发灾难。
一、 风险之源:返回栈(Stack)上的地址
首先,我们要理解一个基本概念:局部变量存放在哪里?
当一个函数被调用时,系统会在一个叫做栈(Stack)的内存区域为它分配空间,用于存放它的参数、局部变量等。栈内存的一大特点是自动管理、先进后出。当函数执行结束时,它所占用的栈空间会被系统立即回收。
这意味着,函数内部的局部变量,其生命周期仅限于该函数的执行期间。
那么,如果我们试图返回一个局部变量的地址,会发生什么?
代码案例 1:返回局部变量的地址
#include <stdio.h>
// 这个函数返回一个局部变量的地址
int* get_stack_address() {
int local_var = 123;
printf("[In get_stack_address] 局部变量 local_var 的地址是: %p\n", (void*)&local_var);
return &local_var; // 危险!返回一个即将被销毁的内存地址
}
int main() {
int* p_wild = NULL;
p_wild = get_stack_address();
// 此时,get_stack_address() 函数已经执行完毕,其栈空间已被释放
// p_wild 现在是一个“野指针”,它指向一块不再属于我们的、不确定的内存区域
printf("[In main] 接收到的地址是: %p\n", (void*)p_wild);
// 尝试通过野指针访问数据,这是未定义行为(Undefined Behavior)
printf("[In main] 尝试解引用野指针 *p_wild: %d\n", *p_wild);
return 0;
}
编译与运行结果分析:
当你编译这段代码时,一个现代的编译器(如GCC或Clang)会给你一个非常明确的警告:
warning: function returns address of local variable [-Wreturn-local-addr]
return &local_var;
^~~~~~~~~~
这个警告告诉你,你正在做一件非常危险的事情。
运行程序,你可能会得到类似下面这样的结果(具体数值会变化):
[In get_stack_address] 局部变量 local_var 的地址是: 0x7ffeea28587c
[In main] 接收到的地址是: 0x7ffeea28587c
[In main] 尝试解引用野指针 *p_wild: 123 <-- 咦?为什么值好像是对的?
陷阱就在这里! 为什么有时候能读出正确的值?这是因为函数返回后,虽然那块栈内存被标记为“可回收”,但系统可能还没来得及用新的数据去覆盖它。你只是“幸运地”在它被踩踏之前瞥了一眼。
如果我们在访问它之前再调用一个函数(比如 printf
),情况就可能完全不同了,因为新的函数调用会使用这块栈空间。
修改后的代码案例 1.1:
#include <stdio.h>
int* get_stack_address() {
int local_var = 123;
return &local_var;
}
void another_function() {
int a = 1;
printf("[In another_function] 我只是一个占位函数,目的是使用一些栈空间。\n");
}
int main() {
int* p_wild = get_stack_address();
printf("[In main] 接收到的地址是: %p\n", (void*)p_wild);
// 在访问野指针之前,调用另一个函数,让它覆盖原来的栈空间
another_function();
// 再次尝试访问,这次极有可能得到一个垃圾值或导致程序崩溃
printf("[In main] 再次尝试解引用野指针 *p_wild: %d\n", *p_wild);
return 0;
}
可能的运行结果:
[In main] 接收到的地址是: 0x7ffc...
[In another_function] 我只是一个占位函数,目的是使用一些栈空间。
[In main] 再次尝试解引用野指针 *p_wild: 345723905 (一个完全随机的垃圾值)
结论:返回局部变量的地址会产生野指针。操作野指针是未定义行为,其后果不可预测,从读到垃圾数据到程序直接崩溃都有可能。这是C编程中必须避免的严重错误。
二、 安全地带:返回全局区(Data/BSS)的地址
与生命周期短暂的局部变量不同,还有一类变量的生命周期与整个程序的运行时间相同。它们就是全局变量和静态变量。
它们存储在内存的全局/静态存储区(通常分为 .data
段和 .bss
段)。这块内存从程序开始运行到程序结束时才会被释放。
因此,返回一个指向全局变量的地址是完全安全的。
代码案例 2:返回全局变量的地址
#include <stdio.h>
// 定义一个全局变量,它存储在 .data 段
int g_global_var = 999;
// 这个函数返回一个全局变量的地址
int* get_data_address() {
printf("[In get_data_address] 全局变量 g_global_var 的地址是: %p\n", (void*)&g_global_var);
return &g_global_var;
}
int main() {
int* p_safe = NULL;
p_safe = get_data_address();
printf("[In main] 接收到的地址是: %p\n", (void*)p_safe);
printf("[In main] 第一次访问 *p_safe: %d\n", *p_safe);
// 我们可以安全地修改它
*p_safe = 1000;
printf("[In main] 修改后,再次访问 *p_safe: %d\n", *p_safe);
printf("[In main] 直接访问全局变量 g_global_var: %d\n", g_global_var);
return 0;
}
运行结果:
[In get_data_address] 全局变量 g_global_var 的地址是: 0x100004010
[In main] 接收到的地址是: 0x100004010
[In main] 第一次访问 *p_safe: 999
[In main] 修改后,再次访问 *p_safe: 1000
[In main] 直接访问全局变量 g_global_var: 1000
结论:因为全局变量的内存贯穿程序始终,返回它的地址是安全的,我们可以在程序的任何地方通过这个指针可靠地访问和修改它。
三、 特殊但安全:返回 static
局部变量的地址
static
关键字可以改变局部变量的“命运”。当一个局部变量被声明为 static
时:
- 1. 存储位置:它不再存储在栈上,而是和全局变量一样,存储在全局/静态存储区。
- 2. 生命周期:它的生命周期延长至整个程序的运行时间。
- 3. 作用域:它的作用域仍然仅限于声明它的函数内部。
这意味着,返回一个 static
局部变量的地址也是安全的。
代码案例 3:返回 static
局部变量的地址
#include <stdio.h>
// 这个函数返回一个 static 局部变量的地址
int* get_static_address() {
// static 变量只在第一次调用时初始化
static int s_local_var = 50;
printf("[In get_static_address] s_local_var 的值是: %d, 地址是: %p\n", s_local_var, (void*)&s_local_var);
s_local_var++; // 修改它的值,下次调用会保留
return &s_local_var;
}
int main() {
int* p1 = get_static_address();
printf("[In main] 第一次调用后,*p1 = %d\n\n", *p1);
int* p2 = get_static_address();
printf("[In main] 第二次调用后,*p2 = %d\n\n", *p2);
// p1 和 p2 指向同一个地址
printf("[In main] p1: %p, p2: %p\n", (void*)p1, (void*)p2);
// 通过 p1 修改,会影响 p2
*p1 = 777;
printf("[In main] 修改 *p1 后, *p2 = %d\n", *p2);
return 0;
}
运行结果:
[In get_static_address] s_local_var 的值是: 50, 地址是: 0x102b1c014
[In main] 第一次调用后,*p1 = 51
[In get_static_address] s_local_var 的值是: 51, 地址是: 0x102b1c014
[In main] 第二次调用后,*p2 = 52
[In main] p1: 0x102b1c014, p2: 0x102b1c014
[In main] 修改 *p1 后, *p2 = 777
结论:static
局部变量因其持久的生命周期,其地址可以被安全返回和使用。
总结
今天我们通过代码和分析,揭示了C语言中一个重要的内存规则:指针的有效性取决于其所指向内存的生命周期。
- 1. 禁止返回局部变量地址:因为它们存储在栈上,函数返回即销毁,返回其地址会产生危险的野指针。
- 2. 可以返回全局变量地址:因为它们存储在全局/静态区,生命周期与程序相同,地址始终有效。
- 3. 可以返回
static
局部变量地址:static
关键字将其从栈移至全局/静态区,赋予了它持久的生命,因此地址也是安全的。