【问题标题】:Why does gcc allow dereferencing to occur when the pointer isn't pointing at the correct type. Why even have types? [closed]当指针未指向正确的类型时,为什么 gcc 允许取消引用。为什么还要有类型? [关闭]
【发布时间】:2013-04-10 23:32:34
【问题描述】:

我昨天发布了关于指向指针的指针等here 并决定将讨论移至新问题答案的 cmets 中。

我的问题是为什么编译器允许这种情况发生:

int *foo = malloc(sizeof(int));
*foo = 8;
int **********blarg = foo;

我收到了一些警告,但是当我像这样对 blarg 进行取消引用时:

*blarg;

它的值是 8。解引用不应该返回某种类型的空值吗?难道编译器不应该知道只有在取消引用之后,例如:

**********blarg;

是否有一个实际的整数值,任何较低级别的取消引用只会返回更多指向指针的指针?

Void 指针对我来说是一个全新的困惑,因为像这样的变量:

void *asdf;

可以指向以下任何类型:

int
char
float
*void
**********void
*******int

你明白了。

所以我的主要问题:

  1. 代码的第一个 sn-p 中到底发生了什么?那些指向指针的指针是否正在创建?
  2. 为什么取消引用在第二个 sn-p 代码中起作用?
  3. (听起来很愚蠢)如果我可以为任何东西分配任何值并且拥有 只要我正确阅读它就可以工作?

【问题讨论】:

  • 谨慎的人不应该受到性能影响,因为它会检查每个取消引用。
  • 编译器给了你一个警告。如果您的问题是,为什么编译器允许代码编译 - 使用-Werror (gcc),它将不再编译,您将不再需要担心您的代码是否存在上述非法行为。
  • 我想一个更好的问题是它甚至可以编译成什么才能运行?希望我有新的大会。
  • @chris:这不是编译时问题吗?
  • @recursive,我感觉到了一些编译时问题。我认为这个问题也包括运行时问题。

标签: c pointers gcc types dereference


【解决方案1】:

举个更简单的例子:

int n;
int *p = &n;
int **p2 = p;

使用 gcc 4.7,带有默认选项,我得到:

warning: initialization from incompatible pointer type [enabled by default]

C 标准说尝试初始化p2违反约束,这意味着需要符合要求的实现才能发出诊断。它并不意味着需要一个符合要求的实现来拒绝翻译单元。必须拒绝翻译单元(源文件)的唯一情况是它包含#error 指令。

因此,通过打印无效初始化程序的警告,gcc 就 ISO C 标准而言已经完成了它的工作。

如果您希望它彻底拒绝它,您可以使用-pedantic-errors 选项,最好与-std=... 选项之一一起使用。

这是语言问题。 gcc 可以默认拒绝这样的程序——那为什么不呢?这是关于 gcc 的问题,而不是关于 C 语言的问题。

自 1989 年 ANSI 发布官方 C 标准之前,gcc 已经存在了很长时间。在 ANSI 之前的日子里,像这样的初始化程序是非法的还不清楚。有(并且可能仍然是)相当多的旧代码依赖于这样的结构,并且恰好可以工作。我推测 gcc 开发人员只是没有觉得现在是进行此类更改并冒着破坏现有代码的好时机。

就个人而言,我希望 gcc 默认会拒绝这样的事情,但您始终可以 (a) 启用更严格的诊断,并 (b) 密切注意警告。

【讨论】:

    【解决方案2】:

    让我们仔细看看您的代码。我注意到您几乎可以肯定是在 32 位模式下编译(在 32 位机器上或在 32 位模式下的 64 位机器上)。

    int *foo = malloc(sizeof(int));
    *foo = 8;
    int **********blarg = foo;
    *blarg;
    

    第一行分配了一个指向foo的指针,该指针指向一个足够大的空间来容纳int。第二行将值8 分配给foo 指向的整数。

    具有 10 级指针(10 个*'s)的第三行会生成警告,因为foo 的类型与blarg 的类型不匹配。但是,编译器会尝试确定您所做的事情的开头或结尾,并将指针从 foo 复制到 blarg

    第四行访问blarg 指向的值。结果是 int *** *** ***(指向 int 的 9 级指针),但它的值是 blarg 指向的值。由于您将8 视为*blarg 的值,因此我们可以合理地推断出sizeof(int *) == sizeof(int) — 因此我对 32 位编译的观察。如果您使用的是 64 位机器(甚至是 Windows 64),*blarg 将访问 8 个字节而不是 4 个字节,并且额外的 4 个字节可能不会全部为零。

    因此,即使您没有预料到,行为也是可以预测的。但由于分配给blarg 的类型不匹配,因此没有严格定义。您也不能安全地使用**blarg 或任何其他更大数量的取消引用*;您将访问无效的内存。

    【讨论】:

    • 我明白了。因此,如果 foo 是指向 long 的指针。这甚至不符合?实际上让我检查一下...好吧,它可以编译,但是当我将 *blarg 打印为 int 时,它会像读取 int 一样读取值,而忽略剩余的位。
    • 如果您使用的是 32 位系统,那么 *blarg 会打印一个 32 位指针值。您必须在 64 位系统上才能看到问题,即使编写的代码也不能保证这一点。如果您分配了int *foo = malloc(2 * sizeof(int));,然后分配了foo[0] = 8; foo[1] = 0xA0B1C2D3;,然后尝试使用printf("*blarg = 0x%.16" PRIXPTR "\n", (uintptr_t)(*blarg)); 打印*blarg,那么您会在输出中看到0x00000008 和0xA0B1C2D3 的组合,但顺序取决于您是否使用部分是大端或小端机器。
    【解决方案3】:

    这是一个有趣的问题。

    归结为实现,拥有类型系统的 C 语言的价值在于以下承诺:“如果您的代码不触发警告,那么它在 C 类型系统的框架中是类型安全的”。程序员可能犯的许多错误都引起了他的注意。反之亦然,如果编译器没有发出警告,程序员可以在一定程度上相信他的代码不包含任何一大类问题。汇编程序员不能这么确定,可能必须仔细检查所有内容。但是 C 程序员只需要仔细检查代码的高级方面,而不是忘记取消引用指针等错误。

    (void*) 经常在那里使用,在那里不可能或不方便将功能、结构……与特定类型联系起来。示例包括集合、消息传递原语。

    您的代码会触发警告,因此编译器当然不会声明您的代码是类型安全的,事实上,访问**blarg(int)*blarg == 8 会在许多架构上导致内存访问错误。 (即使** 不是最终的解引用级别。)

    您是在询问是否正在创建指向指针的指针:当您说时

    int **********blarg [...] ; //10-pointer to int
    

    您只创建了一个变量,它是一个指向 int 的 9 指针的指针。此行没有创建也不应该创建指向 int 的 9 指针,blarg 将指向该指针。

    【讨论】:

    • 所以一个 printf("%d", *blarg);由于使用变量的上下文而起作用?它期望它传递的任何东西都是一个 int。但是,如果某些东西期望 *blarg 是一个指向“8 指针”的指针并试图像 **blarg 那样取消引用它,你会遇到问题吗?还是只是指向内存地址 8?
    • printf("%d", *blarg) 有效,因为blarg 是用内存中的地址初始化的,但是此代码会使用 gcc 发出警告。此外,int *ptr; printf("%f, %d", ptr, 1234); 将无法在 amd64 上打印数字 1234,因为 printf 期望堆栈上未填充 4 字节浮点数,而指针为 8 字节。
    • @Adrain Panasiuk 那么为什么 printf 不像我将 long 传递给 printf("%d", longVar) 时那样只读取 ptr 的前 4 个字节
    • 确实如此,将其解释为浮点数,然后将其余部分解释为 int;并且没有任何标志告诉它解释 1234。
    猜你喜欢
    • 1970-01-01
    • 2012-04-19
    • 2020-12-22
    • 1970-01-01
    • 2016-07-24
    • 2020-05-08
    • 2023-04-02
    • 2020-12-22
    • 1970-01-01
    相关资源
    最近更新 更多