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?


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.


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.


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ć.


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];

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.


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*

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


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);?


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ć.


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
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.