W1. Введение в C++, пространства имён, массивы и векторы, ссылки и константы, вывод типов

Автор

Eugene Zouev, Munir Makhmutov

Дата публикации

29 января 2026 г.

1. Краткое содержание

1.1 Организация курса

Курс Software Systems Analysis and Design (анализ и проектирование программных систем) построен из нескольких компонентов: теория и практика программирования на C++ сочетаются так, чтобы вы получили и концептуальную базу, и навыки написания кода.

1.1.1 Компоненты курса

Курс включает три основных компонента:

  • Лекции — теоретические концепции и основы языка: programming concepts и возможности C++ на концептуальном уровне.
  • Туториалы — дополнительные примеры и подробные разборы тем лекций: отдельные аспекты и практическое применение теории.
  • Лабораторные работы — практика программирования: упражнения и задания, чтобы закрепить навыки.
1.1.2 Оценивание

Итоговая оценка складывается из нескольких частей:

  • Mid-term Exam (промежуточный экзамен): 25% (викторина в Moodle, 10 марта)
  • Final Exam (итоговый экзамен): 30% (письменная форма)
  • Assignments (домашние задания / ассайнменты): 40% (4 задания, регулярная проверка)
  • Посещаемость лаб: 5%
  • Бонус: 5%

Шкала: A [90, 100], B [75, 90), C [60, 75), D [0, 60).

1.2 Как работают программы на C++
1.2.1 От исходного кода до исполняемого файла

Понимание цепочки выполнения проясняет многие дальнейшие темы. Путь кода выглядит так:

  1. Source code (исходный код): вы пишете C++ в файлах .cpp (текст для человека).
  2. Preprocessing (препроцессинг): препроцессор обрабатывает директивы вроде #include (подключение заголовков) и #define (макросы).
  3. Compilation (компиляция): compiler (компилятор) переводит C++ в machine code (машинный код) — двоичные инструкции для CPU.
  4. Linking (линковка): linker (линкер) объединяет скомпилированный код с библиотеками (например C++ Standard Library) в executable (исполняемый файл).
  5. Execution (исполнение): ОС загружает и запускает исполняемый файл.

Ключевое различие:

  • Compile time (время компиляции): шаги 2–4 до запуска программы. Компилятор проверяет типы, вычисляет constant expressions и генерирует машинный код.
  • Runtime (время выполнения): шаг 5 — когда программа реально работает: ввод пользователя, dynamic memory allocation и логика программы.
1.2.2 Что такое компилятор?

Compiler (компилятор) — программа, которая переводит source code в machine code. Распространённые компиляторы C++:

  • GCC (GNU Compiler Collection) — свободный, широко используемый
  • Clang — современный, с понятными сообщениями об ошибках
  • MSVC (Microsoft Visual C++) — для разработки под Windows

Задачи компилятора включают:

  • проверку syntax (синтаксиса);
  • type checking (проверку типов);
  • optimization (оптимизацию);
  • code generation (генерацию кода).
1.2.3 Стандарты C++

C++ развивается; в разных версиях появляются новые возможности:

  • C++98: первый стандартизованный вариант
  • C++11: крупное обновление (auto, range-based for, nullptr)
  • C++14: небольшие улучшения
  • C++17: structured bindings, optional, variant
  • C++20: concepts, ranges, coroutines, modules
  • C++23 и C++26: ещё больше возможностей

В этом курсе используем стандарт C++20: доступны все средства вплоть до C++20 включительно.

1.3 Основы программирования на C++
1.3.1 Первая программа на C++

Базовая программа состоит из нескольких обязательных элементов:

#include <iostream>
int main()
{
    std::cout << "Hello world" << std::endl;
    return 0;
}

Разберём каждый фрагмент:

  • #include <iostream>preprocessor directive (директива препроцессора): подключает стандартную библиотеку ввода-вывода и даёт сущности вроде cout для вывода в консоль.
  • int main()main function (функция main), с которой начинается выполнение. В каждой программе на C++ ровно одна main; среду исполнения интересует именно она.
  • std::coutstandard output stream (стандартный поток вывода) для печати в консоль; это часть пространства имён std.
  • <<stream insertion operator (оператор вставки в поток), перегруженный для потоков ввода-вывода (для целых это был бы сдвиг, здесь — другая семантика).
  • std::endl — символ конца строки и flush буфера вывода.
  • return 0; завершает main и возвращает 0 окружению как признак успешного завершения.
1.3.2 Модель памяти

В программе на C++ различают три вида памяти с разными ролями:

  1. Program memory (память кода): скомпилированные инструкции; обычно только для чтения — самомодифицирующийся код в C++ не допускается.
  2. Heap (куча, dynamic memory): объекты, выделенные в runtime через new и подобные средства; программист задаёт моменты выделения/освобождения. Дисциплина кучи задаётся dynamic semantics (динамической семантикой).
  3. Stack (стек): локальные переменные и информация о вызовах функций; управляется автоматически по области видимости и стеку вызовов. Дисциплина стека следует из static program structure (статической структуры программы), определяемой на этапе компиляции.

%%{init: {'theme': 'base', 'themeVariables': { 'fontFamily': 'Helvetica', 'primaryColor': '#e8f4f8', 'primaryTextColor': '#1f2d3d', 'primaryBorderColor': '#355c7d', 'lineColor': '#355c7d', 'secondaryColor': '#d6eef5', 'tertiaryColor': '#fff3cd', 'background': '#ffffff', 'mainBkg': '#e8f4f8', 'secondBkg': '#d6eef5', 'tertiaryBkg': '#fff3cd', 'clusterBkg': '#f9fbfd', 'clusterBorder': '#355c7d', 'edgeLabelBackground': '#ffffff' }}}%%
%%| fig-cap: "Типичная раскладка памяти процесса: код, статическое хранилище, куча и стек"
%%| fig-width: 5.5
%%| fig-height: 4
flowchart TB
    Code["Сегмент кода<br/>скомпилированные инструкции"]
    Static["Статическое / глобальное хранилище<br/>глобальные и static-объекты"]
    Heap["Куча (heap)<br/>динамика через new"]
    Stack["Стек (stack)<br/>локальные переменные и кадры вызовов"]
    Code --> Static --> Heap --> Stack

1.4 Система типов
1.4.1 Что такое тип?

Type (тип) — базовое понятие C++: для сущности тип задаёт три аспекта:

  1. Values (множество значений), которые может хранить объект.
  2. Operations (операции), допустимые над объектами этого типа.
  3. Relationships (связи): преобразования к другим типам, inheritance и т.д.

Например, тип int:

  • Values: целые в типичном диапазоне (часто от −2 147 483 648 до 2 147 483 647).
  • Operations: создание, уничтожение, копирование, перемещение, арифметика (+, −, *, /), сравнения (==, <, >), побитовые операции и сдвиги.
  • Relationships: преобразования к bool, float, double и др.
1.4.2 Иерархия типов C++

Типы C++ удобно мыслить иерархически.

Fundamental types (фундаментальные типы):

  • Atomic types (атомарные типы): целые (int, short, long, long long), символы (char, wchar_t, char16_t, char32_t, char8_t), вещественные (float, double, long double), логический (bool).
  • Pointers (указатели): переменные-адреса.

User-defined types (пользовательские типы):

  • Compound types (составные типы): массивы, структуры, объединения, классы, перечисления.
1.4.3 Syntax vs semantics

Важное различие в языках программирования:

  • Syntax (синтаксис): правила структуры конструкций («грамматика» языка).
  • Semantics (семантика) — смысл конструкций:
    • Static semantics (статическая семантика): как программа компилируется и проверяется по типам.
    • Dynamic semantics (динамическая семантика): как программа выполняется в runtime.

Важный принцип: синтаксис относительно лёгок, а семантика C++ огромна и сложна. Опирайтесь на понимание того, что делает код, а не только на правила записи.

1.5 Пространства имён (namespaces)
1.5.1 Зачем это нужно

В больших проектах разные части кода могут использовать одни и те же имена в разных смыслах — возникают name clashes (конфликты имён). C++ даёт namespaces (пространства имён) как способ сгруппировать объявления и развести имена.

Namespace — декларативная область, задающая область видимости для идентификаторов (типов, функций, переменных и т.д.) внутри неё; так глобальная область дробится на именованные части.

1.5.2 Синтаксис namespace

Пространство имён задаётся ключевым словом namespace:

namespace Subsystem1
{
    class C1 { ... };
    int a, b;
    void f() { ... }
}

namespace Subsystem2
{
    class C2 { ... };
    int a;  // This is a DIFFERENT 'a' from Subsystem1::a
}
1.5.3 Доступ к членам пространства имён

Снаружи namespace сущности доступны через qualified naming (квалифицированные имена) и scope resolution operator :::

Формат: namespace-name::entity-name

int x = Subsystem1::a;
Subsystem1::f();
1.5.4 Глобальное пространство имён

Вся программа живёт в unnamed (global) namespace (безымянном глобальном пространстве имён). Чтобы явно обратиться к глобальной области, используйте :: без имени namespace:

int a = 5;  // Global variable

namespace Subsystem
{
    int a = 10;  // Different variable
}

int x = Subsystem::a;  // x = 10
int y = a;             // y = 5 (global a)
int z = ::a;           // z = 5 (explicitly global a)
1.5.5 Пространство имён стандартной библиотеки

Сущности C++ Standard Library объявлены в std. Типичные примеры:

  • std::cout, std::cin (I/O streams)
  • std::vector, std::list, std::map (контейнеры)
  • std::string (строковый класс)
  • std::endl (манипулятор конца строки)
1.5.6 Using-объявления

Чтобы упростить запись, using declarations вводят имена из namespace в текущую область:

using namespace std;  // Brings ALL std members into scope
cout << "Hello" << endl;  // No need for std:: prefix

Однако для крупных программ using namespace std; считается плохой практикой: теряется смысл namespaces. Лучше импортировать выборочно:

using std::cout;
using std::endl;
cout << "Hello" << endl;  // Only cout and endl are imported
1.5.7 Продвинутые возможности namespace

Multi-file namespaces: одно namespace может охватывать несколько translation units (исходных файлов):

// File 1
namespace Subsystem1 {
    class C1 { ... };
}

// File 2
namespace Subsystem1 {
    class C2 { ... };  // Still part of Subsystem1
}

Вложенные namespace: иерархическая организация:

namespace OurBigSystem
{
    namespace MySubsystem
    {
        int a;
    }
}

int x = OurBigSystem::MySubsystem::a;
1.5.8 Зачем нужны пространства имён

Представьте приложение с двумя библиотеками: в Library A и Library B есть print() с разным смыслом. Без namespaces имена конфликтуют — компилятор не поймёт, какой print() вызывать.

С namespaces:

LibraryA::print(document);  // Prints a document
LibraryB::print("Debug info");  // Prints debug message

Поэтому стандартная библиотека использует префикс std:: — чтобы не пересекаться с вашим кодом.

1.6 Указатели (pointers)
1.6.1 Что такое указатель?

Перед массивами нужны pointers (указатели): на них завязаны память и массивы в C++.

Pointer — переменная, хранящая memory address (адрес памяти). Вместо значения вроде 5 или 3.14 в ней — место, где значение лежит.

Аналогия: память как многоквартирный дом; у каждой «квартиры» (memory location):

  • address (адрес);
  • contents (содержимое ячейки).

Обычная переменная даёт прямой доступ к содержимому; pointer — к адресу, по которому можно снова получить содержимое.

1.6.2 Синтаксис указателей

Объявление указателя:

int* ptr;        // ptr is a pointer to an integer
double* dptr;    // dptr is a pointer to a double
char* cptr;      // cptr is a pointer to a character

В объявлении * читается как «указатель на». Запись int* ptr — «ptrpointer to int».

1.6.3 Операции с указателями

Две ключевые операции:

Address-of operator (&) (оператор взятия адреса): даёт адрес переменной.

int x = 42;
int* ptr = &x;   // ptr now holds the address of x

&x можно читать как «адрес, где живёт x».

Dereference operator (*) (оператор разыменования): доступ к значению по адресу в pointer.

int x = 42;
int* ptr = &x;   // ptr points to x
int value = *ptr;  // value = 42 (get the value at the address)
*ptr = 100;        // Changes x to 100 (modify the value at the address)

*ptr — «перейти по адресу в ptr и взять то, что там лежит».

%%{init: {'theme': 'base', 'themeVariables': { 'fontFamily': 'Helvetica', 'primaryColor': '#e8f4f8', 'primaryTextColor': '#1f2d3d', 'primaryBorderColor': '#355c7d', 'lineColor': '#355c7d', 'secondaryColor': '#d6eef5', 'tertiaryColor': '#fff3cd', 'background': '#ffffff', 'mainBkg': '#e8f4f8', 'secondBkg': '#d6eef5', 'tertiaryBkg': '#fff3cd', 'clusterBkg': '#f9fbfd', 'clusterBorder': '#355c7d', 'edgeLabelBackground': '#ffffff' }}}%%
%%| fig-cap: "Базовые указатели: указатель хранит адрес, разыменование ведёт к значению по этому адресу"
%%| fig-width: 6
%%| fig-height: 2.8
flowchart LR
    P["p : int*<br/>значение = &a"]
    A["a : int<br/>значение = 5"]
    P -- "хранит адрес" --> A
    D["*p == 5"]
    A --> D

1.6.4 Указатели на практике

Полный пример:

int a = 5;
int* p = &a;     // p points to a

cout << a;       // Prints: 5 (direct access)
cout << p;       // Prints: address of a (e.g., 0x7fff5fbff5ac)
cout << *p;      // Prints: 5 (indirect access through pointer)

*p = 10;         // Changes a to 10
cout << a;       // Prints: 10

Ключевая идея: меняя *p, вы меняете a, потому что в p хранится address (адрес) a.

1.6.5 Зачем нужны указатели

Pointers нужны, в частности, для:

  1. Dynamic memory allocation (динамическое выделение памяти): объекты, размер или время жизни которых неизвестны на этапе компиляции
  2. Efficient function parameters (эффективные параметры функций): передача крупных объектов по адресу вместо копирования
  3. Data structures (структуры данных): связные списки, деревья, графы и т.д.
  4. Low-level programming (низкоуровневое программирование): работа с оборудованием и API ОС
1.6.6 Null pointers

Указатель можно установить в nullptr (в старом коде часто NULL), чтобы явно обозначить, что он ни на что валидное не ссылается:

int* ptr = nullptr;  // ptr doesn't point anywhere

if (ptr == nullptr) {
    cout << "Pointer is null" << endl;
}

// *ptr = 5;  // DANGEROUS! Would crash the program

Всегда инициализируйте указатели либо валидным адресом, либо nullptr. Неинициализированный указатель содержит «мусор» и часто приводит к трудноуловимым ошибкам.

1.7 Массивы и векторы (arrays vs. vectors)
1.7.1 Массивы в стиле C

Разобравшись с pointers, проще понять arrays (массивы): это низкоуровневый механизм, унаследованный от C. Объявление массива задаёт фиксированный набор элементов одного типа.

Синтаксис: T arrayName[size];

  • T — тип элементов
  • sizeconstant expression (константное выражение), известное на этапе компиляции
int Array[10];           // Array of 10 integers
const int x = 7;
void* Ptrs[x*2+5];      // Array of 19 void pointers
int Matrix[10][100];     // 2D array

Основные свойства:

  • Fixed size (фиксированный размер): после объявления не меняется
  • No bounds checking (нет проверки границ): выход за пределы — undefined behavior
  • Decay to pointers (преобразование имени массива в указатель): имя массива ведёт себя как константный указатель на первый элемент
1.7.2 Массивы и указатели

Здесь сходятся arrays и pointers: имя массива по сути — constant pointer (константный указатель) на первый элемент. Это центральная идея для C++.

int Array[10];
// Array is equivalent to: const int* Array

Доступ к элементам:

  • Array[0] эквивалентно *Array
  • Array[i] эквивалентно *(Array + i) (pointer arithmetic)

Зачем это важно: понимая array decay, вы объясняете себе типичное поведение массивов:

  • при передаче массива в функцию на самом деле передаётся указатель;
  • массив «не знает» собственный размер — его нужно вести отдельно;
  • контроль границ — ваша ответственность.

Тесная связь массивов и указателей мощна, но и источник многих багов — поэтому в C++ есть более удобная альтернатива: vectors.

1.7.3 Векторы C++

Vectors (std::vector) — часть C++ Standard Library; это более безопасная и гибкая замена сырому массиву. По сути это dynamically-sized arrays с богатым набором операций.

Отличия от массивов:

Свойство Массивы Векторы
Объявление int A[20]; vector<int> A;
Размер Фиксируется при компиляции Меняется в runtime
Проверка границ Нет Есть через .at()
Возможности В основном индексация Много методов
Производительность Максимальная Почти как у массивов
Безопасность Низкая Значительно выше
1.7.4 Операции с вектором

Объявление и инициализация:

vector<int> v1;                    // Empty vector
vector<int> v2 = { 1, 2, 3 };     // Initialized with values
vector<int> v3(10);                // 10 elements, default-initialized
vector<int> v4(10, 7);             // 10 elements, all with value 7

Добавление элементов:

vector<int> v;
v.push_back(10);  // Add 10 to the end
v.push_back(20);  // Add 20 to the end
v.push_back(30);  // Now v = {10, 20, 30}

Доступ к элементам:

vector<int> v = { 1, 2, 3 };
int x = v[0];     // Access first element (no bounds checking)
v[2] = 777;       // Modify third element
int y = v.at(1);  // Access with bounds checking (throws exception if out of range)

Размер:

cout << v.size() << endl;  // Returns number of elements

Полезные операции: v.clear(), v.empty(), v.front(), v.back(), v.erase(), v.insert() и др.

1.7.5 Range-based for

В C++11 появился удобный синтаксис обхода контейнеров (в том числе vectors):

Только чтение (элемент копируется):

vector<int> v = { 1, 2, 3, 4 };
for (int elem : v) {
    cout << elem << " ";  // elem is a copy of each element
}

Изменение элементов (нужна ссылка):

for (int& elem : v) {
    elem = elem * 10;  // Modifies actual vector elements
}

Вывод типа через auto:

for (auto& elem : v) {
    elem = elem * 10;  // Compiler deduces type automatically
}

Range-based for работает и с массивами в стиле C, и со initializer lists:

int arr[] = {1, 2, 3};
for (int n : arr) {
    cout << n << " ";
}

for (int n : {0, 1, 2, 3}) {  // Initializer list
    cout << n << " ";
}
1.8 Ссылки (references)
1.8.1 Что такое reference?

После pointers проще понять references (ссылки). Reference — это alias (псевдоним) уже существующего объекта: другое имя для той же переменной.

После инициализации ссылка всегда относится к одному и тому же объекту; операции над ссылкой — это операции над объектом, на который она ссылается.

Синтаксис: T& refName = object;

int x = 5;
int& r = x;  // r is now a reference to x

r = 7;       // Same as: x = 7
x = 777;
int v = r;   // v = 777 (reading x through r)

Ключевая идея: r — не копия x, а то же самое x под другим именем; одна и та же область памяти.

1.8.2 References и pointers

И то и другое даёт indirection, но различия важны:

Свойство Указатели Ссылки
Объявление int* p; int& r = x;
Природа Полноценные объекты Не объекты, только псевдонимы
Значение Хранят адрес Нет «собственного» значения
Инициализация Могут быть nullptr или мусором Обязательны при объявлении; null references нет
Переназначение Можно направить на другой объект Всегда тот же объект
Операторы Явные & и * Дополнительные операторы не нужны
Синтаксис *p = 5; r = 5;
1.8.3 Зачем нужны references?

Главная мотивация — efficiency (эффективность): передача крупного объекта by value дорога. Reference даёт функции доступ к оригиналу без копирования.

void f(Huge a) { ... }      // Expensive: copies entire Huge object
void f(Huge& a) { ... }     // Efficient: passes reference (just an address)

Наглядно: передать по значению — как сфотокопировать книгу целиком; передать по ссылке — как дать саму книгу.

1.8.4 Правила и ограничения

Так как ссылка не является отдельным объектом:

  • нет указателей на ссылку;
  • нет массивов ссылок;
  • нет ссылки на ссылку (в смысле «вложенности типа» как у указателей);
  • ссылку нужно инициализировать при объявлении.

При этом допустимо:

  • ссылка на указатель: int* p; int*& rp = p;
  • указатель на «ссылку как тип» в C++ не строится, но ссылка на указатель — да
1.9 Константные типы (const)
1.9.1 Квалификатор const

const задаёт constant types (константные типы): после инициализации объект нельзя менять.

const T описывает множество неизменяемых объектов типа T. Важно: T и const Tразные типы, хотя множество допустимых значений совпадает.

const int b = 777;     // b cannot be modified
b = 5;                 // ERROR: cannot modify const variable

Зачем const?

  1. Safety (безопасность): меньше случайных изменений
  2. Intent (намерение): явно показывает, что значение зафиксировано
  3. Optimization (оптимизация): компилятору проще упрощать код
1.9.2 Константы времени компиляции и выполнения

Compile-time constants инициализируются constant expressions (значение известно при компиляции):

const int b = 777;           // Constant expression
const int c = 2 * b + 5;    // Also constant expression

Компилятор может вычислить их заранее и подставлять литералы.

Run-time constants получают значение из выражений, известных только в runtime:

int input;
cin >> input;
const int x = input + 5;  // Value not known until runtime

Объект по-прежнему неизменяем, но компилятор не может «свернуть» его в константу на этапе компиляции.

На практике: compile-time-константы допускаются в большем числе контекстов (например, размер массива в стандартном C++), run-time-константы дают неизменяемость без знания значения на этапе компиляции.

1.9.3 Constant expressions

Constant expressions — выражения, вычислимые компилятором при компиляции. Они требуются, например, для:

  • размеров массива: const int size = 10; int arr[size];
  • параметров шаблонов
  • меток case в switch
1.9.4 const и указатели

С pointers и const возможны четыре базовые комбинации:

  1. T* ptr; — обычный указатель на изменяемый объект
  2. const T* ptr; — указатель на const-объект (через указатель менять нельзя)
  3. T* const ptr = &obj;const pointer (адрес в указателе не меняется)
  4. const T* const ptr = &obj; — константный указатель на константный объект
int x = 5;
const int y = 10;

int* p1 = &x;           // Can modify x through p1
const int* p2 = &y;     // Cannot modify through p2
int* const p3 = &x;     // p3 always points to x
const int* const p4 = &y;  // p4 always points to y, cannot modify

*p1 = 7;     // OK
*p2 = 7;     // ERROR
p3 = &y;     // ERROR

Как читать объявление: справа налево.

  • const int* ptr — «ptr — указатель на int, который const»
  • int* const ptr — «ptrconst-указатель на int»
1.9 Вывод типов с auto
1.9.1 Спецификатор auto

Ключевое слово auto (в современном C++ оно не storage-class specifier) велит компилятору вывести тип переменной из инициализатора — это type deduction / type inference.

auto x = 7;               // x has type int
auto y = 3.14;            // y has type double
auto z = "hello";         // z has type const char*
auto v = new vector<int>; // v has type vector<int>*

Зачем: имена типов в C++ часто громоздкие; auto смещает акцент на логику.

1.9.2 Правила вывода для auto

Для auto var = expression;:

Тип выражения Выведенный тип var
T* или const T* T* или const T* (указатели сохраняются)
T, const T, T&, const T& T (const и ссылка сбрасываются)

Для auto& var = expression;:

Тип выражения Выведенный тип var
T ошибка
const T const T&
T& T&
int x = 5;
const int y = 10;
int& rx = x;

auto a = x;      // a has type int (not int&)
auto b = y;      // b has type int (const dropped)
auto c = rx;     // c has type int (reference dropped)
auto& d = x;     // d has type int&
auto& e = y;     // e has type const int&

Важно: auto не значит «любой тип», а «вывести точный тип из инициализатора».

1.9.3 Плюсы auto

Короче запись — особенно для сложных типов:

// Instead of:
vector<double*>* v = new vector<double*>(77);

// Write:
auto v = new vector<double*>(77);

Поддерживаемость: если поменялся тип, возвращаемый функцией, фрагменты с auto часто остаются корректными.

Согласованность типов: меньше неявных «лишних» преобразований.

Осторожно: злоупотребление auto ухудшает читаемость; используйте, когда тип очевиден из контекста или слишком длинен.

1.9.4 Ограничения
  • нужен инициализатор: auto x; — ошибка;
  • в одном объявлении нельзя смешивать несовместимые выводы: auto a = 5, b = {1, 2}; — ошибка;
  • auto больше не класс памяти: auto int x; — ошибка.
1.10 Структурированная привязка (structured binding, C++17)
1.10.1 Базовый синтаксис

Structured binding (структурированная привязка, C++17) разбивает объект на части и вводит имена для этих частей одной декларацией.

Синтаксис:

auto [var1, var2, var3] = expression;
auto [var1, var2, var3] { expression };
auto [var1, var2, var3] ( expression );

В текущей области появляются var1, var2, var3, привязанные к подобъектам или элементам результата expression.

Наглядно: как распаковать чемодан — один составной объект разделяется на именованные элементы.

1.10.2 Structured binding и массивы

Для массива:

int a[2] = { 1, 2 };

auto [x, y] = a;        // x and y are copies of a[0] and a[1]
auto& [xr, yr] = a;     // xr and yr are references to a[0] and a[1]

По значению (auto [x, y] = a;):

  • создаётся временная копия массива;
  • x и y относятся к элементам копии;
  • изменения x/y не трогают исходный a.

По ссылке (auto& [x, y] = a;):

  • полной копии нет;
  • x и y — прямые ссылки на элементы a;
  • изменения видны в a.
1.10.3 Structured binding и структуры

Для struct привязка идёт по полям:

struct S {
    int x;
    const double y;
};

S s = {1, 3.14};
auto [a, b] = s;        // a is int, b is const double
const auto [c, d] = s;  // c is const int, d is const double
1.10.4 Tuple и pair

Работает с std::tuple и std::pair:

std::tuple<int, int&> f() { ... }

auto [x, y] = f();       // x is int, y is int&
const auto [z, w] = f(); // z is const int, w is int& (reference not affected by const)

2. Определения

  • Pointer (указатель): переменная, хранящая memory address другой переменной; объявляется с * (например, int* ptr).
  • Address-of operator (&) (оператор взятия адреса): возвращает адрес переменной (&x — адрес x).
  • Dereference operator (*) (оператор разыменования): доступ к значению по адресу, записанному в указателе (*ptr).
  • Null pointer (nullptr): специальное значение указателя «ни на что валидное не ссылается».
  • Pointer arithmetic (арифметика указателей): операции над указателями (например, ptr++ — к следующему элементу).
  • Namespace (пространство имён): декларативная область для идентификаторов, снижающая коллизии имён.
  • Qualified name (квалифицированное имя): имя с указанием пространства имён: namespace::identifier.
  • Scope resolution operator (::) (оператор разрешения области видимости): доступ к членам пространства имён или класса.
  • Using declaration (using-декларация): подключает член пространства имён в текущую область (например, using std::cout;).
  • Array (массив): непрерывный набор элементов одного типа фиксированного размера; размер известен на этапе компиляции.
  • Array decay (преобразование массива в указатель): автоматическое превращение имени массива в указатель на первый элемент.
  • Vector (вектор): динамический массив из C++ Standard Library; заголовок <vector>.
  • Range-based for loop: цикл C++11 for (element : container) по элементам контейнера или массива.
  • Reference (ссылка): псевдоним существующего объекта; после инициализации не меняет объект-референт; сама ссылка не является объектом в обычном смысле.
  • Constant type (константный тип): тип с квалификатором const; объекты нельзя менять после инициализации.
  • Compile-time constant (константа времени компиляции): значение задаётся constant expression на этапе компиляции.
  • Run-time constant (константа времени выполнения): значение известно в runtime, но дальше объект неизменяем.
  • Constant expression (константное выражение): выражение, вычислимое компилятором при компиляции.
  • Type deduction (вывод типа): способность компилятора определить тип переменной по инициализатору.
  • auto specifier: спецификатор типа «вывести тип автоматически».
  • Structured binding (структурированная привязка, C++17): одна декларация вводит несколько имён для частей объекта.
  • Preprocessor directive (директива препроцессора): команда с #, обрабатываемая до компиляции (#include и т.д.).
  • Stream insertion operator (<<) (оператор вставки в поток): перегрузка для вывода в потоки вроде std::cout.
  • Heap (куча): область памяти для динамически выделенных объектов; управление на программисте.
  • Stack (стек): область для локальных переменных и кадров вызовов; управляется по правилам области видимости.
  • Type (тип): классификация: допустимые значения, операции и отношения с другими типами.
  • lvalue: выражение, обозначающее место в памяти и допустимое в левой части присваивания.
  • Indirection (косвенный доступ): доступ к значению через pointer или reference, а не напрямую.

3. Примеры

3.1. Конвертер длительности (секунды → ч:м:с) (Лаба 1, Задание 1)

Напишите программу: на вход — длительность в секундах, на выход — в формате часы : минуты : секунды.

Пример:

Ввод Вывод
124660 34:37:40
Нажмите, чтобы увидеть решение

Ключевая идея: целочисленное деление для часов и минут; оператор % для остатков.

#include <iostream>
using namespace std;

int main()
{
    int totalSeconds;
    cin >> totalSeconds;
    
    // Calculate hours, minutes, and seconds
    int hours = totalSeconds / 3600;
    int minutes = (totalSeconds % 3600) / 60;
    int seconds = totalSeconds % 60;
    
    // Print in required format
    cout << hours << ":" << minutes << ":" << seconds << endl;
    
    return 0;
}

Разбор:

  1. Часы: totalSeconds / 3600 (целая часть — часы).
    • \(124660 \div 3600 = 34\) часа
  2. Остаток после часов: % 3600.
    • \(124660 \mod 3600 = 2260\) секунд
  3. Минуты: остаток делим на 60.
    • \(2260 \div 60 = 37\) минут
  4. Секунды: остаток от деления на 60.
    • \(2260 \mod 60 = 40\) секунд

Ответ: для ввода 124660 вывод 34:37:40

Замечание: ведущие нули (34:07:05) потребуют std::setw и std::setfill из <iomanip>.

3.2. Обмен значений через указатели (Лаба 1, Задание 2a)

Реализуйте свою функцию обмена двух целых, передавая аргументы by pointer (через pointers).

Нажмите, чтобы увидеть решение

Ключевая идея: в pointer хранится address; чтобы поменять исходные переменные, нужно dereference и работать со значениями по этим адресам.

#include <iostream>
using namespace std;

// Swap function using pointers
void swapByPointer(int* a, int* b)
{
    int temp = *a;    // Store value pointed to by a
    *a = *b;          // Set value at a to value at b
    *b = temp;        // Set value at b to saved temp value
}

int main()
{
    int x = 5, y = 10;
    
    cout << "Before swap: x = " << x << ", y = " << y << endl;
    
    swapByPointer(&x, &y);  // Pass addresses of x and y
    
    cout << "After swap: x = " << x << ", y = " << y << endl;
    
    return 0;
}

Разбор:

  1. Сигнатура: void swapByPointer(int* a, int* b) — параметры — pointers на int.
  2. Dereference: оператор * даёт значение по адресу; *a = *b копирует значение из «ячейки» b в «ячейку» a.
  3. Вызов: передаём адреса: swapByPointer(&x, &y).

Ответ: значения исходных переменных меняются местами.

3.3. Обмен значений через ссылки (Лаба 1, Задание 2b)

Та же задача, но передача by reference (references).

Нажмите, чтобы увидеть решение

Ключевая идея: referencealias; изменяя ссылку, вы меняете тот же объект в памяти.

#include <iostream>
using namespace std;

// Swap function using references
void swapByReference(int& a, int& b)
{
    int temp = a;     // a is a reference, directly accesses the original
    a = b;            // Assign value of b to a
    b = temp;         // Assign saved value to b
}

int main()
{
    int x = 5, y = 10;
    
    cout << "Before swap: x = " << x << ", y = " << y << endl;
    
    swapByReference(x, y);  // Pass variables directly (not addresses)
    
    cout << "After swap: x = " << x << ", y = " << y << endl;
    
    return 0;
}

Разбор:

  1. void swapByReference(int& a, int& b) — параметры — references на int.
  2. Разыменование не требуется: a и b — это сами x и y (не «ячейки по адресу», а прямые псевдонимы).
  3. Вызов: swapByReference(x, y).

Сравнение с указателями:

  • короче запись в теле функции;
  • нет null references;
  • намерение «работаем с оригиналом» читается проще.

Ответ: результат тот же, что и у варианта с pointers.

3.4. Удаление дубликатов (массив) (Лаба 1, Задание 3a)

Ввод: \(N\), затем \(N\) целых. Удалите все дубликаты, используя только arrays (массивы).

Пример:

Ввод Вывод
8
1 3 5 3 3 4 1 2
1 3 5 4 2
Нажмите, чтобы увидеть решение

Ключевая идея: вести массив уже встреченных уникальных значений и для каждого нового элемента проверять, не был ли он добавлен ранее.

#include <iostream>
using namespace std;

int main()
{
    int n;
    cin >> n;
    
    int arr[n];  // Note: Variable-length arrays are not standard C++
                 // but supported by some compilers
    
    // Read input
    for (int i = 0; i < n; i++) {
        cin >> arr[i];
    }
    
    // Array to store unique elements
    int unique[n];
    int uniqueCount = 0;
    
    // For each element in original array
    for (int i = 0; i < n; i++) {
        bool isDuplicate = false;
        
        // Check if element already exists in unique array
        for (int j = 0; j < uniqueCount; j++) {
            if (arr[i] == unique[j]) {
                isDuplicate = true;
                break;
            }
        }
        
        // If not a duplicate, add to unique array
        if (!isDuplicate) {
            unique[uniqueCount] = arr[i];
            uniqueCount++;
        }
    }
    
    // Print unique elements
    for (int i = 0; i < uniqueCount; i++) {
        cout << unique[i];
        if (i < uniqueCount - 1) cout << " ";
    }
    cout << endl;
    
    return 0;
}

Разбор:

  1. читаем все \(N\) значений в массив;
  2. второй массив — только уникальные;
  3. для каждого элемента исходного массива проверяем наличие во «втором»; если нет — добавляем и увеличиваем uniqueCount;
  4. печатаем уникальные подряд.

Время: \(O(n^2)\) из‑за вложенных циклов. Память: \(O(n)\).

Ответ: для примера из условия — 1 3 5 4 2.

Замечание: int arr[n] (VLA) не входит в стандарт C++, хотя некоторые компиляторы это принимают; в «каноническом» C++ — динамика или vectors (следующий пункт).

3.5. Удаление дубликатов (vector) (Лаба 1, Задание 3b)

Те же входные данные, но решение на std::vector.

Пример:

Ввод Вывод
8
1 3 5 3 3 4 1 2
1 3 5 4 2
Нажмите, чтобы увидеть решение

Ключевая идея: vectors дают динамический размер и удобные методы вроде push_back(); алгоритм тот же, синтаксис проще.

#include <iostream>
#include <vector>
using namespace std;

int main()
{
    int n;
    cin >> n;
    
    vector<int> arr(n);  // Vector of size n
    
    // Read input
    for (int i = 0; i < n; i++) {
        cin >> arr[i];
    }
    
    vector<int> unique;  // Vector to store unique elements
    
    // For each element in original vector
    for (int i = 0; i < n; i++) {
        bool isDuplicate = false;
        
        // Check if element already exists in unique vector
        for (int j = 0; j < unique.size(); j++) {
            if (arr[i] == unique[j]) {
                isDuplicate = true;
                break;
            }
        }
        
        // If not a duplicate, add to unique vector
        if (!isDuplicate) {
            unique.push_back(arr[i]);
        }
    }
    
    // Print unique elements
    for (int i = 0; i < unique.size(); i++) {
        cout << unique[i];
        if (i < unique.size() - 1) cout << " ";
    }
    cout << endl;
    
    return 0;
}

Разбор:

  1. vector<int> arr(n) — сразу \(n\) элементов (можно читать и в пустой vector через push_back).
  2. vector<int> unique растёт по мере добавления.
  3. push_back добавляет в конец.
  4. size() возвращает текущую длину (у сырого массива длины «в типе» нет).

Плюсы перед массивом: не вести размер вручную, проще границы (.at()), нагляднее код.

Вариант с range-based for:

// Checking for duplicates using range-based for
for (int elem : arr) {
    bool isDuplicate = false;
    for (int uElem : unique) {
        if (elem == uElem) {
            isDuplicate = true;
            break;
        }
    }
    if (!isDuplicate) {
        unique.push_back(elem);
    }
}

Ответ: вывод как в 3.4.

Быстрее: сортировка и сжатие подряд идущих дубликатов даёт порядок \(O(n \log n)\); std::set хранит уникальность автоматически.

3.6. Вывод типов для auto (Лекция 1, Пример 1)

Определите типы переменных в объявлениях:

  1. auto x = 7;
  2. auto a[] = { 1, 2, 3 };
  3. const auto *v = &x;
  4. static auto y = 0.0;
  5. auto int r;
  6. auto m;
  7. auto a=5, b={1,2};
Нажмите, чтобы увидеть решение

Ключевая идея: правила type deduction для auto.

Ответы:

(a) auto x = 7;x имеет тип int (литерал 7).

(b) auto a[] = { 1, 2, 3 }; — тип a: std::initializer_list<int> (фигурный список в такой форме).

(c) const auto *v = &x; (при int x из (a)) — v: const int* (pointer to const int).

(d) static auto y = 0.0;y: double; static на вывод типа не влияет.

(e) auto int r;ошибка: в современном C++ auto не комбинируется с явным int.

(f) auto m;ошибка: у auto должен быть инициализатор.

(g) auto a=5, b={1,2};ошибка: в одной декларации несовместимые выведенные типы (int и initializer_list<int>).

Итог: корректны (a)–(d); (e)–(g) не компилируются.

3.7. Примеры со ссылками (Лекция 1, Пример 2)

Разберите код со references:

void f ( double& a )
{ a += 3.14; }

double d = 7.0;
f(d);

Каково значение d после вызова f(d)?

Также разберите:

int v[20];
int& f ( int i ) { return v[i]; }

f(3) = 7;

Что делает этот фрагмент?

Нажмите, чтобы увидеть решение

Ключевая идея: по reference функция может менять аргумент; возвращаемая ссылка может быть lvalue.

Часть 1: изменение аргумента

void f ( double& a )
{ a += 3.14; }

double d = 7.0;
f(d);  // After this call, d = 10.14

Разбор: areference на d; a += 3.14 меняет сам d. Ответ: d == 10.14.

Сравнение с передачей по значению:

void f ( double a )  // No reference
{ a += 3.14; }

double d = 7.0;
f(d);  // d still equals 7.0 (unchanged)

Часть 2: ссылка как lvalue

int v[20];
int& f ( int i ) { return v[i]; }

f(3) = 7;  // This assigns 7 to v[3]

Разбор: f возвращает reference на v[i]; выражение f(3) = 7 эквивалентно v[3] = 7. Ответ: v[3] == 7.

Так делают, например, перегрузки operator[], чтобы можно было писать matrix[i][j] = 5;.

3.8. Указатели и const (Лекция 1, Пример 3)

По объявлениям ниже — какие операции допустимы?

int x = 5;
const int y = 10;

int* p1 = &x;
const int* p2 = &y;
int* const p3 = &x;
const int* const p4 = &y;

*p1 = 7;     // Operation A
*p2 = 7;     // Operation B
p3 = &y;     // Operation C
Нажмите, чтобы увидеть решение

Ключевая идея: четыре сочетания pointer и const.

Типы:

  1. int* p1 — обычный указатель на изменяемый int.
  2. const int* p2pointer to const (через указатель не пишем; сам указатель переназначать можно).
  3. int* const p3const pointer (адрес фиксирован; объект по адресу менять можно).
  4. const int* const p4 — константный указатель на константу.

Чтение справа налево: int* const p, const int* p.

Операции:

  • A *p1 = 7корректно; x станет 7.
  • B *p2 = 7ошибка (через const int* писать нельзя).
  • C p3 = &yошибка (const pointer не переназначаем) плюс несовместимость int* и const int*.

Допустимо, например:

p2 = &x;       // VALID: can reassign p2
*p3 = 100;     // VALID: can modify through p3
int z = *p2;   // VALID: can read through p2
int w = *p4;   // VALID: can read through p4
Тип Менять объект? Менять указатель?
int* да да
const int* нет да
int* const да нет
const int* const нет нет

Ответ: из трёх показанных в условии допустима только A.

3.9. Поиск в массиве (версия 1) (Туториал 1, Пример 1)

Найти значение в массиве фиксированного размера.

int find1 ( int array[20], int x )
{
    for ( int i = 0; i < 20; i++ )
    {
        if ( array[i] == x ) return i; // success
    }
    return -1; // fail
}
Нажмите, чтобы увидеть решение

Ключевая идея: линейный проход; индекс при успехе, -1 при неудаче.

Замечания: размер «зашит» в 20 — негибко; константа дублируется (DRY). Сложность \(O(n)\) при \(n=20\).

Ответ: работает, но лучше обобщить (следующий пример).

3.10. Поиск в массиве (версия 2) (Туториал 1, Пример 2)

То же через pointers и произвольный размер \(n\).

int* find2 ( int* array, int n, int x )
{
    const int* p = array;
    for ( int i = 0; i < n; i++ )
    {
        if ( *p == x ) return p; // success
        p++;
    }
    return nullptr; // fail
}

Использование:

int A[20];
// ... fill array ...
int* res = find2(A, 20, 5);
Нажмите, чтобы увидеть решение

Ключевая идея: массив передаётся как указатель на первый элемент; возвращаем указатель на найденное или nullptr.

Плюсы к версии 1: параметр \(n\); pointer arithmetic (p++); наглядная семантика «не найдено».

Ответ: заметно универсальнее версии 1.

3.11. Изменение элементов vector (Туториал 1, Пример 3)

Чему равны элементы v6 после цикла?

vector<int> v6 = { 1, 2, 3, 4 };
for ( int elem : v6 )
    elem = elem * 10;
Нажмите, чтобы увидеть решение

Ключевая идея: в range-based for важно, копия это или reference.

Ответ: { 1, 2, 3, 4 }не изменились: elem — копия.

Правильно для изменения:

vector<int> v6 = { 1, 2, 3, 4 };
for ( int& elem : v6 )      // Note the & (reference)
    elem = elem * 10;
// Now v6 = { 10, 20, 30, 40 }

С auto:

for ( auto& elem : v6 )     // Compiler deduces type
    elem = elem * 10;

Правило: только чтение — по значению; правка — Type& или auto&; крупные объекты без копии — const Type&.

3.12. Structured binding и массив (Туториал 1, Пример 4)

Что означают привязки и как связаны переменные?

int a[2] = { 1, 2 };
auto [x, y] = a;
auto& [xr, yr] = a;
Нажмите, чтобы увидеть решение

Ключевая идея: копия массива vs references в structured binding.

auto [x, y] = a — временная копия; x, y — элементы копии.

auto& [xr, yr] = axr, yr ссылаются на a[0], a[1].

x = 100;    // Only changes x, not a[0]
xr = 200;   // Changes both xr AND a[0]

cout << a[0];  // Prints: 200
cout << x;     // Prints: 100

Ответ: x,y — независимые копии; xr,yr — псевдонимы элементов a.

3.13. Structured binding и struct (Туториал 1, Пример 5)

Каковы типы переменных после привязки?

struct S {
    int x;
    const double y;
};

S f() { return S{5, 3.14}; }

const auto [x, y] = f();
Нажмите, чтобы увидеть решение

Ключевая идея: const auto в structured binding добавляет верхнеуровневый const к привязкам; const полей сохраняется.

У S: int x, const double y. Для const auto [x, y] = f();:

  • xconst int (const от const auto);
  • yconst double (const поля сохраняется).

Без const auto:

auto [x, y] = f();
// x would be: int
// y would be: const double (const from member preserved)

Правило: const члена в привязке не «снимается»; верхний const задаётся спецификатором привязки.