Понимание терминологии C, взгляд для пустышек

Рассмотрим следующий пример, давайте назовем этот tcode1 для дальнейшего использования.

/*tocode1.c*/
void func(int a, int b);
void f(int i);
void func( int a, int b){
 printf(“%d %d”, a, b)
}
void f(int i) {
 func(i++, i);
}
int main(void){
 f(5);
}

Этот гипотетический пример возник во время обсуждения за обедом в моем офисе, когда друг сказал, что он ожидал, что a будет 6, b будет 5, но он получил a = 5 и b = 6. Я знал, что порядок, в котором аргументы передаются функции, не указан, поэтому это могло быть a = 6 или b = 5. Но затем мой друг подробно расспросил меня о неопределенном поведении и какова логика в том, чтобы делать эти вещи неопределенными в первую очередь, и тогда я понял, что я знаю только жаргон, но на самом деле не понимаю их. Я не мог передать ему значение терминов. Итак, я снова продолжил свое исследование, и оказалось, что в tcode1 вызов func — это «неопределенное поведение», что плохо; как плохо? Давайте разберемся.

Начнем с того, почему я подумал, что это неуказанное поведение, из стандарта C99 3.4.4.

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

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

поведение, определяемое реализацией — это неопределенное поведение, при котором каждая реализация документирует, как был сделан выбор.

Примером поведения, определяемого реализацией, является распространение старшего бита, когда целое число со знаком сдвигается вправо.

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

А также из стандарта C, 6.5.2.2, параграф 10 [ISO / IEC 9899: 2011]

Каждая оценка в вызывающей функции (включая вызовы других функций), которая иначе не упорядочена до или после выполнения тела вызываемой функции, имеет неопределенную последовательность относительно выполнения вызываемой функции.

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

См. также:  1|Процесс разработки технологий

Рассмотрим следующий пример кода, который зависит от неопределенного поведения и, следовательно, неверен.

/* example1.c */
void fun(int n, int m);
int fun1()
{
 pritnf(“fun1”);
 return 1;
}
int fun2()
{
 printf( “fun2” );
 return 2;
}
…
fun(fun1(), fun2()); // which one is executed first?
/* From <https://stackoverflow.com/questions/2397984/undefined-unspecified-and-implementation-defined-behavior>*/

Как объяснялось выше, мы не можем быть уверены, будет ли выполнено первое выполнение func1 или func2, компилятор должен делать все, что он хочет.

В tcode1.c внутри foo (i ++, I) код зависел от переданных аргументов, и это было плохо, потому что вы не можете зависеть от переданных аргументов. Это доказывает, что код в tcode1.c в лучшем случае не указан, что плохо, если вы хотите писать переносимый код. Но, как я уже сказал, этот код undefined не unspecified, поэтому давайте определим undefined поведение.

Из стандарта C 3.4.3

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

Известные неопределенные поведения:

  • Разделить на ноль
  • Разыменование нулевого указателя
  • Подписанное целочисленное переполнение
  • Использование инициализированной переменной.

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

Из стандарта C в том же месте, где он определяет неопределенное поведение, есть примечание, в котором говорится

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

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

Теперь, когда у нас есть неопределенное поведение и неопределенное поведение, давайте посмотрим, насколько легко дерьмо может попасть в потолок при работе с неопределенным поведением. Мы знаем, что передача аргументов функции не указана.

Взгляните на следующий код.

/* example2.c */
extern void c(int i, int j);
int glob;
int a(void) {
 return glob + 10;
}
int b(void) {
 glob = 42;
 return glob;
}
void func(void) {
 c(a(), b());
}
/* copied from
* https://www.securecoding.cert.org/confluence/display/c/EXP30-C.+Do+not+depend+on+the+order+of+evaluation+for+side+effects
*/

Может случиться так, что сначала вызывается b (), и glob присваивается значение, или может случиться так, что сначала вызывается a (), а доступ к glob осуществляется неинициализированным, что не определено. Также помните основное правило неопределенного поведения

«Если часть кода не определена, не определен весь код».

regehr awsome blog

В случае нашего tcode1 только foo (i ++, i) не определено. Что делает это неопределенным и не неопределенным. Для этого нам нужно сначала понять точки последовательности.

См. также:  Глубокий тур по стручкам какао и Карфагену

Из стандарта c 5.1.2.3

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

Короче говоря, это место, где осела пыль. Поэтому, если вы поместите точку останова между точкой последовательности, вы увидите обновленные значения выражений до этих инструкций. Что это за «побочные эффекты», использованные в приведенном выше определении из стандарта C 5.1.2.3?

Доступ к изменчивому объекту, изменение объекта, изменение файла или вызов функции, которая выполняет любую из этих операций, — все это побочные эффекты, которые представляют собой изменения в состоянии среды выполнения. Оценка выражения может вызвать побочные эффекты.

Другими словами, побочные эффекты — это микрокоманды, за пределами которых код не может быть разбит (по крайней мере, на уровне абстракции). Думайте об этом как об атомах, у вас есть нутроны, протоны, электроны внутри атома, но атом — это самая низкая единица, если вы сломаете атом, вы высвободите большое количество неконтролируемой энергии.

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

  1. доступ к изменчивому объекту
  2. изменение объекта
  3. изменение файла
  4. вызов функции, которая выполняет любой из вышеперечисленных побочных эффектов

Таким образом, выражение состоит из побочных эффектов, и между выражениями возникают точки последовательности. Ниже приведены места, где встречаются точки последовательности, из приложения C стандарта C.

  1. Вызов функции после оценки аргументов.
  2. Конец первого операнда следующих операторов: логическое И &&; логическое ИЛИ || ; условно? ; запятая,.
  3. Конец полного декларатора;
  4. Конец полного выражения: инициализатор; выражение в выражении оператора; управляющее выражение оператора выбора (if или switch); управляющее выражение оператора while или do; каждое из выражений оператора for; выражение в операторе возврата.
  5. Непосредственно перед возвратом библиотечной функции.
  6. После действий, связанных с преобразованием каждой форматированной функции ввода / вывода
  7. спецификатор.
  8. Непосредственно перед и сразу после каждого вызова функции сравнения, и
  9. также между любым вызовом функции сравнения и любым перемещением объектов
  10. передается в качестве аргументов этому вызову.
См. также:  Реализация перечислений в Golang

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

Итак, у нас есть побочные эффекты, мы знаем, что и где находятся точки последовательности, но как tcode1 не определен. Давайте посмотрим на золотое правило неопределенности. из clang_faqs

Между предыдущей и следующей точкой последовательности объект должен иметь свое сохраненное значение, измененное не более одного раза при оценке выражения. Кроме того, к предыдущему значению следует обращаться только для определения значения, которое будет сохранено.

Это правило состоит из двух частей:

  1. В выражении значение объекта должно быть изменено не более одного раза.
  2. Любой доступ к предыдущему значению должен быть выполнен для определения значения, которое будет сохранено.

Согласно правилу следующее неопределенное поведение:

i=i++;

Поскольку значение i изменяется дважды в выражении, это нарушает часть 1 golden_rule. Теперь рассмотрим следующие

a[i]=i++;

Это неправильно, потому что это нарушает часть 2 Golden_rule. Есть несколько обращений к i; и один из них не для определения значения, которое будет сохранено в i. Если i = 2, то, когда компилятор завершит работу с выражением, мы просто не можем быть уверены, являются ли конечные значения a [1] = 1 или, a [2] = 2, или a [1] = 2 (сначала выполняется доступ или сначала увеличивается значение). Также вы можете видеть, что это связано с множеством других неопределенных поведений. Например, массив может иметь размер только 2, и не существует такой вещи, как [2], поэтому, когда наш код обращается к [2], мы получаем значение мусора, которое при манипулировании выполняет код, форматирующий наш жесткий диск. .

Итак, наконец, давайте взглянем на tcode1, почему такое поведение undefined.

foo(i++,i)

Я уверен, что вы уже догадались. Теперь мы знаем, что между аргументами (silver_rule) нет точек последовательности, и из приведенного выше примера должно быть ясно, что он нарушает часть 2 Golden_rule. Таким образом, это неопределенное поведение.

Когда я компилирую и запускаю tcode1, я получаю следующий ответ

$ ./tcode1
5, 6

что показывает, что a = 5, b-6, без предупреждения, без ошибки. Но это не значит, что это будет происходить каждый раз.

Из regehrs blog

Кто-то однажды сказал мне, что в баскетболе нельзя держать мяч и бегать. Я взял баскетбольный мяч, попробовал, и он отлично сработал. Он явно не разбирался в баскетболе.

(Это объяснение принадлежит Роджеру Миллеру через Стива Саммита.)

Понравилась статья? Поделиться с друзьями:
IT Шеф
Добавить комментарий

;-) :| :x :twisted: :smile: :shock: :sad: :roll: :razz: :oops: :o :mrgreen: :lol: :idea: :grin: :evil: :cry: :cool: :arrow: :???: :?: :!: