10.12 – Строковые символьные константы в стиле C

Добавлено 8 июня 2021 в 16:17
Глава 10 – Массивы, строки, указатели и ссылки  (содержание)

Строковые символьные константы в стиле C

В предыдущем уроке мы обсуждали, как создать и инициализировать строку в стиле C, например:

#include <iostream>
 
int main()
{
    char myName[]{ "Alex" }; // фиксированный массив
    std::cout << myName << '\n';
 
    return 0;
}

C++ также поддерживает способ создания строковых символьных констант в стиле C с использованием указателей:

#include <iostream>
 
int main()
{
    const char* myName{ "Alex" }; // указатель на символьную константу
    std::cout << myName << '\n';
 
    return 0;
}

Хотя эти две показанные выше программы работают и дают одинаковые результаты, C++ обрабатывает выделение памяти в них немного по-разному.

В случае с фиксированным массивом программа выделяет память для фиксированного массива длиной 5 и инициализирует эту память строкой "Alex\0". Поскольку память специально выделена для массива, вы можете изменять его содержимое. Сам массив обрабатывается как обычная локальная переменная, поэтому, когда массив выходит за пределы области видимости, память, используемая массивом, освобождается для других целей.

В случае символьной константы то, как компилятор обрабатывает ее, определяется реализацией. Обычно компилятор помещает строку "Alex\0" куда-нибудь в память, доступную только для чтения, а затем устанавливает указывающий на нее указатель. Поскольку эта память может быть доступна только для чтения, лучше всего убедиться, что строка является константной.

В целях оптимизации несколько строковых литералов можно объединить в одно значение. Например:

const char* name1{ "Alex" };
const char* name2{ "Alex" };

Это два разных строковых литерала с одинаковыми значениями. Компилятор может объединить их в один общий строковый литерал, чтобы name1 и name2 указывали на один и тот же адрес. Таким образом, если name1 не является const, изменение name1 также может повлиять на name2 (чего нельзя было ожидать).

В результате того, что строковые литералы хранятся в фиксированном месте в памяти, они имеют статическую, а не автоматическую продолжительность (то есть они уничтожаются в конце программы, а не в конце блока, в котором они определены). Это означает, что когда мы используем строковые литералы, нам не нужно беспокоиться о проблемах с областью видимости. Таким образом, допустимо следующее:

const char* getName()
{
    return "Alex";
}

В приведенном выше коде getName() вернет указатель на строку в стиле C "Alex". Если бы эта функция возвращала по адресу любую другую локальную переменную, эта переменная была бы уничтожена в конце getName(), и вызывающей функции мы вернули бы обратно висячий указатель. Однако, поскольку строковые литералы имеют статическую продолжительность, литерал "Alex" не будет уничтожен при завершении getName(), поэтому вызывающая функция может успешно получить к нему доступ.

Строки в стиле C используются в большом количестве старого или низкоуровневого кода, потому что они занимают очень мало памяти. Современный код должен отдавать предпочтение использованию std::string и std::string_view, поскольку они обеспечивают безопасный и легкий доступ к строке.

std::cout и указатели char

На этом этапе вы, возможно, заметили кое-что интересное в том, как std::cout обрабатывает указатели разных типов.

Рассмотрим следующий пример:

#include <iostream>
 
int main()
{
    int nArray[5]{ 9, 7, 5, 3, 1 };
    char cArray[]{ "Hello!" };
    const char* name{ "Alex" };
 
    std::cout << nArray << '\n'; // nArray раскладывается до типа int*
    std::cout << cArray << '\n'; // cArray раскладывается до типа char*
    std::cout << name << '\n';   // name уже имеет тип char*
 
    return 0;
}

На машине автора эта программа напечатала:

003AF738
Hello!
Alex

Почему массив int напечатал адрес, а символьные массивы напечатали строки?

Ответ заключается в том, что std::cout делает некоторые предположения о ваших намерениях. Если вы передадите ему указатель не на char, он распечатает просто содержимое этого указателя (адрес, который хранит указатель). Однако если вы передадите ему объект типа char* или const char*, он будет предполагать, что вы собираетесь напечатать строку. Следовательно, вместо того, чтобы печатать значение указателя, он будет печатать указанную строку!

Хотя это замечательно в 99% случаев, это может привести к неожиданным результатам. Рассмотрим следующий случай:

#include <iostream>
 
int main()
{
    char c{ 'Q' };
    std::cout << &c;
 
    return 0;
}

В этом случае программист намеревается вывести адрес переменной c. Однако &c имеет тип char*, поэтому std::cout пытается распечатать его как строку! На машине автора этот код напечатал:

Q╠╠╠╠╜╡4;¿■A

Почему он это сделал? Предполагается, что &c (имеющий тип char*) является строкой. Итак, он напечатал 'Q' и продолжил работу. Далее в памяти была куча мусора. В конце концов, он столкнулся с какой-то памятью, содержащей значение 0, которое он интерпретировал как нулевой терминатор и поэтому остановился. То, что получится у вас, может отличаться в зависимости от того, что находится в памяти после переменной c.

Этот случай маловероятен в реальной жизни (поскольку вы вряд ли действительно захотите печатать адреса памяти), но он иллюстрирует, как всё работает под капотом и как программы могут непреднамеренно сойти с рельсов.

Теги

arrayC++ / CppLearnCppstd::coutДля начинающихКонстантаМассивОбучениеПрограммированиеСимвольная константаСтрокаСтрока в стиле C

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.