Работа со строками

char

Мы уже говорили о целочсиленных типах и выяснили, что char - это однобайтовое знаковое целое число (то есть позоляет хранить числа от -128 до 127). Однако в C++ char выделяется из обычных целочисленных типов (таких как short, int и long long). Например, если мы напишем в программе:

1
2
char c = 65;
cout << c;

то внезапно увидим не 65, а единственный символ - заглавную латинскую букву A.

Это происходит, потому что при выводе переменной типа char С++ считает, что мы хотим вывести символ (англ. char). Осталось ответить на главный вопрос:

А почему выводится именно A?

ASCII table

В далёком 1963 году люди решили стандартизировать таблицу между символами и числами. Этот стандарт они назвали ASKII-таблицей, и ниже вы можете увидеть таблицу, преобразующую числа от 0 до 127 в сиволы.

img/ASCII_Code_Chart.svg

  1. Нумерация колонок здесь в 16-ричной системе счисления. То есть, если вы смотртие на яцейку в строке i, столбце j, то её номер $16i+j$. Например, в 4-й строке 1-м столбце (4* 16 + 1 = 65 клетке) находится та самая буква A.
  2. Символы с 0 по 31 являюся служебными
  3. Символы с 32 по 127 можно спокойно печатать и не бояться за жизнь

Служебные символы не печатаются, но они используются в программировании. Например, символ с кодом 10 - это символ перевода строки, вы его пишете как \n, символ с кодом 0 означает конец строки (его можно так же получить с помощью \0), а символ с кодом 7 - звонит в звоночек на вашем ПК (эта технология давно утеряна среди предков, но еще в 2005 вы могли услышать звоночек / характерный пик при включении ПК - это и был тот самый смивол 7).

Безусловно, вам не надо запоминать ACKII-таблицу. Если вам интересен, какой порядковый номер имеет тот или иной символ, то вы можете запустить простейший код:

1
2
int c = 'z';
cout << z;

На самом деле при вниметельном рассмотрении таблицы должен возникнуть очередной вопрос:

А как же хранятся русские символы?

При более внимательном рассмотрении возниакет ещё одна мысль. Вы могли заметить, что мы использовали “не весь потанцеал” типа char. А именно, тип 8-битный, то есть в нем можно хранить числа от 0 до 255, а таблицу мы заполнили только до 127.

Кто украл вторую половину таблицы?

Эти два вопроса связаны.

Вторая половина ASKII-таблицы

Вторую часть таблицы было принято отдать “на outsource”. В разных страных разные буквы, поэтому вторая часть таблицы зависит от “раскладки вашей клавиатуры” (точнее от настроек системы). Более того, даже если вы в России, то существут разные вторые части таблицы, такие как CP1251, KOI8-R и другие, они называются кодировка.

CP1251 выглядит так: CP1251

KOI8-U выглядит так: KOI8-U

На самом деле это сущий кошмар, и в жизни вы ни раз столкнетесь с проблемой кодикровки. Однако, в мире спортивного программирования все сиволы находятся в интервале от 32 до 127, а значит никаких проблем не будет.

Дотошный читатель заметит ещё две пробемы:

  1. В некоторых языках (например, китайских иероглифах) символов больше чем 128, как люди живут?
  2. Неужели, мне надо менять кодировку каждый раз, когда я хочу напечатать сивол из третьего языка?

Обе проблемы решает универсальная кодирока, именуемая Unicode и её модификации: UTF-8, UTF-16, UTF-32 (в зависиомти от количества бит на символ). Об этом я рассказывать не буду, но вы можете прочитать Википедию или посмотреть какое-нибудь видео на YouTube.

Полезные функции при работе с символами

В библиотеке <cctype> есть множество функций, которые помогут вам работать с символами. Например, функция isalpha проверяет, является ли символ буквой, а isdigit - цифрой, to_lower - переводит в нижний регистр. Вот пример использования:

1
2
3
4
5
6
7
8
9
10
11
#include <iostream>
#include <cctype>

using namespace std;

int main() {
    char c = 'A';
    cout << isalpha(c) << '\n'; // 1
    cout << isdigit(c) << '\n'; // 0
    cout << tolower(c) << '\n'; // 'a'
}

Упражнения

Полезно так же понимать, что символы можно сравнивать между собой как и обычные числа, а подряд идущее расположение символов в таблице ASCII позволяет не заучивать функции вида isalpha и isdigit.

Напишите функцию, аналогичную isdigit.

Решение
1
2
3
bool is_digit(char c) {
    return '0' <= c && c <= '9';
}

Напишите функцию, аналогичную to_lower.

Решение
1
2
3
char to_lower(char c) {
    return c - 'A' + 'a';
}

String

Отлично, со строками разобрались! Теперь разбираемся с последовательностью сиволов, или строками.

Есть два стула способа представления строк. Первый, более очевидный, - это хранить строку в виде массива элементов типа char. Например, так:

1
2
char str[10] = { 'a', 'b', 'c' }; // Это называется список инициализации
char str[10] = "abc"; // Но можно и без него

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

Другой способ - это тип string (которым вы будете всегда пользоваться). Он удобен тем, что для него есть множество методов, помогающих работать со строками:

1
2
3
4
5
6
7
string s = "abcd";
cout << s.substr(1, 2) << '\n'; // подстрока с 1 позиции, 2 последующих символа - "bc"
cout << s[3] << '\n'; // 'd'
cout << s.size() << '\n'; // 4
string t = "gg";
cout << t + s; // "abcdgg"
cout << (t < s); // сравнение строк лексикографически, результат - false, выведется 0

Обратите так же внимание на не очень очевидую типизацию в C++ при работе со строками и символами:

1
2
3
4
5
6
7
8
9
"abc" // это массив символов типа const char[4]
'a' // это символ типа char
"abc"s // это строка типа string

string s = "abc"; // работает как конвертация массива в строку
string a = "abc";
string b = a + "kek"; // не работает, потому что "kek" - это const char[4], а не string

// Приучите себя писать "abc"s вместо "abc" во избежение непонятных ошибок

Все методы класса string вы можете найти в документации.

Решаем пару задач

Важный экзамен

Группа студентов написала экзамен в виде теста. Всего в группе было $n$ студентов, а тест состоял из $m$ вопросов, каждый из которых имел $5$ вариантов ответа (A, B, C, D или Е). На каждый вопрос есть ровно один правильный ответ. Правильный ответ на $i$-й вопрос даёт $a_i$ баллов. Неправильные ответы оцениваются нулём баллов. Студенты помнят, какие ответы они дали на экзамене, но не знают, какие ответы являются правильными. Они настроены достаточно оптимистично, а потому интересуются насколько большим может быть суммарный балл всех студентов группы. Напишите программу, которая по заданным ответам студентов и баллам за каждый вопрос определит максимально возможный суммарный балл всех студентов.

Решение

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <iostream>
#include <string>
#include <vector>

using namespace std;

int main() {
    int n, m;
    cin >> n >> m;
  
    vector<vector<int>> count(m, vector<int>(5)); // массив размера M x 5, заполнен изначально нулями
    for (int i = 0; i < n; ++i) { // читаем все строки
        string s;
        cin >> s;
        for (int j = 0; j < m; ++j) {
            count[j][s[j] - 'A']++; // заполняем количество встречаемости ответа s[j] на вопрос j
        }
    }

    vector<int> a(m);
    for (int i = 0; i < m; ++i) { // читаем a
        cin >> a[i];
    }

    int ans = 0;
    for (int i = 0; i < m; ++i) { // перебираем вопросы
        int mx = 0;
        for (int j = 0; j < 5; ++j) { // ищем максимальную встречаемость на вопрос номер i
            if (count[i][j] > mx)
                mx = count[i][j];
        }
        ans += mx * a[i]; // добавляем максимум к ответу
    }
    cout << ans;
}

Леша и сломаный контест

Как-то раз Леша готовил контест про своих друзей и случайно удалил его. К счастью, все задачи сохранились, но теперь их нужно найти среди других задач. Но задач слишком много, чтобы сделать это вручную. Леша просит Вас написать программу, которая по названию задачи определит, принадлежит ли задача этому контесту. Известно, что задача принадлежит контесту тогда и только тогда, когда в ней содержится имя одного друга Леши в качестве подстроки ровно один раз. Его друзей зовут «Danil», «Olya», «Slava», «Ann» и «Nikita».

Решение

Данная задача исключительно на работу со строками. Можно пользоваться substr, можно вспомнить о find и rfind. Остановимся на первом варианте:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <iostream>
#include <string>
#include <vector>

using namespace std;

int main() {
    string s;
    cin >> s;

    bool has_elier_name = false;
    int n = s.size();

    vector<string> friends = { "Danil", "Olya", "Slava", "Ann", "Nikita" }; // Да, у вектора тоже есть список инициализации

    for (int i = 0; i < n; ++i) {
        for (int j = 0; j < friends.size(); ++j) {
            if (s.substr(i, friends[j].size()) == friends[j]) { // если подстрока равна j-му имени
                if (has_elier_name) { // это не первое вхождение
                    cout << "NO";
                    return 0; // завершить программу (работает только внутри main)
                }
                else {
                    has_elier_name = true;
                }
            }
        }
    }

    if (has_elier_name)
        cout << "YES";
    else
        cout << "NO";
}

*Small string optimization

Данный раздел не является обязательным для прочтения и более того не рекомендован, если вы узнаете о C++ в первый раз. Однако если вы уже знакомы с C++ и знаете, что такое куча и стек, знаете устройство вектора, то вам будет интересно узнать о том, как устроен класс string.

В базовом варианте класс string хранит три переменные: указатель на начало строки (в куче), размер зарезервированной памяти и реальный размер строки, аналогично вектору:

1
2
3
4
5
class string {
    char* data;
    size_t size;
    size_t capacity;
};

Однако, как мы помним, работа с кучей дорога. Можно ли как-то уменьшить количество обращений к куче? Может быть в каких-то случаях вовсе не обращаться к памяти куче?

Оказыватся, да, можно! Большинство строк в реальном мире короткие и мы будем активно пользоваться этим фактом.

Размер структуры string равен 24 байта (8 байт на указатель, 8 байт на размер строки и 8 байт на размер зарезервированной памяти), меньше места на стеке занимать в любом случае не получится. Но мы можем воспользоваться этими 24 байтами по-другому, а именно хранить в ней короткие строки побайтово прямо на месте других переменных (указаеля и capacity - 16 байт), а переменную size использовать как флаг: если size равнен 0, то строка короткая и хранится внутри string, если size больше 0, то строка хранится в куче.

Таким образом если строка занимает меньше 16 байт (то есть длины до 15), то она хранится внутри string прямо на стеке, иначе она хранится в куче. Такая оптимизация называется small string optimization.

На C++ это выглядит так:

1
2
3
4
5
6
7
8
9
10
11
12
class string {
    size_t size;
    union {
        struct no_small_buffer_t {
            char* data;
            size_t size;
        } no_small_buffer;
        struct small_buffer_t {
            char data[sizeof(no_small_buffer_t)];
        } small_buffer;
    } impl;
};