diff --git a/README.md b/README.md index 21e7465..a55bdd2 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ ## Лекции -1. Основы языка C++ +1. [Основы языка C++](lectures/lecture01) 2. Системы контроля версий 3. Структурирование кода и данных (функции, указатели) 4. Сборка программ из нескольких файлов diff --git a/lectures/lecture01/README.md b/lectures/lecture01/README.md new file mode 100644 index 0000000..f835739 --- /dev/null +++ b/lectures/lecture01/README.md @@ -0,0 +1,441 @@ +# Лекция 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 уже отсортирован +```