指针越界:C语言给你的自由以及偷偷埋的雷

C语言的指针这东西,用起来挺爽。你想指哪就指哪,想怎么移动就怎么移动。可你有没有想过,这种自由背后藏着什么?

我刚开始学C的时候,也觉得指针挺神奇。直到有一天,我写了个数组访问,不小心越界了,程序没报错,但输出了一堆莫名其妙的数字。那会我才明白,C语言不会保护你,它只会默默看着你犯错。

数组和指针的那些事

咱们先看个简单的例子。

#include <stdio.h>  

int main() {  
    int scores[5] = {85, 90, 78, 92, 88};  
      
    // 这两种写法是一回事  
    int *ptr = scores;        // 数组名自动变成指向首元素的指针  
    int *ptr2 = &scores[0];   // 直接取第一个元素的地址  
      
    printf("scores[0] = %d\n", *ptr);     // 85  
    printf("scores[1] = %d\n", *(ptr+1)); // 90  
      
    return 0;  
}

数组名在C语言里会自动变成指向第一个元素的指针。所以 scores 和 &scores[0] 是一回事。这不要紧,理解了这个,指针移动就简单了。

ptr+1 不是地址加1,而是跳过一个整数的大小。如果整数占4字节,那 ptr+1 就是地址加4。

越界访问:沉默的杀手

问题来了。C语言不检查你访问的数组是否越界。

#include <stdio.h>  

int main() {  
    int scores[5] = {85, 90, 78, 92, 88};  
      
    // 越界了,但程序不会报错  
    printf("scores[5] = %d\n", scores[5]);   // 第6个元素?不存在  
    printf("scores[10] = %d\n", scores[10]); // 更远的地方?  
      
    // 更危险的是写入  
    scores[6] = 999; // 写入不存在的元素  
    printf("scores[2] = %d\n", scores[2]);   // 可能被破坏了  
      
    return0;  
}

你运行这段代码,可能会看到几种情况:

输出一些奇怪的数字。越界读到的是内存里其他位置的数据,可能是别的变量,也可能是垃圾值。

程序看起来正常运行。这是最要命的,bug潜伏着,等某个特定条件才爆发。

程序崩溃。运气好的话,访问到了受保护的内存区域,程序直接挂掉。

为什么C语言不检查越界?因为C的设计哲学是信任程序员,追求效率。每次访问都检查边界会拖慢速度,C假设你知道自己在干什么。

可人总会犯错。这些错误在C语言里往往不会被立即发现,等到出问题的时候,你已经找不到是哪里的锅了。

Rust是怎么干的

咱们看看同样一件事,Rust怎么处理。

fn main() {  
    let scores = [85, 90, 78, 92, 88];  
      
    // 用索引访问,越界会直接panic  
    // println!("scores[5] = {}", scores[5]);   
    // 运行时报错,给出清晰的错误信息  
      
    // 用get()安全访问  
    match scores.get(5) {  
        Some(value) => println!("scores[5] = {}", value),  
        None => println!("索引5越界了"),  
    }  
      
    // 用迭代器遍历,编译期保证安全  
    for score in &scores {  
        println!("成绩: {}", score);  
    }  
}

Rust在内存安全上比C严格得多。

用 scores[i] 访问时,Rust会在运行时检查索引。如果越界,程序立即panic,给出清晰的错误信息,而不是读取垃圾数据。

get() 方法返回 Option<&T>,让你优雅地处理越界情况,不用让程序崩溃。

用 for 循环遍历数组时,编译器在编译期就能保证不会越界,既安全又快。

为啥Rust更安全

Rust的设计哲学是把错误消灭在编译期。虽然编译器会变得严格,但换来的是运行时的安全。

C语言像一把锋利的刀,用得好能切出精美的作品,用得不好就伤到自己。Rust像一把带安全护罩的刀,用起来可能稍微麻烦点,但大大降低了受伤的风险。

我这个人写C代码也经常犯错,指针指错地方、数组访问越界,这些事都干过。后来学了Rust,才发现原来编译器可以帮你挡住这么多坑。

给C语言程序员的建议

既然咱们在学C语言,就得知道指针的强大和危险并存。

始终知道数组的大小。遍历数组时,用明确的边界检查。

用工具辅助。Valgrind、AddressSanitizer这些工具能帮你检测内存错误。

养成好习惯。初始化指针、及时置空、避免悬垂指针。

理解内存布局。了解栈、堆、全局区的区别,明白指针指向哪里。

写代码这事,说白了就是跟自己的粗心做斗争。C语言给你自由,但也要求你负责任。Rust帮你管着点,但你也得适应它的规矩。

这东西没有绝对的好坏,看你需要什么。要是追求极致性能,C语言还是王道。要是想要安全省心,Rust值得试试。

我如今在写C代码的时候,会多想一步:这个指针会不会越界?这个地址会不会无效?多想这么一下,能省掉后面好多排查的功夫。

希望能帮到正在学C语言的你。那会我要是有人这么跟我说,大概能少走不少弯路。

那为啥还要学C

有的同学可能会问,既然Rust这么好,为啥不直接学Rust,非得先跟C较这个劲?

说实话,这事我也想过。但现实情况是,你绕不开C。

操作系统是C写的,Linux内核是C写的,Python的解释器是C写的,你用的很多库底层都是C。这些东西就在那,你得跟它们打交道。不懂C,你看这些代码就跟看天书一样。

再说了,C语言能让你明白计算机到底是怎么干的。内存怎么分配,指针怎么指向,栈和堆是怎么回事。这些东西在高级语言里都被藏起来了,用起来方便,可你不知道底下发生了什么。出了bug的时候,你就抓瞎了。

学C不是为了让它折磨咱们,是为了让你理解底层。等你明白了指针、内存、地址这些东西,再去学Rust,你会发现它的所有权、借用检查器,其实就是在解决C语言里那些让你头疼的问题。没有C的铺垫,Rust的那些概念你会觉得莫名其妙。而且考研408也要用C的。

预览时标签不可点

分类: C