Всё, что было до сих пор, – это C с точки зрения работы с памятью. std::string и потоки –
первое, что делает C++ языком, на котором приятно писать: они берут управление ресурсом на себя,
оставляя вам задачу.
std::string
#include <string>
std::string name = "Иван";
std::string empty; // пустая строка, не мусор
std::string spaces(10, ' '); // десять пробелов
name += " Петров"; // буфер вырастет сам
std::cout << name.size(); // 11 – хранится полем, не считается проходом
std::cout << name[0]; // доступ по индексу, без проверки границ
std::cout << name.at(100); // с проверкой: бросит std::out_of_range
Что здесь важнее синтаксиса: строка владеет своей памятью. Она выделяет буфер в куче,
перевыделяет при росте и освобождает в деструкторе. Утечки не будет, даже если из функции
выйти через return в середине или через исключение.
char* |
std::string |
|
|---|---|---|
| Длина | считается проходом до '\0' |
хранится, size() за O(1) |
| Память | выделяете и освобождаете вы | объект сам |
| Конкатенация | ваша забота о размере буфера | +=, буфер растёт сам |
| Сравнение | strcmp |
==, < |
Может содержать '\0' внутри |
нет | да |
Короткие строки обычно не попадают в кучу вообще: почти все реализации применяют small string optimization – символов до 15–22 хранятся прямо в теле объекта, там же, где лежали бы указатель и размер. Поэтому std::string для короткого имени не дороже char[16] на стеке.
Полезные методы:
std::string s = "hello world";
s.substr(6); // "world"
s.substr(0, 5); // "hello"
s.find("world"); // 6; если не найдено – std::string::npos
s.empty(); // false
s.c_str(); // const char* – для старых C-API
c_str() – мост в мир C-строк: возвращает указатель на внутренний буфер с завершающим нулём.
Указатель действителен, пока строку не изменили; сохранять его надолго нельзя.
Потоки
Поток – абстракция «последовательность символов, откуда читают или куда пишут». Откуда именно – консоль, файл, память – вопрос конкретного класса.
#include <iostream>
int age = 0;
std::cin >> age; // из стандартного ввода
std::cout << "Возраст: " << age << '\n';
std::cerr << "что-то пошло не так\n"; // поток ошибок, не перенаправляется вместе с cout
Операторы << и >> – обычные перегруженные операторы (как их писать для своих типов, разберём
в модуле про дополнительные возможности). Возвращают они сам поток, поэтому и выстраиваются в
цепочку.
std::endl – это не «перевод строки», а перевод строки плюс сброс буфера на устройство. В цикле на миллион итераций разница с '\n' бывает на порядок. Пишите '\n', а std::endl оставьте для случаев, когда вывод должен появиться прямо сейчас – например, перед возможным падением.
Чтение
Оператор >> читает «по словам»: пропускает пробелы, табуляции и переводы строк, читает до
следующего пробельного символа и оставляет его в потоке.
std::string word;
std::cin >> word; // на вводе "Иван Петров" получим "Иван"
std::string line;
std::getline(std::cin, line); // строка целиком, до '\n'
Классическая ловушка – смешивание двух способов:
int n = 0;
std::cin >> n; // прочитали число, '\n' остался в потоке
std::string line;
std::getline(std::cin, line); // прочитал пустой остаток строки, а не следующую
Лечится либо тем, что читают всё через getline и разбирают вручную, либо явным пропуском
остатка: std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');.
Ошибки ввода
Поток помнит своё состояние. Если разбор не удался – например, ждали число, а пришли буквы – поток переходит в состояние ошибки и все последующие чтения молча ничего не делают.
int n = 0;
if (std::cin >> n) {
// прочиталось
} else {
std::cin.clear(); // сбросить флаг ошибки
std::cin.ignore(1024, '\n'); // выбросить то, что не разобралось
}
Проверка if (std::cin >> n) работает потому, что у потока определено преобразование к bool.
Это же выражение – основа идиомы чтения до конца ввода:
int value = 0;
while (std::cin >> value) {
// пока читается – обрабатываем
}
Не проверять результат чтения – ошибка, которая тихо портит данные: при неудачном разборе переменная останется в прежнем значении (в C++11 и новее – будет обнулена), и программа пойдёт считать по мусору. Ввод – это граница доверия, и на границе результат проверяют всегда.
Файлы и строковые потоки
Те же операторы работают с файлами и со строками – меняется только класс потока:
#include <fstream>
#include <sstream>
std::ifstream in("data.txt");
std::string line;
while (std::getline(in, line)) {
std::istringstream parser(line); // разбираем строку как поток
std::string name;
int score = 0;
if (parser >> name >> score) {
// строка разобралась
}
}
std::ofstream out("result.txt");
out << "готово\n"; // файл закроется в деструкторе out
Файл здесь никто не закрывает вручную – деструктор ifstream сделает это сам, в том числе при
выходе через исключение. Это тот же приём владения ресурсом, что и у std::string с памятью, и
именно он в C++ заменяет finally из других языков. Как он устроен изнутри – в следующем модуле.