Что делает это использование указателей непредсказуемым?

В настоящее время я изучаю указатели, и мой профессор предоставил этот фрагмент кода в качестве примера:

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

В комментариях он писал, что мы не можем предсказать поведение программы. Что именно делает его непредсказуемым? Я не вижу в этом ничего плохого.

Ответ 1

Поведение программы не существует, потому что оно плохо сформировано.

char* s = "My String";

Это незаконно. До 2011 года он устарел в течение 12 лет.

Правильная строка:

const char* s = "My String";

Кроме этого, программа в порядке. Ваш профессор должен пить меньше виски!

Ответ 2

Ответ: это зависит от того, какой стандарт С++ вы компилируете. Весь код отлично сформирован по всем стандартам и кинжалу; за исключением этой строки:

char * s = "My String";

Теперь строковый литерал имеет тип const char[10], и мы пытаемся инициализировать его указатель не const. Для всех других типов, отличных от семейства строковых литералов char, такая инициализация всегда была незаконной. Например:

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

Однако в pre-С++ 11 для строковых литералов в §4.2/2 было исключение:

Строковый литерал (2.13.4), который не является широким строковым литералом, может быть преобразован в rvalue типа " указатель на char"; [...]. В любом случае результатом является указатель на первый элемент массива. Это преобразование рассматривается только тогда, когда существует явный соответствующий целевой тип указателя, а не когда требуется общая конвертация из lvalue в rvalue. [Примечание: это преобразование устарело. См. Приложение D.]

Итак, в С++ 03 код отлично подходит (хотя и устарел) и имеет четкое, предсказуемое поведение.

В С++ 11 этот блок не существует - для строковых литералов, преобразованных в char*, такого исключения не существует, поэтому код так же плохо сформирован, как и пример int*, который я только что представил. Компилятор обязан выдать диагностику, и в идеале в таких случаях, которые являются явными нарушениями системы типа С++, мы ожидаем, что хороший компилятор не будет соответствовать только в этом отношении (например, путем выдачи предупреждения), но для отказа вчистую.

Код в идеале не компилируется, но делает это как для gcc, так и для clang (я предполагаю, что там, вероятно, есть много кода, который будет разбит с небольшим усилением, несмотря на то, что системное отверстие этого типа устарело более десяти лет). Код плохо сформирован, и поэтому не имеет смысла рассуждать о том, каково поведение кода. Но, учитывая этот конкретный случай и историю его ранее разрешенного, я не считаю, что это необоснованное растягивание для интерпретации полученного кода, как если бы оно было неявным const_cast, что-то вроде:

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

При этом остальная часть программы отлично подходит, так как вы никогда не нажимаете s снова. Чтение объекта created- const с помощью указателя не const отлично. Написание созданного объекта const с помощью такого указателя имеет поведение undefined:

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

Поскольку в любом коде вашего кода нет изменений через s, программа в порядке С++ 03, не должна компилироваться в С++ 11, но в любом случае - и учитывая, что компиляторы разрешают это, no undefined поведение в нем и кинжал;. С учетом того, что компиляторы все еще [неправильно] интерпретируют правила С++ 03, я не вижу ничего, что могло бы привести к "непредсказуемому" поведению. Напишите на s, хотя и все ставки отключены. В С++ 03 и С++ 11.


& dagger; Хотя, опять же, по определению плохо сформированный код не дает ожиданий разумного поведения
& Dagger; Кроме того, см. Мэтт Макнабб ответ

Ответ 3

Другие ответы показали, что эта программа плохо сформирована в С++ 11 из-за назначения массива const char в char *.

Однако программа была плохо сформирована и до С++ 11.

Перегрузки operator<< находятся в <ostream>. Требование iostream включить ostream было добавлено в С++ 11.

Исторически, большинство реализаций iostream включали ostream в любом случае, возможно, для простоты реализации или, возможно, для обеспечения лучшего QoI.

Но для iostream было бы соответствовать только определение класса ostream без определения перегрузок operator<<.

Ответ 4

Единственная незначительная ошибка, которую я вижу в этой программе, заключается в том, что вы не должны назначать строковый литерал для изменяемого указателя char, хотя это часто принимается как расширение компилятора.

В противном случае эта программа выглядит для меня четко:

  • Правила, которые определяют, как массивы символов становятся символьными указателями при передаче в качестве параметров (например, с cout << s2), четко определены.
  • Массив с нулевым завершением, что является условием для operator<< с char* (или a const char*).
  • #include <iostream> включает <ostream>, который, в свою очередь, определяет operator<<(ostream&, const char*), поэтому все кажется на месте.

Ответ 5

Вы не можете предсказать поведение компилятора по причинам, указанным выше. (Он не должен компилироваться, но может и не быть.)

Если компиляция завершается успешно, то поведение корректно определено. Вы, конечно, можете предсказать поведение программы.

Если он не скомпилирован, программы нет. На скомпилированном языке программа является исполняемым, а не исходным кодом. Если у вас нет исполняемого файла, у вас нет программы, и вы не можете говорить о поведении чего-то, чего не существует.

Итак, я бы сказал, что ваше выражение prof неверно. Вы не можете предсказать поведение компилятора при столкновении с этим кодом, но это отличается от поведения программы. Поэтому, если он собирается собирать гниды, ему лучше убедиться, что он прав. Или, конечно же, вы могли бы неправильно объяснить его, и ошибка в вашем переводе того, что он сказал.

Ответ 6

Как отмечали другие, код является незаконным в С++ 11, хотя он был действителен в более ранних версиях. Следовательно, компилятор для С++ 11 должен выдавать хотя бы одну диагностику, но поведение компилятора или оставшейся части системы сборки не указано далее. Ничто в Стандарте не запретило бы компилятору внезапно выйти из строя в ответ на ошибку, оставив частично написанный объектный файл, который, по мнению компоновщика, был бы действительным, давал бы сломанный исполняемый файл.

Хотя хороший компилятор всегда должен гарантировать, что до того, как он выйдет, любой объектный файл, который, как ожидается, будет выпущен, будет действительным, несуществующим или распознаваемым как недействительным, такие проблемы выходят за рамки юрисдикции Стандарта. Хотя исторически были (и могут быть все еще) некоторые платформы, где неудачная компиляция может привести к появлению законно возникающих исполняемых файлов, которые могут быть повреждены произвольно при загрузке (и мне приходилось работать с системами, в которых ошибки ссылок часто имели такое поведение), Я бы не сказал, что последствия синтаксических ошибок обычно непредсказуемы. В хорошей системе попытка сборки обычно создает исполняемый файл с максимальным усилием компилятора при генерации кода или вообще не создает исполняемый файл. Некоторые системы оставят позади старый исполняемый файл после неудачной сборки, поскольку в некоторых случаях возможность запуска последней успешной сборки может быть полезна, но это также может привести к путанице.

Мое личное предпочтение было бы для дисковых систем для переименования выходного файла, чтобы разрешать редкие случаи, когда этот исполняемый файл был бы полезен, избегая путаницы, которая может возникнуть из-за ошибочной веры, что она запускает новый код, и для встроенные системы программирования, позволяющие программисту указывать для каждого проекта программу, которая должна быть загружена, если действительный исполняемый файл недоступен под нормальным именем [в идеале это то, что безопасно указывает на отсутствие полезной программы]. Набор инструментов встраиваемых систем обычно не имеет никакого представления о том, что должна делать такая программа, но во многих случаях кто-то, кто пишет "настоящий" код для системы, будет иметь доступ к некоторому аппаратно-тестовому коду, который можно легко адаптировать к цель. Однако я не знаю, что видел поведение переименования, и я знаю, что я не видел указанного поведения программирования.