Возврат структуры, содержащей массив - PullRequest
23 голосов
/ 08 января 2012

Следующие простые ошибки кода в gcc 4.4.4

#include<stdio.h>

typedef struct Foo Foo;
struct Foo {
    char f[25];
};

Foo foo(){
    Foo f = {"Hello, World!"};
    return f;
}

int main(){
    printf("%s\n", foo().f);
}

Изменение последней строки на

 Foo f = foo(); printf("%s\n", f.f);

Работает нормально.Обе версии работают при компиляции с -std=c99.Я просто вызываю неопределенное поведение, или что-то в стандарте изменилось, что позволяет коду работать под C99?Почему происходит сбой под C89?

Ответы [ 3 ]

16 голосов
/ 08 января 2012

Я считаю, что поведение не определено как в C89 / C90, так и в C99.

foo().f является выражением типа массива, в частности char[25]. C99 6.3.2.1p3 говорит:

За исключением случаев, когда это операнд оператора sizeof или унарный & , или строковый литерал, используемый для инициализации массива, Выражение с типом «массив тип » преобразуется в выражение с типом «указатель на тип », которое указывает на начальный элемент массива объекта и не является lvalue. Если объект массива имеет класс хранения регистров, поведение не определено.

Проблема в данном конкретном случае (массив, который является элементом структуры, возвращаемой функцией) состоит в том, что не существует «объекта массива». Результаты функции возвращаются по значению, поэтому результатом вызова foo() является значение типа struct Foo, а foo().f - значение (не lvalue) типа char[25].

Насколько я знаю, это единственный случай в C (до C99), в котором вы можете иметь ненулевое выражение типа массива. Я бы сказал, что попытка получить к нему доступ не определяется пропуском, скорее всего потому, что авторы стандарта (понятно ИМХО) не думали об этом случае. Вы, вероятно, увидите разные варианты поведения при разных настройках оптимизации.

Новый стандарт C 2011 года исправляет этот угловой случай, изобретая новый класс хранения. N1570 (ссылка на поздний черновик до C11) в 6.2.4p8 гласит:

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

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

[...]
int main(void ) {
    struct Foo temp = foo();
    printf("%s\n", temp.f);
}
13 голосов
/ 08 января 2012

printf немного забавно, потому что это одна из тех функций, которая принимает varargs . Итак, давайте разберемся с этим, написав вспомогательную функцию bar. Мы вернемся к printf позже.

(я использую "gcc (Ubuntu 4.4.3-4ubuntu5) 4.4.3")

void bar(const char *t) {
    printf("bar: %s\n", t);
}

и вызов этого вместо:

bar(foo().f); // error: invalid use of non-lvalue array

ОК, выдает ошибку. В C и C ++ вам не разрешено передавать массив со значением . Вы можете обойти это ограничение, поместив массив в структуру, например void bar2(Foo f) {...}

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

Я обнаружил, что работает следующий код:

bar(&(foo().f[0]));

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

И чтобы быть полным, это работает отлично, как и должно:

Foo f = foo();
bar(f.f);

Переменная f не является временной, и поэтому мы можем (неявно, во время затухания) взять ее адрес.

printf, 32-битный против 64-битный и странный

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

Когда я впервые скомпилировал и запустил код, он был в 64-битном режиме. Я не видел подтверждения своей теории, пока не скомпилировал 32-битную версию (-m32 в gcc). Конечно же, я получил segfault, как в первоначальном вопросе. (Я получал несколько бессмысленных выходных данных, но без ошибки, когда в 64 битах).

Я реализовал свой собственный my_printf (с абсурдом vararg), который печатал действительное значение char *, прежде чем пытаться печатать буквы, на которые указывает char*. Я назвал это так:

my_printf("%s\n", f.f);
my_printf("%s\n", foo().f);

и это вывод, который я получил ( код на ideone ):

arg = 0xffc14eb3        // my_printf("%s\n", f.f); // worked fine
string = Hello, World!
arg = 0x6c6c6548        // my_printf("%s\n", foo().f); // it's about to crash!
Segmentation fault

Первое значение указателя 0xffc14eb3 является правильным (оно указывает на символы «Привет, мир!»), Но посмотрите на второе 0x6c6c6548. Это коды ASCII для Hell (обратный порядок - немного порядка байтов или что-то в этом роде). Он скопировал массив по значению в printf, и первые четыре байта были интерпретированы как 32-битный указатель или целое число. Этот указатель нигде не указывает, и, следовательно, программа падает, когда пытается получить доступ к этому местоположению.

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

0 голосов
/ 08 января 2012

В MacOS X 10.7.2 оба GCC / LLVM 4.2.1 ('i686-apple-darwin11-llvm-gcc-4.2 (GCC) 4.2.1 (на основе Apple Inc., сборка 5658) (сборка LLVM, 2335.15). 00) ') и GCC 4.6.1 (который я создал) компилируют код без предупреждений (под -Wall -Wextra), как в 32-битном, так и в 64-битном режимах. Все программы работают без сбоев. Это то, что я ожидал; код выглядит хорошо для меня.

Может быть, проблема в Ubuntu - это ошибка в конкретной версии GCC, которая с тех пор была исправлена?

...