【问题标题】:How many levels of pointers can we have?我们可以有多少级别的指针?
【发布时间】:2012-04-22 15:31:24
【问题描述】:

单个变量中允许有多少个指针 (*)?

让我们考虑下面的例子。

int a = 10;
int *p = &a;

同样我们可以有

int **q = &p;
int ***r = &q;

等等。

例如,

int ****************zz;

【问题讨论】:

  • 如果这对你来说真的是个问题,那你就做错了。
  • 您可以继续添加指针级别,直到您的大脑爆炸或编译器崩溃 - 以最快发生的为准。
  • 既然指向指针的指针又是一个指针,那么理论上不应该有任何限制。也许编译器将无法处理超出一些可笑的上限,但是...
  • 使用最新的 c++ 你应该使用类似std::shared_ptr<shared_ptr<shared_ptr<...shared_ptr<int>...>>>
  • @josefx - 这显示了 C++ 标准中的一个问题 - 没有办法提高智能指针的权力。我们必须立即要求延长支持,例如(pow (std::shared_ptr, -0.3))<T> x; 用于 -0.3 级间接。

标签: c++ c pointers language-lawyer limit


【解决方案1】:

C 标准规定了下限:

5.2.4.1 Translation limits

276 实现应能够翻译和执行至少一个程序,该程序至少包含以下每个限制的一个实例:[...]

279 — 12 个指针、数组和函数声明符(任意组合)修改 声明中的算术、结构、联合或 void 类型

上限是特定于实现的。

【讨论】:

  • C++ 标准“建议”一个实现至少支持 256。(可读性建议您不要超过 2 或 3,即使这样:超过一个应该是例外的。)
  • 这个限制是关于单个声明中的数量;它不会对您可以通过多个typedefs 实现多少间接性施加上限。
  • @Kaz - 是的,这是真的。但由于规范(没有双关语)指定了一个强制性的下限,它是所有符合规范的编译器都需要支持的有效上限。当然,它可能低于特定于供应商的上限。换种说法(以使其与 OP 的问题保持一致),它是规范允许的 最大值 (其他任何内容都是特定于供应商的。)有点离题,程序员应该(至少在一般情况下)将其视为他们的上限(除非他们有正当理由依赖供应商特定的上限)......我认为。
  • 另一方面,如果我必须使用具有长链解引用链的代码(特别是当到处都是时),我会开始自残。)
  • @beryllium:这些数字通常来自对预标准化软件的调查。在这种情况下,他们可能查看了常见的 C 程序和现有的 C 编译器,并发现至少有一个编译器会在超过 12 个时出现问题,并且/或者如果将其限制为 12 个,则没有程序会损坏。
【解决方案2】:

实际上,C 程序通常使用无限指针间接寻址。一到两个静态级别是常见的。三重间接很少见。但无限是很常见的。

无限指针间接是在结构的帮助下实现的,当然,不是直接的声明符,这是不可能的。并且需要一个结构,以便您可以在此结构中包含可以终止的不同级别的其他数据。

struct list { struct list *next; ... };

现在您可以拥有list->next->next->next->...->next。这实际上只是多指针间接:*(*(..(*(*(*list).next).next).next...).next).next。而.next 作为结构的第一个成员时基本上是一个 noop,所以我们可以把它想象成***..***ptr

实际上没有限制,因为链接可以通过循环而不是像这样的巨型表达式来遍历,而且结构可以很容易地做成圆形。

因此,换句话说,链表可能是添加另一个间接级别来解决问题的终极示例,因为您在每次推送操作时都动态地执行此操作。 :)

【讨论】:

  • 不过,这是一个完全不同的问题 - 包含指向另一个结构的指针的结构与指针指针非常不同。 int***** 是与 int**** 不同的类型。
  • 这并没有“非常”不同。区别是蓬松的。它比语义更接近语法。指向指针对象的指针,还是指向包含指针的结构对象的指针?这是同样的事情。到达列表的第十个元素是十级寻址间接。 (当然,表达无限结构的能力取决于结构类型能够通过不完全结构类型指向自身,因此list->nextlist->next->next 是同一类型;否则我们将不得不构造一个无限类型.)
  • 当我使用“fluffy”这个词时,我并没有有意识地注意到你的名字很蓬松。潜意识影响?但我确信我以前用过这个词。
  • 还请记住,在机器语言中,只要 R1 在每一步都是有效指针,您就可以迭代 LOAD R1, [R1] 之类的东西。除了“包含地址的单词”之外,不涉及任何类型。是否有声明的类型并不能决定间接性和它有多少级别。
  • 如果结构是圆形的,则不会。如果R1 持有指向自身的地址,那么LOAD R1, [R1] 可以无限循环执行。
【解决方案3】:

理论上:

您可以拥有任意多级的间接。

实际上:

当然,任何消耗内存的东西都不是无限期的,由于主机环境上的可用资源会有所限制。所以实际上,实现可以支持的内容有一个最大限制,并且实现应该适当地记录它。因此,在所有此类工件中,标准并未指定最大限制,但确实指定了下限。

这是参考:

C99 标准 5.2.4.1 翻译限制:

——12 个指针、数组和函数声明符(任意组合)修改一个 声明中的算术、结构、联合或 void 类型。

这指定了每个实现必须支持的下限。请注意,该标准在脚注中进一步说明:

18) 实施应尽可能避免施加固定的翻译限制。

【讨论】:

  • 间接不会溢出任何堆栈!
  • 已更正,我有一种错误的感觉,即阅读和回答 q 作为传递给函数的参数的限制。不知道为什么?!
  • @basile - 我希望堆栈深度成为解析器中的一个问题。许多形式解析算法都将堆栈作为关键组件。大多数 C++ 编译器可能使用递归下降的变体,但即便如此,它也依赖于处理器堆栈(或者,学究式地,依赖于就像有处理器堆栈一样的语言)。更多的语法规则嵌套意味着更深的堆栈。
  • 间接不会溢出任何堆栈!-->不!解析器堆栈可能溢出。 堆栈如何与指针间接相关? 解析器堆栈!
  • 如果*连续为多个类重载,并且每个重载都返回该行中其他类型的对象,那么这种链式函数调用可能存在stackoverflow。
【解决方案4】:

正如人们所说,“理论上”没有限制。但是,出于兴趣,我使用 g++ 4.1.2 运行它,它的大小可达 20,000。编译虽然很慢,所以我没有尝试更高。所以我猜 g++ 也没有施加任何限制。 (尝试设置 size = 10 并在 ptr.cpp 中查看,如果不是很明显。)

g++ create.cpp -o create ; ./create > ptr.cpp ; g++ ptr.cpp -o ptr ; ./ptr

创建.cpp

#include <iostream>

int main()
{
    const int size = 200;
    std::cout << "#include <iostream>\n\n";
    std::cout << "int main()\n{\n";
    std::cout << "    int i0 = " << size << ";";
    for (int i = 1; i < size; ++i)
    {
        std::cout << "    int ";
        for (int j = 0; j < i; ++j) std::cout << "*";
        std::cout << " i" << i << " = &i" << i-1 << ";\n";
    }
    std::cout << "    std::cout << ";
    for (int i = 1; i < size; ++i) std::cout << "*";
    std::cout << "i" << size-1 << " << \"\\n\";\n";
    std::cout << "    return 0;\n}\n";
    return 0;
}

【讨论】:

  • 我尝试的时候不能超过 98242。 (我在 Python 中编写了脚本,将 * 的数量翻了一番,直到我得到一个失败的,而前一个通过了;然后我在该时间间隔内对第一个失败的那个进行了二进制搜索。整个测试花费的时间更少不到一秒钟的时间。)
【解决方案5】:

检查起来很有趣。

  • Visual Studio 2010(在 Windows 7 上),在出现此错误之前,您可以有 1011 个级别:

    致命错误 C1026:解析器堆栈溢出,程序太复杂

  • gcc (Ubuntu), 100k+ * 没有崩溃!我猜硬件是这里的限制。

(仅使用变量声明进行测试)

【讨论】:

  • 确实,一元运算符的产生式是右递归的,这意味着移位归约解析器将在能够进行归约之前将所有 * 节点移动到堆栈中。跨度>
【解决方案6】:

没有限制,查看示例here

答案取决于您所说的“指针级别”是什么意思。如果您的意思是“在单个声明中可以有多少级间接性?”答案是“至少 12 个”。

int i = 0;

int *ip01 = & i;

int **ip02 = & ip01;

int ***ip03 = & ip02;

int ****ip04 = & ip03;

int *****ip05 = & ip04;

int ******ip06 = & ip05;

int *******ip07 = & ip06;

int ********ip08 = & ip07;

int *********ip09 = & ip08;

int **********ip10 = & ip09;

int ***********ip11 = & ip10;

int ************ip12 = & ip11;

************ip12 = 1; /* i = 1 */

如果您的意思是“在程序变得难以阅读之前,您可以使用多少级指针”,这是一个口味问题,但有一个限制。具有两个间接级别(指向某物的指针的指针)很常见。再多的事情就更难想了。除非替代方案更糟,否则不要这样做。

如果您的意思是“在运行时可以有多少级指针间接”,则没有限制。这一点对于循环列表尤其重要,其中每个节点都指向下一个节点。你的程序可以永远跟随指针。

【讨论】:

  • 几乎肯定有一个限制,因为编译器必须在有限的内存中跟踪信息。 (g++ 在我的机器上因内部错误 98242 而中止。我预计实际限制将取决于机器和负载。我也不认为这在实际代码中会成为问题。)
  • 是的@MatthieuM。 :我只是理论上考虑过 :) 感谢 James 完成答案
  • 嗯,链表并不是真正的指向指针的指针,它们是指向包含指针的结构的指针(或者你最终会做很多不必要的转换)跨度>
  • @Random832:Nand 说“如果你的意思是“在运行时你可以有多少级别的指针间接”,所以他明确地取消了只谈论指针指针的限制 (*n) .
  • 我不明白你的意思:'没有限制,请在此处查看示例。' 该示例不能证明没有限制。它只证明了 12 星间接是可能的。两者都不能证明circ_list 示例关于 OP 的问题:您可以遍历指针列表这一事实并不意味着编译器可以编译 n 星间接。
【解决方案7】:

使用指向函数的指针实际上更有趣。

#include <cstdio>

typedef void (*FuncType)();

static void Print() { std::printf("%s", "Hello, World!\n"); }

int main() {
  FuncType const ft = &Print;
  ft();
  (*ft)();
  (**ft)();
  /* ... */
}

如图here 所示:

你好,世界!
你好,世界!
你好世界!

而且它不涉及任何运行时开销,因此您可以随意堆叠它们...直到您的编译器阻塞文件。

【讨论】:

    【解决方案8】:

    没有限制。指针是一块内存,其内容是地址。
    正如你所说的

    int a = 10;
    int *p = &a;
    

    指向指针的指针也是一个变量,其中包含另一个指针的地址。

    int **q = &p;
    

    这里的q是指向持有p地址的指针的指针,该地址已经持有a的地址。

    指向指针的指针并没有什么特别之处。
    因此,持有另一个指针地址的指针链没有限制。
    即。

     int **************************************************************************z;
    

    是允许的。

    【讨论】:

      【解决方案9】:

      每个 C++ 开发人员都应该听说过(臭名昭著的)Three star programmer

      而且似乎真的有一些神奇的“指针屏障”需要伪装

      来自 C2 的引述:

      三星级程序员

      C 程序员的评分系统。您的指针越间接(即变量前的“*”越多),您的声誉就越高。 No-star C 程序员几乎不存在,因为几乎所有重要的程序都需要使用指针。大多数是一星程序员。在过去(嗯,我还年轻,所以至少在我看来这些看起来像旧时代),人们偶尔会发现一段由三星级程序员完成的代码,并且敬畏地颤抖。 有些人甚至声称他们在不止一个间接级别上看到了包含函数指针的三星级代码。对我来说听起来像不明飞行物一样真实。

      【讨论】:

        【解决方案10】:

        请注意,这里有两个可能的问题:我们可以在 C 类型中实现多少级指针间接,以及我们可以将多少级指针间接填充到单个声明符中。

        C 标准允许对前者施加最大值(并为此给出最小值)。但这可以通过多个 typedef 声明来规避:

        typedef int *type0;
        typedef type0 *type1;
        typedef type1 *type2; /* etc */
        

        因此,归根结底,这是一个实现问题,与 C 程序在被拒绝之前可以制作多大/复杂的想法有关,这是非常特定于编译器的。

        【讨论】:

          【解决方案11】:

          我想指出,生成具有任意数量 * 的类型是模板元编程可能发生的事情。我忘记了我到底在做什么,但有人建议我可以通过使用 recursive T* 类型来生成新的不同类型,这些类型在它们之间具有某种元操作。

          模板元编程是一种缓慢下降到疯狂的过程,因此在生成具有数千个间接级别的类型时没有必要找借口。这只是一种将 peano 整数映射到模板扩展的便捷方式,例如,作为一种函数式语言。

          【讨论】:

            【解决方案12】:

            2004 MISRA C 标准的规则 17.5 禁止超过 2 级的指针间接。

            【讨论】:

            • 很确定这是对程序员的建议,而不是对编译器的建议。
            • 我阅读了关于超过 2 级指针间接的规则 17.5 的文档。而且它不一定禁止超过2个级别。它确实声明应该遵循该裁决,因为超过 2 个级别是"non-compliant" 符合他们的标准。他们裁决中的重要词或短语是使用此声明中的词"should"Use of more than 2 levels of indirection can seriously impair the ability to understand the behavior of the code, and should therefore be avoided. 这些是该组织制定的指导方针,而不是语言标准制定的规则。
            【解决方案13】:

            没有像真正的限制这样的东西,但限制是存在的。所有指针都是变量,通常存储在堆栈而不是堆中。堆栈通常很小(在某些链接期间可以更改其大小)。所以假设你有 4MB 堆栈,这是相当正常的大小。假设我们有一个 4 字节大小的指针(指针大小因架构、目标和编译器设置而异)。

            在这种情况下,4 MB / 4 b = 1024 可能的最大数量为 1048576,但我们不应忽视堆栈中还有其他一些东西的事实。

            然而,一些编译器可能有最大数量的指针链,但限制是堆栈大小。因此,如果您在与无穷大链接期间增加堆栈大小并拥有运行具有无穷大内存的机器来运行处理该内存的操作系统,那么您将拥有无限的指针链。

            如果您使用int *ptr = new int; 并将您的指针放入堆中,那么通常方式限制将是堆大小,而不是堆栈。

            编辑只要意识到infinity / 2 = infinity。如果机器有更多内存,则指针大小会增加。所以如果内存是无穷大并且指针的大小是无穷大,那么这是个坏消息...... :)

            【讨论】:

            • A) 指针可以存储在堆上 (new int*)。 B) int*int********** 具有相同的大小,至少在合理的架构上是这样。
            • @rightfold A) 是的,指针可以存储在堆中。但这将是非常不同的事情,例如创建包含指向下一个前一个指针的指针的容器。 B) 当然int*int********** 大小相同,我没说他们有不同。
            • 那么我看不出堆栈大小有什么关系。
            • @rightfold 我一直在考虑通常的数据分布方式,当所有数据都在堆和堆栈中时,它只是指向该数据的指针。这将是通常的方式,但我同意可以将指针放入堆栈。
            • “当然 int* 和 int********** 具有相同的大小” - 标准不保证这一点(尽管我不知道它所在的平台'真)。
            【解决方案14】:

            这取决于您存储指针的位置。如果它们在堆栈中,则您的限制相当低。如果将其存储在堆中,则限制要高得多。

            看看这个程序:

            #include <iostream>
            
            const int CBlockSize = 1048576;
            
            int main() 
            {
                int number = 0;
                int** ptr = new int*[CBlockSize];
            
                ptr[0] = &number;
            
                for (int i = 1; i < CBlockSize; ++i)
                    ptr[i] = reinterpret_cast<int *> (&ptr[i - 1]);
            
                for (int i = CBlockSize-1; i >= 0; --i)
                    std::cout << i << " " << (int)ptr[i] << "->" << *ptr[i] << std::endl;
            
                return 0;
            }
            

            它创建 1M 指针,并在显示什么指向什么时很容易注意到链指向第一个变量 number

            顺便说一句。它使用92K 的 RAM,所以想象一下你能走多深。

            【讨论】:

              猜你喜欢
              • 2020-06-24
              • 1970-01-01
              • 2014-10-30
              • 2019-09-08
              • 1970-01-01
              • 2023-03-23
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多