为什么要在这个"手工列表"中使用指向指针的指针?

Why use a pointer to a pointer in this 'handmade list'?

本文关键字:指针 列表 为什么      更新时间:2023-10-16

我们在学校有一个项目,不管这个项目是关于什么的,它涉及使用具有以下特定结构的链表:

typedef struct _node {
int contents;
_node *next_node;
} *node;

在项目之前,我们被分配了一堆函数来学习使用列表(将节点推到列表的前面或后面,计算节点的数量,搜索特定节点等)。

这并不是那么困难,但是当老师发送基础项目时(这样我们每个人都在同一个地方开始),所有功能都涉及通过引用传递*node。例如:

resultType functionName(node *list, ...) { ... }

我在项目之前使用void函数完成了所有列表函数,因为至少据我所知,我们正在使用指针,所以,只要你不丢失列表标题的内存地址,你就不会丢失内容(和列表的其余部分)。

那么......在这种情况下,将指针传递到指针的意义何在?我错过了什么吗?我问我的老师,他不知道如何解释(要么是这样,要么是他和我一样迷茫)。我的意思是,结构已经是一个指针,那么,为什么要传递列表地址的地址呢?

这取决于在函数中执行的操作functionName。如果要分配给node*指针,或者如果要更改其值超出functionName范围,则需要能够对其进行修改,并且需要将指向指针的指针传递到函数中。

为了通过示例来说明这一点,请比较下面的两个函数。第一个,allocate1能够对指针 a 进行堆分配,因为它接收到指向指针的指针。第二个,allocate2按值接收指针,能够分配数组,但在返回后,分配的空间丢失。这可以通过比较 print 语句中的指针值来查看。

#include <iostream>
void allocate1(double** a, const int n)
{
*a = new double[n];
std::cout << "allocate1: " << *a << std::endl;
}
void allocate2(double* a, const int n)
{
a = new double[n];
std::cout << "allocate2: " << a << std::endl;
}
int main()
{
double* test1 = nullptr;
double* test2 = nullptr;
int n = 10;
allocate1(&test1, 10);
std::cout << "after allocate1: " << test1 << std::endl;
test1[3] = 16; // OK!
allocate2(test2, 10);
std::cout << "after allocate2: " << test2 << std::endl;
test2[3] = 16; // NOT OK!
return 0;
}

我的机器上的输出是:

allocate1: 0x7fcdd2403160
after allocate1: 0x7fcdd2403160
allocate2: 0x7fcdd24031b0
after allocate2: 0x0
Segmentation fault: 11

答案是:因为它很方便。它允许函数操作指针,而不管它存储在何处

关键是,对于链表,您希望在某处存储指向第一个节点的指针。该指针不是struct _node的一部分。但是,您可以将其地址传递给函数,就像列表中任何节点内任何next_node指针的地址一样。也就是说,您不需要专门处理空列表的情况。

举个例子:

void pushFront(node* list, int value) {
node newNode = malloc(sizeof(*newNode));
newNode->contents = value;
newNode->next_node = *list;
*list = newNode;
}

我可以像这样调用这个函数:

int main() {
node myList = NULL;
pushFront(&myList, 42);
printf("The answer is %d!n", myList->contents);
}

请注意,即使myList初始化为NULL!当您浏览列表、沿途插入或删除节点时,也会遵循类似的优雅代码。


旁白:我认为,typedef指针类型是一个坏习惯:它隐藏了一个事实,即事物是一个指针。我非常喜欢以下定义:

typedef struct node node;    //avoid having to write `struct` everywhere
struct node {
int contents;
node *next_node;
};
void pushFront(node** list, int value);

这意味着将函数实现更改为以下内容(仅增加三颗星):

void pushFront(node** list, int value) {
node* newNode = malloc(sizeof(*newNode));
newNode->contents = value;
newNode->next_node = *list;
*list = newNode;
}
int main() {
node* myList = NULL;
pushFront(&myList, 42);
printf("The answer is %d!n", myList->contents);
}

你看,每当我看到像这样的台词

node newNode = malloc(sizeof(*newNode));

我立刻想到了WTF时刻:"为什么newNode被视为指针,而它只是......哦,等等,这真的是指针类型!真的应该有人清理这段代码!另一方面,当我看到

node* newNode = malloc(sizeof(*newNode));

一切都立即清楚了:newNode是一个指针,它以标准方式与malloc()一起分配。

关键是,除非我在类型中看到*,否则我不需要指针。类型定义指针类型打破了这一假设,导致代码的可读性较差。特别是C程序员喜欢在这样的地方显式地表达他们的代码。

当然,你应该坚持老师给你的指导方针,但一定要尽快放弃指针类型定义习惯。

传递给此类函数的东西通常是"head",即指向列表第一个元素的指针。

该头指针通常需要修改:当您将第一个节点插入空列表时(头指针从 NULL 更改为指向实际节点的指针)或从列表中删除第一个元素时(需要将头指针更改为指向下一个列表元素)。

这就是为什么我们需要传递指向头部而不是头部本身的指针。

更好的解决方案是有一个List结构

typedef struct _List {
Node head;
} List;

然后你会传递一个指向List的指针,这是有意义的:在 C 中,你传递一个指向你需要修改的东西的指针。