非常简短的答案:
改变这个:
char **str;
要这样:
char (*str)[10];
并在之后修复代码中的任何错误。的类型str
与您的二维数组的类型不兼容。我冒昧地将返回类型固定为int
for main()
(根据 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
但为什么?要回答这个问题,必须了解类型指针算术的基础知识。
类型指针算术如何工作?
void
在 C 和 C++ 中,除了类型之外的所有指针。具有与指针关联的基本数据类型。例如。
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
iptr
注意和之间的地址差异iptr+1
不是一个字节;它是四个字节(我系统上的宽度)。接下来,用和作为一个字节来演示单个的宽度。最后,我们自定义的数据类型带有它的两个指针值,并显示它是 0x6C,即 108 字节宽。(由于结构填充和字段对齐,它可能会更大,但我们很幸运地看到了这个例子,事实并非如此)。这是有道理的,因为该结构包含两个 4 字节的数据字段(an和 a )和一个 100 个元素宽的 char 缓冲区。int
char
pchr
pchr+1
Data
dptr
dptr+1
int
float
顺便说一句,反过来也是正确的,即使是有经验的 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
放在 double-pointerstr
中。
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
那是反向的“一”一词(反向是由我的系统上的多字节值的字节序处理引起的,但我希望问题很明显。那第二个值呢:
0x6f : o
0x77 : w
0x74 : t
对,就是“二”。那么为什么我们将数组中的字节作为指针呢?
嗯.. 是的,正如我在评论中所说。无论谁告诉您或向您暗示双指针和二维数组是同义词,都是不正确的。回想一下我们对类型指针算术的中断。记住这一点:
str[1]
和这个:
*(str+1)
是同义词。出色地。指针的类型是什么?str
它指向的类型是一个char
指针。因此,这之间的字节数差异:
str + 0
和这个
str + 1
将是 a 的大小char*
。在我的系统上是 4 个字节(我有 32 位指针)。这就解释了为什么表观地址是我们原始数组基数的四个字节str[1]
的数据。
因此,回答您的第一个基本问题(是的,我们终于明白了)。
为什么str[3]
是0xbf7f6292
答:这个:
&str[3]
相当于:
(str + 3)
但是我们从上面知道这(str + 3)
只是存储在 中的地址str
,然后将类型str
指向的宽度的 3 倍,即 a char *
,以字节为单位添加到该地址。出色地。我们从你的第二个printf
知道那个地址是什么:
0xbf7f6286
我们知道您系统上的指针宽度是 4 个字节(32 位指针)。所以...
0xbf7f6286 + (3 * 4)
或者....
0xbf7f6286 + 0x0C = 0xbf7f6292