Wskaźniki C: wskazujące na tablicę o stałym rozmiarze

120

To pytanie jest skierowane do guru C:

W C można zadeklarować wskaźnik w następujący sposób:

char (* p)[10];

.. który zasadniczo stwierdza, że ​​ten wskaźnik wskazuje na tablicę 10 znaków. Zaletą deklarowania takiego wskaźnika jest to, że jeśli spróbujesz przypisać wskaźnik tablicy o różnym rozmiarze do p, otrzymasz błąd czasu kompilacji. Wystąpi również błąd czasu kompilacji, jeśli spróbujesz przypisać wartość prostego wskaźnika znaku do p. Próbowałem tego z gcc i wydaje się, że działa z ANSI, C89 i C99.

Wydaje mi się, że zadeklarowanie takiego wskaźnika byłoby bardzo przydatne - szczególnie podczas przekazywania wskaźnika do funkcji. Zwykle ludzie pisaliby prototyp takiej funkcji:

void foo(char * p, int plen);

Jeśli spodziewasz się bufora o określonej wielkości, po prostu przetestujesz wartość plen. Nie możesz jednak zagwarantować, że osoba, która przekaże ci p, naprawdę da ci pełne prawidłowe miejsce w pamięci w tym buforze. Musisz ufać, że osoba, która nazwała tę funkcję, postępuje właściwie. Z drugiej strony:

void foo(char (*p)[10]);

.. zmusiłoby wywołującego do podania buforu o określonym rozmiarze.

Wydaje się to bardzo przydatne, ale nigdy nie widziałem wskaźnika zadeklarowanego w ten sposób w żadnym kodzie, z którym się spotkałem.

Moje pytanie brzmi: czy jest jakiś powód, dla którego ludzie nie deklarują takich wskazówek? Czy nie widzę jakiejś oczywistej pułapki?

figurassa
źródło
3
uwaga: ponieważ C99 tablica nie musi mieć stałego rozmiaru, jak sugeruje tytuł, 10można ją zastąpić dowolną zmienną w zakresie
MM

Odpowiedzi:

174

To, co mówisz w swoim poście, jest całkowicie poprawne. Powiedziałbym, że każdy programista C dochodzi do dokładnie tego samego odkrycia i do dokładnie tego samego wniosku, gdy (jeśli) osiągnie pewien poziom biegłości w języku C.

Gdy specyfika obszaru aplikacji wywołuje tablicę o określonym stałym rozmiarze (rozmiar tablicy jest stałą czasu kompilacji), jedynym właściwym sposobem przekazania takiej tablicy do funkcji jest użycie parametru wskaźnika do tablicy

void foo(char (*p)[10]);

(w języku C ++ odbywa się to również z odwołaniami

void foo(char (&p)[10]);

).

Umożliwi to sprawdzanie typu na poziomie języka, co zapewni, że tablica o dokładnie poprawnym rozmiarze zostanie dostarczona jako argument. W rzeczywistości w wielu przypadkach ludzie używają tej techniki niejawnie, nawet nie zdając sobie z tego sprawy, ukrywając typ tablicy za nazwą typedef

typedef int Vector3d[3];

void transform(Vector3d *vector);
/* equivalent to `void transform(int (*vector)[3])` */
...
Vector3d vec;
...
transform(&vec);

Zauważ dodatkowo, że powyższy kod jest niezmienny w stosunku do Vector3dtypu będącego tablicą lub struct. Możesz Vector3dw dowolnym momencie przełączyć definicję z tablicy na a structiz powrotem, bez konieczności zmiany deklaracji funkcji. W obu przypadkach funkcje otrzymają obiekt zagregowany „przez odniesienie” (są od tego wyjątki, ale w kontekście tej dyskusji jest to prawdą).

Jednak nie zobaczysz tej metody przekazywania tablic używanej jawnie zbyt często, po prostu dlatego, że zbyt wielu ludzi jest zdezorientowanych dość zawiłą składnią i po prostu nie czuje się wystarczająco dobrze z takimi funkcjami języka C, aby używać ich poprawnie. Z tego powodu w przeciętnym prawdziwym życiu przekazywanie tablicy jako wskaźnika do jej pierwszego elementu jest bardziej popularnym podejściem. Po prostu wygląda na „prostsze”.

Ale w rzeczywistości użycie wskaźnika do pierwszego elementu do przekazywania tablic jest bardzo niszową techniką, sztuczką, która służy bardzo konkretnemu celowi: jego jedynym celem jest ułatwienie przekazywania tablic o różnej wielkości (tj. Rozmiar w czasie wykonywania) . Jeśli naprawdę potrzebujesz mieć możliwość przetwarzania tablic o rozmiarze w czasie wykonywania, to właściwym sposobem przekazania takiej tablicy jest użycie wskaźnika do jej pierwszego elementu z konkretnym rozmiarem dostarczonym przez dodatkowy parametr

void foo(char p[], unsigned plen);

W rzeczywistości w wielu przypadkach bardzo przydatna jest możliwość przetwarzania tablic o rozmiarze w czasie wykonywania, co również przyczynia się do popularności tej metody. Wielu programistów C po prostu nigdy nie napotyka (lub nigdy nie rozpoznaje) potrzeby przetwarzania tablicy o stałym rozmiarze, pozostając w ten sposób nieświadomymi właściwej techniki stałego rozmiaru.

Niemniej jednak, jeśli rozmiar tablicy jest stały, przekazanie jej jako wskaźnika do elementu

void foo(char p[])

jest głównym błędem technicznym, który niestety jest obecnie dość powszechny. W takich przypadkach znacznie lepszym podejściem jest technika typu wskaźnik do tablicy.

Innym powodem, który może utrudniać przyjęcie techniki przekazywania tablic o stałym rozmiarze, jest dominacja naiwnego podejścia do typowania tablic alokowanych dynamicznie. Na przykład, jeśli program wymaga ustalonych tablic typu char[10](jak w twoim przykładzie), przeciętny programista użyje malloctakich tablic, jak

char *p = malloc(10 * sizeof *p);

Tej tablicy nie można przekazać do funkcji zadeklarowanej jako

void foo(char (*p)[10]);

co dezorientuje przeciętnego programistę i sprawia, że ​​rezygnują z deklaracji parametru o stałym rozmiarze bez zastanawiania się nad tym. W rzeczywistości jednak źródłem problemu jest naiwne mallocpodejście. mallocFormat pokazano powyżej powinny być zarezerwowane dla tablic o rozmiarze run-time. Jeśli typ tablicy ma rozmiar w czasie kompilacji, lepszy sposób mallocbędzie wyglądał następująco

char (*p)[10] = malloc(sizeof *p);

Można to oczywiście łatwo przenieść na powyższe zadeklarowane foo

foo(p);

a kompilator przeprowadzi odpowiednie sprawdzenie typu. Ale znowu, jest to zbyt zagmatwane dla nieprzygotowanego programisty C, dlatego nie zobaczysz tego zbyt często w „typowym”, przeciętnym, codziennym kodzie.

Mrówka
źródło
2
Odpowiedź dostarcza bardzo zwięzłego i pouczającego opisu tego, jak odnosi sukcesy sizeof (), jak często zawodzi i jak zawsze zawodzi. Twoje obserwacje większości inżynierów C / C ++ nie rozumiejących i dlatego robienie czegoś, co ich zdaniem rozumieją, jest jedną z bardziej proroczych rzeczy, które widziałem od jakiegoś czasu, a zasłona jest niczym w porównaniu z dokładnością, którą opisuje. poważnie, sir. świetna odpowiedź.
WhozCraig
Właśnie refaktoryzowałem kod w oparciu o tę odpowiedź, brawo i dzięki za Q i A.
Perry
1
Jestem ciekawy, jak radzisz sobie constz nieruchomościami za pomocą tej techniki. const char (*p)[N]Argument nie wydaje się zgodne ze wskaźnikiem do char table[N];Natomiast prosty char*PTR pozostają zgodne z const char*argumentem.
Cyan
4
Warto zauważyć, że aby uzyskać dostęp do elementu tablicy, musisz to zrobić, (*p)[i]a nie *p[i]. Ten ostatni przeskoczy o rozmiar tablicy, co prawie na pewno nie jest tym, czego chcesz. Przynajmniej dla mnie nauczenie się tej składni spowodowało, a nie zapobiegło, błąd; Otrzymałbym poprawny kod szybciej, po prostu przekazując float *.
Andrew Wagner,
1
Tak, @mickey, zasugerowałeś constwskaźnik do tablicy zmiennych elementów. I tak, to zupełnie co innego niż wskaźnik do tablicy niezmiennych elementów.
Cyjan
11

Chciałbym dodać do odpowiedzi AndreyTa (na wypadek, gdyby ktoś natknął się na tę stronę, szukając więcej informacji na ten temat):

Kiedy zaczynam bardziej bawić się tymi deklaracjami, zdaję sobie sprawę, że istnieje z nimi poważny ułomność w C (najwyraźniej nie w C ++). Dość często mamy do czynienia z sytuacją, w której chciałbyś dać wywołującemu wskaźnik const do bufora, do którego zapisałeś. Niestety nie jest to możliwe, gdy deklarujemy taki wskaźnik w C. Innymi słowy, standard C (6.7.3 - Paragraf 8) jest sprzeczny z czymś takim:


   int array[9];

   const int (* p2)[9] = &array;  /* Not legal unless array is const as well */

Wydaje się, że to ograniczenie nie występuje w C ++, co czyni tego typu deklaracje znacznie bardziej użytecznymi. Ale w przypadku C konieczne jest cofnięcie się do zwykłej deklaracji wskaźnika, gdy chcesz mieć stały wskaźnik do bufora o stałym rozmiarze (chyba że sam bufor został zadeklarowany jako const). Więcej informacji znajdziesz w tym wątku poczty: tekst linku

Moim zdaniem jest to poważne ograniczenie i może to być jeden z głównych powodów, dla których ludzie zwykle nie deklarują takich wskaźników w C. Drugim jest fakt, że większość ludzi nawet nie wie, że można zadeklarować taki wskaźnik jako Zwrócił uwagę AndreyT.

figurassa
źródło
2
Wydaje się, że jest to problem specyficzny dla kompilatora. Udało mi się skopiować używając gcc 4.9.1, ale clang 3.4.2 był w stanie przejść z wersji non-const do const bez problemu. Przeczytałem specyfikację C11 (strona 9 w mojej wersji ... część mówiąca o kompatybilności dwóch kwalifikowanych typów) i zgadzam się, że wydaje się, że te konwersje są nielegalne. Jednak w praktyce wiemy, że zawsze możesz automatycznie przekonwertować znak * na stałą * bez ostrzeżenia. IMO, clang jest bardziej konsekwentny w zezwalaniu na to niż gcc, chociaż zgadzam się z tobą, że specyfikacja wydaje się zabraniać jakiejkolwiek z tych automatycznych konwersji.
Doug Richardson,
4

Oczywistym powodem jest to, że ten kod nie kompiluje się:

extern void foo(char (*p)[10]);
void bar() {
  char p[10];
  foo(p);
}

Domyślną promocją tablicy jest niekwalifikowany wskaźnik.

Zobacz także to pytanie , używanie foo(&p)powinno działać.

Keith Randall
źródło
3
Oczywiście foo (p) nie zadziała, foo prosi o wskaźnik do tablicy składającej się z 10 elementów, więc musisz podać adres swojej tablicy ...
Brian R. Bondy.
9
Dlaczego to „oczywisty powód”? Jest oczywiście zrozumiałe, że właściwy sposób wywołania funkcji to foo(&p).
AnT
3
Myślę, że „oczywiste” to niewłaściwe słowo. Miałem na myśli „najprostszy”. Rozróżnienie między p i & p w tym przypadku jest dość niejasne dla przeciętnego programisty C. Ktoś próbujący zrobić to, co sugerował plakat, napisze to, co napisałem, otrzyma błąd kompilacji i zrezygnuje.
Keith Randall
2

Chcę również użyć tej składni, aby umożliwić więcej sprawdzania typów.

Ale zgadzam się również, że składnia i model mentalny używania wskaźników jest prostszy i łatwiejszy do zapamiętania.

Oto kilka innych przeszkód, na które się natknąłem.

  • Dostęp do tablicy wymaga użycia (*p)[]:

    void foo(char (*p)[10])
    {
        char c = (*p)[3];
        (*p)[0] = 1;
    }

    Zamiast tego kuszące jest użycie lokalnego wskaźnika do znaku:

    void foo(char (*p)[10])
    {
        char *cp = (char *)p;
        char c = cp[3];
        cp[0] = 1;
    }

    Ale to częściowo zniweczyłoby cel używania właściwego typu.

  • Należy pamiętać o używaniu operatora address-of podczas przypisywania adresu tablicy do wskaźnika do tablicy:

    char a[10];
    char (*p)[10] = &a;

    Operator address-of pobiera adres całej tablicy &awraz z poprawnym typem do przypisania p. Bez operatora ajest automatycznie konwertowany na adres pierwszego elementu tablicy, tak samo jak in &a[0], który ma inny typ.

    Ponieważ ta automatyczna konwersja już ma miejsce, zawsze zastanawiam się, czy &jest to konieczne. Jest to zgodne ze stosowaniem &zmiennych on innych typów, ale muszę pamiętać, że tablica jest specjalna i potrzebuję jej, &aby uzyskać właściwy typ adresu, mimo że wartość adresu jest taka sama.

    Jednym z powodów mojego problemu może być to, że nauczyłem się K&R C jeszcze w latach 80., co nie pozwalało jeszcze na używanie &operatora na całych tablicach (chociaż niektóre kompilatory ignorowały to lub tolerowały składnię). Co, nawiasem mówiąc, może być kolejnym powodem, dla którego wskaźniki-tablice mają trudności z przyjęciem: działają one poprawnie tylko od czasu ANSI C, a &ograniczenie operatora mogło być kolejnym powodem, aby uznać je za zbyt niewygodne.

  • Gdy nietypedef jest używany do tworzenia typu dla wskaźnika do tablicy (we wspólnym pliku nagłówkowym), wówczas globalny wskaźnik do tablicy wymaga bardziej skomplikowanej deklaracji, aby udostępnić go między plikami:extern

    fileA:
    char (*p)[10];
    
    fileB:
    extern char (*p)[10];
Orafu
źródło
1

Cóż, po prostu C nie robi rzeczy w ten sposób. Tablica typu Tjest przekazywana jako wskaźnik do pierwszego Tw tablicy i to wszystko, co otrzymujesz.

Pozwala to na kilka fajnych i eleganckich algorytmów, takich jak zapętlanie tablicy z wyrażeniami takimi jak

*dst++ = *src++

Wadą jest to, że zarządzanie rozmiarem zależy od Ciebie. Niestety, niepowodzenie w zrobieniu tego świadomie doprowadziło również do milionów błędów w kodowaniu C i / lub możliwości złowrogiej eksploatacji.

To, co zbliża się do tego, o co prosisz w C, to przekazanie a struct (według wartości) lub wskaźnika do jednego (przez odniesienie). Dopóki ten sam typ struktury jest używany po obu stronach tej operacji, zarówno kod, który przekazuje odwołanie, jak i kod, który go używa, są zgodne co do rozmiaru obsługiwanych danych.

Twoja struktura może zawierać dowolne dane; może zawierać twoją tablicę o dobrze określonym rozmiarze.

Mimo to nic nie stoi na przeszkodzie, abyś ty lub niekompetentny lub złośliwy programista używał rzutowań, aby oszukać kompilator i potraktować twoją strukturę jako inną o innym rozmiarze. Niemal nieskrępowana umiejętność robienia tego typu rzeczy jest częścią projektu C.

Carl Smotricz
źródło
1

Tablicę znaków można zadeklarować na kilka sposobów:

char p[10];
char* p = (char*)malloc(10 * sizeof(char));

Prototyp funkcji, która przyjmuje tablicę według wartości, to:

void foo(char* p); //cannot modify p

lub przez odniesienie:

void foo(char** p); //can modify p, derefernce by *p[0] = 'f';

lub według składni tablicy:

void foo(char p[]); //same as char*
s1n
źródło
2
Nie zapominaj, że tablicę o stałym rozmiarze można również dynamicznie alokować jako char (*p)[10] = malloc(sizeof *p).
AnT
Zobacz tutaj, aby uzyskać bardziej szczegółowe omówienie między różnicami między char array [] i char * ptr tutaj. stackoverflow.com/questions/1807530/…
t0mm13b
1

Nie polecam tego rozwiązania

typedef int Vector3d[3];

ponieważ przesłania fakt, że Vector3D ma typ, o którym musisz wiedzieć. Programiści zwykle nie oczekują, że zmienne tego samego typu będą miały różne rozmiary. Rozważ:

void foo(Vector3d a) {
   Vector3D b;
}

gdzie sizeof a! = sizeof b

Per Knytt
źródło
Nie sugerował tego jako rozwiązania. Po prostu użył tego jako przykładu.
figurassa
Hm. Dlaczego sizeof(a)nie jest taki sam jak sizeof(b)?
sherrellbc
0

Może czegoś mi brakuje, ale ... ponieważ tablice są stałymi wskaźnikami, zasadniczo oznacza to, że nie ma sensu przekazywać do nich wskaźników.

Nie mógłbyś po prostu użyć void foo(char p[10], int plen);?

fortran
źródło
4
Tablice NIE są stałymi wskaźnikami. Przeczytaj kilka często zadawanych pytań dotyczących macierzy.
AnT
2
Co się tutaj liczy (tablice jednowymiarowe jako parametry), faktem jest, że rozpadają się one na stałe wskaźniki. Przeczytaj często zadawane pytania dotyczące tego, jak być mniej pedantycznym.
fortran
-2

Na moim kompilatorze (vs2008) traktuje to char (*p)[10]jako tablicę wskaźników znaków, tak jakby nie było nawiasów, nawet jeśli kompiluję jako plik C. Czy kompilator obsługuje tę „zmienną”? Jeśli tak, jest to główny powód, aby go nie używać.

Tyson Jacobs
źródło
1
-1 Źle. Działa dobrze na vs2008, vs2010, gcc. W szczególności ten przykład działa dobrze: stackoverflow.com/a/19208364/2333290
kotlomoy