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


108

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

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

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


2
Вы уверены, что правильно воспроизвели код профессора? Хотя формально можно утверждать, что эта программа может вызывать «непредсказуемое» поведение, в этом нет смысла. И я сомневаюсь, что какой-либо профессор использовал бы что-то настолько эзотерическое, чтобы проиллюстрировать студентам «непредсказуемое».
AnT

1
@Lightness Races in Orbit: компиляторам разрешено "принимать" некорректный код после выдачи необходимых диагностических сообщений. Но спецификация языка не определяет поведение кода. Т.е. из-за ошибки инициализации sпрограмма, принятая каким-либо компилятором, формально ведет себя непредсказуемо.
AnT

2
@TheParamintageCroissant: Нет. Инициализация в наше время плохо сформирована.
Гонки легкости на орбите

2
@ Парамагнитный круассан: Как я сказал выше, язык не требует плохо сформированного кода, чтобы "не удалось скомпилировать". Компиляторы просто обязаны выдать диагностику. После этого им разрешается продолжить и «успешно» скомпилировать код. Однако поведение такого кода не определяется спецификацией языка.
AnT

2
Я хотел бы знать, какой ответ дал вам ваш профессор.
Даниэль В. Кромптон,

Ответы:


125

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

char* s = "My String";

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

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

const char* s = "My String";

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


10
с -pedantic это делает: main.cpp: 6: 16: предупреждение: ISO C ++ запрещает преобразование строковой константы в 'char *' [-Wpedantic]
marcinj

17
@black: Нет, тот факт, что преобразование является незаконным, делает программу некорректной. Он осуждался в прошлом . Мы больше не в прошлом.
Гонки легкости на орбите

17
(Что глупо, потому что это было целью 12-летнего прекращения поддержки)
Lightness Races in Orbit

17
@black: поведение плохо сформированной программы не является «полностью определенным».
Гонки легкости на орбите

11
Тем не менее, речь идет о C ++, а не о какой-то конкретной версии GCC.
Гонки легкости на орбите

81

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

char * s = "My String";

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

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

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

Строковый литерал (2.13.4), который не является широким строковым литералом, может быть преобразован в rvalue типа « указатель на char »; [...]. В любом случае результатом будет указатель на первый элемент массива. Это преобразование учитывается только при наличии явного соответствующего целевого типа указателя, а не тогда, когда есть общая потребность в преобразовании из lvalue в rvalue. [Примечание: это преобразование устарело . См. Приложение 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.


† Хотя, опять же, по определению плохо сформированный код не дает ожидаемого разумного поведения
‡ За исключением того, что см . Ответ Мэтта Макнабба.


Я думаю, что здесь «непредсказуемый» подразумевается профессором, чтобы означать, что нельзя использовать стандарт, чтобы предсказать, что компилятор будет делать с плохо сформированным кодом (помимо выдачи диагностики). Да, он мог бы относиться к этому так, как С ++ 03 говорит, что это должно быть обработано, и (рискуя ошибиться "не истинным шотландцем") здравый смысл позволяет нам с некоторой уверенностью предсказать, что это единственное, что разумный компилятор-писатель когда-либо будет выбирать, компилируется ли код вообще. С другой стороны, он может рассматривать это как значение обратного строкового литерала перед его преобразованием в неконстантный. Стандартный C ++ не заботится.
Стив Джессоп,

2
@SteveJessop Я не верю в такую ​​интерпретацию. Это не неопределенное поведение и не категория плохо сформированного кода, который стандарт помечает как не требующий диагностики. Это простое нарушение системы типов, которое должно быть очень предсказуемым (компилируется и работает нормально на C ++ 03, не компилируется на C ++ 11). Вы не можете использовать ошибки компилятора (или художественные лицензии), чтобы утверждать, что код непредсказуем - иначе весь код был бы тавтологически непредсказуем.
Барри

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

1
Нет. Стандарт не определяет поведение плохо сформированных программ.
Стив Джессоп,

1
@supercat: это справедливо, но я не верю, что это главная причина. Я думаю, что основная причина, по которой стандарт не определяет поведение плохо сформированных программ, заключается в том, что компиляторы могут поддерживать расширения языка, добавляя синтаксис, который не является правильно сформированным (как это делает Objective C). Разрешение реализации сделать полную уборку после неудачной компиляции - это просто бонус :-)
Стив Джессоп

20

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

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

В operator<<перегруженных в <ostream>. Требование iostreamвключения ostreamбыло добавлено в C ++ 11.

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

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


13

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

В остальном эта программа кажется мне четко определенной:

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

12

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

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

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

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


10

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

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

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

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.