Неопределенное поведение: при попытке получить доступ к результату вызова функции

Следующий компилирует и печатает "строку" в качестве вывода.

#include <stdio.h>

struct S { int x; char c[7]; };

struct S bar() {
    struct S s = {42, "string"};
    return s;
}

int main()
{
    printf("%s", bar().c);
}

По-видимому, это вызывает неопределенное поведение в соответствии с

C99 6.5.2.2/5 Если предпринята попытка изменить результат вызова функции или получить к нему доступ после следующей точки последовательности, поведение не определено.

Я не понимаю, где говорится о "следующей точке последовательности". Что тут происходит?

3 ответа

Вы столкнулись с тонким углом языка.

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

  • Когда выражение массива является операндом унарного & оператор (который выдает адрес всего массива);
  • Когда это операнд унарного sizeof или (по состоянию на C11)_Alignofоператор (sizeof arr выдает размер массива, а не размер указателя); а также
  • Когда это строковый литерал в инициализаторе, используемый для инициализации объекта массива (char str[6] = "hello"; не конвертируется "hello" к char*.)

( Черновик N1570 неправильно добавляет _Alignof к списку исключений. На самом деле, по причинам, которые не ясны, _Alignof может применяться только к имени типа, но не к выражению.)

Обратите внимание, что существует неявное предположение: выражение массива в первую очередь ссылается на объект массива. В большинстве случаев это так (самый простой случай - это когда выражение массива является именем объявленного объекта массива), но в этом случае нет объекта массива.

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

Стандарт ISO C 2011 года решает эту проблему, вводя "временное время жизни", которое применяется только к "выражению без значения с типом структуры или объединения, где структура или объединение содержит член с типом массива" ( N1570 6.2.4p8). Такой объект не может быть изменен, и его время жизни заканчивается в конце содержащего полное выражение или полный декларатор.

Таким образом, начиная с C2011, поведение вашей программы четко определено. printf call получает указатель на первый элемент массива, который является частью объекта struct с временным временем жизни; этот объект продолжает существовать до printf звонок заканчивается.

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

Если ваша цель - заставить эту программу работать, а не понять, почему она может потерпеть неудачу, вы можете сохранить результат вызова функции в явном объекте:

const struct s result = bar();
printf("%s", result.c);

Теперь у вас есть объект struct с автоматическим, а не временным сроком хранения, поэтому он существует во время и после выполнения printf вызов.

Точка последовательности появляется в конце полного выражения, т. Е. Когда printf возвращается в этом примере. В других случаях встречаются точки последовательности

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

Вот простой пример не совсем определенного поведения:

char* c = bar().c; *c = 5; // UB

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

В C99 есть точка последовательности при вызове функции после оценки аргументов (C99 6.5.2.2/10).

Так когда bar().c оценивается, это приводит к указателю на первый элемент в char c[7] массив в структуре, возвращаемой bar(), Однако этот указатель копируется в аргумент (безымянный аргумент, как это происходит) для printf()и к тому времени, когда вызов фактически сделан к printf() Функция, указанная выше точка последовательности произошла, поэтому элемент, на который указывал указатель, может больше не существовать.

Как упоминает Кейт Томсон, C11 (и C++) дают более надежные гарантии относительно времени жизни временных, поэтому поведение в соответствии с этими стандартами не будет неопределенным.

Другие вопросы по тегам