在 C 语言中,指针(pointer)是一种特殊的变量,它存储的不是值,而是一个内存地址。指针是 C 语言重点中的重点,有一句话说得好:“不懂指针,C 语言等于白学”。因此小伙伴们一定要把指针每一个细节都扣清楚。
C 指针是什么?
对于 C 指针来说,我们首先要清楚一点: “指针” 就是一个内存地址,而 “指针变量” 就是保存该内存地址对应的变量。
语法:
类型名* 变量名 = 地址;说明:
对于指针操作,我们经常会用到 “&” 和 “*” 这 2 种运算符。
& 普通变量名:表示获取一个变量(值)的内存地址。* 指针变量名:表示获取一个内存地址对应的变量(值)。
怎么来理解这两个运算符呢?我们先来看一个简单例子。
示例 1:使用 C 指针
#include <stdio.h>
int main(void)
{
int a = 10; // 变量 a 的值是一个整数 10
int* p = &a; // 变量 p 的值是 “变量 a 的地址”
printf("%p\n", p); // 使用 %p 输出指针变量的值(内存地址)
printf("%d\n", *p);
return 0;
}运行结果如下。(地址可能因运行环境而异)
61FE14
10分析:
在这个例子中,a 是一个 “普通变量”,p 是一个 “指针变量”。普通变量的值只能是整型、浮点型、字符型这些,而指针变量的值只能是一个内存地址。下面这种写法就是错误的,这是因为 p 的值只能是一个内存地址,我们不能把一个普通变量的值赋值给它。
int a = 10;
int* p = a; 我们可以使用 “&变量名” 的方式来获取一个变量对应的内存地址。请注意,变量名是其所代表的内存空间中存储的值的代名词。我们平常所说的 “获取一个变量的内存地址”,这句话可以等价于 “获取一个值的内存地址”。变量只是一个值的代名词而已,了解这一点非常非常重要!因此我们说使用 “&变量名” 来获取一个值对应的内存地址,这种说法也是没有错的。
int* p = &a; 表示定义了一个整型的指针变量 p,然后把 “a 的内存地址” 赋值给 p。后面如果想要获取 “a 的内存地址”,我们就有 2 种方式:p 和 &a。此外要注意指针变量的定义,下面 2 种方式是等价的。
// 方式 1
int* p = &a;
// 方式 2
int* p;
p = &a; // 注意这里不是 *p = &a;由于这里 p 是一个整型的指针变量,那么它就只能存储整型数据的地址,而不能存储其他类型的地址。下面这种写法就是错误的。
float a = 3.14;
int* p = &a;需要注意的是,指针变量指的是 “p”,而不是 “*p”,所以不能把一个地址赋值给 “*p”。大多数初学的小伙伴很容易搞错。我们记住这一句话就可以了: “int*” 是一个整体,“*” 是属于 “int*” 的一部分,它本质上是一个数据类型(后面我们会说到)。
// 正确
int *p;
p = &a;
// 错误
int *p;
*p = &a;最后需要清楚的是,在C 语言标准中,规定打印指针必须使用 “%p”。
指针也是一种数据类型
我们都知道,C 语言的数据类型主要有 4 种:① 基本类型;② void 类型;③ 指针类型;④ 构造类型。实际上,指针也是一种数据类型。
int* p;在上面这句代码中,int* 是一种数据类型,它是一个指向 int 的指针。指针类型是一种复合类型(如下图所示),它由 “int” 和 “*” 这两个组合而成。

此外,如果想要定义一个指针变量,下面 2 种风格都是可以的。这 2 种风格我们都要了解一下,因为不同的书或教程,可能会使用不同的风格。
int* p; // 风格 1
int *p; // 风格 2在实际开发中,我们强烈推荐使用风格 1,而不推荐风格 2。国内很多书或教程都使用风格 2,正是因为这种风格才导致大量初学的小伙伴理解产生偏差,然后被指针 “劝退”。我们来看一个例子就明白了。
示例 2:指针使用风格 2 写法
#include <stdio.h>
int main(void)
{
int a = 10;
int *p = &a;
*p = 666;
printf("%d\n", a);
printf("%d", *p);
return 0;
}运行结果如下。
666
666分析:
上面例子定义了 2 个变量:a 和 p。a 是一个普通变量,p 是一个指针变量。其中,p 存储的是 a 的内存地址。此时使用 a 和 *p 都可以获得 a 的值。接下来,我们尝试修改 *p 的值,然后打印 a 和 *p 的值,会发现两者的值都变了。
上面例子使用的是风格 2 的写法,这会让初学者产生相当多的困惑,比如:
- int *p 代表的是 p 是 int 类型 ?
- 到底 p 是指针,还是 *p 是指针 ?
- 为什么是使用 *p = 666;,而不是使用 p = 666; ?
但如果你使用的是风格 1,就不会有上面的困扰了。我们将 int *p = &a; 改为 int* p = &a;,此时程序代码修改如下。
#include <stdio.h>
int main(void)
{
int a = 10;
int* p = &a;
*p = 666;
printf("%d", *p);
return 0;
}从上面可以直观地看出,p 的类型是 int* 而非 int。然后由于 p 是指针,我们不能直接将 666 赋值给它,而应该赋值给 *p。
注意: 在使用 int* p 这种风格时,尽量每行只定义一个变量。如果写成 int* p1, p2;,则 C 语言会将 p2 视为普通整数,而非指针。
C 指针和指针变量
在 C 语言中,如何区分指针和指针变量呢?对于上面这个例子来说,&a 可以认为是一个 “指针”,因为它代表的就是一个内存地址。然后 p 是一个 “指针变量”,因为这个变量保存的是一个 “指针”,也就是保存的是一个内存地址。
指针是指具体的一个值,只不过这个值是一个内存地址而已。指针变量是指一个变量,该变量保存的是一个指针。这个就像 “整数” 和 “整数变量” 的关系,整数一般指的是具体的值,比如 10、84、9 等,而整数变量指的是一个值为整数的变量。比如 int year = 2025; 中,2025 就是整数,year 就是一个整数变量。
指针和指针变量本应该是两个不一样的东西,但有一个事实值得注意,那就是不少图书或教程并没有严格区分,而是认为 “指针是指针变量的简称”。比如 int* p = &a;,很多书是这样解释的:声明了一个 int* 类型的指针 p,然后它的值是 a 的内存地址。严格来说,正确说法应该是:声明了一个 int* 类型的指针变量 p,然后它的值是 a 的内存地址。虽然在咱们教程中,我们会严格对这两个进行区分,但如果小伙伴看其他书或教程,应该根据上下文来理解,而不能过于拘泥文字表述。
提示: 绝大多数情况下,你可以将 “指针” 和 “指针变量” 看成是同一个东西。
我们说起一个变量的指针,指的就是这个变量的地址。小伙伴们把 “指针” 二字替换成 “地址” 就可以了。总而言之,我们记住这么一句话就可以了:指针就是地址,而指针变量就是地址变量。
C 指针变量的命名
在 C 语言中,指针变量一般命名为 p 或 ptr(即 pointer 的简写)。为了提高代码的可读性,小伙伴们可以遵循以下规则。
- 对于 double* 类型的指针变量,可以命名为 dptr。
- 对于 char* 类型的指针变量,可以命名为 cptr。
C 指针运算符
在 C 语言中,有一种运算符叫 “指针运算符”,指针运算符只有 2 种: “&” 和 “*” 。其中,& 是取地址运算,* 是取数值运算。
“&” 后面一般接一个普通变量,表示获取该普通变量对应的 “内存地址”。 “*” 后面一般接一个指针变量,表示获取该内存地址对应的 “数值”。
&普通变量名 // 取地址
*指针变量名 // 取数值比如在下面代码中,&a 就表示获取 “变量 a 的地址”,*p 就表示获取 “变量 a 的值”。小伙伴记住这么一句话就可以了:&a 是一个地址,*p 是一个数值。
#include <stdio.h>
int main(void)
{
int a = 10;
int* p = &a;
printf("%p", p);
printf("%d", *p);
return 0;
}示例 3:修改指针变量的值
#include <stdio.h>
int main(void)
{
int a = 10;
int b = 20;
int* p = &a;
p = &b;
printf("%d", *p);
return 0;
}运行结果如下。
20分析:
指针变量和普通变量相似,它本质上还是一个变量,所以它的值是可以改变的。但指针变量又跟普通变量不一样,它的值只能是一个内存地址。所以它的值只能从一个内存地址修改成另一个内存地址。
在这个例子中,指针变量 p 的初始值是 “a 的地址”,然后 p = &b; 表示将它的值修改成 “b 的地址”,所以 *p 就可以代表变量 b 了。
示例 4:修改普通变量的值
#include <stdio.h>
int main(void)
{
int a = 10;
int* p = &a;
printf("%d\n", *p); // 输出 10
*p = 20;
printf("%d", *p); // 输出 20
return 0;
}运行结果如下。
10
20分析:
只要 p 指向 a,那么 *p 就是 a 的别名。*p 不仅拥有和 a 相同的值,而且对 *p 的修改也会改变 a 的值。所以我们将 20 赋值给 *p,本质上就相当于将 20 赋值给 a。下面 2 种方式是等价的。
// 方式 1
*p = 20;
// 方式 2
a = 20;实际上,“*p 获取的是一个变量” 这种说法比 “*p 获取的是一个数值” 这种叫法更好一点。不过呢,变量和数值本身就可以认为是同一个东西。小伙伴们不必纠结太多,只需要能够理解就可以了。
示例 5:多个指针指向同一个变量
#include <stdio.h>
int main(void)
{
int a = 10, *p, *q;
p = &a;
q = p;
*q = 20;
printf("%d, %d, %d\n", a, *p, *q);
return 0;
}运行结果如下。
20, 20, 20分析:
int a = 10, *p, *q; 是一种简写,它等价于:
int a = 10;
int* p;
int* q;p 和 q 这两个指针变量的值其实都是 “变量 a 的地址”,所以 a、*p、*q 代表的是同一个东西。如果我们修改 *q 的值,那么 a 和 *p 的值也会跟着改变。
通过指针访问值
从上面可以知道,如果想要访问一个具体的值,其实我们有 2 种方式来实现。
- 通过 “变量名” 来访问,这种方式又叫做 “直接访问”。
- 通过 “指针变量” 来访问,这种方式又叫做 “间接访问”。
示例 6:直接访问 vs 间接访问
#include <stdio.h>
int main(void)
{
int a = 10;
int* p = &a;
printf("%d\n", a); // 直接访问
printf("%d\n",*p); // 间接访问
return 0;
}运行结果如下。
10
10分析:
小伙伴们要记住一点,在非定义部分时,*p 和 a 是完全等价的,*p 是 a 的一个别名。如果对 *p 的值进行修改,那么 a 的值也会跟着改变。
C 指针的应用场景
初学的小伙伴肯定会感觉指针这东西非常绕,语法又怪怪的。前面花了那么多篇幅去介绍,那么它到底有什么用呢?我们可以先来看一个简单的例子。
示例 7:交换两个变量的值(使用指针)
#include <stdio.h>
void swap(int* p1, int* p2)
{
int temp;
temp = *p1;
*p1 = *p2;
*p2 = temp;
}
int main(void)
{
int a = 66, b = 88;
swap(&a, &b);
printf("a = %d\nb = %d", a, b);
return 0;
}运行结果如下。
a = 88
b = 66分析:
在这个例子中,我们定义了一个函数 swap(),它的功能是用于交换两个变量的值。需要注意的是,swap() 函数的两个实参都是 “内存地址”,而不是 “具体的值”。
在 swap() 函数中,p1 和 p2 都是指针变量,它们的值都是一个内存地址,然后 *p1 就代表变量 a,而 *p2 就代表变量 b,最后我们交换这两个变量的值。
假如不使用指针,能否在 swap() 函数中交换两个变量的值呢?我们可以先试一下,请看下面例子。
示例 8:交换两个变量的值(不使用指针)
#include <stdio.h>
void swap(int p1, int p2)
{
int temp;
temp = p1;
p1 = p2;
p2 = temp;
}
int main(void)
{
int a = 66, b = 88;
swap(a, b);
printf("a = %d\nb = %d", a, b);
return 0;
}运行结果如下。
a = 66
b = 88分析:
怎么回事呢?为什么使用常规的方式,a 和 b 并没有交换值呢?其实原因很简单,这里的 swap() 函数本质上交换的是 p1 和 p2 这两个变量的值。由于函数作用域的影响,它并不会影响主函数 main() 中 a 和 b 的值。
对于这个例子来说,我们这样来理解就简单了:首先一个 C 程序从主函数 main() 开始执行,遇到了 int a = 66, b = 88; 之后,就会开辟 2 个内存地址分配给 a 和 b。然后遇到了 swap(a, b);,也就是执行 swap(66, 88)。注意这里的实参是 “具体的值”,此时就相当于把 66 赋值给 swap() 中的变量 p1,然后把 88 赋值给 swap() 中的变量 p2。这样 p1 的值是 66,p2 的值是 88,然后会另外开辟 2 个内存:一个给 p1,另一个给 p2。算下来,我们总共等于开辟了 4 个内存地址。
对于这个例子来说,swap() 内部执行的本质上是交换 p1 和 p2 这两个变量的值,并不是修改主函数中 a 和 b 的值。如下图所示。
但是对于上一个使用指针的例子来说就不一样了。我们还是从头开始分析:首先一个 C 程序从主函数 main() 开始执行,遇到了 int a = 66, b = 88; 之后,就会开辟 2 个内存地址分配给 a 和 b。然后遇到了 swap(&a, &b);,注意这里传递的不是 “具体的值”,而是 “内存地址”。到了 swap() 内部,p1 存放的是 “a 的地址”,p2 存放的是 “b 的地址”。
接下来 swap() 内部操作的,本质上还是对 a 和 b 这两个变量进行操作,所以 swap() 内部会直接影响主函数 main() 中 a 和 b 的值。
从上面可以知道,如果不使用指针,我们是无法在一个函数修改另一个函数中变量的值的。如果想要修改另一个函数中变量的值,我们应该把 “变量的地址” 作为参数传过去,而不是把 “变量的值” 传递过去。
实际上,指针除了可以在不同函数中共享一份数据之外,更重要的是用于操作引用类型数据(比如数组、字符串等),后面我们会慢慢接触到。
指针的注意事项
在使用 C 指针时,小伙伴们想要特别注意以下事项:
- 指针变量必须先初始化,否则会指向一个未知的地址。访问未初始化的指针会导致程序崩溃或者不可预测的行为。一个好的习惯是在声明指针变量时就将其初始化为 NULL(如果当时没有确切的地址可以赋值)。NULL 表示空指针,即不指向任何有效的内存地址。
- 在使用指针访问变量之前,必须先确保指针指向有效的地址。这包括检查指针是否为 NULL,以及确保指针指向的内存空间仍然有效。
- 使用完指针之后,如果指针指向的是通过动态内存分配(例如 malloc)获得的内存,应该使用 free 函数释放指针所占用的内存空间,以避免内存泄漏。
- 要避免使用野指针。所谓的 “野指针”,指的是指向已经被释放或者无效内存地址的指针。对野指针进行操作会导致程序崩溃或者产生不可预期的行为。常见的产生野指针的情况包括:指针变量未初始化、函数返回局部变量的地址、指针指向的内存被释放后继续使用等。
