本文介绍了 C 语言中的一级指针和二级指针,并通过示例说明了为什么在函数参数中使用一级指针,函数内部对指针形参本身的修改不会影响外部指针的指向。同时,也演示了如何使用二级指针在函数内部改变指针的指向以及进行内存分配操作。

概念

在如下的 A 指向 B、B 指向 C 的指向关系中,首先:

C 是一个变量,里面是「一段内容」,这段内容需要存储在一个地址空间里。C 的起始地址是 0x00000008。

B 是一个指针变量,其内容是 C 的地址 0x00000008(专业术语:B 指向 C),但是 B 本身也要占空间的啊,所以 B 也有地址。B 的起始地址是 0x00000004。

那么,到此为止都比较好理解:

1
2
3
B == 0x00000008;   // B 的内容,也就是 C 的地址 0x00000008
*B == 一段内容; // B 解引用,也就是 B 指针指向的 C 的值
&B == 0x00000004; // B 取地址,B 本身的地址是 0x00000004

查看指针变量 B,就是查看 B 的内容(这里是 C 的地址)。对指针变量 B 解引用,就是查看它的内容(内容是一个地址)下的内容。

指针访问

A 是二级指针变量,其中存放着 B 的地址 0x00000004。A 本身也有地址,是 0x00000000。

1
2
3
4
*A == B == 0x00000008;  // A 解引用,也就是 A 指针指向的 B 的内容(即 B 的内容是 C 的地址)
**A == *B == 一段内容; // B 解引用,也就是 B 指针指向的 C 的内容
A == &B == 0x00000004; // A 存的是 B 的地址,B 的地址是 0x00000004
&A == 0x00000000; // A 取地址,A 本身的地址是 0x00000000

上面三个变量的 C 语言定义如下:

1
2
3
4
5
6
7
int c = 123;  // 一段内容
int *b = &c;
int **a = &b; // 按顺序定义,定义二级指针前要定义一级指针

printf("&c, &b, &a = %p, %p, %p\n", &c, &b, &a);
printf("c, b, a = %d, %p, %p\n", c, b, a);
printf("*b, *a, **a = %d, %p, %d\n", *b, *a, **a);

打印结果:

1
2
3
&c, &b, &a = 0x7fff05148014, 0x7fff05148018, 0x7fff05148020
c, b, a = 123, 0x7fff05148014, 0x7fff05148018
*b, *a, **a = 123, 0x7fff05148014, 123

解释:

  • c, b, a 各自有各自的地址;
  • c 的内容是 123,b 的内容是 c 的地址,a 的内容是 b 的地址;
  • b 解引用是 123,a 解引用就是 b 的内容(即 c 的地址),a 的解引用的解引用(即 b 的解引用)就是 123。

使用

二级指针作为函数参数的作用:在函数外部定义一个指针 p,在函数内给指针变量(是对指针变量,不是对指针解引用)赋值,函数结束后对指针 p 生效,那么我们就需要二级指针

看看下面一段代码:有两个变量 a 和 b、一个指针 q(q 指向 a)。现在,我们想让 q 指向 b(要在函数里面实现)。

一级指针的实现

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

int a = 10, b = 100;
int *q;

void func(int* p) {
printf("note:3, func:&p=%p, p=%p\n", &p, p);
p = &b;
printf("note:4, func:&p=%p, p=%p\n", &p, p);
}

int main(int argc, char* argv[]) {
printf("note:1, &a=%p, &b=%p, &q=%p\n", &a, &b, &q);

q = &a; // 现在让指针 q 指向 a
printf("note:2, *q=%d, q=%p, &q=%p\n", *q, q, &q);
func(q); // 期望在这个函数里让指针 q 指向 b
printf("note:5, *q=%d, q=%p, &q=%p\n", *q, q, &q);

return 0;
}

打印结果:

1
2
3
4
5
note:1, &a=0x5627cc40d010, &b=0x5627cc40d014, &q=0x5627cc40d020
note:2, *q=10, q=0x5627cc40d010, &q=0x5627cc40d020
note:3, func:&p=0x7ffd3af06678, p=0x5627cc40d010
note:4, func:&p=0x7ffd3af06678, p=0x5627cc40d014
note:5, *q=10, q=0x5627cc40d010, &q=0x5627cc40d020

从结果可以看出:

  • note:1 -> a, b, q 各自有各自的地址
  • note:2 -> q 指向了 a,即地址 q 里的内容是 a 的地址
  • note:3 -> 把指针 q 作为参数传入了函数,在函数内部参数 p 的地址不再是 q 的地址、但参数 p 的内容是 q 的内容(即 a 的地址)。是的,参数传递是制作了一个副本,也就是指针 p 和指针 q 不是同一个指针、但是指向的地址却是相同的
  • note:4 -> p 指向了 b
  • note:5 -> 函数退出,p 的修改并不会对 q 造成影响

结论:

编译器总是要为函数的每个参数制作临时副本,指针入参 q 的副本是 p,编译器使得原内存和副本内存指向的内容保持一样,即 p = q,但是这两块内存自身的地址不一样 &p != &q

如果函数体内的程序修改了参数 p 这块副本内存的内容(比如在这里它指向 b 的地址),但是原内存 q 指向的内容不受影响。所以,在这里并不影响函数外的指针 q。

要想在函数内部修改对函数外部可见(生效),这就需要二级指针操作

二级指针的实现

例子一

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

int a = 10, b = 100;
int* q;

void func(int** p) {
printf("note:3, func:&p=%p, p=%p\n", &p, p);
*p = &b;
printf("note:4, func:&p=%p, p=%p\n", &p, p);
}

int main(int argc, char* argv[]) {
printf("note:1, &a=%p, &b=%p, &q=%p\n", &a, &b, &q);

q = &a;
printf("note:2, *q=%d, q=%p, &q=%p\n", *q, q, &q);
func(&q);
printf("note:5, *q=%d, q=%p, &q=%p\n", *q, q, &q);

return 0;
}

这段代码只修改了三处:

  • 函数 func 的形参变成了二级指针 int **p
  • 函数内对二级指针解引用后,指向了 b
  • main 函数中,将一级指针 p 的地址(即二级指针)传入了函数 func

打印结果:

1
2
3
4
5
note:1, &a=0x563b8d952010, &b=0x563b8d952014, &q=0x563b8d952020
note:2, *q=10, q=0x563b8d952010, &q=0x563b8d952020
note:3, func:&p=0x7ffcc73f68b8, p=0x563b8d952020
note:4, func:&p=0x7ffcc73f68b8, p=0x563b8d952020
note:5, *q=100, q=0x563b8d952014, &q=0x563b8d952020

note:3 -> 将一级指针 p 的地址(即二级指针)传入了函数 func。函数内部 &p 就是 副本 p 本身的地址 (&p=…68b8),二级指针 p 的值 (p=…2020) 就是指针 q 的地址 (&q=…2020),即*p == q;通过解引用*p=&b 重新指定了 q 指向的地址(即等价于执行了q=&b)。所以,在函数退出后,q 指向的地址发生改变。

结论:在函数中,通过传递指向指针 (q) 的指针 (&q),可以修改指针 (q) 本身指向的地址 (q=&b)

例子二

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define SIZE (100)

void my_malloc1(char* s1) {s1 = (char*)malloc(SIZE * sizeof(char));}
void my_malloc2(char** s2) {*s2 = (char*)malloc(SIZE * sizeof(char));}

int main(int argc, char* argv[]) {
const int len = 3;
char** p1 = (char**)malloc(len * sizeof(char*));
char** p2 = (char**)malloc(len * sizeof(char*));

memset(p1, 0, sizeof(p1));
memset(p2, 0, sizeof(p2));

for (int i = 0; i < len; i++) {
printf("my_malloc1(), before: p1[%d] is %s\n", i, p1[i] == NULL ? "NULL" : "NOT NULL");
my_malloc1(p1[i]);
printf("my_malloc1(), after: p2[%d] is %s\n", i, p1[i] == NULL ? "NULL" : "NOT NULL");
if (p1[i]) {free(p1[i]);}
}

for (int i = 0; i < len; i++) {
printf("my_malloc2(), before: p2[%d] is %s\n", i, p2[i] == NULL ? "NULL" : "NOT NULL");
my_malloc2(&p2[i]);
printf("my_malloc2(), after: p2[%d] is %s\n", i, p2[i] == NULL ? "NULL" : "NOT NULL");
if (p2[i]) {free(p2[i]);}
}

return 0;
}

打印结果:

1
2
3
4
5
6
7
8
9
10
11
12
my_malloc1(), before: p1[0] is NULL
my_malloc1(), after: p2[0] is NULL
my_malloc1(), before: p1[1] is NULL
my_malloc1(), after: p2[1] is NULL
my_malloc1(), before: p1[2] is NULL
my_malloc1(), after: p2[2] is NULL
my_malloc2(), before: p2[0] is NULL
my_malloc2(), after: p2[0] is NOT NULL
my_malloc2(), before: p2[1] is NULL
my_malloc2(), after: p2[1] is NOT NULL
my_malloc2(), before: p2[2] is NULL
my_malloc2(), after: p2[2] is NOT NULL

在这个示例中,我们尝试在函数内部给函数外申请的二级指针(指针数组 p1, p2 的每个位置(初始时都是空地址)分配一块内存。

在函数 my_malloc1 中,使用一级指针作为函数参数。就相当于给 p1[i] 的拷贝副本 s1 分配内存,p1[i]依然没有分配内存,所以在函数退出后,p1[i]依然是空地址。

在函数 my_malloc2 中,使用二级指针作为函数参数。&p2[i]的拷贝副本为 s2*s2 解引用的内容与拷贝前 &p2[i] 指向的内容一致(是 p2[i]),此时对其分配内存,在函数退出后,p2[i] 是一个分配的有效地址。

总结

在函数中,通过传递指向指针的指针(二级指针),可以修改指针本身指向的地址。

  • 通过二级指针,在函数内部改变指针的指向(例子一)。
  • 通过二级指针,在函数内部进行内存分配(例子二)。

注意,函数形参使用的是一级指针,就是值传递,值传递在函数里面 修改指针 ,改变的是形参的值,而不是实参的值;但可以通过对形参 解引用赋值,来改变实参指向的地址里的值。

参考资料:

  1. https://www.techxiaofei.com/post/cpp/secondary_pointer