【问题标题】:What real use does a double pointer have?双指针有什么实际用途?
【发布时间】:2018-08-07 12:34:44
【问题描述】:

我已经搜索并搜索了这个问题的答案,但找不到我真正“得到”的任何东西。

我对 c++ 非常陌生,无法理解双指针、三指针等的使用。它们有什么意义?

谁能告诉我

【问题讨论】:

  • 和其他指针一样
  • 不要成为three star programmer。使用数据结构封装指针。
  • “它们有什么意义?”喜欢它
  • @Dave:相反,这些是简单而明显的指针的更复杂且不太清晰的替代方案。例如,最常见的双指针实例可能是什么(在 C 中):“int main (int argc, char **argv)”。

标签: c++ pointers


【解决方案1】:

老实说,在编写良好的 C++ 中,您应该很少在库代码之外看到 T**。事实上,你拥有的星星越多,你就越接近中奖an award of a certain nature

这并不是说永远不需要指针指向指针。您可能需要构造一个指向指针的指针,原因与您需要构造指向任何其他类型对象的指针的原因相同。

特别是,我可能希望在数据结构或算法实现中看到这样的东西,当您在动态分配的节点周围进行混洗时,也许?

不过,一般来说,在此上下文之外,如果您需要传递对指针的引用,您会这样做(即T*&),而不是加倍指针,即使这样也应该是相当罕见的。

在 Stack Overflow 上,您会看到人们用指向动态分配的数据指针数组的指针做可怕的事情,试图实现他们能想到的效率最低的“二维向量”。请不要受到他们的启发。

总之,你的直觉并非没有价值。

【讨论】:

  • “事实上,你拥有的星星越多,你就越接近获得某种性质的奖项。” - 为这句话+1。
  • @ArnavBorborah:现在,如果人们为其他每个句子 +1,我会做得很好!
  • @LightnessRacesinOrbit 然后,您应该在每个单词的结尾加上句号,以最大限度地提高投票潜力;)
  • @NathanOliver:可以,但我还没有赢得与逗号拼接的战争,并且还没有准备好开始使用句号拼接:D
  • 任何不同意“三星级程序员”概念的人都应该看看API for (one of?) the most widely deployed SQL engines in the world。只有一个函数具有 *** 参数,它是旧接口的一部分。
【解决方案2】:

您应该/必须了解指针对指针的一个重要原因是...有时您必须通过某些 API(例如 Windows API)与其他语言(例如 C)进行交互。

这些 API 通常具有具有返回指针的输出参数的函数。然而,那些其他语言通常没有引用或兼容(与 C++)引用。这是需要指针到指针的情况。

【讨论】:

  • 谢谢大家。我只想了解它们的用途。除非我找到用处,否则我会避开它们。
【解决方案3】:

我对 c++ 非常陌生,无法理解双指针、三指针等的使用。它们有什么意义?

理解 C 中的指针的诀窍就是回到基础知识,而这些基础知识你可能从未学过。它们是:

  • 变量存储特定类型的值。
  • 指针是一种值。
  • 如果xT 类型的变量,那么&xT* 类型的值。
  • 如果x 的计算结果为T* 类型的值,则*xT 类型的变量。更具体地说...
  • ...如果x 的某个T 类型的变量a 的值等于&aT* 类型的值,那么*xa 的别名。

现在一切如下:

int x = 123;

xint 类型的变量。它的值为123

int* y = &x;

yint* 类型的变量。 xint 类型的变量。所以&xint* 类型的值。因此我们可以将&x存储在y中。

*y = 456;

y 计算为变量y 的内容。这是int* 类型的值。将* 应用于int* 类型的值会得到int 类型的变量。因此,我们可以为它分配 456。 *y 是什么?它是x 的别名。因此,我们刚刚将 456 分配给了x

int** z = &y;

z 是什么?它是int** 类型的变量。 &y 是什么?因为yint* 类型的变量,所以&y 必须是int** 类型的值。因此我们可以将其分配给z

**z = 789;

**z 是什么?从内到外工作。 z 的计算结果为 int**。因此*zint* 类型的变量。它是y 的别名。因此这与*y 相同,我们已经知道那是什么;它是x 的别名。

真的没有,有什么意义?

这里,我有一张纸。它说1600 Pennsylvania Avenue Washington DC。那是房子吗?不,那是一张纸,上面写着房子的地址。但我们可以用那张纸找到房子。

这里,我有一千万张纸,全部编号。论文编号 123456 写着1600 Pennsylvania Avenue。 123456是房子吗?不,是一张纸吗?没有。但它仍然是足够的信息让我找到房子

这就是重点:为了方便,我们经常需要通过多层间接引用实体

也就是说,双指针令人困惑,并且表明您的算法不够抽象。尝试通过使用良好的设计技术来避免它们。

【讨论】:

  • @Tas:那是一个错字。固定的。谢谢!
  • 倒数第二段:完全正确。由于计算机科学中任何问题的解决方案都是添加另一层间接性,因此看到双指针仅意味着有人首先解决了该问题并解决了它。
  • @davidbak 有一个问题无法解决。这是什么?
  • 嗯……那个“。” BLISS-16 中的算子是用来做什么的? (BLISS-32 也有...)
  • @davidbak “我的抽象层太多了。”
【解决方案4】:

在 c++ 中使用较少。但是,在 C 中,它可能非常有用。假设您有一个函数可以分配一些随机内存量并用一些东西填充内存。必须调用一个函数来获取您需要分配的大小,然后调用另一个将填充内存的函数,这将是一件痛苦的事情。相反,您可以使用双指针。双指针允许函数将指针设置为内存位置。它还可以用于其他一些事情,但这是我能想到的最好的事情。

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

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

【讨论】:

  • 由于问题是用 C++ 标记的,因此您的论点确实不能回答问题。
  • @NathanOliver 这几乎是有效的 C++(它缺少演员表)。它甚至不是无用的 C++ 代码——它可以在与 C 互操作时使用。
  • 一个真实的例子:posix_memalign().
  • 我还认为解释为什么双指针在 C 中有用对于理解它们在 C++ 中的作用很有用,即使 C++ 确实提供了其他方法来做同样的事情。
【解决方案5】:

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

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

也可以这样写

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

变量argv 是一个指向char 指针数组的指针。这是传递 C“字符串”数组的标准方法。为什么要这样做?我已经看到它用于多语言支持、错误字符串块等。

不要忘记指针只是一个数字——计算机内部内存“插槽”的索引。仅此而已,仅此而已。所以双指针是一块内存的索引,恰好将另一个索引保存到其他地方。如果您愿意,可以使用数学连接点。

这就是我向孩子们解释指针的方式:

想象计算机内存是一系列盒子。每个盒子上都写有一个数字,从 0 开始,以 1 递增,直到内存的字节数。假设您有一个指向内存中某个位置的指针。这个指针就是箱号。我的指针是,比如说 4。我查看框 #4。里面是另一个数字,这次是 6。所以现在我们查看框 #6,得到我们想要的最后一个数字。我原来的指针(即“4”)是一个双指针,因为它的盒子的内容是另一个盒子的索引,而不是最终结果。

最近似乎指针本身已成为编程的贱民。回到不远的过去,将指针传递给指针是完全正常的。但是随着 Java 的普及,以及 C++ 中传递引用的使用越来越多,对指针的基本理解下降了——尤其是在 Java 成为计算机科学初学者的第一年语言时,超过了 Pascal 和 C。

我认为很多关于指针的毒液是因为人们从来没有正确理解它们。人们不理解的事情会被嘲笑。所以他们变得“太难了”和“太危险了”。我想即使是所谓的博学人士也提倡Smart Pointers 等。这些想法是可以预料的。但实际上有一个非常强大的编程工具。老实说,指针是编程的魔力,毕竟它们只是一个数字。

【讨论】:

    【解决方案6】:

    在许多情况下,Foo*& 可以替代Foo**。在这两种情况下,您都有一个可以修改其地址的指针。

    假设你有一个抽象的非值类型,需要返回,但是返回值被错误码占用了:

    error_code get_foo( Foo** ppfoo )
    

    error_code get_foo( Foo*& pfoo_out )
    

    现在,可变的函数参数很少有用,因此更改最外层指针ppFoo指向的位置的能力很少有用。然而,一个指针是可为空的——所以如果get_foo的参数是可选的,一个指针就像一个可选的引用。

    在这种情况下,返回值是一个原始指针。如果它返回一个拥有的资源,它通常应该是一个std::unique_ptr<Foo>*——一个处于该间接级别的智能指针。

    如果相反,它返回一个指向它不共享所有权的东西的指针,那么原始指针更有意义。

    除了这些“粗略的参数”之外,Foo** 还有其他用途。如果你有一个多态的非值类型,非拥有句柄是Foo*,同样的原因你想要一个int*你想要一个Foo**

    然后你会问“你为什么想要int*?”在现代 C++ 中,int* 是对 int 的非拥有可为空的可变引用。当存储在 struct 中时,它的行为比引用更好(结构中的引用会在分配和复制周围产生令人困惑的语义,尤其是与非引用混合时)。

    您有时可以将 int* 替换为 std::reference_wrapper<int>,以及 std::optional<std::reference_wrapper<int>>,但请注意,它将是简单 int* 的 2 倍。

    所以有正当理由使用int*。一旦你有了它,当你想要一个指向非值类型的指针时,你可以合法地使用Foo**。你甚至可以通过一个连续的int*s 数组来访问int**

    合法地成为三星级程序员变得越来越难。现在你需要一个正当的理由(比如说)想通过间接传递Foo**。通常早在你达到那个点之前,你就应该考虑抽象和/或简化你的代码结构。

    所有这些都忽略了最常见的原因;与 C API 交互。 C 没有unique_ptr,也没有span。它倾向于使用原始类型而不是结构,因为结构需要笨拙的基于函数的访问(没有运算符重载)。

    因此,当 C++ 与 C 交互时,您有时会比等效的 C++ 代码多获得 0-3 个*s。

    【讨论】:

    • 我无法遵循逻辑。例如。 std::unique_ptr<Foo>* 有什么用处,为什么你要返回它,而你已经用错误代码占据了返回类型?
    • @Mikhail error_code get_foo( std::unique_ptr<Foo>* here ) 通过指针参数“返回”其“返回值”,而其实际返回由error_code 占用。这种模式在 COM 和 IDL 之类的东西中进行了形式化,其中不使用异常,语义返回值始终是指针,而语法返回值是错误代码。
    【解决方案7】:

    用途是有一个指向指针的指针,例如,如果你想通过引用将指针传递给一个方法。

    【讨论】:

    • C++ 有引用,但应该首选 foo(type*&)
    • 可能更好的说法是:如果您希望函数修改指针中的值,请传递指针的地址(或在 C++ 中,通过引用传递指针)。
    【解决方案8】:

    双指针有什么实际用途?

    这是一个实际的例子。假设您有一个函数,并且您想向它发送一个字符串参数数组(也许您有一个要向其传递参数的 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 指针数组的第一个指针的指针。由于此时数组大小丢失,您需要将其作为第二个参数传递。

    【讨论】:

    • 这不是 C++,这是“带有 iostreams 的 C”。
    • std::vector&lt;std::string&gt;
    【解决方案9】:

    我使用它的一个例子是在 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,这仍然需要我插入第一个元素的特殊情况,或者形成一个指针。我在该实现中使用指针到指针,因为它允许我在任意位置插入。
    【解决方案10】:

    多重间接在很大程度上是对 C(既没有引用也没有容器类型)的保留。在编写良好的 C++ 中,您不应该看到太多的多重间接,除非您正在处理遗留的 C 库或类似的东西。

    话虽如此,多重间接却不属于一些相当常见的用例。

    在 C 和 C++ 中,在大多数情况下,数组表达式都会从“T 的 N 元素数组”类型“衰减”为“指向 T 的指针”1。所以,假设一个数组定义像

    T *a[N]; // for any type T
    

    当您将a 传递给函数时,如下所示:

    foo( a );
    

    表达式a会从“T *的N元素数组”转换为“指向T *的指针”,或者T **,所以函数实际接收的是

    void foo( T **a ) { ... }
    

    他们弹出的第二个地方是当你想要一个函数修改指针类型的参数时,比如

    void foo( T **ptr )
    {
      *ptr = new_value();
    }
    
    void bar( void )
    {
      T *val;
      foo( &val );
    }
    

    由于 C++ 引入了引用,您可能不会经常看到它。您通常只会在使用基于 C 的 API 时看到这一点。

    您也可以使用多个间接来设置“锯齿状”数组,但您可以使用 C++ 容器实现相同的目的,大大 减少痛苦。但如果你感到自虐:

    T **arr;
    
    try
    {
      arr = new T *[rows];
      for ( size_t i = 0; i < rows; i++ )
        arr[i] = new T [size_for_row(i)];
    }
    catch ( std::bad_alloc& e )
    {
      ...
    }
    

    但大多数情况下,在 C++ 中,唯一应该看到多重间接的情况是指针数组“衰减”为指针表达式本身。


    1. 当表达式是sizeof 或一元&amp; 运算符的操作数,或者是用于在声明中初始化另一个数组的字符串文字时,会出现此规则的例外情况。

    【讨论】:

    • Re,“C ...[没有] 容器类型。”您的意思是,C 标准库 没有。毕竟,C++ 语言中也没有容器类型:这些都是由库例程实现的。无论如何,没有什么能阻止聪明的 C 程序员实现他/她自己的容器类型,也没有什么能阻止更聪明的程序员使用其他人已经实现和调试的容器实现。 (不过,公平地说,直到几十年后,在我开始使用 Java 之后,我才知道将它们称为“容器”。)
    【解决方案11】:

    在 C++ 中,如果要将指针作为 out 或 in/out 参数传递,则通过引用传递:

    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++ 中将原始指针作为输出或输入/输出参数传递,实际上并非如此。这才是重点。如果您可以控制设计,最好以其他方式进行设计。
    • 此外,没有 C/C++ 之类的东西,仅仅因为这种风格是几十年前使用的,并不意味着它现在应该使用。
    • 请不要过多阅读 C/C++,这只是表示“C 和/或 C++”的常用方式。此外,问题是“它们有什么意义”,而不是“我应该使用它们”。
    • 您将如何在示例中使用std::optional
    猜你喜欢
    • 2012-02-17
    • 2013-05-14
    • 2011-05-06
    • 2011-04-25
    • 2011-04-02
    • 2019-05-17
    • 2015-04-22
    • 2021-07-22
    • 1970-01-01
    相关资源
    最近更新 更多