Типизация - механизм, который защищает нас от того, чтобы использовать объект одного типа вместо объекта другого, или позволяет управлять таким использованием. В данном разделе мы научимся именно управлять такими использованием, то есть приводить один тип к другому. Данная операция может быть как и безопасной (например int -> int64_t), так и опасной(int64_t -> int). Рассмотрим инстументы для приведения типов в С++:
Неявное приведение типов. Неявное приведение происходит тогда, когда в выражении мы используем совместимые типы. Пример:
long longInt = 10; // напоминание: 10 - литерал типа int
Когда неявного приведения типов недостаточно, нужно использовать операторы, первый из которых
int a = 7;
double* ptr = (double*) &a;
Недостатки такого приведения: в C++ есть 4 типа приведения (static_cast, reinterpret_cast, const_cast и dynamic_cast, о них будет ниже). Когда мы используем оператор приведения С-стиля, непонятно какой тип cast - a мы хотим использовать кастование, и зачастую используем не то, что хотим.
int a = 7;
int* ptr2 = static_cast<int*>(&a);
Пример неправильного использования static_cast:
int a = 7;
double* ptr3 = static_cast<double*>(&a); // ошибка, потому что мы не можем для одной и той же области памяти использовать и int, и double.
int a = 7;
double* ptr4 = reinterpret_cast<double*>(&a); //теперь эта область памяти храниться как double
const int b = 8;
int* ptr5 = static_cast<int*>(&b); // ошибка
int* ptr6 = const_cast<int*>(&b);
std::pair - контейнер, который хранит в себе ровно 2 элемента. Для понимания всех возможностей std::pair, рассмотрим пример его использования:
std::pair<int, std::string> mypair; // тип первого элемента - int, второго - std::string
mypair = std::make_pair(1, "abacaba"); // первый вариант присвоить значение std::pair
mypair = {1, "abacaba"}; // второй вариант
std::cout << mypair.first << ' ' << mypair.second; // вывод - 1 abacaba
mypair.first = 2;
std::cout << mypair.first; // вывод - 2
Недостатки: нечитаемый код, так как зачастую мы имеем в паре 2 одинаковых типа, которые очень легко перепутать
Чтобы исправить вышеуказанный недостаток, можно воспользоваться еще одним инструментом С++:
Допустим мы хотим создать тип Student, который хранит в себе имя и фамилию студента.
struct Student {
std::string name;
std::string last_name;
int grade = 11; // дефолтная инициализация примитивных типов
};
Ниже представлен пример работы со Student-ом:
Student student;
student.name = "Ivan";
student.last_name = "Ivanov";
std::cout << student.grade; //вывод - 11
Если мы напишем следующий код:
Student student{};
то все поля структуры все поля проинициализируются дефолтными значениями. Если скобочки убрать, то никакие поля инициализироваться не будут. Другой способ инициализации переменных выглядит вот так:
Student student{"Ivan", "Ivanov", 10};
Стоит заметить, что таким способом можно инициализировать не все поля, то есть следующий код будет работать:
Student student{"Ivan", "Ivanov"};
Такой способ инициализации явно имеет недостатки: при изменении порядка объявления переменных возникнут ошибки. Поэтому, начиная с С++20, поля можно инициализировать следующим образом:
Student student{.name = "Ivan", .last_name = "Ivanov"};
Стоит отметить, что поля должны идти в том же порядке, что и в структуре, то есть следующий код компилироваться не будет:
Student student{.last_name = "Ivanov", .name = "Ivan"}; // ошибка
Мотивация: допустим нам на вход подается несколько элементов, мы хотим их как-то хранить и получать их в том же порядке. Для решения вышепоставленной задачи сущестувет несколько контейнеров, рассмотрим некоторые из них:
Рассмотрим синтаксис инициализации данного контейнера:
int numbers[5] = {0}; // numbers эквивалентен [0, 0, 0, 0, 0]. Работает только для 0
int numbers[5] = {1, 2, 3, 4, 5}; // numbers эквивалентен [1, 2, 3, 4, 5]
int numbers[5] = {1}; // numbers эквивалентен [1, 0, 0, 0, 0]
int[5] numbers = {0}; // ОШИБКА
Данный тип используется редко из-за убогого синтакиса. Например массив в функцию надо передавать как указатель, поскольку массив не обладает value-семантикой(то есть его никуда нельзя передать по значению):
void PrintNumbers(int* numbers) {
for (int i = 0; i < 5; ++i) {
std::cout << numbers[i]; // OK, вывод - 12345
}
for (auto number : numbers) { // не работает, так как numbers не хранит информацию о размере
...
}
}
int main() {
int numbers[5] = { 1,2,3,4,5 };
int* numbers_ptr = numbers;
PrintNumbers(numbers_ptr);
}
На смену С-массива пришли другие контейнеры из C++:
Рассмотрим синтаксис объявления данного типа:
std::array<int, 5> numbers = {1, 2, 3, 4, 5};
std::array<int, 5> numbers2 = {1, 2, 3}; //эквивалентно [1, 2, 3, 0, 0]
При этом если теперь заменить в функции PrintNumbers int* на std::array, то все будет работать:
void PrintNumbers(const std::array<int, 5>& numbers) {
for (auto number : numbers) {
std::cout << number;
}
}
int main() {
std::array<int, 5> numbers = {1, 2, 3, 4, 5};
PrintNumbers(numbers);
}
Недостаток std::array - он не умеет добавлять элементы и очень неудобно носить с названием размер массива, поэтому рассмотрим более усовершенствованный контейнер
std::vector имеет похожий синтаксис с std::array:
void PrintNumbers(const std::vector<int>& numbers) {
for (auto number : numbers) {
std::cout << number;
}
}
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5};
PrintNumbers(numbers);
}
Свойства std::vector:
Рассмотрим примеры работы с итераторами:
std::vector<int>::iterator numbers_begin = numbers.begin(); // - итератор на первый элемент массива
std::cout << *numbers_begin; // вывод - 1
auto numbers_end = numbers.end(); // - итератор на элемент ЗА последним
for (auto it = numbers.begin(); it < numbers.end(); ++it) {
std::cout << *it << ' '; // вывод - 1 2 3 4 5
}
У итераторов реализованы операторы +=, -=, –, ++
std::list - реализация двусвязного списка в C++. В отличие от std::vector, он не имеет возможности получения любого элемента за O(1). Поэтому, чтобы достать i-ый элемент из него, нужно написать это вручную:
std::list<int> numbers = {1, 2, 3, 4, 5};
auto it = numbers.begin();
for (size_t i = 0; i < 2; ++i) {
++it;
}
std::cout << *it; // вывод - 3
Так же, мы не можем сравнивать итераторы у std::list, поэтому такой проход компилироваться не будет:
for (auto it = numbers.begin(); it < numbers.end(); ++it) { //ошибка
std::cout << *it << ' ';
}
Но у нас есть операторы == и !=
for (auto it = numbers.begin(); it != numbers.end(); ++it) { работает
std::cout << *it << ' ';
}
Одно из преимуществ std::list перед std::vector - вставка элемента по итератору в произвольное место за O(1):
std::list<int> numbers = {1, 2, 3, 4, 5};
for (auto it = numbers.begin(); it != numbers.end(); ++it) {
numbers.insert(it, 0);
}
numbers.push_front(100);
numbers.push_back(101);
for (auto it = numbers.begin(); it != numbers.end(); ++it) {
std::cout << *it << ' '; // вывод - 100 0 1 0 2 0 3 0 4 0 5 101
}
std::deque - двусвязный список буфферов. У std::deque есть и [], и push_back, и push_front, и insert. Пример использования
std::deque<int> numbers = { 1, 2, 3, 4 };
numbers.insert(numbers.begin() + 1, 5);
numbers.push_back(6);
for (auto elem : numbers) {
std::cout << elem << ' '; // вывод - 1 5 2 3 4 6
}