Jak tablice znaków powinny być używane jako ciągi znaków?

10

Rozumiem, że ciągi w C są tylko tablicami znaków. Wypróbowałem więc następujący kod, ale daje on dziwne wyniki, takie jak wyrzucanie elementów bezużytecznych lub awarie programu:

#include <stdio.h>

int main (void)
{
  char str [5] = "hello";
  puts(str);
}

Dlaczego to nie działa?

Czyści się z gcc -std=c17 -pedantic-errors -Wall -Wextra.


Uwaga: Ten post ma być używany jako kanoniczne FAQ w przypadku problemów wynikających z nieprzydzielenia miejsca dla terminatora NUL podczas deklarowania łańcucha.

Lundin
źródło

Odpowiedzi:

12

Łańcuch AC to tablica znaków, która kończy się zerowym terminatorem .

Wszystkie znaki mają wartość tabeli symboli. Terminator zerowy to wartość symbolu 0(zero). Służy do oznaczania końca łańcucha. Jest to konieczne, ponieważ rozmiar łańcucha nie jest nigdzie przechowywany.

Dlatego za każdym razem, gdy przydzielasz miejsce dla ciągu, musisz uwzględnić wystarczającą ilość miejsca na znak zerowy terminatora. Twój przykład tego nie robi, przydziela miejsce tylko dla 5 znaków "hello". Prawidłowy kod powinien być:

char str[6] = "hello";

Lub równoważnie, możesz napisać samodokumentujący kod dla 5 znaków plus 1 zerowy terminator:

char str[5+1] = "hello";

Podczas dynamicznego przydzielania pamięci dla ciągu w czasie wykonywania należy również przydzielić miejsce dla terminatora zerowego:

char input[n] = ... ;
...
char* str = malloc(strlen(input) + 1);

Jeśli nie dodasz terminatora zerowego na końcu łańcucha, funkcje biblioteczne oczekujące, że łańcuch nie będzie działał poprawnie, a otrzymasz błędy „niezdefiniowanego zachowania”, takie jak wyrzucanie elementów bezużytecznych lub awarie programu.

Najczęstszym sposobem napisać znak zerowy terminatora w C jest za pomocą tzw „ósemkowy sekwencję escape”, patrząc jak ten: '\0'. Jest to w 100% ekwiwalent zapisu 0, ale \służy jako kod samokontrujący do stwierdzenia, że ​​zero ma jawnie oznaczać zerowy terminator. Kod taki jak if(str[i] == '\0')sprawdza, czy określony znak jest terminatorem zerowym.

Pamiętaj, że termin terminator null nie ma nic wspólnego ze wskaźnikami null ani NULLmakrem! Może to być mylące - bardzo podobne nazwy, ale bardzo różne znaczenia. Dlatego terminator zerowy jest czasami określany jako NULjeden L, nie należy go mylić ze NULLwskaźnikami zerowymi. Aby uzyskać szczegółowe informacje, zobacz odpowiedzi na to SO pytanie .

W "hello"twoim kodzie nazywa się literał łańcuchowy . Należy to traktować jako ciąg tylko do odczytu. Te ""środki składniowe, że kompilator dołączy null terminator na końcu łańcucha dosłowne automatycznie. Więc jeśli wydrukujesz, sizeof("hello")dostaniesz 6, a nie 5, ponieważ otrzymasz rozmiar tablicy, w tym terminator zerowy.


Kompiluje się czysto z gcc

Rzeczywiście, nawet ostrzeżenie. Wynika to z subtelnych szczegółów / błędów w języku C, które umożliwiają inicjowanie tablic znaków za pomocą literału łańcuchowego zawierającego dokładnie tyle znaków, ile jest miejsca w tablicy, a następnie po cichu odrzucają terminator zerowy (C17 6.7.9 / 15). Język celowo zachowuje się tak z powodów historycznych, zobacz Niespójna diagnostyka gcc do inicjalizacji łańcucha, aby uzyskać szczegółowe informacje. Zauważ też, że C ++ jest tutaj inny i nie pozwala na użycie tej sztuczki / błędu.

Lundin
źródło
1
Powinieneś wspomnieć o char str[] = "hello";sprawie.
Jabberwocky
@Jabberwocky To jest wiki społeczności, możesz je edytować i dodawać.
Lundin
1
... a może także char *str = "hello";... str[0] = foo;problem.
Jabberwocky
Być może rozszerzysz implikację użycia sizeofna jego użycie na parametrze funkcji, zwłaszcza gdy jest zdefiniowany jako tablica.
Wiatrowskaz
@WeatherVane Powinien być objęty innym FAQ tutaj: stackoverflow.com/questions/492384/…
Lundin
4

Ze standardu C (7.1.1 Definicje terminów)

1 Łańcuch to ciągła sekwencja znaków zakończona pierwszym znakiem zerowym włącznie. Termin łańcuch wielobajtowy jest czasami używany zamiast tego, aby podkreślić specjalne przetwarzanie znaków wielobajtowych zawartych w ciągu lub w celu uniknięcia pomyłki z szerokim ciągiem. Wskaźnik do łańcucha jest wskaźnikiem do jego początkowego (najniżej adresowanego) znaku. Długość łańcucha to liczba bajtów poprzedzających znak zerowy, a wartość ciągu to sekwencja wartości zawartych znaków, w kolejności.

W tej deklaracji

char str [5] = "hello";

literał łańcuchowy "hello"ma wewnętrzną reprezentację podobną do

{ 'h', 'e', 'l', 'l', 'o', '\0' }

więc ma 6 znaków, w tym końcowe zero. Jego elementy służą do inicjalizacji tablicy znaków, strktóra rezerwuje miejsce tylko dla 5 znaków.

Standard C (w przeciwieństwie do standardu C ++) umożliwia taką inicjalizację tablicy znaków, gdy końcowe zero literału łańcucha nie jest używane jako inicjator.

Jednak w rezultacie tablica znaków strnie zawiera łańcucha.

Jeśli chcesz, aby tablica zawierała ciąg, który możesz zapisać

char str [6] = "hello";

Lub tylko

char str [] = "hello";

W ostatnim przypadku rozmiar tablicy znaków jest określany na podstawie liczby inicjatorów literału łańcuchowego równej 6.

Vlad z Moskwy
źródło
0

Czy wszystkie ciągi znaków można uznać za tablicę znaków ( Tak ), czy wszystkie tablice znaków można uznać za ciągi znaków ( Nie ).

Dlaczego nie? i dlaczego to ma znaczenie?

Oprócz innych odpowiedzi wyjaśniających, że długość łańcucha nie jest nigdzie przechowywana jako część łańcucha, a także odniesienia do standardu, w którym łańcuch jest zdefiniowany, drugą stroną jest „Jak funkcje biblioteki C obsługują łańcuchy?”.

Chociaż tablica znaków może zawierać te same znaki, jest to po prostu tablica znaków, chyba że po ostatnim znaku następuje znak kończący wartość nul . Ten znak kończący wartość NUL pozwala na to, aby tablica znaków była traktowana (traktowana jako) ciąg znaków.

Wszystkie funkcje w C, które oczekują ciągu jako argumentu, oczekują, że sekwencja znaków zostanie zakończona na NUL . Dlaczego?

Ma to związek ze sposobem działania wszystkich funkcji łańcucha. Ponieważ długość nie jest uwzględniona jako część tablicy, funkcje łańcuchowe, przewijaj do przodu w tablicy, aż zostanie znaleziony znak nul (np. '\0'- równoważny dziesiętnemu 0). Patrz tabela i opis ASCII . Niezależnie od tego, czy używasz strcpy, strchr, strcspnitp .. Wszystkie funkcje łańcuchowe polegać na nul kończącego znaku są obecne w celu określenia, gdzie koniec tego łańcucha.

Porównanie dwóch podobnych funkcji z string.hpodkreśli znaczenie znaku kończącego wartość nul . Weź na przykład:

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

strcpyFunkcja po prostu kopiuje bajty od srcdo destmomentu nul kończącego znajduje charakter opowiadania strcpygdzie zatrzymać kopiowanie znaków. Teraz weź podobną funkcję memcpy:

    void *memcpy(void *dest, const void *src, size_t n);

Funkcja wykonuje podobną operację, ale nie bierze pod uwagę ani nie wymaga, aby srcparametr był łańcuchem. Ponieważ memcpynie można po prostu skanować do przodu podczas srckopiowania bajtów do, destaż zostanie osiągnięty znak kończący wartość NUL , wymaga jawnej liczby bajtów do skopiowania jako trzeciego parametru. Ten trzeci parametr zapewnia memcpytę samą wielkość informacji, którą strcpymożna uzyskać po prostu skanując do przodu, aż zostanie znaleziony znak kończący wartość nul .

(co podkreśla również to, co idzie nie tak w strcpy(lub jakiejkolwiek funkcji oczekującej łańcucha), jeśli nie dostarczysz funkcji łańcucha zakończonego znakiem nul - nie ma pojęcia, gdzie się zatrzymać i z radością wybiegnie w pozostałą część twojego segmentu pamięci wywoływanie niezdefiniowanego zachowania, dopóki nul-postać nie znajdzie się gdzieś w pamięci - lub wystąpi błąd segmentacji)

Czyli dlaczego funkcje spodziewa się znakiem NUL łańcuch musi być podjęło znakiem NUL łańcuch i dlaczego jest to ważne .

David C. Rankin
źródło
0

Intuicyjnie...

Pomyśl o tablicy jako o zmiennej (przechowuje rzeczy), a łańcuch o wartości (może być umieszczony w zmiennej).

Z pewnością nie są tym samym. W twoim przypadku zmienna jest zbyt mała, aby pomieścić łańcuch, więc łańcuch zostaje odcięty. („ciągi cytowane” w C mają na końcu niejawny znak zerowy).

Możliwe jest jednak przechowywanie łańcucha w tablicy, która jest znacznie większa niż łańcuch.

Pamiętaj, że zwykłe operatory przypisania i porównania ( = == <itp.) Nie działają tak, jak można się spodziewać. Ale strxyzrodzina funkcji jest bardzo bliska, gdy już wiesz, co robisz. Zobacz C FAQ na temat łańcuchów i tablic .

Artelius
źródło