【问题标题】:How are array and pointer types handled internally in C compilers? ( int *a; vs. int a[]; )C 编译器内部如何处理数组和指针类型? ( int *a; 与 int a[]; )
【发布时间】:2009-09-03 11:19:02
【问题描述】:

我需要一位有权威来源的语言律师。

看看下面在 gcc 下编译干净的测试程序:

#include <stdio.h>


void foo(int *a) {
    a[98] = 0xFEADFACE;
}

void bar(int b[]) {
    *(b+498) = 0xFEADFACE;
}

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

int a[100], b[500], *a_p;

*(a+99) = 0xDEADBEEF;
*(b+499) = *(a+99);

foo(a);
bar(b);

printf("a[98] == %X\na[99] == %X\n", a[98], a[99]);
printf("b[498] == %X\nb[499] == %X\n", b[498], b[499]);

a_p = a+98;
*a_p = 0xDEADFACE;

printf("a[98] == %X\na[99] == %X\n", a[98], a[99]);

}

它会产生我期望的输出:

anon@anon:~/study/test_code$ gcc arrayType.c -o arrayType
anon@anon:~/study/test_code$ ./arrayType 
a[98] == FEADFACE
a[99] == DEADBEEF
b[498] == FEADFACE
b[499] == DEADBEEF
a[98] == DEADFACE
a[99] == DEADBEEF

a 和 b 是同一类型吗? int *a 是否在编译器内部处理为与 int a[] 相同的类型?

从实际的角度来看,int a[100], b[500], *a_p, b_a[]; 似乎都是同一类型。在我上面的示例中,我很难相信编译器会在各种情况下不断调整这些类型。我很高兴被证明错了。

有人可以明确详细地为我解决这个问题吗?

【问题讨论】:

    标签: c arrays pointers language-lawyer


    【解决方案1】:

    a 和 b 是同一类型吗? int *a 是否在编译器内部处理为与 int a[] 相同的类型?

    来自the comp.lang.C FAQ

    ...只要数组出现在表达式中,编译器就会隐式生成指向数组第一个元素的指针,就像程序员编写了 &a[0] 一样。 (例外情况是数组是 sizeof 或 & 运算符的操作数,或者是字符数组的字符串字面量初始值设定项...)

    ... 给定一个数组 a 和指针 p,a[i] 形式的表达式导致数组衰减为一个指针,遵循上面的规则,然后像指针变量一样被下标表达式 p[i] (尽管最终的内存访问会有所不同...

    Given declarations of

    char a[] = "hello";
    char *p = "world";
    

    ...当编译器看到表达式a[3] 时,它会发出代码以从位置a 开始,将其移动三个,然后从那里获取字符。当它看到表达式p[3] 时,它会发出代码以从位置p 开始,获取那里的指针值,将指针加3,最后获取指向的字符。 换句话说,a[3] 比名为 a 的对象高 3 位,而p[3]p 指向的对象高 3 位。

    重点是我的。最大的区别似乎是指针是指针的时候会被获取,而如果是数组则没有指针获取。

    【讨论】:

    • 链接中的这句话最简洁地说:“它是指针算术和数组索引 [that] 在 C 中是等价的,指针和数组是不同的。”
    • 似乎数组更有效,因为它们节省了指针的内存获取。似乎a 本身就是“h”的地址,而p 是包含“w”地址的位置的地址。这就是我试图理解的区别。从这个意义上说,数组a 实际上更接近于像int a; 这样的声明而不是int *a; 现在这一切都说得通了。
    【解决方案2】:

    其中一个区别 - int a[x][y]int **a 不可互换。

    http://www.lysator.liu.se/c/c-faq/c-2.html

    2.10:

    数组数组(即 C 中的二维数组)衰减为指向数组的指针,而不是指向指针的指针。

    【讨论】:

      【解决方案3】:

      ab 都是整数数组。 a[0] 不是包含内存地址的内存位置,而是包含 int 的内存位置。

      数组和指针既不完全相同也不可互换。当表达式中出现的 T 数组类型的左值衰减(三个例外)为指向其第一个元素的指针时,数组等效 到指针 iff;结果指针的类型是指向 T 的指针。在查看相关代码的汇编输出时,这一点变得很清楚。三个例外,fyi,是当数组是 sizeof& 的操作数或字符数组的文字字符串初始值设定项时。

      如果你想这样:

      char a[] = "hello";
      char *p = "world";
      

      会产生可以这样表示的数据结构:

         +---+---+---+---+---+---+
      a: | h | e | l | l | o |\0 |
         +---+---+---+---+---+---+
      
         +-----+     +---+---+---+---+---+---+
      p: |  *======> | w | o | r | l | d |\0 |
         +-----+     +---+---+---+---+---+---+
      

      并意识到像 x[3] 这样的引用会产生不同的代码,具体取决于 x 是指针还是数组。编译器的 a[3] 意味着:从位置 a 开始并移动三个过去并在那里获取字符。 p[3] 表示转到位置 p,取消对那里的值的引用,移动三个过去并在那里获取字符。

      【讨论】:

        【解决方案4】:

        来自C language standard

        6.3.2.1.3 除非是 sizeof 运算符的操作数或 一元 & 运算符,或者是用于初始化的字符串文字 一个数组,一个具有“类型数组”类型的表达式是 转换为类型为“类型指针”的表达式 指向数组对象的初始元素,而不是 一个左值。如果数组对象有寄存器存储类,则 行为未定义。

        假设以下代码:

        #include <stdio.h>
        #include <string.h>
        int main(void)
        {
          char foo[10] = {0};
          char *p = foo;
          foo[0] = 'b';
          *(foo + 1) = 'a';
          strcat(foo, "t");
          printf("foo = %s, &foo = %p, &p = %p, sizeof foo = %lu, sizeof p = %lu\n", 
            foo, &foo, &p, (unsigned long) sizeof foo, (unsigned long) sizeof p);
          return 0;
        }
        

        foo 被声明为 char 的 10 元素数组,所有元素都初始化为 0。p 被声明为指向 char 的指针,并被初始化为指向 foo。

        排队

        char *p = foo;
        

        表达式 foo 的类型为“10-element array of char”;由于 foo 不是 sizeof 或 & 的操作数,也不是用于初始化数组的字符串字面量,因此其类型被隐式转换为“指向 char 的指针”并设置为指向数组的第一个元素。这个指针值被复制到 p。

        在行中

        foo[0] = 'b';
        *(foo + 1) = 'a';
        

        表达式 foo 的类型为“10-element array of char”;由于 foo 不是 sizeof 或 & 的操作数,也不是用于初始化数组的字符串字面量,因此其类型被隐式转换为“指向 char 的指针”并设置为指向数组的第一个元素。下标表达式被解释为“`*(foo + 0)”。

        排队

        strcat(foo, "t");
        

        foo 的类型为“10-element array of char”,字符串文字“t”的类型为“2-element array of char”;因为既不是 sizeof 也不是 & 的操作数,虽然 "t" 是字符串文字,但它不用于初始化数组,两者都被隐式转换为类型 "pointer to char",并且指针值被传递给strcat().

        排队

          printf("foo = %s, &foo = %p, &p = %p, sizeof foo = %lu, sizeof p = %lu\n", 
            foo, &foo, &p, (unsigned long) sizeof foo, (unsigned long) sizeof p);
        

        foo 的第一个实例被转换为指向 char 的指针,如上所述。 foo 的第二个实例是 & 运算符的操作数,因此它的类型 not 转换为“指向 char 的指针”,而表达式“&foo”的类型是“指向 10 元素数组的指针”字符”或“字符 (*)[10]”。将此与表达式“&p”的类型类型进行比较,即“指向 char 指针的指针”或“char **”。 foo 的第三个实例是 sizeof 运算符的操作数,因此它的类型再次转换,并且 sizeof 返回分配给数组的字节数。将此与 sizeof p 的结果进行比较,后者返回分配给指针的字节数。

        每当有人告诉您“数组只是一个指针”时,他们就是在混淆上面引用的标准中的部分。数组不是指针,指针也不是数组;但是,在许多情况下,您可以将数组视为它是一个指针,并且您可以将一个指针视为它是一个数组。在第 6、7 和 8 行中,“p”可以替换“foo”。但是,它们不能作为 sizeof 或 & 的操作数互换。

        编辑:顺便说一句,作为函数参数,

        void foo(int *a);
        

        void foo(int a[]);
        

        是等价的。 “a[]”被解释为“*a”。请注意,这适用于函数参数。

        【讨论】:

        • +1 您和马克都回答了问题的不同方面。如果我可以同时接受这两个答案,但 Marks 的答案更能解决我遇到的问题的根源,即访问数组与指针时生成的代码有什么区别。谢谢!
        • @Spidey:章节和诗句,拜托。根据 6.7.6.3/7:“将参数声明为 ''array of type'' 应调整为 ''qualified pointer to type'',其中类型限定符(如果有)是在数组类型派生的 [] 中指定的那些。"
        【解决方案5】:

        看这里:

        2.2:但是我听说 char a[] 和 char *a 是一样的。

        http://www.lysator.liu.se/c/c-faq/c-2.html

        【讨论】:

          【解决方案6】:

          我同意 sepp2k 的回答和 Mark Rushakoff 的 comp.lang.c 常见问题解答报价。让我添加两个声明之间的一些重要区别和一个常见的陷阱。

          1. 当您将 a 定义为数组时(在函数参数以外的上下文中,这是一种特殊情况),您不能编写 a = 0; 要么 一个++; 因为a 不是左值(可以出现在赋值运算符左侧的值)。

          2. 数组定义保留空间,而指针没有。因此,sizeof(array) 将返回存储所有数组元素所需的内存空间(例如,对于 32 位架构上的 10 个整数的数组,10 乘以 4 个字节),而 sizeof(pointer) 将只返回所需的内存空间存储该指针(例如 64 位架构中的 8 个字节)。

          3. 当您添加指针或添加数组声明时,事情肯定会有所不同。例如,int **a 是一个指向整数的指针。它可以用作二维数组(具有不同大小的行),方法是分配一个指向行的指针数组并将每个指针指向内存以存储整数。要访问a[2][3],编译器将获取a[2] 中的指针,然后将三个元素移过它指向的位置以访问该值。将此与b[10][20] 进行对比,b[10][20] 是一个由 10 个元素组成的数组,每个元素都是一个由 20 个整数组成的数组。要访问b[2][3],编译器将通过将 2 乘以 20 个整数的大小并再加上 3 个整数的大小来偏移数组内存区域的开头。

          最后,考虑一下这个陷阱。如果你有一个 C 文件

          int a[10];
          

          在另一个

          extern int *a;
          a[0] = 42;
          

          文件将编译和链接而不会出现错误,但代码不会按照您的预期执行;它可能会因空指针分配而崩溃。原因是在第二个文件中a是一个指针,它的值是第一个文件的a[0]的内容,即最初为0。

          【讨论】:

            【解决方案7】:

            您的示例中有两个 a 和两个 b。

            作为参数

            void foo(int *a) {
                a[98] = 0xFEADFACE;
            }
            
            void bar(int b[]) {
                *(b+498) = 0xFEADFACE;
            }
            

            a 和 b 属于同一类型:指向 int 的指针。

            作为变量

            int *a;
            int b[10];
            

            不是同一时间。第一个是指针,第二个是数组。

            数组行为

            一个数组(变量或非变量)在大多数情况下都被隐式转换 指向其第一个元素的指针中的上下文。 C中的两个上下文 未完成的是 sizeof 的参数和 &amp; 的参数;在 C++ 中有 一些与参考参数和模板相关的更多内容。

            我写了,变量与否,因为转换不只是为了 变量,一些例子:

            int foo[10][10];
            int (*bar)[10];
            
            • foo 是一个由 10 个 10 个整数组成的数组。在大多数情况下,它将是 转换为指向其第一个元素的指针,类型为 指向数组的指针 10 int.

            • foo[10] 是一个 10 int 的数组;在大多数情况下,它将是 转换为指向其第一个元素的指针,类型为 pointer to int

            • *bar 是一个 10 个 int 的数组;在大多数情况下,它将是 转换为指向其第一个元素的指针,类型为 pointer to int

            一些历史

            在B中,C的直系祖先,相当于

            int x[10];
            

            具有我们在当前 C 中编写的效果

            int _x[10];
            int *x = &_x;
            

            即它分配了内存并初始化了一个指向它的指针。有些人似乎误解了它在 C 中仍然是正确的。

            在NB中——当C不再是B但还没有被称为C——时,有一段时间 是否声明了指针

            int x[];
            

            但是

            int foo[10];
            

            将具有当前含义。功能参数的调整是 那个时代的遗迹。

            【讨论】:

            • 我相信 int (*bar)[10]; 是指向 10 个整数数组的指针,int *bar[10]; 是一个 10 个整数指针数组。
            • @罗伯特,对。但我写的是 *bar 而不是 bar。我的目标是展示隐式转换数组的示例->指向第一个元素的指针,而不是解释 C 声明语法。
            【解决方案8】:

            a 和 b 是同一类型吗?

            是的。 [编辑:我应该澄清一下:函数 foo 的参数 a 与函数 bar 的参数 b 类型相同。两者都是指向 int 的指针。 main 中的局部变量 a 与 int 中的局部变量 b 类型相同。两者都是整数数组(实际上它们不是同一类型,因为它们的大小不同。但都是数组)。]

            在编译器内部是否将 int *a 处理为与 int a[] 相同的类型?

            通常不会。例外情况是,当您将 foo bar[] 作为函数的参数写入时(就像您在此处所做的那样),它会自动变为 foo *bar

            但是在声明非参数变量时有很大的不同。

            int * a; /* pointer to int. points nowhere in paticular right now */
            int b[10]; /* array of int. Memory for 10 ints has been allocated on the stack */
            foo(a); /* calls foo with parameter `int*` */
            foo(b); /* also calls foo with parameter `int*` because here the name b basically
                       is a pointer to the first elment of the array */
            

            【讨论】:

            • 您能否提供详细信息,来源?为什么编译器不能处理 int *a;int a[]; 相同?据我所知,它们总是可以互换;
            • 最重要的区别是int a[10] 为堆栈上的 10 个 int 保留空间。 int *b 只为一个 int 指针保留空间,因此您要么必须使 b 指向现有数组(或单个 int),要么使用 malloc 在堆上分配内存并使 b 指向它。另一个区别是&amp;a 的类型是“指向十个整数数组的指针”,而&amp;b 是“指向整数指针的指针”。因此,当您添加到 &amp;a 时,您添加了 10*sizeof(int) 字节的偏移量,而添加到 &amp;b 时添加了 sizeof(int*) 字节。
            • int b[10] 为 10 个整数分配空间并创建指向数组的指针。 int b[] 只会创建一个指向任何地方的指针,就像 int * a 一样。
            • 我明白这一切。我的观点是,在实践中 a[100], b[500], *a_p, b_a[]; 都被视为 int 指针。具体来说,它们都只是一个包含内存地址的内存位置。无论使用何种符号来访问数据,您总是会获得内存地址 + sizeof(int) * 下标,然后是取消引用以获取数据。唯一的区别是 a 和 b 实际上是 const 指针,因为它们指向的内存块不能更改。除此之外,它们都是一样的。
            • 罗伯特,这是彻头彻尾的 C。即使使用结构,你真正得到的只是一块内存。
            【解决方案9】:

            不,它们不一样!一个是指向 int 的指针,另一个是 100 个 int 的数组。所以是的,它们是一样的!

            好的,我会试着解释这个愚蠢的。

            *a 和 a[100] 对于你正在做的事情基本上是一样的。但是如果我们仔细看一下编译器的内存处理逻辑,我们的意思是:

            • *a 编译器,我需要内存,但我会告诉你要多久,所以现在冷静一下!
            • a[100] 编译器,我需要内存现在并且我知道我需要 100 个,所以请确保我们有它!

            两者都是指针。您的代码可以对它们一视同仁,并随心所欲地践踏这些指针附近的内存。但是,a[100] 是在编译时分配的指针的连续内存,而 *a 仅分配指针,因为它不知道您何时需要内存(运行时内存噩梦)。

            那么,谁在乎,对吗?好吧,某些功能,例如sizeof() 关心。 sizeof(a) 将为 *aa[100] 返回不同的答案。这在功能上也会有所不同。在这种函数情况下,编译器知道其中的区别,因此您也可以在代码中使用它来发挥自己的优势,例如 for 循环、memcpy 等。继续尝试。

            这是一个很大的问题,但我在这里给出的答案是这样的。编译器知道细微的差别,它会生成大多数时候看起来一样的代码,但在重要的时候会有所不同。由您决定 *a 或 a[100] 对 cimpiler 意味着什么,以及它将在哪里以不同的方式处理它。它们实际上可以相同,但它们并不相同。更糟糕的是,你可以通过调用一个函数来改变整个游戏。

            呼……难怪像 c# 这样的托管代码现在这么火?!

            编辑: 我还应该补充一点,您可以使用*a_p = X,但请尝试使用您的一个数组来做到这一点!数组像指针一样使用内存,但它们不能移动或调整大小。像*a_p 这样的指针可以指向不同的东西。

            【讨论】:

            • 它们不是两个指针。在某些情况下,例如将它们传递给需要指针的函数,两者都被视为指针。
            • 同意。我说了什么不同的?
            【解决方案10】:

            我将把我的帽子扔进戒指来简单解释一下:

            • 数组是一系列连续的 同一类型的存储位置

            • 指针是单个存储位置的地址

            • 获取数组的地址给出 (即指向)它的地址 第一个元素。

            • 数组的元素可以通过 指向数组第一个元素的指针。这 有效,因为下标运算符 [] 以某种方式在指针上定义 旨在促进这一点。

            • 可以传递一个数组 指针参数是预期的,它 会自动转换成 指向第一个元素的指针(尽管 这 不是 对多个递归 指针级别,或 多维数组)。同样,这是设计使然。

            因此,在许多情况下,同一段代码可以操作数组和连续的内存块,因为数组和指向其第一个元素的指针之间的特殊关系而没有分配为数组。然而它们是不同的类型,并且在某些情况下它们的行为确实不同,例如:指向数组的指针与指向指针的指针完全不同。

            这是一个最近的 SO 问题,涉及指针到数组与指针到指针的问题:Whats the difference between "abc" and {"abc"} in C?

            【讨论】:

              【解决方案11】:

              如果你有一个指向字符数组的指针(并且想要获取该数组的大小),你不能使用 sizeof(ptr) 而必须使用 strlen(ptr)+1!

              【讨论】:

              • 仅当字符数组包含正确的以 null 结尾的字符串时。不是每个字符数组都是这样!
              猜你喜欢
              • 2011-01-05
              • 1970-01-01
              • 2017-11-30
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2023-03-19
              • 1970-01-01
              • 2019-05-06
              相关资源
              最近更新 更多