双指针有什么实际用途?

33

我搜索了很多,但是没有找到我真正理解的答案。

我对c++非常非常新,并且无法理解使用双重、三重指针等的用途。它们有什么意义吗?

有人可以给我启示吗?


11
和任何其他指针一样。 - Slava
12
不要成为一名“三星程序员”。使用数据结构来封装指针。 - NathanOliver
21
这句话的意思是“它们有什么意义?”或者“它们存在的目的是什么?”,需要根据上下文来确定具体指的是哪些事物。 - Lightness Races in Orbit
1
@Dave:相反,那些是比简单明显的指针更复杂、不够清晰的替代方案。例如,在 C 语言中最常见的双重指针实例:“int main (int argc, char **argv)”。 - jamesqf
1
很不幸,每个初学者都会早早地接触到argv,因为它树立了一个非常糟糕的榜样。 - Davislor
显示剩余6条评论
11个回答

52
说实话,在写得很好的C++代码中,你很少会看到T**,除了库代码。事实上,星号越多,你就越接近赢得某种奖项
这并不是说永远不需要指向指针的指针;你可能需要构造一个指向指针的指针,原因与构造指向其他任何类型对象的指针相同。
特别是在数据结构或算法实现中,当你在移动动态分配的节点时,我可能会看到这样的东西?
然而,通常情况下,如果你需要传递指向指针的引用,你只需要这样做(即T*&),而不是加倍使用指针,甚至那也应该相当罕见。
在Stack Overflow上,你会看到人们使用指向动态分配的数据指针数组的指针做着可怕的事情,试图实现他们能想到的最低效的“二维向量”。请不要受到他们的启发。
总之,你的直觉并不是没有价值的。

7
实际上,你拥有的星星数量越多,就越接近赢得某种奖项。 - Arnav Borborah
6
现在,如果人们为其他句子中的每个句子点赞,我就会做得很好! - Lightness Races in Orbit
1
@LightnessRacesinOrbit 你应该在每个单词后面加上句号,以最大化点赞的潜力 ;) - NathanOliver
1
@NathanOliver:可以这样做,但我仍然没有打赢逗号病和句号病的战争,所以还不太准备好开始新的战斗:D - Lightness Races in Orbit
2
任何不同意“三星程序员”概念的人都应该看看世界上最广泛部署的SQL引擎之一的API(http://sqlite.org/capi3ref.html)。只有一个函数具有`***`参数,它是遗留接口的一部分。 - Daniel Kamil Kozar
显示剩余3条评论

16

了解指向指针的重要原因之一是,有时您需要通过某些API(例如Windows API)与其他语言(如C语言)进行接口。

这些API通常具有返回指针的输出参数。但是,那些其他语言通常没有引用或与C ++兼容的引用。这就是需要使用指向指针的情况。


谢谢大家。我只想了解它们的用途。除非我找到用途,否则我会避免使用它们。 - Mark Green

13

在C++中很少使用它。然而,在C语言中,它可以非常有用。假设你有一个函数将分配一些随机大小的内存并填充该内存。调用一个函数获取需要分配的大小,再调用另一个函数填充内存会很麻烦。相反,您可以使用一个双指针。双指针允许函数将指针设置为内存位置。还有一些其他用途,但这是我能想到的最好的事情。

int func(char** mem){
    *mem = malloc(50);
    return 50;
}

int main(){
    char* mem = NULL;
    int size = func(&mem);
    free(mem);
}

1
由于问题标记为C ++,您的论点实际上并没有回答问题。 - NathanOliver
3
这段代码几乎是有效的C++(它缺少一次转换)。而且这并不是无用的C++代码,它可能在与C进行互操作时使用。 - user253751
一个关于编程的实际例子是:posix_memalign() - Davislor
我也认为解释为什么双指针在C中很有用对于理解它们在C++中的作用是有用的,即使C++提供了其他完成相同任务的方法。 - user253751

12
我非常非常新手,对于C++中的双重、三重指针等使用方法无法理解。它们的作用是什么?
理解C语言指针的诀窍就是回到基础,这可能是你从未学过的。它们包括:
- 变量存储特定类型的值。 - 指针是一种值。 - 如果x是类型为T的变量,则&x是类型为T*的值。 - 如果x的求值结果是类型为T*的值,且等于某个类型为T的变量a的&a,则*x是类型为T的变量。更具体地说... - ...如果x的求值结果是类型为T*的值,且等于某个类型为T的变量a的&a,则*x是别名a。
现在一切都明白了。
int x = 123;

x 是一个类型为 int 的变量。它的值是 123

int* y = &x;

y是一个类型为int*的变量。 x是一个类型为int的变量。 因此,&x是类型为int*的值。因此我们可以将&x存储在y中。

*y = 456;

y 评估为变量 y 的内容。这是一个类型为 int* 的值。对类型为 int* 的值应用 * 将给出类型为 int变量。因此,我们可以将456分配给它。那么*y 是什么?它是x的别名。因此,我们刚刚将456分配给了x

int** z = &y;

z是什么?它是一个类型为int**的变量。 &y是什么?由于y是一个类型为int*的变量,&y必须是一个类型为int**的值。因此,我们可以将其赋值给z

**z = 789;

什么是 **z?从内往外看。 z 求值为一个 int** 类型的变量。因此 *z 是一个 int* 类型的变量。它是 y 的别名。因此,这与 *y 是相同的,而我们已经知道它是 x 的别名。
“真正的问题是什么?”
这里,我有一张纸。上面写着“1600 Pennsylvania Avenue Washington DC”。那是一栋房子吗?不,它只是一张写有房子地址的纸。但是我们可以用这张纸来找到这栋房子。
这里,我有一千万张编号的纸。第 123456 张纸上写着“1600 Pennsylvania Avenue”。那么,123456 就是一栋房子吗?不是。它是一张纸吗?也不是。但是这仍然足以让我找到这栋房子。
这就是重点:通常情况下,出于方便的原因,我们需要通过多层间接引用来引用实体。
即便如此,双重指针仍然令人困惑,并且表明您的算法抽象程度不够。请尝试使用良好的设计技术来避免使用双重指针。

@Tas:那是个打错字,已经修正了。谢谢! - Eric Lippert
没错。由于计算机科学中任何问题的解决方案都是添加另一层间接性,因此看到双重指针只意味着有人先遇到了该问题,并解决了它。 - davidbak
@davidbak,有一个问题无法解决。是什么问题? - Eric Lippert
嗯...在BLISS-16中,“.”运算符用于什么?(BLISS-32也有它...) - davidbak
1
@davidbak "我有太多的抽象层。" - Eric Lippert

10

双指针就是指向指针的指针。常见用途是用于字符字符串数组。想象一下几乎每个C/C++程序中的第一个函数:

int main(int argc, char *argv[])
{
   ...
}

这也可以写成

int main(int argc, char **argv)
{
   ...
}

变量argv是一个指向char指针数组的指针。这是传递C语言“字符串”数组的标准方法。为什么要这样做?我看到它用于多语言支持、错误字符串块等。
不要忘记指针只是一个数字——计算机内存中的一个“插槽”的索引。仅此而已。所以双指针是一块内存的索引,恰好保存着另一个指向其他地方的索引。如果你愿意,可以将其视为数学上的连点成线。
这就是我向孩子们解释指针的方式:
想象一下计算机内存是一系列的盒子。每个盒子上都有一个编号,从零开始,逐个增加1,直到内存中有多少字节为止。假设你有一个指向内存某个位置的指针。这个指针只是盒子号码。我的指针是4。我看一下第4个盒子里面是什么。里面有另一个数字,这次是6。现在我们再看一下第6个盒子,得到最终想要的东西。我的原始指针(写着“4”)是一个双指针,因为它的盒子内容是另一个盒子的索引,而不是最终结果。
似乎近年来指针本身已经成为编程中的一个避讳词。在不太久的过去,传递指向指针的指针是完全正常的。但随着Java的普及和C++中传递引用的增加,指针的基本理解下降了——特别是当Java作为一门计算机科学入门语言而非Pascal和C时。

我认为关于指针的很多抨击都是因为人们从未真正理解它们。人们不理解的东西会被嘲笑。所以它们变得“太难”和“太危险”。我猜,即使是一些所谓有学问的人也提倡智能指针等概念,这些想法也是可以预料的。但实际上,它们是非常强大的编程工具。老实说,指针是编程的魔法,毕竟,它们只是一个数字。


1
在许多情况下,Foo*&Foo** 的替代品。在这两种情况下,您都有一个指针,其地址可以被修改。
假设您有一个抽象的非值类型,并且需要返回它,但返回值被错误代码占用:
error_code get_foo( Foo** ppfoo )

或者

error_code get_foo( Foo*& pfoo_out )

现在一个可变的函数参数很少有用,因此改变最外层指针ppFoo指向的能力很少有用。然而,指针是可空的--因此,如果get_foo的参数是可选的,指针就像是可选的引用。
在这种情况下,返回值是一个原始指针。如果它返回一个拥有的资源,通常应该是std::unique_ptr*--在那个间接级别上的智能指针。
相反,如果它返回指向它不共享所有权的东西的指针,那么原始指针更有意义。
除了这些“粗糙的输出参数”之外,还有其他使用Foo**的方法。如果您有一个多态非值类型,非拥有句柄是Foo*,并且您想要拥有int*的相同原因,您将想要拥有Foo**。
这就导致你要问:“为什么你需要一个int *?”在现代C++中,int *是对int的非所有可空可变引用。它在存储在结构体中时表现得比引用更好(将引用放入结构体中会产生关于赋值和复制的混淆语义,特别是如果与非引用混合使用)。有时可以用std::reference_wrapper或std::optional>替换int *,但请注意,它们与简单的int *相比将增大2倍。因此,使用int *有合理的理由。一旦你拥有了它,当你想要指向非值类型的指针时,你就可以合理地使用Foo **。你甚至可以通过在连续数组中拥有int*来获得int **的操作。
合法地成为三星级程序员变得更加困难。现在你需要一个合法的理由(比如)想通过间接方式传递Foo**。通常在达到这一点之前,您应该考虑抽象和/或简化代码结构。
所有这些都忽略了最常见的原因; 与C API交互。C没有unique_ptr,也没有span。它倾向于使用原始类型而不是结构,因为结构需要笨拙的基于函数的访问(没有运算符重载)。
因此,当C++与C交互时,有时会比等效的C++代码多出0-3个*

我无法理解这个逻辑。例如,std::unique_ptr<Foo>* 有什么用处?为什么你还在谈论返回它,而你已经将返回类型与错误代码占据了? - Mikhail
@Mikhail error_code get_foo( std::unique_ptr<Foo>* here ) 通过指针参数“返回”其“返回值”,而实际的返回值由 error_code 承载。这种模式在 COM 和 IDL 等内容中得到了正式化,其中不使用异常,语义返回值始终为指针,而语法返回值为错误代码。 - Yakk - Adam Nevraumont

0

使用它是为了拥有一个指向指针的指针,例如,如果您想通过引用传递指向方法的指针。


2
C++有引用,因此应优先使用foo(type*&) - NathanOliver
可能更好地表述为:如果您希望函数修改指针中的值,请传递指针的地址(或在C++中,通过引用传递指针)。 - Thomas Matthews

0
在C++中,如果您想将指针作为输出或输入/输出参数传递,可以通过引用传递它:
int x;
void f(int *&p) { p = &x; }

但是,引用不能(“合法地”)是nullptr,所以,如果指针是可选的,您需要一个指向指针的指针:

void g(int **p) { if (p) *p = &x; }

没问题,自从C++17以来,你就有了std::optional,但是“双指针”已经成为惯用的C/C++代码数十年了,所以应该没问题。此外,使用方法也不太好,你要么:

void h(std::optional<int*> &p) { if (p) *p = &x) }

这在调用时有点丑陋,除非你已经有了一个std::optional或者:

void u(std::optional<std::reference_wrapper<int*>> p) { if (p) p->get() = &x; }

这本身并不是很好。

此外,有人可能会认为在调用站点上阅读g更好:

f(p);
g(&p); // `&` indicates that `p` might change, to some folks

但是在C++中,你不会真正将一个原始指针作为输出或输入/输出参数传递。这就是关键。如果您掌控设计,最好以其他方式设计它。 - Lightness Races in Orbit
也不存在所谓的C/C++,仅仅因为这种编码风格在几十年前被使用并不意味着现在应该继续使用它。 - Lightness Races in Orbit
2
请不要过分解读 C/C++,它只是一种常见的表示方式,表示“C和/或C ++”。此外,问题是“它们的意义是什么”,而不是“我应该使用它们吗”。 - srdjan.veljkovic
1
你将如何在你的例子中使用 std::optional - Mikhail

0
“双指针”有什么实际用途呢?
以下是一个实际的例子。假设您有一个函数,想要将字符串参数数组传递给它(也许您有一个 DLL 想要传递参数)。代码可能如下所示:
#include <iostream>

void printParams(const char **params, int size)
{
    for (int i = 0; i < size; ++i)
    {
        std::cout << params[i] << std::endl;
    }
}

int main() 
{
    const char *params[] = { "param1", "param2", "param3" };
    printParams(params, 3);

    return 0;
}

你将会发送一个由 const char 指针组成的数组,每个指针都指向以空字符结尾的 C 字符串的开头。编译器将会将你的数组降解为函数参数指针,因此你得到的是 const char **,即指向第一个 const char 指针数组的指针。由于在此时数组大小已经丢失,因此你需要将其作为第二个参数传递。

1
这不是C++,而是“带有iostreams的C”。 - Mikhail
std::vector<std::string> - spectras

0
一个我使用它的案例是在 C 语言中操作链表的函数。
有这样一个场景:
struct node { struct node *next; ... };

对于链表节点,以及

struct node *first;

指向第一个元素。所有操作函数都采用struct node **,因为我可以保证即使列表为空,该指针也是非NULL的,而且我不需要任何特殊情况来进行插入和删除:

void link(struct node *new_node, struct node **list)
{
    new_node->next = *list;
    *list = new_node;
}

void unlink(struct node **prev_ptr)
{
    *prev_ptr = (*prev_ptr)->next;
}

如果要在列表开头插入元素,只需传递一个指向first指针的指针,即使first的值为NULL,它也会执行正确的操作。

struct node *new_node = (struct node *)malloc(sizeof *new_node);
link(new_node, &first);

这个概念是否最好通过定义一个 struct list { struct node * head; }; 来表达呢?然后使用 struct list *。更好的是,如果你在某个时刻需要存储更多的东西(例如:列表长度或尾指针),你不必改变所有的签名。 - spectras
@ spectras,这仍然需要我特殊处理第一个元素的插入,或者形成指向指针的指针。我在该实现中使用指向指针的指针,因为它允许我在任意位置插入。 - Simon Richter

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接