是什么让这种指针的使用变得不可预测?
我现在正在学习指针,而我的教授提供这段代码就是一个例子:
//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; }
他在评论中写道,我们无法预测该计划的行为。 究竟是什么让它变得不可预测? 我没有看到任何错误。
该scheme的行为是不存在的,因为它是不健全的。
char* s = "My String";
这是非法的。 在2011年之前,它已经被弃用了12年。
正确的路线是:
const char* s = "My String";
除此之外,该计划是好的。 你的教授应该less喝点威士忌!
答案是:这取决于你编译的C ++标准。 所有的代码在所有的标准中都是完美的 – 除了这一行:
char * s = "My String";
现在,string常量的types是const char[10]
,我们试图初始化一个非const的指针。 对于string文字族以外的所有其他types,这种初始化总是非法的。 例如:
const int arr[] = {1}; int *p = arr; // nope!
但是,在pre-C ++ 11中,对于string文本,在§4.2/ 2中有一个例外:
不是宽string文字的string文字(2.13.4)可以转换为“ 指向char的指针 ”types的右值。 […]。 无论哪种情况,结果都是指向数组的第一个元素的指针。 这种转换只有在有明确的适当的指针目标types时才被考虑,而不是在一般需要将左值转换为右值时。 [注意:此转换已被弃用 。 见附件D.
所以在C ++ 03中,代码是完全正确的(虽然不推荐使用),并且具有清晰的可预测的行为。
在C ++ 11中,该块不存在 – string文本没有转换为char*
例外,所以代码和我刚才提供的int*
例子一样。 编译器有义务发出一个诊断信息,理想情况下,在这种明显违反C ++types系统的情况下,我们希望一个好的编译器不仅要符合这个要求(例如发出警告),还要顾左右而言他。
该代码理想情况下不应该编译 – 但在gcc和clang(我假设,因为可能有很多代码将被打破,收益甚微,尽pipe这种types的系统漏洞已被废弃十多年)。 代码是格式不正确的,因此推断代码的行为可能是没有意义的。 但是考虑到这个特定的情况以及它以前被允许的历史,我不认为这是一个不合理的延伸,将结果代码解释为一个隐含的const_cast
,如下所示:
const int arr[] = {1}; int *p = const_cast<int*>(arr); // OK, technically
有了这个,程序的其余部分是完全正确的,因为你再也不会碰到s
了。 通过非const
指针读取创build的const
对象是完全正确的。 通过这样的指针写一个创build的const
对象是未定义的行为:
std::cout << *p; // fine, prints 1 *p = 5; // will compile, but undefined behavior, which // certainly qualifies as "unpredictable"
由于在代码中没有任何修改,所以程序在C ++ 03中是正常的,在C ++ 11中编译失败,但是无论如何 – 在编译器允许的情况下,它仍然没有未定义的行为†。 由于编译器仍然[不正确]解释C ++ 03规则,我没有看到任何会导致“不可预知”行为的事情。 尽pipe如此,所有的赌注都没有了。 在C ++ 03和C ++ 11中。
†虽然从定义来看,不规范的代码不会产生合理行为的期望
‡除外,请参阅麦克纳布的回答
其他的答案已经涵盖了这个程序在C ++ 11格式不正确,因为一个const char
数组赋予一个char *
。
然而,这个程序在C ++ 11之前也是格式不对的。
operator<<
重载在<ostream>
。 iostream
包含ostream
的要求被添加到C ++ 11中。
从历史上看,大多数实现都包含iostream
无论如何也许是为了方便实现,或者为了提供更好的QoI。
但是它将符合iostream
只定义ostream
类而不定义operator<<
overloads。
我在这个程序中看到的唯一一个错误的东西是,你不应该把一个string字面赋值给一个可变的char
指针,尽pipe这经常被认为是一个编译器扩展。
否则,这个程序似乎对我来说是明确的:
- 规定字符数组如何作为parameter passing时变成字符指针的规则(例如
cout << s2
)是明确的。 - 该数组是空终止的,这是
operator<<
带有char*
(或const char*
)的条件。 -
#include <iostream>
包含<ostream>
,后者又定义了operator<<(ostream&, const char*)
,所以一切似乎都在原地。
出于上述原因,您无法预测编译器的行为。 (它不应该编译,但可能不会)。
如果编译成功,那么行为是明确的。 你当然可以预测程序的行为。
如果编译失败,就没有程序。 在编译语言中,程序是可执行文件,而不是源代码。 如果你没有一个可执行文件,你没有一个程序,你不能谈论不存在的东西的行为。
所以我想说你的教授的说法是错误的。 当面对这个代码时,你无法预测编译器的行为,但是这与程序的行为不同。 所以如果他要挑剔的话,他最好确定他是对的。 或者,当然,你可能错误地引用了他,错误在于你的翻译。
正如其他人所指出的那样,这个代码在C ++ 11下是非法的,尽pipe它在早期版本中是有效的。 因此,C ++ 11的编译器需要至less发布一个诊断信息,但编译器的行为或构build系统的其余部分在此之外是未指定的。 标准中的任何内容都不会禁止编译器突然退出以响应错误,而是留下一个链接器可能认为是有效的部分编写的对象文件,从而产生可执行文件。
虽然一个好的编译器应该总是保证在它退出之前它所期望产生的任何目标文件将是有效的,不存在的或可识别的无效的,这些问题不属于标准的pipe辖范围。 虽然历史上曾经(也可能是)一些平台,其中一个失败的编译可能会导致出现合法的可执行文件在加载时以任意方式崩溃(而且我必须使用链接错误经常出现这种行为的系统) ,我不会说语法错误的后果通常是不可预测的。 在一个好的系统上,一个试图编译通常会产生一个编译器在代码生成时尽最大努力的可执行文件,或者根本不会生成可执行文件。 有些系统会在构build失败后遗留旧的可执行文件,因为在某些情况下能够运行最后的成功构build可能是有用的,但这也会导致混淆。
我个人的偏好是基于磁盘的系统重新命名输出文件,以便在可能的情况下执行这个可执行文件,同时避免因错误地相信一个人正在运行新代码而导致的混淆,以及embedded式编程系统允许程序员为每个项目指定一个应该加载的程序,如果一个有效的可执行文件在正常的名字下是不可用的(理想情况是安全地表示没有可用的程序)。 embedded式系统工具集通常无法知道这样的程序应该做什么,但是在许多情况下,为系统编写“真实”代码的人可以访问一些硬件testing代码,这些代码很容易适应目的。 我不知道我已经看到了重命名的行为,但是我知道我没有看到指定的编程行为。