Dlaczego nie mogę uzyskać dostępu do wskaźnika do wskaźnika dla tablicy stosów?

35

Proszę spojrzeć na następujący kod. Próbuje przekazać tablicę jako char**funkcję:

#include <stdio.h>
#include <stdlib.h>

static void printchar(char **x)
{
    printf("Test: %c\n", (*x)[0]);
}

int main(int argc, char *argv[])
{
    char test[256];
    char *test2 = malloc(256);

    test[0] = 'B';
    test2[0] = 'A';

    printchar(&test2);            // works
    printchar((char **) &test);   // crashes because *x in printchar() has an invalid pointer

    free(test2);

    return 0;
}

Fakt, że mogę go skompilować tylko poprzez jawne rzutowanie &test2na char**już wskazane, że ten kod jest zły.

Mimo to zastanawiam się, co dokładnie jest w tym złego. Mogę przekazać wskaźnik do wskaźnika do dynamicznie przydzielonej tablicy, ale nie mogę przekazać wskaźnika do wskaźnika dla tablicy na stosie. Oczywiście mogę łatwo obejść ten problem, najpierw przypisując tablicę do zmiennej tymczasowej, na przykład:

char test[256];
char *tmp = test;
test[0] = 'B';
printchar(&tmp);

Ale czy ktoś może mi wyjaśnić, dlaczego nie char[256]można char**bezpośrednio przesyłać?

Andreas
źródło

Odpowiedzi:

29

Ponieważ testnie jest wskaźnikiem.

&testdostaje wskaźnik do tablicy typu char (*)[256], który nie jest zgodny char**(ponieważ tablica nie jest wskaźnikiem). Powoduje to niezdefiniowane zachowanie.

emlai
źródło
3
Ale dlaczego kompilator C następnie pozwolić przechodząc coś typu char (*)[256]do char**?
ComFreek
@ComFreek Podejrzewam, że przy maksymalnych ostrzeżeniach i -Werror nie pozwala na to.
PiRocks
@ComFreek: Naprawdę na to nie pozwala. Muszę zmusić kompilator do zaakceptowania go przez jawne przesłanie go do char**. Bez tej obsady nie można go skompilować.
Andreas
38

testjest tablicą, a nie wskaźnikiem i &testjest wskaźnikiem do tablicy. To nie jest wskaźnik do wskaźnika.

Być może powiedziano ci, że tablica jest wskaźnikiem, ale jest to niepoprawne. Nazwa tablicy to nazwa całego obiektu - wszystkich elementów. To nie jest wskaźnik do pierwszego elementu. W większości wyrażeń tablica jest automatycznie konwertowana na wskaźnik do pierwszego elementu. Jest to wygoda, która jest często przydatna. Istnieją jednak trzy wyjątki od tej zasady:

  • Tablica jest operandem sizeof.
  • Tablica jest operandem &.
  • Tablica jest literałem łańcuchowym używanym do inicjalizacji tablicy.

W &testtablica jest operandem &, więc automatyczne przekształcenie nie występuje. Wynikiem &testjest wskaźnik do tablicy 256 char, która ma typ char (*)[256], a nie typ char **.

Aby uzyskać wskaźnik do wskaźnika do charz test, to trzeba najpierw zrobić wskaźnik do char. Na przykład:

char *p = test; // Automatic conversion of test to &test[0] occurs.
printchar(&p);  // Passes a pointer to a pointer to char.

Innym sposobem myślenia o tym jest uświadomienie sobie, że testnazywa cały obiekt - całą tablicę 256 char. Nie nazywa wskaźnika, więc w &testnie ma wskaźnika, którego adres można by pobrać, więc nie można go utworzyć char **. Aby utworzyć char **, musisz najpierw mieć char *.

Eric Postpischil
źródło
1
Czy ta lista trzech wyjątków jest wyczerpująca?
Ruslan
8
@ Ruslan: Tak, według C 2018 6.3.2.1 3.
Eric Postpischil
Aha, aw C11 był także _Alignofoperator wymieniony oprócz sizeofi &. Zastanawiam się, dlaczego go usunęli ...
Ruslan
@ Ruslan: Zostało to usunięte, ponieważ to był błąd. _Alignofakceptuje tylko nazwę typu jako operand i nigdy nie przyjmuje tablicy ani żadnego innego obiektu jako operand. (Nie wiem dlaczego; wydaje się, że mogłoby to być składniowe i gramatyczne sizeof, ale tak nie jest.)
Eric Postpischil
6

Rodzaj test2jest char *. Więc, rodzaj &test2zostaną char **który jest zgodny z typem parametru xz printchar().
Rodzaj testjest char [256]. Więc, rodzaj &testbędzie char (*)[256]co jest nie zgodne z typem parametru xz printchar().

Pokażę ci różnicę w adresach testi test2.

#include <stdio.h>
#include <stdlib.h>

static void printchar(char **x)
{
    printf("x = %p\n", (void*)x);
    printf("*x  = %p\n", (void*)(*x));
    printf("Test: %c\n", (*x)[0]);
}

int main(int argc, char *argv[])
{
    char test[256];
    char *test2 = malloc(256);

    test[0] = 'B';
    test2[0] = 'A';

    printf ("test2 : %p\n", (void*)test2);
    printf ("&test2 : %p\n", (void*)&test2);
    printf ("&test2[0] : %p\n", (void*)&test2[0]);
    printchar(&test2);            // works

    printf ("\n");
    printf ("test : %p\n", (void*)test);
    printf ("&test : %p\n", (void*)&test);
    printf ("&test[0] : %p\n", (void*)&test[0]);

    // Commenting below statement
    //printchar((char **) &test);   // crashes because *x in printchar() has an invalid pointer

    free(test2);

    return 0;
}

Wynik:

$ ./a.out 
test2 : 0x7fe974c02970
&test2 : 0x7ffee82eb9e8
&test2[0] : 0x7fe974c02970
x = 0x7ffee82eb9e8
*x  = 0x7fe974c02970
Test: A

test : 0x7ffee82eba00
&test : 0x7ffee82eba00
&test[0] : 0x7ffee82eba00

Wskaż tutaj:

Wyjście (adres pamięci) z test2i &test2[0]jest numerycznie same, a ich rodzaj jest taki sam, który jest char *.
Ale test2i &test2są różnymi adresami, a ich typ również jest inny.
Rodzaj test2jest char *.
Rodzaj &test2jest char **.

x = &test2
*x = test2
(*x)[0] = test2[0] 

Wyjście (adres pamięci) w test, &testi &test[0]ma liczbowo takie same , ale ich rodzaj jest inaczej .
Rodzaj testjest char [256].
Rodzaj &testjest char (*) [256].
Rodzaj &test[0]jest char *.

Jak pokazuje wynik &testjest taki sam jak &test[0].

x = &test[0]
*x = test[0]       //first element of test array which is 'B'
(*x)[0] = ('B')[0]   // Not a valid statement

Dlatego otrzymujesz błąd segmentacji.

HS
źródło
3

Nie możesz uzyskać dostępu do wskaźnika do wskaźnika, ponieważ &testnie jest wskaźnikiem - to tablica.

Jeśli weźmiesz adres tablicy, rzuć tablicę i adres tablicy na (void *)i porównaj je, będą one (z wyjątkiem możliwej pedantrii wskaźnikowej) równoważne.

To, co naprawdę robisz, jest podobne do tego (ponownie, z wyjątkiem ścisłego aliasingu):

putchar(**(char **)test);

co jest oczywiście błędne.

SS Anne
źródło
3

Kod oczekuje argumentu xz printcharpkt do pamięci, które zawiera (char *).

W pierwszym wywołaniu wskazuje na wykorzystaną pamięć, test2a zatem jest rzeczywiście wartością wskazującą na a (char *), ta ostatnia wskazuje na przydzieloną pamięć.

W drugim wywołaniu nie ma jednak miejsca, w którym (char *)można by przechowywać taką wartość, dlatego nie można wskazać takiej pamięci. W obsadzie do (char **)dodałeś byłby usunięty błąd kompilacji (o konwersji (char *)do (char **)), ale to nie miałoby przechowywania pojawiają się znikąd, aby zawierać (char *)inicjowane punktu do pierwszych znaków testu. Rzutowanie wskaźnika w C nie zmienia rzeczywistej wartości wskaźnika.

Aby uzyskać to, co chcesz, musisz to zrobić wyraźnie:

char *tempptr = &temp;
printchar(&tempptr);

Zakładam, że twój przykład jest destylacją znacznie większego fragmentu kodu; na przykład być może chcesz printcharzwiększyć wartość wskazywaną (char *)przez przekazaną xwartość, aby przy następnym wywołaniu drukowany był następny znak. Jeśli tak nie jest, to dlaczego nie przekazujesz (char *)znaku do postaci, która ma zostać wydrukowana, a nawet samej postaci?

Kevin Martin
źródło
Dobra odpowiedź; Zgadzam się, że najprostszym sposobem na zachowanie prostoty jest zastanowienie się, czy istnieje obiekt C, który przechowuje adres tablicy, tj. Obiekt wskaźnika, którego adres można uzyskać, aby uzyskać char **. Zmienne / obiekty tablicowe to po prostu tablica, której adres jest niejawny i nigdzie nie przechowywany. Brak dodatkowego poziomu pośredniego dostępu do nich, w przeciwieństwie do zmiennej wskaźnikowej, która wskazuje na inną pamięć.
Peter Cordes
0

Podobno przyjęcie adresu testjest tym samym, co przyjęcie adresu test[0]:

#include <stdio.h>
#include <stdlib.h>

static void printchar(char **x)
{
    printf("[printchar] Address of pointer to pointer: %p\n", (void *)x);
    printf("[printchar] Address of pointer: %p\n", (void *)*x);
    printf("Test: %c\n", **x);
}

int main(int argc, char *argv[])
{
    char test[256];
    char *test2 = malloc(256);

    printf("[main] Address of test: %p\n", (void *)test);
    printf("[main] Address of the address of test: %p\n", (void *)&test);
    printf("[main] Address of test2: %p\n", (void *)test2);
    printf("[main] Address of the address of test2: %p\n", (void *)&test2);

    test[0] = 'B';
    test2[0] = 'A';

    printchar(&test2);            // works
    printchar(&test);   // crashes because *x in printchar() has an invalid pointer

    free(test2);

    return 0;
}

Skompiluj to i uruchom:

forcebru$ clang test.c -Wall && ./a.out
test.c:25:15: warning: incompatible pointer types passing 'char (*)[256]' to
      parameter of type 'char **' [-Wincompatible-pointer-types]
    printchar(&test);   // crashes because *x in printchar() has an inva...
              ^~~~~
test.c:4:30: note: passing argument to parameter 'x' here
static void printchar(char **x)
                             ^
1 warning generated.
[main] Address of test: 0x7ffeeed039c0
[main] Address of the address of test: 0x7ffeeed039c0 [THIS IS A PROBLEM]
[main] Address of test2: 0x7fbe20c02aa0
[main] Address of the address of test2: 0x7ffeeed039a8
[printchar] Address of pointer to pointer: 0x7ffeeed039a8
[printchar] Address of pointer: 0x7fbe20c02aa0
Test: A
[printchar] Address of pointer to pointer: 0x7ffeeed039c0
[printchar] Address of pointer: 0x42 [THIS IS THE ASCII CODE OF 'B' in test[0] = 'B';]
Segmentation fault: 11

Zatem ostateczną przyczyną błędu segmentacji jest to, że ten program spróbuje wyrejestrować adres bezwzględny 0x42(znany również jako 'B'), którego program nie ma uprawnień do odczytu.

Chociaż w przypadku innego kompilatora / maszyny adresy będą się różnić: Wypróbuj online! , ale z jakiegoś powodu nadal to otrzymasz:

[main] Address of test: 0x7ffd4891b080
[main] Address of the address of test: 0x7ffd4891b080  [SAME ADDRESS!]

Ale adres, który powoduje błąd segmentacji, może być zupełnie inny:

[printchar] Address of pointer to pointer: 0x7ffd4891b080
[printchar] Address of pointer: 0x9c000000942  [WAS 0x42 IN MY CASE]
ForceBru
źródło
1
Podanie adresu testnie jest tym samym, co pobranie adresu test[0]. Pierwszy ma typ char (*)[256], a drugi typ char *. Nie są kompatybilne, a standard C pozwala im mieć różne reprezentacje.
Eric Postpischil
Podczas formatowania wskaźnika %pnależy go przekonwertować na void *(ponownie ze względu na kompatybilność i reprezentację).
Eric Postpischil
1
printchar(&test);może ulec awarii, ale zachowanie nie jest zdefiniowane w standardzie C, a ludzie mogą obserwować inne zachowania w innych okolicznościach.
Eric Postpischil
Odp. „Ostateczną przyczyną błędu segmentacji jest to, że ten program spróbuje wyrejestrować adres bezwzględny 0x42 (znany również jako„ B ”), który prawdopodobnie jest zajęty przez system operacyjny.”: Jeśli wystąpi błąd segmentu, próba odczytu lokalizacja, oznacza to, że nic nie jest tam zmapowane, nie że jest zajęty przez system operacyjny. (Z wyjątkiem tego, że może tam być coś zamapowanego jako, powiedzmy, wykonanie tylko bez uprawnień do odczytu, ale jest to mało prawdopodobne.)
Eric Postpischil
1
&test == &test[0]narusza ograniczenia w C 2018 6.5.9 2, ponieważ typy nie są kompatybilne. Standard C wymaga implementacji w celu zdiagnozowania tego naruszenia, a wynikowe zachowanie nie jest zdefiniowane przez standard C. Oznacza to, że Twój kompilator może wygenerować kod oceniający je jako równe, ale inny kompilator może nie.
Eric Postpischil
-4

Reprezentacja char [256]zależy od implementacji. Nie może być taki sam jak char *.

Rzutowanie &testtekstu char (*)[256]na char **nieokreślone zachowanie.

Niektóre kompilatory mogą robić to, czego oczekujesz, a na innych nie.

EDYTOWAĆ:

Po przetestowaniu za pomocą gcc 9.2.1 wydaje się, że printchar((char**)&test)przekazuje test wartość jako przekazaną wartość char**. To tak, jakby instrukcja była printchar((char**)test). W printcharfunkcji xjest wskaźnikiem do pierwszego znaku testu tablicy, a nie podwójnym wskaźnikiem do pierwszego znaku. Podwójne xusunięcie odniesienia powoduje błąd segmentacji, ponieważ 8 pierwszych bajtów tablicy nie odpowiada poprawnemu adresowi.

Otrzymuję dokładnie to samo zachowanie i wynik podczas kompilacji programu z klangiem 9.0.0-2.

Może to być traktowane jako błąd kompilatora lub wynik niezdefiniowanego zachowania, którego wynik może być specyficzny dla kompilatora.

Innym nieoczekiwanym zachowaniem jest to, że kod

void printchar2(char (*x)[256]) {
    printf("px: %p\n", *x);
    printf("x: %p\n", x);
    printf("c: %c\n", **x);
}

Dane wyjściowe to

px: 0x7ffd92627370
x: 0x7ffd92627370
c: A

Dziwne zachowanie jest takie xi *xma tę samą wartość.

To jest kompilator. Wątpię, czy to określa język.

chmike
źródło
1
Czy masz na myśli reprezentację char (*)[256] zależy implementacji? Reprezentacja char [256]nie jest istotna w tym pytaniu - to tylko garść bitów. Ale nawet jeśli masz na myśli, że reprezentacja wskaźnika do tablicy różni się od reprezentacji wskaźnika do wskaźnika, to również nie ma sensu. Nawet jeśli mają te same reprezentacje, kod OP nie działałby, ponieważ wskaźnik do wskaźnika może być dereferencyjnie dwukrotnie, jak to jest zrobione printchar, ale wskaźnik do tablicy nie może, niezależnie od reprezentacji.
Eric Postpischil
@EricPostpischil obsada od char (*)[256]dochar ** jest akceptowane przez kompilator, ale nie daje oczekiwanego wyniku, ponieważ a char [256]nie jest tym samym co a char *. Zakładałem, że kodowanie jest inne, w przeciwnym razie dałoby oczekiwany wynik.
chmike
Nie wiem, co rozumiesz przez „oczekiwany wynik”. Jedyną specyfikacją w standardzie C tego, jaki powinien być wynik, jest to, że jeśli wyrównanie jest nieodpowiedniechar ** , zachowanie jest niezdefiniowane, a w przeciwnym razie, jeśli wynik zostanie przekonwertowany z powrotem char (*)[256], będzie on równy pierwotnemu wskaźnikowi. Przez „oczekiwany wynik” możesz rozumieć, że jeśli (char **) &testjest dalej konwertowany na a char *, to jest równy &test[0]. Nie jest to mało prawdopodobne, aby implementacje korzystały z płaskiej przestrzeni adresowej, ale nie jest to wyłącznie kwestia reprezentacji.
Eric Postpischil
2
Również „rzutowanie i test typu char (*) [256] na char ** daje nieokreślone zachowanie.” nie jest poprawne. C 2018 6.3.2.3 7 umożliwia konwersję wskaźnika do typu obiektu na dowolny inny wskaźnik na typ obiektu. Jeśli wskaźnik nie jest poprawnie wyrównany dla typu odwołania (typem odniesienia char **jest char *), zachowanie jest niezdefiniowane. W przeciwnym razie konwersja jest zdefiniowana, chociaż wartość jest tylko częściowo zdefiniowana, zgodnie z moim komentarzem powyżej.
Eric Postpischil
char (*x)[256]to nie to samo co char **x. Powód xi*x tą samą wartością wskaźnika jest to, że xjest to po prostu wskaźnik do tablicy. Twoja *x jest tablicą , a użycie jej w kontekście wskaźnika powoduje powrót do adresu tablicy . Nie ma błędu kompilatora (ani tego, co (char **)&testrobi), wystarczy trochę gimnastyki umysłowej, aby dowiedzieć się, co się dzieje z typami. (cdecl wyjaśnia to jako „deklaruj x jako wskaźnik do tablicy 256 znaków”). Nawet korzystanie char*z dostępu do reprezentacji obiektowej char**nie jest UB; może alias wszystko.
Peter Cordes