第一讲:C语言基础回顾——指针的本质、指针与数组、指针与函数、动态内存管理

各位同学,欢迎来到《C语言数据结构与算法50讲》。我是你们的老朋友,一个写了十几年C语言的工程师。

今天咱们聊点实在的——指针。说实话,指针是C语言的灵魂,也是很多人的噩梦。我见过太多人栽在指针上,包括我自己刚入行那会儿。但别怕,今天我把这些坑一个个给你指出来。

一、指针的本质:它就是个地址

指针是什么?说白了,指针就是一个变量,里面存的是另一个变量的内存地址。你想想看,我们平时用变量名访问数据,其实背后就是通过地址去找。指针就是把这个地址显式地拿出来了。

核心理解:指针变量本身也有地址,它存的是别人的地址。别搞混了。

int a = 10;
int *p = &a;   // p存的是a的地址
printf("%d\n", *p);  // 通过p找到a,输出10

这里有个关键点:*p 是解引用,意思是“去p存的地址那里,把值取出来”。我刚开始学的时候,总把 p*p 搞混。记住:p 是地址,*p 是值。

二、指针与数组:剪不断理还乱

数组名是什么?数组名就是数组首元素的地址。但注意,它是个常量,不能修改。我见过有人写 arr++,编译直接报错——数组名不是左值。

int arr[5] = {1, 2, 3, 4, 5};
int *p = arr;   // p指向arr[0]
printf("%d\n", *(p + 2));  // 输出3

这里 p + 2 不是简单的地址加2,而是加 2 * sizeof(int)。指针的加减法,是以它指向的类型大小为步长的。这个特性叫“指针的算术运算”。

我的习惯:遍历数组时,我更喜欢用指针而不是下标。因为指针操作更底层,效率更高。但要注意边界,别越界。

还有一个容易混淆的点:&arrarr 的区别。 arr 是数组首元素地址,类型是 int*&arr 是整个数组的地址,类型是 int (*)[5]。虽然数值相同,但步长不同。你试试 arr + 1&arr + 1,结果差很远。

三、指针与函数:传参的艺术

C语言函数参数传递是值传递。你想在函数里修改外部变量?必须传指针。这个道理很简单,但很多人写代码时就是忘了。

void swap(int *a, int *b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}

int x = 5, y = 10;
swap(&x, &y);  // 传地址,才能改值

函数指针呢?就是指向函数的指针。我曾在项目中用函数指针实现回调机制,比如排序时传入比较函数。这样代码更灵活。

int compare(int a, int b) {
    return a > b ? 1 : 0;
}

void sort(int *arr, int n, int (*cmp)(int, int)) {
    // 排序逻辑,用cmp比较
}

注意:函数指针的类型必须和指向的函数签名完全一致。参数个数、类型、返回值,一个都不能错。我曾经因为参数类型不匹配,调试了一下午。

四、动态内存管理:malloc/calloc/realloc/free

动态内存是C语言的利器,也是万恶之源。用好了,程序高效灵活;用不好,段错误、内存泄漏找上门。

函数 功能 区别
malloc 分配指定字节的内存 不初始化,内容随机
calloc 分配指定数量*大小的内存 初始化为0
realloc 调整已分配内存的大小 可能移动内存块
free 释放动态分配的内存 必须和malloc/calloc/realloc配对
int *p = (int*)malloc(10 * sizeof(int));
if (p == NULL) {
    // 处理内存分配失败
    return -1;
}
// 使用p...
free(p);
p = NULL;  // 避免野指针

这里有个细节:malloc 返回的是 void*,需要强制类型转换。虽然C语言允许隐式转换,但我建议显式写出来,代码更清晰。

避坑指南:我曾经在项目中忘记检查 malloc 返回值,结果在内存紧张时程序直接崩溃。从那以后,我每次分配内存都检查 NULL

realloc 是个好东西,但要注意:它可能返回新的地址。如果你直接用 p = realloc(p, new_size),万一失败,原来的内存就丢了。正确做法是用临时变量接一下。

int *new_p = (int*)realloc(p, 20 * sizeof(int));
if (new_p != NULL) {
    p = new_p;
} else {
    // 处理失败,p仍然有效
}

五、常见内存错误与调试

内存错误是C程序员的日常。我总结了几种最常见的,你看看自己中过几个。

  1. 野指针:指针没有初始化,或者指向了已释放的内存。解决办法:定义时初始化为NULL,释放后也置为NULL。
  2. 内存泄漏:分配了内存没释放。程序跑久了,内存越占越多。用工具如Valgrind可以检测。
  3. 越界访问:访问了数组或内存块之外的位置。比如 arr[10] 但数组只有5个元素。这种错误往往不会立即崩溃,但会在某个时刻突然出问题。
  4. 重复释放:对同一块内存调了两次free。后果很严重,程序直接崩。解决办法:free后立即置NULL。
  5. 使用已释放的内存:free之后还去读写。这叫“悬空指针”。

我曾经:在一个网络服务器项目中,因为内存泄漏导致服务运行三天后崩溃。查了整整两天,最后发现是一个循环里忘了free。从那以后,我写代码时坚持“谁分配谁释放”的原则。

调试内存问题,我推荐几个方法:

  • printf 打印指针地址和值,跟踪变化
  • 用Valgrind检测内存泄漏和越界
  • 用AddressSanitizer(ASan)编译时加 -fsanitize=address
  • 写单元测试,覆盖边界情况

六、知识体系总览

下面这张图,把今天讲的内容串起来了。你可以把它当作一个思维导图,复习时对照着看。

指针的本质 指针与数组 数组名是常量指针 指针算术运算 &arr vs arr 指针与函数 传指针修改外部变量 函数指针 回调机制 动态内存管理 malloc/calloc/realloc/free 检查NULL返回值 realloc安全用法 常见内存错误 野指针 / 内存泄漏 越界 / 重复释放 悬空指针 调试方法 printf跟踪 Valgrind / ASan 单元测试 掌握指针 = 掌握C语言的灵魂

好了,今天的内容就到这里。指针这东西,光看是学不会的,得多写多练。下一章咱们聊结构体,到时候会用到今天讲的动态内存分配,记得复习。


专注资料整理