Является ли инициализация char [] строковым литералом плохой практикой?

44

Я читал тему под названием «strlen vs sizeof» на CodeGuru , и в одном из ответов говорится, что «в любом случае [так] плохая практика - инициализировать [sic] charмассив со строковым литералом».

Это правда или это только его (хотя и "элитный член") мнение?


Вот оригинальный вопрос:

#include <stdio.h>
#include<string.h>
main()
{
    char string[] = "october";
    strcpy(string, "september");

    printf("the size of %s is %d and the length is %d\n\n", string, sizeof(string), strlen(string));
    return 0;
}

правильно. размер должен быть длина плюс 1 да?

это выход

the size of september is 8 and the length is 9

размер должен быть 10 обязательно. это похоже на вычисление размера строки до того, как она будет изменена на strcpy, но на длину после.

Что-то не так с моим синтаксисом или как?


Вот ответ :

В любом случае, плохо инициализировать массив символов строковым литералом. Поэтому всегда делайте одно из следующего:

const char string1[] = "october";
char string2[20]; strcpy(string2, "september");
Коул Джонсон
источник
Обратите внимание на «const» в первой строке. Может ли быть так, что автор предположил использовать c ++ вместо c? В c ++ это «плохая практика», потому что литерал должен быть константным, и любой недавний компилятор c ++ выдаст предупреждение (или ошибку) о назначении константного литерала неконстантному массиву.
Андре
@ Андре C ++ определяет строковые литералы как константные массивы, потому что это единственный безопасный способ работы с ними. Проблема в том, что С не является проблемой, поэтому у вас есть социальное правило, которое обеспечивает безопасность
Калет
@Caleth. Я знаю, я больше пытался утверждать, что автор ответа подходил к «плохой практике» с точки зрения c ++.
Андре
@ Андре, это не плохая практика в C ++, потому что это не практика , а прямая ошибка типа. Это должно быть ошибка типа в C, но это не так, поэтому вы должны иметь правило руководства по стилю, говорящее: «Это запрещено»
Caleth

Ответы:

59

В любом случае, плохо инициализировать массив символов строковым литералом.

Автор этого комментария никогда не оправдывает его, и я нахожу это утверждение загадочным.

В C (и вы пометили это как C), это практически единственный способ инициализировать массив charсо строковым значением (инициализация отличается от присваивания). Вы можете написать либо

char string[] = "october";

или

char string[8] = "october";

или

char string[MAX_MONTH_LENGTH] = "october";

В первом случае размер массива берется из размера инициализатора. Строковые литералы хранятся в виде массивов charс завершающим 0 байтом, поэтому размер массива равен 8 ('o', 'c', 't', 'o', 'b', 'e', ​​'r', 0). Во вторых двух случаях размер массива указывается как часть объявления (8 и MAX_MONTH_LENGTH, что бы это ни было).

Что вы не можете сделать, это написать что-то вроде

char string[];
string = "october";

или

char string[8];
string = "october";

и т. д. В первом случае объявление stringявляется неполным, поскольку не указан размер массива и нет инициализатора, из которого можно получить размер. В обоих случаях =не будет работать, потому что a) выражение массива, такое как, stringможет не быть целью назначения и b) =оператор не определен для копирования содержимого одного массива в другой в любом случае.

К тому же, вы не можете написать

char string[] = foo;

где fooдругой массив char. Эта форма инициализации будет работать только со строковыми литералами.

РЕДАКТИРОВАТЬ

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

char string[] = {'o', 'c', 't', 'o', 'b', 'e', 'r', 0};

или

char string[] = {111, 99, 116, 111, 98, 101, 114, 0}; // assumes ASCII

но на глазах проще использовать строковые литералы.

РЕДАКТИРОВАТЬ 2

Чтобы назначить содержимое массива вне объявления, вам необходимо использовать либо strcpy/strncpy(для строк с нулем в конце), либо memcpy(для любого другого типа массива):

if (sizeof string > strlen("october"))
  strcpy(string, "october");

или

strncpy(string, "october", sizeof string); // only copies as many characters as will
                                           // fit in the target buffer; 0 terminator
                                           // may not be copied, but the buffer is
                                           // uselessly completely zeroed if the
                                           // string is shorter!
Джон Боде
источник
@KeithThompson: не согласен, просто добавил его для полноты картины.
Джон Боде
16
Обратите внимание, что char[8] str = "october";это плохая практика. Мне нужно было буквально считать себя, чтобы убедиться, что это не переполнение и оно не работает при обслуживании ... например, исправление орфографической ошибки от seprateдо separateсломается, если размер не обновляется.
Джечлин
1
Я согласен с djechlin, это плохая практика по указанным причинам. Ответ JohnBode вообще не комментирует аспект «плохой практики» (который является основной частью вопроса !!), он просто объясняет, что вы можете или не можете сделать для инициализации массива.
мастов
Незначительное: поскольку возвращаемое значение 'length' strlen()не содержит нулевого символа, использование MAX_MONTH_LENGTHдля удержания максимального размера, необходимого для char string[]часто, выглядит неправильно. IMO, здесь MAX_MONTH_SIZEбыло бы лучше.
chux - Восстановить Monica
10

Единственная проблема, которую я помню, это присвоение строкового литерала char *:

char var1[] = "september";
var1[0] = 'S'; // Ok - 10 element char array allocated on stack
char const *var2 = "september";
var2[0] = 'S'; // Compile time error - pointer to constant string
char *var3 = "september";
var3[0] = 'S'; // Modifying some memory - which may result in modifying... something or crash

Например, возьмите эту программу:

#include <stdio.h>

int main() {
  char *var1 = "september";
  char *var2 = "september";
  var1[0] = 'S';
  printf("%s\n", var2);
}

На моей платформе (Linux) происходит сбой при попытке записи на страницу, помеченную как доступную только для чтения. На других платформах может отображаться «сентябрь» и т. Д.

Тем не менее, инициализация литералом делает конкретную сумму резервирования, так что это не будет работать:

char buf[] = "May";
strncpy(buf, "September", sizeof(buf)); // Result "Sep"

Но это будет

char buf[32] = "May";
strncpy(buf, "September", sizeof(buf));

В качестве последнего замечания - я бы не стал использовать strcpy:

char buf[8];
strcpy(buf, "very long string very long string"); // Oops. We overwrite some random memory

Хотя некоторые компиляторы могут преобразовать его в безопасный вызов strncpy, гораздо безопаснее:

char buf[1024];
strncpy(buf, something_else, sizeof(buf)); // Copies at most sizeof(buf) chars so there is no possibility of buffer overrun. Please note that sizeof(buf) works for arrays but NOT pointers.
buf[sizeof(buf) - 1] = '\0';
Мацей Печотка
источник
По-прежнему существует риск переполнения буфера, strncpyпоскольку он не завершает копируемую строку, если длина something_elseбольше чем sizeof(buf). Я обычно устанавливаю последний символ buf[sizeof(buf)-1] = 0для защиты от этого или, если bufинициализируется нулями, использую sizeof(buf) - 1в качестве длины копии.
syockit
Используйте strlcpyили strcpy_sили даже snprintfесли вам нужно.
user253751
Исправлена. К сожалению, нет простого переносимого способа сделать это, если вы не имеете роскоши работать с новейшими компиляторами ( strlcpyи snprintfне доступны напрямую на MSVC, по крайней мере, заказы и strcpy_sне на * nix).
Мацей Пехотка
@MaciejPiechotka: Ну, слава богу, Unix отклонил приложение, спонсируемое Microsoft.
Дедупликатор
6

Одна вещь, которую ни один поток не поднимает, состоит в следующем:

char whopping_great[8192] = "foo";

против

char whopping_great[8192];
memcpy(whopping_great, "foo", sizeof("foo"));

Первый будет делать что-то вроде:

memcpy(whopping_great, "foo", sizeof("foo"));
memset(&whopping_great[sizeof("foo")], 0, sizeof(whopping_great)-sizeof("foo"));

Последний делает только memcpy. Стандарт C настаивает на том, что если какая-либо часть массива инициализируется, все это так. Так что в этом случае лучше сделать это самостоятельно. Я думаю, что это, возможно, было то, к чему добиралась treuss.

Точно

char whopping_big[8192];
whopping_big[0] = 0;

лучше чем либо:

char whopping_big[8192] = {0};

или

char whopping_big[8192] = "";

ps Для получения бонусных баллов вы можете сделать:

memcpy(whopping_great, "foo", (1/(sizeof("foo") <= sizeof(whopping_great)))*sizeof("foo"));

бросить время компиляции делить на ноль ошибок, если вы собираетесь переполнить массив.

Ричард Файф
источник
5

Прежде всего потому, что у вас не будет размера char[]переменной / конструкции, которую вы могли бы легко использовать в программе.

Пример кода по ссылке:

 char string[] = "october";
 strcpy(string, "september");

stringразмещается в стеке длиной 7 или 8 символов. Я не могу вспомнить, завершен ли он таким образом или нет - нить, на которую вы ссылались, заявила, что это так.

Копирование «сентябрь» поверх этой строки является очевидным переполнением памяти.

Другая проблема возникает, если вы переходите stringк другой функции, чтобы другая функция могла писать в массив. Вы должны указать другой функции, как долго будет работать массив, чтобы он не создавал переполнение. Вы можете передать stringрезультат, strlen()но поток объясняет, как это может взорваться, если stringне завершено нулем.

Вам лучше выделить строку с фиксированным размером (предпочтительно определяемым как константа), а затем передать массив и фиксированный размер другой функции. Комментарии @John Bode верны, и есть способы уменьшить эти риски. Они также требуют больше усилий с вашей стороны, чтобы их использовать.

По моему опыту, значение, которое я инициализировал char[], обычно слишком мало для других значений, которые мне нужно поместить в него. Использование определенной константы помогает избежать этой проблемы.


sizeof stringдаст вам размер буфера (8 байт); используйте результат этого выражения вместо того, strlenкогда вы беспокоитесь о памяти.
Кроме того , вы можете сделать проверку перед вызовом , strcpyчтобы увидеть , если ваш целевой буфер достаточно большой для исходной строки: if (sizeof target > strlen(src)) { strcpy (target, src); }.
Да, если у вас есть , чтобы передать массив в функцию, вы должны будете пройти его физический размер , а также: foo (array, sizeof array / sizeof *array);. - Джон Боде

Сообщество
источник
2
sizeof stringдаст вам размер буфера (8 байт); используйте результат этого выражения вместо того, strlenкогда вы беспокоитесь о памяти. Кроме того , вы можете сделать проверку перед вызовом , strcpyчтобы увидеть , если ваш целевой буфер достаточно большой для исходной строки: if (sizeof target > strlen(src)) { strcpy (target, src); }. Да, если у вас есть , чтобы передать массив в функцию, вы должны будете пройти его физический размер , а также: foo (array, sizeof array / sizeof *array);.
Джон Боде
1
@JohnBode - спасибо, и это хорошие моменты. Я включил ваш комментарий в мой ответ.
1
Точнее, большинство ссылок на имя массива stringприводят к неявному преобразованию в char*, указывая на первый элемент массива. Это теряет информацию о границах массива. Вызов функции - это только один из многих контекстов, в которых это происходит. char *ptr = string;Другой. Даже string[0]является примером этого; []оператор работает на указатели, а не непосредственно на массивах. Рекомендуемая литература: Раздел 6 comp.lang.c FAQ .
Кит Томпсон
Наконец ответ, который на самом деле относится к вопросу!
мастов
2

Я думаю, что идея «плохой практики» исходит из того, что эта форма:

char string[] = "october is a nice month";

делает неявно strcpy из исходного машинного кода в стек.

Более эффективно обрабатывать только ссылку на эту строку. Как с:

char *string = "october is a nice month";

или напрямую:

strcpy(output, "october is a nice month");

(но, конечно, в большинстве кода это, вероятно, не имеет значения)

тото
источник
Разве он не сделает копию, только если вы попытаетесь изменить ее? Я думаю, что компилятор будет умнее этого
Коул Джонсон
1
А как насчет случаев, char time_buf[] = "00:00";когда вы собираетесь изменять буфер? char *Инициализируется строковый литерал устанавливается в адрес первого байта, поэтому пытается изменить это приводит к неопределенному поведению , так как метод хранения строкового литерала неизвестна (определяется реализацией), в то время как изменения в байтах char[]является совершенно законным , потому что инициализация копирует байты в пространство для записи, выделенное в стеке. Сказать, что это «менее эффективный» или «плохая практика» без уточнения нюансов, char* vs char[]вводит в заблуждение.
Брэден Бест
-3

Никогда не бывает очень долго, но вам следует избегать инициализации char [] в string, потому что «string» - это const char *, и вы назначаете его в char *. Так что, если вы передадите этот char [] методу, который изменяет данные, у вас может быть интересное поведение.

Как сказал коммент, я смешал немного char [] с char *, что не очень хорошо, так как они немного отличаются.

Нет ничего плохого в назначении данных массиву char, но поскольку целью использования этого массива является использование его как 'string' (char *), легко забыть, что вы не должны изменять этот массив.

Дайниус
источник
3
Неправильно. Инициализация копирует содержимое строкового литерала в массив. Объект массива не constбудет, если вы не определите его таким образом. (И строковые литералы в C не являются таковыми const, хотя любая попытка изменить строковый литерал имеет неопределенное поведение.) char *s = "literal";Есть поведение, о котором вы говорите; это лучше написать какconst char *s = "literal";
Кит Томпсон
действительно, моя вина, я смешал char [] с char *. Но я не был бы так уверен насчет копирования контента в массив. Быстрая проверка с помощью компилятора MS C показывает, что 'char c [] = "asdf";' создаст строку в сегменте const, а затем назначит этот адрес переменной массива. Это на самом деле причина, почему я сказал об избежании присваиваний неконстантному массиву символов.
Дайний,
Я скептически Попробуйте эту программу и дайте мне знать, какой вывод вы получаете.
Кит Томпсон
2
«А вообще« asdf »- это константа, поэтому она должна быть объявлена ​​как const». - То же самое рассуждение потребовало бы constвключения int n = 42;, потому что 42это константа.
Кит Томпсон
1
Неважно, на какой машине вы находитесь. Стандарт языка гарантирует, что его cможно изменять. Это точно такая же сильная гарантия, как и та, которая 1 + 1дает оценку 2. Если программа, на которую я ссылался выше, делает что-то кроме печати EFGH, это указывает на несоответствующую реализацию языка Си.
Кит Томпсон