С++ 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').