Первые два вида представления в C++, с которыми вам стоит познакомится это C-string и std::string.

Первый вариант пришёл к нам из языка C, но до сих пор используется и является неотъемлемой частью C++.

Второй вариант, забегая вперёд является классом и взаимодействие с ним происходит, в том числе, через методы класса. Проблема заключается в том что сейчас вы не знаете что такое класс и тем более о других элементах стандартной библиотеки, которым является std::string.

Так что разобрать си-строки вместе std::string и сделать это сейчас - это логичное, хотя и компромиссное решение.

C-string

Это самый стандартный тип строк, который использовался до C++ и используется сейчас в старых API и даже относительно современных библиотеках, таких как ImGui.

Основание для переноса концепции C-string обосновано необходимостью поддержки старых интерфейсов API, низкоуровневой работы и потому что литерал "Hello" в C++ по умолчанию имеет тип const char*, что важно понимать при инициализации std::string в дальнейшем.

void ImDrawList::AddText(
    const ImVec2& pos, ImU32 col, const char* text_begin, const char* text_end
    )
{
    AddText(NULL, 0.0f, pos, col, text_begin, text_end);
}

Как мы видим в параметрах тут используется тип const char* - то есть указатель на массив типа char.

По сути C-string это обычный массив символов со всеми его свойствами.

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

Но это так же означает наличие всех минусов ручной работы с памятью - опасность и неудобность.

Как вы помните длина массива в таких случаях должна хранится отдельно.

В этом случае тут используется другое решение - нулевой терминатор \0.

За счёт этого литерала обеспечивается реализация функций, где требуется длина строки.

Объявление, инициализация, ввод/вывод С-string

Мы можем объявить строку следующим образом:

char str1[] = "Hello"; // '\0' добавляется автоматически
char str2[6] = {'H', 'e', 'l', 'l', 'o', '\0'}; // то же самое явно
char str3[10] = "Hello"; // массив больше, чем нужно, остаток — '\0'

Вывод строки работает как у обычной переменной:

char str1[6] = {'H', 'e', 'l', 'l', 'o', '\0'}; 
std::cout << str1 << std::endl;

Если мы забудем про \0 в конце строки то у нас может произойти такая ситуация:

char str1[5] = {'H', 'e', 'l', 'l', 'o'}; 
std::cout << str1 << std::endl;
Hello╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠↓F⌡═ ⌂

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

Это поведение ничем не гарантированно и зависит от того где написан этот код, каким компилятором собран и ещё множества факторов.

char text[100];
std::cin >> text;
std::cout << text;
first second
first

Таким образом можно прочитать текст с клавиатуры.

Но тут у нас строка читается до первого пробела.

Если будет введено более 99 символов, то может произойти выход за пределы массивы - отсюда неопределённое поведение.

char text[100];
std::cin.width(100);
std::cin >> text;
std::cout << text;

В таких случаях стоит использовать std::cin.width.

char text[100];
std::cin.getline(text, 100);
std::cout << text;

Или же можно использовать std::cin.getline.

Если размер строки заранее неизвестен и требуется считать весь ввод, приходится реализовывать побуквенное чтение через while (std::cin.get(c) && c != '\n'), а при достижении текущего предела буфера вручную увеличивать выделенную память с помощью new, перераспределяя данные в новый массив большей длины.

Особенности работы с C-string

Нужно отметить, что строка такого типа неявно распадается в указатель на первый символ и тут имеет место быть арифметика указателей

char str1[] = "Hello";
std::cout << *str1 << '\n'; // 'H'
std::cout << *(str1 + 1) << '\n'; // 'e'
std::cout << str1[2] << '\n'; // 'l'

Из этого следует, что мы можем использовать механику обращения по индексу для доступа к элементам:

char str1[] = "Hello";
std::cout << str1[0] << '\n'; // 'H'
str1[0] = 'h';
std::cout << str1[0] << '\n'; // 'h'

Мы можем создавать строку через указатель

const char* str1 = "Hello";
str1[0] = 'h'; // expression must be a modifiable value

Но в этом случае мы не можем изменять данные, потому что память выделяется в read-only секции памяти.

char* str1 = "Hello"; 
// a value of type "const char *" cannot be used
// to initialize an entity of type "char *"

Убрать модификатор const мы не можем, потому что изначально литералы такого типа имеют тип const char*.

int main() {
	const char* str1 = "Hello";
	std::cout << sizeof(str1) << std::endl;
	char str2[] = "Hello";
	std::cout << sizeof(str2) << std::endl;
}
8
6

Тут мы получаем разные результаты выполнения оператора sizeof.

В первом случае переменная str1 имеет тип const char*, поскольку приложение скомпилировано в x64. Массив неявно преобразуется в указатель на первый элемент.

Во втором случае переменная str2 имеет тип char[6] и по этому мы получаем sizeof(char) * 6.

То есть только в ряде случаев мы можем получить длину строки таким образом.

Функции для работы со C-string

Для операций мы можем использовать функции strlen, strcpy, strcmp из заголовочного файла <cstring>.

#include <iostream>

int main() {
	const char* str1 = "Hello"; 
	printf("%zu", strlen(str1));
}
5

Тут код у нас компилируется без ошибки, по крайней мере у меня, при использования MSVC в режиме x64.

Причиной этому, по всей видимости, является то, что <cstring> используется внутри <iostream>.

Это не гарантируется стандартом, но конкретная реализация может так сделать.

#include <cstdio>

int main() {
	const char* str1 = "Hello"; 
	printf("%zu", strlen(str1)); // 'strlen': identifier not found
}

А вот так уже получаем ошибку без #include <cstring>.

Как вы уже поняли функция strlen используется для получения длинны строки.

#include <iostream>

int main() {
	char source[] = "Hello";
	char target[10];

	strcpy_s(target, source);

	std::cout << target;
}

Функция strcpy копирует строку. Тут мы используем strcpy_s вместо strcpy из-за особенностей компилятора по аналогии со scanf и scanf_s.

char a[] = "Hello";
char b[] = "Hello";

std::cout << (a == b) << std::endl;
0

Мы не можем "в лоб" сравнивать строки, потому что это фактически сравнение адресов указателей.

char a[] = "Hello";
char b[] = "Hello";

std::cout << strcmp(a, b) << std::endl;
0

Для сравнения мы используем функцию strcmp.

В этом случае мы так же получаем 0, потому что эта функция возвращает 0 в случае, если строки равны.

В случае, если строки различны, то применяется лексикографическое сравнение и результатом могут стать отличные от нуля числа.

char a[100] = "Hello ";
char b[] = "world";
std::strcat(a, b);
std::cout << a;
Hello world

Как вы догадываетесь чтобы выполнить конкатенацию строк нам будет необходимо вручную работать с каждым символом. Тут может помочь функция strcat.

Одна из тривиальных задач связанная со строками - это преобразование в число и обратно.

Оформите подписку, чтобы иметь доступ ко всем публикациям без ограничений. Вы получите мгновенный доступ к полным версиям материалов, исходному коду и новым публикациям сразу после их выхода.

Я регулярно публикую материал и разборы ситуаций для различных игр, движков, а так же подходов разработки включая Internal, External и DMA.

Эта публикация содержит только часть статьи.

Полные уроки по C++, реверс-инжинирингу и разработке софта вы можете найти тут: