我遇到了这个问题,取消引用a-3内存地址给了我存在的字符b

char *b = "welcome";
{
    char *a = "home";
    printf("%c", *(a-3));
}

我找不到任何相关标准来支持这一点。

我尝试使用在线 C 编译器编译它。它返回了一个存储在 中的字符b

我想了解如何实现这一执行。

3

  • 1
    不相关:*(a-3)写得更清楚a[-3]


    – 


  • 5
    这是undefined behavior因为你问的a[-3]是恰好是某个字符b


    – 

  • 5
    您无法通过做实验来了解 C 中允许什么或不允许什么,您必须阅读标准(或一本好书)。


    – 


5 个回答
5

找不到任何相关标准来支持这一点

那是因为没有。由于您访问"home"越界,因此程序的行为未定义,这意味着程序可以执行几乎任何事情,包括它在您的案例中所做的事情。

永远不要依赖未定义的行为。

由于您试图通过取消引用内存地址来访问“home”之外的区域,因此该程序具有未定义的行为。但是,要回答您关于如何执行的问题,编译器可以将字符串存储在内存中的h o m e \0字符串之后w e l c o m e \0。由于指针指向的地址是字符串的第一个字符,a因此将指向h内存中的。如果从地址指向的地址中减去 3 a,您将返回内存中的 3 个字节,传递“welcome”末尾的空字符,传递e,并将mWelcome 中的字符作为要打印的字符。不过,正如另一个答案所提到的,这是未定义的行为,永远不应依赖它,因为您正在访问a字符串的边界之外。

0

想要了解这个执行是如何实现的

程序具有未定义行为,因为访问指针指向的数组之外的内存a是无效的:行为未定义,执行可能没有可见的副作用,也可能失败并显示明确的错误消息,程序可以打印与您的假设一致的字符或一些不同的东西,或者突然终止,或者进入无限循环或其他任何事情……包括不同运行的不同行为。未定义行为不是什么可期待的。

标准并没有给出关于内存中字符串文字地址的很多保证:对于不同的字符串内容,它们必须不同,但是相对顺序是未定义的,事实上,相对顺序可能根本没有意义,因为指向不同数组的指针只能进行比较以确定相等性,而不能进行比较具有<<=>的相对顺序>=

相同字符串文字的不同实例可能具有相同的地址或不同的地址,对此也没有保证。

字符串文字甚至可以重叠:"hello world"并且"world"可以放置在内存中,以便"hello world" + 6 == "world"。早期的 C 编译器曾经利用这一点来减少内存占用。现代编译器和链接器仍然可以做到这一点,或者不能,不保证。

您的程序有未定义的行为,但有一种可移植的方法可以使用来测试这些条件printf

#include <stdio.h>

int main(void) {
    const char *a = "home";
    const char *b = "welcome";
    const char *c = "come";
    const char *d = "home";

    printf("%p: %s\n", (void *)a, a);
    printf("%p: %s\n", (void *)b, b);
    printf("%p: %s\n", (void *)c, c);
    printf("%p: %s\n", (void *)d, d);
    return 0;
}

输出将显示:

  • 对于具有线性地址空间的体系结构,字符串常量在内存中的相对顺序
  • a指向的字符串d在内存中是否相同或者不同。
  • 该字符串"come"是字符串的后缀"welcome"还是单独的字符串常量。

使用相同或不同的编译器,在相同或不同的目标上多次尝试此程序…即使在同一台机器上再次运行相同的可执行文件,输出也可能不同。不保证。

我想了解如何实现这一执行。

至少有三种方式可以思考这个问题:

  1. 正如其他几个答案所解释的那样,这里的行为是undefined,这意味着几乎任何事情都可能发生。程序可以打印“welcome”。或者,从理论上讲,它可以打印“hello”,或“goodbye”,或“djwjnsdvjnvjsd”,或者什么都不打印,或者什么都不打印。
  2. 是的,编译器确实倾向于将字符串存储在内存的大致相同部分。因此,如果一个字符串以这种方式“紧挨着”另一个字符串,也就不足为奇了。
  3. 你显然不会希望在“真实”程序中依赖这种东西。它今天可能看起来能用,但实际上却不是因为正确的原因而起作用,所以它明天可能会停止工作。

另一个问题是,你在哪里“遇到”了这段代码,它的作者试图传授什么教训?希望这段代码不是作为优秀代码的示例呈现的,你希望在自己的工作中效仿。据推测,它被呈现为一种好奇心,作者认为你可能会从中学到一些东西。(然后,你可能学到的教训是否是有价值的,那是另一回事。)

不,它们不必如此。此外,如果其中一个字符串文字是另一个字符串文字的后缀,则编译器允许重叠字符串文字,因此您不能假设它们会具有某种相对位置。顺便说一句,编译器不会强制将字符串文字连续放置,可以强制执行某种排列(引入间隙),可以按源位置的反向顺序放置它们(因此它们不必一个接一个地立即出现或按照它们在源中出现的顺序出现),或者可以根据其他因素定位它们(​​例如,编译器可以允许您修改字符串文字,根据生成的代码中的使用情况触发不在内存中重叠它们,这只是一个想法,并不是说任何编译器都使用它)

此外,来自编译单元的字符串文字可以位于最终可执行文件的不同部分,因此内存位置完全取决于链接器如何处理它们。

想要了解这个执行是如何实现的

由于代码不可编译,因此无法执行代码。但无论如何,您使用的表达式 ( *(a-3)) 实际上等同于a[-3]或甚至(-3)[a] — 这是最后一个遗留结构,仅说明加法的交换性,但可以追溯到第一个编译器的时代,所以请不要*(a-3)在您想说时使用a[-3]) 指向字符串文字数组之外的三个位置。如果您将代码更正为可编译,则仍然可以执行,因为在 C 中没有进行数组边界检查(出于效率原因),这就是触发未定义行为的原因。未定义行为意味着可以生成可执行代码,但执行结果根本无法预测。

由于代码语法不正确,我假设(可能是错误的)两个字符串文字都在同一个编译单元中。因此,只有当其中一个字符串文字恰好是另一个字符串文字的后缀(它们不是)时,编译器才能重叠这两个字符串文字,因此这些字符串文字永远不会重叠。