# Лекция 1. Основы C++ Эта лекция каждый год разная. Ниже конспект занятия 20.02. ## Как писать код > Любой дурак может написать код, понятный компьютеру. > Хорошие программисты пишут код, понятный людям. *Мартин Фаулер* Знать конструкции языка программирования — как знать переводы слов. Это необходимо, чтобы общаться, но ясной речью мы называем не набор слов, а правильно выбранные и расположенные слова. Написание программы начинается с идеи и подобно переводу. Чем точнее конструкция языка будет соответствовать идее, которую хочется выразить, тем проще будет разобраться в программе. Код читается гораздо чаще, чем пишется, поэтому писать понятно выгодно в плане экономии усилий и другим, и себе через некоторое время. Во-первых, это выбор правильных конструкций и типов. Например, есть переменная, содержащая количество значений. Можно было бы объявить ее как `int n`. Но `int` — это знаковый тип, то есть читатель, увидев такую строку, задумается, что здесь означает знак, а когда разберется, что знак не нужен, должен будет на протяжении оставшейся программы помнить об этом. Беззнаковый тип подойдет лучше: `unsigned int n`. «Но почему не `long`, это осознанный выбор?» — спросит читатель. Если объявить `size_t n`, вопроса не возникнет: это переменная, которая содержит количество или размер какого-то набора. Но что это за количество? **Правильное именование критически важно.** Если это количество чисел, имеет смысл назвать переменную `number_count`. Не нужно использовать имена из одной буквы, если это не дословная реализация математических выкладок. Не нужно ispolzovat translit, потому что это трудно читать. Наконец, код нужно опрятно оформлять. Пример плохого оформления: с первого взгляда трудно понять, как между собой связаны `if` и `for`, сложно продраться через нагромождение операторов в формуле. ```cpp if((i>0)&&(bins[i-1] 0) && (bins[i - 1] < bins[i])) { for (int j = 0; j < bins[i] - 1; j++) { cout<<'*'; } ``` В среде CodeBlock можно в контекстном меню (по правой кнопке мыши) выбрать пункт «Format use AStyle», чтобы автоматически расставить отступы. Однако стоит выработать привычку сразу писать код аккуратно. Соглашения о том, как именно форматировать код, например, ставить ли `{` на той же строке, что и `if`, или на следующей, называются соглашением о кодировании (coding style). Обычно команда принимает какой-нибудь стиль и все участники ему следуют. Ни в каком реальном проекте код как попало не пишут. ## Повторение конструкций C++ ### Скобки Фигурные скобки лучше использовать всегда, даже если они необязательны: ```cpp if (условие) { единственная инструкция, формально не требующая скобок } ``` В этом случае, когда понадобится доработать код и расширить тело под `if`, нет риска забыть добавить скобки и получить корректную с точки зрения языка, но логически неправильную программу. Лучше также ставить скобки в выражениях, чтобы читателю не приходилось вспоминать приоритеты операций. Тому, кто пишет код, может быть очевидно то, что читатель при беглом взгляде не вспомнит. ```cpp if ((foo != 1) && (bar > 0)) // внутренние скобки сугубо для наглядности ``` ### Переменные Переменные лучше объявлять как можно ближе к месту первого использования, а не все сразу в начале программы или функции. Дело в том, что при чтении кода программист держи в голове контекст: представление о том, какие есть переменные и чему они могут быть равны в данном месте программы. Чем меньше контекст, тем проще разобраться. Лучше давать переменным осмысленные начальные значения, если возможно. ### `switch` Выражение, по которому делается переход, должно быть интегрального типа, в это входят целочисленные типы, `char` и перечисления (`enum`). Значения в `case` должны быть константами. Это не могут быть переменные. Как выполнять одни и те же действия для нескольких значений? ```cpp char answer; cout << "Да (y/Y) или нет (n/N)? "; cin >> answer; switch (answer) { case 'y': case 'Y': cout << "Ответ утвердительный.\n"; break; case 'n': case 'N': cout << "Ответ отрицательный.\n"; break; default: cout << "Ответ неопределенный.\n"; } ``` Допустим, был совершен переход к `case 'y'`. Команды будут выполняться, пока не встретится `break`. Строка `case 'Y'` будет просто проигнорирована. **Это практически единственный случай, когда имеет смысл `case` без `break`.** Исключения из правила встречаются, но очень редко. ### `using namespace std;` В большой программе велика вероятность того, что два типа или две функции, которые доступны одновременно, будет логично назвать одинаково. Хуже того, это может случиться не в разных частях одной программы, когда можно найти компромисс, а между программой и библиотекой. Например, в программе может быть функция `print()` для печати чего-нибудь. Это значит, что если такая функция появится в стандартной библиотеке — а она недавно появилась — возникнет конфликт (перегрузки функций рассматривать здесь не будем). Аналогия из повседневной жизни — тёзки. Решение — фамилии: есть полное имя для точного обращения, но в большинстве случаев удобно и возможно обращаться только по имени. Пространства имен (namespaces) играют ту же роль, что и фамилии. Например, все типы, функции и переменные стандартной библиотеки находятся в пространстве имен `std` (standard): `cout` — это `std::cout` и т. д. При использовании конструкции `using namespace std;` компилятор при обращении к имени [`foo`](https://en.wikipedia.org/wiki/Foobar) сначала попробует найти `foo`, а если не найдет, то `std::foo`. Мы вернемся к пространствам имен через пару лекций, когда будем изучать программы из нескольких модулей. ## Тип данных `vector` Если нужен массив, длина которого неизвестна на этапе компиляции, можно создать этот массив динамически оператором `new`: ```cpp size_t count; cin >> count; float* numbers = new float[count]; // ... delete[] numbers; ``` Однако есть ряд проблем: * Вместе с массивом `numbers` нужно везде передавать его длину `count`. * Нужно не забыть освободить память под `numbers` оператором `delete[]`. Если этого не сделать, память останется занятой, хотя программа уже не будет с ней работать. Это называется утечкой памяти (memory leak). * Если нужно добавить элемент, нужно выделить новый динамический массив, скопировать туда элементы старого и освободить старый. Поэтому в современных программах на C++ динамическую память стараются не использовать напрямую. Вместо этого применяют более удобные типы данных, которые могут использовать динамическую память внутри себя, но с точки зрения программиста лишены перечисленных недостатков. Например, уже известный тип `string` делает удобной работу с динамическим массивом символов (элементов типа `char`). Новый тип для работы с динамическими массивами из любых элементов — `vector`. Для его использования нужно подключить заголовочный файл: ```cpp #include ``` При объявлении переменной-вектора нужно указать тип элементов в угловых скобках: ```cpp vector numbers; ``` При такой форме, без указания начального значения, вектор будет пуст (то есть будет иметь 0 элементов). Можно сразу указать размер `count`: ```cpp vector numbers(count); ``` Значение `count` должно быть уже задано в этот момент. Частая ошибка на лабораторных работах: ```cpp size_t count; vector numbers(count); // ошибка: значение count еще не введено! cin >> count; ``` Вектор автоматически инициализирует свои элементы-числа нулями. Можно создать вектор, который будет содержать заданный набор элементов. Это может быть удобно в примерах и тестах: ```cpp vector numbers{1, 2, 3, 4, 5}; ``` Память, выделенную под элементы вектора, не требуется явно освобождать, как это было с динамическим массивом и оператором `delete[]`. Как только переменная `numbers` выйдет из области видимости (из блока, где она объявлена), память будет автоматически освобождена. К элементам вектора можно получить доступ по индексу, как с массивом: ```cpp cin >> numbers[3]; cout << numbers[3]; ``` При доступе по индексу не проверяется, что он в допустимых границах. Узнать текущий размер вектора можно методом `size()`, как у `string`: ```cpp for (size_t i = 0; i < numbers.size(); i++) { cin >> numbers[i]; } ``` Первый элемент вектора можно получить как `numbers[0]` или `numbers.front()` Последний элемент можно получить как `numbers[numbers.size() - 1]` или как `numbers.back()`. Разумеется, первый и последний элемент есть только при `numbers.size() > 0`. Здесь показан пример перебора элементов вектора по индексу. Но код программы должен быть как можно ближе к своему смыслу. Здесь написано: увеличивать значение индекса, пока оно не превысит такое-то, а то, что этот код перебирает элементы вектора, видно лишь косвенно. Для перебора векторов (и других коллекций) есть более ясная форма цикла `for`, **диапазонный `for` (range-based for-loop):** ```cpp for (float number : numbers) { cout << number; } ``` Тело цикла будет выполнено для каждого элемента `numbers`, который в теле цикла будет иметь имя `number`. Иногда требуется увеличить или усечь вектор до определенного размера. Метод `resize()` изменяет размер вектора на заданный, выделяя при необходимости новую память и заполняя новые элементы нулями: ```cpp numbers.resize(count * 2); ``` Иногда невозможно заранее сказать размер вектора, например, если числа считываются, пока не будет введен 0. Метод `push_back()` добавляет значение в конец вектора, увеличивая его размер: ```cpp float number; while (true) { cin >> number; if (number == 0) { break; } numbers.push_back(number); } ``` Добавить значение в начало вектора, или в любое другое его место, можно методом `insert(позиция вставки, новый элемент)`: ```cpp numbers.insert(0, 42); ``` Элементы после мета вставки будут автоматически сдвинуты. Очевидно, что это определенная работа, которая хоть и не видна в коде, но замедляет выполнение. Поэтому лучше часто не делать эту операцию с векторами, а если алгоритму это необходимо, возможно, найдется лучший тип, чем вектор. В векторе можно искать значение: ```cpp auto it = numbers.find(42); ``` Каков тип переменной `it`? К сожалению, это не индекс элемента. При работе с векторами и другими контейнерами стандартной библиотеки широко применяются так называемые **итераторы,** именно им является `it`. Итератор — это обобщение указателя. Итератор представляет место элемента. Они придуманы и используются затем, чтобы одинаковый код мог работать как с простыми структурами данных, такими как вектор, так и с более сложными. Например, имея итератор `it`, можно переместить его к следующему элементу кодом `++it`. Для вектора переход к следующему элементу — это просто увеличение указателя или индекса, поэтому замена их на итераторы не кажется ценной. Однако, например, для связанного списка `list` переход к следующему элементу — совсем другая операция, но `list` тоже использует итераторы и переход тоже делается `++it`. Благодаря этому, можно заменить тип `numbers` с `vector`, а старый код, использующий итераторы, продолжит работать. Итак, если элемент `42` нашелся в `numbers`, `it` будет итератором, представляющим позицию этого элемента. А если `42` нет в `numbers`? Метод `find()` вернет специальное значение итератора: ```cpp auto it = numbers.find(42); if (it == numbers.end()) { cout << "Элемент не найден.\n"; } else { cout << *it; // 42 } ``` В примере также показано разыменование итератора `*it`, которое работает так же, как разыменование указателя, то есть будет получено число 42. Проверка, что `it` не равен `numbers.end()` перед тем, как пользоваться `it`, похожа на проверку указателя на `nullptr` (`NULL`). Однако `numbers.end()` не подобен нулевому указателю. Это специальный итератор, представляющий позицию *за концом* вектора: ``` numbers.size() == 5 |←-----------------→| +---+---+---+---+---+ | 1 | 2 | 3 | 4 | 5 | +---+---+---+---+---+ ↑ ↑ numbers.begin() numbers.end() ``` Аналогично, метод `begin()` возвращает итератор, представляющий первый элемент. Схематично можно сказать, что эти итераторы описывают весь вектор как полуинтервал `[begin; end)`. Зная об этих двух итераторах, а также о том, что итератор можно сдвигать к следующему элементу, можно написать такой код: ```cpp for (auto it = numbers.begin(); it != numbers.end(); ++it) { cin >> *it; // ввод значения элемента, на котором находится it } ``` Изначально `it` стоит на начальном элементе, затем перемещается к следующему, пока не окажется за концом вектора. То есть это снова цикла перебора элементов. На самом деле, диапазонный цикл `for` является просто более наглядной формой записи для такого цикла **(синтаксическим сахаром, syntax sugar).** Иногда такая явная форма бывает удобнее. Рассмотрим метод для удаления элемента из вектора: ```cpp numbers.erase(it); ``` Он принимает итератор, находящийся на элементе, который нужно удалить. Сложность возникает, если нужно делать это в цикле: ```cpp for (auto it = numbers.begin(); it != numbers.end(); ++it) { if (*it % 2 == 0) { numbers.erase(it); } } ``` Он не будет работать правильно. Допустим, встретился элемент с четным значением, и для `it` был вызван `erase()`. После этого элемента, на котором стоял `it`, больше нет, то есть `it` больше не является корректным значением итератора — говорят, что `erase()` его **инвалидирует (invalidate).** Применять `++it` к инвалидированному итератору не имеет смысла. Как исправить ошибку? Метод `erase()` возвращает итератор, стоящий сразу за удаленным элементом, то есть либо на следующем оставшемся элементе, либо `numbers.end()`, если удаленный элемент был последним. Таким образом, правильный алгоритм будет таким: если нужно удалить элемент, то следующим значением `it` будет результат `erase()`, иначе следующее значение нужно получать через `++it`: ```cpp auto it = numbers.begin() while (it != numbers.end()) { if (*it % 2 == 0) { it = numbers.erase(it); } else { ++it; } } ``` Одна из частых задач — упорядочить вектор по возрастанию. Для этого можно воспользоваться стандартной функцией `sort()`: ```cpp #include // ... sort(numbers.begin(), numbers.end()); // здесь numbers уже отсортирован ```