Як масиви символів повинні використовуватися як рядки?


10

Я розумію, що рядки в C - це просто масиви символів. Тому я спробував наступний код, але він дає дивні результати, такі як вихід сміття або збої програми:

#include <stdio.h>

int main (void)
{
  char str [5] = "hello";
  puts(str);
}

Чому це не працює?

Він складено чисто з gcc -std=c17 -pedantic-errors -Wall -Wextra.


Примітка: Ця публікація призначена для використання як канонічного поширеного запитання для проблем, що виникають із-за невдачі місця для термінатора NUL при оголошенні рядка.

Відповіді:


12

Рядок змінного струму - це масив символів, який закінчується нульовим термінатором .

Усі символи мають значення таблиці символів. Нульовим термінатором є значення символу 0(нуль). Він використовується для позначення кінця рядка. Це необхідно, оскільки розмір рядка ніде не зберігається.

Тому щоразу, коли ви виділяєте місце для рядка, ви повинні містити достатньо місця для нульового символу термінатора. Ваш приклад цього не робить, він виділяє місце лише для 5 символів "hello". Правильний код повинен бути:

char str[6] = "hello";

Або рівнозначно, ви можете написати код самодокументування на 5 символів плюс 1 нульовий термінатор:

char str[5+1] = "hello";

Коли динамічно розподіляється пам'ять для рядка під час виконання, вам також потрібно виділити місце для нульового термінатора:

char input[n] = ... ;
...
char* str = malloc(strlen(input) + 1);

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

Найбільш поширений спосіб , щоб написати нульовий термінатор символ в C є використання так званої «вісімкове послідовність», дивлячись , як це: '\0'. Це на 100% еквівалентно написанню 0, але \служить кодом самодокументування, щоб стверджувати, що нуль явно означає нульовий термінатор. Код, такий як if(str[i] == '\0')буде перевіряти, чи є конкретним символом нульовий термінатор.

Зверніть увагу, що термін null термінатор не має нічого спільного з нульовими покажчиками або NULLмакросом! Це може заплутати - дуже схожі назви, але дуже різні значення. Ось чому нульовий термінатор іноді називають NULодним L, не плутати з NULLнульовими вказівниками. Детальнішу інформацію див. У відповіді на це запитання .

Код "hello"у вашому коді називається літеральним рядком . Це слід розцінювати як рядок лише для читання. У ""синтаксичному означає , що компілятор додасть нульовий термінатор в кінці рядка буквальних автоматично. Отже, якщо ви роздрукуєте, sizeof("hello")ви отримаєте 6, а не 5, тому що ви отримаєте розмір масиву, включаючи нульовий термінатор.


Він чисто компілюється з gcc

Дійсно, навіть не попередження. Це пов’язано з тонкою деталізацією / недоліком на мові С, яка дозволяє ініціалізувати масиви символів рядковим літералом, який містить стільки ж символів, скільки є місця в масиві, а потім мовчки відкинути нульовий термінатор (C17 6.7.9 / 15). Мова цілеспрямовано поводиться так з історичних причин, див. Невідповідну діагностику gcc для ініціалізації рядків для деталей. Також зауважте, що C ++ тут ​​інший і не дозволяє використовувати цю хитрість / недолік.


1
Ви повинні згадати char str[] = "hello";випадок.
Jabberwocky

@Jabberwocky Це вікі спільноти, не соромтеся редагувати та надавати внески.
Лундін

1
... а може бути і char *str = "hello";... str[0] = foo;проблема.
Бармаглот

Можливо, розширити сенс використання sizeofдо його використання на функціональному параметрі, особливо коли він визначений як масив.
Погода Vane

@WeatherVane Потрібно охопити іншим поширеним запитанням тут: stackoverflow.com/questions/492384/…
Lundin

4

Із стандарту С (7.1.1. Визначення термінів)

1 Рядок - це суцільна послідовність символів, що закінчується першим нульовим символом і включає його. Термін багатобайтовий рядок іноді використовується замість того, щоб підкреслити спеціальну обробку, що надається багатобайтовим символам, що містяться в рядку, або щоб уникнути плутанини з широким рядком. Вказівник на рядок - це вказівник на його початковий (найнижчий адресний) символ. Довжина рядка - це кількість байтів, що передують нульовому символу, а значення рядка - це послідовність значень містяться символів у порядку.

У цій декларації

char str [5] = "hello";

рядковий літерал "hello"має таке внутрішнє подання

{ 'h', 'e', 'l', 'l', 'o', '\0' }

тому він має 6 символів, включаючи закінчуючий нуль. Його елементи використовуються для ініціалізації масиву символів, strякий залишає простір лише для 5 символів.

Стандарт C (протилежний стандарту C ++) дозволяє таку ініціалізацію символьного масиву, коли закінчуючий нуль рядкового літералу не використовується як ініціалізатор.

Однак в результаті символьний масив strне містить рядка.

Якщо ви хочете, щоб масив містив рядок, який ви можете записати

char str [6] = "hello";

або просто

char str [] = "hello";

В останньому випадку розмір масиву символів визначається з числа ініціалізаторів рядкового літералу, що дорівнює 6.


0

Чи всі рядки можна вважати масивом символів ( Так ), чи всі масиви символів можна вважати рядками ( Ні ).

Чому ні? і чому це важливо?

На додаток до інших відповідей, що пояснюють, що довжина рядка ніде не зберігається як частина рядка та посилання на стандарт, де визначена рядок, зворотний бік - "Як функції бібліотеки C обробляють рядки?"

Хоча масив символів може містити однакові символи, це просто масив символів, якщо за останнім символом не супроводжується символом, що закінчується нульовим завершенням . Цей нульовий символ - це те, що дозволяє масиву символів вважати рядком.

Усі функції в C, які очікують рядок як аргумент, очікують, що послідовність символів буде скасована на нуль . Чому?

Це пов'язано з тим, як працюють всі рядкові функції. Оскільки довжина не включена як частина масиву, рядкові функції сканують в масив вперед, поки не буде знайдено нульовий символ (наприклад '\0'- еквівалент десятковому 0). Див. Таблицю та опис ASCII . Незалежно від того, чи використовуєте ви strcpy, strchr, strcspnі т.д .. Все рядкові функції покладаються на NUL кінцевого характеру присутнім визначити , де кінець цього рядка є.

Порівняння двох подібних функцій від string.hпідкреслить важливість нульового припинення характеру. Візьмемо для прикладу:

    char *strcpy(char *dest, const char *src);

strcpyФункція просто копіює байти з srcдо destдо NUL кінцевого символ, не знайдено розповідаючи , strcpyде зупинити копіювання символів. Тепер візьміть аналогічну функцію memcpy:

    void *memcpy(void *dest, const void *src, size_t n);

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

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

Ось чому функції, які очікують нуль-закінченого рядка, повинні передаватися нульовим завершеним рядком і чому це має значення .


0

Інтуїтивно ...

Подумайте про масив як про змінну (містить речі), а рядок - як значення (можна розмістити у змінній).

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

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

Зауважте, що звичайні оператори призначення та порівняння ( = == <тощо) не працюють так, як ви могли очікувати. Але strxyzсімейство функцій зближується, коли ви дізнаєтеся, чим займаєтесь. Див. Поширені запитання C щодо рядків та масивів .

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