Чи завжди рядки C недійсні, або це залежить від платформи?


13

Зараз я працюю з вбудованими системами і з'ясовую способи реалізації рядків на мікропроцесорі без операційної системи. Поки що я роблю, це просто використовувати ідею мати покажчики символів, що припиняються NULL, і трактувати їх як рядки, де NULL означає кінець. Я знаю, що це досить часто, але чи завжди ви можете розраховувати на те, що це так?

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

Дозвольте бути більш конкретним, але для моєї справи. Я реалізую систему, яка приймає та обробляє команди через послідовний порт. Чи можу я залишити свій код обробки команд однаковим, а потім очікувати, що рядкові об'єкти, створені на RTOS (які містять команди), будуть припинені NULL? Або це було б інакше, залежно від ОС?

Оновлення

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


3
Я давно не використовував C, але не можу придумати час, коли я зіткнувся з реалізацією, яка не використовувала рядки, що закінчуються NULL. Це частина стандартного С, якщо я добре пам’ятаю (як я вже сказав,
минув

1
Я не фахівець з C, але, наскільки я знаю, всі рядки в C - це масиви char, недійсні. Ви можете створити власний тип рядка, але вам доведеться самостійно реалізувати всі функції маніпуляції з рядками.
Мачадо


1
@MetalMikester Ви думаєте, що ця інформація може бути знайдена в стандартній специфікації C?
Снуп

3
@Snoopy Швидше за все, так. Але насправді, коли ми говоримо про рядки в C, вони просто масив символів, який закінчується на NULL, і це все, якщо ви не використовуєте якусь нестандартну бібліотеку струн, але це все одно не про що ми тут говоримо. Сумніваюся, ви знайдете платформу, яка цього не поважає, особливо якщо одна із сильних сторін - це портативність.
MetalMikester

Відповіді:


42

Речі, які називаються "C рядками", будуть припинені на нуль на будь-якій платформі. Ось так стандартні функції бібліотеки С визначають кінець рядка.

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


4
просто додати; Зазвичай у вас є десь ціле число, щоб відстежувати довжину рядка, і тоді ви отримуєте власну структуру даних, щоб зробити це правильно, щось подібне до класу QString у Qt
Рудольф Олах

8
Справа в суті: Я працюю з програмою C, яка використовує щонайменше п’ять різних рядкових форматів: charмасиви, що закінчуються нулем , charмасиви з довжиною, закодованою в першому байті (загальновідомі як "рядки Pascal"), на wchar_tбазі версій обох вище, і charмасиви, що поєднують обидва методи: довжину, кодовану в першому байті, і нульовий символ, що закінчує рядок.
Марк

4
@Mark Взаємодія з великою кількістю сторонніх компонентів / додатків чи застарілий безладний код?
Ден піднімається Firelight

2
@DanNeely, усе вищезазначене. Пасхальні рядки для взаємодії з класичним MacOS, рядками C для внутрішнього використання та Windows, широкими рядками для додавання підтримки Unicode та рядками збиття, тому що хтось намагався бути розумним і створити рядок, який міг би одночасно взаємодіяти як з MacOS, так і з Windows.
Марк

1
@ Марк ... і звичайно, ніхто не готовий витрачати гроші на погашення технічного боргу, оскільки класичний MacOS давно мертвий, а ублюдки - це подвійний кластерфрак кожен раз, коли їх потрібно торкатися. Мої симпатії.
Ден піднімається Firelight

22

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

Конвенція про NULприпинення переходить до попереднього стандарту C, і за 30+ років я не можу сказати, що я зіткнувся з середовищем, яке робить щось інше. Така поведінка була кодифікована в C89 та продовжує залишатися частиною стандарту мови С (посилання на проект C99):

  • Розділ 6.4.5 встановлює етап для NULзакінчених рядків, вимагаючи NULдодавання до рядкових літералів.
  • Розділ 7.1.1 доводить це до функцій у стандартній бібліотеці, визначаючи рядок як "суміжну послідовність символів, що закінчується і включає перший нульовий символ".

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


2
Однією з причин було б уникнути необхідності знаходити кінець тієї ж рядки знову і знову.
Paŭlo Ebermann

@ PaŭloEbermann Правильно. За рахунок того, що потрібно передати два значення замість одного. Що трохи неприємно, якщо ви просто передаєте рядковий літерал, як у printf("string: \"%s\"\n", "my cool string"). Єдиним способом пройти чотири параметри в цьому випадку (крім якогось байта завершення) було б визначити рядок як щось подібне std::stringдо C ++, у якого є свої проблеми та обмеження.
cmaster - відновити моніку

1
Розділ 6.4.5 не вимагає завершення літерального рядка з нульовим символом. У ньому прямо зазначається: " Літеральний рядок символу не повинен бути рядком (див. 7.1.1), тому що нульовий символ може бути вбудований в нього послідовністю втечі \ 0. "
bzeaman

1
@bzeaman Зноска говорить про те, що ви можете побудувати літеральний рядок, який не відповідає визначенню рядка 7.1.1, але речення, яке посилається на нього, відповідає сумісним компіляторам - NULвикорінюйте їх незалежно від того: "На етапі 7 перекладу байт або код значення нуля додається до кожної багатобайтової послідовності символів, яка є результатом рядкового літералу або літералу. " Функції бібліотеки, що використовують визначення 7.1.1, зупиняються спочатку, коли NULвони знаходять, і не знають чи не піклуються про те, щоб додаткові символи існували поза ним.
Blrfl

Я стою виправлений. Я шукав різні терміни, такі як "null", але пропустив 6.4.5.5, згадуючи "нульове значення".
bzeaman

3

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

Немає строкового типу даних на мові С, але є рядкові букви .

Якщо ви помістите рядковий літерал у свою програму, він зазвичай припиняється на NUL (але дивіться особливий випадок, обговорюваний у коментарях нижче). Тобто, якщо ви помістите "foobar"в місце, де const char *очікується значення, компілятор видасть foobar⊘на const / кодовий сегмент / розділ вашої програми, і значення виразу буде вказівником на адресу, де він зберігав fсимвол. (Примітка. Я використовую для позначення байта NUL.)

Єдиний інший сенс, у якому мова С має рядки, - це деякі стандартні підпрограми бібліотеки, які працюють на завершених NUL послідовностях символів. Ці бібліотечні процедури не існуватимуть у чистому металевому середовищі, якщо ви самі не портуєте їх.

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


2
Re: "Якщо ви помістите рядковий літерал у свою програму, він завжди буде припинено NUL": Ви впевнені в цьому? Я майже впевнений, що (наприклад) char foo[4] = "abcd";є дійсним способом створення ненульового завершення масиву з чотирьох символів.
ruakh

2
@ruakh, ой! це справа, яку я не розглядав. Я думав про рядковий літерал, який з’являється в місці, де очікується char const * вираз . Я забув, що ініціалізатори C іноді можуть підкорятися різним правилам.
Соломон повільно

@ruakh Літеральний рядок закінчується NUL. Масив - ні.
jamesdlin

2
@ruakh у вас є char[4]. Це не струна, але вона була ініціалізована з однієї
Калет

2
@Caleth, "ініціалізований з одного" - це не те, що має відбуватися під час виконання. Якщо ми додамо ключове слово staticдо прикладу Руаха, тоді компілятор може випустити не abcd, що закінчується NUL, в ініціалізований сегмент даних, щоб змінна ініціалізувалася завантажувачем програми. Отже, Руак мав рацію: Існує принаймні один випадок, коли поява рядкового літералу в програмі не вимагає від компілятора випускати NUL-завершений рядок. (ps, я фактично склав приклад з gcc 5.4.0, і компілятор не випромінював NUL.)
Соломон повільно

2

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

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

Наприклад, у ситуаціях, що запам'ятовують пам'ять, я б розглядав характеристики мого компілятора стосовно набору інструкцій процесора, наприклад. У додатку, де рядки обробляються дуже багато, можливо, буде бажано використовувати дескриптори, такі як довжина рядка. Я думаю про випадок, коли процесор особливо ефективний у роботі з короткими компенсаціями та / або відносними компенсаціями з адресними регістрами.

Що важливіше у вашій програмі: розмір та ефективність коду чи сумісність із ОС чи Бібліотекою? Ще одним врахуванням може бути ремонтопридатність. Чим далі ви відхилитесь від умовності, тим складніше буде підтримувати когось іншого.


1

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

Струни з нульовим завершенням. Тобто, вони закінчуються нульовим символом, NUL. Вони не закінчуються нульовим покажчиком NULL, що є абсолютно різного роду значенням із зовсім іншим призначенням.

NULгарантовано мати ціле значення нуль. У межах рядка він також матиме розмір основного типу символів, який зазвичай буде 1.

NULLне гарантовано мати цілий тип взагалі. NULLпризначений для використання в контексті покажчика, і, як правило, очікується, що він має тип вказівника, який не повинен перетворюватись на символ чи ціле число, якщо ваш компілятор корисний. Незважаючи на те, що визначення NULLвключає гліф 0, воно фактично не має такого значення [1], і якщо ваш компілятор не реалізує константу як односимвольний характер #define(багато хто з них не робить, бо NULL насправді це не повинно бути значимим у не- контекст вказівника), отже, розширений код не гарантує, що він фактично включає нульове значення (навіть якщо він заплутано включає нульовий гліф).

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

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

Тому, хоч рядки C недійсні, вони закінчуються не NULL, а написані NUL(як правило, написані '\0'). Код, який явно використовує NULLяк строковий термінатор, буде працювати на платформах із простою структурою адреси і навіть компілюватиметься з багатьма компіляторами, але це абсолютно не правильно.


[1] фактичне значення нульового вказівника вставляється компілятором, коли він читає 0 маркер у контексті, де він буде перетворений у тип вказівника. Це не є перетворенням з цілого значення 0, і не гарантується, що воно буде утримуватися, якщо використовується що-небудь, крім самого маркера 0, наприклад, динамічне значення зі змінної; перетворення також не є оборотним, і нульовий покажчик не повинен давати значення 0 при перетворенні в ціле число.


Чудова точка. Я надіслав редагування, щоб допомогти зрозуміти це.
Monty Harder

" NULгарантовано матиме ціле значення нуль." -> C не визначає NUL. Натомість C визначає, що рядки мають остаточну нульову функцію, байт зі всіма бітами, встановленими 0.
chux - Відновити Моніку

1

Я використовував рядок на C, це означає, що символи з нульовим завершенням називаються Strings.

При використанні в бареметалі або в будь-яких операційних системах, таких як Windows, Linux, RTOS: (FreeRTO, OSE) не виникне жодних проблем.

У вбудованому світі нульове завершення насправді допомагає більше маркувати символ як рядок.

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

Вам може бути цікаво, що насправді є рядком на C?

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

Щоразу, коли ви пишете рядок, укладений у подвійні лапки, C автоматично створює для нас масив символів, що містить цей рядок, що закінчується символом \ 0.

Наприклад, ви можете оголосити і визначити масив символів та ініціалізувати його з констатою рядка:

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

Відверта відповідь: Вам не потрібно турбуватися про використання символів з нульовим припиненням, це працює незалежно від будь-якої платформи.


Дякую, не знав, що при оголошенні подвійними лапками NULавтоматично додається a .
Snoop

1

Як вже говорили інші, нульове припинення є досить універсальним для стандарту С. Але (як і інші вказували) не на 100%. Для (іншого) прикладу операційна система VMS зазвичай використовувала те, що вона називала "дескриптори рядків" http://h41379.www4.hpe.com/commerce/c/docs/5492p012.html, доступ до яких на C здійснюється за допомогою #include <descriptionp.h >

На рівні програми можна використовувати нульове припинення чи ні, проте розробник вважає за потрібне. Але для VMS-матеріалів низького рівня абсолютно потрібні дескриптори, які взагалі не використовують нульове завершення (детальніше див. Вище за посиланням). Це значною мірою для того, що всі мови (C, збірка тощо), які безпосередньо використовують внутрішні системи VMS, можуть мати спільний інтерфейс з ними.

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


Сьогодні припинення 0 насправді досить незвичне. C ++ std :: string не робить, Java String не робить, Objective-C NSString не робить, Swift String не робить - в результаті кожна бібліотека мов підтримує рядки з кодами NUL всередині рядка (що неможливо з C рядки з зрозумілих причин).
gnasher729

@ gnasher729 Я змінив "... досить універсальний" на "майже універсальний для стандарту C", який, сподіваюся, усуває будь-яку неоднозначність і залишається правильним сьогодні (і що я мав на увазі відповідно до теми та питання ОП).
Джон Форкош

0

З мого досвіду вбудованих систем критичного та безпечного безпеки в режимі реального часу, не рідкість використання конвенцій рядків C і PASCAL, тобто надання довжини рядків як першого символу (що обмежує довжину до 255), а також закінчення рядок щонайменше з одним 0x00, ( NUL), що зменшує придатний розмір до 254.

Одна з причин цього - знати, скільки даних ви очікуєте після отримання першого байту, а інша полягає в тому, що в таких системах уникають динамічних розмірів буфера, де це можливо - виділення фіксованого розміру 256 буфера швидше і безпечніше (ні потрібно перевірити, якщо mallocне вдалося). Інша полягає в тому, що інші системи, з якими ви спілкуєтесь, можуть бути записані не в ANSI-C.

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.