是什么使这种指针的使用无法预测?


108

我目前正在学习指针,我的教授提供了以下代码作为示例:

//We cannot predict the behavior of this program!

#include <iostream>
using namespace std;

int main()
{
    char * s = "My String";
    char s2[] = {'a', 'b', 'c', '\0'};

    cout << s2 << endl;

    return 0;
}

他在评论中写道,我们无法预测程序的行为。究竟是什么使得它不可预测?我认为没有错。


2
您确定您正确地复制了教授的代码吗?尽管从形式上可能会争辩说该程序可能产生“不可预测的”行为,但这样做是没有意义的。而且我怀疑任何一位教授都会使用如此神秘的东西向学生说明“不可预测的”。
AnT

1
@Lightness Races in Orbit:在发出所需的诊断消息后,允许编译器“接受”格式错误的代码。但是语言规范并没有定义代码的行为。即由于的初始化错误s,程序如果被某些编译器接受,则正式地具有不可预测的行为。
AnT

2
@TheParamagneticCroissant:否。在现代,初始化格式不正确。
Lightness Races in Orbit

2
@顺磁性牛角包:正如我上面所说,该语言不需要格式错误的代码即可“无法编译”。只需要求编译器发出诊断信息。之后,允许他们继续操作并“成功”编译代码。但是,语言规范未定义此类代码的行为。
AnT

2
我很想知道您的教授给您的答案是什么。
丹尼尔W.康普顿

Answers:


125

该程序的行为不存在,因为它的格式不正确。

char* s = "My String";

这是非法的。在2011年之前,已弃用12年。

正确的行是:

const char* s = "My String";

除此之外,该程序还可以。您的教授应该少喝威士忌!


10
使用-pedantic可以做到:main.cpp:6:16:警告:ISO C ++禁止将字符串常量转换为'char *'[-Wpedantic]
marcinj

17
@black:不,转换是非法的事实使程序格式错误。过去不推荐使用。我们不再是过去。
Lightness Races in Orbit

17
(这是愚蠢的,因为那是12年弃用的目的)
轻轨比赛(于

17
@black:格式不正确的程序的行为 “完全定义”。
Lightness Races in Orbit

11
无论如何,问题在于C ++,而不是GCC的某些特定版本。
Lightness Races in Orbit

81

答案是:这取决于您要编译的C ++标准。所有代码在所有标准中的格式都完全正确‡,以下行除外:

char * s = "My String";

现在,字符串文字具有类型const char[10],我们正在尝试初始化指向它的非常量指针。对于除char字符串文字系列之外的所有其他类型,此类初始化始终是非法的。例如:

const int arr[] = {1};
int *p = arr; // nope!

但是,在C ++ 11之前的版本中,对于字符串文字,第4.2 / 2节中有一个例外:

可以将不是宽字符串文字的字符串文字(2.13.4)转换为类型为“ 指针 ”的右值;[...]。无论哪种情况,结果都是指向数组第一个元素的指针。仅当存在显式的适当的指针目标类型时才考虑此转换,而在通常需要从左值转换为右值时则不考虑这种转换。[注意:不建议使用此转换。见附件D. ]

因此,在C ++ 03中,代码非常完美(尽管已弃用),并且具有清晰可预测的行为。

在C ++ 11中,该块不存在-转换为的字符串文字没有这种例外char*,因此代码与int*我刚才提供的示例一样格式错误。编译器有义务发出诊断信息,理想情况下,在这种情况下(例如明显违反C ++类型系统的情况),我们希望好的编译器不仅在这方面符合要求(例如,发出警告),而且还会失败彻底

理想情况下,该代码不应编译-但应同时在gcc和clang上进行编译(我认为是因为尽管有十多年不赞成使用这种类型的系统漏洞,但仍有很多代码会以很少的收益被破坏)。代码格式错误,因此无法推断代码的行为。但是考虑到这种特定情况以及以前允许的历史记录,我认为将结果代码解释为隐式是不合理的const_cast,例如:

const int arr[] = {1};
int *p = const_cast<int*>(arr); // OK, technically

这样,该程序的其余部分就可以很好地工作了,因为您再也无需实际触摸s。通过非指针读取创建的const对象const完全可以。通过这样的指针一个创建const对象是未定义的行为:

std::cout << *p; // fine, prints 1
*p = 5;          // will compile, but undefined behavior, which
                 // certainly qualifies as "unpredictable"

由于无需s在代码中的任何地方进行修改,因此该程序在C ++ 03中很好,应该无法在C ++ 11中编译,但无论如何都可以进行-鉴于编译器允许这样做,因此在其中仍然没有未定义的行为† 。考虑到编译器仍在[错误地]解释C ++ 03规则,我认为没有什么会导致“不可预测的”行为。s尽管写,但所有的赌注都关闭了。在C ++ 03和C ++ 11中。


†尽管如此,顾名思义,格式错误的代码不会产生合理的行为预期
‡除非不是,请参阅Matt McNabb的回答


我认为教授在这里“不可预测”的意思是说,人们不能使用该标准来预测编译器将对格式错误的代码进行何种处理(除了发出诊断信息之外)。是的,它可以像C ++ 03所说的那样对待它,并且(冒“ No True Scotsman”谬误的风险)常识使我们可以自信地预测这是明智的编译器编写器唯一的事情会选择是否完全编译代码。再一次,它可以将其视为将字符串文字转换为非常量之前的含义。标准C ++不在乎。
史蒂夫·杰索普

2
@SteveJessop我不赞成这种解释。这既不是未定义的行为,也不是标准标记为不需要诊断的格式错误的代码类别。这是一个简单的类型系统冲突,应该非常可预测(在C ++ 03上编译并执行常规操作,而在C ++ 11上编译失败)。您不能真正使用编译器错误(或艺术许可证)来暗示代码不可预测-否则所有代码在重言式上都是不可预测的。
巴里

我不是在谈论编译器错误,而是在谈论标准是否定义了代码的行为(如果有)。我怀疑教授是否也在做同样的事情,“无法预测”只是一种saying断的说法,即现行标准并未定义行为。无论如何,对我而言,似乎比教授错误地认为这是一个格式正确的程序,行为不确定。
史蒂夫·杰索普

1
不,不是的。该标准未定义格式错误的程序的行为。
史蒂夫·杰索普

1
@supercat:这是一个公平的观点,但我不认为这是主要原因。我认为该标准未指定格式不正确的程序的行为的主要原因是,编译器可以通过添加格式不正确的语法来支持语言的扩展(如Objective C那样)。允许实现在编译失败后彻底清除所有麻烦:-)
史蒂夫·杰索普

20

其他答案已经涵盖了该程序在C ++ 11中的格式错误,原因是将const char数组分配给char *

但是,该程序在C ++ 11之前也是格式错误的。

operator<<重载是<ostream>。C ++ 11中添加了iostream对包含的要求ostream

从历史上看,无论如何,大多数实现都iostream包括在内ostream,可能是为了易于实现,或者可能是为了提供更好的QoI。

但是iostream只定义ostream类而不定义operator<<重载是符合要求的。


13

我在该程序中看到的唯一略有错误的事情是char,尽管通常将其作为编译器扩展接受,但您不应将字符串文字分配给可变指针。

否则,该程序对我来说定义明确:

  • 明确规定了将字符数组作为参数传递时如何变为字符指针的规则(例如with cout << s2)。
  • 该数组以null终止,这是operator<<使用char*(或const char*)的条件。
  • #include <iostream>包括<ostream>,反过来定义operator<<(ostream&, const char*),因此一切似乎都已就绪。

12

由于上述原因,您无法预测编译器的行为。(它应该无法编译,但可能无法编译。)

如果编译成功,则行为是明确定义的。您当然可以预测程序的行为。

如果编译失败,则没有程序。在编译语言中,程序是可执行文件,而不是源代码。如果没有可执行文件,就没有程序,也无法谈论不存在的东西的行为。

所以我想说你教授的说法是错误的。面对此代码,您无法预测编译器的行为,但这与程序的行为截然不同。因此,如果他要采摘尼特,最好确保他是对的。或者,当然,您可能错误地引用了他,而错误在于您对他所说内容的翻译。


10

正如其他人指出的那样,该代码在C ++ 11下是非法的,尽管在早期版本中有效。因此,需要用于C ++ 11的编译器发出至少一个诊断信息,但除此之外,未指定编译器或构建系统其余部分的行为。标准中的任何内容都不能禁止编译器因错误而突然退出,而留下链接器可能认为有效的部分编写的目标文件,从而导致可执行文件损坏。

尽管优秀的编译器应始终确保在退出之前确保预期产生的任何目标文件是有效的,不存在的或可识别为无效的,但此类问题不在标准的管辖范围之内。历史上曾经(而且可能仍然是)某些平台上的编译失败会导致出现合法外观的可执行文件,而这些可执行文件在加载时会以任意方式崩溃(而且我不得不在链接错误经常有这种行为的系统上工作) ,我不会说语法错误的后果通常不可预测。在一个好的系统上,尝试进行的构建通常将要么尽最大努力在编译器的代码生成中生成可执行文件,要么根本不生成可执行文件。某些系统在构建失败后会留下旧的可执行文件,

我个人的偏爱是基于磁盘的系统重命名输出文件,以允许该可执行文件有用的极少数情况,同时避免因误认为一个人正在运行新代码而导致的混乱,以及嵌入式编程系统允许程序员为每个项目指定一个程序,如果在正常名称下没有有效的可执行文件,则应加载该程序(理想情况下可以安全地表明缺少可用程序)。嵌入式系统工具集通常无法知道此类程序应该做什么,但是在许多情况下,为系统编写“真实”代码的人将可以访问一些硬件测试代码,这些代码可以轻松地适应于目的。我不知道我是否看到过重命名行为,

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.