В чем преимущество возврата указателя на структуру по сравнению с возвратом всей структуры в return
выражении функции?
Я говорю о таких функциях, как fopen
и другие низкоуровневые функции, но, вероятно, есть функции более высокого уровня, которые также возвращают указатели на структуры.
Я считаю, что это скорее выбор дизайна, а не просто вопрос программирования, и мне любопытно узнать больше о преимуществах и недостатках этих двух методов.
Одна из причин, по которой я думал, что это было бы преимуществом для возврата указателя на структуру, состоит в том, чтобы иметь возможность более легко определить, произошла ли ошибка при возврате NULL
указателя.
Я полагаю, что вернуть полную структуру NULL
будет сложнее или менее эффективно. Это веская причина?
источник
gets()
функцию. Некоторые программисты по-прежнему испытывают отвращение к копированию структур, старые привычки сильно умирают.FILE*
фактически является непрозрачной ручкой. Код пользователя не должен заботиться о его внутренней структуре.&
и получить доступ к члену.
».Ответы:
Есть несколько практических причин, по которым такие функции, как
fopen
указатели возврата, вместо экземпляровstruct
типов:struct
типа от пользователя;В случае типов , такие как
FILE *
, это потому , что вы не хотите подвергать детали представления типа - к пользователю - этоFILE *
объект служит непрозрачной ручкой, и вы просто передать эту ручку для различных процедур ввода / вывода (и в то время какFILE
это часто реализовано какstruct
тип, это не обязательно ).Итак, вы можете выставить неполный
struct
тип где-нибудь в заголовке:Хотя вы не можете объявить экземпляр неполного типа, вы можете объявить указатель на него. Так что я могу создать
FILE *
и назначить к нему черезfopen
,freopen
и т.д., но я не могу напрямую манипулировать объект он указывает.Также вероятно, что
fopen
функция выделяетFILE
объект динамически, используяmalloc
или подобный. В этом случае имеет смысл вернуть указатель.Наконец, возможно, вы храните какое-то состояние в
struct
объекте, и вам нужно сделать это состояние доступным в нескольких разных местах. Если вы возвращаете экземплярыstruct
типа, эти экземпляры будут отдельными объектами в памяти друг от друга и в конечном итоге выйдут из синхронизации. Возвращая указатель на один объект, все ссылаются на один и тот же объект.источник
Есть два способа «вернуть структуру». Вы можете вернуть копию данных, или вы можете вернуть ссылку (указатель) на них. Обычно предпочитают возвращать (и вообще обходить) указатель по нескольким причинам.
Во-первых, копирование структуры занимает намного больше процессорного времени, чем копирование указателя. Если это то, что ваш код делает часто, это может вызвать заметную разницу в производительности.
Во-вторых, независимо от того, сколько раз вы копируете указатель, он все равно указывает на одну и ту же структуру в памяти. Все модификации к нему будут отражены в одной структуре. Но если вы копируете саму структуру, а затем вносите изменения, изменение отображается только в этой копии . Любой код, который содержит другую копию, не увидит изменения. Иногда, очень редко, это то, что вы хотите, но в большинстве случаев это не так, и это может вызвать ошибки, если вы ошиблись.
источник
В дополнение к другим ответам иногда стоит возвращать небольшое
struct
значение. Например, можно вернуть пару данных и некоторый код ошибки (или успеха), связанный с этим.Для примера
fopen
возвращает только одни данные (открытыеFILE*
) и в случае ошибки выдает код ошибки черезerrno
псевдоглобальную переменную. Но, возможно, было бы лучше вернуть astruct
из двух членов:FILE*
дескриптор и код ошибки (который будет установлен, если дескриптор файлаNULL
). По историческим причинам это не так (и об ошибках сообщается черезerrno
глобальный, который сегодня является макросом).Обратите внимание, что язык Go имеет приятную нотацию для возврата двух (или нескольких) значений.
Также обратите внимание, что в Linux / x86-64 ABI и соглашения о вызовах (см. Страницу x86-psABI ) указывают, что a
struct
из двух скалярных членов (например, указатель и целое число, или два указателя, или два целых числа) возвращается через два регистра (и это очень эффективно и не идет через память).Таким образом, в новом C-коде возврат небольшого C-кода
struct
может быть более читабельным, более ориентированным на потоки и более эффективным.источник
rdx:rax
. Таким образомstruct foo { int a,b; };
, возвращается упакованный вrax
(например, с shift / или), и должен быть распакован с shift / mov. Вот пример на Годболт . Но x86 может использовать младшие 32 бита 64-битного регистра для 32-битных операций, не заботясь о старших битах, так что это всегда слишком плохо, но определенно хуже, чем использование 2 регистров большую часть времени для структур с двумя членами.std::optional<int>
возвращает логическое значение в верхней половинеrax
, поэтому для его проверки необходима 64-битная константа маскиtest
. Или вы могли бы использоватьbt
. Но это отстой для вызывающей и сравниваемой вызываемой стороны с использованиемdl
, которое компиляторы должны делать для «частных» функций. Также связано: libstdc ++std::optional<T>
не копируется тривиально, даже когда T, поэтому всегда возвращается через скрытый указатель: stackoverflow.com/questions/46544019/… . (libc ++'s тривиально копируемый)struct { int a; _Bool b; };
в C, если абонент хочет проверить логическое значение, поскольку тривиальным-копируемыми C ++ Структуры используют один и тот же ABI , как С.div_t div()
Вы на правильном пути
Обе указанные вами причины действительны:
Если у вас есть текстура (например) где-то в памяти, и вы хотите ссылаться на эту текстуру в нескольких местах вашей программы; было бы неразумно делать копию каждый раз, когда вы хотите сослаться на нее. Вместо этого, если вы просто передадите указатель для ссылки на текстуру, ваша программа будет работать намного быстрее.
Самая большая причина - динамическое распределение памяти. Часто, когда программа компилируется, вы не знаете точно, сколько памяти вам нужно для определенных структур данных. Когда это произойдет, объем памяти, который вам нужно использовать, будет определен во время выполнения. Вы можете запросить память, используя 'malloc', а затем освободить ее, когда вы закончите использовать 'free'.
Хорошим примером этого является чтение из файла, указанного пользователем. В этом случае вы не представляете, насколько большим может быть файл при компиляции программы. Вы можете только выяснить, сколько памяти вам нужно, когда программа действительно запущена.
И malloc, и free возвращают указатели на места в памяти. Поэтому функции, использующие динамическое распределение памяти, будут возвращать указатели туда, где они создали свои структуры в памяти.
Кроме того, в комментариях я вижу, что есть вопрос, можете ли вы вернуть структуру из функции. Вы действительно можете сделать это. Следующее должно работать:
источник
struct incomplete* foo(void)
. Таким образом, я могу объявлять функции в заголовке, но определять только структуры в C-файле, что позволяет инкапсуляцию.Что-то вроде a на
FILE*
самом деле не является указателем на структуру в том, что касается клиентского кода, а является формой непрозрачного идентификатора, связанного с какой-либо другой сущностью, такой как файл. Когда программа вызываетfopen
, она, как правило, не заботится ни о каком содержимом возвращаемой структуры - все, о чем она будет заботиться, - это то, что другие функции вродеfread
будут делать с ней все, что им нужно.Если стандартная библиотека хранит
FILE*
информацию о, например, текущей позиции чтения в этом файле, вызовfread
должен иметь возможность обновить эту информацию. Имеяfread
получить указатель наFILE
делает это легко. Еслиfread
вместо этого получитьFILE
, он не сможет обновитьFILE
объект, удерживаемый вызывающей стороной.источник
Сокрытие информации
Наиболее распространенным является скрытие информации . С, скажем, не имеет возможности сделать поля
struct
приватными, не говоря уже о методах доступа к ним.Так что, если вы хотите принудительно запретить разработчикам видеть и манипулировать содержимым объекта pointee, например
FILE
, единственный способ - не дать им получить доступ к его определению, обрабатывая указатель как непрозрачный, размер которого pointee и определения неизвестны внешнему миру. В этом случае определениеFILE
будет видно только тем, кто реализует операции, для которых требуется его определение, напримерfopen
, в то время как общему заголовку будет видна только декларация структуры.Двоичная совместимость
Сокрытие определения структуры может также помочь обеспечить передышку для сохранения бинарной совместимости в API-интерфейсах dylib. Это позволяет разработчикам библиотеки изменять поля в непрозрачной структуре, не нарушая бинарную совместимость с теми, кто использует библиотеку, поскольку природа их кода должна знать только то, что они могут делать со структурой, а не то, насколько она велика или какие поля в нем есть.
Например, я могу запустить некоторые древние программы, созданные в эпоху Windows 95 сегодня (не всегда идеально, но на удивление многие все еще работают). Скорее всего, в некотором коде этих древних двоичных файлов использовались непрозрачные указатели на структуры, размер и содержание которых изменились с эпохи Windows 95. Тем не менее, программы продолжают работать в новых версиях окон, поскольку они не были открыты для содержимого этих структур. При работе с библиотекой, где важна двоичная совместимость, то, что клиент не подвергает воздействию, обычно может меняться без нарушения обратной совместимости.
КПД
Как правило, это менее эффективно, если предположить, что тип может практически уместиться и быть распределенным в стеке, если обычно за кулисами не используется гораздо менее обобщенный распределитель памяти, чем
malloc
, например, уже выделенная память пула распределителя фиксированного размера, а не переменного размера. В данном случае это компромисс безопасности, скорее всего, позволить разработчикам библиотеки поддерживать инварианты (концептуальные гарантии), связанные сFILE
.Это не такая веская причина, по крайней мере, с точки зрения производительности,
fopen
возвращать указатель, поскольку единственная причина, по которой он возвращает,NULL
- это невозможность открыть файл. Это было бы оптимизацией исключительного сценария в обмен на замедление всех распространенных путей выполнения. В некоторых случаях может быть веская причина повышения производительности, чтобы сделать проекты более прямыми, чтобы они возвращали указатели, чтобы позволитьNULL
возвращаться в некоторых постусловиях.Для файловых операций издержки относительно тривиальны по сравнению с самими файловыми операциями, и ручного управления в
fclose
любом случае избежать нельзя. Поэтому мы не можем избавить клиента от необходимости освобождения (закрытия) ресурса, предоставляя определениеFILE
и возвращая его по значениюfopen
или ожидая значительного увеличения производительности, учитывая относительную стоимость самих файловых операций, чтобы избежать выделения кучи. ,Горячие точки и исправления
Однако в других случаях я профилировал много расточительного кода на C в устаревших кодовых базах с горячими точками
malloc
и ненужными пропусками обязательного кэша в результате слишком частого использования этой практики с непрозрачными указателями и ненужного выделения слишком большого количества вещей в куче, иногда в большие петли.Вместо этого я использую альтернативную практику - раскрыть определения структуры, даже если клиент не предназначен для их подмены, используя стандарт соглашения об именах, чтобы сообщить, что никто другой не должен касаться полей:
Если в будущем возникнут проблемы с бинарной совместимостью, я нахожу это достаточно хорошим, чтобы просто избыточно зарезервировать дополнительное пространство для будущих целей, например:
Это зарезервированное пространство немного расточительно, но может спасти жизнь, если в будущем мы обнаружим, что нам нужно добавить еще немного данных,
Foo
не ломая двоичные файлы, которые используют нашу библиотеку.По моему мнению, скрытие информации и двоичная совместимость, как правило, являются единственной достойной причиной, позволяющей только выделять кучу структур помимо структур переменной длины (что всегда будет требоваться, или, по крайней мере, будет немного неудобно использовать в противном случае, если клиент должен был выделить память в стеке способом VLA для выделения VLS). Даже большие структуры часто дешевле вернуть по значению, если это означает, что программное обеспечение работает намного больше с горячей памятью в стеке. И даже если бы они не были дешевле вернуть по стоимости при создании, можно было бы просто сделать это:
... инициализировать
Foo
из стека без возможности лишнего копирования. Или клиент даже имеет свободу размещенияFoo
в куче, если он хочет по какой-то причине.источник