C 语言指针详解

指针是 C 语言的灵魂,同时,指针也是 C 语言里最难搞的内容,初学者很容易就被绕进去。因此,出这个文章来详细解答一下 C 语言中指针的应用。同时,也会举一些常见的用法和易错点。

指针的定义

指针是什么?指针是一个变量。什么是变量?可以改变的数叫做变量。就像其他的数据类型一样,指针也可以被改变。它存的是其他变量的地址。

指针在计算机中占用的空间与计算机的位数有关。如果计算机是 32 位的,那么指针占用 4 个字节;如果计算机是 64 位的,那么指针占用 8 个字节。为什么是这个样子的呢?

以 64 位计算机为例。首先,我们先来了解一下计算机寻址。计算机是在一块内存空间上运行的,那么为了可以精确的表示这个内存空间。计算机需要给不同的地址赋予唯一的标记。而计算机是二进制的,因此可以通过位的不同来表示不同的地址空间。而对于计算机来说,最小的地址空间是字节,因此,对于 64 位的计算机,它可以表示的空间为 $0\sim2^{64}-1$ 字节的空间。那么为了可以存储这个占用 64 位的信息,就需要 $64/8=8$ 个字节的空间了。因此,64 位的计算机的指针是 8 位。同时,这也是指针这个类型被开发出来的原因。

在 C 语言中,定义一个指针的语法如下:

1
[类型] *[变量名];
  • 类型:要定义的目标类型。
  • 变量名:当前定义出来的指针的名字。

例如:

  • 定义一个 int 类型的指针,他的名字是 pMyPointer int *pMyPointer;
  • 定义一个 int* 类型的指针,他的名字是 ppMyPointerint* *ppMyPointer;
  • 定义一个 struct myStruct 类型的指针,他的名字是 pMyStructstruct myStruct *pMyStruct;

通过上面的例子,我们可以知道:

  1. 定义一个指针的语法是严格遵守上述格式的。
  2. 指针的类型可以是任意类型。可以是内置数据类型,也可以是自定义数据类型。

指针和其他变量的区别

既然指针和其他类型一样,都是变量,那么他们之间的区别是什么呢?答案是:没有区别

指针也会占用地址空间,指针也可以被改变,指针也可以被复制,指针也可以被相加… 总之,没有什么操作是指针所特有的。

指针的使用

在 C 语言中,如何使用一个指针呢?这里介绍两个操作符。

  • & :取地址符。它的作用是获取一个变量的地址。可以通过这个操作符来获取一个变量的地址,从而来给指针赋值。
  • * :解引用。它的作用是获取一个指针所指向的内容。

以一段代码来看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[]) {
// 定义一个int类型的指针
int *pMyPointer;
// 定义一个int类型的变量
int val;
// 给变量赋值
val = 5;
// 给指针赋值
pMyPointer = &val;
// 分别输出 变量的值 以及 指针的值
printf("val: %d, pMyPointer: %x", val, pMyPointer);
// 输出指针指向的值
printf("pMyPointer point to: %d", *pMyPointer);
return 0;
}

从这个代码片段的运行结果来看,我们可以得出以下的结论:

  1. 指针存放的是一个变量的地址
  2. 可以通过 & 来获取一个变量的地址,通过 * 来获取一个地址所指向的内存的值。

为了加深理解,我画了如下的图:

c-pointer

指针与函数

在 C 语言里,我们经常会写一系列函数来辅助我们完成一些我们想要的功能。

我们知道,函数在定义的时候,可以传入变量。但是当我们需要通过函数来改变外部的变量的时候,会发现当我们直接传入的变量是无法被改变的。因为函数在调用的时候,程序会自动复制一份相同的变量,此时,程序中存在两个完全一样的变量,他们之间相互独立,互不干扰。函数在执行的时候会使用复制出来的这个变量,而不会使用传入的变量。

那么,如何让函数可以改变外部的变量呢?

函数在定义的时候,可以在它的形参的位置传入指针。通过操作指针所指向的地址来间接的修改函数外部的变量。为什么可以通过指针来改变变量的值呢?当函数在调用的时候,虽然程序中是存在两个相同的变量的,但是它们的值是一样的。因此这两个指针都是指向同一个地址的。而这个地址上的变量代表的值就是这个变量的值,所以可以通过修改这个地址上的值来间接改变这个地址上的变量代表的值。

好,在我们知道了这个知识点了以后,我们再来看看这个函数本身。不知道看这个文章的各位有没有想过,一个函数只能传入变量嘛,可不可以传入一个函数呢?答案是可以的。

我们这里先讲一个知识点:函数指针指针函数

指针函数:返回值是一个指针的函数
函数指针:一个指向函数的指针

这里分别举一个例子:
指针函数:

1
2
3
4
5
int *func(int x) { //定义一个返回值类型为int *的函数
int *p = (int *)malloc(sizeof(int)); //动态分配内存空间
*p = x; //给指针所指向的变量赋值
return p; //返回指针
}

函数指针:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
int max(int x, int y) { //定义一个求最大值的函数
return x > y ? x : y;
}

int main(void) {
int (*p)(int, int) = &max; //定义一个指向max函数的指针变量p,&可以省略
int a, b, c;
printf("请输入两个数字:\n");
scanf("%d %d", &a, &b);
c = p(a, b); //通过指针变量p调用max函数,等价于c = max(a, b);
printf("最大的数字是: %d\n", c);
return 0;
}

看清楚这两个的区别了嘛?接下来我来讲一个函数指针。

一个函数的声明由函数的返回值类型,函数名和函数的参数列表组成。函数名是一个指向函数的运行入口的常量。因为是常量,所以它不可以被改变,但是可以被读取,即,可以把这个常量赋值给其他的一个变量。这个变量就是函数指针。

函数指针是一个指向函数的指针变量,它可以用来调用函数或作为回调函数的参数。注意,这是一个指针。

一个函数指针的定义方式如下:

1
函数返回值类型 (* 指针变量的名字)(函数的形参)

例:

1
2
3
4
5
6
7
8
9
// 被指函数:一个返回最大值的函数
int max(int a, int b) {
return a > b ? a : b;
}

// 定义一个函数指针,它指向的是max函数
int (* my_func_pointer)(int, int);

my_func_pointer = max;

易混淆点

例一

我们来看下面的这个程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>

void f(int *p, int *q);

int main(int argc, char *argv[]) {
int m = 1, n = 2;
int *r = &m;

f(r, &n);

printf("%d,%d", m, n);
}

void f(int *p, int *q) {
p = p + 1;
*q = *q + 1;
}

可以注意到,这里的 f 函数给我们挖了一个坑。我们先来看这个 f 的参数:f 函数传入两个参数,两个参数的都是 int 类型的指针

我们知道,在 C 语言,函数传值的方式只有一种,就是拷贝传入。什么是拷贝传入呢?拷贝传入是在函数调用的时候,程序会把被传入的变量复制出一个一模一样的变量。这样,程序里就会有两个一模一样的变量,一个在函数外,一个在函数内,两个变量相互独立,互不干扰。函数在执行的时候使用的是函数内的那个变量,而不是函数外的那个变量。因此,不论函数内对这个变量有什么操作,函数的外的那个变量始终不变。这也是为什么函数在处理一个传入的变量的时候,不论函数内部怎么变,函数外的那个变量没有任何改变。

知道了上述原理以后,我们来看 f 的第一个参数。第一个参数传入的是 r,因此,在执行这个函数的过程中,程序里有两个 r,一个位于函数的外部,即 main 函数内的那个,一个位于 f 内部。函数使用的是函数内部的那个变量 r,而不是函数外部的那个变量 r,因此,函数内部对 r + 1,对于函数外部的变量 r 是没有任何影响的。

我们再来看 f 传入的第二个参数。第二个函数传入的是 n 变量的地址。因此,函数 f 在执行的时候,形参 q 存储的是变量 n 的地址。F 函数在获取了这个地址的值以后加 1 ,然后再将结果赋值给这到这个地址上,由于 这个地址 的地址指的是函数外部变量 n 所在地址,所以变量 n 被改变了。