【问题标题】:Pointers to pointers vs. normal pointers指向指针的指针与普通指针
【发布时间】:2016-10-30 19:50:11
【问题描述】:

指针的目的是保存特定变量的地址。那么下面代码的内存结构应该是这样的:

int a = 5;
int *b = &a;

……内存地址……值
一个 ... 0x000002 ................... 5
b ... 0x000010 ................... 0x000002

好的,好的。然后假设现在我要保存指针 *b 的地址。那么我们一般定义一个双指针,**c,为

int a = 5;
int *b = &a;
int **c = &b;

那么内存结构是这样的:

……内存地址……值
一个 ... 0x000002 ................... 5
b ... 0x000010 ................... 0x000002
c ... 0x000020 ................... 0x000010

所以**c指的是*b的地址。

现在我的问题是,为什么这种类型的代码,

int a = 5;
int *b = &a;
int *c = &b;

生成警告?

如果指针的目的只是为了保存内存地址,我认为如果我们要保存的地址是指变量、指针、双指针等,应该没有层次结构,所以下面的类型的代码应该是有效的。

int a = 5;
int *b = &a;
int *c = &b;
int *d = &c;
int *e = &d;
int *f = &e;

【问题讨论】:

  • 除了这么多好的答案,我可以发表一个简单的评论。 Clang 编译器在尝试编译代码中有问题的部分时会发出这个明确的警告:warning: incompatible pointer types initializing 'int *' with an expression of type 'int **'; remove & [-Wincompatible-pointer-types]。这可能已经说明了一切。
  • 初学者经常感到困惑,因为他们认为“地址”本身就是一种数据类型。他们不是。 X 类型数据的地址是。对于不同的类型,它们是不同的。这使您相信 int * 和 int ** 是相同的。
  • "如果指针的目的只是为了保存内存地址",则不是。指针的目的是保存对象的“内存地址”及其类型。只需开始取消引用指针,您就会看到。
  • 抱歉,不要失礼,只是想知道,是什么让 this 问题如此有用?这在任何中等的 C 书,指针第二章或第三篇文章中都有,更不用说,在 SO 中多次讨论过。我错过了什么明显的东西吗?
  • @SouravGhosh 这家伙可能通过从一些人口众多的初学者论坛链接来提升他的代表问题

标签: c pointers


【解决方案1】:

int a = 5;
int *b = &a;   
int *c = &b;

您收到警告,因为&b 的类型为int **,并且您尝试初始化int * 类型的变量。这两种类型之间没有隐式转换,导致警告。

举个你想工作的更长的例子,如果我们尝试取消引用f,编译器会给我们一个int,而不是我们可以进一步取消引用的指针。

另请注意,在许多系统上,intint* 的大小不同(例如,指针可能是 64 位长,int 可能是 32 位长)。如果你取消引用f 并得到一个int,你会丢失一半的值,然后你甚至不能将它转换为一个有效的指针。

【讨论】:

  • 在为 C 设计的系统(以及一些现代嵌入式系统)上,同一程序中有不同类型的指针 - 例如,近指针和远指针,或数据和代码指针. Pointer 不是 int 值,人们;不要再假装它只是因为它“大部分都有效”
  • @Luaan:C 是为早期的 DEC 小型计算机设计的,在 1970 年代的 PDP-11 上最常见。这些机器没有“近”和“远”指针。 “Near”和“far”是 Microsoft 对 C 的扩展,用于支持 IBM PC 上脑死的 Intel 8086/8088 分段架构。 (在 PC 项目开始时,IBM 正在另一个站点生产和销售基于摩托罗拉 68000 的实验室计算机。想象一下,如果这两个站点进行了交谈,那么仅使用阿司匹林就可以节省开支,避免了细分市场的麻烦......)跨度>
  • @Luaan 实际上不是真的。直到其他供应商和 ANSI/ISO 开始着手解决这种可移植性问题。 1972 C 对交换指针和整数非常乐观;两者都是 16 位的,指针是平的。 void * 甚至不存在,你总是可以使用 char * :)
【解决方案2】:

如果指针的目的只是为了保存内存地址,我想 如果我们要保存的地址应该没有层次结构 指变量,指针,双指针,...等

在运行时,是的,指针只保存一个地址。但是在编译时,每个变量都有一个类型。正如其他人所说,int*int** 是两种不同的、不兼容的类型。

有一种类型,void*,可以满足您的需求:它只存储一个地址,您可以为其分配任何地址:

int a = 5;
int *b = &a;
void *c = &b;

但是当您想取消引用 void* 时,您需要自己提供“缺失”类型信息:

int a2 = **((int**)c);

【讨论】:

  • 如果我理解正确的话,指针的类型告诉 CPU 从引用内存地址开始读取了多少内存。所以表达式'int * b = &a; printf("%d', * b); ' 表示从a的地址开始,我们读取4个字节。这是可能的,因为我们将int的大小定义为4个字节,并打印那个整数。但是大小是多少int*, int**, int***?这是否因系统而异?那么是什么(编译器、cpu 或其他?)定义了它的大小?
  • 字体大小只是一个考虑因素。还有其他的。
  • 考虑一个指向某物 p 的指针。现在假设您要存储 p 指向的值。诠释 i = *p;编译器需要知道指针的类型。在内存中,double 看起来与 int 有很大不同。这与指针的大小无关,而是它应该如何对待所指向的数据。
  • @user42298 指针的大小通常取决于 CPU 和编译器。为 64 位编译的程序,在 64 位 CPU 上运行,具有 64 位宽的指针。但是您也可以编译 32 位并在 64 位 CPU 上运行它,然后指针是 32 位的,操作系统会负责正确运行。顺便说一句,int 也可以有不同的大小,但int32_t 保证正好有 32 位。
  • 这是最全面的答案。
【解决方案3】:

现在我的问题是,为什么这种类型的代码,

int a = 5; 
int *b = &a; 
int *c = &b; 

生成警告?

你需要回到基础。

  • 变量有类型
  • 变量保存值
  • 指针就是值
  • 指针指向变量
  • 如果p是一个指针值那么*p是一个变量
  • 如果v 是一个变量,那么&v 是一个指针

现在我们可以找出您发帖中的所有错误。

那么假设现在我要保存指针*b的地址

没有。 *b 是一个 int 类型的变量。它不是指针。 b 是一个变量,其 value 是一个指针。 *b 是一个变量,它的值是一个整数。

**c指的是*b的地址。

不不不。绝对不。如果您要理解指针,必须正确理解这一点。

*b 是一个变量;它是变量a 的别名。变量a的地址就是变量b的值。 **c 不是指a 的地址。相反,它是一个变量,它是变量a别名。 (*b 也是如此。)

正确的说法是:变量cb地址。或者,等效地:c 的值是一个指向 b 的指针。

我们怎么知道这个?回到基本面。你说c = &b。那么c 的值是多少?一个指针。到什么? b

确保您完全了解基本规则。

现在您希望了解变量和指针之间的正确关系,您应该能够回答您的代码为什么会出错的问题。

【讨论】:

  • 我认为每次 OP 在帖子中说 b 或 **c 时,他们真的只是想说 b 和 c。你涵盖了很多,但并没有真正涵盖问题的最后一部分(“为什么 int 不能指向另一个 int*”),这正是 OP 真正想问 IMO 的。跨度>
  • 让解引用运算符和'是指针'限定符成为同一个符号的天才想法,无论如何......
  • @mbrig:这是丹尼斯·里奇的天才想法。如果不清楚为什么这是一个天才的想法,那么你就没有在精神上正确地解析语言。当我们说int * b; 时,我们只是说“bint* 类型的变量。我们还说*bint 类型的变量。这里的天才想法是,您可以在心理上将其视为int* bint *b,并且任何一种解释都是正确的。
  • 是的,我查看了一些相关的问题,这对我来说真的很有意义,这是有史以来第一次。不过我还是有点怀疑。
【解决方案4】:

如果你想得到一个正确的警告并且你想编译代码,C 的类型系统需要这个。仅使用一级深度的指针,您将无法知道指针是指向指针还是实际整数。

如果您取消引用类型int**,您知道您得到的类型是int*,同样如果您取消引用int*,类型是int。根据您的建议,类型将是模棱两可的。

从你的例子来看,不可能知道c是指向int还是int*

c = rand() % 2 == 0 ? &a : &b;

c 指向什么类型?编译器不知道,所以下一行是不可能执行的:

*c;

在 C 中,所有类型信息在编译后都会丢失,因为每种类型都在编译时进行检查并且不再需要。您的建议实际上会浪费内存和时间,因为每个指针都必须有关于指针中包含的类型的额外运行时信息。

【讨论】:

    【解决方案5】:

    指针是具有附加类型语义的内存地址的抽象,并且在像 C 类型这样的语言中很重要。

    首先,不能保证int *int ** 具有相同的大小或表示形式(在现代桌面架构上它们确实如此,但您不能相信它是普遍正确的)。

    其次,类型对于指针运算很重要。给定一个T * 类型的指针p,表达式p + 1 产生下一个T 类型的object 的地址。因此,假设以下声明:

    char  *cp     = 0x1000;
    short *sp     = 0x1000;  // assume 16-bit short
    int   *ip     = 0x1000;  // assume 32-bit int
    long  *lp     = 0x1000;  // assume 64-bit long
    

    表达式cp + 1 为我们提供了下一个char 对象的地址,即0x1001。表达式sp + 1 为我们提供了下一个short 对象的地址,即0x1002ip + 1 给我们0x1004lp + 1 给我们0x1008

    所以,给定

    int a = 5;
    int *b = &a;
    int **c = &b;
    

    b + 1 给我们下一个int 的地址,c + 1 给我们下一个指针int 的地址。

    如果您希望函数写入指针类型的参数,则需要指针到指针。取以下代码:

    void foo( T *p )    
    {
      *p = new_value(); // write new value to whatever p points to
    }
    
    void bar( void )
    {
      T val;
      foo( &val );     // update contents of val
    }
    

    这适用于任何类型的T。如果我们用指针类型P *替换T,代码就变成了

    void foo( P **p )    
    {
      *p = new_value(); // write new value to whatever p points to
    }
    
    void bar( void )
    {
      P *val;
      foo( &val );     // update contents of val
    }
    

    语义完全一样,只是类型不同;形式参数p 总是比变量val 多一级间接。

    【讨论】:

      【解决方案6】:

      如果我们要保存的地址引用变量、指针、双指针,我认为应该没有层次结构

      如果没有“层次结构”,很容易在没有任何警告的情况下生成 UB - 这太可怕了。

      考虑一下:

      char c = 'a';
      char* pc = &c;
      char** ppc = &pc;
      printf("%c\n", **ppc);   // compiles ok and is valid
      printf("%c\n", **pc);    // error: invalid type argument of unary ‘*’
      

      编译器给我一个错误,从而帮助我知道我做错了什么,我可以纠正错误。

      但没有“层次结构”,例如:

      char c = 'a';
      char* pc = &c;
      char* ppc = &pc;
      printf("%c\n", **ppc);   // compiles ok and is valid
      printf("%c\n", **pc);    // compiles ok but is invalid
      

      编译器不会给出任何错误,因为没有“层次结构”。

      但是当行:

      printf("%c\n", **pc);
      

      执行,它是 UB(未定义行为)。

      首先*pc 读取char 就好像它是一个指针,即可能读取 4 或 8 个字节,即使我们只保留 1 个字节。那是UB。

      如果程序没有因为上面的 UB 而崩溃,只是返回了一些乱码,那么第二步就是取消对乱码的引用。再次UB。

      结论

      类型系统通过将 int*、int**、int*** 等视为不同的类型来帮助我们检测错误。

      【讨论】:

        【解决方案7】:

        如果指针的目的只是为了保存内存地址,我认为如果我们要保存的地址引用变量、指针、双指针等应该没有层次结构,所以下面的代码类型应该有效。

        我认为这是你的误解:指针本身的目的是存储内存地址,但指针通常也有一个类型,以便我们知道它指向的地方会发生什么。

        特别是,不像你,其他人真的希望有这种层次结构,以便知道如何处理指针指向的内存内容。

        附加类型信息是 C 指针系统的关键。

        如果你这样做了

        int a = 5;
        

        &a 意味着你得到的是一个int *,所以如果你取消引用它又是一个int

        将其提升到一个新的水平,

        int *b = &a;
        int **c = &b;
        

        &b 也是一个指针。但不知道背后隐藏着什么,分别。它指向什么,它是无用的。重要的是要知道取消引用指针会显示原始类型的类型,因此 *(&b)int ***(&b) 是我们使用的原始 int 值。

        如果您觉得在您的情况下不应该有类型的层次结构,您可以随时使用void *,尽管直接可用性非常有限。

        【讨论】:

          【解决方案8】:

          如果指针的目的只是为了保存内存地址,我认为如果我们要保存的地址引用变量、指针、双指针等应该没有层次结构,所以下面的代码类型应该有效。

          这对机器来说是正确的(毕竟大致上一切都是数字)。但是在许多语言中,变量是类型化的,这意味着编译器可以确保您正确使用它们(类型对变量施加正确的上下文)

          确实,指向指针的指针和指针(可能)使用相同数量的内存来存储它们的值(注意对于 int 和指向 int 的指针不是这样,地址的大小与房子的大小)。

          因此,如果您有一个地址的地址,您应该按原样使用而不是作为简单地址,因为如果您将指向指针的指针作为简单指针访问,那么您将能够像处理 int 的地址一样是一个 int,它不是(替换 int 没有其他任何东西,你应该看到危险)。您可能会感到困惑,因为所有这些都是数字,但在日常生活中您不会:我个人对 1 美元和 1 条狗有很大的影响。 dog 和 $ 是类型,你知道你可以用它们做什么。

          你可以在汇编中编程并制作你想要的东西,但你会发现它是多么危险,因为你几乎可以做你想做的事,尤其是奇怪的事情。是的,修改地址值是危险的,假设您有一辆自动驾驶汽车,它应该在距离表示的地址交付东西:1200 memory street (address) 并假设街道房屋相隔 100ft(1221 是无效地址),如果您能够以整数形式随意操作地址,您将能够尝试在 1223 进行投递,并将数据包放在人行道中间。

          另一个示例可以是房屋、房屋地址、该地址的地址簿中的条目号。这三个都是不同的概念,不同的类型......

          【讨论】:

          • 机器也不一定如此。在旧系统(和一些现代嵌入式系统)中,您有不同类型的指针 - 例如,x86 架构具有近(16 位)和远(32 位)指针。 C 对您隐藏(抽象)了这个事实,但对于应用程序以 16 位模式在 x86 计算机上运行至关重要。还有其他示例,例如分段模式(段+偏移,其中空指针在实际机器代码中不为零)。 C 没有很多高级抽象,但它有很多低级抽象——这就是 C 的全部目的。
          • @Luaan:有很多机器指向函数的指针和指向数据的指针是不同的;存在指针的目标类型影响其大小的机器,但很少见。如果 C 定义了一个可选的“指向任何类型的数据指针”类型,这将很有用,该类型将在所有类型的数据使用相同表示的实现上定义,因为目前是编写可以使用的函数的唯一方法所有这些指针(例如,为了排序)都是使用 memcpy、memmove 或字符类型来操作它们,并使用这些技术中的任何一种......
          • ...将要求编译器将双间接指针视为可能对每种类型的所有内容(包括字符、整数和浮点值)进行别名处理,而不仅仅是指针,但传统上,C 标准一直不愿定义任何无法在所有实现中处理的内容。
          • 所有这一切都是完全正确的,我知道,有时需要简化一些事情。我的意思是,在原始机器中,事情没有语言那么严格,而且在使用语言时我们不能像在原始机器中那样思考。
          【解决方案9】:

          有不同的类型。这是有充分理由的:

          拥有……

          int a = 5;
          int *b = &a;
          int **c = &b;
          

          …表达式…

          *b * 5
          

          … 是有效的,而表达式…

          *c * 5
          

          没有意义。

          重要的不是,如何存储指针或指向指针的指针,而是指向什么

          【讨论】:

            【解决方案10】:

            C 语言是强类型的。这意味着,对于每个地址,都有一个 type,它告诉编译器如何解释该地址的值。

            在你的例子中:

            int a = 5;
            int *b = &a;
            

            a的类型是intb的类型是int *(读作“指向int的指针”)。使用您的示例,内存将包含:

            ..... memory address ...... value ........ type
            a ... 0x00000002 .......... 5 ............ int
            b ... 0x00000010 .......... 0x00000002 ... int*
            

            类型实际上并没有存储在内存中,只是编译器知道,当你阅读a时,你会找到一个int,而当你阅读b时,你'将找到可以找到int的地方的地址。

            在你的第二个例子中:

            int a = 5;
            int *b = &a;
            int **c = &b;
            

            c 的类型是int **,读作“指向int 的指针”。这意味着,对于编译器:

            • c 是一个指针;
            • 当你读取c时,你会得到另一个指针的地址;
            • 当您读取其他指针时,您将获得 int 的地址。

            也就是说,

            • c 是一个指针 (int **);
            • *c 也是一个指针(int *);
            • **cint

            记忆中会包含:

            ..... memory address ...... value ........ type
            a ... 0x00000002 .......... 5 ............ int
            b ... 0x00000010 .......... 0x00000002 ... int*
            c ... 0x00000020 .......... 0x00000010 ... int**
            

            由于“类型”不与值一起存储,并且指针可以指向任何内存地址,编译器知道某个地址的值类型的方式基本上是通过获取指针的类型,并删除最右边*


            顺便说一下,这是针对常见的 32 位架构。对于大多数 64 位架构,您将拥有:

            ..... memory address .............. value ................ type
            a ... 0x0000000000000002 .......... 5 .................... int
            b ... 0x0000000000000010 .......... 0x0000000000000002 ... int*
            c ... 0x0000000000000020 .......... 0x0000000000000010 ... int**
            

            现在每个地址都是 8 个字节,而 int 仍然只有 4 个字节。由于编译器知道每个变量的类型,它可以很容易地处理这种差异,并且读取 8 个字节的指针和 4 个字节的int

            【讨论】:

              【解决方案11】:

              为什么这种类型的代码会产生警告?

              int a = 5;
              int *b = &a;   
              int *c = &b;
              

              & 运算符产生一个指向对象的指针,即&a 的类型为int *,因此将其分配(通过初始化)给b,它也是int * 的类型是有效的。 &b 产生一个指向对象b 的指针,即&b 是指向int * 的类型指针,即int **

              C 在赋值运算符的约束(初始化时保持)中说(C11,6.5.16.1p1):“两个操作数都是指向兼容类型的合格或不合格版本的指针” .但在 C 定义中,兼容类型int **int *不是兼容类型。

              因此,int *c = &b; 初始化中存在约束冲突,这意味着编译器需要进行诊断。

              这里规则的一个基本原理是标准不保证两种不同的指针类型具有相同的大小(void * 和字符指针类型除外),即sizeof (int *) 和@987654337 @ 可以是不同的值。

              【讨论】:

                【解决方案12】:

                这是因为任何指针 T* 实际上是 pointer to a T(或 address of a T)类型,其中 T 是指向的类型。在这种情况下,* 可以读作pointer to a(n)T 是指向的类型。

                int     x; // Holds an integer.
                           // Is type "int".
                           // Not a pointer; T is nonexistent.
                int   *px; // Holds the address of an integer.
                           // Is type "pointer to an int".
                           // T is: int
                int **pxx; // Holds the address of a pointer to an integer.
                           // Is type "pointer to a pointer to an int".
                           // T is: int*
                

                这用于解引用目的,其中解引用运算符将​​采用T*,并返回一个类型为T 的值。返回类型可以看作是截断最左边的“指向 a(n) 的指针”,并且是剩下的。

                  *x; // Invalid: x isn't a pointer.
                      // Even if a compiler allows it, this is a bad idea.
                 *px; // Valid: px is "pointer to int".
                      // Return type is: int
                      // Truncates leftmost "pointer to" part, and returns an "int".
                *pxx; // Valid: pxx is "pointer to pointer to int".
                      // Return type is: int*
                      // Truncates leftmost "pointer to" part, and returns a "pointer to int".
                

                请注意,对于上述每个操作,取消引用运算符的返回类型如何匹配原始 T* 声明的 T 类型。

                这极大地帮助原始编译器和程序员解析指针的类型:对于编译器,地址运算符将* 添加到类型,取消引用运算符从类型中删除*,以及任何不匹配是一个错误。对于程序员来说,*s 的数量直接表明您正在处理多少个间接级别(int* 总是指向 intfloat** 总是指向 float*,而这又总是指向 float*指向float等)。


                现在,考虑到这一点,仅使用单个 * 而不管间接级别的数量有两个主要问题:

                1. 编译器要取消引用指针要困难得多,因为它必须重新引用最近的赋值来确定间接级别,并适当地确定返回类型。
                2. 指针对程序员来说更难理解,因为很容易忘记有多少间接层。

                在这两种情况下,确定值的实际类型的唯一方法是回溯它,迫使您寻找其他地方来找到它。

                void f(int* pi);
                
                int main() {
                    int x;
                    int *px = &x;
                    int *ppx = &px;
                    int *pppx = &ppx;
                
                    f(pppx);
                }
                
                // Ten million lines later...
                
                void f(int* pi) {
                    int i = *pi; // Well, we're boned.
                    // To see what's wrong, see main().
                }
                

                这是一个非常危险的问题,通过*s的数量直接代表间接级别很容易解决。

                【讨论】:

                  猜你喜欢
                  • 1970-01-01
                  • 2011-08-04
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 2017-09-03
                  • 2021-12-04
                  • 2018-04-05
                  • 2016-01-27
                  相关资源
                  最近更新 更多