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的。
预览时标签不可点