Распространенные ошибки

11.

Использование функции strncpy как функции "безопасного" копирования строк

Функция

strncpy

появилась в стандартной библиотеке языка С для решения одной узкоспециализированной задачи - перевода классических нуль-терминированных строк в так называемые

строки фиксированной ширины

.

Строки фиксированной ширины

- это особый способ представления строк, который использовался в файловой системе Unix во времена, когда язык С был еще очень молодым.

В формате фиксированной ширины строка хранится в соответствии со следующими правилами:

1. Все строки хранятся в буфере заранее заданной фиксированной ширины N

2. Если строка имеет длину меньше N, то все лишние позиции в строке заполняются нулевыми символами

3. Если строка имеет длину ровно N, то она полностью заполняет буфер и не заканчивается нулевым символом

В частности, файловая система Unix System V использовала такие строки длины 14 для представления имен файлов. Такой формат представления строк был выбран с одной-единственной целью - сэкономить один байт при представлении имен максимальной длины. При использовании классических нуль-терминированных строк имя длины 14 потребовало бы 15 байт, в то время как в соответствии с соглашениями формата фиксированной ширины такая строка умещается в 14 байт.

Для того, чтобы сравнивать строки фиксированной ширины между собой применялась функция

memcmp

на всю фиксированную ширину буфера. Требование заполнения всего неиспользуемого хвоста строки нулями как раз и обеспечивало работоспособность и корректность такого сравнения.

Так вот, функция

strncpy

предназначалась именно для инициализации таких буферов. Например, приведенные ниже вызовы функции

strncpy

формируют правильно проинициализированные строки фиксированной ширины 14 в своих целевых буферах

CВыделить код

1
2
3
4

char target1[14], target2[14];
 
strncpy(target1, "hello", sizeof target1);
strncpy(target2, "hello, world !", sizeof target2);

Обратите внимание, что во втором случае целевой буфер не будет нуль-терминирован, в полном соответствии со спецификацией строк фиксированной ширины. А в первом случае он будет, неформально выражаясь, нуль-терминирован 9 раз.

Однако времена поменялись, необходимость экономить один байт из пятнадцати практически отпала и функция

strncpy

стала почти бесполезной... Но тут случилось страшное. Кто-то малознакомый и с конкретной спецификацией функции

strncpy

и с изначальным предназначением вдруг пришел к выводу, что эта функция предназначена (и пригодна) для "безопасного" копирования обыкновенных нуль-терминированых строк. Такого человека можно понять, ибо функция

strncpy

названа довольно неудачно: ее название как-будто намекает на то, что эта функция предназначена именно для ограниченного по длине копирования строк. А документация... Кто ж читает документацию, когда и из названия функции все понятно?

Разумеется, пользователи функции быстро заметили, что она не выполняет нуль-терминацию целевой строки, когда при копировании был достигнут предел длины целевого буфера. В результате родилась следующая неприглядная идиома

CВыделить код

1
2
3
4

char target[BUFFER_SIZE];
...
strncpy(target, source, BUFFER_SIZE);
target[BUFFER_SIZE - 1] = '0';

Т.е. после использования

strncpy

надо обязательно не забыть выполнить нуль-терминацию целевой строки "на всякий случай". Именно в рамках такого применения функция

strncpy

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

Понятно, что

strncpy

в такой роли решает поставленную задачу, но никогда не работает хорошо. Если исходная строка короче целевого буфера, то

strncpy

будет выполнят дурную и ненужную работу: заполнять всю хвостовую часть буфера нулями. Если же строка длиннее целевого буфера, то

strncpy

наоборот забудет выполнить нужную и важную работу: выполнить нуль-терминацию целевой строки. Такое применение

strncpy

чем-то сродни забиванию шурупов молотком: понятно, что неправильно, но вроде держится.

Другими словами, если функию

strncpy

еще можно как-то заставить работать для безопасного копирования строк - при помощи ручной нуль-терминации целевой строки (как в примере выше), то использование

strncpy

без такой нуль-терминации всегда является ошибкой. Однако в реальном коде часто можно встретить именно такое использование

strncpy

с полным игнорированием необходимости обеспечить нуль-терминацию.

К сожалению, стандартная библиотека языка С не предоставляет хорошей функции, выполняющей именно ограниченное по длине копирование обычных С-строк (это еще одна причина популярности

strncpy

в этой роли). Поэтому реализации стандартной библиотеки в системах Unix традиционно предоставляют свою де-факто стандартную функцию -

strlcpy

- предназначенную именно для этой цели.

strlcpy

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

strlcpy

, то лучше написать эту функцию самостоятельно, чем замусоривать ваш код некачественным применением

strncpy

.

Но, если уж вам все таки хочется пользоваться

strncpy

, то помните - эта функция не гарантирует того, что ваша строка будет правильно нуль-терминирована. Не забываете обеспечить корректную нуль-терминацию самостоятельно.