Константность. Структуры
Константность
Константная переменная доступна только для чтения, ее изменение невозможно. Значение константной переменной должно быть зафиксировано в момент присваивания (то есть может быть неизвестно в момент компиляции)
Например:
#include <iostream>
int main() {
// известна до компиляции (compile time)
const int const_var_1 = 42;
int number;
std::cin >> number;
// определяется только во время исполнения (run time)
const int const_var_2 = 2 * number;
+= 1; // невозможно, будет ошибка компиляции
const_var_1
}
В первую очередь константность необходима для ясности программы: мы намеренно накладываем ограничение на использование переменной, что гарантировать ее неизменность в нашем коде, так и при вызове сторонних функций и методов.
Например, некоторые методы (которые его изменяют) константного вектора (const std::vector<T>
) недоступны:
#include <iostream>
#include <vector>
int main() {
const std::vector<int> v = {1, 3, 5};
std::cout << v.size() << "\n"; // 3
.clear(); // nope: Compilation Error
v[0] = 0; // nope again: Compilation Error
v}
Константные ссылки
Как известно, ссылка – это псевдоним переменной, который ведет себя ровно так же, как и “оригинал”. Теперь предстоит узнать, что можно создать псевдоним только для чтения – это и будет константная ссылка
int main() {
int x = 42;
int& ref = x;
const int& const_ref = x; // константная ссылка
++x; // инкремент оригинала -- ОК
++ref; // инкремент обычной ссылки -- ОК
++const_ref; // изменение по константной ссылке невозможно -- CE
}
Наиболее это полезно в аргументах функции. Рассмотрим модифицированный пример из прошлой главы и объясним, почему в нем стоило использовать именно константную ссылку:
#include <iostream>
#include <string>
size_t countChar(const std::string& line, char chr_to_count) {
size_t cnt = 0;
for (const char& chr : line) {
if (chr == chr_to_count) {
++cnt;
}
}
return cnt;
}
int main() {
std::string input_line;
std::getline(std::cin, input_line);
// мы уверены, что строке не будет изменена
std::cout << countChar(input_line, 'a') << '\n';
// было бы невозможно с неконстантной ссылкой: переменная константна
const std::string const_line = "another constant line";
std::cout << countChar(const_line, 'a') << '\n';
// было бы невозможно с неконстантной ссылкой: переменной по факту нет
std::cout << countChar("some random line with many aaaaa", 'a') << '\n';
}
Использование константной ссылки позволяет:
расширить возможности для вызова функции: можно использовать константные переменные и литералы
убедиться, что переменная нигде не изменится: иначе будет ошибка компиляции
дать больше гарантий пользователям функций: уже по сигнатуре функции можно понять, какие переменные подлежат изменению, а какие – нет
Вложенные std::vector<T>
Вложенными векторами мы будем называть векторы из векторов, то есть объекты типа std::vector<std::vector<int>>
. Это вектор, элементами которого являются векторы, а их элементами являются числа.
Например:
#include <iostream>
#include <vector>
int main() {
const std::vector<std::vector<int>> data = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};
std::cout << data[0][0] << ' ' << data[1][1] << ' ' << data[2][2] << '\n';
// 1 5 9
}
Так работает индексация в двумерном векторе:
Индексация в двумерном векторе: сначала получаем ряд, потом элемент в этом ряду
Двумерный вектор из примера выше
Создадим двумерный вектора и выведем его элементы. Затем выведем только его диагональ.
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
size_t m, n;
std::cin >> m >> n; // число строк и столбцов
// создаём матрицу matrix из m строк
std::vector<std::vector<int>> matrix(m);
// std::vector<std::vector<int>> matrix(m, std::vector<int>(n));
for (size_t i = 0; i < m; ++i) {
[i].resize(n);
matrixfor (size_t j = 0; j < n; ++j) {
std::cin >> matrix[i][j];
}
}
// тип matrix[i] -- это std::vector<int> -- как и раньше,
//переходим к элементу вектора по индексу
// напечатаем матрицу, выводя элементы через табуляцию
for (size_t i = 0; i < m; ++i) {
for (size_t j = 0; j < n; ++j) {
std::cout << matrix[i][j] << '\t'; // сначала индекс строки, затем индекс элемента в этой строке
}
std::cout << '\n';
}
std::cout << '\n';
// элементы на диагонале обладают равными индексами (см. иллюстрации выше)
for (size_t i = 0; i < std::min(n, m); ++i) {
std::cout << matrix[i][i] << ' ';
}
std::cout << '\n';
}
Вывод программы выше, если будет введена квадратная матрица из примера выше:
1 2 3
4 5 6
7 8 9
1 5 9
Напишем функцию, которая будет получать по константной ссылке двумерный вектор чисел и возвращать наибольшее значение. Главное, что здесь нужно отметить – перебор всех элементов почти не изменился от того, что было рассмотрено в прошлых главах.
#include <iostream>
#include <vector>
int findMaxValue(const std::vector<std::vector<int>>& matrix) {
int maxValue = matrix[0][0];
// во время изучения рекомендуется явно писать даже сложные типы и не использовать тип auto
for (const std::vector<int>& row : matrix) {
for (const int& num : row) {
if (num > maxValue) {
= num;
maxValue }
}
}
return maxValue;
}
int main() {
std::vector<std::vector<int>> matrix = {{1, 5, 3}, {8, 2, 6}, {4, 9, 7}};
int maxVal = findMaxValue(matrix);
std::cout << maxVal << '\n'; // 9
}
Можно аналогично реализовать функцию поиск наибольшего значения в двумерном векторе с использование обращения по индексу:
int findMaxValue(const std::vector<std::vector<int>>& matrix) {
int maxValue = matrix[0][0];
for (size_t i = 0; i < matrix.size(); ++i) {
for (size_t j = 0; j < matrix[i].size(); j++) {
if (matrix[i][j] > maxValue) {
= matrix[i][j];
maxValue }
}
}
return maxValue;
}
В домашнем задании буду задачи на обход двумерных векторов для поиска максимального значения при соблюдении разных условий
Структуры
Часто, особенно в больших программах, может возникнуть желание объединить набор переменных не только логически, но и в коде: например, единым названием некого объекта. Структуры позволяют объединять переменные и создавать новые “составные” типы.
Создадим структуру, которая будет хранить информацию о человеке, его:
имя
рост
возраст
флаг, был ли он отчислен из университета
#include <iostream>
#include <vector>
#include <string>
struct Person {
std::string name; // Имя
int height; // Рост в сантиметрах
int age; // Возраст
bool expelled; // Флаг: был ли отчислен из университета
};
int main() {
;
Person person1.name = "Алексей";
person1.height = 180;
person1.age = 22;
person1.expelled = false;
person1
{"Мария", 165, 20, true};
Person person2
= {"Иван", 175, 23, false};
Person person3
{"Екатерина", 170, 21, false};
Person person4{"Петр", 185, 24, true};
Person person5
// Создаём вектор людей
std::vector<Person> people = {person1, person2, person3, person4, person5};
// Выводим информацию обо всех людях в векторе
std::cout << "Информация о людях:\n";
for (const Person& person : people) {
std::cout << "Имя: " << person.name << "\n"
<< "Рост: " << person.height << " см\n"
<< "Возраст: " << person.age << " лет\n"
<< "Отчислен из университета: ";
if (person.expelled) {
std::cout << "Да";
} else {
std::cout << "Нет";
}
std::cout << "\n" << "---------------------------\n";
}
return 0;
}
Вывод такой программы:
Информация о людях:
Имя: Алексей
Рост: 180 см
Возраст: 22 лет
Отчислен из университета: Нет
---------------------------
Имя: Мария
Рост: 165 см
Возраст: 20 лет
Отчислен из университета: Да
---------------------------
Имя: Иван
Рост: 175 см
Возраст: 23 лет
Отчислен из университета: Нет
---------------------------
Имя: Екатерина
Рост: 170 см
Возраст: 21 лет
Отчислен из университета: Нет
---------------------------
Имя: Петр
Рост: 185 см
Возраст: 24 лет
Отчислен из университета: Да
---------------------------
Точка как структура
Рассмотрим точки в трехмерном пространстве, то есть тройки вещественных чисел \((x, y, z)\). Напишем 2 функции:
нахождение расстояния между двумя точками
нахождение ближайшей точки из динамического массива
std::vector
\(p(p_1, p_2) = \sqrt{(x_1 - x_0)^2 + (y_1 - y_0)^2 + (z_1 - z_0)^2}\)
#include <iostream>
#include <vector>
#include <cmath>
#include <limits>
struct Point {
double x = 0.0;
double y = 0.0;
double z = 0.0;
};
double getDistance(const Point& first, const Point& second) {
double sq_disatnce = std::pow(first.x - second.x, 2) + std::pow(first.y - second.y, 2)
+ std::pow(first.z - second.z, 2);
return std::sqrt(sq_disatnce);
}
(const Point& target, const std::vector<Point>& points) {
Point findClosestPoint= points[0];
Point closest_point double minDistance = getDistance(closest_point, target);
for (const Point& point : points) {
double distance = getDistance(point, target);
if (distance < minDistance) {
= distance;
minDistance = point;
closest_point }
}
return closest_point;
}
int main() {
// Определим несколько точек
(1.0, 2.0, 3.0);
Point p1(4.0, 5.0, 6.0);
Point p2(7.0, 8.0, 9.0);
Point p3(2.0, 2.0, 2.0);
Point p4std::vector<Point> points = {p1, p2, p3};
double dist = getDistance(p1, p2);
std::cout << "Расстояние между p1 и p2: " << dist << '\n';
// Найдём ближайшую точку к p4 из вектора
= findClosestPoint(p4, points);
Point closest std::cout << "Ближайшая точка к p4: (" << closest.x << ", " << closest.y << ", " << closest.z
<< ")\n";
}
Расстояние между p1 и p2: 5.19615
Ближайшая точка к p4: (1, 2, 3)
Выравнивание структур
Сумма размеров полей структуры не всегда совпадет с настоящим ее размером. Например, следующие структуры будут иметь одинаковый размер:
#include <iostream>
struct TestStruct1 {
double a;
double b;
};
struct TestStruct2 {
double a;
char b;
};
int main() {
std::cout << sizeof(TestStruct1) << '\n'; // 16 = 8 + 8
std::cout << sizeof(TestStruct2) << '\n'; // 16 != 8 + 1
}
Процессору быстрее и проще читать “выравненные” (“aligned”) адреса в памяти – те, которые делятся на 4 (реже – на 8). В случае структур процессору проще отсчитывать размеры наибольшего поля в структуре (в нашем случае double
). То есть компилятор применяет выравние структуры (structure alignment), добавляя лишние невидимые байты между полями
В действительности TestStruct2
будет дополнена тремя байтами выравнивания и сохранится как:
struct TestStruct2 {
double a;
char b;
// или проще char gap[7]; но мы не смотрели такие массивы
char n1;
char n2;
char n3;
char n4;
char n5;
char n6;
char n7;
};
К добавленным полям выравнивания нельзя получить доступ по имени (у них его нет).
Более сложный случай выравнивания:
struct TestStruct1 { // 24 байта
char a;
int b;
char c;
double d;
};
struct TestStruct1 { // 24 байта
char a;
char gap_1[3];
int b;
char c;
char gap_2[7];
double d;
};
struct TestStruct1 { // 16 байта
double d;
int b;
char c;
char a;
};
Последний пример иллюстрирует, что аккуратная перестановка полей структуры позволяет иногда экономить память на выравнивании полей.