В 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)
Ответы
Ответ 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 );
}
Это только один пример, где вы можете увидеть, как функция возвращает структуру.