繁体   English   中英

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

[英]What makes this usage of pointers unpredictable?

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

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

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

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

char* s = "My String";

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

正确的是:

const char* s = "My String";

除此之外,该计划很好。 你的教授应少喝威士忌!

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

char * s = "My String";

现在,字符串文字具有类型const char[10] ,我们正在尝试初始化一个非const指针。 对于除字符串文字的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的回答

其他答案已经涵盖了由于将const char数组赋值给char * ,这个程序在C ++ 11中char *

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

operator<< overloads在<ostream> 在C ++ 11中添加了iostream包含ostream的要求。

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

但是, iostream只能定义ostream类而不定义operator<< overloads。

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

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

  • 当作为参数(例如cout << s2 )传递时,规定字符数组如何成为字符指针的规则是明确定义的。
  • 该数组以null结尾,这是operator<< with char* (或const char* )的条件。
  • #include <iostream>包含<ostream> ,后者又定义了operator<<(ostream&, const char*) ,所以一切都显示在原位。

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

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

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

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

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

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM