char хранит не букву, а её числовой код. Буквой значение становится только при выводе, потому что поток интерпретирует char как символ.

Символ — это число

char letter = 'v';
std::cout << letter << '\n';                      // v
std::cout << static_cast<int>(letter) << '\n';    // 118 в ASCII

В памяти нет маленькой нарисованной буквы. В кодировке, совместимой с ASCII, латинской v соответствует код 118, поэтому на такой системе char letter = 118; задаст ровно тот же символ:

char v = 'v';
char v118 = 118;
std::cout << v << v118 << '\n';   // vv

Тип влияет не только на диапазон значений, но и на смысл операции вывода. Это первый случай в курсе, где одно и то же значение печатается по-разному в зависимости от типа. См. static_cast.

sizeof(char) равен единице по определению — это и есть байт C++.

Арифметика с символами

Раз это число, с ним работают обычные операции:

char lower = 'v';
char upper = static_cast<char>(lower - ('a' - 'A'));
std::cout << upper << '\n';   // V

Разность 'a' - 'A' — расстояние между строчными и заглавными в таблице кодов. Вычитание выполняется уже в int: char проходит целочисленное продвижение, см. Арифметические преобразования типов. Обратное преобразование в char приходится писать явно, иначе это сужение.

Формула опирается на устройство кодировки и не является универсальным способом менять регистр: для кириллицы или произвольного Unicode-текста она не работает.

std::cout << sizeof(cx) << '\n';        // 1
std::cout << sizeof(cx + cx) << '\n';   // 4

Знаковость не определена

Обычный char может вести себя как знаковый или как беззнаковый — это решает реализация. При этом char, signed char и unsigned char — три разных типа, даже если два из них совпадают по поведению.

Практическое правило: char для символов, а для маленьких чисел явно signed char или unsigned char. Иначе код с char c = 200; ведёт себя по-разному на разных платформах.

Строки и нулевой байт

Строковый литерал — не char, а массив символов с завершающим нулевым байтом:

const char* greet = "Hello, Vova!\n\0Goodbye, Vova!\n";
std::cout << greet;   // Hello, Vova!

Печатается только первая половина: вывод идёт до первого нулевого байта, а \0 поставлен посередине. В памяти вторая половина есть, для std::cout строка уже закончилась.

std::cout << sizeof("A\0B") << '\n';   // 4: 'A', '\0', 'B', '\0'
std::cout << sizeof('A') << '\n';      // 1

Видимый текст и размер литерала описывают разные вещи. См. Литералы.

Тонкости

  • Присваивать строковый литерал в char* нельзя — только в const char*. GCC пропускает это с предупреждением, Clang с -pedantic-errors выдаёт ошибку. Разбор — L01 - Разбор - main.cpp.
  • Один char не обязан вмещать любой символ Unicode. Кириллическая буква в UTF-8 занимает два байта, поэтому std::string с русским текстом длиннее, чем кажется по экрану.
  • Для других представлений текста существуют wchar_t, char8_t, char16_t, char32_t.
  • '0' это код 48, а не ноль. Цифру-символ переводят в число как c - '0'.

Частые ошибки

  • Считать, что std::cout << c напечатает код символа.
  • Использовать char как маленькое целое и нарваться на разную знаковость.
  • Применять формулу смены регистра к нелатинскому тексту.
  • Считать длину UTF-8 строки в символах, а не в байтах.