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 &test2
na 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ć?
char (*)[256]
dochar**
?char**
. Bez tej obsady nie można go skompilować.test
jest tablicą, a nie wskaźnikiem i&test
jest 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:
sizeof
.&
.W
&test
tablica jest operandem&
, więc automatyczne przekształcenie nie występuje. Wynikiem&test
jest wskaźnik do tablicy 256char
, która ma typchar (*)[256]
, a nie typchar **
.Aby uzyskać wskaźnik do wskaźnika do
char
ztest
, to trzeba najpierw zrobić wskaźnik dochar
. Na przykład:Innym sposobem myślenia o tym jest uświadomienie sobie, że
test
nazywa cały obiekt - całą tablicę 256char
. Nie nazywa wskaźnika, więc w&test
nie 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 *
.źródło
_Alignof
operator wymieniony opróczsizeof
i&
. Zastanawiam się, dlaczego go usunęli ..._Alignof
akceptuje 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 gramatycznesizeof
, ale tak nie jest.)Rodzaj
test2
jestchar *
. Więc, rodzaj&test2
zostanąchar **
który jest zgodny z typem parametrux
zprintchar()
.Rodzaj
test
jestchar [256]
. Więc, rodzaj&test
będziechar (*)[256]
co jest nie zgodne z typem parametrux
zprintchar()
.Pokażę ci różnicę w adresach
test
itest2
.Wynik:
Wskaż tutaj:
Wyjście (adres pamięci) z
test2
i&test2[0]
jest numerycznie same, a ich rodzaj jest taki sam, który jestchar *
.Ale
test2
i&test2
są różnymi adresami, a ich typ również jest inny.Rodzaj
test2
jestchar *
.Rodzaj
&test2
jestchar **
.Wyjście (adres pamięci) w
test
,&test
i&test[0]
ma liczbowo takie same , ale ich rodzaj jest inaczej .Rodzaj
test
jestchar [256]
.Rodzaj
&test
jestchar (*) [256]
.Rodzaj
&test[0]
jestchar *
.Jak pokazuje wynik
&test
jest taki sam jak&test[0]
.Dlatego otrzymujesz błąd segmentacji.
źródło
Nie możesz uzyskać dostępu do wskaźnika do wskaźnika, ponieważ
&test
nie 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):
co jest oczywiście błędne.
źródło
Kod oczekuje argumentu
x
zprintchar
pkt do pamięci, które zawiera(char *)
.W pierwszym wywołaniu wskazuje na wykorzystaną pamięć,
test2
a 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:
Zakładam, że twój przykład jest destylacją znacznie większego fragmentu kodu; na przykład być może chcesz
printchar
zwiększyć wartość wskazywaną(char *)
przez przekazanąx
wartość, 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?źródło
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ęć.Podobno przyjęcie adresu
test
jest tym samym, co przyjęcie adresutest[0]
:Skompiluj to i uruchom:
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:
Ale adres, który powoduje błąd segmentacji, może być zupełnie inny:
źródło
test
nie jest tym samym, co pobranie adresutest[0]
. Pierwszy ma typchar (*)[256]
, a drugi typchar *
. Nie są kompatybilne, a standard C pozwala im mieć różne reprezentacje.%p
należy go przekonwertować navoid *
(ponownie ze względu na kompatybilność i reprezentację).printchar(&test);
może ulec awarii, ale zachowanie nie jest zdefiniowane w standardzie C, a ludzie mogą obserwować inne zachowania w innych okolicznościach.&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.Reprezentacja
char [256]
zależy od implementacji. Nie może być taki sam jakchar *
.Rzutowanie
&test
tekstuchar (*)[256]
nachar **
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)
przekazujetest
wartość jako przekazaną wartośćchar**
. To tak, jakby instrukcja byłaprintchar((char**)test)
. Wprintchar
funkcjix
jest wskaźnikiem do pierwszego znaku testu tablicy, a nie podwójnym wskaźnikiem do pierwszego znaku. Podwójnex
usunię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
Dane wyjściowe to
Dziwne zachowanie jest takie
x
i*x
ma tę samą wartość.To jest kompilator. Wątpię, czy to określa język.
źródło
char (*)[256]
zależy implementacji? Reprezentacjachar [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 zrobioneprintchar
, ale wskaźnik do tablicy nie może, niezależnie od reprezentacji.char (*)[256]
dochar **
jest akceptowane przez kompilator, ale nie daje oczekiwanego wyniku, ponieważ achar [256]
nie jest tym samym co achar *
. Zakładałem, że kodowanie jest inne, w przeciwnym razie dałoby oczekiwany wynik.char **
, zachowanie jest niezdefiniowane, a w przeciwnym razie, jeśli wynik zostanie przekonwertowany z powrotemchar (*)[256]
, będzie on równy pierwotnemu wskaźnikowi. Przez „oczekiwany wynik” możesz rozumieć, że jeśli(char **) &test
jest dalej konwertowany na achar *
, 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.char **
jestchar *
), zachowanie jest niezdefiniowane. W przeciwnym razie konwersja jest zdefiniowana, chociaż wartość jest tylko częściowo zdefiniowana, zgodnie z moim komentarzem powyżej.char (*x)[256]
to nie to samo cochar **x
. Powódx
i*x
tą samą wartością wskaźnika jest to, żex
jest 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 **)&test
robi), 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 korzystaniechar*
z dostępu do reprezentacji obiektowejchar**
nie jest UB; może alias wszystko.