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

tru*_*gnt 108 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;
}
Run Code Online (Sandbox Code Playgroud)

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

Lig*_*ica 124

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

char* s = "My String";
Run Code Online (Sandbox Code Playgroud)

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

正确的是:

const char* s = "My String";
Run Code Online (Sandbox Code Playgroud)

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

  • @black:不,转换是非法的这一事实会导致程序格式不正确.它在过去被弃用了.我们不再是过去了. (17认同)
  • (这是愚蠢的,因为这是12年弃用的目的) (17认同)
  • @black:不正确的程序的行为是_not_"完美定义". (17认同)
  • 无论如何,问题是关于C++,而不是关于某些特定版本的GCC. (11认同)
  • 使用-pedantic:main.cpp:6:16:警告:ISO C++禁止将字符串常量转换为'char*'[ - Wedantic] (10认同)
  • @DanielJour:您需要更好的警告设置.`-Wall -Wextra -pedantic` plzkthx (4认同)
  • @black为了澄清,它在C++ 11之前被弃用了.从C++ 11开始,它完全是非法的,不应该按照标准编译.目前的编译器似乎仍然允许它(他们可能不想破坏代码). (3认同)
  • @black [GCC 6默认为C++ 14](https://gcc.gnu.org/gcc-6/changes.html) (3认同)
  • @black gcc 4默认为gnu ++ 98.我认为gcc 5也有 (3认同)
  • 听起来你的教授在过去几年没有更新他的课程资料...... (2认同)

Bar*_*rry 81

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

char * s = "My String";
Run Code Online (Sandbox Code Playgroud)

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

const int arr[] = {1};
int *p = arr; // nope!
Run Code Online (Sandbox Code Playgroud)

但是,在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
Run Code Online (Sandbox Code Playgroud)

有了这个,程序的其余部分完全没问题,因为你再也没有真正接触s过.通过非指针读取创建的const对象const是完全可以的.通过这样的指针编写一个创建const对象是未定义的行为:

std::cout << *p; // fine, prints 1
*p = 5;          // will compile, but undefined behavior, which
                 // certainly qualifies as "unpredictable"
Run Code Online (Sandbox Code Playgroud)

由于s代码中的任何地方都没有修改,所以程序在C++ 03中很好,但是无法在C++ 11中编译但是无论如何 - 并且考虑到编译器允许它,它仍然没有未定义的行为† .由于允许编译器仍[错误地]解释C++ 03规则,我认为没有任何会导致"不可预测"的行为.写到,s但所有的赌注都关闭了.在C++ 03和C++ 11中.


†虽然,根据定义,不正确的代码不会产生合理行为的期望
‡除非没有,请参阅Matt McNabb的答案

  • @SteveJessop我不买那个解释.这既不是未定义的行为,也不是标准标记为无需诊断的错误代码类别.这是一个简单的类型系统违规应该是非常可预测的(在C++ 03上编译和做正常的事情,无法在C++ 11上编译).您无法真正使用编译器错误(或艺术许可证)来表明代码是不可预测的 - 否则所有代码都会在实际上无法预测. (2认同)

M.M*_*M.M 20

其他答案已经涵盖了这个程序在C++ 11中的格式不正确,因为将const char数组分配给了char *.

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

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

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

但它只符合iostream定义ostream类而不定义operator<<重载.


zne*_*eak 13

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

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

  • 当作为参数(例如with cout << s2)传递时,规定字符数组如何成为字符指针的规则是明确定义的.
  • 该数组是以null结尾的,这是operator<<char*(或a const char*)的条件.
  • #include <iostream>包括<ostream>,反过来定义operator<<(ostream&, const char*),所以一切似乎都到位.


Gra*_*ham 12

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

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

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

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


sup*_*cat 10

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

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

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