Приховування інформації
Яка перевага повернення вказівника на структуру на відміну від повернення всієї структури в операторі повернення функції?
Найпоширеніший - це приховування інформації . Скажімо, C не має можливості робити поля struct
приватними, не кажучи вже про надання методів доступу до них.
Отже, якщо ви хочете змусити перешкоджати розробникам не бачити та не змінювати вміст pointee, наприклад FILE
, єдиний єдиний спосіб - не допустити їх впливу на його визначення, трактуючи вказівник як непрозорий, розмір pointee та визначення зовнішнього світу невідомі. Тоді визначення FILE
волі буде видимим лише тим, хто реалізує операції, які потребують його визначення, наприклад fopen
, тоді як лише загальне оголошення буде видиме для загального заголовка.
Бінарна сумісність
Приховування визначення структури також може допомогти забезпечити приміщення для дихання для збереження бінарної сумісності в API дилібу. Це дозволяє реалізаторам бібліотеки змінювати поля в непрозорій структурі, не порушуючи бінарної сумісності з тими, хто використовує бібліотеку, оскільки природа їх коду лише повинна знати, що вони можуть робити зі структурою, а не наскільки вона велика або які поля Це має.
Як приклад, я фактично можу запустити деякі старовинні програми, побудовані в епоху Windows 95 сьогодні (не завжди ідеально, але на диво багато хто все ще працює). Цілком ймовірно, що частина коду для цих стародавніх двійкових файлів використовувала непрозорі покажчики на структури, розміри та вміст яких змінився з епохи Windows 95. Проте програми продовжують працювати в нових версіях Windows, оскільки вони не піддавалися впливу вмісту цих структур. Під час роботи над бібліотекою, де важлива бінарна сумісність, те, що клієнт не піддається впливу, як правило, дозволяється змінювати, не порушуючи зворотної сумісності.
Ефективність
Повернути повну структуру, яка є NULL, було б важче або, напевно, менш ефективним. Це поважна причина?
Зазвичай це менш ефективно, якщо припустити, що тип може практично підходити і бути розподіленим на стеці, якщо, як правило, не використовується набагато менш узагальнений розподільник пам'яті, який використовується за кадром, ніж malloc
, наприклад, фіксований розмір, а не змінний розмір об'єднаної пам'яті, об'єднаний у пам'ять. У цьому випадку, найімовірніше, це безпека, що дозволяє розробникам бібліотек підтримувати інваріанти (концептуальні гарантії), пов'язані з цим FILE
.
Це не така вагома причина, принаймні з точки зору продуктивності, щоб fopen
повернути покажчик, оскільки єдина причина, по якій він повертається, NULL
- це невдача відкрити файл. Це було б оптимізацією виняткового сценарію в обмін на уповільнення всіх загальноприйнятих випадків виконання. У деяких випадках може бути поважна причина продуктивності, щоб зробити конструкції простішими, щоб змусити їх повертати покажчики, щоб вони NULL
могли повернутись за деякими пост-умовами.
Для файлових операцій накладні витрати відносно досить тривіальні в порівнянні з самими файловими операціями, і керівництва fclose
не можна уникнути. Тож це не так, як ми можемо врятувати клієнтові клопоту звільнити (закрити) ресурс, виставивши визначення FILE
та повернути його за значенням fopen
або очікувати великого підвищення продуктивності, враховуючи відносну вартість самих файлових операцій, щоб уникнути купірування розподілу .
Точки та виправлення
В інших випадках, проте, я перепрофілював багато марнотратнього коду С у застарілих кодових базах з гарячими точками malloc
та непотрібними обов'язковими помилками кешу в результаті використання цієї практики занадто часто з непрозорими вказівниками та виділення занадто багато речей на купі, іноді в великі петлі.
Альтернативна практика, яку я використовую замість цього, полягає у викритті дефініцій структури, навіть якщо клієнт не призначений для їх підробки, використовуючи стандарт конвенції про іменування, щоб повідомити, що ніхто більше не повинен торкатися полів:
struct Foo
{
/* priv_* indicates that you shouldn't tamper with these fields! */
int priv_internal_field;
int priv_other_one;
};
struct Foo foo_create(void);
void foo_destroy(struct Foo* foo);
void foo_something(struct Foo* foo);
Якщо в майбутньому виникають проблеми з сумісністю бінарних даних, я вважаю, що це досить добре, щоб просто зайво залишити додатковий простір для майбутніх цілей, як-от так:
struct Foo
{
/* priv_* indicates that you shouldn't tamper with these fields! */
int priv_internal_field;
int priv_other_one;
/* reserved for possible future uses (emergency backup plan).
currently just set to null. */
void* priv_reserved;
};
Цей зарезервований простір трохи марнотратний, але може врятувати життя, якщо ми виявимо в майбутньому, що нам потрібно додати ще трохи даних, Foo
не порушуючи бінарні файли, які використовують нашу бібліотеку.
На мою думку, приховування інформації та бінарної сумісності, як правило, є єдиною гідною причиною дозволяти лише розподіл структур, крім структур змінної довжини (які завжди вимагатимуть цього або хоча б бути трохи незручним для використання в іншому випадку, якщо клієнт повинен був виділити пам'ять на стеці в режимі VLA для виділення VLS). Навіть великі структури часто дешевше повернутись за вартістю, якщо це означає, що програмне забезпечення працює набагато більше з гарячою пам'яттю на стеці. І навіть якби вони не дешевше поверталися за вартістю при створенні, можна просто зробити це:
int foo_create(struct Foo* foo);
...
/* In the client code: */
struct Foo foo;
if (foo_create(&foo))
{
foo_something(&foo);
foo_destroy(&foo);
}
... ініціалізувати Foo
зі стека без можливості зайвої копії. Або клієнт навіть має свободу виділяти Foo
на купу, якщо хоче з якихось причин.