Що обґрунтовує нульові завершені рядки?


281

Наскільки я люблю C і C ++, я не можу не почухати голову при виборі нульових завершених рядків:

  • Попередньо встановлені (тобто Pascal) рядки існували до C
  • Префіксні рядки по довжині роблять кілька алгоритмів швидшими, дозволяючи шукати постійну тривалість часу.
  • Попередньо встановлені рядки по довжині ускладнюють помилки перевиконання буфера.
  • Навіть на 32-бітовій машині, якщо ви дозволяєте рядку мати розмір доступної пам'яті, строка з попередньою фіксованою довжиною лише на три байти ширше, ніж нульовий завершений рядок. На 16-бітних машинах це один байт. На 64-бітних машинах 4 Гб є розумним обмеженням довжини рядків, але навіть якщо ви хочете розширити його до розміру машинного слова, 64-бітні машини, як правило, мають достатньо пам'яті, роблячи додаткові сім байтів на зразок нульового аргументу. Я знаю, що оригінальний стандарт C був написаний для шалено бідних машин (з точки зору пам'яті), але аргумент ефективності мене тут не продає.
  • Практично будь-яка інша мова (наприклад, Perl, Pascal, Python, Java, C # тощо) використовує рядки з попередньою приставкою. Ці мови, як правило, б'ють С у орієнтирах маніпулювання рядками, оскільки вони ефективніші за допомогою рядків.
  • C ++ дещо виправив це за допомогою std::basic_stringшаблону, але масиви простих символів, які очікують, що нульові завершені рядки все ще поширені. Це також недосконало, оскільки воно вимагає виділення купи.
  • Нульові завершені рядки повинні резервувати символ (а саме - null), який не може існувати в рядку, тоді як рядки з попередньою приставкою по довжині можуть містити вбудовані нулі.

Деякі з цих речей з’явилися на світ останнім часом, ніж C, тому для С було б сенс не знати про них. Однак декілька були очевидними задовго до появи С. Чому замість очевидно переважних префіксів довжини було обрано нульові закінчені рядки?

EDIT : Оскільки деякі запитували факти (і не сподобалися ті, які я вже надав) щодо моєї точки зору ефективності, вони випливають із кількох речей:

  • Concat з використанням нульових кінцевих рядків вимагає складності часу O (n + m). Префіксація по довжині часто вимагає лише O (м).
  • Довжина з використанням нульових кінцевих рядків вимагає O (n) часової складності. Префіксація довжини - O (1).
  • Довжина та лаконічність - на сьогоднішній день найбільш поширені струнні операції. Є кілька випадків, коли нульові завершені рядки можуть бути більш ефективними, але вони трапляються набагато рідше.

З наведених нижче відповідей, це деякі випадки, коли нульові завершені рядки є більш ефективними:

  • Коли потрібно відрізати початок рядка і потрібно передати його якомусь методу. Ви дійсно не можете це робити в постійний час з префіксом довжини, навіть якщо вам дозволяється знищити початковий рядок, оскільки, префікс довжини, ймовірно, повинен дотримуватися правил вирівнювання.
  • У деяких випадках, коли ви просто перебираєте символи рядка за символом, можливо, ви зможете зберегти регістр процесора. Зауважте, що це працює лише у випадку, якщо ви не динамічно виділили рядок (Тому що тоді вам доведеться звільнити її, вимагаючи використання цього реєстру CPU, який ви зберегли, щоб утримувати покажчик, який ви спочатку отримали від malloc та друзів).

Жодне з перерахованого вище майже не таке поширене, як довжина і лаконічність.

У відповідях нижче є ще один твердження:

  • Потрібно відрізати кінець рядка

але це невірно - це стільки ж часу для нульових завершених і довжиною префіксів рядків. (Строкові завершені рядки просто приклеюють нуль там, де ви хочете, щоб був новий кінець, префіксатори довжини просто віднімають від префікса.)


110
Я завжди вважав це обрядом проходження для всіх програмістів на C ++, щоб написати власну бібліотеку струн.
Джульєтта

31
Що це стосується очікування раціональних пояснень зараз. Я гадаю, ви хочете наступне почути обґрунтування для x86 або DOS? Наскільки я переживаю, перемагає найгірша технологія. Кожного разу. І найгірше представлення струн.
jalf

4
Чому ви стверджуєте, що рядки префіксації довжини є вищими? Зрештою, C стала популярною, оскільки використовувала рядки з нульовим завершенням, які відрізняли його від інших мов.
Даніель К. Собрал

44
@Daniel: C стала популярною тому, що це просте, ефективне та портативне представлення програм, що виконуються на машинах Von Neumann, і тому, що воно використовувалося для Unix. Це, звичайно, не тому, що він вирішив використовувати нульові завершені рядки. Якби це було гарним дизайнерським рішенням, люди скопіювали б це, а вони ні. Вони, звичайно, скопіювали майже все інше з C.
Біллі ОНЕал

4
Concat - це лише O (m) з префіксацією довжини, якщо ви знищите одну з рядків. Інакше така ж швидкість. Найбільш розповсюдженими рядками C (історично) були друк та сканування. В обох цих випадках нульове припинення відбувається швидше, оскільки це зберігає один регістр.
Даніель К. Собрал

Відповіді:


195

З пащі коня

Жоден з BCPL, B або C сильно не підтримує символьні дані мовою; кожна трактує рядки подібно до векторів цілих чисел і доповнює загальні правила кількома умовами. І в BCPL, і в B рядковий літерал позначає адресу статичної області, ініціалізованої символами рядка, упакованої в комірки. У BCPL перший упакований байт містить кількість символів у рядку; в B, немає ніякого кількості і рядок завершується спеціальним символом, який Б прописаний *e. Ця зміна була внесена частково, щоб уникнути обмеження довжини рядка, викликаного утримуванням рахунку в 8- або 9-бітовому слоті, і частково тому, що підтримка підрахунку здавалася, на наш досвід, менш зручною, ніж використання термінатора.

Денніс М Річі, розвиток мови С


12
Ще одна відповідна цитата: "... семантика рядків повністю підпадає під більш загальні правила, що регулюють усі масиви, і в результаті мова простіше описати ..."
AShelly

151

У C немає рядка як частини мови. "Рядок" в C - це лише покажчик на char. Тож, можливо, ви ставите неправильне запитання.

"Яке обґрунтування виходу із типу рядка" може бути більш релевантним. На це я зазначив би, що C не є об'єктно-орієнтованою мовою і має лише базові типи значень. Рядок - це концепція вищого рівня, яка повинна бути реалізована шляхом комбінування значень інших типів. C знаходиться на нижчому рівні абстракції.

у світлі бурхливого шквалу внизу:

Я просто хочу зазначити, що я не намагаюся сказати, що це дурне чи погане питання, або що спосіб представлення рядків С є найкращим вибором. Я намагаюся уточнити, що питання було б більш лаконічним, якщо взяти до уваги той факт, що C не має механізму для диференціації рядка як типу даних від байтового масиву. Це найкращий вибір з огляду на потужність обробки та пам’яті сучасних комп’ютерів? Напевно, ні. Але задній погляд - це завжди 20/20 і все таке :)


29
char *temp = "foo bar";є дійсним твердженням на С ... ей! це не струна? чи це недійсне припинення?
Янік Рошон

56
@Yanick: це просто зручний спосіб сказати компілятору створити масив char із нулем в кінці. це не 'рядок'
Роберт С Сіакчо

28
@calavera: Але це могло так само просто означати "Створити буфер пам'яті з цим рядковим вмістом та двома байтовим префіксом",
Біллі ONeal

14
@Billy: ну оскільки "рядок" насправді є лише покажчиком на char, що еквівалентно покажчику на байт, то як би ви знали, що буфер, з яким ви маєте справу, насправді призначений бути "рядком"? для позначення цього знадобиться новий тип, окрім char / byte *. може бути структура?
Robert S Ciaccio

27
Я думаю, що @calavera вірно, у C немає типу даних для рядків. Гаразд, ви можете вважати масив символів, як рядок, але це не означає, що це завжди рядок (під рядком я маю на увазі послідовність символів з певним значенням). Двійковий файл - це масив символів, але ці символи для людини нічого не означають.
BlackBear

106

Питання задається як річ Length Prefixed Strings (LPS)проти zero terminated strings (SZ), але в основному викриваються переваги строкових префіксів. Це може здатися непосильним, але якщо чесно, ми також повинні врахувати недоліки LPS та переваги SZ.

Як я розумію, питання може бути навіть зрозумілим як упереджений спосіб запитати "які переваги нульових кінцевих струн?".

Переваги (я бачу) нульових завершених струн:

  • дуже просто, немає необхідності вводити нові поняття в мові, масиви char / char покажчики можуть робити.
  • Основна мова просто включає мінімальний синтаксичний цукор, щоб перетворити щось з подвійних лапок у купу символів (справді купа байтів). У деяких випадках його можна використовувати для ініціалізації речей, абсолютно не пов'язаних з текстом. Наприклад, формат файлу зображення xpm є дійсним джерелом C, що містить дані зображення, закодовані у вигляді рядка.
  • до речі, ви можете поставити нуль в строковий літерал, компілятор буде просто додати ще один в кінці буквальним: "this\0is\0valid\0C". Це струна? чи чотири струни? Або купа байтів ...
  • рівна реалізація, відсутність прихованої непрямості, прихованого цілого числа.
  • не задіяно прихованого розподілу пам'яті (ну деякі сумнозвісні нестандартні функції, такі як strdup, виконують розподіл, але це, головним чином, джерело проблеми).
  • немає жодної конкретної проблеми для невеликого чи великого обладнання (уявіть собі тягар управління 32-бітною префіксом на 8-бітових мікроконтролерах або обмеження обмеження розміру рядка менше 256 байт, це була проблема, яку я насправді мав із Turbo Pascal eons тому).
  • реалізація струнних маніпуляцій - лише декілька дуже простих функцій бібліотеки
  • ефективний для основного використання рядків: постійний текст читається послідовно з відомого початку (переважно повідомлення користувачеві).
  • завершальний нуль навіть не є обов’язковим, доступні всі необхідні інструменти для маніпулювання символами, як купа байтів. Виконуючи ініціалізацію масиву в С, можна навіть уникнути термінатора NUL. Просто встановіть потрібний розмір. char a[3] = "foo";є дійсним C (не C ++) і не ставить кінцевий нуль у a.
  • узгоджується з точкою зору unix "все є файлом", включаючи "файли", які не мають внутрішньої довжини, як stdin, stdout. Вам слід пам’ятати, що відкриті примитиви для читання та запису реалізуються на дуже низькому рівні. Це не виклики бібліотеки, а системні дзвінки. І той самий API використовується для двійкових або текстових файлів. Примітиви для читання файлів отримують буферну адресу та розмір та повертають новий розмір. І ви можете використовувати рядки як буфер для запису. Використання іншого виду представлення рядків означає, що ви не можете легко використовувати буквальний рядок як буфер для виведення, або вам доведеться зробити так, щоб він мав дуже дивну поведінку під час передавання char*. А саме не повертати адресу рядка, а натомість повернути фактичні дані.
  • Дуже легко маніпулювати текстовими даними, прочитаними з файлу на місці, без непотрібної копії буфера, просто вставити нулі в потрібні місця (ну, не дуже, якщо в сучасних C, так як рядки з подвійним цитуванням - це const char масиви, які зазвичай зберігаються в даних, що не змінюються сегмент).
  • попереднє додавання деяких int значень будь-якого розміру означатиме проблеми вирівнювання. Початкова довжина повинна бути вирівняна, але немає ніяких причин робити це для даних символів (і знову ж таки, примушування вирівнювання рядків означало б проблеми, коли вони трактували їх як купу байтів).
  • довжина відома під час компіляції для постійних буквальних рядків (sizeof). То чому б хто-небудь хотів зберігати його в пам'яті, попередньо додаючи його до фактичних даних?
  • певним чином C поступає як (майже) всі інші, рядки розглядаються як масиви знаків. Оскільки довжина масиву не керується C, то логічна довжина не керується ні для рядків. Дивовижне лише те, що 0 елемент додано в кінці, але це просто на рівні основної мови під час введення рядка між подвійними лапки. Користувачі можуть ідеально викликати функції маніпулювання рядками, що пропускають довжину, або навіть замість цього використовувати звичайну записку. SZ - просто заклад. У більшості інших мов керується довжина масиву, логічно, що це однаково для рядків.
  • в сучасний час наборів символів 1 байт недостатньо, і вам часто доводиться стикатися з кодованими рядками Unicode, де кількість символів сильно відрізняється від кількості байтів. Це означає, що користувачі, ймовірно, хочуть більше, ніж "просто розмір", а також іншу інформацію. Дотримуючись довжину, нічого не використовуйте (особливо немає природного місця для їх зберігання) щодо цих інших корисних відомостей.

З цього приводу не потрібно скаржитися в рідкісному випадку, коли стандартні рядки C дійсно неефективні. Ваги доступні. Якщо я дотримувався цієї тенденції, я повинен скаржитися, що стандартний C не включає жодних функцій підтримки регулярних виразів ... але насправді всі знають, що це не реальна проблема, оскільки для цього є бібліотеки. Отже, коли ефективність маніпуляції з рядками потрібна, чому б не використовувати бібліотеку на зразок bstring ? Або навіть струни C ++?

EDIT : Я недавно мав погляд на D рядків . Досить цікаво бачити, що обране рішення не є ні префіксом розміру, ні нульовим завершенням. Як і в C, буквальні рядки, укладені у подвійні лапки, - це лише коротка рука для незмінних масивів char, а мова також має рядкове ключове слово, що означає, що (незмінний масив char).

Але масиви D набагато багатші, ніж масиви С. У разі статичних масивів довжина відома під час виконання, тому немає необхідності зберігати довжину. У компілятора є його під час компіляції. Що стосується динамічних масивів, довжина доступна, але документація D не вказує, де вона зберігається. З усього, що ми знаємо, компілятор міг вибрати, щоб він зберігав його в якомусь регістрі або в якійсь змінній, що зберігається далеко від даних символів.

У звичайних масивах char або нелітеральних рядках немає остаточного нуля, отже, програмісту потрібно поставити його сам, якщо він хоче викликати якусь функцію C від D. У конкретному випадку буквених рядків, однак компілятор D все одно поставив нуль у кінець кожної нитки (щоб дозволити легкий присвоєння C рядкам полегшити виклик функції C?), але цей нуль не є частиною рядка (D не враховує його в розмірі рядка).

Єдине, що мене дещо розчарувало, - це те, що рядки повинні бути utf-8, але довжина, мабуть, все одно повертає кількість байтів (принаймні, це правда в моєму компіляторі gdc) навіть при використанні багатобайтових символів. Мені незрозуміло, чи це помилка компілятора чи за призначенням. (Гаразд, я, мабуть, дізнався, що сталося. Щоб сказати компілятору D, джерело використовує utf-8, ви повинні на початку поставити якийсь дурний порядок байт. Я пишу дурний, бо я знаю, що це не редактор, особливо для UTF- 8, який повинен бути сумісним з ASCII).


7
... Продовження ... Деякі ваші моменти, я думаю, просто помилкові, тобто аргумент "все є файлом". Файли є послідовним доступом, рядки C - ні. Префіксація по довжині також може бути виконана з мінімальним синтаксичним цукром. Єдиний розумний аргумент тут - намагання керувати 32-бітовими префіксами на невеликому (тобто 8-бітовому) обладнанні; Я думаю, що це можна було б просто вирішити, сказавши, що розмір довжини визначається реалізацією. Зрештою, це і std::basic_stringробить.
Біллі ONeal

3
@Billy ONeal: у моїй відповіді дійсно є дві різні частини. Одне - про те, що є частиною "основної мови С", а інше - про те, що стандартні бібліотеки мають надавати. Що стосується підтримки рядків, то з основної мови є лише один елемент: значення подвійної цитати додається до групи байтів. Я не дуже щасливий, ніж ти з поведінкою С. Я магічно додаю, що нуль в кінці кожного подвійного закривається вкладеною купою байтів - це досить погано. Я вважаю за краще і явну \0в кінці, коли програмісти хочуть цього замість неявного. Попередня довжина набагато гірша.
kriss

2
@Billy ONeal: це просто неправда, використання стосується того, що є ядром, а що - бібліотеками. Найбільший момент - це коли C використовується для реалізації ОС. На цьому рівні відсутні бібліотеки. C також часто використовується у вбудованих контекстах або для пристроїв програмування, де у вас часто існують однакові обмеження. У багатьох випадках Joes's, мабуть, не повинен взагалі використовувати C: "Добре, ви хочете його на консолі? У вас консоль? Ні? Дуже погано ..."
kriss

5
@Billy "Ну, для .01% програмістів на C, що реалізують операційні системи, добре". Інші програмісти можуть здійснити похід. C був створений для запису операційної системи.
Даніель Ч. Собрал

5
Чому? Тому що там сказано, що це мова загального призначення? Це говорить про те, що робили люди, які писали це, коли створювали? Для чого він використовувався перші кілька років свого життя? Отже, що це говорить про те, що зі мною не згоден? Це мова загального призначення, створена для запису операційної системи . Чи заперечує це?
Даніель К. Собрал

61

Я думаю, що це має історичні причини і знайшов це у вікіпедії :

На час розробки С (та мов, з яких воно походить) пам'ять була надзвичайно обмеженою, тому використання лише одного байта накладних даних для зберігання довжини рядка було привабливим. Єдина популярна на той час альтернатива, яка зазвичай називається "рядок Pascal" (хоча вона використовується і в ранніх версіях BASIC), використовувала провідний байт для зберігання довжини рядка. Це дозволяє рядку містити NUL, а для знаходження довжини потрібен лише один доступ до пам'яті (O (1) (постійний) час). Але один байт обмежує довжину до 255. Це обмеження довжини було набагато більш обмежуючим, ніж проблеми з рядком C, тому рядок C взагалі виграла.


2
@muntoo Хм ... сумісність?
хачик

19
@muntoo: Тому що це порушить монументальні суми існуючого коду C і C ++.
Біллі ONeal

10
@muntoo: Парадигми приходять і йдуть, але застарілий код назавжди. Будь-яка майбутня версія C повинна продовжувати підтримувати рядки, що закінчуються 0, інакше застарілий код за 30 років повинен бути переписаний (що не відбудеться). І поки старий спосіб доступний, люди продовжуватимуть користуватися, оскільки саме з цим вони знайомі.
Джон Боде

8
@muntoo: Повірте, мені іноді хочеться, щоб я міг. Але я все-таки віддаю перевагу рядкам, що закінчуються 0, над рядками Pascal.
Джон Боде

2
Говоріть про спадщину ... Структурам C ++ тепер призначено припинення NUL.
Джим Балтер

32

Калавера має рацію , але оскільки люди, схоже, не розуміють його точки зору, я наведу кілька прикладів коду.

Спочатку розглянемо, що таке C: проста мова, де весь код має досить прямий переклад на машинну мову. Усі типи вписуються в регістри та на стеку, і для цього не потрібна операційна система чи велика бібліотека часу запуску, оскільки це було призначено для запису цих речей (завдання, яке чудово підходить, враховуючи там до сьогодні навіть не є ймовірним конкурентом).

Якщо C мав stringтип, наприклад, intабо char, це був би тип, який не вписувався в реєстр чи стек, і вимагав би будь-яким чином обробляти пам'ять (з усією її підтримуючою інфраструктурою). Усі вони йдуть проти основних положень С.

Отже, рядок в C:

char s*;

Отже, припустимо тоді, що це були префікси довжини. Давайте запишемо код для об'єднання двох рядків:

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;
}

Іншою альтернативою буде використання структури для визначення рядка:

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

На даний момент для всіх маніпуляцій з рядками потрібно буде виконати два розподіли, що на практиці означає, що ви проходите через бібліотеку, щоб виконати будь-яку обробку з нею.

Найсмішніше ... як Структури , які роблять існують в C! Вони просто не використовуються для щоденного показу повідомлень користувачеві.

Отже, ось що робить Калавера: у C немає рядкового типу . Щоб зробити що-небудь з ним, вам доведеться взяти вказівник і розшифрувати його як вказівник на два різні типи, і тоді стає дуже актуальним, який розмір рядка, і його не можна просто залишити як "визначено реалізацією".

Тепер C може в будь-якому разі обробляти пам'ять, а memфункції в бібліотеці ( <string.h>навіть, навіть!) Забезпечують усі необхідні інструменти для обробки пам'яті як пари вказівника та розміру. Так звані "рядки" в C були створені лише з однією метою: показ повідомлень у контексті написання операційної системи, призначеної для текстових терміналів. І для цього достатньо нульового припинення.


2
1. +1. 2. Очевидно, якби поведінка мови за замовчуванням була б здійснена за допомогою префіксів довжини, для полегшення цього були б інші речі. Наприклад, усі ваші ролі там були заховані дзвінками strlenта друзями. Що стосується проблеми з "відведенням її до реалізації", ви можете сказати, що префікс є будь-яким а shortна цільовому полі. Тоді все ваше кастинг все-таки спрацювало б. 3. Я можу придумувати надумані сценарії протягом усього дня, які роблять одну чи іншу систему поганою.
Біллі ONeal

5
@Billy Бібліотечна річ досить правдива, окрім того, що C розроблений для мінімального використання бібліотеки або його відсутності. Використання прототипів, наприклад, не було поширеним на початку. Скажімо, префікс shortфактично обмежує розмір рядка, який, здається, є однією справою, яку вони не хотіли. Сам, працюючи з 8-бітовими рядками BASIC і Pascal, струнами COBOL фіксованого розміру та подібними речами, швидко став величезним шанувальником рядків С необмеженого розміру. Сьогодні 32-бітний розмір оброблятиме будь-яку практичну рядок, але додавати ці байти на початку було проблематично.
Даніель К. Собрал

1
@Billy: По-перше, дякую Даніелю ... ти, здається, розумієш, до чого я потрапляю. По-друге, Біллі, я думаю, ти все ще не вистачаєш точки, яка тут робиться. Я, наприклад, не сперечаюся за плюси і мінуси приставлення рядкових типів даних до їх довжини. Те , що я говорю, і що Деніел дуже чітко підкреслив, що існує рішення , прийняте в ході здійснення C , щоб не обробляти цей аргумент взагалі . Рядки не існують, що стосується основної мови. Рішення про обробку рядків залишається програмістом ... і нульове припинення стало популярним.
Robert S Ciaccio

1
+1 від мене. Ще одне, що я хотів би додати; Структура, яку ви пропонуєте, пропускає важливий крок до реального stringтипу: вона не знає символів. Це масив "char" ("char" у машинному лінгві - це стільки ж символу, скільки "слово" - те, що люди називали б словом у реченні). Рядок символів - це концепція вищого рівня, яка може бути реалізована поверх масиву, charякщо ви ввели поняття кодування.
Фріріх Раабе

2
@ DanielC.Sobral: Також структура, яку ви згадуєте, не вимагала б двох розподілів. Або використовуйте його так, як у вас є у стеці (тому bufпотрібен лише розподіл), або використовуйте struct string {int len; char buf[]};та виділяйте всю річ одним виділенням як гнучкий член масиву, і передайте її навколо як string*. (Або, мабуть, struct string {int capacity; int len; char buf[]};з очевидних причин виступу)
Mooing Duck

20

Очевидно, що для продуктивності та безпеки вам потрібно буде дотримуватися довжини струни під час роботи з нею, а не багаторазово виконувати strlenабо еквівалент на ній. Однак зберігання довжини у фіксованому місці безпосередньо перед вмістом рядка - неймовірно погана конструкція. Як вказував Йорген у коментарях до відповіді Санджита, це виключає трактування хвоста струни як струни, що, наприклад, робить безліч загальних операцій, подібних path_to_filenameабо filename_to_extensionнеможливих, не виділяючи нову пам'ять (і створюючи можливість виходу з ладу та помилок) . І, звичайно, виникає проблема, в якій ніхто не може погодитися, скільки байтів повинно займати поле довжини рядка (безліч поганих "рядків Pascal"

Дизайн C дозволяє програмісту вибрати, чи / де / як зберігати довжину, набагато більш гнучким та потужним. Але звичайно програміст повинен бути розумним. C карає дурість програмами, які руйнують, перемелюють або зупиняють ваших ворогів.


+1. Було б непогано мати стандартне місце для зберігання довжини, хоча так, щоб ті з нас, хто хоче щось подібне до префіксації довжини, не мусили скрізь писати тонни "клей-коду".
Біллі ONeal

2
Немає можливого стандартного місця щодо рядкових даних, але ви, звичайно, можете використовувати окрему локальну змінну (перераховуючи її, а не передаючи її, коли остання не зручна і перша не надто марнотратна) або структуру з покажчиком на рядок (і ще краще, прапор, який вказує, чи структура "володіє" вказівником для цілей розподілу, чи це посилання на рядок, який належить в іншому місці. І звичайно, ви можете включити в структуру гнучкий масив масиву для гнучкості при розподілі рядок зі структурою, коли вона вам підходить.
R .. GitHub СТОП ДОПОМОГА ICE

13

Ледачість, регістрація ощадливості та портативності з урахуванням складової кишки будь-якої мови, особливо C, що знаходиться на один крок вище складання (таким чином, успадковується багато застарілого коду збірки). Погодьтеся, оскільки нульова таблиця була б марною в ті дні ASCII, це (і, ймовірно, так само добре, як і контрольний графік EOF).

подивимось у псевдокоді

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

всього 1 користування реєстром

випадок 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

всього використано 2 регістри

Це може здатися короткозорим на той час, але з огляду на ощадливість у коді та реєстрі (які були PREMIUM на той час, час, коли ви знаєте, вони використовують перфокарт). Таким чином, він був швидшим (коли швидкість процесора можна було порахувати в кГц), цей "Hack" був досить проклятим і портативним для без реєстрації процесора з легкістю.

Для аргументу я реалізую 2 загальних рядкових операції

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

складність O (n), де в більшості випадків рядок PASCAL дорівнює O (1), оскільки довжина рядка попередньо пов'язана зі структурою рядка (це також означає, що цю операцію потрібно було б виконати на більш ранній стадії).

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

складність O (n) і попередження довжини рядка не змінили б складність операції, хоча я визнаю, це займе 3 рази менше часу.

З іншого боку, якщо ви використовуєте рядок PASCAL, вам слід було б переробити свій API для врахування довжини реєстру та бітової нестабільності, рядок PASCAL отримав добре відоме обмеження на 255 char (0xFF), оскільки довжина зберігалася в 1 байті (8 біт ), і саме вам потрібна довша рядок (16 біт-> що завгодно), вам доведеться враховувати архітектуру в одному шарі вашого коду, що означатиме, в більшості випадків, несумісні рядкові API, якщо ви хочете довший рядок.

Приклад:

Один файл був записаний за допомогою вашої попередньо встановленої строкової версії api на 8-бітному комп'ютері, а потім доведеться читати, скажімо, на 32-бітному комп'ютері, що б лінька програма вважала, що ваші 4байти - це довжина рядка, а потім виділяють цю кількість пам'яті потім спробуйте прочитати стільки байтів. Інший випадок - читання рядка 32 байтів PPC (маленький ендіан) на x86 (великий ендіан), звичайно, якщо ви не знаєте, що один пишеться другим, виникнуть проблеми. 1 байт довжиною (0x00000001) стане 16777216 (0x0100000), що становить 16 Мб для читання рядка в 1 байт. Звичайно, ви б сказали, що люди повинні погодитись на один стандарт, але навіть 16-бітовий однокодовий має мало і велику небезпеку.

Звичайно, і у C будуть свої проблеми, але вони будуть дуже слабо зачеплені питаннями, порушеними тут.


2
@deemoowoor: Concat: O(m+n)з нульовими рядками, O(n)типовими для всіх інших. Довжина O(n)з нульовими рядками, O(1)всюди в іншому місці. Приєднуйтесь: O(n^2)з нульовими рядками, O(n)всюди в іншому місці. Є деякі випадки, коли нульові завершені рядки є більш ефективними (наприклад, просто додайте один до випадку вказівника), але лаконічні і довжина є на сьогодні найбільш поширеними операціями (довжина, принаймні, потрібна для форматування, виведення файлів, відображення консолі тощо) . Якщо ви кешуєте довжину для амортизації, O(n)ви просто зазначили, що довжина повинна зберігатися разом із рядком.
Біллі ONeal

1
Я погоджуюся, що в сьогоднішньому коді цей тип рядка неефективний і схильний до помилок, але, наприклад, на дисплеї консолі насправді не потрібно знати довжину рядка, щоб ефективно відображати його, вихідний файл насправді не повинен знати про рядок довжина (виділяючи кластер лише на ходу), а форматування рядків у цей час у більшості випадків здійснювалося за фіксованою довжиною рядка. У будь-якому випадку ви повинні писати неправильний код, якщо ви нарікаєте на C складність O (n ^ 2), я впевнений, що я можу записати його в O (n) складності
dhhh

1
@dvhh: я не сказав n ^ 2 - я сказав m + n - це все ще лінійно, але вам потрібно шукати до кінця початкового рядка, щоб зробити конкатенацію, тоді як з префіксом довжини не шукайте необхідно. (Це насправді лише черговий наслідок тривалості, що вимагає лінійного часу)
Біллі ONeal

1
@Billy ONeal: з простої цікавості я зробив прихильність свого поточного проекту C (близько 50000 рядків коду) для викликів функції маніпуляції з рядками. strlen 101, strcpy та варіанти (strncpy, strlcpy): 85 (у мене також є кілька сотень буквальних рядків, що використовуються для повідомлення, маються на увазі копії), strcmp: 56, strcat: 13 (а 6 - це об'єднання нульової довжини рядка для виклику strncat) . Я погоджуюся, що префікс довжини пришвидшить дзвінки до strlen, але не до strcpy або strcmp (можливо, якщо API strcmp не використовує загальний префікс). Найцікавіше стосовно вищезазначених коментарів - це те, що strcat трапляється дуже рідко.
kriss

1
@supercat: не дуже, подивіться на деякі реалізації. Короткі рядки використовують буфер на основі короткого стека (без розподілу купи). Купу використовують лише тоді, коли вони збільшуються. Але сміливо надайте фактичну реалізацію своєї ідеї як бібліотеки. Зазвичай неприємності виявляються лише тоді, коли ми доходимо до деталей, а не в загальному дизайні.
kriss

9

Багато в чому С був примітивним. І мені це сподобалось.

Це був крок над мовою складання, даючи вам майже таку ж ефективність із мовою, яку було набагато простіше писати та підтримувати.

Нульовий термінатор простий і не потребує спеціальної підтримки з боку мови.

Озираючись назад, це не здається таким зручним. Але я використовував мову складання ще в 80-х роках, і тоді це здавалося дуже зручним. Я просто думаю, що програмне забезпечення постійно розвивається, а платформи та інструменти постійно удосконалюються.


Я не бачу нічого примітивнішого щодо нульових завершених рядків, ніж будь-що інше. Паскаль передує C і в ньому використовується префіксація довжини. Звичайно, це було обмежено 256 символами на рядок, але просто використання 16-бітного поля вирішило б проблему в переважній більшості випадків.
Біллі ONeal

Те, що обмежило кількість символів, - це саме той тип питань, про який потрібно задуматися, роблячи щось подібне. Так, ви можете зробити це довше, але тоді байти мали значення. А 16-бітове поле буде достатньо довгим для всіх випадків? Зазвичай, ви повинні визнати, що нульове закінчення є концептуально примітивним.
Джонатан Вуд

10
Або ви обмежуєте довжину рядка, або обмежуєте вміст (немає нульових символів), або ви приймаєте додаткові накладні витрати від 4 до 8 байт. Безкоштовного обіду немає. На момент створення нульовий завершений рядок мав ідеальний сенс. У монтажі я іноді використовував верхній біт символу для позначення кінця рядка, економивши ще один байт!
Марк Викуп

Точно, Марк: Безкоштовного обіду немає. Це завжди компроміс. У наші дні нам не потрібно йти на такі самі компроміси. Але тоді цей підхід здавався таким же хорошим, як і будь-який інший.
Джонатан Вуд

8

Якщо припустити, що C реалізує рядки на шляху Pascal, префіксуючи їх по довжині: чи 7-символьна довга струна є тим самим ТИП ДАНИХ, що і 3-рядова рядок? Якщо відповідь "так", то який код повинен створювати компілятор, коли я присвоюю перший другому? Чи слід обрізати або автоматично змінити розмір рядка? Якщо змінити розмір, чи повинна ця операція захищатись замком, щоб зробити її безпечною ниткою? Сторона С підходила до всіх цих питань, подобається це чи ні :)


2
Помилка .. ні, це не було. C-підхід взагалі не дозволяє присвоїти 7-ти рядковій рядку 3-х символьній рядку.
Біллі ONeal

@Billy ONeal: чому б і ні? Наскільки я це розумію в цьому випадку, всі рядки мають однаковий тип даних (char *), тому довжина не має значення. На відміну від Паскаля. Але це було обмеженням Паскаля, а не проблемою з рядками з префіксом довжини.
Олівер Мейсон

4
@Billy: Я думаю, ви щойно переосмислили точку Крістіана. C займається цими питаннями, не займаючись ними взагалі. Ви все ще думаєте з точки зору C, що фактично містить поняття рядка. Це просто вказівник, тож ви можете призначити його будь-що, що хочете.
Robert S Ciaccio

2
Це як ** матриця: "немає рядка".
Robert S Ciaccio

1
@calavera: Я не бачу, як це щось підтверджує. Ви можете вирішити це так само, як і префіксація довжини ... тобто взагалі не дозволяти призначення.
Біллі ONeal

8

Я якось зрозумів, що питання означає, що немає підтримки компілятора для рядків з префіксом довжини в C. Наступний приклад показує, що принаймні ви можете запустити власну бібліотеку рядків C, де довжини рядків підраховуються під час компіляції, з такою конструкцією:

#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;
}

Однак це не матиме жодних проблем, тому що вам потрібно бути обережними, коли спеціально звільнити цей рядковий вказівник та коли він буде статично виділений (буквальний charмасив).

Редагувати: Як більш пряма відповідь на питання, я вважаю, що це було таким чином, як C міг би підтримувати обидві, які мають доступну довжину рядка (як константа часу компіляції). лише покажчики та нульове припинення.

Звичайно, здається, що робота з нульовими завершеними рядками була рекомендованою практикою, оскільки стандартна бібліотека взагалі не приймає довжину рядків як аргументи, а оскільки витяг довжини не є таким простим кодом char * s = "abc", як показано в моєму прикладі.


Проблема полягає в тому, що бібліотеки не знають існування вашої структури і все ще обробляють такі речі, як вбудовані нулі неправильно. Крім того, це насправді не відповідає на запитання, яке я задав.
Біллі ONeal

1
Це правда. Отже, більша проблема полягає в тому, що немає кращого стандартного способу надання інтерфейсів із рядковими параметрами, ніж звичайні старі рядки з нульовим завершенням. Я б все-таки стверджував, що є бібліотеки, які підтримують подачу в парах довжини вказівника (ну, принаймні, ви можете побудувати з ними C ++ std :: string).
Pyry Jahkola

2
Навіть якщо ви зберігаєте довжину, ви ніколи не повинні допускати рядків із вбудованими нулями. Це основний здоровий глузд. Якщо у ваших даних можуть бути нулі, ви ніколи не повинні використовувати їх з функціями, які очікують рядки.
R .. GitHub СТОП ДОПОМОГАЙТЕ

1
@supercat: З точки зору безпеки я би вітав це надмірність. Інакше необізнані (або позбавлені сну) програмісти закінчують об'єднання бінарних даних та рядків та передавання їх у речі, які очікують [недійсні] рядки ...
R .. GitHub STOP HELPING ICE

1
@R ..: Хоча методи, які очікують нульових кінцевих рядків, як правило, очікують a char*, багато методів, які не очікують припинення нуля, також очікують a char*. Більш суттєва перевага розділення типів стосуватиметься поведінки Unicode. Реалізація рядків може бути доречною для збереження прапорів щодо того, чи відомо, що рядки містять певні типи символів, або відомо, що вони не містять їх (наприклад, пошук 999,990-ї точки коду в рядку з мільйонними символами, який, як відомо, не містить будь-які символи, що виходять за межі основної багатомовної площини, будуть на порядок швидшими ...
supercat

6

"Навіть на 32-бітовій машині, якщо ви дозволяєте рядку мати розмір наявної пам'яті, строка з попередньою фіксацією лише на три байти ширше, ніж нульовий завершений рядок."

По-перше, додаткові 3 байти можуть мати значні накладні витрати для коротких рядків. Зокрема, нитка завдовжки зараз займає в 4 рази більше пам’яті. Деякі з нас використовують 64-розрядні машини, тому нам потрібно або 8 байт для зберігання рядка нульової довжини, або формат рядка не може впоратися з найдовшими рядками, які підтримує платформа.

Також можуть бути вирішені питання вирівнювання. Припустимо, у мене є блок пам'яті, що містить 7 рядків, як-от "соло \ 0секунди \ 0 \ 0четверте \ 0 п'ять \ 0 \ 0сев." Другий рядок починається зі зміщення 5. Обладнання може вимагати вирівнювання 32-бітових цілих чисел за адресою, кратною 4, тому вам доведеться додавати прокладки, ще більше збільшуючи накладні витрати. Представлення C дуже порівняно з пам'яттю. (Ефективність пам’яті хороша; це, наприклад, допомагає кешувати продуктивність.)


Я вважаю, що я вирішив усе це у запитанні. Так, на платформах x64 32-бітний префікс не може вмістити всі можливі рядки. З іншого боку, ви ніколи не хочете, щоб рядок був таким великим, як нульовий завершений рядок, тому що, щоб зробити все, вам доведеться вивчити всі 4 мільярди байт, щоб знайти кінець майже для кожної операції, яку ви могли б зробити для цього. Крім того, я не кажу, що нульові завершені рядки завжди злі - якщо ви будуєте одну з цих блокових структур, і ваша конкретна програма прискорена саме таким побудовою, займіться цим. Я просто хочу, щоб мовна поведінка мови не робила цього.
Біллі ONeal

2
Я цитував цю частину вашого запитання, оскільки, на мій погляд, це недооцінювало питання ефективності. Подвоєння або подрібнення вимог до пам'яті (для 16-бітної та 32-бітної відповідно) може бути великою вартістю продуктивності. Довгі рядки можуть бути повільними, але принаймні вони підтримуються і все ще працюють. Мій інший пункт, щодо вирівнювання, ви зовсім не згадуєте.
Брангдон

Вирівнювання може бути вирішено, вказавши, що значення, що перевищують UCHAR_MAX, повинні вести себе як би запаковані та розпаковані за допомогою доступу в байт та зсуву бітів. Відповідно сконструйований тип рядка може запропонувати ефективність зберігання, по суті порівняну з нульовими завершеними рядками, одночасно дозволяючи перевіряти межі буферів без додаткових накладних витрат на пам'ять (використовуйте один біт у префіксі, щоб сказати, чи буфер "повний"; якщо він не є, і останній байт не дорівнює нулю, цей байт буде представляти решту пробілу. Якщо буфер не заповнений, а останній байт дорівнює нулю, то останні 256 байт були б не використані, тому ...
supercat

... можна було зберегти у цьому просторі точну кількість невикористаних байтів з нульовою додатковою вартістю пам'яті). Вартість роботи з префіксами компенсується можливістю використання таких методів, як fgets (), без необхідності передавати довжину рядка (оскільки буфери знають, наскільки вони великі).
supercat

4

Нульове припинення дозволяє здійснювати швидкі операції на основі покажчика.


5
Так? Які "операції швидкого вказівника" не працюють з префіксацією довжини? Що ще важливіше, інші мови, які використовують префіксацію довжини, швидше, ніж маніпуляція з рядком C wrt.
Біллі ONeal

12
@billy: За допомогою префіксованих рядків по довжині ви не можете просто взяти вказівник рядка і додати до нього 4, і очікуйте, що він все ще буде дійсним рядком, тому що він не має префікса довжини (все одно не дійсний).
Йорген Зіґвардссон

3
@j_random_hacker: Сполучення набагато гірше для рядків asciiz (O (m + n) замість потенційно O (n)), а concat набагато частіше, ніж будь-яка з інших перерахованих тут операцій.
Біллі ONeal

3
є одна tiiny операції мало що стає все більш дорогим з рядками з завершальним нульовими: strlen. Я б сказав, що це трохи недолік.
джельф

10
@Billy ONeal: всі інші також підтримують регулярний вираз. І що ? Використовуйте бібліотеки, для чого вони створені. C - це про максимальну ефективність і мінімалізм, не включаючи батареї. Інструменти C також дозволяють реалізувати рядок Prefixed Length, використовуючи структури дуже легко. І ніщо не забороняє вам реалізовувати програми маніпулювання рядками через управління власною довжиною та буферами char. Це, як правило, те, що я роблю, коли хочу працездатність і використовую C, а не викликати жменю функцій, які очікують нуль в кінці буфера символів - це не проблема.
kriss

4

Один момент ще не згадується: коли C був сконструйований, було багато машин, де "char" не було восьми біт (навіть сьогодні є платформи DSP там, де його немає). Якщо хтось вирішить, що рядки мають бути встановлені за допомогою префікса по довжині, скільки префіксів довжини "char" слід використовувати? Використання двох може накласти штучне обмеження на довжину рядків для машин з 8-бітовим символом та 32-бітним адресаційним простором, при цьому витрачаючи місце на машини з 16-бітним символом та 16-бітовим простором адресації.

Якщо хотілося б дозволити ефективно зберігати рядки довільної довжини, і якщо 'char' завжди було 8-бітним, можна було б - за певних витрат у швидкості та розмірі коду - визначити схему - рядок з префіксом парного числа N буде довжиною N / 2 байт, рядок з префіксом непарного значення N і парним значенням M (зчитування назад) може бути ((N-1) + M * char_max) / 2 і т.д., і вимагати будь-якого буфера, який претензії пропонувати певний простір для утримання рядка, повинні містити достатню кількість байтів, що передують цьому простору, для обробки максимальної довжини. Те, що "char" не завжди становить 8 біт, однак ускладнить таку схему, оскільки кількість знаків "char", необхідних для утримання довжини рядка, буде змінюватися залежно від архітектури процесора.


Префікс легко може мати розмір, визначений реалізацією, як і є sizeof(char).
Біллі ONeal

@BillyONeal: sizeof(char)це одне. Завжди. Префікс може мати розмір, визначений реалізацією, але це було б незручно. Крім того, немає реального способу дізнатися, яким повинен бути "правильний" розмір. Якщо один містить багато 4-символьних рядків, нульове накладення накладає 25% накладних витрат, тоді як чотирибайтовий префікс довжини накладає 100% накладних витрат. Крім того, витрачений час на упаковку та розпакування префіксів довжиною чотирьох байт може перевищити вартість сканування 4-байтних рядків на нульовий байт.
supercat

1
Ага, так. Ти маєш рацію. Префікс легко може бути чимось іншим, ніж знаком char. Все, що ставило б вимоги до вирівнювання на цільовій платформі, було б добре. Я не збираюся туди ходити - я вже доводив це до смерті.
Біллі ONeal

Якщо припустити, що рядки мають префікс довжини, можливо, найнебезпечнішим ділом буде size_tпрефікс (відхід пам’яті буде проклятий, це було б найбезпечнішим --- дозволяючи рядки будь-якої можливої ​​довжини, які могли б вписатися в пам'ять). Насправді, це свого роду , що робить D; масиви є struct { size_t length; T* ptr; }, а рядки - це просто масиви immutable(char).
Тім Час

@ TimČas: Якщо рядки не повинні бути вирівняними за словом, у вартості роботи з короткими рядками на багатьох платформах переважатиме вимога упакувати та розпакувати довжину; Я справді не вважаю це практичним. Якщо потрібно, щоб рядки були змістово-агностичними масивами байтів довільного розміру, я думаю, що було б краще тримати довжину окремо від вказівника до даних символів, і мова, яка дозволить отримати обидва фрагменти інформації для буквальних рядків .
supercat

2

Багато дизайнерських рішень навколо C випливають з того, що коли він був спочатку реалізований, проходження параметрів було дещо дорогим. З огляду на вибір між напр

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);
}

проти

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);
}

останній був би дещо дешевшим (і, таким чином, кращим), оскільки вимагав проходження лише одного параметра, а не двох. Якщо виклику методу не потрібно було знати базової адреси масиву, ані індексу всередині нього, передача єдиного вказівника, що поєднує два, буде дешевше, ніж передавати значення окремо.

Хоча існує багато розумних способів, за допомогою яких C могла б кодувати довжини рядків, підходи, які були винайдені до цього часу, мали б усі необхідні функції, які повинні мати можливість працювати з частиною рядка, щоб прийняти базову адресу рядка та бажаний індекс у вигляді двох окремих параметрів. Використання нульового байтового завершення дозволило уникнути цієї вимоги. Хоча інші підходи були б кращими для сучасних машин (сучасні компілятори часто передають параметри в регістри, а memcpy можна оптимізувати способами strcpy () - еквіваленти не можуть). Достатній виробничий код використовує нульові байтові завершені рядки, які важко змінити на що-небудь інше.

PS - В обмін на незначну швидкість покарання за деякі операції та невеликий додатковий накладний накладення на довші струни, можна було б мати методи, які працюють із рядками, приймаючи покажчики безпосередньо на рядки, буферні рядки, перевірені межею , або структури даних, що ідентифікують підрядки іншого рядка. Така функція, як "strcat", виглядала б приблизно як [сучасний синтаксис]

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);
  }
}

Трохи більший, ніж метод strcat K&R, але він підтримує перевірку меж, чого метод K&R не робить. Крім того, на відміну від поточного методу, можна було б легко об'єднати довільну підрядку, наприклад

/* 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);
}

Зауважте, що термін експлуатації рядка, поверненого temp_substring, буде обмежений строком sта src, який коли-небудь був коротшим (саме тому метод вимагаєinf передачі - якщо він був локальним, він би загинув, коли метод повернувся).

З точки зору вартості пам'яті, рядки та буфери до 64 байт мали б один байт накладних витрат (такий же, як і рядки з нульовим завершенням); довших рядків було б трохи більше (якщо одна дозволена кількість накладних витрат між двома байтами і максимально необхідною буде компромісом часу / простору). Спеціальне значення байта "довжина / режим" буде використовуватися для вказівки на те, що рядковій функції була надана структура, що містить байт прапора, вказівник і довжину буфера (яке потім може довільно індексуватися в будь-який інший рядок).

Звичайно, K&R не реалізувала подібного, але це, швидше за все, тому що вони не хотіли витрачати багато зусиль на обробку струн - область, де навіть сьогодні багато мов здаються досить анемічними.


Немає нічого, що не завадило б char* arrвказувати на структуру форми struct { int length; char characters[ANYSIZE_ARRAY] };або подібну форму, яка б все-таки була прохідною як єдиний параметр.
Біллі ONeal

@BillyONeal: дві проблеми з таким підходом: (1) Це дозволило б лише передавати рядок у цілому, тоді як даний підхід також дозволяє передавати хвіст рядка; (2) він буде витрачати значне місце при використанні з невеликими струнами. Якби K&R хотіли витратити деякий час на струнах, вони могли б зробити речі набагато більш надійними, але я не думаю, що вони мали намір використовувати їхню нову мову через десять років, набагато менше сорок.
supercat

1
Цей біт про конвенцію про закликання - це просто така історія, яка не має відношення до реальності ... це не було врахуванням у дизайні. А конвенції про дзвінки на основі реєстру вже були "придумані". Крім того, такі підходи, як два покажчики, не були варіантом, оскільки структури не були першокласними ... тільки примітиви були присвоювані або прохідні; Копіювання структури не надійшло до UNIX V7. Потрібна memcpy (яка також не існувала) просто для копіювання рядкового вказівника - жарт. Спробуйте написати повну програму, а не лише окремі функції, якщо ви робите вигляд мовного дизайну.
Джим Балтер

1
"це, швидше за все, тому, що вони не хотіли витрачати багато зусиль на обробку струн" - дурниця; весь домен програми раннього UNIX був обробкою рядків. Якби не це, ми ніколи про це не чули.
Джим Балтер

1
"Я не думаю, що" буфер char починається з int, що містить довжину ", є більш магічним", - це якщо ви збираєтеся str[n]звернутися до правильного символу. Це такі речі, про які люди, що обговорюють це , не замислюються .
Джим Балтер

2

За словами Джоела Спольського в цій публікації в блозі ,

Це тому, що мікропроцесор PDP-7, на якому були винайдені UNIX та мова програмування C, мав тип рядка ASCIZ. ASCIZ означав "ASCII з Z (нуль) в кінці".

Побачивши всі інші відповіді тут, я переконаний, що навіть якщо це правда, це лише частина причини того, що C має нульові завершені "рядки". Ця публікація досить висвітлює те, як прості речі, такі як рядки, насправді можуть бути досить важкими.


2
Подивіться, я поважаю Джоеля за багато речей; але це щось, де він спекулює. Відповідь Ганса Пасанта надходить безпосередньо від винахідників C.
Біллі ONeal

1
Так, але якщо те, що каже Спольський, є правдивим, то це було б частиною «зручності», про яку вони мали на увазі. Ось чому я і включив цю відповідь.
БенК

AFAIK .ASCIZбув лише твердженням асемблера, щоб створити послідовність байтів, за якими слід 0. Це просто означає, що нульовий завершений рядок був добре усталеною концепцією в той час. Це не означає, що нульові завершені рядки були чимось пов’язаними з архітектурою PDP- *, за винятком того, що ви можете писати щільні петлі, що складаються з MOVB(скопіювати байт) та BNE(гілка, якщо останній скопійований байт не був нульовим).
Адріан Ш

Це дозволяє показати, що C - стара, в'яла, ледача мова.
purec

2

Не обов'язково, але контрапункт, кодований довжиною

  1. Деякі форми кодування динамічної довжини перевершують кодування статичної довжини, що стосується пам'яті, все залежить від використання. Просто подивіться на UTF-8 для доказу. По суті це розширюваний масив символів для кодування одного символу. Для цього використовується один біт для кожного розширеного байта. Для завершення NUL використовується 8 біт. Префікс довжини. Я думаю, що можна досить правильно називати нескінченною довжиною, використовуючи 64 біти. Як часто ви потрапляєте у справу із вашими зайвими бітами, є вирішальним фактором. Тільки 1 надзвичайно велика струна? Кого хвилює, якщо ви використовуєте 8 або 64 біти? Багато невеликих рядків (тобто рядків англійських слів)? Тоді ваші витрати на префікс становлять великий відсоток.

  2. Струни з заданою довжиною, що дозволяють заощадити час, - це не реальна річ . Незалежно від того, чи потрібні ваші надані дані, щоб забезпечити довжину, ви підраховуєте час компіляції або вам справді надаються динамічні дані, які ви повинні кодувати як рядок. Ці розміри обчислюються в якийсь момент алгоритму. Може бути надана окрема змінна для зберігання розміру нульового завершеного рядка . Що робить порівняння на заощадження часу. В одному просто є додатковий NUL в кінці ... але якщо кодування довжини не включає цей NUL, то між ними буквально немає різниці. Ніяких алгоритмічних змін взагалі не потрібно. Просто попередній пропуск, який ви повинні вручну спроектувати самостійно, а не компілятор / час виконання, зробіть це за вас. C здебільшого полягає в тому, щоб робити справи вручну.

  3. Додатковий префікс довжини є точкою продажу. Мені не завжди потрібна додаткова інформація для алгоритму, тому необхідність робити це для кожної рядки робить моє попереднє обчислення + час обчислення ніколи не може опуститися нижче O (n). (Тобто генератор апаратних випадкових чисел 1-128. Я можу витягнути з "нескінченного рядка". Скажімо, він створює символи лише так швидко. Отже, наша довжина рядка змінюється весь час. Але моє використання даних, мабуть, не хвилює, як У мене є багато випадкових байтів. Він просто хоче, щоб наступний доступний невикористаний байт, як тільки він зможе отримати його після запиту, я міг би чекати на пристрої. Але я також міг би передбачити буфер символів, попередньо прочитаний. Порівняння довжини - це непотрібна витрата обчислень. Нульова перевірка є більш ефективною.)

  4. Префікс довжини є хорошим захистом від переповнення буфера? Це також є розумним використанням функцій бібліотеки та їх реалізацією. Що робити, якщо я передаю неправильно сформовані дані? Мій буфер завдовжки 2 байти, але я кажу, що функція 7! Напр.: Якщо get () призначений для використання на відомих даних, у нього може бути внутрішня перевірка буфера на тестування компільованих буферів та malloc ()дзвінки та досі дотримуйтесь спец. Якщо це було призначено для використання в якості труби для невідомого STDIN для отримання невідомого буфера, то явно не можна знати розмір буфера, що означає, що довжина аргументу є безглуздою, тут вам потрібно щось інше, як перевірка канарів. З цього питання ви не можете перефіксувати деякі потоки та входи по довжині, ви просто не можете. Це означає, що перевірка довжини має бути вбудована в алгоритм, а не в магічну частину системи набору тексту. TL; DR NUL-припинення ніколи не повинно бути небезпечним, воно просто закінчилося таким чином через неправильне використання.

  5. Точка зустрічного лічильника: NUL-закінчення дратує двійкові. Тут вам або потрібно робити префікс довжини або трансформувати байти NUL якимось чином: втечі-коди, перестановка діапазону тощо ... що, звичайно, означає більше використання пам'яті / скорочення інформації / більше операцій на байт. Тут префікс довжини здебільшого виграє війну. Єдине перетворення трансформації полягає в тому, що ніяких додаткових функцій не потрібно писати для покриття рядків з префіксом довжини. Що означає, що у ваших більш оптимізованих підпрограмах суб-O (n) ви можете змусити їх автоматично діяти як їх еквіваленти O (n), не додаючи більше коду. Зворотний бік - це, звичайно, час / пам'ять / стискання при використанні на важких струнах NUL.Залежно від того, яка частина вашої бібліотеки закінчується дублюванням для роботи з бінарними даними, може бути доцільним працювати виключно з рядками з префіксами довжини. Це сказало, що можна також зробити те ж саме з рядками з префіксом довжини ... -1 довжина може означати закінчення NUL, і ви можете використовувати рядки, що закінчуються NUL, всередині терміналів, що закінчуються по довжині.

  6. Concat: "O (n + m) vs O (m)" Я припускаю, що ви посилаєтесь на m як на загальну довжину рядка після об'єднання, тому що обидва повинні мати мінімальну кількість операцій (ви не можете просто виконати -на рядок 1, що робити, якщо вам доведеться перерозподіляти?). І я припускаю, що n - це міфічна кількість операцій, яких вам більше не доведеться робити через попереднє обчислення. Якщо так, то відповідь проста: попередньо обчислити. Якщови наполягаєте, що у вас завжди буде достатньо пам'яті, щоб не потрібно перерозподіляти, і це основа нотації big-O, тоді відповідь ще простіша: виконайте двійковий пошук у виділеній пам'яті для кінця рядка 1, явно є великий вибір нескінченних нулів після рядка 1 для нас, щоб не турбуватися про realloc. Там легко потрапив до журналу (n), і я ледве намагався. Що, якщо ви згадуєте, журнал (n) по суті є лише такою великою, як 64 на справжньому комп'ютері, що по суті схоже на вимову O (64 + m), що по суті є O (m). (І так, що логіка використовується в аналізі часу виконання реальних структур даних, які використовуються сьогодні. Це не фігня з вершини моєї голови.)

  7. Concat () / Len () ще раз : Запам'ятовуйте результати. Легко. Перетворює всі обчислення в попередні обчислення, якщо це можливо / необхідно. Це алгоритмічне рішення. Це не насильне обмеження мови.

  8. Передавання рядкового суфіксу простіше / можливо з припиненням NUL. Залежно від того, як реалізовано префікс довжини, він може бути руйнівним для початкового рядка, а іноді навіть не бути можливим. Потрібна копія та передайте O (n) замість O (1).

  9. Передача аргументів / відсилання посилань менше для закінчення NUL проти префікса довжини. Очевидно тому, що ви передаєте менше інформації. Якщо вам не потрібна довжина, це дозволяє економити багато слідів і дозволяє оптимізувати.

  10. Можна обдурити. Це дійсно просто вказівник. Хто каже, що ви повинні читати це як рядок? Що робити, якщо ви хочете прочитати його як окремий символ чи поплавок? Що робити, якщо ви хочете зробити навпаки і прочитати поплавок як рядок? Якщо ви обережні, ви можете зробити це з припиненням NUL. Ви не можете зробити це з префіксом довжини, це тип даних, що виразно відрізняється від покажчика, як правило. Вам, швидше за все, доведеться побудувати рядок по байтах і отримати довжину. Звичайно, якщо ви хотіли чогось схожого на цілий поплавок (мабуть, у ньому є NUL), вам доведеться все-таки прочитати байт-байт, але деталі залишається вам вирішити.

TL; DR Чи використовуєте ви двійкові дані? Якщо ні, то припинення NUL дозволяє отримати більше алгоритмічної свободи. Якщо так, то ваша головна проблема - кількість коду та швидкість / пам'ять / стиснення. Суміш двох підходів чи запам'ятовування може бути найкращим.


9 було своєрідно непідставно / неправильно представлене. Попередня фіксація довжини не має цієї проблеми. Lcent проходження як окрема змінна. Ми говорили про префікс, але я захопився. Ще добре подумати, тому я залишу його там. : d
Чорний

1

Я не купую відповідь "С не має рядка". Щоправда, C не підтримує вбудовані типи вищого рівня, але ви все одно можете представляти структури даних у C, і ось що є рядком. Те, що рядок є лише вказівником на C, не означає, що перші N байтів не можуть набувати особливого значення як довжини.

Розробники Windows / COM будуть дуже добре знайомі з BSTRтипом, який саме такий - це попередньо встановлена ​​довжина C рядка, де фактичні дані символів починаються не з байта 0.

Тож здається, що рішення про використання нульового припинення - це просто те, що люди віддали перевагу, а не необхідність мови.


-3

gcc приймайте наведені нижче коди:

char s [4] = "abcd";

і це нормально, якщо ми розглядаємо це як масив символів, але не рядок. Тобто ми можемо отримати доступ до нього за допомогою s [0], s [1], s [2] і s [3], або навіть з memcpy (dest, s, 4). Але ми отримаємо безладний персонаж, коли будемо намагатися з put (s), або ще гірше з strcpy (dest, s).


@ Adrian W. Це дійсно C. Рівні точної довжини мають спеціальний облік і NUL для них опущено. Це, як правило, нерозумно, але може бути корисним у випадках, наприклад, як заповнення структур заголовків, які використовують "рядки" FourCC.
Кевін Тібідеу

Ти правий. Це дійсно C, збиратиметься та поводиться так, як описано kkaaii. Причина погіршення голосів (не моя ...), ймовірно, в тому, що ця відповідь жодним чином не відповідає на питання ОП.
Адріан Ш
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.