Jakie jest uzasadnienie dla ciągów zakończonych znakiem null?

Mimo, że kocham C i c++, nie mogę się powstrzymać od podrapania się po głowie przy wyborze null zakończonych ciągów:

  • Length prefixed (tj. Pascal) strings existed before C
  • Ciągi z prefiksem długości przyspieszają kilka algorytmów, umożliwiając wyszukiwanie stałej długości czasu.
  • Ciągi z prefiksem długości utrudniają wywoływanie błędów przekroczenia bufora.
  • nawet na 32-bitowej maszynie, jeśli pozwolisz, aby ciąg był wielkości dostępnej pamięci, a długość poprzedzonego łańcucha jest tylko o trzy bajty szersza niż zakończony łańcuch null. Na maszynach 16-bitowych jest to pojedynczy bajt. Na maszynach 64-bitowych 4GB jest rozsądnym limitem długości łańcucha, ale nawet jeśli chcesz rozszerzyć go do rozmiaru słowa maszynowego, maszyny 64-bitowe zwykle mają wystarczającą ilość pamięci, co czyni dodatkowe siedem bajtów argumentem zerowym. Wiem, że oryginalny standard C został napisany dla szalenie ubogich maszyn (jeśli chodzi o pamięć), ale argument o wydajności mnie nie sprzedaje proszę.
  • Perl, Pascal, Python, Java, C#, itp.) używają ciągów poprzedzonych długością. Języki te zazwyczaj pokonują C w kontrolkach manipulacji ciągami, ponieważ są bardziej wydajne z ciągami.
  • C++ poprawił to nieco za pomocą szablonu std::basic_string, ale zwykłe tablice znaków oczekujące zakończonych znakiem null są nadal wszechobecne. Jest to również niedoskonałe, ponieważ wymaga alokacji sterty.
  • zakończone znakiem Null łańcuchy muszą zarezerwować znak (mianowicie null), które nie mogą istnieć w łańcuchu, podczas gdy ciągi poprzedzone prefiksem length mogą zawierać wbudowane null.

Kilka z tych rzeczy wyszło na jaw niedawno niż C, więc byłoby sensowne, gdyby C O nich nie wiedział. Jednak kilka było jeszcze przed powstaniem C. Dlaczego zamiast prefiksu o oczywiście wyższej długości wybrano zakończone znakiem null?

EDIT : skoro niektórzy pytali o fakty (a nie podobały mi się te, które już podane) z mojej strony powyżej wynika to z kilku rzeczy:

  • Concat używając zakończonych null łańcuchów wymaga O (n + m) złożoności czasu. Prefiks długości często wymaga tylko O (m).
  • długość przy użyciu zakończonych null łańcuchów wymaga O (n) złożoności czasowej. Przedrostek długości to O(1).
  • Length i concat są zdecydowanie najczęstszymi operacjami ciągów. Istnieje kilka przypadków, w których łańcuchy zakończone znakiem null mogą być bardziej wydajne, ale występują one znacznie mniej często.

Z poniższych odpowiedzi wynika, że są to przypadki, w których łańcuchy zakończone znakiem null są bardziej efektywne:

  • Kiedy musisz odciąć początek łańcucha i przekazać go jakiejś metodzie. Tak naprawdę nie można tego zrobić w stałym czasie z prefiksem długości, nawet jeśli możesz zniszczyć oryginalny ciąg znaków, ponieważ prefiks długości prawdopodobnie musi być zgodny z regułami wyrównania.
  • w niektórych przypadkach, gdy po prostu zapętlasz znak po znaku może być w stanie zapisać rejestr procesora. Zauważ, że działa to tylko w przypadku, gdy nie przydzieliłeś dynamicznie ciągu znaków (ponieważ wtedy musiałbyś go zwolnić, wymagając użycia zapisanego rejestru CPU do przechowywania wskaźnika, który pierwotnie otrzymałeś od malloca i znajomych).
Żadne z powyższych nie jest tak powszechne jak długość i Konkatedra.

W odpowiedziach poniżej jest jeszcze jedno:

  • musisz odciąć koniec łańcucha

Ale to jeden jest niepoprawny - jest to taka sama ilość czasu dla zakończonych znakiem null i poprzedzonych prefiksem długości łańcuchów. (Zakończone Null ciągi po prostu przyklejają null tam, gdzie ma być nowy koniec, prefiksy długości po prostu odejmują od prefiksu.)

Author: PointerToConstantChar, 2010-12-11

18 answers

Z ust konia

Żaden z BCPL, B, lub C nie obsługuje dane postaci silnie w język; każdy traktuje struny bardzo jak wektory liczb całkowitych i uzupełnia ogólne zasady przez kilka konwencje. Zarówno w BCPL jak i B a string literal oznacza adres statyczny obszar zainicjowany za pomocą znaki ciągu, zapakowane w komórki. W BCPL pierwszy spakowany bajt zawiera ilość znaków w ciąg; w B nie ma hrabia i ciągi są zakończone przez znak specjalny, który B *e. Zmiana ta została dokonana częściowo aby uniknąć ograniczenia długości ze Sznurka spowodowanego trzymaniem policzyć w gnieździe 8 - lub 9-bitowym, oraz częściowo dlatego, że utrzymanie liczby z naszego doświadczenia wynika, że mniej wygodne niż używanie terminatora.

Dennis M Ritchie, rozwój języka C

 204
Author: Hans Passant,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-11-13 17:32:15

C nie ma ciągu znaków jako części języka. 'String' w C jest tylko wskaźnikiem do znaku. Więc może zadajesz złe pytanie.

" jakie jest uzasadnienie pominięcia typu string " może być bardziej istotne. W tym miejscu chciałbym zwrócić uwagę, że C nie jest językiem zorientowanym obiektowo i ma tylko podstawowe typy wartości. Ciąg znaków jest pojęciem wyższego poziomu, które musi być zaimplementowane przez w jakiś sposób łączenie wartości innych typów. C jest na niższym poziomie abstrakcji.

In światło szalejącego szkwału poniżej:

Chcę tylko zaznaczyć, że nie próbuję powiedzieć, że jest to głupie lub złe pytanie, lub że sposób reprezentowania ciągów C jest najlepszym wyborem. Staram się wyjaśnić, że pytanie byłoby bardziej zwięźle powiedziane, jeśli wziąć pod uwagę fakt, że C nie ma mechanizmu różnicującego łańcuch znaków jako typ danych od tablicy bajtów. Czy jest to najlepszy wybór w świetle mocy przetwarzania i pamięci dzisiejszych komputerów? Pewnie nie. Ale z perspektywy czasu zawsze 20/20 i takie tam :)

 152
Author: Robert S Ciaccio,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2020-06-20 09:12:55

Pytanie jest zadawane jako rzecz Length Prefixed Strings (LPS) vs zero terminated strings (SZ), ale głównie ujawnia korzyści z prefiksów długości. To może wydawać się przytłaczające, ale szczerze mówiąc należy wziąć pod uwagę również wady płyt LP i zalety SZ.

Jak to rozumiem, pytanie może być nawet rozumiane jako tendencyjny sposób na pytanie " Jakie są zalety zerowych zakończonych ciągów ?".

Zalety (widzę) zerowych zakończonych ciągów:

  • bardzo proste, nie trzeba wprowadzać nowych pojęcia w języku, char tablice / wskaźniki znaków mogą to zrobić.
  • język podstawowy zawiera tylko minimalny cukier składniowy do konwersji coś pomiędzy podwójnymi cudzysłowami a bunch of chars (naprawdę bunch of bajtów). W niektórych przypadkach może być stosowany aby zainicjować wszystko całkowicie niezwiązane z tekstem. Na przykład xpm format pliku obrazu jest prawidłowym źródłem C który zawiera dane obrazu zakodowane jako sznurek.
  • przy okazji, możesz umieścić zero w literalnym łańcuchu, kompilator będzie just also dodaj kolejny na końcu literału: "this\0is\0valid\0C". Czy to sznurek ? albo cztery struny ? Albo kilka bajtów...
  • implementacja płaska, bez ukrytego indrection, bez ukrytej liczby całkowitej.
  • brak ukrytych alokacji pamięci (cóż, niektóre niesławne nie standardowe funkcje, takie jak strdup wykonać przydział, ale to głównie źródłem problemu).
  • brak konkretnego problemu dla małego lub dużego sprzętu (wyobraź sobie obciążenie dla Zarządzaj długością przedrostka 32 bitów na 8 mikrokontrolery bits, lub na ograniczenia ograniczenia rozmiaru łańcucha do mniej niż 256 bajtów, to był problem, który miałem z Turbo Pascalem eony temu).
  • implementacja manipulacji łańcuchami to tylko garść bardzo prosta funkcja biblioteczna
  • efektywny dla głównego zastosowania ciągów znaków : stały odczyt tekstu kolejno od znanego początku (głównie wiadomości dla użytkownika).
  • zakończenie zera nie jest nawet obowiązkowe, wszystkie niezbędne narzędzia manipulować charsami jak banda bajty są dostępne. Podczas wykonywania inicjalizacja tablicy w C, można nawet unikaj terminatora NUL. Just ustaw odpowiedni rozmiar. char a[3] = "foo"; jest poprawnym C (nie C++) i nie stawia ostatniego zera w a.
  • spójny z uniksowym punktem widzenia "wszystko jest plikiem", w tym "pliki", które nie mają wewnętrznej długości jak stdin, stdout. Należy pamiętać, że zaimplementowane są Open read and write primitives na bardzo niskim poziomie. Nie są to wywołania biblioteki, ale wywołania systemowe. I ten sam API jest używany dla plików binarnych lub tekstowych. Odczyt plików pobiera adres bufora oraz rozmiar i zwraca nowy rozmiar. I możesz użyć ciągów jako bufora do zapisu. Używanie innego rodzaju strun reprezentacja sugerowałaby, że nie można łatwo użyć literalnego ciągu jako bufora do wyjścia, lub musiałbyś sprawić, by miał bardzo dziwne zachowanie podczas rzucania go do char*. Mianowicie nie zwracać adresu łańcucha, ale zwracać rzeczywiste dane.
  • bardzo łatwe do manipulowania dane tekstowe odczytywane z pliku w miejscu, bez zbędnej kopii bufora, wystarczy wstawić zera w odpowiednich miejscach(cóż, nie do końca w nowoczesnym C, ponieważ podwójne ciągi cytowane są tablicami const char obecnie Zwykle trzymanymi w nie modyfikowanym segmencie danych).
  • poprzedzanie niektórych wartości int o dowolnej wielkości oznaczałoby problemy z wyrównaniem. Początkowy długość powinna być wyrównana, ale nie ma powodu, aby to robić dla danych znaków (i ponownie, wymuszenie wyrównania strun oznaczałoby problemy przy traktowaniu ich jako wiązki z bajtów).
  • długość jest znana w czasie kompilacji dla stałych ciągów literalnych (sizeof). Więc dlaczego miałby czy ktoś chce to zapisać w pamięci ?
  • w sposób, w jaki C działa jak (prawie) każdy inny, łańcuchy są postrzegane jako tablice znaków. Ponieważ długość tablicy nie jest zarządzana przez C, Długość logiczna również nie jest zarządzana dla łańcuchów. Jedyną zaskakującą rzeczą jest to, że 0 element dodany na końcu, ale to tylko na poziomie języka podstawowego podczas wpisywania ciągu między podwójnym cytaty. Użytkownicy mogą doskonale wywoływać funkcje manipulacji ciągiem przekazując długość, lub nawet używać zwykłego memcopy zamiast. SZ są tylko placówką. W większości innych języków długość tablicy jest zarządzana, jest to logiczne, że jest taka sama dla łańcuchów.
  • w dzisiejszych czasach i tak 1-bajtowe zestawy znaków nie są wystarczające i często trzeba mieć do czynienia z zakodowanymi ciągami unicode, w których liczba znaków jest bardzo różna od liczby bajtów. Oznacza to, że użytkownicy będą prawdopodobnie chcieli więcej niż " tylko rozmiar", ale także inne informacje. Zachowanie długości nie daje użytkownikowi nic (szczególnie nie ma naturalnego miejsca do ich przechowywania) w odniesieniu do tych innych przydatnych informacji.

To powiedziawszy, nie ma potrzeby narzekać w rzadkich przypadkach, gdy standardowe ciągi C są rzeczywiście nieefektywne. Libs są dostępne. Jeśli podążałem za tym trendem, powinienem narzekać, że standard C nie zawiera żadnych funkcji wsparcia regex... ale tak naprawdę każdy wie, że to nie jest prawdziwy problem, ponieważ istnieją biblioteki do tego cel. Więc kiedy wydajność manipulacji ciągiem jest pożądana, dlaczego nie użyć biblioteki takiej jak bstring ? A może nawet ciągi C++?

EDIT : ostatnio zajrzałem do D strings . Na tyle interesujące jest to, że wybrane rozwiązanie nie jest ani przedrostkiem rozmiaru, ani zakończeniem zerowym. Podobnie jak w C, dosłowne łańcuchy zamknięte w podwójnych cudzysłowach są po prostu krótką ręką dla niezmiennych tablic znaków, a język ma również słowo kluczowe string, co oznacza, że (niezmienny znak array).

Ale tablice D są znacznie bogatsze niż tablice C. W przypadku tablic statycznych długość jest znana w czasie wykonywania, Więc nie ma potrzeby zapisywania długości. Kompilator ma go w czasie kompilacji. W przypadku tablic dynamicznych długość jest dostępna, ale dokumentacja D nie podaje, gdzie jest przechowywana. Z tego, co wiemy, kompilator może wybrać, aby zachować go w jakimś rejestrze lub w jakiejś zmiennej przechowywanej z dala od danych znaków.

Na zwykłych tablicach znaków lub nie dosłownych łańcuchach nie ma końcowe zero, stąd programista musi to samo umieścić, jeśli chce wywołać jakąś funkcję C z D. w konkretnym przypadku ciągów literalnych, jednak kompilator D nadal umieszcza zero na końcu każdego łańcucha (aby ułatwić rzucanie do ciągów C, aby ułatwić wywołanie funkcji C ?), ale to zero nie jest częścią łańcucha (D nie zlicza go w rozmiarze łańcucha).

Jedyną rzeczą, która mnie nieco rozczarowała, jest to, że ciągi mają być utf-8, ale długość najwyraźniej nadal zwraca kilka bajtów (przynajmniej tak jest w moim kompilatorze gdc) nawet przy użyciu znaków wielobajtowych. Nie jest dla mnie jasne, czy jest to błąd kompilatora, czy cel. (OK, prawdopodobnie dowiedziałem się, co się stało. Aby powiedzieć kompilatorowi D, że twoje źródło używa utf - 8, musisz na początku umieścić jakiś głupi znak kolejności bajtów. Piszę głupio, bo wiem, że edytor tego nie robi, zwłaszcza dla UTF-8, który ma być zgodny z ASCII).

 112
Author: kriss,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-05-26 12:44:14

Myślę, że ma to uzasadnienie historyczne i znalazłem to w Wikipedii:

W czasie C (i języków, które pochodzi z) zostały opracowane, pamięć była niezwykle ograniczona, więc używanie tylko jeden bajt nad głową do przechowywania długość sznurka była atrakcyjna. Na jedyna wówczas popularna alternatywa, zwykle nazywany " ciągiem Pascala" (choć używane również przez wczesne wersje BASIC), używał wiodącego bajtu do przechowywania długość sznurka. Pozwala to na ciąg zawierający NUL i wykonany znalezienie długości wymaga tylko jednego dostęp do pamięci(O (1) (Stały) czas). Ale jeden bajt ogranicza długość do 255. To ograniczenie długości było znacznie bardziej restrykcyjne niż problemy z Ciąg C, czyli ogólnie ciąg C wygrałem.

 64
Author: khachik,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-01-19 22:09:20

Calavera jest W porządku , ale ponieważ ludzie nie rozumieją jego punktu, podam kilka przykładów kodu.

Najpierw zastanówmy się, czym jest C: prostym językiem, w którym cały kod ma dość bezpośrednie tłumaczenie na język maszynowy. Wszystkie typy mieszczą się w rejestrach i na stosie, i nie wymaga systemu operacyjnego ani dużej biblioteki uruchomieniowej do uruchomienia, ponieważ były przeznaczone do zapisu tych rzeczy (zadanie, do którego doskonale się Nadaje, biorąc pod uwagę, że jest to nie jest nawet prawdopodobnym konkurentem do dziś).

Gdyby C miał typ string, taki jak int lub char, byłby to typ, który nie mieściłby się w rejestrze ani w stosie i wymagałby alokacji pamięci (wraz z całą infrastrukturą wspierającą) do obsługi w jakikolwiek sposób. Wszystko to wbrew podstawowym zasadom C.

Więc ciąg w C to:

char s*;

Przyjmijmy więc, że były to prefiksy długości. Napiszmy kod łączący dwa ciągi:

char* concat(char* s1, char* s2)
{
    /* What? What is the type of the length of the string? */
    int l1 = *(int*) s1;
    /* How much? How much must I skip? */
    char *s1s = s1 + sizeof(int);
    int l2 = *(int*) s2;
    char *s2s = s2 + sizeof(int);
    int l3 = l1 + l2;
    char *s3 = (char*) malloc(l3 + sizeof(int));
    char *s3s = s3 + sizeof(int);
    memcpy(s3s, s1s, l1);
    memcpy(s3s + l1, s2s, l2);
    *(int*) s3 = l3;
    return s3;
}

Inną alternatywą byłoby użycie struktury do zdefiniowania ciągu znaków:

struct {
  int len; /* cannot be left implementation-defined */
  char* buf;
}

W tym momencie, każda manipulacja łańcuchami wymagałaby dwóch przydziałów, co w praktyce oznacza, że przechodziłbyś przez bibliotekę, aby ją obsługiwać.

Najśmieszniejsze jest to... struktury takie jak czy istnieją w C! Nie są one po prostu używane do codziennego wyświetlania wiadomości do obsługi użytkownika.

Oto punkt, w którym Calavera: nie ma typu string w C . Aby cokolwiek z tym zrobić, trzeba wziąć wskaźnik i dekodować go jako wskaźnik do dwóch różnych typów, a następnie staje się bardzo istotne, jaki jest rozmiar łańcucha, i nie może być po prostu pozostawiony jako "implementation defined".

Teraz C Może obsłużyć pamięć w każdym razie, A mem funkcje w bibliotece (w <string.h>, nawet!) zapewnij wszystkie narzędzia potrzebne do obsługi pamięci jako parę wskaźnika i rozmiaru. Tzw. "struny" w języku C zostały stworzone tylko w jednym celu: Pokazywanie wiadomości w kontekście pisania systemu operacyjnego przeznaczonego dla terminali tekstowych. I do tego wystarczy zerowe zakończenie.

 33
Author: Daniel C. Sobral,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 11:54:59

Oczywiście dla wydajności i bezpieczeństwa, będziesz chciał zachować długość łańcucha podczas pracy z nim, a nie wielokrotnie wykonywać strlen lub odpowiednik na nim. Jednak przechowywanie długości w ustalonym miejscu tuż przed zawartością łańcucha jest niesamowicie złym projektem. Jak zauważył Jörgen w komentarzach do odpowiedzi Sanjita, wyklucza to traktowanie ogona łańcucha jako łańcucha, co na przykład uniemożliwia wiele typowych operacji, takich jak path_to_filename lub filename_to_extension bez przydzielanie nowej pamięci (oraz możliwość wystąpienia awarii i obsługi błędów). I oczywiście jest problem, że nikt nie może się zgodzić, ile bajtów powinno zajmować pole długości łańcucha (wiele złych języków "Pascal string" używało pól 16-bitowych lub nawet 24-bitowych, które wykluczają przetwarzanie długich łańcuchów).

Projekt C pozwalający programiście wybrać, czy/gdzie / jak przechowywać długość jest znacznie bardziej elastyczny i wydajny. Ale oczywiście programista musi być mądry. C karze głupotę za pomocą programów, które się rozbijają, zatrzymują lub dają wrogom korzenie.

 21
Author: R.. GitHub STOP HELPING ICE,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-12-11 22:10:58

Leniwość, oszczędność rejestracji i przenośność, biorąc pod uwagę gut asemblacji dowolnego języka, zwłaszcza C, który jest o jeden krok powyżej assembly (dziedzicząc wiele kodu dziedziczonego assembly). Zgodziłbyś się, że znak null byłby bezużyteczny w tych dniach ASCII (i prawdopodobnie tak dobry jak znak kontrolny EOF).

Zobaczmy w pseudo kodzie

function readString(string) // 1 parameter: 1 register or 1 stact entries
    pointer=addressOf(string) 
    while(string[pointer]!=CONTROL_CHAR) do
        read(string[pointer])
        increment pointer

Total 1 Register use

Przypadek 2

 function readString(length,string) // 2 parameters: 2 register used or 2 stack entries
     pointer=addressOf(string) 
     while(length>0) do 
         read(string[pointer])
         increment pointer
         decrement length

RAZEM 2 używane rejestry

To może wydawać się krótkowzroczne. czas, ale biorąc pod uwagę oszczędność w kodzie i rejestrze (które były PREMIUM w tym czasie, kiedy wiesz, używają karty dziurkowanej ). Dzięki temu, że był szybszy ( gdy prędkość procesora można było policzyć w kHz), Ten "Hack" był całkiem dobry i przenośny, aby bez rejestracji procesor z łatwością.

Dla argumentacji zaimplementuję 2 common string operation

stringLength(string)
     pointer=addressOf(string)
     while(string[pointer]!=CONTROL_CHAR) do
         increment pointer
     return pointer-addressOf(string)

Złożoność O (N), gdzie w większości przypadków łańcuch Pascala jest O(1), ponieważ długość łańcucha jest wstępnie przypisana do łańcucha struktura (oznaczałoby to również, że operacja ta musiałaby być przeprowadzona na wcześniejszym etapie).

concatString(string1,string2)
     length1=stringLength(string1)
     length2=stringLength(string2)
     string3=allocate(string1+string2)
     pointer1=addressOf(string1)
     pointer3=addressOf(string3)
     while(string1[pointer1]!=CONTROL_CHAR) do
         string3[pointer3]=string1[pointer1]
         increment pointer3
         increment pointer1
     pointer2=addressOf(string2)
     while(string2[pointer2]!=CONTROL_CHAR) do
         string3[pointer3]=string2[pointer2]
         increment pointer3
         increment pointer1
     return string3

Złożoność O (n) i poprzedzanie długości łańcucha nie zmieniłoby złożoności operacji, podczas gdy przyznaję, że zajęłoby to 3 razy mniej czasu.

Z drugiej strony, jeśli używasz Pascala string, będziesz musiał przeprojektować swoje API, biorąc pod uwagę długość rejestru kont i BIT-endianness, Pascal string dostał dobrze znane ograniczenie 255 char (0xFF) beacause długość była przechowywany w 1 bajcie (8 bitów), i to, że chciałeś dłuższy łańcuch znaków (16 bitów->cokolwiek), musisz wziąć pod uwagę architekturę w jednej warstwie kodu, co oznaczałoby w większości przypadków niekompatybilny Łańcuch API, jeśli chcesz dłuższy łańcuch.

Przykład:

Jeden plik został napisany z API łańcucha na 8-bitowym komputerze, a następnie musiałby być odczytany na powiedzmy 32-bitowym komputerze, co zrobiłby leniwy program, który uważa, że Twoje 4bajty są długością łańcucha wtedy przydziel tę ilość pamięci, a następnie spróbuj odczytać tyle bajtów. Innym przypadkiem byłby PPC 32 bajtowy łańcuch odczytywany (little endian) na x86 (big endian), oczywiście jeśli nie wiesz, że jeden jest napisany przez drugiego, pojawiłyby się problemy. Długość 1 bajtu (0x00000001) zmieniłaby się w 16777216 (0x0100000), czyli 16 MB do odczytu ciągu 1 bajtu. Oczywiście można powiedzieć, że ludzie powinni zgodzić się na jeden standard, ale nawet 16-bitowy unicode ma małą i dużą endianness.

Oczywiście, że C ma swoje kwestie również, ale, będzie bardzo mało dotknięte przez kwestie podniesione tutaj.

 14
Author: dvhh,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-12-13 03:29:29

Pod wieloma względami C było prymitywne. I bardzo mi się podobało.

Był to krok powyżej języka asemblowania, dający prawie taką samą wydajność z językiem, który był znacznie łatwiejszy do napisania i utrzymania.

Terminator null jest prosty i nie wymaga specjalnego wsparcia ze strony języka.

Patrząc wstecz, nie wydaje się to wygodne. Ale używałem języka montażu w latach 80-tych i wydawało się to bardzo wygodne w tym czasie. Po prostu uważam, że oprogramowanie ciągle się rozwija, a platformy i narzędzia stają się coraz bardziej wyrafinowane.

 10
Author: Jonathan Wood,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-12-11 23:02:16

Zakładając przez chwilę, że C zaimplementował ciągi znaków w sposób Pascala, poprzedzając je długością: czy łańcuch o długości 7 znaków jest tym samym typem danych, co łańcuch o długości 3 znaków? Jeśli odpowiedź brzmi tak, to jaki kod powinien wygenerować kompilator, gdy przypisuję pierwszy do drugiego? Czy łańcuch powinien być obcięty, czy automatycznie zmieniany rozmiar? W przypadku zmiany rozmiaru, czy ta operacja powinna być zabezpieczona blokadą, aby była bezpieczna? Strona z podejściem C postawiła wszystkie te kwestie, czy ci się to podoba czy nie:)

 8
Author: Cristian,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-12-12 04:26:17

Jakoś zrozumiałem, że pytanie sugeruje, że nie ma wsparcia dla kompilatorów dla ciągów z prefiksem długości w C. poniższy przykład pokazuje, że przynajmniej możesz uruchomić własną bibliotekę ciągów C, gdzie długości łańcuchów są liczone w czasie kompilacji, z konstrukcją taką jak:

#define PREFIX_STR(s) ((prefix_str_t){ sizeof(s)-1, (s) })

typedef struct { int n; char * p; } prefix_str_t;

int main() {
    prefix_str_t string1, string2;

    string1 = PREFIX_STR("Hello!");
    string2 = PREFIX_STR("Allows \0 chars (even if printf directly doesn't)");

    printf("%d %s\n", string1.n, string1.p); /* prints: "6 Hello!" */
    printf("%d %s\n", string2.n, string2.p); /* prints: "48 Allows " */

    return 0;
}

Nie spowoduje to jednak żadnych problemów, ponieważ trzeba uważać, kiedy konkretnie zwolnić ten wskaźnik Łańcuchowy i kiedy jest on przydzielany statycznie (literalna tablica char).

Edit: jako bardziej bezpośrednia odpowiedź na pytanie, moim zdaniem był to sposób, w jaki C może obsługiwać zarówno o długości łańcucha dostępnego (jako stała czasowa kompilacji), jeśli jest to potrzebne, ale nadal bez napowietrznej pamięci, jeśli chcesz używać tylko wskaźników i zerowego zakończenia.

Oczywiście wydaje się, że praca z łańcuchami zakończonymi zerowo była zalecaną praktyką, ponieważ biblioteka standardowa w ogóle nie przyjmuje długości łańcuchów jako argumentów, a ponieważ wydobywanie długości nie jest tak prostym kodem, jak char * s = "abc", Jak pokazuje mój przykład.

 8
Author: Pyry Jahkola,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-12-12 08:19:09

"nawet na maszynie 32-bitowej, jeśli pozwolisz, aby łańcuch był wielkości dostępnej pamięci, łańcuch o długości poprzedzonej jest tylko o trzy bajty szerszy niż łańcuch zakończony znakiem null."

Po pierwsze, dodatkowe 3 bajty mogą być znaczne narzuty dla krótkich łańcuchów. W szczególności ciąg o zerowej długości zajmuje teraz 4 razy więcej pamięci. Niektórzy z nas używają maszyn 64-bitowych, więc albo potrzebujemy 8 bajtów, aby zapisać ciąg o zerowej długości, albo format ciągów nie radzi sobie z najdłuższymi ciągami podpory platformy.

Mogą być również problemy z dostosowaniem do potrzeb. Załóżmy, że mam blok pamięci zawierający 7 ciągów, jak "solo\0second\0\0four\0five\0 \ 0seventh". Drugi ciąg zaczyna się od przesunięcia 5. Sprzęt może wymagać wyrównania 32-bitowych liczb całkowitych pod adresem wielokrotności 4, więc musisz dodać wypełnienie, zwiększając narzut jeszcze bardziej. Reprezentacja C jest bardzo wydajna w porównaniu z pamięcią. (Wydajność pamięci jest dobra; wspomaga wydajność pamięci podręcznej, na przykład.)

 6
Author: Brangdon,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-07-23 12:45:26

Zakończenie null pozwala na szybkie operacje oparte na wskaźnikach.

 4
Author: Sanjit Saluja,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-12-11 20:22:05

Jeszcze nie wspomniano o jednym punkcie: kiedy projektowano C, było wiele maszyn, w których 'znak' nie był ośmiobitowy (nawet dziś są Platformy DSP, na których go nie ma). Jeśli ktoś zdecyduje, że ciągi mają być z prefiksem length, to ile 'char' jest wart z prefiksu length należy użyć? Użycie dwóch narzuciłoby sztuczne ograniczenie długości łańcucha dla maszyn z 8-bitowym znakiem i 32-bitową przestrzenią adresacyjną, podczas gdy marnowanie miejsca na maszynach z 16-bitowym znakiem i 16-bitową przestrzenią adresacyjną.

Jeśli jeden chciał, aby łańcuchy o dowolnej długości były przechowywane efektywnie, a jeśli 'char' były zawsze 8-bitowe, można by--dla pewnego kosztu szybkości i rozmiaru kodu -- zdefiniować schemat, w którym łańcuch poprzedzony liczbą parzystą N będzie miał długość N/2 bajtów, łańcuch poprzedzony nieparzystą wartością N i parzystą wartością M (odczyt wstecz) może być ((N-1) + m*char_max) / 2, itd. i wymagają, aby każdy bufor, który twierdzi, że oferuje pewną ilość miejsca do przechowywania ciągu znaków, musiał pozwolić wystarczającej ilości bajtów poprzedzających tę przestrzeń do obsługi maksymalna długość. Jednak fakt, że' char 'nie zawsze ma 8 bitów, skomplikowałby taki schemat, ponieważ liczba 'char' wymagana do przechowywania ciągu znaków różniłaby się w zależności od architektury procesora.

 4
Author: supercat,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-01-25 16:12:57

Wiele decyzji projektowych dotyczących C wynika z faktu, że w momencie jego implementacji przekazywanie parametrów było nieco kosztowne. Biorąc pod uwagę wybór między np.

void add_element_to_next(arr, offset)
  char[] arr;
  int offset;
{
  arr[offset] += arr[offset+1];
}

char array[40];

void test()
{
  for (i=0; i<39; i++)
    add_element_to_next(array, i);
}

Kontra

void add_element_to_next(ptr)
  char *p;
{
  p[0]+=p[1];
}

char array[40];

void test()
{
  int i;
  for (i=0; i<39; i++)
    add_element_to_next(arr+i);
}
Ten ostatni byłby nieco tańszy (a zatem preferowany), ponieważ wymagał podania tylko jednego parametru, a nie dwóch. Jeśli wywołana metoda nie musi znać adresu bazowego tablicy ani indeksu w jej obrębie, podanie pojedynczego wskaźnika łączącego te dwie wartości byłoby tańsze niż przekazywanie wartości osobno.

Chociaż istnieje wiele rozsądnych sposobów, w jakie C mogło zakodować długości łańcuchów, podejścia, które zostały wymyślone do tego czasu, będą miały wszystkie wymagane funkcje, które powinny być w stanie pracować z częścią łańcucha, aby zaakceptować podstawowy adres łańcucha i pożądany indeks jako dwa oddzielne parametry. Użycie zerowego bajtu umożliwiło uniknięcie tego wymogu. Chociaż inne podejścia byłyby lepsze z dzisiejszym maszyny (nowoczesne Kompilatory często przekazują parametry w rejestrach, a memcpy mogą być zoptymalizowane w sposób, w jaki StrCpy ()-odpowiedniki nie mogą) wystarczająco dużo kodu produkcyjnego używa zakończonych zero bajtami łańcuchów, że trudno jest zmienić je na cokolwiek innego.

PS--w zamian za lekką karę prędkości przy niektórych operacjach i odrobinę dodatkowego obciążenia na dłuższych strunach, byłoby możliwe, aby metody pracujące z strunami akceptowały wskaźniki bezpośrednio do strun, bounds-checked string bufory lub struktury danych identyfikujące podciągi innego ciągu. Funkcja taka jak "strcat" wyglądałaby jak [nowoczesna składnia]

void strcat(unsigned char *dest, unsigned char *src)
{
  struct STRING_INFO d,s;
  str_size_t copy_length;

  get_string_info(&d, dest);
  get_string_info(&s, src);
  if (d.si_buff_size > d.si_length) // Destination is resizable buffer
  {
    copy_length = d.si_buff_size - d.si_length;
    if (s.src_length < copy_length)
      copy_length = s.src_length;
    memcpy(d.buff + d.si_length, s.buff, copy_length);
    d.si_length += copy_length;
    update_string_length(&d);
  }
}

Nieco większy niż metoda K & R strcat, ale obsługiwałby sprawdzanie granic, czego metoda K & R nie ma. ponadto, w przeciwieństwie do obecnej metody, możliwe byłoby łatwe łączenie dowolnego podłańcucha, np.

/* Concatenate 10th through 24th characters from src to dest */

void catpart(unsigned char *dest, unsigned char *src)
{
  struct SUBSTRING_INFO *inf;
  src = temp_substring(&inf, src, 10, 24);
  strcat(dest, src);
}

Zauważ, że czas życia łańcucha zwracanego przez temp_substring będzie ograniczony przez wartości s i src, które zawsze były krótsze (dlatego metoda wymaga podania inf - gdyby była lokalna, umarłaby po powrocie metody).

Jeśli chodzi o koszt pamięci, ciągi i bufory do 64 bajtów miałyby jeden bajt narzutu (tak samo jak ciągi zakończone zero); dłuższe ciągi miałyby nieco więcej (niezależnie od tego, czy dozwolone są ilości narzutu między dwoma bajtami, a wymagane maksimum byłoby wymianą czasu/przestrzeni). Specjalną wartością bajtu length / mode będzie służy do wskazania, że funkcja łańcuchowa otrzymała strukturę zawierającą bajt flagi, wskaźnik i długość bufora(który mógł następnie indeksować dowolnie do dowolnego innego łańcucha).

Oczywiście, K&r Nie zaimplementował niczego takiego, ale najprawdopodobniej dlatego, że nie chcieli poświęcać wiele wysiłku na obsługę łańcuchów--obszar, w którym nawet dzisiaj wiele języków wydaje się raczej anemiczne.

 2
Author: supercat,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-03-06 04:24:02

Według Joela Spolsky ' ego w ten wpis na blogu ,

To dlatego, że mikroprocesor PDP-7, na którym wynaleziono UNIX i język programowania C, miał Typ asciz string. ASCIZ oznaczało " ASCII z (zero) na końcu."

Po obejrzeniu wszystkich innych odpowiedzi tutaj, jestem przekonany, że nawet jeśli to prawda, to jest to tylko część powodu dla C zakończone null "strings". Ten post jest dość pouczający, jak proste rzeczy, takie jak sznurki mogą właściwie to będzie ciężko.

 2
Author: BenK,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-06-24 06:11:04

Nie uzasadnienie koniecznie ale kontrapunkt do długości-zakodowane

  1. Niektóre formy kodowania długości dynamicznej są lepsze od kodowania długości statycznej, jeśli chodzi o pamięć, wszystko zależy od użycia. Wystarczy spojrzeć na UTF-8 Dla dowodu. Jest to w zasadzie rozszerzalna tablica znaków do kodowania pojedynczego znaku. Dla każdego rozszerzonego bajtu używa się pojedynczego bitu. Zakończenie NUL używa 8 bitów. Długość-prefiks myślę, że można rozsądnie określić nieskończoną długość również przy użyciu 64 bitów. To, jak często trafisz w przypadku dodatkowych bitów, jest decydującym czynnikiem. Tylko 1 bardzo duży sznurek? Kogo obchodzi, czy używasz 8 czy 64 bitów? Wiele małych strun (czyli strun angielskich słów)? Wtedy koszty prefiksu są dużym procentem.

  2. Struny z prefiksem długości pozwalające na oszczędność czasu to nie jest prawdziwa rzecz . Niezależnie od tego, czy dostarczone dane muszą mieć podaną długość, liczysz w czasie kompilacji, czy naprawdę jesteś dostarczany dynamicznie dane, które należy zakodować jako ciąg znaków. Wielkości te są obliczane w pewnym momencie algorytmu. Można podać oddzielną zmienną przechowującą rozmiar zakończonego znakiem null łańcucha . Co sprawia, że porównanie oszczędności czasu jest dyskusyjne. Jeden ma tylko dodatkowe NUL na końcu... ale jeśli długość kodowania nie zawiera tego NUL, to nie ma dosłownie żadnej różnicy między tymi dwoma. Nie jest wymagana żadna zmiana algorytmu. Tylko pre-pass musisz ręcznie zaprojektować sam zamiast mając kompilator / runtime zrobić to za Ciebie. C polega głównie na ręcznym robieniu rzeczy.

  3. Długość - prefiks jest opcjonalny jest punktem sprzedaży. Nie zawsze potrzebuję tych dodatkowych informacji dla algorytmu, więc bycie wymaganym do zrobienia tego dla każdego ciągu sprawia, że mój czas precompute+compute nigdy nie jest w stanie spaść poniżej O(n). (Tj. sprzętowy generator liczb losowych 1-128. Mogę wyciągnąć z "nieskończonego Sznurka". Powiedzmy, że generuje tylko znaki tak szybko. Więc nasza długość sznurka zmienia się cały czas. Ale mój użycie danych prawdopodobnie nie obchodzi ile losowych bajtów mam. Po prostu chce następnego dostępnego nieużywanego bajtu, jak tylko może go uzyskać po żądaniu. Mogę czekać na urządzenie. Ale mogę też mieć bufor znaków wstępnie odczytanych. Porównanie długości jest niepotrzebnym marnotrawstwem obliczeń. Sprawdzenie null jest bardziej efektywne.)

  4. Długość-prefiks jest dobrym zabezpieczeniem przed przepełnieniem bufora? Podobnie jest z rozsądnym wykorzystaniem funkcji bibliotecznych i ich implementacją. Co jeśli zdam w wadach data? Mój bufor ma 2 bajty, ale mówię funkcji, że jest 7! Ex: Jeśligets () było przeznaczone do użycia na znanych danych, mogło mieć wewnętrzne sprawdzenie bufora, które testowało skompilowane bufory imalloc () wywołania i nadal podążać za specyfikacją. Jeśli miał być używany jako rura dla nieznanego STDIN, aby dotrzeć do nieznanego bufora, to wyraźnie nie można wiedzieć o rozmiarze bufora, co oznacza, że długość arg jest bezcelowa, potrzebujesz czegoś innego, jak sprawdzenie kanarka. Jeśli o to chodzi, nie możesz długości-prefiks niektórych strumieni i wejść, po prostu nie możesz. co oznacza, że sprawdzanie długości musi być wbudowane w algorytm, a nie magiczną część systemu typowania. TL;DR nul-zakończony nigdy nie musiał być niebezpieczny, po prostu zakończył się w ten sposób przez niewłaściwe użycie.

  5. Counter-counter point: nul-termination jest denerwujące na binarnym. Musisz albo zrobić prefiks długości, albo przekształcić bajty NUL w jakiś sposób: escape-codes, range remapping, itp... co oczywiście oznacza more-memory-usage/reduced-information / more-operations-per-byte. Długość-prefiks najczęściej wygrywa tutaj wojnę. Jedyną zaletą przekształcenia jest to, że nie trzeba pisać żadnych dodatkowych funkcji, aby pokryć Ciągi z prefiksem długości. Co oznacza, że na Twoich bardziej zoptymalizowanych procedurach sub-O (n) możesz je automatycznie działać jako ich odpowiedniki O(n) bez dodawania więcej kodu. Minusem jest oczywiście strata czasu/pamięci / kompresji w przypadku użycia na ciężkich strunach NUL. w zależności od tego, ile z twojej Biblioteki kończy się duplikacją, aby operować na danych binarnych, może mieć sens praca wyłącznie z ciągami z prefiksem długości. To powiedziawszy, można również zrobić to samo z ciągami z prefiksem długości... -1 długość może oznaczać zakończenie NUL i możesz użyć zakończonych NUL łańcuchów wewnątrz zakończonych length.

  6. Concat: "O(n+M) vs O (m)" zakładam, że odnosisz się do m jako całkowitej długości łańcucha po konkatenacji, ponieważ obie muszą mieć taką liczbę operacji minimum (ty nie możesz po prostu przyczepić się do sznurka 1, co jeśli będziesz musiał ponownie przyczepić?). Zakładam, że n to mityczna ilość operacji, których nie musisz już wykonywać z powodu wstępnego obliczenia. Jeśli tak, to odpowiedź jest prosta: wstępnie Oblicz. Jeśli nalegasz, że zawsze będziesz miał wystarczająco dużo pamięci, aby nie musieć reallocować i to jest podstawa notacji big-O, wtedy odpowiedź jest jeszcze prostsza: wykonaj wyszukiwanie binarne na przydzielonej pamięci dla końca ciągu 1, Oczywiście istnieje duża próbka nieskończonych zer po string 1 dla nas, aby nie martwić się o realloc. Tam, łatwo dostać n do log(n) i ledwo próbowałem. Który, Jeśli przypomnisz sobie log (n) jest w zasadzie tylko tak duży jak 64 na prawdziwym komputerze, co jest zasadniczo jak mówienie O(64+M), które jest zasadniczo O (m). (I tak, że logika została użyta w analizie czasu wykonywania rzeczywistych struktur danych w użyciu dzisiaj. To nie jest gówno z głowy.)

  7. Concat () / Len() ponownie : Zapamiętaj wyniki. Spokojnie. Zamienia wszystkie obliczenia na wstępnie oblicza, jeśli to możliwe/konieczne. Jest to decyzja algorytmiczna. To nie jest wymuszone ograniczenie języka.

  8. Przekazywanie przyrostka jest łatwiejsze/możliwe z zakończeniem NUL. W zależności od tego, jak zaimplementowany jest prefiks length, może być destrukcyjny na oryginalnym łańcuchu, a czasami nawet nie jest to możliwe. Wymaga kopii i podania O (n) zamiast O (1).

  9. Argument-passing / de-reference is less for nul-terminated versus length-prefix. Oczywiście, ponieważ przekazujesz mniej informacji. Jeśli nie potrzebujesz długości, oszczędza to dużo miejsca i umożliwia optymalizację.

  10. Możesz oszukiwać. To tylko wskazówka. Kto powiedział, że musisz to czytać jako ciąg znaków? Co zrobić, jeśli chcesz go odczytać jako pojedynczy znak lub float? Co zrobić, jeśli chcesz zrobić odwrotnie i odczytać float jako ciąg znaków? Jeśli jesteś ostrożny, możesz to zrobić z zerowym zakończeniem. Nie można tego zrobić z prefiksem length - jest to typ danych wyraźnie różniący się od wskaźnik typowo. Najprawdopodobniej będziesz musiał zbudować łańcuch bajt po bajcie i uzyskać długość. Oczywiście, jeśli chcesz czegoś w rodzaju całego float (prawdopodobnie ma w sobie NUL), i tak musisz czytać bajty po bajtach, ale o szczegółach decydujesz.

TL; DR Czy używasz danych binarnych? Jeśli nie, to nul-termination pozwala na większą swobodę algorytmiczną. Jeśli tak, to ilość kodu vs szybkość / pamięć / kompresja jest twoim głównym problemem. Mieszanka tych dwóch podejście lub memoizacja może być najlepsza.

 2
Author: Black,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-08-28 03:13:55

Nie kupuję odpowiedzi "C nie ma sznurka". To prawda, że C nie obsługuje wbudowanych typów wyższego poziomu, ale nadal można reprezentować struktury danych w C i tym jest łańcuch znaków. Fakt, że ciąg znaków jest tylko wskaźnikiem w C nie oznacza, że pierwsze n bajtów nie może przybrać specjalnego znaczenia jako długość.

Programiści Windows/COM będą bardzo zaznajomieni z typem BSTR, który jest Dokładnie w taki sposób - ciąg C z prefiksem długości, w którym rzeczywiste dane znakowe zaczynają się nie od bajtu 0.

Wydaje się więc, że decyzja o użyciu zakończenia zerowego jest po prostu tym, co ludzie preferują, a nie koniecznością języka.

 1
Author: Mr. Boy,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2020-02-11 19:04:26

Gcc akceptuje poniższe kody:

Char s [4] = "abcd";

I jest ok, jeśli traktujemy is jako tablicę znaków, ale nie string. Oznacza to, że możemy uzyskać do niego dostęp za pomocą s[0], s[1], s[2] I s[3], a nawet za pomocą memcpy(dest, s, 4). Ale będziemy mieć niechlujne znaki, gdy spróbujemy z puts( s), lub gorzej z strcpy (dest, s).

 -3
Author: kkaaii,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-06-20 01:21:12