Jaka jest różnica między tablicą znaków a wskaźnikiem znaków w C?

216

Próbuję zrozumieć wskaźniki w C, ale obecnie jestem mylony z następującymi:

  • char *p = "hello"

    Jest to wskaźnik char wskazujący na tablicę znaków, zaczynający się od h .

  • char p[] = "hello"

    To tablica, która przechowuje cześć .

Jaka jest różnica, gdy przekazuję obie te zmienne do tej funkcji?

void printSomething(char *p)
{
    printf("p: %s",p);
}
diesel
źródło
5
To nie byłoby poprawne: char p[3] = "hello";Ciąg inicjalizujący jest zbyt długi dla rozmiaru deklarowanej tablicy. Literówka?
Cody Gray
16
A char p[]="hello";może wystarczy!
deepdive
1
możliwy duplikat Jaka jest różnica między char s [] a char * s w C? To prawda, że ​​pyta również konkretnie o parametr funkcji, ale to nie jest charspecyficzne.
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
1
musisz zrozumieć, że są one zasadniczo różne. jedyną wspólną cechą jest to, że podstawą arry p [] jest stały wskaźnik, który umożliwił dostęp do tablicy p [] za pomocą wskaźnika. Sam p [] przechowuje pamięć dla ciągu, podczas gdy * p wskazuje tylko na adres pierwszego elementu tylko JEDNEJ CHAR (tzn. wskazuje na bazę już przydzielonego ciągu). Aby lepiej to zilustrować, rozważ poniżej: char * cPtr = {'h', 'e', ​​'l', 'l', 'o', '\ 0'}; ==> jest to błąd, ponieważ cPtr jest wskaźnikiem tylko do znaku char cBuff [] = {'h', 'e', ​​'l', 'l', 'o', '\ 0'}; ==> To jest ok, bcos cBuff sam w sobie jest tablicą
znaków

Odpowiedzi:

223

char*i char[] są różnymi typami , ale nie we wszystkich przypadkach jest to od razu widoczne. Wynika to z tego, że tablice rozpadają się na wskaźniki , co oznacza, że ​​jeśli wyrażenie typu char[]jest podane tam, gdzie jedno z typówchar* oczekuje się , kompilator automatycznie konwertuje tablicę na wskaźnik na swój pierwszy element.

Przykładowa funkcja printSomethingoczekuje wskaźnika, więc jeśli spróbujesz przekazać do niej tablicę w następujący sposób:

char s[10] = "hello";
printSomething(s);

Kompilator udaje, że to napisałeś:

char s[10] = "hello";
printSomething(&s[0]);
Jon
źródło
Czy coś się zmieniło od 2012 roku do teraz. W przypadku tablicy znaków „s” wypisuje całą tablicę .. tzn. „Cześć”
Bhanu Tez
@BhanuTez Nie, w jaki sposób dane są przechowywane i co się z nimi dzieje, to osobne obawy. W tym przykładzie wypisano cały ciąg, ponieważ w ten sposób printfobsługuje się %sciąg formatu: zacznij od podanego adresu i kontynuuj aż do napotkania pustego terminatora. Jeśli chcesz wydrukować tylko jeden znak, możesz %cna przykład użyć ciągu formatu.
iX3
Chciałem tylko zapytać, czy char *p = "abc";znak NULL \0jest automatycznie dołączany, jak w przypadku tablicy char []?
KPMG
dlaczego mogę ustawić, char *name; name="123";ale mogę zrobić to samo z inttypem? I po użyciu %c, aby wydrukować name, wyjście jest nieczytelny ciąg znaków: ?
TomSawyer
83

Zobaczmy:

#include <stdio.h>
#include <string.h>

int main()
{
    char *p = "hello";
    char q[] = "hello"; // no need to count this

    printf("%zu\n", sizeof(p)); // => size of pointer to char -- 4 on x86, 8 on x86-64
    printf("%zu\n", sizeof(q)); // => size of char array in memory -- 6 on both

    // size_t strlen(const char *s) and we don't get any warnings here:
    printf("%zu\n", strlen(p)); // => 5
    printf("%zu\n", strlen(q)); // => 5

    return 0;
}

foo * i foo [] są różnymi typami i są obsługiwane przez kompilator w różny sposób (wskaźnik = adres + reprezentacja typu wskaźnika, tablica = wskaźnik + opcjonalna długość tablicy, jeśli jest znana, na przykład, jeśli tablica jest przydzielana statycznie ), szczegóły można znaleźć w standardzie. A na poziomie środowiska uruchomieniowego nie ma między nimi żadnej różnicy (w asemblerze, cóż, prawie, patrz poniżej).

Istnieje również pokrewny w C FAQ pytanie :

P : Jaka jest różnica między tymi inicjalizacjami?

char a[] = "string literal";   
char *p  = "string literal";   

Mój program ulega awarii, jeśli próbuję przypisać nową wartość do p [i].

Odp . : Dosłowny ciąg znaków (formalny termin na ciąg cudzysłowu w źródle C) może być użyty na dwa nieco inne sposoby:

  1. Jako inicjalizator tablicy char, podobnie jak w deklaracji char a [], określa początkowe wartości znaków w tej tablicy (i, jeśli to konieczne, jej rozmiar).
  2. Gdziekolwiek indziej zamienia się w nienazwaną, statyczną tablicę znaków, a ta nienazwana tablica może być przechowywana w pamięci tylko do odczytu, a zatem niekoniecznie musi być modyfikowana. W kontekście wyrażeń tablica jest jak zwykle konwertowana na wskaźnik (patrz sekcja 6), więc druga deklaracja inicjuje p, aby wskazywać pierwszy element tablicy bez nazwy.

Niektóre kompilatory mają przełącznik kontrolujący, czy literały łańcuchowe są zapisywalne, czy nie (do kompilowania starego kodu), a niektóre mogą mieć opcje powodujące, że literały łańcuchowe są formalnie traktowane jako tablice const char (dla lepszego wychwytywania błędów).

Zobacz także pytania 1.31, 6.1, 6.2, 6.8 i 11.8b.

Referencje: K & R2 Sec. 5,5 p. 104

ISO Sec. 6.1.4, ust. 6.5.7

Uzasadnienie Sec. 3.1.4

H&S Sec. 2.7.4 s. 31–2

JJJ
źródło
W sizeof (q), dlaczego q nie rozpada się na wskaźnik, jak wspomina @Jon w swojej odpowiedzi?
garyp 21.04.16
@garyp q nie rozpada się na wskaźnik, ponieważ sizeof jest operatorem, a nie funkcją (nawet jeśli sizeof był funkcją, q rozpadłby się tylko wtedy, gdy funkcja oczekiwała znaku char).
GiriB,
dzięki, ale printf ("% u \ n" zamiast printf ("% zu \ n", myślę, że powinieneś usunąć z.
Zakaria
33

Jaka jest różnica między tablicą znaków a wskaźnikiem znaków w C?

Projekt C99 N1256

Istnieją dwa różne zastosowania literałów ciągów znaków:

  1. Zainicjuj char[]:

    char c[] = "abc";      

    Jest to „więcej magii” i opisane w 6.7.8 / 14 „Inicjalizacja”:

    Tablica typu znaków może być inicjalizowana literałem ciągu znaków, opcjonalnie ujętym w nawiasy klamrowe. Kolejne znaki literału łańcucha znaków (w tym kończący znak null, jeśli jest miejsce lub tablica ma nieznany rozmiar), inicjują elementy tablicy.

    To tylko skrót do:

    char c[] = {'a', 'b', 'c', '\0'};

    Jak każda inna zwykła tablica, cmoże być modyfikowana.

  2. Wszędzie indziej: generuje:

    Więc kiedy piszesz:

    char *c = "abc";

    Jest to podobne do:

    /* __unnamed is magic because modifying it gives UB. */
    static char __unnamed[] = "abc";
    char *c = __unnamed;

    Zwróć uwagę na niejawne przesłanie od char[]do char *, co jest zawsze legalne.

    Następnie, jeśli zmodyfikujesz c[0], zmodyfikujesz również __unnamed, czyli UB.

    Jest to udokumentowane w 6.4.5 „Literały łańcuchowe”:

    5 W fazie tłumaczenia 7 bajt lub kod o wartości zero jest dołączany do każdej wielobajtowej sekwencji znaków wynikającej z literału lub literałów z ciągu. Wielobajtowa sekwencja znaków jest następnie używana do zainicjowania tablicy statycznego czasu przechowywania i długości wystarczającej do przechowywania sekwencji. W przypadku literałów ciągów znaków elementy tablicy mają typ char i są inicjowane pojedynczymi bajtami wielobajtowej sekwencji znaków [...]

    6 Nie jest określone, czy tablice te są różne, pod warunkiem że ich elementy mają odpowiednie wartości. Jeśli program spróbuje zmodyfikować taką tablicę, zachowanie jest niezdefiniowane.

6.7.8 / 32 „Inicjalizacja” daje bezpośredni przykład:

PRZYKŁAD 8: Deklaracja

char s[] = "abc", t[3] = "abc";

definiuje „zwykłe” obiekty tablicy znaków sit którego elementy są inicjalizowane napisowych charakter.

Ta deklaracja jest identyczna z

char s[] = { 'a', 'b', 'c', '\0' },
t[] = { 'a', 'b', 'c' };

Zawartość tablic można modyfikować. Z drugiej strony deklaracja

char *p = "abc";

definiuje za ppomocą typu „wskaźnik na char” i inicjuje go, aby wskazywał na obiekt o typie „tablica char” o długości 4, którego elementy są inicjowane literałem ciągu znaków. W przypadku próby pzmodyfikowania zawartości tablicy zachowanie jest niezdefiniowane.

Implementacja GCC 4.8 x86-64 ELF

Program:

#include <stdio.h>

int main(void) {
    char *s = "abc";
    printf("%s\n", s);
    return 0;
}

Kompiluj i dekompiluj:

gcc -ggdb -std=c99 -c main.c
objdump -Sr main.o

Dane wyjściowe zawierają:

 char *s = "abc";
8:  48 c7 45 f8 00 00 00    movq   $0x0,-0x8(%rbp)
f:  00 
        c: R_X86_64_32S .rodata

Wniosek: GCC przechowuje char*go w .rodatasekcji, a nie w .text.

Jeśli zrobimy to samo dla char[]:

 char s[] = "abc";

otrzymujemy:

17:   c7 45 f0 61 62 63 00    movl   $0x636261,-0x10(%rbp)

więc zostaje zapisany na stosie (względem %rbp ).

Zauważ jednak, że domyślny skrypt linkera umieszcza .rodatai .textw tym samym segmencie, który wykonał, ale nie ma uprawnień do zapisu. Można to zaobserwować przy:

readelf -l a.out

który zawiera:

 Section to Segment mapping:
  Segment Sections...
   02     .text .rodata
Ciro Santilli
źródło
2
@ leszek.hanusz Niezdefiniowane zachowanie stackoverflow.com/questions/2766731/... Google „C language UB” ;-)
Ciro Santilli 郝海东 冠状 病 六四 事件
9

Nie wolno zmieniać zawartości stałej ciągu, na co pwskazują pierwsze . Drugi pto tablica zainicjowana stałą ciąg, i można zmienić jego zawartość.

potrzebbie
źródło
6

W takich przypadkach efekt jest taki sam: w końcu podajesz adres pierwszego znaku w ciągu znaków.

Deklaracje oczywiście nie są takie same.

Poniżej znajduje się pamięć dla ciągu znaków, a także wskaźnik znaków, a następnie inicjuje wskaźnik, aby wskazywał pierwszy znak w ciągu.

char *p = "hello";

Podczas gdy poniższe odkłada pamięć tylko na ciąg. Dzięki temu może zużywać mniej pamięci.

char p[10] = "hello";
Jonathan Wood
źródło
codeplusplus.blogspot.com/2007/09/… „Jednak inicjowanie zmiennej wymaga ogromnej wydajności i spacji dla tablicy”
leef
@leef: Myślę, że to zależy od tego, gdzie znajduje się zmienna. Jeśli jest w pamięci statycznej, myślę, że tablica i dane mogą być przechowywane w obrazie EXE i nie wymagają żadnej inicjalizacji. W przeciwnym razie tak, z pewnością może być wolniejsze, jeśli dane muszą zostać przydzielone, a następnie dane statyczne muszą zostać skopiowane.
Jonathan Wood
3

O ile pamiętam, tablica jest w rzeczywistości grupą wskaźników. Na przykład

p[1]== *(&p+1)

to prawdziwe stwierdzenie

CosminO
źródło
2
Opisałbym tablicę jako wskaźnik do adresu bloku pamięci. Dlatego dlaczego *(arr + 1)przenosi Cię do drugiego członka arr. Jeśli *(arr)wskazuje na 32-bitowy adres pamięci, np. bfbcdf5eWtedy *(arr + 1)wskazuje na bfbcdf60(drugi bajt). Dlatego wyjście poza zakres tablicy doprowadzi do dziwnych rezultatów, jeśli system operacyjny nie ulegnie awarii. Jeśli int a = 24;jest pod adresem bfbcdf62, dostęp arr[2]może się zwrócić 24, zakładając, że segfault nie nastąpi wcześniej.
Braden Best
3

Z APUE , sekcja 5.14:

char    good_template[] = "/tmp/dirXXXXXX"; /* right way */
char    *bad_template = "/tmp/dirXXXXXX";   /* wrong way*/

... W przypadku pierwszego szablonu nazwa jest przypisywana do stosu, ponieważ używamy zmiennej tablicowej. W przypadku drugiego imienia używamy jednak wskaźnika. W takim przypadku na stosie znajduje się tylko pamięć samego wskaźnika; kompilator organizuje zapis łańcucha w segmencie pliku wykonywalnego tylko do odczytu. Gdy mkstempfunkcja próbuje zmodyfikować ciąg, występuje błąd segmentacji.

Cytowany tekst pasuje do wyjaśnienia @Ciro Santilli.

Stóg
źródło
1

char p[3] = "hello"? należy char p[6] = "hello"pamiętać, że na końcu „łańcucha” w C. znajduje się znak „\ 0”

w każdym razie tablica w C jest tylko wskaźnikiem do pierwszego obiektu obiektów dopasowujących w pamięci. jedyne różne są w semantyce. podczas gdy możesz zmienić wartość wskaźnika, aby wskazywał na inną lokalizację w pamięci, tablica po utworzeniu zawsze będzie wskazywać na tę samą lokalizację.
również podczas korzystania z tablic „nowe” i „usuń” są wykonywane automatycznie.

Roee Gavirel
źródło