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

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

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

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

108 голосов | спросил trungnt 4 PM00000090000003331 2015, 21:19:33

6 ответов


0

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

char* s = "My String";

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

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

const char* s = "My String";

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

ответил Lightness Races in Orbit 4 PM00000090000001031 2015, 21:25:10
0

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


† Хотя опять-таки по определению некорректно сформированный код не дает никаких ожиданий разумного поведения
‡ За исключением случаев, см. ответ Мэтта Макнабба
ответил Barry 4 PM00000090000004331 2015, 21:50:43
0

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

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

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

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

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

ответил M.M 5 AM00000020000005831 2015, 02:22:58
0

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

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

  • Правила, определяющие, как символьные массивы становятся символьными указателями при передаче в качестве параметров (например, с помощью cout << s2), хорошо определены. /li>
  • Массив завершается нулем, что является условием для operator<< с char* (или const char*).
  • #include <iostream> включает в себя <ostream>, который, в свою очередь, определяет operator<<(ostream&, const char*), поэтому все выглядит на своих местах.
ответил zneak 4 PM00000090000000331 2015, 21:25:03
0

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

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

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

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

ответил Graham 5 AM00000030000005231 2015, 03:00:52
0

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

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

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

ответил supercat 5 AM00000010000001331 2015, 01:26:13

Похожие вопросы

Популярные теги

security × 330linux × 316macos × 2827 × 268performance × 244command-line × 241sql-server × 235joomla-3.x × 222java × 189c++ × 186windows × 180cisco × 168bash × 158c# × 142gmail × 139arduino-uno × 139javascript × 134ssh × 133seo × 132mysql × 132