Невозможно изменить строку C - PullRequest
8 голосов
/ 21 сентября 2009

Рассмотрим следующий код.

int main(void) {
    char * test = "abcdefghijklmnopqrstuvwxyz";
    test[5] = 'x';
    printf("%s\n", test);
    return EXIT_SUCCESS;
}

На мой взгляд, это должно печатать abcdexghij. Тем не менее, он просто завершается, ничего не печатая.

int main(void) {
    char * test = "abcdefghijklmnopqrstuvwxyz";
    printf("%s\n", test);
    return EXIT_SUCCESS;
}

Это, однако, работает просто отлично, так что я неправильно понял концепцию манипулирования строками C или что-то еще? В случае, если это важно, я использую Mac OS X 10.6, и это 32-разрядный двоичный файл, который я компилирую.

Ответы [ 5 ]

28 голосов
/ 21 сентября 2009

Символьные указатели, определенные со значением инициализации, попадают в сегмент только для чтения. Чтобы сделать их изменяемыми, вам нужно либо создать их в куче (например, с помощью new / malloc), либо определить их как массив.

Не модифицируется:

char * foo = "abc";

Изменяемые:

char foo[] = "abc";
7 голосов
/ 29 сентября 2013

Этот ответ хороший, но не совсем полный.

char * test = "abcdefghijklmnopqrstuvwxyz";

A строковый литерал относится к объекту анонимного массива типа char[N] со статической продолжительностью хранения (то есть он существует для всего выполнения программы), где N - длина строки плюс один за окончание '\0'. Этот объект не const, но любая попытка изменить его имеет неопределенное поведение. (Реализация может сделать строковые литералы доступными для записи, если она выберет, но большинство современных компиляторов этого не делают.)

Объявление выше создает такой анонимный объект типа char[27] и использует адрес первого элемента этого объекта для инициализации test. Таким образом, присваивание типа test[5] = 'x' пытается изменить массив и имеет неопределенное поведение; как правило, это приведет к краху вашей программы. (При инициализации используется адрес, потому что литерал является выражением типа массива, который в большинстве контекстов неявно преобразуется в указатель на первый элемент массива.)

Обратите внимание, что в C ++ строковые литералы на самом деле const, и приведенное выше объявление будет недопустимым. В C или C ++ лучше всего объявить test как указатель на const char:

const char *test = "abcdefghijklmnopqrstuvwxyz";

, поэтому компилятор предупредит вас, если вы попытаетесь изменить массив с помощью test.

(Строковые литералы C не являются const по историческим причинам. До стандарта ANSI C 1989 года ключевое слово const не существовало. Требовать его использования в объявлениях, аналогичных вашим, было бы сделано для более безопасного кода, но это потребовалось бы изменить существующий код, чего комитет ANSI пытался избежать. Вам следует притвориться , что строковые литералы равны const, даже если это не так. Если вы используете gcc, Опция -Wwrite-strings заставит компилятор обрабатывать строковые литералы как const - что делает gcc несоответствующим.)

Если вы хотите изменить строку, на которую ссылается test, вы можете определить ее следующим образом:

char test[] = "abcdefghijklmnopqrstuvwxyz";

Компилятор смотрит на инициализатор, чтобы определить, насколько большим должен быть test. В этом случае test будет иметь тип char[27]. Строковый литерал все еще ссылается на анонимный объект массива, доступный только для чтения, но его значение скопировано в test. (Строковый литерал в инициализаторе, который используется для инициализации объекта массива, является одним из контекстов, в которых массив не «затухает» по указателю; другие - когда это операнд унарного & или sizeof.) дальнейших ссылок на анонимный массив нет, компилятор может его оптимизировать.

В этом случае test представляет собой массив, содержащий 26 символов, которые вы указали, плюс терминатор '\0'. Время жизни этого массива зависит от того, где объявлено test, что может иметь или не иметь значения. Например, если вы сделаете это:

char *func(void) {
    char test[] = "abcdefghijklmnopqrstuvwxyz";
    return test; /* BAD IDEA */
}

звонящий получит указатель на то, что больше не существует. Если вам нужно обратиться к массиву вне области действия, в которой определен test, вы можете определить его как static или выделить его с помощью malloc:

char *test = malloc(27);
if (test == NULL) {
    /* error handling */
}
strcpy(test, "abcdefghijklmnopqrstuvwxyz";

, поэтому массив будет существовать до тех пор, пока вы не вызовете free(). Нестандартная функция strdup() делает это (она определяется POSIX, но не ISO C).

Обратите внимание, что test может быть указателем или массивом, в зависимости от того, как вы его объявили. Если вы передадите test в строковую функцию или любую функцию, которая принимает char*, это не имеет значения, но что-то вроде sizeof test будет вести себя очень по-разному в зависимости от того, является ли test указателем или массивом. .

comp.lang.c FAQ отлично. Раздел 8 охватывает символы и строки, и вопрос 8.5 указывает на вопрос 1.32, в котором рассматривается ваш конкретный вопрос. Раздел 6 посвящен часто сбивающим с толку отношениям между массивами и указателями.

4 голосов
/ 21 сентября 2009

Строковые литералы не могут быть изменены; лучше предположить, что это не так. Смотрите здесь для более подробной информации.

4 голосов
/ 21 сентября 2009

Вы должны привыкнуть сопоставлять тип переменной с типом инициализатора. В этом случае:

const char* test = "abcdefghijklmnopqrstuvwxyz";

Таким образом, вы получите ошибку компилятора, а не ошибку времени выполнения. Повышение уровня предупреждения компилятора до максимума также может помочь избежать таких ловушек. Почему это не ошибка в C, вероятно, является историческим; ранние компиляторы позволяли это, и запрещение этого могло нарушить слишком много существующего кода, когда язык был стандартизирован. Однако сейчас операционные системы не позволяют этого, поэтому он академический.

0 голосов
/ 07 февраля 2010

Do:

 char * bar = strdup(foo);
 bar[5] = 'x';

strdup делает модифицируемую копию.

И да, вам действительно нужно проверить, что strdup не вернул NULL.

...