前言欢迎来到第七章如果说 C 语言有什么是 绕不过去、也学不懂 的那一定是指针了。很多人学 C 语言学到指针就卡住了甚至因为学不会指针而放弃了 C 语言。但我要告诉你指针没有那么难只是需要换个角度去理解。本章我们就从最基础的 内存和地址 讲起一步步带你搞懂什么是地址什么是指针指针变量怎么定义、怎么用和*这两个符号到底是什么意思指针怎么和数组配合使用指针作为函数参数有什么用空指针、野指针是什么为什么危险学完本章你会对 C 语言有全新的理解 —— 你不再只是 用 变量而是能 看到 变量在内存中的样子。准备好了吗让我们开始这段 硬核 之旅一、先搞懂内存和地址在讲指针之前我们得先搞明白程序运行时数据存在哪里答案是存在内存里。1.1 内存是什么你可以把内存想象成一栋超级大的酒店酒店里有很多很多房间每个房间大小一样1 字节每个房间都有一个唯一的房间号房间里可以放数据地址 0x100 0x101 0x102 0x103 0x104 ... ┌─────┬─────┬─────┬─────┬─────┐ 数据 │ 10 │ A │ 0 │ ... │ ... │ └─────┴─────┴─────┴─────┴─────┘这里的 房间号 就是内存地址简称地址。1.2 变量和地址的关系当你定义一个变量时int a 10;编译器就会在内存中找一块空闲的地方比如 4 个字节因为 int 占 4 字节把 10 放进去然后把这块空间和名字a关联起来。假设a存在地址0x100处 地址 0x100 0x101 0x102 0x103 ┌─────┬─────┬─────┬─────┐ 数据 │ 10 │ ← 变量a └─────┴─────┴─────┴─────┘平时我们用变量名a来访问这块内存这是 高级玩法—— 编译器帮我们把名字翻译成了地址。但 C 语言允许我们直接操作地址这就是指针的由来。二、什么是指针2.1 指针就是地址指针就是内存地址。就这么简单。变量a的地址就是指向a的指针你知道了一个变量的地址就能通过这个地址找到这个变量读写它的值2.2 指针变量专门用来存放地址的变量就叫指针变量。还是用酒店的比喻普通变量 房间里放着数据指针变量 房间里放着另一个房间的房间号普通变量a 指针变量p ┌──────────┐ ┌──────────┐ │ 10 │ │ 0x100 │ ← 存的是a的地址 └──────────┘ └──────────┘ 0x100 0x200指针变量p里存的是a的地址0x100我们就说p 指向 a。三、指针变量的定义和使用3.1 定义指针变量定义指针变量的语法类型名 *指针变量名;比如int *p; // p是一个指针变量指向int类型 char *cp; // cp是一个指针变量指向char类型 double *dp; // dp是一个指针变量指向double类型这里的*是类型的一部分表示 这是一个指针类型。int*是 指向 int 的指针 类型char*是 指向 char 的指针 类型double*是 指向 double 的指针 类型写法小提示int *p和int* p都可以意思一样。前者强调 p 是指针后者强调类型是 int*。习惯上 C 语言更常用int *p的写法。3.2 取地址运算符怎么获取一个变量的地址用运算符。变量名→ 得到这个变量的地址int a 10; int *p; p a; // 把a的地址赋值给p现在p指向a这样p里面就存了a的地址我们就说 p 指向 a。3.3 * 解引用运算符怎么通过指针访问它指向的变量用*运算符。*指针变量→ 访问指针指向的内容int a 10; int *p a; printf(%d\n, *p); // 输出 10*p就是p指向的变量a *p 20; // 通过指针修改a的值 printf(%d\n, a); // 输出 20a的值被改了看到了吗通过*p我们可以直接读写a的值 —— 因为*p就是a本身。3.4 一个完整的例子#include stdio.h int main() { int a 10; int *p; // 定义一个int指针变量p p a; // p指向a printf(a的值%d\n, a); // 10 printf(a的地址%p\n, a); // 类似 0x7ffd... 每次运行可能不一样 printf(p的值%p\n, p); // 和a一样因为p存的就是a的地址 printf(p指向的值%d\n, *p); // 10*p就是a *p 20; // 通过指针修改a printf(修改后a的值%d\n, a); // 20 return 0; }运行结果地址每次可能不一样a的值10 a的地址0x7ffdabcdef10 p的值0x7ffdabcdef10 p指向的值10 修改后a的值203.5 和 * 的对比这两个符号新手很容易搞混我们来对比一下符号名称作用例子取地址运算符拿到变量的地址a→ 得到 a 的地址*解引用运算符通过地址访问内容*p→ 得到 p 指向的内容可以理解为一对相反的操作从变量 → 地址*从地址 → 变量所以*a就是a本身 —— 先取地址再解引用绕一圈又回来了。四、指针的大小指针变量也是变量也要占内存。那它占几个字节呢答案是取决于平台。32 位系统指针占4 字节32 位地址64 位系统指针占8 字节64 位地址⚠️重要所有类型的指针大小都是一样的int *ip; char *cp; double *dp; // 在64位系统上它们都是8字节为什么因为指针存的是地址地址的长度是固定的 —— 不管你指向的是 int、char 还是 double地址都是一样长的。那指针类型有什么用类型决定了解引用时读几个字节、怎么解释这些字节。int*解引用时读 4 个字节解释成整数char*解引用时读 1 个字节解释成字符double*解引用时读 8 个字节解释成浮点数简单记指针大小都一样4 或 8 字节类型决定了解释方式。五、指针的运算指针不是只能存地址它还能做一些运算。5.1 指针的加减指针可以加上或减去一个整数表示向前或向后移动几个单位。int arr[5] {1, 2, 3, 4, 5}; int *p arr[0]; // p指向第0个元素 printf(%d\n, *p); // 1第0个元素 printf(%d\n, *(p1));// 2第1个元素 printf(%d\n, *(p2));// 3第2个元素⚠️注意指针加 1不是地址加 1 个字节而是加一个元素的大小。int*加 1 → 地址加 4 字节int 占 4 字节char*加 1 → 地址加 1 字节double*加 1 → 地址加 8 字节这就是指针类型的意义 —— 编译器知道一步迈多大。5.2 指针自增自减指针也可以用和--int arr[5] {1, 2, 3, 4, 5}; int *p arr[0]; printf(%d , *p); // 先取*p的值1然后p printf(%d , *p); // 25.3 两个指针相减两个同类型的指针可以相减得到它们之间相差几个元素int arr[5] {1, 2, 3, 4, 5}; int *p1 arr[0]; int *p2 arr[3]; printf(%td\n, p2 - p1); // 3相差3个元素两个同类型指针相减结果 两个指针之间相差的元素个数不是字节差。计算公式地址差字节单个元素字节大小int占 4 字节p1 地址arr[0]p2 地址arr[3]地址字节差 3 × 4 12 字节 元素个数 12 ÷ 4 35.4 指针比较两个指针可以比较大小!判断谁的地址大、谁的地址小或者是否指向同一个地方。int arr[5] {1, 2, 3, 4, 5}; int *p arr[0]; while (p arr[5]) // 还没到末尾就继续 { printf(%d , *p); p; }⚠️注意指针运算不是随便玩的。只有当两个指针指向同一个数组中的元素时相减和比较才有意义。六、指针与数组指针和数组的关系非常密切 —— 密切到什么程度数组名本质上就是一个指针常量指针。6.1 数组名就是首元素地址int arr[5] {1, 2, 3, 4, 5}; // arr 就是 arr[0]也就是第0个元素的地址 printf(%p\n, arr); // 数组名 printf(%p\n, arr[0]); // 第0个元素的地址 // 这两个输出是一样的数组名arr就是数组第 0 个元素的地址类型是int*。6.2 用指针遍历数组既然数组名就是首元素地址那我们可以用指针来遍历数组#include stdio.h int main() { int arr[5] {1, 2, 3, 4, 5}; int *p; // 方式一用指针偏移 for (int i 0; i 5; i) { printf(%d , *(arr i)); // arri就是第i个元素的地址 } printf(\n); // 方式二移动指针 for (p arr; p arr 5; p) { printf(%d , *p); } printf(\n); return 0; }两种方式都能遍历输出都是1 2 3 4 5 1 2 3 4 56.3 下标法和指针法是等价的在 C 语言中arr[i] 等价于 *(arr i)没错你平时写的arr[i]编译器本质上就是当成*(arr i)来处理的。甚至因为加法交换律i[arr]也是合法的等价于arr[i]—— 当然没人会这么写只是说明它们本质上是一回事。理解了这一点你就懂了数组和指针为什么那么像。它们只是语法形式不同底层是一回事。6.4 数组名是常量指针⚠️注意数组名是常量指针它的值不能改int arr[5] {1, 2, 3, 4, 5}; arr; // ❌ 错误数组名不能修改数组名就像一个固定的门牌号不能变。但普通指针变量可以随便改想指哪就指哪。七、指针与函数指针最经典的应用之一就是作为函数参数。7.1 为什么需要指针传参先看一个问题写一个函数交换两个变量的值。新手常见的错误写法#include stdio.h void swap(int x, int y) { int temp x; x y; y temp; } int main() { int a 10, b 20; swap(a, b); printf(a%d, b%d\n, a, b); // 输出什么 return 0; }运行结果a10, b20咦怎么没交换成功因为 C 语言的函数参数是值传递—— 传过去的是值的副本。x和y是a和b的拷贝函数里改的是x和y跟外面的a、b没关系函数结束x、y就销毁了外面的a、b还是原来的值7.2 用指针实现交换想在函数里修改外面的变量怎么办传地址过去你把变量的地址告诉函数函数就能通过地址找到这个变量直接修改它的内容。#include stdio.h void swap(int *x, int *y) // 参数是指针类型 { int temp *x; // 把x指向的值存起来 *x *y; // 把y指向的值放到x指向的地方 *y temp; // 把存的值放到y指向的地方 } int main() { int a 10, b 20; swap(a, b); // 传地址过去 printf(a%d, b%d\n, a, b); return 0; }运行结果a20, b10成功了为什么这次可以因为传过去的是a和b的地址函数里通过*x和*y直接操作a和b本身改的就是外面那两个变量不是副本记住想在函数内部修改外部变量就得传指针地址。这是指针最常用的场景之一。7.3 数组作为函数参数数组作为函数参数时传的也是指针 —— 准确说是数组首元素的地址。// 这三种写法是等价的本质上都是指针 void func(int arr[]) {} void func(int arr[5]) {} void func(int *arr) {}因为传的是指针所以函数里修改数组元素外面的数组也会跟着变 —— 因为操作的是同一块内存。#include stdio.h void set_first(int *arr, int value) { arr[0] value; // 等价于 *arr value } int main() { int arr[5] {1, 2, 3, 4, 5}; set_first(arr, 100); printf(%d\n, arr[0]); // 输出 100被改了 return 0; }八、空指针和野指针8.1 空指针 NULL如果一个指针什么都不指向我们可以把它设为空指针用NULL表示。int *p NULL; // p是空指针不指向任何有效的内存NULL本质上就是(void*)0也就是地址 0。地址 0 是系统保留的不能访问。为什么要有空指针指针变量刚定义的时候如果还不知道指向哪就先设为 NULL避免指针乱指使用指针之前可以先判断是不是 NULL防止出错if (p ! NULL) // 先判断是不是空指针 { *p 10; // 不是空的再用 }好习惯指针暂时不用的时候设为 NULL避免变成野指针。8.2 野指针野指针就是指向的位置不确定、或者指向的内存已经无效的指针。常见的野指针来源① 指针未初始化int *p; // p的值是随机的不知道指向哪 *p 10; // ❌ 危险可能会崩溃② 指针指向已经释放的内存后面学动态内存的时候会遇到③ 越界访问int arr[5]; int *p arr[10]; // 越界了指向的地方不属于这个数组野指针的危害很大可能程序直接崩溃段错误可能悄悄修改了别的数据程序结果不对最可怕的是有时候能跑通有时候崩溃很难调试怎么避免野指针定义指针时就初始化要么指向合法地址要么设为 NULL释放内存后指针立即置为 NULL使用指针前检查是否为 NULL注意数组边界不要越界九、本章小结指针是 C 语言的核心概念我们来梳理一下重点1. 基本概念地址内存单元的编号就像房间号指针就是地址指针变量专门存地址的变量指向p 存了 a 的地址就说 p 指向 a2. 两个关键运算符取地址a得到 a 的地址*解引用*p访问 p 指向的内容3. 指针运算指针 整数向前移动 n 个元素指针 - 整数向后移动 n 个元素指针 - 指针相差几个元素指针比较判断地址大小⚠️ 只有指向同一数组时运算才有意义4. 指针与数组数组名就是首元素地址常量指针不能改arr[i]等价于*(arr i)用指针可以遍历数组5. 指针与函数函数参数默认是值传递改不了外面的变量想在函数里改外面的变量传指针过去数组作为参数本质上传的是指针6. 空指针与野指针NULL空指针什么都不指向野指针指向不确定的位置非常危险好习惯及时初始化、用完置空、用前检查学习建议指针是 C 语言的分水岭一定要多动手、多画图。建议你每学一个指针的概念就画一张内存图把地址、变量、指针的关系画出来 —— 画着画着就懂了。十、写在最后C gives you enough rope to shoot yourself in the foot.—— C 语言社区名言翻译成中文大概是C 语言给了你足够的绳子让你能开枪打中自己的脚。指针就是那根 绳子—— 它给了你极大的自由和力量让你能直接操作内存写出高效的代码但同时也把危险暴露给了你一不小心就会 伤到自己。但请不要害怕指针。它不是洪水猛兽只是一个工具。用好了它就是你手中的利剑用不好才会伤到自己。多练习、多思考你终会驾驭它。⚠️ 关于本文由于笔者水平有限文中难免存在疏漏、错误或表述不够准确的地方。如果你在阅读过程中发现了任何问题 —— 无论是概念错误、代码 bug、还是表述不清 —— 都欢迎在评论区留言指正我会及时修正。技术之路道阻且长行则将至。我们互相学习共同进步