Символы и строки

Как компьютер хранит символы и строки

1. Символы, текст и кодирование

Когда мы работаем с текстом, нам кажется, что компьютер «видит» буквы, цифры и знаки препинания так же, как и человек. На самом деле это не так.

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

Компьютер умеет работать только с числами. Поэтому возникает идея:

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

Такой процесс называется кодированием символов. Например, мы можем договориться, что буква A будет храниться как число 65, буква B — как 66 и так далее.

Важно понимать, что символ и его внешний вид — это разные вещи. То, как символ выглядит, определяется шрифтом. Шрифт отвечает за форму буквы, толщину линий, наклон и другие визуальные детали.

Компьютер не хранит шрифт внутри текста — он хранит только числовые коды символов, а шрифт применяется уже при отображении текста на экране.

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

Выглядят они одинаково, но относятся к разным алфавитам и имеют разное значение. Поэтому в памяти компьютера они обязаны иметь разные числовые коды, даже если визуально их нельзя отличить.

2. Кодировка ASCII

Одной из самых старых и известных систем кодирования символов является ASCII (от англ. American Standard Code for Information Interchange).

В ASCII каждому символу сопоставляется число от 0 до 127. Это означает, что для хранения одного символа достаточно 7 бит информации. Однако на практике символы обычно хранили в 1 байте (8 бит), потому что это было удобнее для компьютеров.

Char  Dec  Oct  Hex | Char  Dec  Oct  Hex | Char  Dec  Oct  Hex | Char Dec  Oct   Hex
-------------------------------------------------------------------------------------
(nul)   0 0000 0x00 | (sp)   32 0040 0x20 | @      64 0100 0x40 | `      96 0140 0x60
(soh)   1 0001 0x01 | !      33 0041 0x21 | A      65 0101 0x41 | a      97 0141 0x61
(stx)   2 0002 0x02 | "      34 0042 0x22 | B      66 0102 0x42 | b      98 0142 0x62
(etx)   3 0003 0x03 | #      35 0043 0x23 | C      67 0103 0x43 | c      99 0143 0x63
(eot)   4 0004 0x04 | $      36 0044 0x24 | D      68 0104 0x44 | d     100 0144 0x64
(enq)   5 0005 0x05 | %      37 0045 0x25 | E      69 0105 0x45 | e     101 0145 0x65
(ack)   6 0006 0x06 | &      38 0046 0x26 | F      70 0106 0x46 | f     102 0146 0x66
(bel)   7 0007 0x07 | '      39 0047 0x27 | G      71 0107 0x47 | g     103 0147 0x67
(bs)    8 0010 0x08 | (      40 0050 0x28 | H      72 0110 0x48 | h     104 0150 0x68
(ht)    9 0011 0x09 | )      41 0051 0x29 | I      73 0111 0x49 | i     105 0151 0x69
(nl)   10 0012 0x0a | *      42 0052 0x2a | J      74 0112 0x4a | j     106 0152 0x6a
(vt)   11 0013 0x0b | +      43 0053 0x2b | K      75 0113 0x4b | k     107 0153 0x6b
(np)   12 0014 0x0c | ,      44 0054 0x2c | L      76 0114 0x4c | l     108 0154 0x6c
(cr)   13 0015 0x0d | -      45 0055 0x2d | M      77 0115 0x4d | m     109 0155 0x6d
(so)   14 0016 0x0e | .      46 0056 0x2e | N      78 0116 0x4e | n     110 0156 0x6e
(si)   15 0017 0x0f | /      47 0057 0x2f | O      79 0117 0x4f | o     111 0157 0x6f
(dle)  16 0020 0x10 | 0      48 0060 0x30 | P      80 0120 0x50 | p     112 0160 0x70
(dc1)  17 0021 0x11 | 1      49 0061 0x31 | Q      81 0121 0x51 | q     113 0161 0x71
(dc2)  18 0022 0x12 | 2      50 0062 0x32 | R      82 0122 0x52 | r     114 0162 0x72
(dc3)  19 0023 0x13 | 3      51 0063 0x33 | S      83 0123 0x53 | s     115 0163 0x73
(dc4)  20 0024 0x14 | 4      52 0064 0x34 | T      84 0124 0x54 | t     116 0164 0x74
(nak)  21 0025 0x15 | 5      53 0065 0x35 | U      85 0125 0x55 | u     117 0165 0x75
(syn)  22 0026 0x16 | 6      54 0066 0x36 | V      86 0126 0x56 | v     118 0166 0x76
(etb)  23 0027 0x17 | 7      55 0067 0x37 | W      87 0127 0x57 | w     119 0167 0x77
(can)  24 0030 0x18 | 8      56 0070 0x38 | X      88 0130 0x58 | x     120 0170 0x78
(em)   25 0031 0x19 | 9      57 0071 0x39 | Y      89 0131 0x59 | y     121 0171 0x79
(sub)  26 0032 0x1a | :      58 0072 0x3a | Z      90 0132 0x5a | z     122 0172 0x7a
(esc)  27 0033 0x1b | ;      59 0073 0x3b | [      91 0133 0x5b | {     123 0173 0x7b
(fs)   28 0034 0x1c | <      60 0074 0x3c | \      92 0134 0x5c | |     124 0174 0x7c
(gs)   29 0035 0x1d | =      61 0075 0x3d | ]      93 0135 0x5d | }     125 0175 0x7d
(rs)   30 0036 0x1e | >      62 0076 0x3e | ^      94 0136 0x5e | ~     126 0176 0x7e
(us)   31 0037 0x1f | ?      63 0077 0x3f | _      95 0137 0x5f | (del) 127 0177 0x7f

Расположение символов в ASCII

В таблице ASCII есть чёткая логика:

Это сделано очень удобно: например, разница между заглавной и строчной буквой — ровно 32.

Управляющие символы

Первые 32 символа (коды 0–31) не имеют графического изображения. Это служебные (управляющие) символы, которые используются для технических целей.

Несколько примеров:

Эти символы не отображаются как буквы или знаки, но играют важную роль при работе с текстом.

3. Ограничения ASCII и проблема национальных алфавитов

У ASCII есть серьёзное ограничение: в нём нет русских букв и вообще нет поддержки большинства языков мира.

Исторически люди пытались решить эту проблему так:

Так появились разные кодировки:

Но у этого подхода было много проблем:

Кроме того, такой подход принципиально не масштабируется:

4. Unicode — современная система кодирования

Для решения всех этих проблем была создана система Unicode.

Unicode — это единая таблица, в которой каждому символу всех письменных языков мира сопоставлен уникальный номер (кодовая точка).

На сегодняшний день стандарт Unicode включает более 150 000 символов (точное число постоянно растёт).

В Unicode входят:

Например: эмодзи 😀 имеет код U+1F600

Так как количество символов очень большое, на один символ требуется больше одного байта памяти. Существует несколько способов хранения Unicode-символов в памяти (UTF-8, UTF-16, UTF-32), но все они используют больше информации, чем ASCII.

Тип char в C++

1. Зачем нужен char и какие у него значения

В языке C++ для хранения однобайтового символа используется тип char.
Один байт — это 8 бит, поэтому в памяти char хранит некоторое число от 0 до 255, но вопрос в том, как именно это число трактуется.

Чаще всего char рассматривают как знаковый тип, тогда его значения обычно лежат в диапазоне:

Однако в C++ есть три разных «варианта» символьного байта:

То есть запись char x; не гарантирует, будет ли это знаковый или беззнаковый тип. Поэтому строго говоря, использование char для хранения чисел может быть небезопасным: на разных системах один и тот же код может вести себя по-разному.

Но в задачах и при изучении строк мы обычно работаем с обычными символами ASCII, поэтому для простоты дальше будем писать просто char.

2. Как присваивать значение char и как его выводить

Переменной типа char можно присвоить:

Пример: присваиваем символ

#include <iostream>
using namespace std;

int main() {
    char c = 'A';
    cout << c << "\n"; // выведется символ A
}

Если вывести переменную char напрямую, то на экран обычно выводится символ, соответствующий коду внутри char.

Пример: присваиваем число (код)

#include <iostream>
using namespace std;

int main() {
    char c = 65;
    cout << c << "\n"; // выведется A
}

Как получить число (код) из char

Если привести char к типу int, то мы увидим число, которое хранится в этом байте (то есть код символа).

#include <iostream>
using namespace std;

int main() {
    char c = 'A';
    cout << (int)c << "\n"; // выведется 65
}

3. Арифметика и сравнения с char

Тип char — это целочисленный тип, значит с ним можно делать:

Пример: 'A' + 1 даёт 'B'

Если код 'A' равен 65, то код 'A' + 1 равен 66, то есть это 'B'.

#include <iostream>
using namespace std;

int main() {
    char a = 'A';
    char b = a + 1;
    cout << b << "\n"; // выведется B
}

Можно проверить и числом:

#include <iostream>
using namespace std;

int main() {
    cout << (int)('A' + 1) << "\n"; // выведется 66
}

Пример: сравнение символов

Сравнение идёт по их числовым кодам:

#include <iostream>
using namespace std;

int main() {
    cout << ('A' < 'B') << "\n"; // выведется 1 (истина)
}

4. Ввод символа с клавиатуры в char

С помощью cin можно считать один символ и записать его в переменную char.

#include <iostream>
using namespace std;

int main() {
    char c;
    cin >> c;      // считывает один символ (пропуская пробелы и переводы строк)
    cout << c << "\n";
}

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

Строки и тип string в C++

1. Что такое string и как создавать строки

В C++ тип string — это удобный тип для хранения текста. Его можно понимать как аналог vector<char>, то есть динамический массив символов, который сам умеет увеличивать размер, хранить длину и поддерживать операции добавления и удаления.

Чтобы пользоваться строками, нужно подключить заголовок:

#include <string>

Создание строк

#include <iostream>
#include <string>
using namespace std;

int main() {
    string a;              // пустая строка
    string b = "Hello";    // инициализация строковым литералом
    string c("World");     // другой способ инициализации
}

Можно создать строку из нескольких одинаковых символов (как у vector):

string s(5, 'x'); // "xxxxx"

2. Конкатенация, доступ по индексу и изменение символов

Конкатенация строк

Главная операция со строками — конкатенация, то есть «склеивание».

#include <iostream>
#include <string>
using namespace std;

int main() {
    string a = "Hello";
    string b = "World";

    string c = a + ", " + b + "!";
    cout << c << "\n"; // Hello, World!

    a += "!!!";
    cout << a << "\n"; // Hello!!!
}

Доступ по индексу

К строке можно обращаться как к массиву: s[i]. Индексация начинается с нуля.

#include <iostream>
#include <string>
using namespace std;

int main() {
    string s = "abcd";
    cout << s[0] << "\n"; // a
    cout << s[3] << "\n"; // d
}

Изменение отдельных символов

#include <iostream>
#include <string>
using namespace std;

int main() {
    string s = "cat";
    s[0] = 'b';
    cout << s << "\n"; // bat
}

3. Методы resize и size у строк

Метод resize(n) изменяет длину строки.

#include <iostream>
#include <string>
using namespace std;

int main() {
    string s = "hello";

    s.resize(3);
    cout << s << "\n"; // hel

    s.resize(6, '!');
    cout << s << "\n"; // hel!!!
}

Если второй аргумент не указан, строка дополняется нулевыми символами '\0'.

Метод s.size() возвращает длину строки.

4. Ввод строк: cin >> s и getline

Ввод через cin >> s

При вводе через cin >> s строка считывается до первого разделяющего символа (пробела, табуляции или перевода строки).

#include <iostream>
#include <string>
using namespace std;

int main() {
    string s;
    cin >> s;
    cout << s << "\n";
}

Если ввести:

Hello world

то в строку попадёт только "Hello".

Ввод всей строки с помощью getline

Если нужно считать всю строку до нажатия Enter, используется функция getline.

#include <iostream>
#include <string>
using namespace std;

int main() {
    string line;
    getline(cin, line);
    cout << line << "\n";
}

5. Методы строк (нельзя использовать в контесте)

Тип string содержит многие методы, знакомые по vector, например:

insert — вставка

#include <iostream>
#include <string>
using namespace std;

int main() {
    string s = "I like tea";
    s.insert(7, "green ");
    cout << s << "\n"; // I like green tea
}

erase — удаление

#include <iostream>
#include <string>
using namespace std;

int main() {
    string s = "I like green tea";
    s.erase(7, 6);
    cout << s << "\n"; // I like tea
}

substr — подстрока

#include <iostream>
#include <string>
using namespace std;

int main() {
    string s = "programming";
    string t = s.substr(3, 4);
    cout << t << "\n"; // gram
}