Безопасно ли вставлять строку, в которой нет места для нулевого терминатора?

Рассмотрим следующий код:

const char foo[] = "lorem ipsum"; // foo is an array of 12 characters
const auto length = strlen(foo); // length is 11
string bar(length, '\0'); // bar was constructed with string(11, '\0')

strncpy(data(bar), foo, length);
cout << data(bar) << endl;

Насколько я понимаю, string всегда выделяются скрытым нулевым элементом. Если это так, то bar действительно выделяет 12 символов, при этом 12th является скрытым '\0', и это совершенно безопасно ... Если я ошибаюсь, тогда cout приведет к неопределенному поведению, потому что нет нулевого терминатора .

Может кто-нибудь подтвердить за меня? Это законно?


Было много вопросов о том, почему использовать strncpy вместо простого конструктора string(const char*, const size_t). Мое намерение состояло в том, чтобы сделать код моей игрушки близким к моему фактическому коду, который содержит vsnprintf. К сожалению, даже получив здесь отличные ответы, я обнаружил, что vsnprintf не ведет себя так же, как strncpy, и я задал следующий вопрос здесь: Почему vsnprintf не записывает такое же количество символов, как strncpy?

Как же этого не могло быть? Предполагая, конечно, что вы не копируете больше байтов, чем имеется доступное буферное пространство.

enhzflep 10.01.2019 17:09

@TrebuchetMS Да, спасибо, я исправил этот комментарий.

Jonathan Mee 10.01.2019 17:10

У вас есть реальный пример использования этого? Если вы дадите std::string c-образную струну, он будет делать то же самое, не царапая голову.

NathanOliver 10.01.2019 17:10

@NathanOliver Да, я использую vsnprintf для заполнения string. Просто казалось, что это только усложняет вопрос, который нужно задать, и не форсирует вопрос.

Jonathan Mee 10.01.2019 17:13

что такое string и что такое data()?

Slava 10.01.2019 17:18

@Slava У меня есть злой using namespace std до этого игрушечного примера. Итак, все это извлекается из стандартного пространства имен.

Jonathan Mee 10.01.2019 17:20

Тогда почему бы не std::string bar( foo, length ); вместо надоедливого strncpy()?

Slava 10.01.2019 17:21

@JonathanMee Есть ли причина, по которой вы не используете stringstream, чтобы полностью этого избежать?

NathanOliver 10.01.2019 17:29

@Slava На самом деле, потому что я не так делаю в своем реальном коде. Но это сработало бы. В моем реальном коде я дважды вызываю vsnprintf, один раз, чтобы получить размер, который я использую для выделения string, затем я снова вызываю vsnprintf, заполняя string.

Jonathan Mee 10.01.2019 17:34

Почему бы не использовать с ним char buffer[arbitrarySize], затем vsnprintf, а затем просто создать std::string, используя этот буфер и размер? Вы действительно думаете, что вызов vsnprintf() дважды более эффективен, чем создание массива символов в стеке?

Slava 10.01.2019 17:38

@NathanOliver va_list используется как способ, позволяющий интерфейсу C принимать информацию журнала. Так что не гарантируется, что на другой стороне интерфейса даже будет stringstream.

Jonathan Mee 10.01.2019 17:39

@Slava arbitrarySize может быть слишком маленьким, не так ли?

Jonathan Mee 10.01.2019 17:43

Вы имели ввиду cout << bar << endl? В противном случае я не уверен в сути этой части вопроса.

Lightness Races in Orbit 10.01.2019 17:49

Наверное. В любом случае я бы вернул std в ваш пример и изменил его на snprintf( bar.data(), bar.size(), "format", data ) вместо strncpy(), чтобы избежать путаницы.

Slava 10.01.2019 17:53

@Slava Двойной вызов - довольно распространенный подход, позволяющий избежать случайного угадывания необходимого размера. Хотя компромиссы различаются.

Lightness Races in Orbit 10.01.2019 17:54

@LightnessRacesinOrbit Это действительно вопрос о лежащем в основе string. Я хочу знать, будет ли существовать 12-й «скрытый нулевой символ», даже если я построю его как string(11, '\0')

Jonathan Mee 10.01.2019 17:59

@LightnessRacesinOrbit, конечно, тогда вопрос должен отражать это вместо уродливого примера использования strncpy(), который приводит к вопросу, почему бы не создать std::string прямо из него? Использование snprintf() в этом примере нисколько не усложнит его.

Slava 10.01.2019 18:02

@Slava Я согласен с тем, что использование strncpy здесь не является необходимым и не рекомендуется (и некоторое время назад я поддержал ваш комментарий по этому поводу). Хотя это не отменяет вопрос!

Lightness Races in Orbit 10.01.2019 18:33

@LightnessRacesinOrbit Я не говорил, что это так (и я бы отклонил этот вопрос, если я так думаю), я просто предложил улучшения. Использование snprintf или подобных ему понятно, использование strncpy в этом случае стимулирует несвязанные дискуссии.

Slava 10.01.2019 19:40

@Slava В самом деле! :)

Lightness Races in Orbit 10.01.2019 19:43
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
20
249
4
Перейти к ответу Данный вопрос помечен как решенный

Ответы 4

Да, это безопасно согласно char * strncpy (char * назначение, const char * source, size_t num):

Copy characters from string

Copies the first num characters of source to destination. If the end of the source C string (which is signaled by a null-character) is found before num characters have been copied, destination is padded with zeros until a total of num characters have been written to it.

Проблема в том, что есть дополнительный символ, который нужно скопировать.

Matthieu Brucher 10.01.2019 17:12

Не в этом вопрос, очевидно, что strncpy не будет записывать за границу. Вопрос в том, выйдет ли coutчитать за пределы допуска. Исправьте ответ, чтобы задать вопрос, или удалите.

Jonathan Mee 10.01.2019 17:14

@Matthieu - Не обязательно. Если целью является std::string, он сам знает ее длину.

StoryTeller - Unslander Monica 10.01.2019 17:15

@StoryTeller, однако, отсутствует \0 (при использовании с data(bar)). Это действительно нормально при использовании самой строки.

Matthieu Brucher 10.01.2019 17:16

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

StoryTeller - Unslander Monica 10.01.2019 17:18

@StoryTeller действительно полностью согласен.

Matthieu Brucher 10.01.2019 17:19

Здесь есть две разные вещи.

Во-первых, добавляет ли strncpy дополнительный \0 в этом случае (11 элементов, не относящихся к \0, которые должны быть скопированы в строку размером 11). Ответ - нет:

Copies at most count characters of the byte string pointed to by src (including the terminating null character) to character array pointed to by dest.

If count is reached before the entire string src was copied, the resulting character array is not null-terminated.

Так что звонок в порядке.

Затем data() дает вам правильную строку с завершением \0:

c_str() and data() perform the same function. (since C++11)

Таким образом, похоже, что для C++ 11 вы в безопасности. В документации не указано, выделяет ли строка дополнительный \0 или нет, но API ясно, что то, что вы делаете, прекрасно.

Чтобы расширить ваше утверждение, вы говорите, что stringне является выделен скрытым нулевым ограничителем?

Jonathan Mee 10.01.2019 17:18

Распределение должно работать таким образом, чтобы c_str() / data() был O(1), учитывая, что при индексировании str[str.size()] вы получаете '\0' (начиная с C++ 11). На практике они всегда так работали.

Lightness Races in Orbit 10.01.2019 17:52

Вы выделили 11-значный std::string. Вы не пытаетесь читать или писать что-либо помимо этого, так что эта часть будет в безопасности.

Итак, настоящий вопрос заключается в том, не испортили ли вы внутреннюю структуру струны. Если вы не сделали ничего недопустимого, как это возможно? Если требуется, чтобы строка содержала внутри 12-байтовый буфер с нулевым заполнением в конце для выполнения своего контракта, это будет иметь место независимо от того, какие операции вы выполняли.

Ответ принят как подходящий

Это безопасно, если вы копируете символы [0, size()) в строку. За [basic.string] / 3

In all cases, [data(), data() + size()] is a valid range, data() + size() points at an object with value charT() (a “null terminator”), and size() <= capacity() is true.

Таким образом, string bar(length, '\0') дает вам строку с size(), равным 11, с неизменным нулевым ограничителем в конце (всего 12 символов фактического размера). Пока вы не перезаписываете этот нулевой терминатор и не пытаетесь прописать его, все в порядке.

Не уверен, что с ним все будет в порядке - std::string::length() выдаст "неправильную" информацию

Slava 10.01.2019 17:22

@Slava Как бы это было не так? Строка начинается с размера 11, который не может измениться, если они не используют строковую функцию. Если они копируют только 5 символов, у них все еще остается строка размером 11, у нее просто есть 6 дополнительных нулей, завершающих ее.

NathanOliver 10.01.2019 17:24

Я заключил «неправильно» в двойные кавычки. Да, он покажет правильный размер буфера, но использование этого в качестве строки может привести к некрасивым проблемам, которые очень сложно обнаружить и исправить позже, когда эта строка будет использоваться.

Slava 10.01.2019 17:26

@Slava std::string может содержать нулевые символы, в отличие от C-строки.

Mark Ransom 10.01.2019 17:26
strlen считает только строки в стиле C. Действительно, размер, указанный в строке, является правильным.
Matthieu Brucher 10.01.2019 17:28

@MarkRansom Я понимаю это, поэтому я не утверждаю, что он наверняка сломается, но есть довольно высокая вероятность, что в дальнейшем возникнут проблемы - поскольку большинство разработчиков ожидают, что length() предоставит вам длину строки, а не размер буфера. Поэтому я бы избегал такого кода, если не хочу проблем, и использовал бы std::vector<char>, если мне нужен буфер.

Slava 10.01.2019 17:28

@Slava Я определенно вижу потенциал для проблем, но я думаю, что вы его преувеличиваете. length() действительно дает вам истинный размер строки, это сбивает с толку strlen(). Вы столкнетесь с проблемой только тогда, когда смешаете код, который работает с string и C-строками, и только тогда, если у вас есть встроенный нулевой символ, что не относится к приведенному примеру.

Mark Ransom 10.01.2019 17:32

@MarkRansom "Вы столкнетесь с проблемой только тогда, когда смешаете код, который работает со строкой и C-строкой", правильно говоря, что вы, вероятно, будете в порядке с человеком, который спрашивает, можно ли использовать strncpy(), поскольку маловероятно, что вы будете иметь дело с C-струны. Да, я преувеличиваю.

Slava 10.01.2019 17:33

@Slava Я слышу сарказм в твоем ответе. Я надеюсь, что причина для копирования на string в первую очередь состоит в том, чтобы работать над ним в этой форме с этого момента, и с этого момента встроенный нуль в основном безвреден. Опять же, как показывает вопрос.

Mark Ransom 10.01.2019 17:40

@MarkRansom Я пытаюсь сказать, что если кто-то пытается преобразовать strncpy() в std::string, то есть довольно высокая вероятность, что эта строка будет преобразована / обработана как C-строка позже. Поэтому я бы не стал использовать std::string в качестве буфера, если не хочу тратить много времени на отладку, которую трудно выявить. Но это всего лишь мое скромное мнение, поэтому я заключил слово «неправильно» в двойные кавычки.

Slava 10.01.2019 17:45

@Slava «поскольку большинство разработчиков ожидают, что length () даст вам длину строки, а не размер буфера». Но это именно то, что он делает. Строка - это последовательность байтов. Ближе к концу есть несколько нулевых байтов. Вам следует избавиться от привычки думать, что «строка» эквивалентна «c-строке». (Между тем, буфер мог бы быть несколько больше благодаря .reserve() и т. д.)

Lightness Races in Orbit 10.01.2019 17:50

@Slava "тогда очень высока вероятность того, что эта строка будет преобразована / обработана как C-строка позже" обрабатывали может быть проблемой, но преобразованный (с использованием strlen и strcpy of the data ()) должен быть безопасным.

Bob__ 10.01.2019 17:57

@LightnessRacesinOrbit "Вам следует избавиться от привычки думать, что" строка "эквивалентна" c-строке "." Проблема не в том, чтобы избавиться от этой привычки, а в том, чтобы заставить всех разработчиков делать это. У меня нет такой возможности, поэтому безопаснее использовать std::vector<char>, когда мне нужен буфер, и std::string, когда мне нужна строка, которую можно рассматривать как C-строку.

Slava 10.01.2019 18:00

@Slava Если разработчики вокруг вас относятся к std::string как к C-строке, тогда они не разработчики на C++ .... (хотя я соглашусь, что vector<char> - или vector<byte>! - часто лучше по ряду причин)

Lightness Races in Orbit 10.01.2019 18:14

Другие вопросы по теме