Czy łańcuchy C zawsze kończą się na null, czy zależy to od platformy?

13

Obecnie pracuję z systemami osadzonymi i zastanawiam się, jak zaimplementować ciągi w mikroprocesorze bez systemu operacyjnego. Do tej pory używam idei, że NULL kończy wskaźniki wskaźników i traktuje je jako ciągi znaków, w których NULL oznacza koniec. Wiem, że jest to dość powszechne, ale czy zawsze możesz na to liczyć?

Powodem, dla którego pytam, jest to, że w pewnym momencie zastanawiałem się nad użyciem systemu operacyjnego w czasie rzeczywistym i chciałbym ponownie wykorzystać jak najwięcej mojego obecnego kodu. Czy wobec różnych dostępnych opcji mogę się spodziewać, że łańcuchy będą działać tak samo?

Pozwólcie, że będę bardziej konkretny w mojej sprawie. Wdrażam system, który pobiera i przetwarza polecenia przez port szeregowy. Czy mogę zachować ten sam kod przetwarzania poleceń, a następnie oczekiwać, że obiekty łańcuchowe utworzone w systemie RTOS (który zawiera polecenia) zostaną zakończone na NULL? Czy może byłoby inaczej w zależności od systemu operacyjnego?

Aktualizacja

Po zapoznaniu się z tym pytaniem ustaliłem, że nie odpowiada ono dokładnie na to, o co pytam. Samo pytanie dotyczy tego, czy należy zawsze podawać długość łańcucha, co jest zupełnie inne niż to, o które pytam, i chociaż niektóre odpowiedzi zawierały przydatne informacje, nie są to dokładnie to, czego szukam. Odpowiedzi tam wydawały się uzasadniać, dlaczego lub dlaczego nie kończyć łańcucha ze znakiem zerowym. Różnica w stosunku do tego, o co pytam, polega na tym, czy mogę mniej więcej oczekiwać, że wrodzone ciągi różnych platform zakończą swoje ciągi zerowe, bez konieczności wychodzenia i wypróbowania każdej platformy, jeśli ma to sens.

Podejrzeć
źródło
3
Dawno nie używałem C, ale nie mogę sobie wyobrazić czasu, kiedy natknąłem się na implementację, która nie używała ciągów zakończonych znakiem NULL. Jest to część standardu C, jeśli dobrze pamiętam (tak jak powiedziałem, minęło trochę czasu ...)
MetalMikester
1
Nie jestem specjalistą w C, ale o ile wiem, wszystkie ciągi w C są tablicami char, zakończonymi zerem. Możesz jednak utworzyć własny typ ciągu, ale wszystkie funkcje manipulacji ciągiem musiałyby być wdrożone samodzielnie.
Machado
1
@MetalMikester Myślisz, że te informacje można znaleźć w standardowej specyfikacji C?
Snoop,
3
@Snoopy Najprawdopodobniej tak. Ale tak naprawdę, gdy mówimy o ciągach w C, to tylko tablica znaków, które kończą się na NULL i to tyle, chyba że użyjesz jakiejś niestandardowej biblioteki ciągów, ale o tym tutaj nie mówimy. Wątpię, czy znajdziesz platformę, która tego nie szanuje, zwłaszcza że jedną z zalet C jest przenośność.
MetalMikester

Odpowiedzi:

42

Rzeczy zwane „ciągami C” zostaną zakończone zerem na dowolnej platformie. W ten sposób standardowe funkcje biblioteki C określają koniec łańcucha.

W języku C nic nie stoi na przeszkodzie, aby mieć tablicę znaków, która nie kończy się na null. Będziesz jednak musiał użyć innej metody, aby uniknąć spływu końca łańcucha.

Simon B.
źródło
4
po prostu dodać; zazwyczaj masz gdzieś liczbę całkowitą, aby śledzić długość łańcucha, a następnie kończysz z niestandardową strukturą danych, aby zrobić to dobrze, coś w rodzaju klasy QString w Qt
Rudolf Olah
8
Przykład: pracuję z programem C, który wykorzystuje co najmniej pięć różnych formatów ciągów: chartablice zakończone znakiem null , chartablice o długości zakodowanej w pierwszym bajcie (powszechnie znane jako „ciągi Pascala”), wchar_twersje obu powyżej oraz chartablice, które łączą obie metody: długość zakodowana w pierwszym bajcie i znak null kończący ciąg.
Mark
4
@Mark Interfejs z wieloma komponentami / aplikacjami innych firm lub starszym bałaganem kodu?
Dan Is Fiddling By Firelight
2
@ DanNeely, wszystkie powyższe. Ciągi Pascal do współpracy z klasycznym MacOS, ciągi C do użytku wewnętrznego i Windows, szerokie ciągi do dodawania obsługi Unicode i ciągi drania, ponieważ ktoś próbował być sprytny i stworzyć ciąg, który mógłby łączyć się zarówno z MacOS, jak i Windows w tym samym czasie.
Mark
1
@ Mark ... i oczywiście nikt nie jest skłonny wydawać pieniędzy na spłatę długu technicznego, ponieważ klasyczny MacOS już dawno nie żyje, a łańcuchy drania są podwójnym klastrem za każdym razem, gdy trzeba je dotknąć. Moja sympatia.
Dan Is Fiddling By Firelight
22

Określenie znaku kończącego należy do kompilatora literałów i implementacji standardowej biblioteki ciągów w ogóle. Nie jest to określane przez system operacyjny.

Konwencja NULwypowiedzenia sięga wcześniejszego standardu C i za ponad 30 lat nie mogę powiedzieć, że wpadłem na środowisko, które robi cokolwiek innego. To zachowanie zostało skodyfikowane w C89 i nadal stanowi część standardu języka C (link jest do wersji roboczej C99):

  • Podrozdział 6.4.5 określa etapy dla NULłańcuchów-terminali, wymagając, aby NULdopisywać je do literałów łańcuchowych.
  • Sekcja 7.1.1 wprowadza tę funkcję do funkcji w bibliotece standardowej, definiując ciąg znaków jako „ciągłą sekwencję znaków zakończoną pierwszym znakiem zerowym włącznie”.

Nie ma powodu, dla którego ktoś nie mógłby napisać funkcji, które obsługują ciągi zakończone przez inną postać, ale nie ma również powodu, aby zerwać z ustalonym standardem w większości przypadków, chyba że twoim celem jest dopasowanie programistów. :-)

Blrfl
źródło
2
Jednym z powodów byłoby unikanie konieczności ciągłego odnajdywania końca tego samego ciągu.
Paŭlo Ebermann
@ PaŭloEbermann Right. Kosztem podania dwóch wartości zamiast jednej. Co jest nieco irytujące, jeśli po prostu przekażesz literał łańcuchowy jak w printf("string: \"%s\"\n", "my cool string"). Jedynym sposobem na obejście czterech parametrów w tym przypadku (innych niż bajt kończący) byłoby zdefiniowanie łańcucha, który będzie podobny std::stringdo C ++, który ma swoje własne problemy i ograniczenia.
cmaster
1
Sekcja 6.4.5 nie wymaga się ciągiem znaków zostać zakończony znakiem null. Wyraźnie stwierdza: „ Dosłowny ciąg znaków nie musi być ciągiem (patrz 7.1.1), ponieważ znak null może być w nim osadzony przez sekwencję specjalną \ 0.
bzeaman
1
@bzeaman Przypis mówi, że możesz skonstruować literał łańcucha, który nie spełnia definicji łańcucha 7.1.1, ale odnoszące się do niego zdanie mówi, że zgodne kompilatory NUL-je upowszechniają bez względu na wszystko: „W fazie tłumaczenia 7 bajt lub kod o wartości zero jest dołączany do każdej wielobajtowej sekwencji znaków, która wynika z literału łańcuchowego lub literałów. ” Funkcje biblioteczne korzystające z definicji 7.1.1 zatrzymują się przy pierwszym NULznalezieniu i nie będą wiedzieć ani nie przejmować się, że poza nim istnieją dodatkowe znaki.
Blrfl
Poprawiono mnie. Szukałem różnych terminów, takich jak „null”, ale brakowało 6.4.5.5, wspominając o „wartości zero”.
bzeaman
3

Pracuję z systemami osadzonymi ... bez systemu operacyjnego ... używam ... pomysłu, że NULL kończy wskaźniki wskaźników i traktuje je jako ciągi znaków, w których NULL oznacza koniec. Wiem, że jest to dość powszechne, ale czy zawsze możesz na to liczyć?

W języku C nie ma typu danych ciągu, ale istnieją literały ciągu .

Jeśli wstawisz dosłowny ciąg znaków w swoim programie, zwykle zakończy się ono NUL (ale zobacz specjalny przypadek omówiony w komentarzach poniżej). To znaczy, jeśli umieścisz "foobar"w miejscu, w którym const char *oczekiwana jest wartość, kompilator wyemituje foobar⊘do const / code segment / section twojego programu, a wartość wyrażenia będzie wskaźnikiem do adresu, w którym zapisał fznak. (Uwaga: używam do oznaczenia bajtu NUL.)

Jedynym innym sensem, w którym język C ma ciągi znaków, jest kilka standardowych procedur bibliotecznych, które działają na sekwencjach znaków zakończonych znakiem NUL. Te procedury biblioteczne nie będą istnieć w środowisku bez systemu metalowego, chyba że sam je przeniesiesz.

Są po prostu kodem --- nie różnią się od kodu, który sam piszesz. Jeśli nie złamiesz ich podczas przenoszenia, zrobią to, co zawsze (np. Zatrzymają się na NUL.)

Solomon Slow
źródło
2
Re: „Jeśli wstawisz literał łańcuchowy w swoim programie, zawsze będzie on zakończony NUL”: Czy jesteś tego pewien? Jestem prawie pewien, że (np.) char foo[4] = "abcd";Jest prawidłowym sposobem na utworzenie nie zakończonej zerami tablicy czterech znaków.
ruakh
2
@ruakh, Ups! to przypadek, którego nie rozważałem. Myślałem o dosłownym łańcuchu znaków, który pojawia się w miejscu, w którym oczekuje się char const * wyrażenia . Zapomniałem, że inicjalizatory C mogą czasem przestrzegać różnych reguł.
Solomon Slow
@ruakh Literał łańcuchowy jest zakończony znakiem NUL. Tablica nie jest.
jamesdlin
2
@ruakh masz char[4]. To nie jest struna, ale została zainicjowana z jednego
Caleth
2
@Caleth, „inicjowane z jednego” nie jest czymś, co musi się zdarzyć w czasie wykonywania. Jeśli dodamy słowo kluczowe staticdo przykładu Ruakh, wówczas kompilator może emitować „abcd” nie zakończony NUL do segmentu danych tak, że zmienna jest inicjowana przez moduł ładujący program. Tak więc Ruakh miał rację: istnieje co najmniej jeden przypadek, w którym pojawienie się literału łańcucha w programie nie wymaga od kompilatora emitowania łańcucha zakończonego przez NUL. (ps, właściwie skompilowałem przykład z gcc 5.4.0, a kompilator nie wyemitował NUL.)
Solomon Slow
2

Jak wspomnieli inni, zerowe kończenie ciągów jest konwencją biblioteki standardowej C. Możesz obsługiwać ciągi w dowolny sposób, jeśli nie zamierzasz używać standardowej biblioteki.

Dotyczy to każdego systemu operacyjnego z kompilatorem „C”, a także możesz pisać programy „C”, które nie działają w prawdziwym systemie operacyjnym, jak wspomniałeś w swoim pytaniu. Przykładem może być sterownik drukarki atramentowej, którą kiedyś zaprojektowałem. W systemach wbudowanych obciążenie pamięci systemu operacyjnego może nie być konieczne.

W sytuacjach, w których brakuje pamięci, patrzyłem na cechy mojego kompilatora na przykład na zestaw instrukcji procesora. W aplikacji, w której łańcuchy są często przetwarzane, może być pożądane użycie deskryptorów, takich jak długość łańcucha. Mam na myśli przypadek, w którym procesor jest szczególnie wydajny w pracy z krótkimi przesunięciami i / lub względnymi przesunięciami z rejestrami adresów.

Co jest ważniejsze w Twojej aplikacji: rozmiar i wydajność kodu, czy zgodność z systemem operacyjnym lub biblioteką? Inną kwestią może być łatwość konserwacji. Im bardziej odejdziesz od konwencji, tym trudniej będzie utrzymać kogoś innego.

Hugh Buntu
źródło
1

Inni zajmowali się tym, że w C łańcuchy są w dużej mierze tym, co z nich robisz. Ale wydaje się, że w twoim pytaniu jest pewne zamieszanie z powodu samego terminatora i z jednej perspektywy może to być to, czym martwi się ktoś na twojej pozycji.

Ciągi C są zakończone zerem. Oznacza to, że są one zakończone znakiem null NUL. Nie są zakończone wskaźnikiem zerowym NULL, który jest zupełnie innym rodzajem wartości o zupełnie innym celu.

NULma gwarantowaną zerową wartość całkowitą. W ciągu ciągu będzie również mieć rozmiar podstawowego typu znaku, który zwykle wynosi 1.

NULLnie ma gwarancji, że w ogóle będzie mieć liczbę całkowitą. NULLjest przeznaczony do użycia w kontekście wskaźnika i ogólnie oczekuje się, że będzie miał typ wskaźnika, który nie powinien być konwertowany na znak lub liczbę całkowitą, jeśli twój kompilator jest dobry. Chociaż definicja NULLobejmuje glif 0, nie ma gwarancji, że faktycznie ma tę wartość [1], i chyba że kompilator implementuje stałą jako jeden znak #define(wielu nie, ponieważ NULL tak naprawdę nie powinno mieć znaczenia w kontekst kontekstowy), dlatego nie ma gwarancji, że rozszerzony kod faktycznie zawiera wartość zerową (nawet jeśli myląco wiąże się z glifem zerowym).

Jeśli NULLzostanie wpisany, jest mało prawdopodobne, aby miał rozmiar 1 (lub inny rozmiar znaku). Może to powodować dodatkowe problemy, chociaż rzeczywiste stałe znaków również nie mają większego rozmiaru.

Teraz większość ludzi zobaczy to i pomyśli: „zerowy wskaźnik jako coś innego niż zero-bitowy? Co za nonsens” - ale takie założenia są bezpieczne tylko na popularnych platformach, takich jak x86. Ponieważ wyraźnie wspomniałeś o zainteresowaniu kierowaniem na inne platformy, musisz wziąć to pod uwagę, ponieważ wyraźnie oddzieliłeś swój kod od założeń dotyczących charakteru relacji między wskaźnikami i liczbami całkowitymi.

Dlatego, mimo że łańcuchy C są zakończone zerem, nie są one zakończone przez NULL, ale przez NUL(zwykle zapisywane '\0'). Kod, który jawnie używa NULLjako terminatora łańcucha, będzie działał na platformach o prostej strukturze adresu, a nawet będzie się kompilował z wieloma kompilatorami, ale absolutnie nie jest poprawny C.


[1] rzeczywista wartość wskaźnika zerowego jest wstawiana przez kompilator, gdy odczytuje on 0 token w kontekście, w którym zostałby przekonwertowany na typ wskaźnika. Nie jest to konwersja z wartości całkowitej 0 i nie ma gwarancji, że zostanie zachowana, jeśli 0zostanie użyty inny element niż sam token , na przykład wartość dynamiczna ze zmiennej; konwersja również nie jest odwracalna, a wskaźnik zerowy nie musi dawać wartości 0 po przekształceniu na liczbę całkowitą.

Leushenko
źródło
Świetny punkt Przesłałem poprawkę, aby to wyjaśnić.
Monty Harder
NULgwarantuje się, że liczba całkowita będzie równa zero.” -> C nie definiuje NUL. Zamiast tego C określa, że ​​ciągi mają końcowy znak zerowy , bajt ze wszystkimi bitami ustawionymi na 0.
chux - Przywróć Monikę
1

Używam ciągów w C, co oznacza, że ​​znaki z zakończeniem zerowym nazywane są Ciągami.

Nie będzie mieć żadnych problemów, gdy używasz go w systemie baremetal lub w jakichkolwiek systemach operacyjnych, takich jak Windows, Linux, RTOS: (FreeRTO, OSE).

W świecie osadzonym zakończenie zerowe faktycznie pomaga bardziej tokenować znak jako ciąg.

W wielu systemach krytycznych dla bezpieczeństwa używałem takich ciągów w języku C.

Być może zastanawiasz się, co to właściwie jest string w C?

Ciągi w stylu C, które są tablicami, istnieją również literały ciągów, takie jak „to”. W rzeczywistości oba te typy ciągów to po prostu zbiory postaci siedzących obok siebie w pamięci.

Za każdym razem, gdy piszesz ciąg zamknięty w podwójnych cudzysłowach, C automatycznie tworzy dla nas tablicę znaków, zawierającą ten ciąg, zakończoną znakiem \ 0.

Na przykład możesz zadeklarować i zdefiniować tablicę znaków oraz zainicjować ją ciągiem znaków:

char string[] = "Hello cruel world!";

Prosta odpowiedź: tak naprawdę nie musisz się martwić o użycie znaków z zerowym zakończeniem, działa to niezależnie od platformy.

zwisający wskaźnik
źródło
Dzięki, nie wiedziałem, że po zadeklarowaniu z podwójnymi cudzysłowami, a NULjest automatycznie dołączane.
Snoop,
1

Jak powiedzieli inni, zakończenie zerowe jest dość uniwersalne dla standardu C. Ale (jak zauważyli inni) nie 100%. W (innym) przykładzie system operacyjny VMS zwykle używał tak zwanej „deskryptorów ciągów” http://h41379.www4.hpe.com/commercial/c/docs/5492p012.html dostępny w C przez #include <descrip.h >

Rzeczy na poziomie aplikacji mogą używać zakończenia zerowego lub nie, jednak deweloper uważa to za stosowne. Ale niskiego poziomu VMS absolutnie wymaga deskryptorów, które w ogóle nie używają terminacji zerowej (szczegółowe informacje można znaleźć w powyższym linku). Jest to w dużej mierze tak, że wszystkie języki (C, asembler itp.), Które bezpośrednio używają wewnętrznych VMS mogą mieć wspólny interfejs z nimi.

Więc jeśli spodziewasz się podobnej sytuacji, możesz być bardziej ostrożny, niż może to sugerować „uniwersalne zakończenie zerowe”. Byłbym bardziej ostrożny, gdybym robił to, co robisz, ale dla moich rzeczy na poziomie aplikacji można bezpiecznie założyć zerowe zakończenie. Po prostu nie sugerowałbym ci tego samego poziomu bezpieczeństwa. Twój kod może w pewnym momencie wymagać połączenia z asemblerem i / lub innym kodem języka, który może nie zawsze być zgodny ze standardem C ciągów zakończonych znakiem null.

John Forkosh
źródło
Dzisiaj zakończenie 0 jest w rzeczywistości dość niezwykłe. C ++ std :: string nie, Java String nie, Objective-C NSString nie, Swift String nie - w rezultacie każda biblioteka języków obsługuje łańcuchy z kodami NUL wewnątrz łańcucha (co jest niemożliwe z C ciągi z oczywistych powodów).
gnasher729
@ gnasher729 Zmieniłem „… prawie uniwersalny” na „prawie uniwersalny dla standardu C”, co, mam nadzieję, usuwa wszelkie niejasności i pozostaje poprawne dzisiaj (i to właśnie miałem na myśli, zgodnie z tematem i pytaniem PO).
John Forkosh
0

Z mojego doświadczenia z wbudowanymi, krytycznymi dla bezpieczeństwa systemami czasu rzeczywistego nierzadko zdarza się stosować zarówno konwencje ciągów C, jak i PASCAL, tj. Podać długość ciągów jako pierwszy znak (co ogranicza długość do 255) i zakończyć ciąg z co najmniej jednym 0x00, ( NUL), co zmniejsza użyteczną wielkość do 254.

Jednym z powodów jest to, że wiesz, ile danych oczekujesz po odebraniu pierwszego bajtu, a innym jest to, że w takich systemach unika się dynamicznych rozmiarów buforów, tam gdzie to możliwe - przydzielanie stałego rozmiaru bufora 256 jest szybsze i bezpieczniejsze (nie trzeba sprawdzić, jeśli się mallocnie powiedzie). Innym jest to, że inne systemy, z którymi się komunikujesz, mogą nie być napisane w ANSI-C.

W każdej pracy osadzonej ważne jest ustanowienie i utrzymanie Dokumentu Kontroli Interfejsu (IDC), który definiuje wszystkie struktury komunikacyjne, w tym formaty ciągów, endianness, rozmiary liczb całkowitych itp., Tak szybko, jak to możliwe ( najlepiej przed rozpoczęciem ), i powinien być twoim, a wszystkie zespoły, święta księga pisząc systemu - jeśli ktoś chce wprowadzić nową strukturę i formatowanie to musi być udokumentowane tam pierwszy i każdy, które mogłyby mieć wpływ poinformował, ewentualnie z opcją do zawetowania zmian .

Steve Barnes
źródło