Чи ініціалізація знака [] зі строковою літеральною неправильною практикою?


44

Я читав нитку під назвою "strlen vs sizeof" у CodeGuru , і одна з відповідей говорить, що "все одно [sic] погана практика ініціалізувати [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, але довжина після.

Чи щось не так у моєму синтаксисі чи що?


Ось відповідь :

Інакше погана практика ініціалізувати масив char із рядковим літералом. Тому завжди виконайте одне з наступного:

const char string1[] = "october";
char string2[20]; strcpy(string2, "september");

Зверніть увагу на "const" у першому рядку. Чи може бути, що автор припустив c ++ замість c? У c ++ це "погана практика", тому що літерал повинен бути const, а будь-який останній компілятор c ++ видасть попередження (або помилку) про присвоєння const literal масиву non-const.
Андре

@ André C ++ визначає літеральні рядки як масиви const, оскільки це єдиний безпечний спосіб поводження з ними. Це не проблема, тому у вас є соціальне правило, яке застосовує безпечну річ
Калет

@Caleth. Я знаю, я більше намагався стверджувати, що автор відповіді наближався до "поганої практики" з точки зору c ++.
Андре

@ André - це не погана практика в C ++, оскільки це не практика , це помилка прямого типу. Це має бути помилка типу в C, але це не так, тому у вас має бути правило керівництва стилем, яке скаже вам "Це заборонено"
Caleth

Відповіді:


59

Інакше погана практика ініціалізувати масив char із рядковим літералом.

Автор цього коментаря ніколи насправді не виправдовує цього, і я вважаю це твердження дивним.

У 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. Ця форма ініціалізації працюватиме лише з рядковими літералами.

EDIT

Я повинен змінити це, щоб сказати, що ви також можете ініціалізувати масиви, щоб утримувати рядок з ініціалізатором у стилі масиву, наприклад

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

або

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

але на очах простіше використовувати рядкові букви.

EDIT 2

Щоб призначити вміст масиву поза декларацією, вам потрібно буде використовувати або strcpy/strncpy(для рядків, що закінчуються 0), або 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"зламається", якщо розмір не буде оновлений.
djechlin

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

Незначне: Оскільки значення "length", яке повертається strlen(), не містить нульового символу, використовуючи MAX_MONTH_LENGTHдля утримання максимального розміру, необхідного для char string[]часто виглядає не так. IMO, MAX_MONTH_SIZEтут буде краще.
chux - Поновіть Моніку

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якщо вам доведеться.
користувач253751

Виправлено. На жаль, не існує простого портативного способу зробити це, якщо у вас немає розкоші працювати з новітніми компіляторами ( strlcpyі snprintfви не доступні безпосередньо в MSVC, принаймні замовлення та strcpy_sне на * nix).
Maciej Piechotka

@MaciejPiechotka: Ну, слава богу Unix відхилив додаток k, спонсорований мікрософт.
Дедупликатор

6

Одне, що не викликає жодна тема, це:

char whopping_great[8192] = "foo";

vs.

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 наполягає на тому, що якщо будь-яка частина масиву ініціалізується, це все є. Так що в цьому випадку краще зробити це самостійно. Я думаю, що це, можливо, було те, про що йшлося.

Точно

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" є const char *, і ви призначаєте його до char *. Тож якщо ви передасте цей знак [] методу, який змінює дані, ви можете мати цікаву поведінку.

Як сказала похвала, я змішав трохи char [] з char *, це не добре, оскільки вони трохи відрізняються.

Немає нічого поганого в призначенні даних масиву char, але оскільки намір використовувати цей масив - використовувати його як "рядок" (char *), легко забути, що ви не повинні змінювати цей масив.


3
Неправильно. Ініціалізація копіює вміст літерального рядка в масив. Об'єкт масиву не є, constякщо ви не визначите його таким чином. (І рядкові літерали в C не є const, хоча будь-яка спроба змінити літеральний рядок має неозначене поведінку.) char *s = "literal";Має таку поведінку, про яку ви говорите; краще написано якconst char *s = "literal";
Кіт Томпсон

Дійсно, я винен, я змішав char [] з char *. Але я не був би таким впевненим у копіюванні вмісту в масив. Швидкий пошук за допомогою компілятора MS C показує, що 'char c [] = "asdf";' створить 'рядок' у сегменті const, а потім призначить цю адресу змінній масиву. Це насправді причина, чому я сказав про те, щоб уникнути присвоєння масиву non const char.
Дайній

Я скептично. Спробуйте цю програму і дайте мені знати, який результат ви отримаєте.
Кіт Томпсон

2
"І взагалі" asdf "є постійною, тому його слід оголосити як const." - Ті ж міркування закликали б constувімкнути int n = 42;, тому що 42це постійна.
Кіт Томпсон

1
Не має значення, на якій машині ви знаходитесь. Стандарт мови гарантує cзміну. Це точно така ж гарантія, як та, яка 1 + 1оцінює 2. Якщо програма, до якої я посилався вище, робить щось інше, ніж друк EFGH, це вказує на невідповідну реалізацію C.
Кіт Томпсон
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.