Co to jest tablica do zaniku wskaźnika?

384

Co to jest tablica do zaniku wskaźnika? Czy istnieje jakikolwiek związek ze wskaźnikami tablicowymi?

Vamsi
źródło
73
mało znany: operator jednoargumentowy plus może być użyty jako „operator rozpadu”: Biorąc pod uwagę int a[10]; int b(void);, +ajest on wskaźnikiem int i +bjest wskaźnikiem funkcji. Przydatne, jeśli chcesz przekazać go do szablonu akceptującego referencję.
Johannes Schaub - litb
3
@ litb - parens zrobiłby to samo (np. (a) powinno być wyrażeniem, którego wynikiem jest wskaźnik), prawda ?.
Michael Burr
21
std::decayz C ++ 14 byłby mniej niejasnym sposobem rozkładania tablicy na jednoargumentowy +.
legends2k
21
@ JohannesSchaub-litb, ponieważ pytanie to jest oznaczone zarówno C, jak i C ++, chciałbym wyjaśnić, że chociaż +ai +bsą legalne w C ++, to jest nielegalne w C (C11 6.5.3.3/1 „Operand unarny +lub -operator powinien mieć typ arytmetyczny ”)
MM
5
@lege Right. Ale przypuszczam, że nie jest to tak mało znane jak sztuczka z unary +. Powodem, dla którego wspomniałem, było nie tylko to, że rozkłada się, ale także dlatego, że jest to fajna zabawa;)
Johannes Schaub - litb

Odpowiedzi:

283

Mówi się, że tablice „rozpadają się” na wskaźniki. Tablicy C ++ zadeklarowanej jako int numbers [5]nie można ponownie wskazać, tzn. Nie można powiedzieć numbers = 0x5a5aff23. Co ważniejsze, termin zanik oznacza utratę rodzaju i wymiaru; numbersrozpadają się int*, tracąc informacje o wymiarach (liczba 5), ​​a typ już nie int [5]istnieje. Poszukaj tutaj przypadków, w których rozpad się nie zdarza .

Jeśli przekazujesz tablicę według wartości, tak naprawdę to kopiujesz wskaźnik - wskaźnik do pierwszego elementu tablicy jest kopiowany do parametru (którego typ powinien być również wskaźnikiem typu elementu tablicy). Działa to z powodu rozkładającej się tablicy; po rozpadzie sizeofnie daje już pełnego rozmiaru tablicy, ponieważ zasadniczo staje się wskaźnikiem. Dlatego preferowane jest (między innymi) przekazywanie przez referencję lub wskaźnik.

Trzy sposoby przekazywania w tablicy 1 :

void by_value(const T* array)   // const T array[] means the same
void by_pointer(const T (*array)[U])
void by_reference(const T (&array)[U])

Dwa ostatnie podadzą odpowiednie sizeofinformacje, a pierwszy nie, ponieważ argument tablicy zanikł, aby zostać przypisanym do parametru.

1 Stała U powinna być znana w czasie kompilacji.

phoebus
źródło
8
Jak przebiega pierwsze przejście według wartości?
rlbond
10
by_value przekazuje wskaźnik do pierwszego elementu tablicy; w kontekście parametrów funkcji T a[]jest identyczny z T *a. by_pointer przekazuje to samo, tyle że wartość wskaźnika jest teraz kwalifikowana const. Jeśli chcesz przekazać wskaźnik do tablicy (w przeciwieństwie do wskaźnika do pierwszego elementu tablicy), składnia jest następująca T (*array)[U].
John Bode
4
„z wyraźnym wskaźnikiem do tej tablicy” - jest to niepoprawne. Jeśli ajest tablicą char, to ajest typu char[N]i rozpadnie się na char*; ale &ajest rodzaju char(*)[N]i nie ulegnie rozkładowi.
Pavel Minaev,
5
@FredOverflow: Więc jeśli Uzmiany nie musisz pamiętać, aby zmienić go w dwóch miejscach lub zaryzykować ciche błędy ... Autonomia!
Wyścigi lekkości na orbicie
4
„Jeśli przekazujesz tablicę według wartości, tak naprawdę to kopiujesz wskaźnik” To nie ma sensu, ponieważ tablice nie mogą być przekazywane przez wartość, kropka.
juanchopanza
103

Tablice są w zasadzie takie same jak wskaźniki w C / C ++, ale nie do końca. Po przekonwertowaniu tablicy:

const int a[] = { 2, 3, 5, 7, 11 };

do wskaźnika (który działa bez rzutowania, a zatem może się zdarzyć nieoczekiwanie w niektórych przypadkach):

const int* p = a;

tracisz zdolność sizeofoperatora do zliczania elementów w tablicy:

assert( sizeof(p) != sizeof(a) );  // sizes are not equal

Tę utraconą zdolność określa się jako „rozkład”.

Aby uzyskać więcej informacji, zapoznaj się z tym artykułem o rozkładzie macierzy .

system PAUZA
źródło
51
Tablice nie są zasadniczo takie same jak wskaźniki; to zupełnie inne zwierzęta. W większości kontekstów tablicę można traktować tak, jakby była wskaźnikiem, a wskaźnik można traktować tak, jakby była tablicą, ale jest tak blisko, jak to możliwe.
John Bode
20
@John, proszę wybaczyć mój nieprecyzyjny język. Próbowałem dotrzeć do odpowiedzi, nie zagłębiając się w długą historię, a „w zasadzie… ale nie całkiem” jest tak dobrym wytłumaczeniem, jak kiedykolwiek dostałem na studiach. Jestem pewien, że każdy zainteresowany może uzyskać dokładniejszy obraz z twojego pozytywnego komentarza.
system PAUZA
„działa bez rzutowania” oznacza to samo, co „dzieje się niejawnie”, gdy mówimy o konwersjach typu
MM
47

Oto, co mówi standard (C99 6.3.2.1/3 - Inne operandy - Wartości, tablice i desygnatory funkcji):

Z wyjątkiem sytuacji, gdy jest to operand operatora sizeof lub unary & operator lub dosłowny ciąg znaków używany do inicjalizacji tablicy, wyrażenie o typie „tablica typu” jest konwertowane na wyrażenie o wskaźniku typu „ wpisz '', który wskazuje początkowy element obiektu tablicy i nie jest wartością.

Oznacza to, że prawie za każdym razem, gdy nazwa tablicy jest używana w wyrażeniu, jest ona automatycznie konwertowana na wskaźnik do pierwszego elementu w tablicy.

Zauważ, że nazwy funkcji działają w podobny sposób, ale wskaźniki funkcji są używane znacznie rzadziej i w bardziej wyspecjalizowany sposób, że nie powoduje to prawie tyle zamieszania, co automatyczna konwersja nazw tablic na wskaźniki.

Standard C ++ (4.2 konwersja tablic na wskaźniki) rozluźnia wymaganie konwersji do (wyróżnienie moje):

Wartość lub wartość typu „tablica NT” lub „tablica nieznanych granic T” można przekonwertować na wartość typu „wskaźnik na T.”

Tak więc konwersja nie musi się odbywać tak, jak zawsze w C (pozwala to na przeciążenie funkcji lub dopasowanie szablonów do typu tablicy).

Dlatego też w C należy unikać używania parametrów tablic w prototypach / definicjach funkcji (moim zdaniem - nie jestem pewien, czy istnieje jakakolwiek ogólna zgoda). Powodują zamieszanie i są fikcją - użyj parametrów wskaźnika, a zamieszanie może nie zniknąć całkowicie, ale przynajmniej deklaracja parametru nie kłamie.

Michael Burr
źródło
2
Jaki jest przykładowy wiersz kodu, w którym „wyrażenie mające typ„ tablica typu ”to„ literał łańcuchowy używany do inicjalizacji tablicy ”?
Garrett,
4
@Garrett char x[] = "Hello";. Szereg 6 elementów "Hello"nie ulega rozpadowi; zamiast tego xotrzymuje rozmiar, 6a jego elementy są inicjowane z elementów "Hello".
MM
30

„Rozpad” odnosi się do niejawnej konwersji wyrażenia z typu tablicowego na typ wskaźnika. W większości kontekstów, gdy kompilator widzi wyrażenie tablicowe, konwertuje typ wyrażenia z „tablicy N-elementowej T” na „wskaźnik do T” i ustawia wartość wyrażenia na adres pierwszego elementu tablicy . Wyjątki od tej reguły są przypadki, gdy tablica jest operand się z tych sizeoflub &operatorów, czy tablica jest ciągiem dosłowne używany jako inicjator w deklaracji.

Załóżmy następujący kod:

char a[80];
strcpy(a, "This is a test");

Wyrażenie ajest typu „80-elementowa tablica char”, a wyrażenie „This is a test” jest typu „16-elementowa tablica char” (w C; w C ++ literały łańcuchowe to tablice const char). Jednak w wywołaniu do strcpy()żadne wyrażenie nie jest operandem sizeoflub &, więc ich typy są domyślnie konwertowane na „wskaźnik na char”, a ich wartości są ustawiane na adres pierwszego elementu w każdym z nich. Co strcpy()otrzymuje nie są tablice, ale wskaźniki, co widać w jego prototypu:

char *strcpy(char *dest, const char *src);

To nie to samo, co wskaźnik tablicowy. Na przykład:

char a[80];
char *ptr_to_first_element = a;
char (*ptr_to_array)[80] = &a;

Zarówno ptr_to_first_elementi ptr_to_arraymają taką samą wartość ; adres bazowy Są to jednak różne typy i są traktowane inaczej, jak pokazano poniżej:

a[i] == ptr_to_first_element[i] == (*ptr_to_array)[i] != *ptr_to_array[i] != ptr_to_array[i]

Pamiętaj, że wyrażenie a[i]jest interpretowane jako *(a+i)(który działa tylko wtedy, gdy typ tablica jest przekształcany do typu wskaźnika), tak jak a[i]i ptr_to_first_element[i]działają tak samo. Wyrażenie (*ptr_to_array)[i]jest interpretowane jako *(*a+i). Wyrażenia *ptr_to_array[i]i ptr_to_array[i]mogą prowadzić do ostrzeżeń lub błędów kompilatora w zależności od kontekstu; na pewno zrobią coś złego, jeśli oczekujesz od nich oceny a[i].

sizeof a == sizeof *ptr_to_array == 80

Ponownie, gdy tablica jest operandem sizeof, nie jest konwertowana na typ wskaźnika.

sizeof *ptr_to_first_element == sizeof (char) == 1
sizeof ptr_to_first_element == sizeof (char *) == whatever the pointer size
                                                  is on your platform

ptr_to_first_element jest prostym wskaźnikiem do char.

John Bode
źródło
1
Nie jest ? (długość 14 + 1 dla \ 0)"This is a test" is of type "16-element array of char""15-element array of char"
chux - Przywróć Monikę
16

Tablice w C nie mają wartości.

Wszędzie tam, gdzie oczekiwana jest wartość obiektu, ale obiekt jest tablicą, używany jest adres jego pierwszego elementu z typem pointer to (type of array elements).

W funkcji wszystkie parametry są przekazywane przez wartość (tablice nie są wyjątkiem). Gdy przekazujesz tablicę w funkcji, „rozkłada się ona na wskaźnik” (sic); kiedy porównasz tablicę do czegoś innego, znowu „rozpada się ona na wskaźnik” (sic); ...

void foo(int arr[]);

Funkcja foo oczekuje wartości tablicy. Ale w C tablice nie mają żadnej wartości! Więc foodostaje zamiast adres pierwszego elementu tablicy.

int arr[5];
int *ip = &(arr[1]);
if (arr == ip) { /* something; */ }

W powyższym porównaniu arrnie ma wartości, więc staje się wskaźnikiem. Staje się wskaźnikiem do int. Ten wskaźnik można porównać ze zmienną ip.

W składni indeksowania tablic, do której jesteś przyzwyczajony, znowu arr jest „rozkładany na wskaźnik”

arr[42];
/* same as *(arr + 42); */
/* same as *(&(arr[0]) + 42); */

Tablica nie rozpada się na wskaźnik tylko wtedy, gdy jest operandem operatora sizeof, operatora & (adres operatora) lub literału łańcuchowego używanego do inicjalizacji tablicy znaków.

pmg
źródło
5
„Tablice nie mają wartości” - co to ma znaczyć? Oczywiście tablice mają wartość ... są obiektami, możesz mieć wskaźniki, a w C ++ odniesienia do nich itp.
Pavel Minaev
2
Uważam, że „Wartość” jest zdefiniowana w C jako interpretacja bitów obiektu według typu. Trudno mi znaleźć przydatne znaczenie tego typu tablic. Zamiast tego możesz powiedzieć, że konwertujesz na wskaźnik, ale to nie interpretuje zawartości tablicy, tylko pobiera jej lokalizację. Otrzymujesz wartość wskaźnika (i jest to adres), a nie wartość tablicy (byłaby to „sekwencja wartości zawartych elementów”, zgodnie z definicją „łańcucha”). To powiedziawszy, myślę, że uczciwie jest mówić „wartość tablicy”, kiedy oznacza się wskaźnik, który się otrzymuje.
Johannes Schaub - litb
w każdym razie wydaje mi się, że istnieje niewielka dwuznaczność: wartość obiektu i wartość wyrażenia (jak w „wartości”). Jeśli zostanie zinterpretowany w ten drugi sposób, to wyrażenie tablicowe z pewnością ma wartość: jest to wynik wynikające z rozkładu go na wartość i jest wyrażeniem wskaźnikowym. Ale jeśli zostanie zinterpretowany w ten sam sposób, to oczywiście nie ma użytecznego znaczenia dla obiektu tablicowego.
Johannes Schaub - litb
1
+1 dla frazy z niewielką poprawką; dla tablic nie jest to nawet triplet, tylko dwuwiersz [lokalizacja, typ]. Czy miałeś na myśli coś innego o trzeciej lokalizacji w przypadku tablicy? Nie mogę wymyślić żadnego.
legends2k
1
@ legends2k: Myślę, że użyłem trzeciej lokalizacji w tablicach, aby uniknąć uczynienia ich szczególnym przypadkiem posiadania tylko dwuwierszów. Może [lokalizacja, typ, nieważność ] byłoby lepsze.
pmg
8

To kiedy gnije tablica i jest wskazywana ;-)

Właściwie to po prostu to, że jeśli chcesz gdzieś przekazać tablicę, ale zamiast niej jest przekazywany wskaźnik (bo kto, do diabła, przekazałby ci całą tablicę), ludzie mówią, że słaba tablica uległa rozkładowi na wskaźnik.

Michael Krelin - haker
źródło
Ładnie powiedziane. Co byłoby ładną tablicą, która nie rozpada się na wskaźnik lub taki, który nie ulega rozpadowi? Czy możesz podać przykład w C? Dzięki.
Unheilig,
@Uheheilig, oczywiście, można spakować próżniowo tablicę do struktury i przekazać strukturę.
Michael Krelin - haker
Nie jestem pewien, co rozumiesz przez „pracę”. Nie można uzyskać dostępu poza tablicą, ale działa zgodnie z oczekiwaniami, jeśli spodziewasz się, co naprawdę się stanie. To zachowanie (choć ponownie oficjalnie niezdefiniowane) zostaje zachowane.
Michael Krelin - haker
Rozpad występuje również w wielu sytuacjach, które nigdzie nie przekazują tablicy (jak opisano w innych odpowiedziach). Na przykład a + 1.
MM
3

Rozkład tablicy oznacza, że ​​kiedy tablica jest przekazywana jako parametr do funkcji, jest traktowana identycznie jak wskaźnik („rozpada się”).

void do_something(int *array) {
  // We don't know how big array is here, because it's decayed to a pointer.
  printf("%i\n", sizeof(array));  // always prints 4 on a 32-bit machine
}

int main (int argc, char **argv) {
    int a[10];
    int b[20];
    int *c;
    printf("%zu\n", sizeof(a)); //prints 40 on a 32-bit machine
    printf("%zu\n", sizeof(b)); //prints 80 on a 32-bit machine
    printf("%zu\n", sizeof(c)); //prints 4 on a 32-bit machine
    do_something(a);
    do_something(b);
    do_something(c);
}

Istnieją dwa powikłania lub wyjątki od powyższego.

Po pierwsze, w przypadku tablic wielowymiarowych w C i C ++ tracony jest tylko pierwszy wymiar. Wynika to z faktu, że tablice są ułożone w pamięci w sposób ciągły, więc kompilator musi znać wszystkie oprócz pierwszego wymiaru, aby móc obliczyć przesunięcia w tym bloku pamięci.

void do_something(int array[][10])
{
    // We don't know how big the first dimension is.
}

int main(int argc, char *argv[]) {
    int a[5][10];
    int b[20][10];
    do_something(a);
    do_something(b);
    return 0;
}

Po drugie, w C ++ możesz użyć szablonów, aby wydedukować rozmiar tablic. Microsoft używa tego dla wersji C ++ funkcji Secure CRT, takich jak strcpy_s , i możesz użyć podobnej sztuczki, aby niezawodnie uzyskać liczbę elementów w tablicy .

Josh Kelley
źródło
1
rozpad zdarza się w wielu innych sytuacjach, nie tylko przekazując tablicę do funkcji.
MM
0

tl; dr: Kiedy użyjesz zdefiniowanej tablicy, faktycznie będziesz używać wskaźnika do jej pierwszego elementu.

A zatem:

  • Kiedy piszesz arr[idx], tak naprawdę po prostu mówisz *(arr + idx).
  • funkcje nigdy tak naprawdę nie przyjmują tablic jako parametrów, a jedynie wskaźniki, nawet jeśli podasz parametr tablicy.

Sortuj wyjątki od tej reguły:

  • Możesz przekazać tablice o stałej długości do funkcji w ciągu struct.
  • sizeof() podaje rozmiar zajmowany przez tablicę, a nie rozmiar wskaźnika.
einpoklum
źródło
0

Mogę śmiało myśleć, że istnieją cztery (4) sposoby przekazywania tablicy jako argumentu funkcji. Również tutaj jest krótki, ale działający kod dla twojego przeglądu.

#include <iostream>
#include <string>
#include <vector>
#include <cassert>

using namespace std;

// test data
// notice native array init with no copy aka "="
// not possible in C
 const char* specimen[]{ __TIME__, __DATE__, __TIMESTAMP__ };

// ONE
// simple, dangerous and useless
template<typename T>
void as_pointer(const T* array) { 
    // a pointer
    assert(array != nullptr); 
} ;

// TWO
// for above const T array[] means the same
// but and also , minimum array size indication might be given too
// this also does not stop the array decay into T *
// thus size information is lost
template<typename T>
void by_value_no_size(const T array[0xFF]) { 
    // decayed to a pointer
    assert( array != nullptr ); 
}

// THREE
// size information is preserved
// but pointer is asked for
template<typename T, size_t N>
void pointer_to_array(const T (*array)[N])
{
   // dealing with native pointer 
    assert( array != nullptr ); 
}

// FOUR
// no C equivalent
// array by reference
// size is preserved
template<typename T, size_t N>
void reference_to_array(const T (&array)[N])
{
    // array is not a pointer here
    // it is (almost) a container
    // most of the std:: lib algorithms 
    // do work on array reference, for example
    // range for requires std::begin() and std::end()
    // on the type passed as range to iterate over
    for (auto && elem : array )
    {
        cout << endl << elem ;
    }
}

int main()
{
     // ONE
     as_pointer(specimen);
     // TWO
     by_value_no_size(specimen);
     // THREE
     pointer_to_array(&specimen);
     // FOUR
     reference_to_array( specimen ) ;
}

Mógłbym również sądzić, że pokazuje to wyższość C ++ w porównaniu z C. Przynajmniej w odniesieniu (w zamierzeniu pun) przekazywania tablicy przez referencję.

Oczywiście istnieją wyjątkowo surowe projekty bez alokacji sterty, bez wyjątków i bez std :: lib. Można powiedzieć, że natywna obsługa macierzy w C ++ to kluczowa funkcja języka.

Gladiator szefa kuchni
źródło