Czy inicjowanie char [] za pomocą dosłownego ciągu znaków jest złą praktyką?

44

Czytałem wątek zatytułowany „strlen vs sizeof” na CodeGuru , a jedna z odpowiedzi mówi, że „w każdym razie [sic] złą praktyką jest inicjowanie [sic] chartablicy z dosłownym ciągiem znaków”.

Czy to prawda, czy to tylko jego (choć „elitarny członek”) opinia?


Oto oryginalne pytanie:

#include <stdio.h>
#include<string.h>
main()
{
    char string[] = "october";
    strcpy(string, "september");

    printf("the size of %s is %d and the length is %d\n\n", string, sizeof(string), strlen(string));
    return 0;
}

dobrze. rozmiar powinien być równy długości plus 1 tak?

to jest wynik

the size of september is 8 and the length is 9

rozmiar powinien z pewnością wynosić 10. to jak obliczanie rozmiaru ciągu przed zmianą przez strcpy, ale długość po nim.

Czy coś jest nie tak z moją składnią?


Oto odpowiedź :

W każdym razie złą praktyką jest inicjałowanie tablicy znaków literałem łańcuchowym. Dlatego zawsze wykonaj jedną z następujących czynności:

const char string1[] = "october";
char string2[20]; strcpy(string2, "september");
Cole Johnson
źródło
Zwróć uwagę na „const” w pierwszym wierszu. Czy to możliwe, że autor założył c ++ zamiast c? W c ++ jest to „zła praktyka”, ponieważ literał powinien być const, a każdy najnowszy kompilator c ++ wyświetli ostrzeżenie (lub błąd) o przypisaniu stałego literału do tablicy innej niż const.
André
@ André C ++ definiuje literały łańcuchowe jako tablice const, ponieważ jest to jedyny bezpieczny sposób radzenia sobie z nimi. To, że C nie jest problemem, więc masz społeczną zasadę, która wymusza bezpieczną rzecz
Caleth
@Caleth. Wiem, starałem się bardziej argumentować, że autor odpowiedzi podchodził do „złej praktyki” z perspektywy c ++.
André
@ André to nie jest zła praktyka w C ++, ponieważ nie jest to praktyka , to błąd typu „up up”. To powinno być to błąd typu w C, ale to nie jest, więc trzeba mieć regułę prowadzącą styl informacją „To zabronione”
Caleth

Odpowiedzi:

59

W każdym razie złą praktyką jest inicjałowanie tablicy znaków literałem łańcuchowym.

Autor tego komentarza tak naprawdę nigdy go nie usprawiedliwia, a stwierdzenie to mnie zastanawia.

W C (i oznaczyłeś to jako C), jest to prawie jedyny sposób na zainicjowanie tablicy charz wartością ciągu (inicjalizacja różni się od przypisania). Możesz napisać albo

char string[] = "october";

lub

char string[8] = "october";

lub

char string[MAX_MONTH_LENGTH] = "october";

W pierwszym przypadku rozmiar tablicy jest pobierany z rozmiaru inicjatora. Literały łańcuchowe są przechowywane jako tablice charz końcowym bajtem 0, więc rozmiar tablicy wynosi 8 („o”, „c”, „t”, „o”, „b”, „e”, „r”, 0). W dwóch pozostałych przypadkach rozmiar tablicy jest określony jako część deklaracji (8 i MAX_MONTH_LENGTH, cokolwiek by się nie zdarzyło).

Czego nie może zrobić, to napisać coś

char string[];
string = "october";

lub

char string[8];
string = "october";

itd. W pierwszym przypadku deklaracja stringjest niekompletna, ponieważ nie określono rozmiaru tablicy i nie ma inicjatora, z którego można by pobrać rozmiar. W obu przypadkach =nie zadziała, ponieważ a) wyrażenie tablicowe, które stringmoże nie być celem zadania ib) =operator nie jest zdefiniowany tak, aby kopiować zawartość jednej tablicy do drugiej.

Z tego samego powodu nie możesz pisać

char string[] = foo;

gdzie foojest kolejna tablica char. Ta forma inicjalizacji będzie działać tylko z literałami łańcuchowymi.

EDYTOWAĆ

Powinienem to zmienić, aby powiedzieć, że możesz także inicjować tablice, aby przechowywać ciąg znaków za pomocą inicjatora w stylu tablicowym

char string[] = {'o', 'c', 't', 'o', 'b', 'e', 'r', 0};

lub

char string[] = {111, 99, 116, 111, 98, 101, 114, 0}; // assumes ASCII

ale łatwiej jest oczom używać literałów łańcuchowych.

EDYCJA 2

Aby przypisać zawartość tablicy poza deklaracją, musisz użyć albo strcpy/strncpy(dla łańcuchów zakończonych 0) lub memcpy(dla dowolnego innego typu tablicy):

if (sizeof string > strlen("october"))
  strcpy(string, "october");

lub

strncpy(string, "october", sizeof string); // only copies as many characters as will
                                           // fit in the target buffer; 0 terminator
                                           // may not be copied, but the buffer is
                                           // uselessly completely zeroed if the
                                           // string is shorter!
John Bode
źródło
@KeithThompson: nie zgadzam się, po prostu dodałem go ze względu na kompletność.
John Bode
16
Pamiętaj, że char[8] str = "october";jest to zła praktyka. Musiałem dosłownie zwęglić się, aby upewnić się, że nie jest to przepełnienie i że psuje się podczas konserwacji ... np. Poprawianie błędu pisowni od sepratedo zepsuje się, separatejeśli rozmiar nie zostanie zaktualizowany.
djechlin
1
Zgadzam się z djechlin, to zła praktyka z podanych powodów. Odpowiedź JohnBode'a wcale nie komentuje aspektu „złej praktyki” (która jest główną częścią pytania !!), po prostu wyjaśnia, co możesz, a czego nie możesz zrobić, aby zainicjować tablicę.
mastov
Drobne: Ponieważ wartość „długość” zwrócona z strlen()nie zawiera znaku zerowego, użycie MAX_MONTH_LENGTHdo utrzymania maksymalnego rozmiaru wymaganego char string[]często wygląda źle. IMO, MAX_MONTH_SIZEbyłoby lepiej tutaj.
chux - Przywróć Monikę
10

Jedyny problem, jaki pamiętam, to przypisanie literału ciągów do char *:

char var1[] = "september";
var1[0] = 'S'; // Ok - 10 element char array allocated on stack
char const *var2 = "september";
var2[0] = 'S'; // Compile time error - pointer to constant string
char *var3 = "september";
var3[0] = 'S'; // Modifying some memory - which may result in modifying... something or crash

Na przykład weź ten program:

#include <stdio.h>

int main() {
  char *var1 = "september";
  char *var2 = "september";
  var1[0] = 'S';
  printf("%s\n", var2);
}

To na mojej platformie (Linux) ulega awarii podczas próby zapisu na stronie oznaczonej jako tylko do odczytu. Na innych platformach może drukować „wrzesień” itp.

To powiedziawszy - inicjalizacja przez literał powoduje określoną liczbę rezerwacji, więc to nie zadziała:

char buf[] = "May";
strncpy(buf, "September", sizeof(buf)); // Result "Sep"

Ale to będzie

char buf[32] = "May";
strncpy(buf, "September", sizeof(buf));

Ostatnia uwaga - w ogóle nie używałbym strcpy:

char buf[8];
strcpy(buf, "very long string very long string"); // Oops. We overwrite some random memory

Chociaż niektóre kompilatory mogą zmienić to w bezpieczne wywołanie, strncpyjest o wiele bezpieczniejsze:

char buf[1024];
strncpy(buf, something_else, sizeof(buf)); // Copies at most sizeof(buf) chars so there is no possibility of buffer overrun. Please note that sizeof(buf) works for arrays but NOT pointers.
buf[sizeof(buf) - 1] = '\0';
Maciej Piechotka
źródło
Nadal istnieje ryzyko przepełnienia bufora, strncpyponieważ nie kończy on kopiowanego łańcucha, gdy długość something_elsejest większa niż sizeof(buf). Zwykle ustawiam ostatni znak, buf[sizeof(buf)-1] = 0aby go zabezpieczyć, lub jeśli bufjest inicjowany na zero, użyj sizeof(buf) - 1jako długości kopii.
syockit
Użyj strlcpylub strcpy_snawet snprintfjeśli musisz.
user253751
Naprawiony. Niestety nie ma łatwego przenośnego sposobu na zrobienie tego, chyba że masz luksus pracy z najnowszymi kompilatorami ( strlcpyi snprintfnie jesteś bezpośrednio dostępny w MSVC, przynajmniej zamówienia i strcpy_snie ma * nix).
Maciej Piechotka
@MaciejPiechotka: Dzięki Bogu Unix odrzucił sponsorowany przez Microsoft załącznik k.
Deduplicator
6

Jedną rzeczą, której nie porusza żaden wątek, jest to:

char whopping_great[8192] = "foo";

vs.

char whopping_great[8192];
memcpy(whopping_great, "foo", sizeof("foo"));

Ten pierwszy zrobi coś takiego:

memcpy(whopping_great, "foo", sizeof("foo"));
memset(&whopping_great[sizeof("foo")], 0, sizeof(whopping_great)-sizeof("foo"));

Ten ostatni robi tylko memcpy. Standard C nalega, aby inicjalizować dowolną część tablicy, wszystko jest w porządku. W takim przypadku lepiej to zrobić samemu. Wydaje mi się, że o to właśnie chodziło.

Na pewno

char whopping_big[8192];
whopping_big[0] = 0;

jest lepszy niż:

char whopping_big[8192] = {0};

lub

char whopping_big[8192] = "";

ps Aby uzyskać punkty bonusowe, możesz:

memcpy(whopping_great, "foo", (1/(sizeof("foo") <= sizeof(whopping_great)))*sizeof("foo"));

aby rzucić czas kompilacji podziel przez błąd zero, jeśli masz zamiar przepełnić tablicę.

Richard Fife
źródło
5

Przede wszystkim dlatego, że nie będziesz mieć wielkości char[]zmiennej / konstrukcji, której można łatwo użyć w programie.

Przykładowy kod z linku:

 char string[] = "october";
 strcpy(string, "september");

stringjest przydzielany na stosie jako 7 lub 8 znaków. Nie mogę sobie przypomnieć, czy jest zakończone zerem w ten sposób, czy nie - wątek, do którego linkujesz, stwierdził, że tak.

Kopiowanie „września” nad tym ciągiem jest oczywistym przepełnieniem pamięci.

Kolejne wyzwanie pojawia się, jeśli przejdziesz stringdo innej funkcji, aby inna funkcja mogła zapisać się w tablicy. Trzeba powiedzieć, że inne funkcje, jak długo tablica jest tak , że nie tworzy przepełnienie. Możesz przekazać stringwraz z wynikiem, strlen()ale wątek wyjaśnia, jak to może wybuchnąć, jeśli stringnie jest zakończone zerem.

Lepiej jest przydzielić ciąg znaków o stałym rozmiarze (najlepiej zdefiniowanym jako stała), a następnie przekazać tablicę i stały rozmiar innej funkcji. Komentarze Johna Bode'a są poprawne i istnieją sposoby na ograniczenie tego ryzyka. Wymagają również więcej wysiłku z Twojej strony, aby z nich korzystać.

Z mojego doświadczenia char[]wynika , że wartość, którą zainicjowałem, jest zwykle zbyt mała dla innych wartości, które muszę tam umieścić. Korzystanie ze zdefiniowanej stałej pomaga uniknąć tego problemu.


sizeof stringpoda rozmiar bufora (8 bajtów); użyj wyniku tego wyrażenia zamiast strlengdy martwisz się pamięcią.
Podobnie można wykonać test przed wywołanie strcpyaby sprawdzić, czy bufor docelowy jest wystarczająco duży dla łańcucha źródłowego: if (sizeof target > strlen(src)) { strcpy (target, src); }.
Tak, jeśli masz przekazać tablicę do funkcji, musisz przekazać swoje fizyczne wymiary, a także: foo (array, sizeof array / sizeof *array);. - John Bode

Społeczność
źródło
2
sizeof stringpoda rozmiar bufora (8 bajtów); użyj wyniku tego wyrażenia zamiast strlengdy martwisz się pamięcią. Podobnie można wykonać test przed wywołanie strcpyaby sprawdzić, czy bufor docelowy jest wystarczająco duży dla łańcucha źródłowego: if (sizeof target > strlen(src)) { strcpy (target, src); }. Tak, jeśli masz przekazać tablicę do funkcji, musisz przekazać swoje fizyczne wymiary, a także: foo (array, sizeof array / sizeof *array);.
John Bode,
1
@JohnBode - dzięki, a to są dobre punkty. Włączyłem twój komentarz do mojej odpowiedzi.
1
Mówiąc dokładniej, większość odniesień do nazwy tablicy stringpowoduje niejawną konwersję na char*, wskazując na pierwszy element tablicy. To powoduje utratę informacji o granicach tablicy. Wywołanie funkcji jest tylko jednym z wielu kontekstów, w których tak się dzieje. char *ptr = string;jest inny. Nawet string[0]jest tego przykładem; []operator pracuje nad wskaźnikami, a nie bezpośrednio na macierzach. Sugerowana literatura: Sekcja 6 comp.lang.c FAQ .
Keith Thompson
Wreszcie odpowiedź, która faktycznie odnosi się do pytania!
mastov
2

Myślę, że pomysł „złej praktyki” wynika z faktu, że ta forma:

char string[] = "october is a nice month";

czyni domyślnie strcpy ze źródłowego kodu maszynowego na stos.

Bardziej wydajne jest obsługiwanie tylko linku do tego ciągu. Podobnie jak w przypadku:

char *string = "october is a nice month";

lub bezpośrednio:

strcpy(output, "october is a nice month");

(ale oczywiście w większości kodów to chyba nie ma znaczenia)

Toto
źródło
Czy nie zrobiłby kopii tylko, jeśli spróbujesz ją zmodyfikować? Sądzę, że kompilator byłby mądrzejszy
Cole Johnson
1
Co z przypadkami, w char time_buf[] = "00:00";których będziesz modyfikować bufor? char *Inicjowane ciągiem znaków jest ustawiona na adres pierwszego bajtu, więc próbuje modyfikować wyniki w niezdefiniowanej zachowań, ponieważ metoda przechowywania łańcuchem znaków jest nieznany (wdrożenie zdefiniowane), natomiast modyfikując bajtów: a char[]to dlatego, że całkowicie legalne inicjalizacja kopiuje bajty do miejsca do zapisu przydzielonego na stosie. Stwierdzenie, że jest to „mniej wydajne” lub „zła praktyka” bez rozwijania niuansów, char* vs char[]jest mylące.
Braden Best
-3

Nigdy nie jest naprawdę długi czas, ale należy unikać inicjalizacji char [] do string, ponieważ „string” jest const char *, a ty przypisujesz go do char *. Więc jeśli przekażesz ten znak [] metodzie, która zmienia dane, możesz mieć ciekawe zachowanie.

Jak powiedział komenda, wymieszałem trochę char [] z char *, to nie jest dobre, ponieważ się trochę różnią.

Nie ma nic złego w przypisywaniu danych do tablicy char, ale ponieważ intencją użycia tej tablicy jest użycie jej jako „łańcucha” (char *), łatwo zapomnieć, że nie należy modyfikować tej tablicy.

Dainius
źródło
3
Błędny. Inicjalizacja kopiuje zawartość literału łańcucha do tablicy. Obiekt tablicy nie jest, constchyba że zdefiniujesz go w ten sposób. (A literały łańcuchowe w C nie są const, chociaż każda próba modyfikacji literału łańcuchowego ma nieokreślone zachowanie.) char *s = "literal";Ma takie zachowanie, o którym mówisz; lepiej napisać jakoconst char *s = "literal";
Keith Thompson
rzeczywiście moja wina, że ​​pomieszałem char [] z char *. Ale nie byłbym tak pewny kopiowania treści do tablicy. Szybkie sprawdzenie za pomocą kompilatora MS C pokazuje, że „char c [] =" asdf "; ' utworzy „ciąg” w stałym segmencie, a następnie przypisze ten adres do zmiennej tablicowej. To właściwie powód, dla którego powiedziałem o unikaniu przypisań do tablicy non const char.
Dainius
Jestem sceptyczny. Wypróbuj ten program i daj mi znać, jaki wynik uzyskasz.
Keith Thompson
2
„Ogólnie rzecz biorąc,„ asdf ”jest stałą, dlatego należy ją zadeklarować jako const.” - To samo rozumowanie wymagałoby constwłączenia int n = 42;, ponieważ 42jest stałe.
Keith Thompson
1
Nie ma znaczenia, na jakiej maszynie jesteś. Standard językowy gwarantuje, że cmożna go modyfikować. To dokładnie tak silna gwarancja, jak ta, która 1 + 1ocenia 2. Jeśli program, do którego się przyłączyłem, robi coś innego niż drukowanie EFGH, oznacza to niezgodną implementację C.
Keith Thompson,