二级指针和函数指针

* 可以作为定义指针时的形式说明符和取出指针变量保存的地址所指向的内存单元的值。我们可以通过 * 结合地址的方式来访问内存单元中的数据并存入数据;而&是取地址运算符,通过它得到变量的地址。

二级指针

1
2
3
4
5
int num = 10;

int* p = # // 一级指针

int** m = &p; // 二级指针

指向关系如下:

二级指针3.png

图中 m 是指针变量,类型为 int**,用以指向一级指针,即存储一级指针的地址。p 是指针变量,类型为 int*,用以指向变量,即存储变量的地址。num 是变量,类型为 int,用以存储实际数据,即实际数据的地址。

指针变量 m 存储的是 指针变量 p 的地址,指针变量 p 存储的是 变量 num 的地址,变量 num 存储的数据 10。

二级指针2.png

那么二级指针作为参数传递的时候,究竟应该传一级指针,还是传二级指针呢?

在回答这个问题之前,可以回顾之前的一级指针,即如何让 swap 函数的两个形参实际交换数据。

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
void Swap1(int a, int b) {	// 交换失败
int tmp = a;
a = b;
b = tmp;
}

void Swap2(int *a, int *b) { // 交换成功
int tmp = *a;
*a = *b;
*b = tmp;
}

int main(void) {

int a = 10;
int b = 20;
Swap1(a, b);
printf("a = %d , b = %d\n", a, b);

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

return 0;
}

如果你传递变量的值或 const 指针,那么你不会交换成功。如果你传递变量的地址,那么你会交换成功。

那我们就以链表的头插法来举例,先看看错误示范:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Node* add_before(Node* head, Node* tail, int val)
{
Node* new_node = malloc(sizeof(Node));
if (!new_node) {
perror("malloc failed\n");
exit(-1);
}
new_node->data = val;
new_node->next = head;

head = new_node; // 这只会修改函数内部的局部副本,而不会影响外部的 head

if (tail == NULL) {
tail = new_node; // 这也只会修改函数内部的局部副本
}

return new_node;
}

我们在函数外部传递 链表 head 进来,函数形参这边也是以一级指针接收这个链表,那么我们传递的是这个链表的副本。所以,如果我们需要成功更改这个链表的话,那就需要传递这个链表的地址,即在传递参数到函数中时,需要对该链表进行取地址,即 &head。那么我们函数的形参如果要接受这个参数,就得是 **head 才能接收这个链表的内存地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void add_before_head(Node** phead, Node** ptail, int val)
{
// 创建一个 Node 节点
Node* new_node = malloc(sizeof(Node));
if (!new_node) {
perror("malloc failed\n");
exit(-1);
}
new_node->data = val;
new_node->next = *phead;
*phead = new_node;

if (*ptail == NULL) {
*ptail = new_node;
}
}

千言万语,不抵一张图:

二级指针4.png

为了更清楚地解释:

  • 当你传递 Node* head,你实际上传递的是指向链表第一个节点的指针的副本。这意味着在函数内部修改 head,只是修改了这份副本的值,而不会修改调用函数时传入的 head 的值。
  • 你确实可以遍历链表,因为传入的 head 指针指向的是链表的第一个节点,它仍然指向链表结构。但是,当你试图修改 head 本身时,例如将它指向新的节点时,这些修改只会作用在副本上,无法反映到原来的链表头指针。

你传递进去的是链表头结点的指针,也就是 headtail,它们确实是指向链表头部和尾部的指针。但是,C 语言中的函数参数是按值传递的,也就是说你传递的是 headtail副本。在函数内部修改这些副本,并不会影响到原来的指针。

1
2
3
4
5
6
7
8
9
10
void test(Node* head) {
head = malloc(sizeof(Node)); // 这里的 head 是一个局部变量的副本
head->data = 42;
}

int main() {
Node* head = NULL;
test(head); // 调用函数后,head 仍然是 NULL
// head 仍然没有指向新的节点
}

在上面的例子中,head 被传递给 test 函数,但是 test 内部的修改不会影响 main 函数中的 head,因为传递的只是一个副本。

那我们继续回到之前的话题。head 指针变量 指向 Node1节点内存区域,现在新节点 new_node 已经头插成功,就需要让 head 能够再次成为头结点(之前是,现在已经不是的缘故)。

head 指向的是 Node1,可以对这个节点进行数据修改,指向其他节点,但唯独不可以更改自己的内存地址。就以下面这个例子说明。num1 和 num2 是相同类型,可以进行赋值,但这个修改的是 num1 的值,但不是 num1 的内存地址。也就是说 num1 代表的内存地址存储的数据已经被修改,但是 num1 这个变量的地址本身是没有被改变的。

1
2
3
4
int num1 = 10;
int num2 = 20;

num1 = num2;

我们知道改变 变量的值有两种方式,一个是修改对应内存地址的数据,一个是修改所指向的对象。当前这个变量的地址需要通过 & 来获取,而存储这个结果的只能是一级指针。那我们的链表本身就是多个节点连接的,通过一个头结点就能找到所有连接的节点。头节点本身就是一个指针,即一级指针。如果我们想要修改这个指针的数据,传递一级指针即可,就是它变量本身。如果我们想要修改这个指针指向的地址,就得获取它本身的内存地址,即对其进行取地址运算,然后把新对象的地址赋值给它,实现更改所指对象了。

1
2
3
4
5
6
void test(Node* head) {	// 修改节点的数据,传递一级指针就够了
while (head != NULL) {
head->data = 2 * head->data;
head = head->next;
}
}

如何修改?那就是修改 head 指针变量指向的地址,即让 head 原本指向 Node1(0x001) 现在指向 new_node(0x000)。也就是要拿到 head 指针变量本身的地址,phead 就是记录着 head 指针变量本身的地址,通过 *phead = new_node 就修改成功了。

二级指针5.png

函数指针

函数指针常见的应用是回调函数,它只支持两个操作:

  • 解引用*
  • 函数调用()
1
return_type (*pointer_name)(parameter_list);
  • return_type:函数返回类型
  • pointer_name:函数指针的名字
  • parameter_list:函数的参数列表

比方说:int (*func) (int, int),函数指针的名称为 func,返回值类型为 int,形参为两个 int 类型变量。

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

// 定义一个函数指针 'func',该指针指向一个接受两个 int 参数并返回 int 的函数
int (*func) (int, int);

// 定义一个函数 'add',接受两个 int 参数,返回它们的和
int add(int n1, int n2) {
return n1 + n2;
}

int main(void) {
// 将函数指针 'func' 指向函数 'add'
func = add;

// 使用函数指针 'func' 调用函数 'add',传入参数 1 和 2
int num = func(1, 2);

printf("num = %d\n", num);

return 0;
}

函数指针必须和接受的函数有相同的形参个数和对应的类型,以及相同的返回值类型。

我们也可以不用声明函数指针再去指向函数,可以直接定义,见下:

1
2
3
int (*func) (int, int) = add;

int num = func(1, 2);