В C, как бы я выбрал, возвращать ли структуру или указатель на структуру?
работа над моей мышцей C в последнее время и просмотр многих библиотек, с которыми я работал, безусловно, дали мне хорошее представление о том, что такое хорошая практика. Одна вещь, которую я не видел-это функция, которая возвращает структуру:
something_t make_something() { ... }
из того, что я впитал это "правильный" способ сделать это:
something_t *make_something() { ... }
void destroy_something(something_t *object) { ... }
архитектура в фрагменте кода 2 является гораздо более популярным, чем фрагмент 1. Итак, теперь я спрашиваю, почему я должен когда-либо возвращать структуру напрямую, как в фрагменте 1? Какие различия следует учитывать при выборе между двумя вариантами?
кроме того, как этот параметр сравнивать?
void make_something(something_t *object)
6 ответов:
, когда
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);
это почти всегда о стабильности ABI. Двоичная стабильность между версиями библиотеки. В тех случаях, когда это не так, иногда речь идет о динамическом размере структур. Редко речь идет о чрезвычайно больших
structs или производительность.
это чрезвычайно редко, что выделение
structв куче и возвращая его почти так же быстро, как возвращая его по значению. Элементstructдолжно быть огромным.действительно, скорость не является причиной за методом 2, возврат по указателю, вместо возврата по значению.
Метод 2 существует для стабильности ABI. Если у вас есть
structи ваша следующая версия библиотеки добавляет к ней еще 20 полей, потребители вашей предыдущей версии библиотеки совместимы на уровне двоичного кода если им передают предварительно сконструированные указатели. Дополнительные данные за пределами концаstructОни знают о чем-то, о чем они не должны знать.если вы вернете его стек, вызывающий выделяет память для него, и они должны согласиться с вами о том, насколько он велик. Если ваша библиотека обновлена с момента их последнего перестроения, вы собираетесь уничтожить стек.
Метод 2 также позволяет скрыть дополнительные данные как до, так и после возвращаемого указателя (какие версии, добавляющие данные в конец структуры, являются вариантом). Вы можете закончить структуру массивом переменного размера или добавить указатель с некоторыми дополнительными данными, или оба.
если вы хотите stack-allocated
structs в стабильном ABI, почти все функции, которые говорят с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они работают.
как правило, вы никогда не должны пройти
structобъекты по значению. На практике это будет хорошо, если они будут меньше или равны максимальному размеру, который ваш процессор может обрабатывать в одной инструкции. Но стилистически, как правило, избегают его даже тогда. Если вы никогда не передаете структуры по значению, вы можете позже добавить членов в структуру, и это не повлияет на производительность.думаю, что
void make_something(something_t *object)является наиболее распространенным способом использования структур в C. Вы оставляете выделение для вызывающего абонента. Это эффективно, но не красиво.однако объектно-ориентированные программы на C используют
something_t *make_something()так как они построены с понятием непрозрачного типа, которая заставляет вас использовать указатели. Является ли возвращенный указатель на динамическую память или что-то другое, зависит от реализации. OO с непрозрачным типом часто является одним из самых элегантных и лучших способов разработки более сложных программ на C, но, к сожалению, немногие программисты на C знают/заботятся об этом.
некоторые плюсы первого подхода:
- меньше кода писать.
- более идиоматично для случая использования возврата нескольких значений.
- работает на системах, которые не имеют динамическое распределение.
- вероятно, быстрее для небольших или небольших объектов.
- нет утечки памяти из-за забывая
free.некоторые минусы:
- если объект большой (скажем, мегабайт) , может вызвать переполнение стека, или может быть медленным, если компиляторы не оптимизируют его наилучшим образом.
- может удивить людей, которые узнали C в 1970-х годах, когда это было невозможно, и не в курсе.
- не работает с объектами, которые содержат указатель на часть себя.
Я несколько удивлен.
разница в том, что Пример 1 создает структуру в стеке, Пример 2 создает ее в куче. В коде C или C++, который фактически является C, это идиоматично и удобно создавать большинство объектов в куче. В C++ этого нет, в основном они идут в стек. Причина в том, что если вы создаете объект в стеке, деструктор вызывается автоматически, если вы создаете его в куче, он должен быть вызван explicitly.So это намного проще обеспечить там нет утечек памяти и для обработки исключений все идет на стеке. В C деструктор должен быть вызван явно в любом случае, и нет понятия специальной функции деструктора (у вас есть деструкторы, конечно, но они просто обычные функции с именами, такими как destroy_myobject()).
теперь исключение в C++ предназначено для низкоуровневых объектов-контейнеров, например векторов, деревьев, хэш-карт и т. д. Они сохраняют элементы кучи, и у них есть деструкторы. Сейчас самая память-тяжелая объекты состоят из нескольких непосредственных элементов данных, дающих размеры, идентификаторы, теги и т. д., а затем остальную информацию в структурах STL, возможно, вектор пиксельных данных или карту английских пар слово / значение. Таким образом, большая часть данных фактически находится в куче, даже в C++.
а современный C++ устроен так, что этот паттерн
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); }будет компилироваться в довольно эффективный код. Большой элемент наблюдения не будет генерировать ненужные копии макета.
В отличие от некоторых других языков (например, Python), C не имеет понятия a кортежа!--7-->. Например, в 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 ); }это только один пример, где вы можете увидеть функцию, возвращающую структуру.