指针与二维数组

5
在下面,有人能解释一下执行时获得的输出吗:
#include<stdio.h>
void main()
{
char a[5][10]={"one","two","three","four","five"};
char **str=a;
printf("%p ", &a[0]);
printf("\n%p ", &str[0]);
printf("\n%p ", &str[3]);
printf("\n%p ", &str[1][56]);
printf("\n%p ", &(*(*(str+4)+1)));
}

以下是观察到的输出:
0xbf7f6286 
0xbf7f6286 
0xbf7f6292 
0x38 
0x1 
  • &a[0]是数组起始地址的地址
  • &str[0]也是这样

    有人可以解释一下为什么str[3]的地址是0xbf7f6292吗?据我理解,&str[0]和&str[3]应该相差30个字节。

    同时,请有人解释一下其他两种情况的输出。

提前感谢您的帮助。


你的数组 a,即使有两个维度,也不等同于指向指针的指针。 - WhozCraig
5个回答

8

简短回答:

将此改为:

char **str;

成为这样:
char (*str)[10];

随后修复代码中的任何错误。 str 的类型与您的二维数组的类型不兼容。我自行将main()的返回类型修复为int(根据C标准,不允许使用void)。我还标记了您可能存在未定义行为的源:

#include <stdio.h>
int main()
{
    char a[5][10]={"one","two","three","four","five"};
    char (*str)[10] = a;
    printf("%p ", &a[0]);
    printf("\n%p ", &str[0]);
    printf("\n%p ", &str[3]);
    printf("\n%p ", &str[1][56]);       // this is undefined behaviour
    printf("\n%p ", &(*(*(str+4)+1)));
    return 0;
}

需要注意的是最后的printf()语句中,有一个多余的&(*(...))部分是不必要的。可以去掉它,这样语句等同于:

    printf("\n%p ", *(str+4)+1);

输出(系统相关)

0x7fff5fbff900 
0x7fff5fbff900 
0x7fff5fbff91e 
0x7fff5fbff942 
0x7fff5fbff929 

非常(非常,非常)长的答案

您假设二维数组等同于指向指针的指针是错误的。它们仅在使用语法上类似。以下是数组 a 在内存中的大致布局。

char a[5][10]

    0   1   2   3   4   5   6   7   8   9 
  -----------------------------------------
0 | o | n | e | 0 |   |   |   |   |   |   |
  -----------------------------------------
1 | t | w | o | 0 |   |   |   |   |   |   |
  -----------------------------------------
2 | t | h | r | e | e | 0 |   |   |   |   |
  -----------------------------------------
3 | f | o | u | r | 0 |   |   |   |   |   |
  -----------------------------------------
4 | f | i | v | e | 0 |   |   |   |   |   |
  -----------------------------------------

请注意,第二行的起始地址,即&a[1],比第一行,即&a[0](也就是整个数组的起始地址)多了十个字节。以下演示了这一点:
int main()
{
    char a[5][10] = { "one", "two", "three", "four", "five" };

    printf("&a    = %p\n", &a);
    printf("&a[0] = %p\n", &a[0]);
    printf("a[0]  = %p\n", a[0]);
    printf("&a[1] = %p\n", &a[1]);
    printf("a[1]  = %p\n", a[1]);
    return 0;
}

输出

&a    = 0x7fff5fbff8e0
&a[0] = 0x7fff5fbff8e0
a[0]  = 0x7fff5fbff8e0
&a[1] = 0x7fff5fbff8ea
a[1]  = 0x7fff5fbff8ea

请注意,位于a[1]位置的地址距离数组开头有10字节(0x0a十六进制)。但是为什么呢?要回答这个问题,必须了解类型指针算术的基础知识。
类型指针算术是如何工作的? 在C和C++中,除void指针外,所有指针都有类型。它们与指针相关联的有一个基本数据类型。例如。
int *iptr = malloc(5*sizeof(int));

iptr指向一个内存区域,该区域的大小被分配为五个整数。像处理数组一样访问它,看起来像这样:

iptr[1] = 1;

但我们同样可以像这样解决它:
*(iptr+1) = 1;

结果是一样的,将值1存储在第二个数组插槽中(0插槽为第一个)。我们知道解引用运算符 * 允许通过地址(存储在指针中的地址)进行访问。但是,(iptr+1)如何知道跳过四个字节(如果您的int类型为32位,则为八个字节,如果它们为64位,则为八个字节)以访问下一个整数插槽?

答案是:由于类型指针算术。编译器知道指针底层的类型宽度(在本例中,int类型的宽度)。当您将标量值添加或减去指针时,编译器会生成适当的代码来考虑这个“类型宽度”。它也可以使用用户定义的类型。以下是演示:

#include <stdio.h>

typedef struct Data
{
    int ival;
    float fval;
    char buffer[100];
} Data;

int main()
{
    int ivals[10];
    int *iptr = ivals;
    char str[] = "Message";
    char *pchr = str;
    Data data[2];
    Data *dptr = data;

    printf("iptr    = %p\n", iptr);
    printf("iptr+1  = %p\n", iptr+1);

    printf("pchr    = %p\n", pchr);
    printf("pchr+1  = %p\n", pchr+1);

    printf("dptr    = %p\n", dptr);
    printf("dptr+1  = %p\n", dptr+1);

    return 0;
}

输出

iptr    = 0x7fff5fbff900
iptr+1  = 0x7fff5fbff904
pchr    = 0x7fff5fbff8f0
pchr+1  = 0x7fff5fbff8f1
dptr    = 0x7fff5fbff810
dptr+1  = 0x7fff5fbff87c

请注意,iptriptr+1之间的地址差异不是一个字节,而是四个字节(在我的系统上是int的宽度)。接下来,使用pchrpchr+1演示了单个char的宽度为一个字节。最后,我们自定义的数据类型Data及其两个指针值dptrdptr+1显示它的宽度为0x6C,即108个字节(由于结构包装和字段对齐,它可能更大,但在这个例子中我们很幸运它没有变得更大)。这是有道理的,因为该结构包含两个4字节的数据字段(一个int和一个float)以及一个100个元素宽的char缓冲区。
顺便说一下,反过来也是正确的,这经常被经验丰富的C/C++程序员所忽视。这就是类型指针的差异。如果您在有效内存的连续区域中有两个指定类型的有效指针:
int ar[10];
int *iptr1 = ar+1;
int *iptr5 = ar+5;

你认为执行以下操作会得到什么结果:

printf("%lu", iptr5 - iptr1);

答案是...4。你可能会说,这并不是什么大不了的事情?但请不要被表面所迷惑。当你使用指针算法来确定缓冲区中特定元素的偏移量时,这非常方便。
总之,当你有以下表达式时:
int ar[5];
int *iptr = ar;

iptr[1] = 1;

你可以理解为这与以下内容等价:
*(iptr+1) = 1;

简单来说,就是“取出存储在iptr变量中的地址,加上1个int所占用的字节数,然后将值1存储在通过返回地址引用的内存中。”

网站栏:这也可以。看看你能否想到为什么。

1[iptr] = 1;

回到您(我们)的示例数组,现在看看当我们通过双指针引用相同的地址a时会发生什么(这是完全不正确的,您的编译器应该至少警告您有关分配的问题):

char **str = a; // Error: Incompatible pointer types: char ** and char[5][10]

这么做行不通。但是假设它能够运行,char **是指向字符指针的指针。这意味着该变量本身仅持有一个指针,它没有基本行宽等概念。因此,假设您将a的地址放入双指针str中。

char **str = (char **)(a); // Should NEVER do this, here for demonstration only.
char *s0 = str[0];  // what do you suppose this is?

我们的测试程序做了轻微更新:

int main()
{
    char a[5][10] = { "one", "two", "three", "four", "five" };
    char **str = (char **)a;
    char *s0 = str[0];
    char *s1 = str[1];

    printf("&a    = %p\n", &a);
    printf("&a[0] = %p\n", &a[0]);
    printf("a[0]  = %p\n", a[0]);
    printf("&a[1] = %p\n", &a[1]);
    printf("a[1]  = %p\n", a[1]);

    printf("str   = %p\n", str);
    printf("s0    = %p\n", s0);
    printf("s1    = %p\n", s1);

    return 0;
}

给我们以下结果:
&a    = 0x7fff5fbff900
&a[0] = 0x7fff5fbff900
a[0]  = 0x7fff5fbff900
&a[1] = 0x7fff5fbff90a
a[1]  = 0x7fff5fbff90a
str   = 0x7fff5fbff900
s0    = 0x656e6f
s1    = 0x6f77740000

好的,str 看起来就是我们想要的内容,但 s0 中的东西是什么呢?那是字母的ASCII字符值。哪些字母?查看一个不错的ASCII表可以快速得到它们是:

0x65 : e
0x6e : n
0x6f : o

这是单词“one”的反向拼写(由于我的系统处理多字节值的字节序问题而导致反向,但我希望问题很明显)。那第二个值呢:

0x6f : o
0x77 : w
0x74 : t

是的,这就是“两个”。那么为什么我们在数组中得到的字节是指针呢?

嗯...是的,正如我在评论中所说的那样。告诉你或者暗示你双重指针和二维数组是同义词的人是错误的。回想一下我们对类型指针算术的探索。记住这个:

str[1]

并且还有这个:

*(str+1)

这些术语是同义词。那么,str指针的类型是什么?它所指向的类型是char指针。因此,这两者之间的字节计数差异为:

str + 0

并且这个

str + 1

char* 的大小将是字节。在我的系统上,这是4个字节(我有32位指针)。 这就解释了为什么str[1]中的表面地址是我们最初数组基础上的4个字节数据。

因此,回答你的第一个基本问题(是的,我们终于到了那一步)。

为什么str[3]0xbf7f6292

答案:这个:

&str[3]

等同于这个:

(str + 3)

但是我们从上面知道(str + 3)只是存储在str中的地址,然后再加上3倍str类型的宽度所指向的char *,以字节为单位指向那个地址。好了,我们从你的第二个printf中知道了那个地址是什么:
0xbf7f6286

我们知道您系统上指针的宽度为4个字节(32位指针)。因此...

0xbf7f6286 + (3 * 4)

or....

0xbf7f6286 + 0x0C = 0xbf7f6292

1
真是个惊人的答案!!提问者一定很幸运才能遇到这样的答案,我会把这个问题收藏起来,就为了这篇帖子!!! - Barath Ravikumar
@user2165725,如果你发现任何答案有用,可以通过接受答案来表示。 - Barath Ravikumar

3

前两个printf显然打印同一个地址,即二维数组的第一个元素,它是一个一维数组。因此,这两个printf都指向数组的开始,也就是a [0] [0]。

char **str是一个指针,因此当您执行str [3]时,它在内部意味着(str + 3),并且最可能您的代码正在运行32位机器上,因此根据指针算术,它将向前跳12字节。不要与数组混淆,str是一个二维指针而不是数组。

0xbf7f6286 str [0] 0xbf7f6292 str [3]差异为0xc,这是预期的正确值。

printf(“\n%p ”,&amp; str [1] [56]); printf(“\n%p ”,&amp;((str + 4)+1));

由于您对指针算术的错误理解,上述最后两个语句正在尝试打印。

我希望这将有助于解决您的疑虑。


我有一个疑问:由于a是一个二维数组,它指向二维数组的起始地址。但是,str是一个指向指针的指针。由于我们复制了str=a,所以指针变量str包含a的地址,并且指针变量str将在某个位置'x'创建。因此,当我们打印&str[0]时,它应该打印'x'而不是a的地址,对吗?如果我哪里错了,请详细说明。 - user2165725

1
char a[5][10]={"one","two","three","four","five"};
char **str=a;

让我解释一下双星指针的工作原理以及它与数组的关系,这样你就有更好的机会自己尝试解决问题。
通常情况下,双星指针被赋予另一个指针的“地址”。
例如:
char   a =  5;
char  *b = &a;
char **c = &b;

所以当您对c进行一次取消引用时
printf("\n%d",*c)

当你获取 'b' 所包含的值时,实际上获得的是 'a' 的地址。

当你对 'c' 进行两次解引用操作时

printf("\n%d",**c)

你可以直接获取变量'a'的值。

现在来讲讲数组和指针之间的关系。

数组的名称是其地址的同义词,这意味着数组的名称就是其自身的位置。

例如:

int array [5] = {1,2,3,4,5};
printf("\n%p",array);
printf("\n%p",&array);
printf("\n%p",&array[0]);

它们都会打印相同的地址。

因此,这是一个无效的指针赋值。

char **str=a;

因为 **str 期望的是一个指向保存地址的指针的地址,但你只传递了一个地址(数组名 = a)。
简而言之,一个双星指针不应该用于处理二维数组。
这个 问题 处理如何使用指针处理二维数组。

0
当你说 str+3 时,它意味着 str+(3*sizeof(char **))。因此,如果 'char **' 的大小为4字节,则 str 增加了12个字节。

0
如果您想打印一个二维数组,应该使用两个循环(我建议使用两个'for'循环),并将一个嵌套在另一个内。
至于输出,它们(我不是要陈述显而易见的)都是地址,因为当您在访问数组元素之前键入“&”时,您正在请求其地址。
由于您将数组a设置为str,实际上是将指针str设置为数组'a'指向的地址。
当您使用[int n]访问运算符时,您实际上是告诉计算机查找数组(或指针)实际指向位置后'n'个地址槽。这就是为什么比前面两个多16个的原因。
最后两个看起来像十六进制数,我不太确定那最后两行输出的内容。

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