Вы не можете выбрать более 25 тем
Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.
442 строки
23 KiB
Markdown
442 строки
23 KiB
Markdown
# Лекция 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 уже отсортирован
|
|
```
|