Поиск…


Вступление

В C некоторые выражения дают неопределенное поведение . Стандарт явно не определяет, как должен себя вести компилятор, если он сталкивается с таким выражением. В результате компилятор может делать все, что сочтет нужным, и может давать полезные результаты, неожиданные результаты или даже сбой.

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

замечания

Что такое Undefined Behavior (UB)?

Неопределенное поведение - это термин, используемый в стандарте C. Стандарт C11 (ISO / IEC 9899: 2011) определяет термин неопределенное поведение как

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

Что произойдет, если в моем коде есть UB?

Это результаты, которые могут произойти из-за неопределенного поведения в соответствии со стандартом:

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

Следующая цитата часто используется для описания (менее формально, хотя) результатов, происходящих от неопределенного поведения:

«Когда компилятор сталкивается с [определенной неопределенной конструкцией], это законно для того, чтобы заставить демонов вылететь из вашего носа» (подразумевается, что компилятор может выбрать любой произвольно причудливый способ интерпретации кода без нарушения стандарта ANSI C)

Почему существует UB?

Если это так плохо, почему они просто не определяли его или не определяли его реализацию?

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

Почему UB трудно отслеживать?

Существует по крайней мере две причины, по которым неопределенное поведение создает ошибки, которые трудно обнаружить:

  • Компилятор не обязан - и, как правило, не может надежно - предупреждать вас о неопределенном поведении. Фактически требуя, чтобы это сделало это, явилось бы прямо против причины существования неопределенного поведения.
  • Непредсказуемые результаты могут не начинаться разворачиваться в точную точку операции, где происходит конструкция, поведение которой не определено; Неопределенное поведение окрашивает все исполнение и его последствия могут произойти в любое время: во время, после или даже до неопределенной конструкции.

Рассмотрим разворот нулевого указателя: компилятор не требуется для диагностики разыменования нулевых указателей и даже не мог, так как во время выполнения любой указатель, переданный в функцию, или в глобальной переменной может иметь значение NULL. И когда происходит разыменование нулевого указателя, стандарт не требует, чтобы программа была повреждена. Скорее, программа может произойти сбой раньше, позже или вообще не сбой; он может даже вести себя так, как если бы нулевой указатель указывал на действительный объект и вел себя нормально, только для того, чтобы сбой при других обстоятельствах.

В случае нулевого указателя разыменования, C язык отличается от управляемых языков , таких как Java или C #, где определяется поведение нуль-указатель разыменования: генерируется исключение, в точное время ( NullPointerException в Java, NullReferenceException в C #) , поэтому те, которые приходят с Java или C #, могут ошибочно полагать, что в этом случае программа C должна сбой, с выдачей диагностического сообщения или без него .

Дополнительная информация

Существует несколько таких ситуаций, которые следует четко различать:

  • Явно неопределенное поведение, то есть где стандарт C явно сообщает вам, что вы не в курсе.
  • Неявно неопределенное поведение, когда в стандарте нет текста, который предвидит поведение для ситуации, в которую вы ввели вашу программу.

Также имейте в виду, что во многих местах поведение определенных конструкций сознательно не определено стандартом C, чтобы оставить место для разработчиков компилятора и библиотек, чтобы придумать свои собственные определения. Хорошим примером являются сигналы и обработчики сигналов, где расширения C, такие как стандарт операционной системы POSIX, определяют гораздо более сложные правила. В таких случаях вам просто нужно проверить документацию своей платформы; стандарт C не может вам ничего сказать.

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

Из-за таких проблем важно (тем более, что компиляторы не всегда предупреждают нас о UB), чтобы программирование на языке C было, по крайней мере, знакомым с вещами, которые вызывают неопределенное поведение.

Следует отметить, что есть некоторые инструменты (например, инструменты статического анализа, такие как PC-Lint), которые помогают обнаруживать неопределенное поведение, но опять же, они не могут обнаружить все случаи неопределенного поведения.

Вызов нулевого указателя

Это пример разыменования указателя NULL, вызывающего неопределенное поведение.

int * pointer = NULL;
int value = *pointer; /* Dereferencing happens here */

Указатель NULL гарантируется стандартом C для сравнения неравного с любым указателем на действительный объект, а разыменование его вызывает неопределенное поведение.

Изменение любого объекта более одного раза между двумя точками последовательности

int i = 42;
i = i++; /* Assignment changes variable, post-increment as well */
int a = i++ + i--;

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

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

(Стандарт C99, раздел 6.5, пункт 2)

Эта схема оказалась слишком грубой, что привело к появлению некоторых выражений, демонстрирующих неопределенное поведение в отношении C99, которые, вероятно, не должны делать. C2011 сохраняет точки последовательности, но вводит более тонкий подход к этой области на основе последовательности и отношений, которые он называет «секвенированными до»:

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

(Стандарт C2011, раздел 6.5, пункт 2)

Полные детали отношения «sequenced before» слишком длинны для описания здесь, но они дополняют последовательности, а не вытесняют их, поэтому они влияют на определение поведения для некоторых оценок, поведение которых ранее не было определено. В частности, если есть точка последовательности между двумя оценками, то перед точкой последовательности «секвенируется до» после.

Следующий пример имеет четкое поведение:

int i = 42;
i = (i++, i+42); /* The comma-operator creates a sequence point */

Следующий пример имеет неопределенное поведение:

int i = 42;
printf("%d %d\n", i++, i++); /* commas as separator of function arguments are not comma-operators */

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

Отсутствует оператор возврата в функции возврата значения

int foo(void) {
  /* do stuff */
  /* no return here */
}

int main(void) {
  /* Trying to use the (not) returned value causes UB */
  int value = foo();
  return 0;
}

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

Обратите внимание, что неопределенное поведение происходит только в том случае, если вызывающий пытается использовать / получить доступ к значению функции. Например,

int foo(void) {
  /* do stuff */
  /* no return here */
}

int main(void) {
  /* The value (not) returned from foo() is unused. So, this program
   * doesn't cause *undefined behaviour*. */
  foo();
  return 0;
}
C99

main() функция является исключением из этого правила в том , что это возможно для того , чтобы быть прекращено без оператора возврата , потому что предполагается , возвращаемое значение 0 автоматически будет использоваться в данном случае 2.


1 ( ISO / IEC 9899: 201x , 6.9.1 / 12)

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

2 ( ISO / IEC 9899: 201x , 5.1.2.2.3 / 1)

достигая}, который завершает основную функцию, возвращает значение 0.

Подписанное целочисленное переполнение

В параграфе 6.5 / 5 как C99, так и C11 оценка выражения создает неопределенное поведение, если результат не является представимым значением типа выражения. Для арифметических типов это называется переполнением . Беззнаковая целочисленная арифметика не переполняется, поскольку применяется параграф 6.2.5 / 9, в результате чего любой результат без знака, который в противном случае был бы вне диапазона, был бы уменьшен до значения внутри диапазона. Однако не существует аналогичного положения для знаковых целочисленных типов; они могут и переполняются, производя неопределенное поведение. Например,

#include <limits.h>      /* to get INT_MAX */

int main(void) {
    int i = INT_MAX + 1; /* Overflow happens here */
    return 0;
}

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

int square(int x) {
    return x * x;  /* overflows for some values of x */
}

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

С другой стороны, рассмотрим этот тривиальный пример переполненной цепочки со знаком целочисленной арифметики:

int zero(int x) {
    return x - x;  /* Cannot overflow */
}

Связь между операндами оператора вычитания гарантирует, что вычитание никогда не переполняется. Или рассмотрим этот несколько более практичный пример:

int sizeDelta(FILE *f1, FILE *f2) {
    int count1 = 0;
    int count2 = 0;
    while (fgetc(f1) != EOF) count1++;  /* might overflow */
    while (fgetc(f2) != EOF) count2++;  /* might overflow */

    return count1 - count2; /* provided no UB to this point, will not overflow */
}

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

Использование неинициализированной переменной

int a; 
printf("%d", a);

Переменная a - это int с автоматическим временем хранения. В приведенном выше примере пример пытается напечатать значение неинициализированной переменной ( a никогда не инициализировалось). Автоматические переменные, которые не инициализированы, имеют неопределенные значения; доступ к ним может привести к неопределенному поведению.

Примечание. Переменные со статическим или потоковым локальным хранилищем, включая глобальные переменные без static ключевого слова, инициализируются либо нулем, либо их инициализированным значением. Следовательно, законно.

static int b;
printf("%d", b);

Очень распространенная ошибка состоит в том, чтобы не инициализировать переменные, которые служат счетчиками равными 0. Вы добавляете к ним значения, но поскольку начальное значение является мусором, вы будете вызывать Undefined Behavior , например, в вопросе Компиляция на терминале выдает предупреждение указателя и странные символы .

Пример:

#include <stdio.h>

int main(void) {
    int i, counter;
    for(i = 0; i < 10; ++i)
        counter += i;
    printf("%d\n", counter);
    return 0;
}

Выход:

C02QT2UBFVH6-lm:~ gsamaras$ gcc main.c -Wall -o main
main.c:6:9: warning: variable 'counter' is uninitialized when used here [-Wuninitialized]
        counter += i;
        ^~~~~~~
main.c:4:19: note: initialize the variable 'counter' to silence this warning
    int i, counter;
                  ^
                   = 0
1 warning generated.
C02QT2UBFVH6-lm:~ gsamaras$ ./main
32812

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

int main(void)
{
    int *p;
    p++; // Trying to increment an uninitialized pointer.
}

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

Вывод указателя на переменную за пределами ее срока службы

int* foo(int bar)
{
    int baz = 6;
    baz += bar;
    return &baz; /* (&baz) copied to new memory location outside of foo. */
} /* (1) The lifetime of baz and bar end here as they have automatic storage   
   * duration (local variables), thus the returned pointer is not valid! */

int main (void)
{
    int* p;

    p = foo(5);  /* (2) this expression's behavior is undefined */
    *p = *p - 6; /* (3) Undefined behaviour here */

    return 0;
}

Некоторые компиляторы с благодарностью указывают на это. Например, gcc предупреждает:

warning: function returns address of local variable [-Wreturn-local-addr]

и clang предупреждает:

warning: address of stack memory associated with local variable 'baz' returned 
[-Wreturn-stack-address]

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

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

(2) Согласно ISO / IEC 9899: 2011 6.2.4 §2 «Значение указателя становится неопределенным, когда объект, на который он указывает, достигает конца своего срока службы».

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

Деление на ноль

int x = 0;
int y = 5 / x;  /* integer division */

или же

double x = 0.0;
double y = 5.0 / x;  /* floating point division */

или же

int x = 0;
int y = 5 % x;  /* modulo operation */

Для второй строки в каждом примере, где значение второго операнда (x) равно нулю, поведение не определено.

Обратите внимание, что большинство реализаций математики с плавающей запятой будут следовать стандарту (например, IEEE 754), и в этом случае операции, такие как деление на ноль, будут иметь согласованные результаты (например, INFINITY ), даже если в стандарте C указано, что операция не определена.

Доступ к памяти за пределами выделенного фрагмента

A указатель на кусок памяти, содержащий n элементов, может быть разыменован только в том случае, если он находится в memory диапазона и memory + (n - 1) . Выделение указателя за пределами этого диапазона приводит к неопределенному поведению. В качестве примера рассмотрим следующий код:

int array[3];
int *beyond_array = array + 3;
*beyond_array = 0; /* Accesses memory that has not been allocated. */

Третья строка обращается к четвертому элементу в массиве, длина которого составляет всего 3 элемента, что приводит к неопределенному поведению. Точно так же поведение второй строки в следующем фрагменте кода также недостаточно четко определено:

int array[3];
array[3] = 0;

Обратите внимание, что указание последнего элемента массива не является неопределенным поведением ( beyond_array = array + 3 здесь четко определено), но разыменование его ( *beyond_array - неопределенное поведение). Это правило также выполняется для динамически распределенной памяти (например, буферов, созданных через malloc ).

Копирование перекрывающейся памяти

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

Например, это ...

#include <string.h> /* for memcpy() */

char str[19] = "This is an example";
memcpy(str + 7, str, 10);

... пытается скопировать 10 байт, где области памяти источника и назначения перекрываются тремя байтами. Чтобы визуализировать:

               overlapping area
               |
               _ _
              |   |
              v   v
T h i s   i s   a n   e x a m p l e \0
^             ^
|             |
|             destination
|
source

Из-за перекрытия результирующее поведение не определено.

Среди стандартных библиотечных функций с ограничением такого рода являются memcpy() , strcpy() , strcat() , sprintf() и sscanf() . В стандарте говорится об этих и некоторых других функциях:

Если копирование происходит между перекрывающимися объектами, поведение не определено.

Функция memmove() является основным исключением из этого правила. Его определение указывает, что функция ведет себя так, как если исходные данные были сначала скопированы во временный буфер, а затем записаны на целевой адрес. Не существует исключения для перекрытия областей источника и получателя, а также никакой необходимости в одном, поэтому memmove() имеет memmove() поведение в таких случаях.

Различие отражает эффективность против . общий компромисс. Копирование таких функций, как эти функции, обычно происходит между непересекающимися областями памяти, и часто во время разработки можно узнать, будет ли конкретный экземпляр копирования памяти в эту категорию. Предполагая, что не-перекрытие дает сравнительно более эффективные реализации, которые не дают достоверных результатов, когда предположение не выполняется. Большинство функций библиотеки C допускают более эффективные реализации, а memmove() заполняет пробелы, обслуживая случаи, когда источник и получатель могут или перекрываются. Однако для обеспечения правильного эффекта во всех случаях он должен выполнять дополнительные тесты и / или использовать сравнительно менее эффективную реализацию.

Чтение неинициализированного объекта, который не поддерживается памятью

C11

Чтение объекта приведет к неопределенному поведению, если объект равен 1 :

  • неинициализированным
  • с автоматической продолжительностью хранения
  • его адрес никогда не принимается

Переменная a в приведенном ниже примере удовлетворяет всем этим условиям:

void Function( void )
{
    int a;
    int b = a;
} 

1 (Цитируется по: ISO: IEC 9899: 201X 6.3.2.1 Lvalues, массивы и обозначения функций 2)
Если lvalue обозначает объект с продолжительностью автоматического хранения, который мог быть объявлен с классом хранения регистров (никогда не был принят его адрес), и этот объект не инициализирован (не объявлен с инициализатором, и его назначение не было выполнено до использования ), поведение не определено.

Гонка данных

C11

C11 представила поддержку для нескольких потоков исполнения, что дает возможность расследовать данные. Программа содержит гонку данных, если к объекту в ней обращаются 1 двумя разными потоками, где по крайней мере один из доступа является неатомным, по крайней мере один изменяет объект, а семантика программы не позволяет гарантировать, что два доступа не могут перекрываться во время. 2 Хорошо помните, что фактическая параллельность задействованных доступов не является условием гонки данных; Гонки данных охватывают более широкий класс проблем, возникающих из (разрешенных) несоответствий в представлениях разных потоков.

Рассмотрим этот пример:

#include <threads.h>

int a = 0;

int Function( void* ignore )
{
    a = 1;

    return 0;
}

int main( void )
{
    thrd_t id;
    thrd_create( &id , Function , NULL );

    int b = a;

    thrd_join( id , NULL );
}

Основные вызовы нити thrd_create , чтобы начать новую нить работает функция Function . Второй поток изменяет a , а основной поток читает a . Ни один из этих доступов не является атомарным, и эти два потока ничего не делают ни индивидуально, ни совместно, чтобы гарантировать, что они не перекрываются, поэтому существует гонка данных.

Среди способов, с помощью которых эта программа могла избежать гонки данных,

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

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


1 Изменение или чтение объекта.

2 (Цитируется по ISO: IEC 9889: 201x, раздел 5.1.2.4 «Многопоточные исполнения и расписания данных»)
Выполнение программы содержит гонку данных, если она содержит два конфликтующих действия в разных потоках, по крайней мере один из которых не является атомарным, и не происходит до другого. Любая такая гонка данных приводит к неопределенному поведению.

Чтение значения указателя, который был освобожден

Даже просто считывание значения освобожденного указателя (т. Е. Без попытки разыменования указателя) является неопределенным поведением (UB), например

char *p = malloc(5);
free(p);
if (p == NULL) /* NOTE: even without dereferencing, this may have UB */
{

}

Цитирование ISO / IEC 9899: 2011 , раздел 6.2.4. §2:

[...] Значение указателя становится неопределенным, когда объект, на который он указывает (или только что прошел), достигает конца своего срока службы.

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

Изменить строковый литерал

В этом примере кода указатель char p инициализируется адресом строкового литерала. Попытка изменить строковый литерал имеет неопределенное поведение.

char *p = "hello world";
p[0] = 'H'; // Undefined behavior

Однако изменение измененного массива char напрямую или через указатель, естественно, не является неопределенным поведением, даже если его инициализатор является литеральной строкой. Хорошо:

char a[] = "hello, world";
char *p = a;

a[0] = 'H';
p[7] = 'W';

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

Освобождение памяти дважды

Освободить память дважды - это неопределенное поведение, например

int * x = malloc(sizeof(int));
*x = 9;
free(x);
free(x);

Цитата из стандарта (7.20.3.2. Свободная функция C99):

В противном случае, если аргумент не соответствует указателю, ранее возвращенному функцией calloc, malloc или realloc, или если пространство было освобождено вызовом free или realloc, поведение не определено.

Использование неверного спецификатора формата в printf

Использование неверного спецификатора формата в первом аргументе printf вызывает неопределенное поведение. Например, приведенный ниже код вызывает неопределенное поведение:

long z = 'B';
printf("%c\n", z);

Вот еще один пример

printf("%f\n",0);

Над строкой кода указано неопределенное поведение. %f ожидает двойной. Однако 0 имеет тип int .

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

warning: format specifies type 'double' but the argument has type
      'int' [-Wformat]
    printf("%f\n",0);
            ~~    ^
            %d

Преобразование между типами указателей приводит к неверно выровненному результату

Следующие могут иметь неопределенное поведение из-за неправильного выравнивания указателя:

 char *memory_block = calloc(sizeof(uint32_t) + 1, 1);
 uint32_t *intptr = (uint32_t*)(memory_block + 1);  /* possible undefined behavior */
 uint32_t mvalue = *intptr;

Неопределенное поведение происходит по мере преобразования указателя. Согласно C11, если преобразование между двумя типами указателей приводит к некорректному результату (6.3.2.3), поведение не определено . Здесь uint32_t может потребовать выравнивания 2 или 4, например.

calloc с другой стороны, требуется вернуть указатель, подходящий для любого типа объекта; поэтому memory_block правильно выровнен, чтобы содержать uint32_t в своей начальной части. Затем в системе, где uint32_t требует выравнивания 2 или 4, memory_block + 1 будет нечетным адресом и, следовательно, не будет правильно выровнен.

Обратите внимание, что стандарт C требует, чтобы операция литья была неопределенной. Это наложено, потому что на платформах, где адреса сегментированы, байт-адрес memory_block + 1 может даже не иметь правильного представления в виде целочисленного указателя.

Кастинг char * для указателей на другие типы без каких-либо проблем с выравниванием иногда используется неправильно для декодирования упакованных структур, таких как заголовки файлов или сетевые пакеты.

Вы можете избежать неопределенного поведения, связанного с неправильным преобразованием указателя, используя memcpy :

memcpy(&mvalue, memory_block + 1, sizeof mvalue);

Здесь не происходит преобразования указателя в uint32_t* и байты копируются один за другим.

Эта операция копирования для нашего примера приводит только к допустимому значению mvalue потому что:

  • Мы использовали calloc , поэтому байты правильно инициализированы. В нашем случае все байты имеют значение 0 , но любая другая правильная инициализация.
  • uint32_t - это точный тип ширины и не имеет битов заполнения
  • Любой произвольный битовый шаблон является допустимым представлением для любого неподписанного типа.

Сложение или вычитание указателя неправильно ограничено

Следующий код имеет неопределенное поведение:

char buffer[6] = "hello";
char *ptr1 = buffer - 1;  /* undefined behavior */
char *ptr2 = buffer + 5;  /* OK, pointing to the '\0' inside the array */
char *ptr3 = buffer + 6;  /* OK, pointing to just beyond */
char *ptr4 = buffer + 7;  /* undefined behavior */

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

Кроме того, это естественно неопределенное поведение для разыменования указателя, который указывает только на массив:

char buffer[6] = "hello";
char *ptr3 = buffer + 6;  /* OK, pointing to just beyond */
char value = *ptr3;       /* undefined behavior */

Изменение константной переменной с помощью указателя

int main (void)
{
    const int foo_readonly = 10;
    int *foo_ptr;

    foo_ptr = (int *)&foo_readonly; /* (1) This casts away the const qualifier */
    *foo_ptr = 20; /* This is undefined behavior */

    return 0;
}

Цитирование ISO / IEC 9899: 201x , раздел 6.7.3 §2:

Если предпринимается попытка изменить объект, определенный с помощью типа, соответствующего const, с использованием значения lvalue с неконстантированным классом, поведение не определено. [...]


(1) В GCC это может вызвать следующее предупреждение: warning: assignment discards 'const' qualifier from pointer target type [-Wdiscarded-qualifiers]

Передача нулевого указателя на преобразование printf% s

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

char *foo = NULL;
printf("%s", foo); /* undefined behavior */

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

(null)

для кода выше. Однако добавьте (просто) новую строку в строку формата, и вы получите сбой:

char *foo = 0;
printf("%s\n", foo); /* undefined behavior */

В этом случае это происходит потому, что GCC имеет оптимизацию, которая возвращает printf("%s\n", argument); в вызов puts с puts(argument) , и puts в Glibc не обрабатывает нулевые указатели. Все это поведение стандартно.

Обратите внимание: нулевой указатель отличается от пустой строки . Итак, справедливо следующее и не имеет неопределенного поведения. Он просто напечатает новую строку :

char *foo = "";
printf("%s\n", foo);

Несогласованная привязка идентификаторов

extern int var;
static int var; /* Undefined behaviour */

C11, §6.2.2, 7 гласит:

Если внутри единицы перевода один и тот же идентификатор появляется как с внутренней, так и с внешней связью, поведение не определено.

Обратите внимание: если предыдущее объявление идентификатора будет видимым, оно будет иметь связь с предыдущим объявлением. C11, §6.2.2, 4 позволяет:

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

/* 1. This is NOT undefined */
static int var;
extern int var; 


/* 2. This is NOT undefined */
static int var;
static int var; 

/* 3. This is NOT undefined */
extern int var;
extern int var; 

Использование fflush на входном потоке

Стандарты POSIX и C явно fflush что использование fflush во входном потоке является неопределенным поведением. fflush определяется только для выходных потоков.

#include <stdio.h>

int main()
{
    int i;
    char input[4096];

    scanf("%i", &i);
    fflush(stdin); // <-- undefined behavior
    gets(input);

    return 0;
}

Нет стандартного способа удаления непрочитанных символов из входного потока. С другой стороны, некоторые реализации используют fflush для очистки буфера stdin . Microsoft определяет поведение fflush во входном потоке: если поток открыт для ввода, fflush очищает содержимое буфера. Согласно POSIX.1-2008, поведение fflush не определено, если входной файл не доступен для поиска.

Дополнительную информацию см. В разделе Использование fflush(stdin) .

Бит-сдвиг с использованием отрицательных отсчетов или за пределами ширины типа

Если значение счетчика сдвига является отрицательным значением, то обе операции сдвига влево и вправо не определены 1 :

int x = 5 << -3; /* undefined */
int x = 5 >> -3; /* undefined */

Если сдвиг влево выполняется по отрицательному значению , он не определен:

int x = -5 << 3; /* undefined */

Если сдвиг влево выполняется по положительному значению, и результат математического значения не представляется в типе, он не определен 1 :

/* Assuming an int is 32-bits wide, the value '5 * 2^72' doesn't fit 
 * in an int. So, this is undefined. */
       
int x = 5 << 72;

Обратите внимание, что сдвиг вправо по отрицательному значению (.eg -5 >> 3 ) не является неопределенным, а определяется реализацией .


1 Цитирование ISO / IEC 9899: 201x , раздел 6.5.7:

Если значение правого операнда отрицательное или больше или равно ширине продвинутого левого операнда, поведение не определено.

Изменение строки, возвращаемой функциями getenv, strerror и setlocale

Изменение строк, возвращаемых стандартными функциями getenv() , strerror() и setlocale() не определено. Таким образом, реализации могут использовать статическое хранилище для этих строк.

Функция getenv (), C11, §7.22.4.7, 4 , говорит:

Функция getenv возвращает указатель на строку, связанную с элементом согласованного списка. Указанная строка не должна изменяться программой, но может быть перезаписана последующим вызовом функции getenv.

Функция strerror (), C11, §7.23.6.3, 4 говорит:

Функция strerror возвращает указатель на строку, содержимое которой является локальным. Указанный массив не должен быть изменен программой, но может быть перезаписан последующим вызовом функции strerror.

Функция setlocale (), C11, §7.11.1.1, 8 гласит:

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

Точно так же функция localeconv() возвращает указатель на struct lconv который не должен быть изменен.

Функция localeconv (), C11, §7.11.2.1, 8 гласит:

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

Возврат из функции, объявленной с помощью спецификатора функции `_Noreturn` или` noreturn`

C11

Спецификатор функции _Noreturn был введен в C11. Заголовок <stdnoreturn.h> предоставляет макрос noreturn который расширяется до _Noreturn . Поэтому использование _Noreturn или noreturn из <stdnoreturn.h> является прекрасным и эквивалентным.

Функция, объявленная с помощью _Noreturn (или noreturn ), не может вернуться к вызывающей noreturn . Если такая функция не вернется к абоненту, поведение не определено.

В следующем примере func() объявляется с noreturn спецификатора noreturn но возвращается к вызывающему.

#include <stdio.h>
#include <stdlib.h>
#include <stdnoreturn.h>

noreturn void func(void);

void func(void)
{
    printf("In func()...\n");
} /* Undefined behavior as func() returns */

int main(void)
{
    func();
    return 0;
}

gcc и clang вызывают предупреждения для вышеуказанной программы:

$ gcc test.c
test.c: In function ‘func’:
test.c:9:1: warning: ‘noreturn’ function does return
 }
 ^
$ clang test.c
test.c:9:1: warning: function declared 'noreturn' should not return [-Winvalid-noreturn]
}
^

Пример использования noreturn который имеет четко определенное поведение:

#include <stdio.h>
#include <stdlib.h>
#include <stdnoreturn.h>

noreturn void my_exit(void);

/* calls exit() and doesn't return to its caller. */
void my_exit(void)
{
    printf("Exiting...\n");
    exit(0);
}

int main(void)
{
    my_exit();
    return 0;
}


Modified text is an extract of the original Stack Overflow Documentation
Лицензировано согласно CC BY-SA 3.0
Не связан с Stack Overflow