C/C++:指向字符串常量的指针优化

15

看一下这段代码:

#include <iostream>
using namespace std;

int main()
{
    const char* str0 = "Watchmen";
    const char* str1 = "Watchmen";
    char* str2 = "Watchmen";
    char* str3 = "Watchmen";

    cerr << static_cast<void*>( const_cast<char*>( str0 ) ) << endl;
    cerr << static_cast<void*>( const_cast<char*>( str1 ) ) << endl;
    cerr << static_cast<void*>( str2 ) << endl;
    cerr << static_cast<void*>( str3 ) << endl;

    return 0;
}

它会产生以下输出:

0x443000
0x443000
0x443000
0x443000

这是在运行于Cygwin下的g++编译器上。即使没有打开任何优化选项(-O0),指针仍然指向相同的位置。

编译器是否总是会优化到搜索所有字符串常量以查看它们是否相等?能否依赖这种行为?


1
请记住字符串常量是只读的。因此,优化是完全有效的。如果您想要可编辑的字符串,您需要将它们声明为数组而不是指针。然后您将获得不同的地址。 - Martin York
6个回答

31

它不能被依赖,它是一种优化,不属于任何标准。

我已经修改了你代码中相应的行:

const char* str0 = "Watchmen";
const char* str1 = "atchmen";
char* str2 = "tchmen";
char* str3 = "chmen";

输出-O0优化级别的结果为:
0x8048830
0x8048839
0x8048841
0x8048848

但对于 -O1,它是:

0x80487c0
0x80487c1
0x80487c2
0x80487c3

正如您所见,GCC(v4.1.2)在所有后续的子字符串中重复使用第一个字符串。编译器可以自行选择如何在内存中排列字符串常量。


我在Cygwin上尝试使用g++ v4.3.2,但没有看到所有指针都偏移到同一个字符串常量的行为。 - Ashwin Nanjappa
@Ash:我在GCC 3.4.6上再次尝试了一下,行为仍然存在。你是如何编译代码的?当启用优化>=O1时,行为会发生。 - anon
这似乎是平台的问题。我这次在Linux上尝试了一下(而不是Cygwin),看到了优化的行为。再次感谢分享这个信息 :-) - Ashwin Nanjappa

17

这是一种非常简单的优化方法,可能大多数编译器作者甚至不认为它是一种优化。毕竟将优化标志设置为最低级别并不意味着“完全天真无知”。

编译器在合并重复字符串文字时的侵略性会有所不同。他们可能限制自己在单个子程序中处理 —— 将这四个声明放在不同的函数中而不是一个函数中,则可能会看到不同的结果。其他编译器可能在整个编译单元中执行此操作。其他编译器可能依靠链接器,在多个编译单元之间进行进一步合并。

除非您特定编译器的文档说可以这样做,否则您不能依赖此行为。语言本身在这方面并没有要求。即使不考虑可移植性,我也会对在我的代码中依赖它感到谨慎,因为即使在单个供应商的编译器的不同版本之间,行为也可能发生变化。


我喜欢你的表述。对我来说,“不优化”只是意味着“不要做任何可能使调试更困难的事情”。 - T.E.D.
2
@T.E.D.:我不明白这会让调试变得更困难。毕竟,指针仍然指向正确的字符串内容。 - rubenvb

12

你肯定不应该依赖这种行为,但大多数编译器会这样做。任何字面值(“Hello”,42等)都将被存储一次,并且指向它的任何指针自然会解析到该单个引用。

如果你发现需要依赖这个,请安全地重构代码如下:

char *watchmen = "Watchmen";
char *foo = watchmen;
char *bar = watchmen;

5
这句话的意思是:它是const char*类型(吹毛求疵地说,应该加上分号) - rubenvb

9

当然,你不能指望它一定如此。优化器可能对你进行一些巧妙的操作,而应该允许它这样做。

但这种情况非常普遍。我还记得在1987年,一个同学使用DEC C编译器时出现了奇怪的bug,他所有的字面值3都变成了11(数字可能已更改以保护无辜者)。他甚至执行printf ("%d\n", 3),打印的是11

他叫我过去看这个奇怪的问题(为什么这让人想起我?),经过大约30分钟的思考,我们找到了原因。有一行代码大致是这样的:

if (3 = x) break;

请注意单个“=”字符。是的,那是一个笔误。编译器出了点小问题,导致它允许这种情况发生。结果就是把整个程序中所有的字面量3都变成了此时x中的任何值。
总之,很明显C编译器将所有的字面量3放在同一个地方。如果80年代的C编译器能够做到这一点,那么这应该是非常普遍的。

哇——80年代中后期的DEC编译器允许将值分配给文字常量? - Peter
我看到这种情况发生了。如果学校没有采取任何措施或者类似的补丁,我不会感到惊讶。 - T.E.D.
谢谢你分享过去的小知识,Ted。那真是太酷了! :-) - Ashwin Nanjappa
2
+1 是对不写可怕的代码,如 if (0 == var) 的最佳理由。 - R.. GitHub STOP HELPING ICE

5
我不会依赖这种行为,因为我怀疑C或C++标准是否会明确规定这种行为,但编译器这样做是有道理的。即使在没有向编译器指定任何优化的情况下,它表现出这种行为也是有道理的;其中没有任何权衡。
在C或C++中,所有字符串字面值(例如"string literal")都是只读的,因此是常量。当你这样说:
char *s = "literal";

您在某种程度上将字符串降级为非const类型。但是,您无法摆脱字符串的只读属性:如果尝试操作它,您将在运行时而不是编译时被捕获。(这实际上是使用“const char *”将字符串字面量分配给变量的好理由。)

2
不,它并不可靠,但将只读字符串常量存储在池中是一种相当简单且有效的优化。只需存储一个字符串的字母表列表,然后在最后将它们输出到对象文件中即可。想想平均代码库中有多少"\n"或""常量。
如果编译器想要更加花哨,它还可以重复使用后缀:"\n"可以通过指向"Hello\n"的最后一个字符来表示。但这可能带来很少的好处,却增加了很大的复杂性。
无论如何,我不相信标准实际上对任何东西存储的位置有任何规定。这将是一个非常特定于实现的事情。如果你把两个这样的声明放在一个单独的.cpp文件中,那么事情也会发生变化(除非你的编译器进行了重要的链接工作)。

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