为什么我不能访问堆栈数组的指针?


35

请看下面的代码。它尝试将数组作为a char**传递给函数:

#include <stdio.h>
#include <stdlib.h>

static void printchar(char **x)
{
    printf("Test: %c\n", (*x)[0]);
}

int main(int argc, char *argv[])
{
    char test[256];
    char *test2 = malloc(256);

    test[0] = 'B';
    test2[0] = 'A';

    printchar(&test2);            // works
    printchar((char **) &test);   // crashes because *x in printchar() has an invalid pointer

    free(test2);

    return 0;
}

我只能通过显式强制转换&test2char**已编译的事实来暗示此代码是错误的。

不过,我想知道这到底有什么问题。我可以将指针传递给动态分配的数组的指针,但是不能将指针传递给堆栈上的数组的指针。当然,我可以通过首先将数组分配给一个临时变量来轻松解决该问题,如下所示:

char test[256];
char *tmp = test;
test[0] = 'B';
printchar(&tmp);

仍然有人可以向我解释为什么直接投射char[256]到它无效char**吗?

Answers:


29

因为test不是指针。

&test获取指向类型char (*)[256]不兼容char**的数组的指针(因为数组不是指针)。这导致未定义的行为。


3
但是,为什么C编译器然后允许将类型传递char (*)[256]char**呢?
ComFreek

@ComFreek我怀疑带有最大警告和-Werror,它不允许这样做。
PiRocks

@ComFreek:它实际上是不允许的。我必须通过将其显式转换为来强制编译器接受它char**。没有该强制转换,它将无法编译。
安德烈亚斯

38

test是一个数组,而不是指针,并且&test是指向该数组的指针。它不是指向指针的指针。

您可能已经被告知数组是一个指针,但这是不正确的。数组的名称是整个对象(所有元素)的名称。它不是指向第一个元素的指针。在大多数表达式中,数组会自动转换为指向其第一个元素的指针。这是一个经常有用的便利。但是此规则有三个例外:

  • 数组是的操作数sizeof
  • 数组是的操作数&
  • 该数组是用于初始化数组的字符串文字。

在中&test,数组是的操作数&,因此不会发生自动转换。的结果&test是一个指向256数组的指针,该数组的char类型char (*)[256]不是char **

要获得指向charfrom 的指针test,您首先需要制作一个指向的指针char。例如:

char *p = test; // Automatic conversion of test to &test[0] occurs.
printchar(&p);  // Passes a pointer to a pointer to char.

对此进行考虑的另一种方法是,实现test对整个对象(整个256数组)的命名char。它没有命名指针,因此in &test中没有指针可以使用其地址,因此不能产生一个char **。为了创建一个char **,您必须首先有一个char *


1
这三个例外列表是否详尽无遗?
Ruslan

8
@Ruslan:是的,根据C 2018 6.3.2.1 3.
Eric Postpischil

哦,在C11 _Alignof中,除了sizeof和还提到了运算符&。我想知道为什么他们将其删除...
Ruslan

@Ruslan:之所以删除是因为这是一个错误。_Alignof仅接受类型名称作为操作数,而从不接受数组或任何其他对象作为操作数。(我不知道为什么;在语法和语法上似乎像sizeof,但事实并非如此。)
Eric Postpischil

6

的类型test2char *。因此,类型&test2char **是与参数的类型兼容xprintchar()
的类型testchar [256]。因此,类型&testchar (*)[256]带参数的类型兼容xprintchar()

让我告诉你在地址方面的差异testtest2

#include <stdio.h>
#include <stdlib.h>

static void printchar(char **x)
{
    printf("x = %p\n", (void*)x);
    printf("*x  = %p\n", (void*)(*x));
    printf("Test: %c\n", (*x)[0]);
}

int main(int argc, char *argv[])
{
    char test[256];
    char *test2 = malloc(256);

    test[0] = 'B';
    test2[0] = 'A';

    printf ("test2 : %p\n", (void*)test2);
    printf ("&test2 : %p\n", (void*)&test2);
    printf ("&test2[0] : %p\n", (void*)&test2[0]);
    printchar(&test2);            // works

    printf ("\n");
    printf ("test : %p\n", (void*)test);
    printf ("&test : %p\n", (void*)&test);
    printf ("&test[0] : %p\n", (void*)&test[0]);

    // Commenting below statement
    //printchar((char **) &test);   // crashes because *x in printchar() has an invalid pointer

    free(test2);

    return 0;
}

输出:

$ ./a.out 
test2 : 0x7fe974c02970
&test2 : 0x7ffee82eb9e8
&test2[0] : 0x7fe974c02970
x = 0x7ffee82eb9e8
*x  = 0x7fe974c02970
Test: A

test : 0x7ffee82eba00
&test : 0x7ffee82eba00
&test[0] : 0x7ffee82eba00

指向此处注意:

的输出(存储器地址)test2&test2[0]数字相同,并且它们的类型也是相同的,其是char *
但是test2&test2是不同的地址,它们的类型也不同。
的类型test2char *
的类型&test2char **

x = &test2
*x = test2
(*x)[0] = test2[0] 

的输出(存储地址)test&test并且&test[0]数字相同但它们的类型是不同的
的类型testchar [256]
的类型&testchar (*) [256]
的类型&test[0]char *

如输出所示&test与相同&test[0]

x = &test[0]
*x = test[0]       //first element of test array which is 'B'
(*x)[0] = ('B')[0]   // Not a valid statement

因此,您会遇到细分错误。


3

您不能访问指向指针的指针,因为&test它不是指针-它是一个数组。

如果采用数组的地址,将数组和数组的地址转换为(void *),然后进行比较,则它们将是等效的(除非有可能出现指针指针)。

您真正在做的事情与此类似(再次,除非使用严格的别名):

putchar(**(char **)test);

这显然是错误的。


3

你的代码期望的说法xprintchar,以点包含内存(char *)

在第一个调用中,它指向用于的存储test2,因此实际上是指向a的值,(char *)后者指向分配的内存。

但是,在第二个调用中,没有地方(char *)可以存储任何此类值,因此不可能指向此类存储器。向(char **)您添加的强制转换会消除编译错误(关于转换(char *)(char **)),但不会使存储显得空洞而无法包含(char *)指向测试的第一个字符的初始化。C中的指针强制转换不会更改指针的实际值。

为了获得想要的东西,您必须明确地做到这一点:

char *tempptr = &temp;
printchar(&tempptr);

我假设您的示例是对大量代码的精炼;例如,您可能想printchar增加(char *)传递的x值所指向的值,以便在下一次调用时打印下一个字符。如果不是这种情况,为什么不直接(char *)指向要打印的字符,甚至不传递字符本身呢?


好答案; 我同意保持这一点最简单的方法是考虑是否有一个C对象保存数组的地址,即一个指针对象,您可以使用该地址获取a char **。数组变量/对象简单地该阵列,该地址是隐式的,不存储在任何地方。与指向其他存储的指针变量不同,没有额外的间接访问级别。
Peter Cordes

0

显然,采用的地址test与采用的地址相同test[0]

#include <stdio.h>
#include <stdlib.h>

static void printchar(char **x)
{
    printf("[printchar] Address of pointer to pointer: %p\n", (void *)x);
    printf("[printchar] Address of pointer: %p\n", (void *)*x);
    printf("Test: %c\n", **x);
}

int main(int argc, char *argv[])
{
    char test[256];
    char *test2 = malloc(256);

    printf("[main] Address of test: %p\n", (void *)test);
    printf("[main] Address of the address of test: %p\n", (void *)&test);
    printf("[main] Address of test2: %p\n", (void *)test2);
    printf("[main] Address of the address of test2: %p\n", (void *)&test2);

    test[0] = 'B';
    test2[0] = 'A';

    printchar(&test2);            // works
    printchar(&test);   // crashes because *x in printchar() has an invalid pointer

    free(test2);

    return 0;
}

编译并运行:

forcebru$ clang test.c -Wall && ./a.out
test.c:25:15: warning: incompatible pointer types passing 'char (*)[256]' to
      parameter of type 'char **' [-Wincompatible-pointer-types]
    printchar(&test);   // crashes because *x in printchar() has an inva...
              ^~~~~
test.c:4:30: note: passing argument to parameter 'x' here
static void printchar(char **x)
                             ^
1 warning generated.
[main] Address of test: 0x7ffeeed039c0
[main] Address of the address of test: 0x7ffeeed039c0 [THIS IS A PROBLEM]
[main] Address of test2: 0x7fbe20c02aa0
[main] Address of the address of test2: 0x7ffeeed039a8
[printchar] Address of pointer to pointer: 0x7ffeeed039a8
[printchar] Address of pointer: 0x7fbe20c02aa0
Test: A
[printchar] Address of pointer to pointer: 0x7ffeeed039c0
[printchar] Address of pointer: 0x42 [THIS IS THE ASCII CODE OF 'B' in test[0] = 'B';]
Segmentation fault: 11

因此,导致分段错误的最终原因是该程序将尝试取消引用绝对地址0x42(也称为'B'),您的程序无权读取该绝对地址。

尽管使用不同的编译器/机器,但地址将有所不同:在线尝试!,但由于某些原因,您仍然会得到此信息:

[main] Address of test: 0x7ffd4891b080
[main] Address of the address of test: 0x7ffd4891b080  [SAME ADDRESS!]

但是导致分段错误的地址可能会有所不同:

[printchar] Address of pointer to pointer: 0x7ffd4891b080
[printchar] Address of pointer: 0x9c000000942  [WAS 0x42 IN MY CASE]

1
以的地址test是不一样服用的地址test[0]。前者有类型char (*)[256],后者有类型char *。它们不兼容,并且C标准允许它们具有不同的表示形式。
埃里克·Postpischil

当使用格式化指针时%p,应将其转换为void *(再次出于兼容性和表示的原因)。
埃里克·波斯特皮希尔

1
printchar(&test);可能会为您崩溃,但是该行为不是C标准定义的,并且在其他情况下,人们可能会观察到其他行为。
埃里克·Postpischil

Re“因此,分段错误的最终原因是该程序将尝试取消引用可能由OS占用的绝对地址0x42(也称为“ B”)。”:如果有段错误试图读取一个位置,意味着没有任何内容映射到该位置,而不是被操作系统占用。(除了可能有映射的内容,例如,仅具有执行权限,没有读取权限,但这不太可能。)
Eric Postpischil

1
&test == &test[0]违反了C 2018 6.5.9 2中的约束,因为类型不兼容。C标准要求实现来诊断此违规,并且C标准未定义所产生的行为。这意味着您的编译器可能会生成评估它们是否相等的代码,而另一个编译器可能不会。
埃里克·Postpischil

-4

的表示char [256]取决于实现。不得与相同char *

铸造&testchar (*)[256]char **收益不确定的行为。

对于某些编译器,它可能会实现您所期望的,而对其他编译器则不然。

编辑:

经过gcc 9.2.1的测试后,看来printchar((char**)&test)实际上是传递test 给的值char**。好像指令是printchar((char**)test)。在该printchar函数中,x是指向数组测试的第一个字符的指针,而不是指向第一个字符的双指针。双重取消引用会x导致分段错误,因为数组的前8个字节不对应于有效地址。

使用clang 9.0.0-2编译程序时,我得到完全相同的行为和结果。

这可能被视为编译器错误,或者是未定义行为的结果,其结果可能是特定于编译器的。

另一个意外行为是该代码

void printchar2(char (*x)[256]) {
    printf("px: %p\n", *x);
    printf("x: %p\n", x);
    printf("c: %c\n", **x);
}

输出是

px: 0x7ffd92627370
x: 0x7ffd92627370
c: A

怪异的行为是x*x具有相同的值。

这是编译器。我怀疑这是由语言定义的。


1
您的意思是表示char (*)[256]依赖于实现吗?char [256]在这个问题中,的表示形式无关紧要,只是一堆。但是,即使您的意思是指向数组的指针的表示形式不同于指向指针的指针的表示形式,也错失了重点。即使它们具有相同的表示形式,OP的代码也无法正常工作,因为指向指针的指针可以被取消引用两次,就像在中所做的那样printchar,但是指向数组的指针无论表示形式如何都不能。
埃里克·波斯特皮希尔

@EricPostpischil从char (*)[256]转换char **为,已被编译器接受,但未产生预期的结果,因为a char [256]与a不同char *。我假设编码是不同的,否则它将产生预期的结果。
chmike

我不知道您所说的“预期结果”是什么意思。C标准中关于结果应该是的唯一规范是,如果对齐方式不足以char **,则行为是不确定的,否则,如果结果转换回char (*)[256],其结果将等于原始指针。所谓“预期结果”,可能意味着如果(char **) &test进一步转换为a char *,则它等于&test[0]。在使用平面地址空间的实现中,这并不是不太可能的结果,但这不是纯粹的表示问题。
埃里克·Postpischil

2
另外,“将类型为char(*)[256]的测试&test转换为char **会产生未定义的行为。” 是不正确的。C 2018 6.3.2.3 7允许将指向对象类型的指针转​​换为指向该对象类型的任何其他指针。如果指针未针对引用类型正确对齐(的引用类型char **char *),则行为未定义。否则,根据我上面的评论,虽然只部分定义了值,但定义了转换。
埃里克·Postpischil

char (*x)[256]与...不同char **x。之所以x*xprint指针值相同,是因为x它只是指向数组的指针。 *x 是数组,在指针上下文中使用它会衰减回数组的地址。那里没有编译器错误(或做什么(char **)&test),只需要一点点精神体操就能弄清楚类型的问题。(cdecl将其解释为“将x声明为char数组256的指针”)。即使使用char*来访问a的对象表示char**也不是UB。它可以别名任何东西。
Peter Cordes
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.