Подтвердить что ты не робот

В C, как бы я решил, следует ли возвращать структуру или указатель на структуру?

Работая над моей мышцей C в последнее время и просматривая многие библиотеки, с которыми я работаю, я дал мне хорошее представление о том, что такое хорошая практика. Одна вещь, которую я НЕ видел, - это функция, которая возвращает структуру:

something_t make_something() { ... }

Из того, что я понял, это "правильный" способ сделать это:

something_t *make_something() { ... }
void destroy_something(something_t *object) { ... }

Архитектура в фрагменте кода 2 FAR более популярна, чем фрагмент 1. Итак, теперь я спрашиваю: зачем мне когда-либо возвращать структуру напрямую, как в фрагменте 1? Какие различия следует учитывать при выборе между двумя параметрами?

Кроме того, как этот параметр сравнивается?

void make_something(something_t *object)
4b9b3361

Ответ 1

Когда something_t мал (читайте: копирование примерно так же дешево, как копирование указателя), и вы хотите, чтобы он был распределен по стекам по умолчанию:

something_t make_something(void);

something_t stack_thing = make_something();

something_t *heap_thing = malloc(sizeof *heap_thing);
*heap_thing = make_something();

Когда something_t велико или вы хотите, чтобы он был выделен в виде кучи:

something_t *make_something(void);

something_t *heap_thing = make_something();

Независимо от размера something_t, и если вы не заботитесь о том, где его выделяли:

void make_something(something_t *);

something_t stack_thing;
make_something(&stack_thing);

something_t *heap_thing = malloc(sizeof *heap_thing);
make_something(heap_thing);

Ответ 2

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


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

Действительно, скорость не является причиной отставания метода 2 от возвратного указателя вместо возврата в значение.

Техника 2 существует для стабильности ABI. Если у вас есть struct, и ваша следующая версия библиотеки добавит к ней еще 20 полей, пользователи вашей предыдущей версии библиотеки будут бинарно совместимы, если им будут переданы предварительно сконструированные указатели. Дополнительные данные за пределами struct, о которых они знают, это то, о чем им не нужно знать.

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

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

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

Итак,

something_t make_something(unsigned library_version) { ... }

где library_version используется библиотекой, чтобы определить, какая версия something_t должна возвращаться, и она изменяет, сколько из стека она манипулирует. Это невозможно с помощью стандартного C, но

void make_something(something_t* here) { ... }

есть. В этом случае something_t может иметь поле version в качестве своего первого элемента (или поля размера), и вам потребуется, чтобы он был заполнен до вызова make_something.

Другой код библиотеки, принимающий something_t, будет запрашивать поле version, чтобы определить, в какой версии something_t они работают.

Ответ 3

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

Я думаю, что void make_something(something_t *object) является наиболее распространенным способом использования структур в C. Вы оставляете выделение вызывающему. Это эффективно, но не очень.

Однако объектно-ориентированные C-программы используют something_t *make_something(), поскольку они построены с понятием непрозрачного типа, что заставляет вас использовать указатели. Является ли возвращаемый указатель точкой в ​​динамической памяти или что-то еще зависит от реализации. OO с непрозрачным типом часто является одним из самых элегантных и лучших способов разработки более сложных программ на C, но, к сожалению, немногие программисты C знают об этом.

Ответ 4

Некоторые плюсы первого подхода:

  • Меньше кода для написания.
  • Более идиоматический для случая использования нескольких значений.
  • Работает в системах, не имеющих динамического распределения.
  • Вероятно, быстрее для маленьких или маленьких объектов.
  • Отсутствует утечка памяти из-за забывания free.

Некоторые минусы:

  • Если объект большой (скажем, мегабайт), может вызвать переполнение стека или может быть медленным, если компиляторы не оптимизируют его хорошо.
  • Может удивить людей, которые изучали C в 1970-х годах, когда это было невозможно, и не обновлялись.
  • Не работает с объектами, которые содержат указатель на часть себя.

Ответ 5

Я несколько удивлен.

Разница в том, что пример 1 создает структуру в стеке, пример 2 создает ее в куче. В C или С++-коде, который эффективно C, он идиоматичен и удобен для создания большинства объектов в куче. В С++ это не так, в основном они идут в стек. Причина в том, что если вы создаете объект в стеке, деструктор вызывается автоматически, если вы создаете его в куче, его нужно вызывать явно. Поэтому гораздо проще обеспечить отсутствие утечек памяти и обработку исключений. все идет по стеклу. В C деструктор должен быть вызван эксплицитно в любом случае, и нет понятия специальной функции деструктора (у вас есть деструкторы, конечно, но они являются просто нормальными функциями с такими именами, как destroy_myobject()).

Теперь исключение в С++ для объектов контейнера низкого уровня, например. векторы, деревья, хэш-карты и т.д. Они сохраняют кучи, и у них есть деструкторы. Теперь большинство объектов с большой памятью состоят из нескольких непосредственных членов данных, дающих размеры, идентификаторы, теги и т.д., А затем остальную информацию в структурах STL, возможно, вектор пиксельных данных или карту английских пар слов/значений. Таким образом, большая часть данных на самом деле находится в куче, даже в С++.

И современный С++ разработан так, что этот шаблон

class big
{
    std::vector<double> observations; // thousands of observations
    int station_x;                    // a bit of data associated with them
    int station_y; 
    std::string station_name; 
}  

big retrieveobservations(int a, int b, int c)
{
    big answer;
    //  lots of code to fill in the structure here

    return answer;
}

void high_level()
{
   big myobservations = retriveobservations(1, 2, 3);
}

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

Ответ 6

В отличие от некоторых других языков (например, Python), C не имеет понятия tuple. Например, в Python допустимо следующее:

def foo():
    return 1,2

x,y = foo()
print x, y

Функция foo возвращает два значения в виде кортежа, которые назначаются x и y.

Так как C не имеет понятия кортежа, неудобно возвращать несколько значений из функции. Один из способов - определить структуру для хранения значений, а затем вернуть структуру, например:

typedef struct { int x, y; } stPoint;

stPoint foo( void )
{
    stPoint point = { 1, 2 };
    return point;
}

int main( void )
{
    stPoint point = foo();
    printf( "%d %d\n", point.x, point.y );
}

Это только один пример, где вы можете увидеть, как функция возвращает структуру.