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.
źródło
Odpowiedzi:
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.
źródło
char
tablice zakończone znakiem null ,char
tablice o długości zakodowanej w pierwszym bajcie (powszechnie znane jako „ciągi Pascala”),wchar_t
wersje obu powyżej orazchar
tablice, które łączą obie metody: długość zakodowana w pierwszym bajcie i znak null kończący ciąg.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
NUL
wypowiedzenia 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):NUL
łańcuchów-terminali, wymagając, abyNUL
dopisywać je do literałów łańcuchowych.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. :-)
źródło
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 podobnystd::string
do C ++, który ma swoje własne problemy i ograniczenia.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 pierwszymNUL
znalezieniu i nie będą wiedzieć ani nie przejmować się, że poza nim istnieją dodatkowe znaki.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órymconst char *
oczekiwana jest wartość, kompilator wyemitujefoobar⊘
do const / code segment / section twojego programu, a wartość wyrażenia będzie wskaźnikiem do adresu, w którym zapisałf
znak. (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.)
źródło
char foo[4] = "abcd";
Jest prawidłowym sposobem na utworzenie nie zakończonej zerami tablicy czterech znaków.char const *
wyrażenia . Zapomniałem, że inicjalizatory C mogą czasem przestrzegać różnych reguł.char[4]
. To nie jest struna, ale została zainicjowana z jednegostatic
do 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.)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.
źródło
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 zerowymNULL
, który jest zupełnie innym rodzajem wartości o zupełnie innym celu.NUL
ma gwarantowaną zerową wartość całkowitą. W ciągu ciągu będzie również mieć rozmiar podstawowego typu znaku, który zwykle wynosi 1.NULL
nie ma gwarancji, że w ogóle będzie mieć liczbę całkowitą.NULL
jest 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ż definicjaNULL
obejmuje glif0
, 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
NULL
zostanie 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 przezNUL
(zwykle zapisywane'\0'
). Kod, który jawnie używaNULL
jako 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śli0
zostanie 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ą.źródło
NUL
gwarantuje się, że liczba całkowita będzie równa zero.” -> C nie definiujeNUL
. Zamiast tego C określa, że ciągi mają końcowy znak zerowy , bajt ze wszystkimi bitami ustawionymi na 0.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.
Na przykład możesz zadeklarować i zdefiniować tablicę znaków oraz zainicjować ją ciągiem znaków:
Prosta odpowiedź: tak naprawdę nie musisz się martwić o użycie znaków z zerowym zakończeniem, działa to niezależnie od platformy.
źródło
NUL
jest automatycznie dołączane.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.
źródło
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ę
malloc
nie 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 .
źródło