C 指针(保姆级)

在 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” 和 “*” 这两个组合而成。

C 数据类型

此外,如果想要定义一个指针变量,下面 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 函数释放指针所占用的内存空间,以避免内存泄漏。
  • 要避免使用野指针。所谓的 “野指针”,指的是指向已经被释放或者无效内存地址的指针。对野指针进行操作会导致程序崩溃或者产生不可预期的行为。常见的产生野指针的情况包括:指针变量未初始化、函数返回局部变量的地址、指针指向的内存被释放后继续使用等。

上一篇: C 递归函数

下一篇: C 数组指针

给站长反馈

绿叶网正在不断完善中,小伙伴们如果发现任何问题,还望多多给站长反馈,谢谢!

邮箱:lvyenet@vip.qq.com

「绿叶网」服务号
绿叶网服务号放大
关注服务号,微信也能看教程。
绿叶网服务号