Ukrywanie informacji
Jaka jest zaleta zwracania wskaźnika do struktury w porównaniu do zwracania całej struktury w instrukcji return funkcji?
Najczęstszym z nich jest ukrywanie informacji . C nie ma, powiedzmy, możliwości tworzenia pól struct
prywatnych, nie mówiąc już o zapewnieniu metod dostępu do nich.
Jeśli więc chcesz silnie uniemożliwić programistom wyświetlanie i manipulowanie zawartością pointee, na przykład FILE
, jedynym sposobem jest zapobieganie narażeniu ich na definicję poprzez traktowanie wskaźnika jako nieprzezroczystego, którego rozmiar pointee i definicje są nieznane światu zewnętrznemu. Definicja FILE
będzie wtedy widoczna tylko dla tych, którzy wykonują operacje wymagające jej definicji, na przykład fopen
, podczas gdy tylko deklaracja struktury będzie widoczna dla nagłówka publicznego.
Kompatybilność binarna
Ukrywanie definicji struktury może również pomóc w zapewnieniu oddechu w celu zachowania zgodności binarnej w interfejsach API dylib. Pozwala to implementatorom bibliotek zmieniać pola w nieprzezroczystej strukturze bez naruszania binarnej kompatybilności z tymi, którzy korzystają z biblioteki, ponieważ charakter ich kodu musi tylko wiedzieć, co mogą zrobić ze strukturą, a nie jej wielkości lub pól to ma.
Jako przykład mogę dziś uruchomić niektóre starożytne programy zbudowane w czasach Windows 95 (nie zawsze idealnie, ale zaskakująco wiele nadal działa). Istnieje prawdopodobieństwo, że część kodu tych starożytnych plików binarnych używała nieprzezroczystych wskaźników do struktur, których rozmiar i zawartość zmieniły się od czasów Windows 95. Jednak programy nadal działają w nowych wersjach systemu Windows, ponieważ nie były narażone na zawartość tych struktur. Podczas pracy nad biblioteką, w której ważna jest zgodność binarna, to, na co klient nie jest narażony, zwykle może się zmieniać bez naruszania zgodności wstecznej.
Wydajność
Zwrócenie pełnej struktury, która ma wartość NULL, byłoby trudniejsze lub mniej wydajne. Czy to ważny powód?
Zazwyczaj jest mniej wydajny, zakładając, że ten typ może praktycznie pasować i być alokowany na stosie, chyba że za sceną jest używany znacznie mniej uogólniony alokator pamięci malloc
, niż , jak już przydzielona pamięć puli alokatora o stałej wielkości zamiast zmiennej. W tym przypadku jest to kompromis w zakresie bezpieczeństwa, który najprawdopodobniej pozwala twórcom bibliotek zachować niezmienniki (gwarancje koncepcyjne) FILE
.
Nie jest to tak ważny powód, przynajmniej z punktu widzenia wydajności, aby fopen
zwrócić wskaźnik, ponieważ jedynym powodem, dla którego zwraca NULL
to niepowodzenie otwarcia pliku. Byłoby to optymalizowanie wyjątkowego scenariusza w zamian za spowolnienie wszystkich ścieżek wykonywania typowych przypadków. W niektórych przypadkach może istnieć uzasadniony powód produktywności, aby uprościć projekty, aby zwracały wskaźniki i pozwalały NULL
na zwrot w określonych warunkach.
W przypadku operacji na plikach narzut jest stosunkowo dość trywialny w porównaniu z samymi operacjami na plikach, a instrukcji i fclose
tak nie można uniknąć. Więc to nie tak, że możemy zaoszczędzić klientowi kłopotów z uwolnieniem (zamknięciem) zasobu poprzez ujawnienie definicji FILE
i zwrócenie jej wartości fopen
lub oczekiwanie znacznego wzrostu wydajności, biorąc pod uwagę względny koszt samych operacji na plikach, aby uniknąć przydziału sterty .
Hotspoty i poprawki
W innych przypadkach jednak sprofilowałem wiele marnotrawczego kodu C w starszych bazach kodów z punktami dostępowymi malloc
i niepotrzebnymi obowiązkowymi brakami pamięci podręcznej w wyniku zbyt częstego używania tej praktyki z nieprzejrzystymi wskaźnikami i niepotrzebnego przydzielania zbyt wielu rzeczy na stosie, czasem w duże pętle.
Alternatywną praktyką, której używam, jest ujawnianie definicji struktur, nawet jeśli klient nie ma zamiaru ich modyfikować, używając standardu konwencji nazewnictwa, aby poinformować, że nikt inny nie powinien dotykać pól:
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);
Jeśli w przyszłości pojawią się problemy z kompatybilnością binarną, uważam, że wystarczające jest rezerwowanie dodatkowej przestrzeni na przyszłe cele, na przykład:
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;
};
Ta zarezerwowana przestrzeń jest trochę marnotrawstwem, ale może uratować życie, jeśli w przyszłości okaże się, że musimy dodać więcej danych Foo
bez niszczenia plików binarnych, które korzystają z naszej biblioteki.
Moim zdaniem ukrywanie informacji i zgodność binarna to zazwyczaj jedyny słuszny powód, aby zezwolić na alokację sterty struktur oprócz struktur o zmiennej długości (które zawsze tego wymagałyby, lub przynajmniej byłyby trochę niewygodne w użyciu, gdyby klient musiał przydzielić pamięć na stosie w sposób VLA do alokacji VLS). Nawet duże struktury są często tańsze w zwracaniu według wartości, jeśli oznacza to, że oprogramowanie działa znacznie bardziej z gorącą pamięcią na stosie. I nawet jeśli nie byłyby tańsze, by zwracać wartość po stworzeniu, można po prostu to zrobić:
int foo_create(struct Foo* foo);
...
/* In the client code: */
struct Foo foo;
if (foo_create(&foo))
{
foo_something(&foo);
foo_destroy(&foo);
}
... aby zainicjować Foo
ze stosu bez możliwości zbędnej kopii. Lub klient ma nawet swobodę alokacji Foo
na stercie, jeśli z jakiegoś powodu tego chce.