Родитель
							
								
									d57d26497a
								
							
						
					
					
						Сommit
						c1fc5cfd63
					
				| @ -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]<bins[i])){ | ||||
| 
 | ||||
| for(int j=0; j<bins[i]-1; j++) { | ||||
| cout<<'*'; | ||||
|         } | ||||
| ``` | ||||
| 
 | ||||
| Пример хорошего оформления того же кода: | ||||
| вложенность блоков выделена отступами, | ||||
| вокруг операторов стоят пробелы. | ||||
| 
 | ||||
| ```cpp | ||||
| if ((i > 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<T>` | ||||
| 
 | ||||
| Если нужен массив, длина которого неизвестна на этапе компиляции, | ||||
| можно создать этот массив динамически оператором `new`: | ||||
| 
 | ||||
| ```cpp | ||||
| size_t count; | ||||
| cin >> count; | ||||
| float* numbers = new float[count]; | ||||
| // ... | ||||
| delete[] numbers; | ||||
| ``` | ||||
| 
 | ||||
| Однако есть ряд проблем: | ||||
| 
 | ||||
| * Вместе с массивом `numbers` нужно везде передавать его длину `count`. | ||||
| 
 | ||||
| * Нужно не забыть освободить память под `numbers` оператором `delete[]`. | ||||
|     Если этого не сделать, память останется занятой, хотя программа уже | ||||
|     не будет с ней работать. Это называется утечкой памяти (memory leak). | ||||
| 
 | ||||
| * Если нужно добавить элемент, нужно выделить новый динамический массив, | ||||
|     скопировать туда элементы старого и освободить старый. | ||||
| 
 | ||||
| Поэтому в современных программах на C++ | ||||
| динамическую память стараются не использовать напрямую. | ||||
| Вместо этого применяют более удобные типы данных, | ||||
| которые могут использовать динамическую память внутри себя, | ||||
| но с точки зрения программиста лишены перечисленных недостатков. | ||||
| 
 | ||||
| Например, уже известный тип `string` делает удобной работу | ||||
| с динамическим массивом символов (элементов типа `char`). | ||||
| 
 | ||||
| Новый тип для работы с динамическими массивами из любых элементов — `vector<T>`. | ||||
| 
 | ||||
| Для его использования нужно подключить заголовочный файл: | ||||
| 
 | ||||
| ```cpp | ||||
| #include <vector> | ||||
| ``` | ||||
| 
 | ||||
| При объявлении переменной-вектора нужно указать тип элементов в угловых скобках: | ||||
| 
 | ||||
| ```cpp | ||||
| vector<float> numbers; | ||||
| ``` | ||||
| 
 | ||||
| При такой форме, без указания начального значения, вектор будет пуст | ||||
| (то есть будет иметь 0 элементов). Можно сразу указать размер `count`: | ||||
| 
 | ||||
| ```cpp | ||||
| vector<float> numbers(count); | ||||
| ``` | ||||
| 
 | ||||
| Значение `count` должно быть уже задано в этот момент. | ||||
| Частая ошибка на лабораторных работах: | ||||
| 
 | ||||
| ```cpp | ||||
| size_t count; | ||||
| vector<float> numbers(count);  // ошибка: значение count еще не введено! | ||||
| cin >> count; | ||||
| ``` | ||||
| 
 | ||||
| Вектор автоматически инициализирует свои элементы-числа нулями. | ||||
| Можно создать вектор, который будет содержать заданный набор элементов. | ||||
| Это может быть удобно в примерах и тестах: | ||||
| 
 | ||||
| ```cpp | ||||
| vector<float> 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<T>` | ||||
| переход к следующему элементу — совсем другая операция, | ||||
| но `list<T>` тоже использует итераторы и переход тоже делается `++it`. | ||||
| Благодаря этому, можно заменить тип `numbers` с `vector<float` на `list<float>`, | ||||
| а старый код, использующий итераторы, продолжит работать. | ||||
| 
 | ||||
| Итак, если элемент `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 <algorithm> | ||||
| // ... | ||||
| sort(numbers.begin(), numbers.end()); | ||||
| // здесь numbers уже отсортирован | ||||
| ``` | ||||
					Загрузка…
					
					
				
		Ссылка в новой задаче