Чи повинні функції бібліотеки С завжди очікувати довжини рядка?


15

В даний час я працюю над бібліотеки , написаної на C. Багато функцій цієї бібліотеки Очікувати рядок , як char*і const char*в своїх міркуваннях. Я почав з тих функцій, завжди очікуючи довжини рядка, size_tтак що нульове завершення не потрібно. Однак при написанні тестів це призводило до частого використання strlen()таких матеріалів:

const char* string = "Ugh, strlen is tedious";
libFunction(string, strlen(string));

Довіра користувачеві передавати належним чином завершені рядки призведе до менш безпечного, але більш короткого та (на мій погляд) читабельного коду:

libFunction("I hope there's a null-terminator there!");

Отже, яка тут розумна практика? Зробити API більш складним у використанні, але змусити користувача думати про їх введення або документувати вимогу для нульового завершення рядка та довіряти абоненту?

Відповіді:


4

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

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

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

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

Пізніше відредагуйте : це досить жива дискусія, тому я додам, що довіряти всім нижче і вище, щоб ви були приємними, і використовувати функції str * бібліотеки, це нормально, поки ви не побачите класичні речі, як-от output = malloc(strlen(input)); strcpy(output, input);або while(*src) { *dest=transform(*src); dest++; src++; }. Я майже чую «Лакримозу» Моцарта на задньому плані.


1
Я не розумію вашого прикладу API Windows, який вимагає від абонента вказувати довжину рядків. Наприклад, типова функція API Win32, така як CreateFileприймає LPTCSTR lpFileNameпараметр як вхід. Довжина рядка від абонента не очікується. Насправді використання рядків, що закінчуються NUL, настільки вбудовані, що в документації навіть не згадується, що ім'я файлу повинно бути закінченим NUL (але, звичайно, повинно бути).
Грег Х'югілл

1
Насправді в Win32 LPSTRтип говорить про те, що рядки можуть бути завершені NUL, а якщо ні , то це буде вказано в пов'язаній специфікації. Тому, якщо спеціально не вказано інше, очікується, що такі рядки в Win32 будуть припинені NUL.
Грег Хьюгілл

Чудова справа, я був неточним. Подумайте, що CreateFile та його купа існують з Windows NT 3.1 (на початку 90-х); поточний API (тобто з моменту впровадження Strsafe.h в XP SP2 - із публічними вибаченнями Microsoft) явно знехтував усі речі, які закінчуються NULL. Перший раз, коли Microsoft відчув дуже шкода за використання рядків, що закінчуються NULL, насправді був набагато раніше, коли їм довелося ввести BSTR у специфікації OLE 2.0, щоб якось привести VB, COM та старий WINAPI в одній лодці.
vski

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

1
Так. Рядки незмінні як на JVM / Dalvik, так і на .NET CLR на рівні платформи, а також на багатьох інших мовах. Я б пішов так далеко і міркував, що рідний світ ще не може цього зробити (стандарт C ++ 11) через а) спадщину (ви насправді не так багато отримуєте, маючи лише частину ваших струн незмінних) і b ) для цієї роботи вам справді потрібні GC та рядкова таблиця, а розподільники діапазону в C ++ 11 не можуть її дуже скоротити.
vski

16

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

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


-1, вибачте, це просто нераціонально.
vski

У старі часи це не завжди було правдою. Я багато працював з двійковими протоколами, які розміщували рядкові дані у поля фіксованої довжини, які не припинялися NULL. У таких випадках дуже сильно працювали з функціями, що займали тривалість. Я ще не робив С за десятиліття.
Gort the Robot

4
@vski, як змушує користувача викликати "strlen" перед викликом цільової функції чимось, щоб уникнути проблем із переповненням буфера? Принаймні, якщо ви самі перевіряєте довжину в межах цільової функції, ви можете бути впевнені в тому, яке відчуття довжини використовується (включаючи термінальний нуль чи ні).
Чарльз Е. Грант

@Charles E. Grant: Дивіться коментар вище про StringCbCat та StringCbCatN в Strsafe.h. Якщо у вас просто є char * і немає довжини, тоді вам справді не залишається іншого вибору, крім використання функцій str *, але суть у тому, щоб переносити довжину навколо, таким чином, вона стає опцією між str * і strn * функції яких переважні останні.
vski

2
@vski Немає необхідності обходити довжину рядка . Там є необхідність пройти навколо буфера довжини «s. Не всі буфери є рядками, і не всі рядки буфери.
jamesdlin

10

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

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

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

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


Категорично не згоден з таким підходом. Ніколи не довіряйте своїм абонентам, особливо за API бібліотеки, докладайте максимум зусиль, щоб поставити під сумнів речі, які вони вам дають, і вийти з ладу. Переносьте промальовану довжину, робота з рядками, що закінчуються NULL, - це не те, що означає "бути розкутим з вашими абонентами та суворим з вашими кальєрами".
vski

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

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

2

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

Дані символів, що не закінчуються нулем, ніколи не повинні називатися "рядками". Її обробка (і метання довжин навколо) зазвичай має бути інкапсульована в межах бібліотеки, а не частиною API. Потрібна довжина як параметр, щоб уникнути одиночних викликів strlen (), ймовірно, передчасна оптимізація.

Довіряти абоненту функції API не є небезпечним ; невизначена поведінка є цілком нормальною, якщо задокументовані передумови не виконані.

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


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

1

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


4
Незалежно від того, чи функціонує бібліотека з вбудованими NULL в рядках, слід дуже добре зафіксувати. Більшість функцій бібліотеки С зупиняються на NULL або довжині, залежно від того, що відбувається раніше. (І якщо написано грамотно, ті, хто не забирає тривалості, ніколи не використовують strlenу циклі тестування.)
Gort the Robot

1

Ви повинні розрізняти проходження навколо рядка і проходження навколо буфера .

У C рядки традиційно закінчуються NUL. Цього цілком розумно очікувати. Тому зазвичай не потрібно пропускати довжину струни; з ним можна обчислити, strlenякщо це необхідно.

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

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

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


Це саме те, що питання та інші відповіді пропустили.
Blrfl

0

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

void use_string(char *string, int length);

можна визначити макрос:

#define use_strlit(x) use_string(x, sizeof ("" x "")-1)

а потім викликати його, як показано на:

void test(void)
{
  use_strlit("Hello");
}

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

Альтернативним підходом у C99 було б визначити тип структури «вказівник і довжина» та визначити макрос, який перетворює рядковий літерал у складений літерал цього типу структури. Наприклад:

struct lstring { char const *ptr; int length; };
#define as_lstring(x) \
  (( struct lstring const) {x, sizeof("" x "")-1})

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

struct lstring *p;
if (foo)
{
  p = &as_lstring("Hello");
}
else
{
  p = &as_lstring("Goodbye!");
}
use_lstring(p);

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

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