Программа курса← Программирование на языке C++
1Введение в язык C++
2Как выполняются программы на C++
3Структуры и классы
4Объектно-ориентированное программирование
5Дополнительные возможности C++
6Шаблоны
7Методические материалы

std::string и потоки ввода-вывода

Всё, что было до сих пор, – это 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 из других языков. Как он устроен изнутри – в следующем модуле.