С++ char расположение нулевого ограничителя массива
Я учащийся, изучающий С++, и я пытаюсь понять, как работают массивы символов с нулевым символом. Предположим, что я определяю массив char следующим образом:
char* str1 = "hello world";
Как и ожидалось, strlen(str1)
равно 11, и оно заканчивается нулем.
Где С++ помещает нулевой ограничитель, если все 11 элементов из вышеприведенного массива char заполнены символами "hello world"? На самом деле он выделяет массив длиной 12 вместо 11, а 12-й символ - '\0'
? CPlusPlus.com кажется, что один из 11 должен быть '\0'
, если он действительно не выделяет 12.
Предположим, что я делаю следующее:
// Create a new char array
char* str2 = (char*) malloc( strlen(str1) );
// Copy the first one to the second one
strncpy( str2, str1, strlen(str1) );
// Output the second one
cout << "Str2: " << str2 << endl;
Это выводит Str2: hello worldatcomY╗°g♠↕
, который, как я полагаю, является С++, читающим память в местоположении, на которое указывает указатель char* str2
, до тех пор, пока он не встретит, что он интерпретирует как нулевой символ.
Однако, если я тогда сделаю это:
// Null-terminate the second one
str2[strlen(str1)] = '\0';
// Output the second one again
cout << "Terminated Str2: " << str2 << endl;
Он выводит Terminated Str2: hello world
, как ожидалось.
Но не пишет ли в str2[11]
, что мы пишем вне выделенного пространства памяти str2
, так как str2[11]
является 12-м байтом, но мы выделили только 11 байтов?
Запуск этого кода, похоже, не вызывает каких-либо предупреждений компилятора или ошибок во время выполнения. Безопасно ли это делать на практике? Было бы лучше использовать malloc( strlen(str1) + 1 )
вместо malloc( strlen(str1) )
?
Ответы
Ответ 1
В случае строкового литерала компилятор фактически резервирует дополнительный элемент char
для элемента \0
.
// Create a new char array
char* str2 = (char*) malloc( strlen(str1) );
Это распространенная ошибка, которую делают программисты C. При распределении хранилища для char*
вам необходимо выделить количество символов + 1 для хранения \0
. Не выделяя дополнительное хранилище, это означает, что эта строка также является незаконной
// Null-terminate the second one
str2[strlen(str1)] = '\0';
Здесь вы на самом деле пишете информацию о конце выделенной памяти. При распределении элементов X последний юридический байт, к которому вы можете получить доступ, является смещением адреса памяти на X - 1
. Запись в элемент X
вызывает поведение undefined. Он часто работает, но это тикающая бомба замедленного действия.
Правильный способ записать это следующим образом
size_t size = strlen(str1) + sizeof(char);
char* str2 = (char*) malloc(size);
strncpy( str2, str1, size);
// Output the second one
cout << "Str2: " << str2 << endl;
В этом примере str2[size - 1] = '\0'
на самом деле не требуется. Функция strncpy
заполняет все лишние пробелы нулевым терминатором. Здесь в str1
есть только size - 1
элементы, поэтому последний элемент в массиве не нужен и будет заполнен \0
Ответ 2
На самом деле он выделяет массив длиной 12 вместо 11, а 12-й символ - '\ 0'?
Да.
Но не пишет в str2[11]
подразумевает, что мы пишем вне выделенного пространства памяти str2
, так как str2[11]
является 12-м байтом, но мы выделили только 11 байтов?
Да.
Было бы лучше использовать malloc( strlen(str1) + 1 )
вместо malloc( strlen(str1) )
?
Да, потому что вторая форма недостаточно длинна, чтобы скопировать строку.
Запуск этого кода, похоже, не вызывает каких-либо предупреждений компилятора или ошибок времени выполнения.
Обнаружение этого во всех, кроме простейших случаях, является очень сложной задачей. Поэтому авторы компилятора просто не беспокоятся.
Такая сложность - именно то, почему вы должны использовать std::string
вместо строчных строк C-стиля, если вы пишете С++. Это так просто:
std::string str1 = "hello world";
std::string str2 = str1;
Ответ 3
Я думаю, вас смущает возвращаемое значение strlen
. Он возвращает длину строки, и ее не следует путать с размером массива, который содержит строку. Рассмотрим этот пример:
char* str = "Hello\0 world";
Я добавил нулевой символ в середине строки, что совершенно верно. Здесь массив будет иметь длину 13 (12 символов + конечный нулевой символ), но strlen(str)
вернет 5, потому что перед первым нулевым символом должно быть 5 символов. strlen
просто подсчитывает символы до тех пор, пока не будет найден нулевой символ.
Итак, если я использую ваш код:
char* str1 = "Hello\0 world";
char* str2 = (char*) malloc(strlen(str1)); // strlen(str1) will return 5
strncpy(str2, str1, strlen(str1));
cout << "Str2: " << str2 << endl;
Массив str2 будет иметь длину 5 и не будет прерван нулевым символом (потому что strlen
не учитывает его). Это то, что вы ожидали?
Ответ 4
Литерал "hello world"
представляет собой массив char
, который выглядит так:
{ 'h', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd', '\0' }
Итак, да, литерал имеет размер 12 char
.
Кроме того, malloc( strlen(str1) )
выделяет память на 1 байт, чем требуется, так как strlen
возвращает длину строки, не включая терминатор NUL. Запись на str[strlen(str1)]
записывает 1 байт за выделенным объемом памяти.
Ваш компилятор не скажет вам об этом, но если вы запустите свою программу через valgrind или аналогичную программу, доступную в вашей системе, она сообщит вам, что вы получаете доступ к памяти, которой вы не должны быть.
Ответ 5
Для стандартной строки C длина массива, который хранит строку, всегда один символ длиннее, чем длина строки в символах. Таким образом, ваша строка "hello world"
имеет длину строки 11, но для этого требуется массив поддержки с 12 элементами.
Причиной этого является просто чтение этой строки. Функции, обрабатывающие эти строки, в основном считывают символы строки один за другим, пока не найдут символ завершения '\0'
и не остановятся в этой точке. Если этот символ отсутствует, эти функции просто продолжают чтение памяти, пока они не попадут в область защищенной памяти, которая заставляет операционную систему хоста убить ваше приложение или пока не найдет символ завершения.
Также, если вы инициализируете массив символов длиной 11 и записываете строку "hello world"
в нее, это вызовет серьезные проблемы. Поскольку ожидается, что массив будет содержать не менее 12 символов. Это означает, что байт, который следует за массивом в памяти, перезаписывается. В результате возникают непредсказуемые побочные эффекты.
Кроме того, пока вы работаете с С++, вы можете посмотреть std:string
. Этот класс доступен, если вы используете С++ и обеспечивает лучшую обработку строк. Возможно, стоит подумать над этим.
Ответ 6
Я думаю, что вам нужно знать, что массивы char начинаются с 0 и идут до тех пор, пока длина массива-1 и длина массива позиции не будут иметь терминатор ('\ 0').
В вашем случае:
str1[0] == 'h';
str1[10] == 'd';
str1[11] == '\0';
Вот почему правильно str2 [strlen (str1)] = '\ 0';
Проблема с выходом после strncpy заключается в том, что она копирует 11 элементов (0..10), поэтому вам нужно вручную установить терминатор (str2 [11] = '\ 0').