Wskaźniki potrójne w C: czy to kwestia stylu?


Czuję, że potrójne wskaźniki w C są uważane za „złe”. Korzystam z nich od czasu do czasu.
Począwszy od podstaw,

jeden wskaźnik

ma dwa cele: stworzyć tablicę i pozwolić funkcji modyfikować jej zawartość (przekazywać przez referencję):
char *a;
a = malloc...

lub
void foo (char *c);//means I'm going to modify the parameter in foo.
{ *c = 'f'; }char a;
foo(&a);

Podwójnie

wskaźnik

może być tablicą 2D (lub tablicą tablic, ponieważ każda „kolumna” lub „wiersz” nie musi mieć tej samej długości). Osobiście lubię go używać, gdy muszę przekazać tablicę 1D:
void foo (char **c);//means I'm going to modify the elements of an array in foo.
{ (*c)[0] = 'f'; }char *a;
a = malloc...
foo(&a);

Mi pomaga opisać, co robi fu. Nie jest to jednak konieczne:
void foo (char *c);//am I modifying a char or just passing a char array?
{ c[0] = 'f'; }char *a;
a = malloc...
foo(a);

zadziała też.
Zgodnie z pierwszą odpowiedzią na

to pytanie
https://coderoad.ru/1106957/
jeśli
foo
zmienia rozmiar tablicy, wymagany jest podwójny wskaźnik.
Możesz wyraźnie zobaczyć, jak to potrwa

potrójny wskaźnik

(i nie tylko). W moim przypadku, gdybym przekazywał tablicę wskaźników (lub tablicę tablic), użyłbym tego. Oczywiście jest to wymagane, jeśli przejdziesz do funkcji, która zmienia rozmiar tablicy wielowymiarowej. Oczywiście tablice tablic nie są zbyt powszechne, ale są inne przypadki.
Więc jakie są te konwencje? Czy to naprawdę tylko kwestia stylu/czytelności w połączeniu z faktem, że wielu osobom trudno jest owinąć głowę wskazówkami?
Zaproszony:
Anonimowy użytkownik

Anonimowy użytkownik

Potwierdzenie od:

Używanie wskaźników potrójnych + szkodzi zarówno czytelności, jak i łatwości konserwacji.
Załóżmy, że masz tutaj małą deklarację funkcji:
void fun(int***);

Hmmm. Czy argument jest nieregularną tablicą 3-D, wskaźnikiem do tablicy nieregularnej 2-D lub wskaźnikiem do wskaźnika do tablicy (na przykład funkcja przydziela tablicę i przypisuje wskaźnik do int wewnątrz funkcji)
Porównajmy to z:
void fun(IntMatrix*);

Możesz oczywiście użyć potrójnych wskaźników do liczb całkowitych, aby pracować z macierzami.

Ale to nie to, czym oni są
... Fakt, że są one tutaj zaimplementowane jako potrójne wskaźniki, nie ma

relacje

do użytkownika.
Złożone struktury danych powinny być

kapsułkowane
... To jedna z głównych idei programowania obiektowego. Nawet w C możesz do pewnego stopnia zastosować tę zasadę. Zawiń strukturę danych w strukturę (lub, bardzo często w C, używając „uchwytów”, czyli wskaźników do niekompletnego typu - ten idiom zostanie wyjaśniony w dalszej części odpowiedzi).
Załóżmy, że zaimplementowałeś macierze jako tablice postrzępione
double
. W porównaniu z ciągłymi tablicami 2D, są gorsze, gdy je iterują (ponieważ nie należą do tego samego bloku ciągłej pamięci), ale można uzyskać do nich dostęp za pomocą notacji tablicowej, a każdy ciąg może mieć inny rozmiar.
Więc teraz problem polega na tym, że nie możesz teraz zmienić widoków, ponieważ użycie wskaźników jest zakodowane na stałe w kodzie niestandardowym, a teraz utkniesz z gorszą implementacją.
Nie stanowiłoby to nawet problemu, gdybyś zamknął go w strukturze.
typedef struct Matrix_
{
double** data;
} Matrix;double get_element(Matrix* m, int i, int j)
{
return m->data[i][j];
}

po prostu zmienia się na
typedef struct Matrix_
{
int width;
double data[];//C99 flexible array member
} Matrix;double get_element(Matrix* m, int i, int j)
{
return m->data[i*m->width+j];
}

Metoda deskryptora działa w następujący sposób: w pliku nagłówkowym deklarujesz niedokończoną strukturę i wszystkie funkcje, które działają na wskaźniku do tej struktury:
// struct declaration with no body. 
struct Matrix_;
// optional: allow people to declare the matrix with Matrix* instead of struct Matrix*
typedef struct Matrix_ Matrix;Matrix* create_matrix(int w, int h);
void destroy_matrix(Matrix* m);
double get_element(Matrix* m, int i, int j);
double set_element(Matrix* m, double value, int i, int j);

w pliku źródłowym deklarujesz aktualną strukturę i definiujesz wszystkie funkcje:
typedef struct Matrix_
{
int width;
double data[];//C99 flexible array member
} Matrix;double get_element(Matrix* m, int i, int j)
{
return m->data[i*m->width+j];
}/* definition of the rest of the functions */

reszta świata nie wie, co zawiera
struct Matrix_
, ani nie zna jej rozmiaru. Oznacza to, że użytkownicy nie mogą bezpośrednio deklarować wartości, ale tylko za pomocą wskaźnika do funkcji
Matrix
i
create_matrix
. Jednak fakt, że użytkownik nie zna rozmiaru, oznacza, że ​​jest od niego niezależny - co oznacza, że ​​możemy dowolnie usuwać lub dodawać elementy do
struct Matrix_
.
Anonimowy użytkownik

Anonimowy użytkownik

Potwierdzenie od:

W większości przypadków użycie 3 poziomów pośrednich jest objawem złych decyzji projektowych w innych częściach programu. Jest to więc uważane za złą praktykę i istnieją żarty na temat „trzech gwiazdek programistów”, gdzie w przeciwieństwie do ocen restauracji, im więcej gwiazdek, tym gorsza jakość.
Potrzeba trzech poziomów pośrednich często wynika z nieporozumień dotyczących prawidłowego dynamicznego przydzielania tablic wielowymiarowych. Często jest źle rozumiany nawet w książkach o programowaniu, po części dlatego, że robienie tego dobrze było kłopotliwe przed standardem C99. Mój post Pytania i odpowiedzi

poprawnie rozprowadza tablice wielowymiarowe
https://coderoad.ru/42094465/
rozwiązując ten sam problem, a także ilustruje, jak wiele poziomów pośrednich sprawi, że kod będzie jeszcze trudniejszy do odczytania i utrzymania.
Chociaż, jak wyjaśnia ten post, są sytuacje, w których
typ **
może mieć sens. Przykładem jest zmienna tabela ciągów o zmiennej długości. A kiedy pojawi się potrzeba
type **
, możesz wkrótce ulec pokusie, aby użyć
type ***
, ponieważ musisz zwrócić swój
type **
za pomocą parametru funkcji.
Najczęściej taka potrzeba pojawia się w sytuacji, gdy projektujesz jakiś kompleks ADT. Na przykład, załóżmy, że kodujemy tablicę skrótów, w której każdy indeks jest połączoną listą połączoną, a każdy węzeł na połączonej liście jest tablicą. Zatem poprawnym rozwiązaniem jest przeprojektowanie programu tak, aby używał struktur zamiast kilku poziomów pośrednich. Tabela skrótów, połączona lista i tablica muszą być różnymi typami, samodzielnymi typami bez wzajemnej świadomości.
W ten sposób, używając odpowiedniego projektu, automatycznie unikniemy wielu gwiazd.
Ale tak jak w przypadku każdej zasady dobrej praktyki programistycznej, zawsze są wyjątki. Jest całkiem możliwe, że mamy taką sytuację:
  • Musisz zaimplementować tablicę ciągów.
  • Liczba wierszy jest zmienna i może się zmieniać w czasie wykonywania.
  • Długość sznurków jest zmienna.

ty

czy możesz

zaimplementuj powyższe jako ADT, ale mogą być również dobre powody, aby zachować prostotę i prostotę, używając
char * [n]
. Następnie masz dwie opcje dynamicznego rozpowszechniania tych informacji:
char* (*arr_ptr)[n] = malloc( sizeof(char*[n]) );

lub
char** ptr_ptr = malloc( sizeof(char*[n]) );

Pierwsza jest bardziej poprawna formalnie, ale też uciążliwa. Ponieważ powinno być używane jak
(* arr_ptr) [i] = "string";
, podczas gdy alternatywa może być używana jak
ptr_ptr [i] = "string";
.
Załóżmy teraz, że musimy umieścić wywołanie malloc wewnątrz funkcji,

i

typ zwracania jest zarezerwowany dla kodów błędów, jak to zwykle robi się w C API. Wtedy te dwie alternatywy będą wyglądać tak:
err_t alloc_arr_ptr (size_t n, char* (**arr)[n])
{
*arr = malloc( sizeof(char*[n]) ); return *arr == NULL ? ERR_ALLOC : OK;
}

lub
err_t alloc_ptr_ptr (size_t n, char*** arr)
{
*arr = malloc( sizeof(char*[n]) ); return *arr == NULL ? ERR_ALLOC : OK;
}

Trudno się spierać i powiedzieć, że pierwsza opcja jest bardziej czytelna, a także wiąże się z uciążliwym dostępem wymaganym przez dzwoniącego. Trzygwiazdkowa alternatywa jest właściwie bardziej elegancka w tym bardzo szczególnym przypadku.
Dlatego nie powinniśmy dogmatycznie odrzucać trzech poziomów pośrednictwa. Ale wybór ich użycia musi być dobrze poinformowany, ze świadomością, że mogą tworzyć brzydki kod i że istnieją inne alternatywy.
Anonimowy użytkownik

Anonimowy użytkownik

Potwierdzenie od:

Więc jakie są te konwencje? Czy to naprawdę tylko kwestia stylu/czytelności w połączeniu z faktem, że wielu osobom trudno jest owinąć głowę wskazówkami?

Wielokrotne pośrednictwo nie jest złym stylem ani czarną magią, a jeśli masz do czynienia z danymi wielowymiarowymi, będziesz miał do czynienia z wysokim poziomem pośrednictwa; jeśli naprawdę masz do czynienia ze wskaźnikiem do wskaźnika do wskaźnika do
T
, napisz
T *** p;
. Nie chowaj wskaźników za typedefami,

Jeśli

tylko użytkownik tego typu nie powinien zawracać sobie głowy jego wskaźnikiem. Na przykład, jeśli podasz typ jako „uchwyt”, który jest przekazywany do interfejsu API, na przykład:
typedef ... *Handle;Handle h = NewHandle();
DoSomethingWith( h, some_data );
DoSomethingElseWith( h, more_data );
ReleaseHandle( h );

to dokładnie z
typedef
. Ale jeśli
h
kiedykolwiek będzie wymagało dereferencji, np.
printf( "Handle value is %d\n", *h );

następnie

nie rób tego
... Jeśli twój użytkownik

musisz wiedzieć

że
h
jest wskaźnikiem do
int
<sup>
1
</sup>
Aby używać go poprawnie, ta informacja

nie

powinien chować się za typedef.
Powiem, że w moim doświadczeniu nie miałem do czynienia z wyższymi poziomami pośrednictwa; triple indirection było najwyższe i nie musiałem go używać więcej niż kilka razy. Jeśli regularnie napotykasz & > dane trójwymiarowe, zauważysz wysoki poziom pośredni, ale jeśli zrozumiesz, jak

praca

wyrażenia wskaźnikowe i pośrednie nie powinny stanowić problemu.
<sup>
1. Albo wskaźnik do wskaźnika do
int
, albo wskaźnik do wskaźnika do wskaźnika do
struct grdlphmp
, lub cokolwiek innego.
</sup>
Anonimowy użytkownik

Anonimowy użytkownik

Potwierdzenie od:

Niestety źle zrozumiałeś pojęcie wskaźnika i tablic w C. Pamiętaj o tym

tablice nie są wskaźnikami

.

Zaczynając od podstaw, pojedynczy wskaźnik ma dwa cele: stworzyć tablicę i pozwolić funkcji modyfikować jej zawartość (przekazywać przez referencję):

Kiedy deklarujesz wskaźnik, musisz go zainicjować, zanim użyjesz go w swoim programie. Można to zrobić, przekazując do niego adres zmiennej lub dynamicznie przydzielając pamięć.

W tym drugim przypadku wskaźnik może być używany jak tablice indeksowane (ale nie jest to tablica).

Podwójny wskaźnik może być tablicą 2D (lub tablicą tablic, ponieważ każda „kolumna” lub „wiersz” nie musi mieć takiej samej długości). Osobiście lubię go używać, gdy muszę przekazać tablicę 1D:

Znowu źle. Tablice nie są wskaźnikami i na odwrót. Wskaźnik do wskaźnika nie jest tablicą 2D.

Radziłbym przeczytać

sekcja c-faq 6. Tablice i wskaźniki
http://www.c-faq.com/aryptr/index.html
.
Anonimowy użytkownik

Anonimowy użytkownik

Potwierdzenie od:

Po dwóch poziomach braku orientacji zrozumienie staje się trudne. Co więcej, jeśli powodem, dla którego przekazujesz te potrójne (lub więcej) wskaźników do swoich metod jest fakt, że mogą one ponownie przydzielać i ustawiać pewną wskazaną pamięć, odchodzi to od koncepcji metod jako „funkcji”, które po prostu zwracają wartości wpływać na stan. Wpływa również negatywnie na zrozumienie i łatwość konserwacji poza pewnym punktem.
Ale bardziej fundamentalnie, natknąłeś się tutaj na jeden z głównych stylistycznych zastrzeżeń do potrójnego wskaźnika:

Można wyraźnie zobaczyć, jak potrzebny byłby potrójny wskaźnik (a nawet więcej).

Problem jest tutaj i poza nim: kiedy dojdziesz do trzech poziomów, na czym się zatrzymasz? Oczywiście,

mogą

mają określoną liczbę poziomów pośrednich. Ale lepiej jest po prostu mieć znajomą granicę gdzieś, gdzie przejrzystość jest nadal dobra, ale elastyczność jest wystarczająca. Dwa to dobra liczba. „Programowanie trzech gwiazdek”, jak to się czasem nazywa, jest w najlepszym przypadku kontrowersyjne; jest to albo genialne, albo bolesne dla każdego, kto później potrzebuje obsługiwać kod.

Aby odpowiedzieć na pytania, Zaloguj się lub Zarejestruj się