是什么让指针的使用变得不可预测?

时间:2015-08-04 18:19:33

标签: c++ pointers

我目前正在学习指针,我的教授提供了这段代码作为例子:

//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;
}

他在评论中写道,我们无法预测该计划的行为。究竟是什么让它变得无法预测?我认为没有错。

6 个答案:

答案 0 :(得分:124)

程序的行为是不存在的,因为它是不正确的。

char* s = "My String";

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

正确的行是:

const char* s = "My String";

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

答案 1 :(得分:81)

答案是:它取决于你正在编译的C ++标准。所有代码都完美地符合所有标准‡,除了这一行:

char * s = "My String";

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

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

但是,在pre-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's answer

答案 2 :(得分:20)

其他答案表明,由于将const char数组分配给char *,因此该程序在C ++ 11中格式不正确。

然而,该程序在C ++ 11之前也是格式不正确的。

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

从历史上看,大多数实现都有iostream包括ostream,可能是为了便于实施,或者为了提供更好的QoI。

iostream只能定义ostream类而不定义operator<<重载。

答案 3 :(得分:13)

我在这个程序中看到的唯一稍微错误的事情是你不应该将字符串文字分配给可变char指针,尽管这通常被接受为编译器扩展。

否则,这个程序对我来说似乎很明确:

  • 当作为参数(例如cout << s2)传递时,规定字符数组如何成为字符指针的规则是明确定义的。
  • 数组以空值终止,这是operator<< char*(或const char*)的条件。
  • #include <iostream>包含<ostream>,后者又定义operator<<(ostream&, const char*),因此所有内容都显示在原位。

答案 4 :(得分:12)

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

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

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

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

答案 5 :(得分:10)

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

虽然一个好的编译器应该在它退出之前始终确保它所生成的任何目标文件有效,不存在或可识别为无效,但这些问题不属于标准的管辖范围。虽然历史上(可能仍然是)某些平台上的编译失败会导致合法出现的可执行文件在加载时以任意方式崩溃(并且我不得不使用链接错误通常具有此类行为的系统) ,我不会说语法错误的后果通常是不可预测的。在一个好的系统上,尝试构建通常会产生一个可执行文件,编译器在代码生成时尽最大努力,或者根本不会生成可执行文件。一些系统会在构建失败后留下旧的可执行文件,因为在某些情况下能够运行上一次成功构建可能会有用,但这也会导致混淆。

我个人倾向于基于磁盘的系统重命名输出文件,以允许在可执行文件有用的极少数情况下,同时避免因错误地认为运行新代码而导致的混淆,以及嵌入式编程系统,允许程序员为每个项目指定一个程序,如果在正常名称下没有有效的可执行文件,那么应该加载该程序[理想情况是安全地指示缺少可用程序的东西]。嵌入式系统工具集通常无法知道这样的程序应该做什么,但在许多情况下,为系统编写“真实”代码的人可以访问一些硬件测试代码,这些代码可以很容易地适应目的。我不知道我已经看到了重命名行为,但我知道我没有看到指示的编程行为。