Загружаем лабораторные…
// C++ · Лекция 10

Константность и права доступа

Квалификатор const, четыре формы указателей, константные ссылки, время жизни временных объектов и выбор параметров функций.

Что означает const

const — квалификатор типа, ограничивающий допустимые операции модификации через соответствующее выражение. const int и int — разные cv-квалифицированные формы типа.

Идея лекции: константность — ограничение возможностей доступа, а не обещание, что числовое значение нигде и никогда не изменится.

const int a = 5; можно читать и сравнивать, но нельзя присвоить ему новое значение и даже повторно записать 5.

Квалификатор типа

const дополняет тип. В записи int const x = 5; смысл тот же, что у const int x = 5;.

const_basics.cpp
#include <iostream>
int main() {
    int x = 5;
    const int y = x;        // значение скопировано
    x = 9;                  // OK
    // y = 9;               // CE
    // ++y;                 // CE
    int z = y;              // OK: читаем y
    std::cout << x << ' ' << y << ' ' << z; // 9 5 5
}
const запрещает изменение объекта, а не чтение его значения.
Визуализация / разрешённые и запрещённые операции

Сначала определите, какой именно объект создаётся. const int snapshot = original создаёт отдельное число и запрещает менять его после инициализации. Изменение original не обновит snapshot автоматически (это сохранённая копия, а не наблюдение за исходной переменной). В визуализации сравните чтение и операции записи для обычного и константного int.

✓ допустимо: чтение
Квалификатор const запрещает операции изменения даже при присваивании того же самого значения.
Проверка — логическая модель правил C++ для обычного int.
Инициализация ≠ присваивание

const int x = 5; создаёт объект с исходным значением. x = 5; — последующее присваивание и ошибка компиляции, хотя значение совпадает.

Константные ссылки

Константная ссылка

const T& — ссылка, через которую нельзя менять объект. Она не может быть переназначена, как и обычная T&, но позволяет удобно читать уже существующий объект без копирования.

const int& r = x; создаёт ещё одно имя x с доступом только на чтение. Изменение исходного x отражается при чтении r.

Const lvalue reference

const T& — ссылка на cv-квалифицированный объект. Неконстантная T& не может связываться с const T.

const_reference.cpp
#include <iostream>
int main() {
    int x = 5;
    int& writable = x;
    const int& readable = x;
    writable = 6;         // OK
    // readable = 7;      // CE
    std::cout << readable; // 6
    // int& error = readable; // CE
}
Два имени того же объекта с разными возможностями модификации.
Визуализация / одно значение, разные права доступа

Константная ссылка даёт имя существующему объекту, через которое нельзя выполнять изменение. Она не создаёт копию и не обещает, что значение никто не изменит другим способом. Сравните ссылку на изменяемый int со ссылкой на действительно константный объект: ограничение доступа одинаковое, а свойства исходных объектов различаются.

x : int (R/W)
↙ ↘
r : const int& (R)
Один объект: x / r
5
x: чтение и запись; r: только чтение
r и x обозначают один объект
Разные имена предоставляют разный набор операций к одной ячейке.
Иллюстрация не предполагает, что const-ссылка хранится в отдельной ячейке памяти.

Создание константного объекта в динамической памяти

new const int(5) возвращает const int*. Поскольку объект константный, его нужно инициализировать сразу. Обычный int* не может принять такой указатель неявно.

const_new.cpp
int main() {
    const int* one = new const int(5);
    const int* many = new const int[3]{1, 2, 3};
    // const int* bad = new const int; // CE: объект не инициализирован
    // int* bad2 = new const int(5);   // CE: теряется const
    delete one;
    delete[] many;
}
И константный объект, и константный массив нужно создать с допустимой инициализацией.
const T& — не «неизменяемая ссылка»

Ссылка и так не перенаправляется. В const T& ограничивается доступ к самому T. Запись T& const некорректна.

Передача параметров

Три стратегии

В функцию можно передать объект по значению (T), по изменяемой ссылке (T&) или по константной ссылке (const T&). Выбор определяет копирование и право изменять исходный объект.

Для крупного объекта вроде строки, когда нужно лишь читать, типичный выбор — const std::string&. Если нужно изменить аргумент вызывающего кода — std::string&. Для простых небольших значений (int, double) обычно лучше копирование.

Передача по значению

Образуется самостоятельный объект-параметр; его изменения не затрагивают аргумент.

Передача по ссылке

Параметр именует исходный объект без копирования всего объекта.

parameter_kinds.cpp
#include <iostream>
#include <string>
void consume(int x) { ++x; }                    // копия
void modify(std::string& s) { s += "!"; }         // меняем оригинал
void inspect(const std::string& s) {
    std::cout << s.size();                       // только чтение
}
int main() {
    int x = 3;
    std::string s = "abc";
    consume(x);                // x всё ещё 3
    modify(s);                 // s стало "abc!"
    inspect(s);
}
Выбор формы параметра — это договор о копировании и изменении данных.
Визуализация / выбираем сигнатуру функции

Выбор параметра начинается с намерения функции: ей нужно прочитать данные, изменить оригинал или получить свою копию? Маленькое число удобно передать по значению. Строку для чтения часто передают по const-ссылке. Если функции нужна собственная изменяемая строка, параметр по значению прямо выражает это намерение.

Tкопирование / владение
T&изменение оригинала
const T&чтение без копии
Рекомендация: void f(const T& value)
Это стартовое правило из лекции, а не абсолютный закон. Перемещение, оптимизации и владение данными могут изменить решение.
Точное T зависит от выбранного типа; диаграмма отражает выбор интерфейса.
Визуализация / что именно меняет функция

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

main::x
5
f::x
5
отдельный объект
Вызов функции с x = 5
У const T& исходный объект может меняться извне, но функция не может менять его через параметр.
ФормаКопия объектаМеняет оригиналПодходит для
void f(T x)
Да (или перемещение / устранение копии)
Нет
Небольшие типы; владение копией
void f(T& x)
Нет
Может
Изменение исходного объекта
void f(const T& x)
Нет
Нет через x
Чтение большого объекта без копии
Про «const T по значению»

В лекции это не рекомендуется как стиль для параметров. На практике const T внутри определения функции может защищать локальную копию, но в сигнатуре не меняет механизм передачи и не образует отдельную перегрузку.

Разобранный пример и самостоятельная проверка

Перед написанием функции задайте три вопроса. Нужно ли изменить объект вызывающего кода? Нужно ли только прочитать большой объект? Нужна ли функции собственная копия? Для небольшого int или double обычно выбирают значение. Для изменения исходной строки подойдёт string&. Для чтения уже существующей строки без её копирования удобно const string&. Слово const здесь ограничивает операции через данный параметр, а не делает исходную неконстантную строку навсегда неподвижной. Другой разрешённый путь может её изменить. Если функция должна получить собственный объект, передача по значению остаётся осмысленным выбором даже для строки. Это стартовые рекомендации, а не утверждение, что любая ссылка быстрее любой копии. Сначала выберите нужное поведение и владение, затем проверяйте стоимость. Именованная константа задаётся при создании; последующее присваивание ей недопустимо. Указатели с несколькими уровнями const для этого первого выбора не нужны.

code-guided-parameters.cpp
#include <cstddef>
#include <iostream>
#include <string>

std::size_t length(const std::string& text) {
    return text.size();
}

void addMark(std::string& text) {
    text += '!';
}

int main() {
    std::string text = "hello";
    const std::size_t oldLength = length(text);
    addMark(text);
    std::cout << oldLength << ' ' << text << '\n';
}
Чтение через const& и изменение через & — разные контракты.
Результат5 hello!

Константность указателей

Главное различие

const int* p — указатель, через который можно лишь читать int. int* const p — неизменяемый указатель на изменяемый int. Положение const относительно * меняет смысл.

Представь две самостоятельные сущности: объект-указатель, хранящий адрес, и объект по этому адресу. Для каждой может быть своё право на изменение. Наведись на Как читать справа налево?Найди имя p и двигайся наружу по декларации.const int* pуказатель на константный intint* const pконстантный указатель на intconst int* const pконстантный указатель на константный int для краткой памятки.

Low-level const

Константность объекта под указателем: const int*.

Top-level const

Константность самой переменной-указателя: int* const.

const_pointers.cpp
int main() {
    int x = 1, y = 2;
    int* p = &x;                 // запись ✓  адрес ✓
    const int* ro = &x;         // запись ✕  адрес ✓
    int* const fixed = &x;      // запись ✓  адрес ✕
    const int* const both = &x; // запись ✕  адрес ✕
    *p = 3;
    ro = &y;
    *fixed = 4;
    // *ro = 5; // CE
    // fixed = &y; // CE
    (void)both;
}
Положение const относительно * определяет, что именно константно.
Визуализация / права чтения, записи и переназначения

В объявлении указателя задаются два независимых ограничения: можно ли записать другой адрес в сам указатель и можно ли изменить объект через разыменование. Поэтому const int* и int* const решают разные задачи. Прочитайте тип, выберите соответствующую форму в визуализации и отдельно попробуйте изменить адрес и значение.

запись ✓ · адрес ✓
запись ✕ · адрес ✓
запись ✓ · адрес ✕
запись ✕ · адрес ✕
x @ 0x1000
5
← p
y @ 0x1004
9
не выбран
Выбрано: int* p
Два независимых измерения: можно ли менять адрес в p и можно ли менять объект через *p. Значения адресов условные.
Разрешённость операции определяется типом, а не тем, куда конкретно сейчас указывает p.
ТипИзменить *pПеренаправить p
int* p
Да
Да
const int* p
Нет
Да
int* const p
Да
Нет
const int* const p
Нет
Нет

Преобразование const

Неявное преобразование

Указатель T* может неявно преобразовываться в const T*. Обратно, const T* → T*, неявное преобразование запрещено: нельзя самовольно получить право записи.

Важно различать константность объекта и ограничение доступа через указатель. Если const int* view = &x;, другой путь к неконстантному x может изменить его.

Qualification conversion

Преобразование, меняющее cv-квалификацию типа по правилам языка; простое добавление const на первом уровне безопасно.

qualification.cpp
#include <iostream>
int main() {
    int x = 4;
    int* rw = &x;
    const int* ro = rw;     // OK: права урезаны
    // int* again = ro;    // CE: неявно снять const нельзя
    x = 9;                  // OK: сам x не const
    std::cout << *ro;       // 9
}
Указатель на константу предоставляет ограниченный интерфейс доступа.
Визуализация / добавляем или удаляем право записи

Преобразование int* в const int* создаёт ещё один способ обратиться к тому же объекту, но с меньшими правами. Оно не делает исходный int константным. При копировании самого значения ситуация другая: const int можно прочитать и использовать для создания нового изменяемого int.

int*
→
const int*
✓ допустимое неявное преобразование
Добавили ограничение на запись через новый указатель.
Модель сравнивает конкретные пары типов; произвольные составные типы имеют дополнительные правила.
const-представление не замораживает исходный int

int x = 4; const int* p = &x; x = 9; допустимо, а чтение *p даст 9. Запрет касается операции записи именно через p.

Продление времени жизни

Lifetime extension

При прямом связывании const T& с временным объектом его время жизни в ряде случаев продлевается до времени жизни ссылки. Но это правило имеет важные исключения.

const std::string& s = "hello"; создаёт временный объект std::string и привязывает к нему ссылку. При такой локальной инициализации объект доживёт до конца области видимости ссылки.

Temporary object

Временный объект — объект с ограниченным временем жизни, обычно создаваемый при вычислении выражения.

Dangling reference

Ссылка, которая больше не обозначает живой объект; использование объекта через неё — UB.

temporary_lifetime.cpp
#include <string>
void inspect(const std::string& s) { (void)s; }
const std::string& bad() {
    return std::string("temporary"); // висячая ссылка!
}
int main() {
    const std::string& s = "hello"; // жизнь временной строки продлена
    inspect(std::string("a"));      // существует на время вызова
    // auto& dangling = bad();      // не используем
    (void)s;
}
Связывание с временным объектом продлевает жизнь не во всех контекстах.
Визуализация / когда уничтожается временный объект

Время жизни зависит от того, где и как связывается ссылка. Локальная const-ссылка, непосредственно связанная с временным результатом, может продлить его жизнь до конца своего блока. Возврат ссылки из другой функции такого продления не создаёт. Поэтому для самостоятельного результата функции обычно проще вернуть значение.

const std::string& s = "abc";
1. создана временная строка
2. ссылка s привязана
3. используем s
4. конец блока → объект уничтожен
Временная строка живёт до конца области видимости s.
Правила lifetime extension имеют дополнительные исключения; приведены характерные случаи из лекции.
Возврат const-ссылки не продлевает время жизни

const std::string& bad() { return std::string("x"); } возвращает висячую ссылку в правилах C++20. После C++26 подобное связывание непосредственно в return является ошибкой компиляции.

Практика: файловые потоки для лабораторной

Этот раздел нужен перед лабораторной 7 «Файлы». Он использует уже знакомые строки, циклы, указатели и время жизни объектов. Файл позволяет сохранить данные после завершения программы (проще говоря, значения остаются на диске, а не только в переменных).

#include <fstream> подключает std::ifstream для чтения и std::ofstream для записи. Они похожи на std::cin и std::cout, но связаны с именованным файлом. Относительный путь отсчитывается от рабочей папки запущенной программы, которая может отличаться от папки исходника.

Текстовый и двоичный файл

В текстовом файле число 123 представлено символами, а в двоичном можно сохранить байты самого объекта. Содержимое двоичного файла не обязательно читается в текстовом редакторе.

Первое занятие: текстовые строки и слова

Файловый поток похож на знакомый cin, но читает из указанного файла. Перед первым чтением проверим, удалось ли его открыть. Путь notes.txt отсчитывается от рабочей папки процесса, поэтому место сохранения исходника не всегда подходит. Getline получает очередную строку; пустая строка тоже считается строкой. Внутри неё istringstream последовательно выделяет слова, разделённые пробельными символами. Число строк увеличивается после успешного getline, число слов — после успешного извлечения слова. Проверять eof до чтения не нужно: он не предсказывает успех следующей операции. После завершения цикла отдельно проверяем, был ли обычный конец файла, а не ошибка ввода. Пустой файл даёт два нуля. Локальный ifstream автоматически закрывает ресурс при выходе из main. Для записи понадобится ещё одна проверка: перед сообщением об успехе убедиться, что поток успешно завершил сохранение. Бинарные числа и изменение исходного файла изучаются отдельным шагом.

code-guided-file.cpp
#include <cstddef>
#include <fstream>
#include <iostream>
#include <sstream>
#include <string>

int main() {
    std::ifstream input("notes.txt");
    if (!input) {
        std::cerr << "Cannot open notes.txt\n";
        return 1;
    }
    std::size_t lines = 0;
    std::size_t words = 0;
    std::string line;
    while (std::getline(input, line)) {
        ++lines;
        std::istringstream row(line);
        std::string word;
        while (row >> word) {
            ++words;
        }
    }
    if (input.bad() || !input.eof()) {
        std::cerr << "Read error\n";
        return 1;
    }
    std::cout << lines << ' ' << words << '\n';
}
notes.txt из двух строк Hello world и Welcome to C++: 2 строки, 5 слов.
Результат2 5

Открытие, чтение и конец файла

Сразу после открытия проверьте поток: if (!input) означает, что читать нельзя. Проверка eof() до попытки чтения не предсказывает её успех. В обычном цикле while (input >> value) тело выполняется только после успешного извлечения; последнюю переменную нельзя использовать ещё раз после неудачного чтения.

Для строгой проверки файла нужно отличить чистый конец данных от испорченной записи. Пример ниже сначала пропускает пробельные символы через std::ws, проверяет конец, а затем читает число. Поэтому пустой файл даёт нулевую сумму, а текст вместо числа, переполнение типа и обрыв недопустимой записи дают отказ. bad() указывает на серьёзную ошибку ввода-вывода.

Создайте рядом с программой numbers.txt с содержимым 4 -2 7. Результат будет count=3 sum=9. Вариант без завершающего перевода строки тоже должен работать. Константы имени файла и допустимых границ объявлены вне main.

read_text_file.cpp
#include <fstream>
#include <iostream>

constexpr char INPUT_FILE[] = "numbers.txt";
constexpr int MAX_RECORDS = 1000000;
constexpr int MAX_ABSOLUTE_VALUE = 1000000;

int main() {
    std::ifstream input(INPUT_FILE);
    if (!input) {
        std::cerr << "Cannot open input\n";
        return 1;
    }
    long long sum = 0;
    int count = 0;
    while (true) {
        input >> std::ws;
        if (input.bad()) {
            std::cerr << "Read error\n";
            return 1;
        }
        if (input.eof()) break;
        int value = 0;
        if (!(input >> value) || value < -MAX_ABSOLUTE_VALUE
                || value > MAX_ABSOLUTE_VALUE || count == MAX_RECORDS) {
            std::cerr << "Invalid record\n";
            return 1;
        }
        sum += value;
        ++count;
    }
    std::cout << "count=" << count << " sum=" << sum << '\n';
}
numbers.txt: 4 -2 7. Результат: count=3 sum=9. Пустой файл допустим.

Запись и владение ресурсом

std::ofstream output("result.txt"); открывает файл для записи и по умолчанию заменяет прежнее содержимое. Для добавления используют std::ios::app. Записывайте через output << value << '\n'; и проверяйте состояние после операций. Для проверяемого завершения вызовите output.close() и ещё раз проверьте поток: ошибка может обнаружиться при сбросе буфера.

Файловый поток закрывает свой ресурс при разрушении объекта — это пример RAII из лекции 9. Ранний return тоже разрушает локальные потоки. Но автоматическое закрытие не заменяет проверку результата записи: перед сообщением об успехе нужно убедиться, что данные сохранены.

«Типизированный» файл целых чисел

В условиях лабораторной этим словом обозначен двоичный файл последовательных значений int. Режим std::ios::binary отключает текстовые преобразования. write и read получают адрес байтов и их количество. В примере reinterpret_cast даёт доступ к представлению int через указатель на char; это не преобразование числа в текст.

Такой учебный формат читается той же программой на той же платформе: размер int, порядок байтов и представление зависят от реализации. Не применяйте этот способ к std::string или std::vector: байты их служебных объектов не являются сохранённым содержимым контейнера.

Следующий пример создаёт собственный демонстрационный файл, записывает три числа и читает их без массива-буфера. После неуспешного чтения gcount() показывает число реально извлечённых байтов: ненулевой неполный хвост означает повреждённую запись. Чистый конец наступает, когда извлечено ноль байтов.

binary_records.cpp
#include <fstream>
#include <iostream>

constexpr char DATA_FILE[] = "binary-demo.dat";

int main() {
    // This example deliberately creates its own demonstration file.
    std::ofstream output(DATA_FILE, std::ios::binary | std::ios::trunc);
    if (!output) return 1;
    const int values[]{4, -2, 7};
    for (int value : values) {
        output.write(reinterpret_cast<const char*>(&value), sizeof(value));
        if (!output) return 1;
    }
    output.close();
    if (!output) return 1;

    std::ifstream input(DATA_FILE, std::ios::binary);
    if (!input) return 1;
    int value = 0;
    int count = 0;
    long long sum = 0;
    while (input.read(reinterpret_cast<char*>(&value), sizeof(value))) {
        ++count;
        sum += value;
    }
    if (input.bad() || !input.eof() || input.gcount() != 0) {
        std::cerr << "Incomplete record or read error\n";
        return 1;
    }
    std::cout << "count=" << count << " sum=" << sum << '\n';
}
Пример создаёт binary-demo.dat. Результат: count=3 sum=9.

Матрица неизвестного размера без загрузки в массив

Читайте файл построчно через std::getline. std::istringstream из <sstream> позволяет читать числа из одной строки тем же оператором >>. Это строковый поток, а не массив всей матрицы. Число значений первой строки задаёт ожидаемое число столбцов; остальные строки должны иметь столько же значений.

Создайте matrix.txt из двух строк: 1 2 и 3 4. Пример определит размер 2 × 2 и сумму 10 без хранения таблицы. Пустой файл, пустая строка, нечисловой элемент и строки разной длины отклоняются. Для задания со столбцами сделайте повторный проход, выбирая нужную позицию в каждой строке.

matrix_file.cpp
#include <fstream>
#include <iostream>
#include <sstream>
#include <string>

constexpr char MATRIX_FILE[] = "matrix.txt";
constexpr int MAX_SIDE = 100;
constexpr int MAX_ABSOLUTE_VALUE = 1000000;

int main() {
    std::ifstream input(MATRIX_FILE);
    if (!input) return 1;
    std::string line;
    int rows = 0;
    int columns = -1;
    long long sum = 0;
    while (std::getline(input, line)) {
        std::istringstream row(line);
        int currentColumns = 0;
        while (true) {
            row >> std::ws;
            if (row.eof()) break;
            int value = 0;
            if (!(row >> value) || currentColumns == MAX_SIDE
                    || value < -MAX_ABSOLUTE_VALUE
                    || value > MAX_ABSOLUTE_VALUE) return 1;
            sum += value;
            ++currentColumns;
        }
        if (currentColumns == 0 || rows == MAX_SIDE) return 1;
        if (columns == -1) columns = currentColumns;
        if (columns != currentColumns) return 1;
        ++rows;
    }
    if (input.bad() || !input.eof() || rows == 0) return 1;
    std::cout << "rows=" << rows << " columns=" << columns
              << " sum=" << sum << '\n';
}
matrix.txt: строки 1 2 и 3 4. Результат: rows=2 columns=2 sum=10.

Повторные проходы и рабочие файлы

Для нового прохода проще создать новый std::ifstream с тем же именем. Если используете прежний поток, сначала вызовите clear(), чтобы снять признаки конца и ошибки, затем seekg(0) и проверьте результат. tellg() возвращает позицию потока, а не порядковый номер числа. Номер записи считайте отдельной переменной с нуля.

Для удаления участка сначала найдите его границы, затем новым проходом перепишите сохраняемые записи в другой файл. Не перезаписывайте исходник до проверки результата. Для учебного отчёта достаточно оставить оба файла и показать их содержимое. Сортировку файла без массива можно выполнить несколькими проходами: на каждом искать следующий минимум и записывать нужное число его повторений.

Перед защитой проверьте пустой файл, одну запись, повторные экстремумы, отсутствие нужного элемента, недоступный путь, испорченный текст и неполную двоичную запись. Состояние операций чтения и gcount() описаны в разделе проекта стандарта C++ о неформатированном вводе.

Перегрузка и const

Разрешение перегрузок

f(T&) и f(const T&) — разные перегрузки. Для изменяемого объекта обычно предпочтительна T&, для константного — const T&.

Константность аргумента влияет на допустимость и предпочтение перегрузок, принимающих ссылки. Но f(T) и f(const T) по верхнеуровневому const параметра по значению перегружать нельзя.

Overload resolution

Правила выбора наиболее подходящей функции среди одноимённых кандидатов по аргументам и преобразованиям.

const_overloading.cpp
#include <iostream>
void f(int&) { std::cout << "mutable "; }
void f(const int&) { std::cout << "readonly "; }
int main() {
    int x = 5;
    const int cx = 7;
    f(x);            // mutable
    f(cx);           // readonly
    f(42);           // readonly
    // void g(int); void g(const int); // одна сигнатура
}
Константность ссылки влияет на выбор перегрузки.
Визуализация / выбор f(T&) или f(const T&)

Сначала проверьте, может ли аргумент вообще связаться с параметром каждой функции. Затем сравните подходящие варианты. Для пары int& и const int& изменяемая переменная лучше подходит к int&, а константа и временное значение — к const int&. Это правило относится к конкретной паре перегрузок в примере.

f(T&)изменяемая ссылка
f(const T&)константная ссылка
Выбрана перегрузка: f(T&)
Приоритет у варианта без добавления const.
Симулируется только указанная пара перегрузок; при других кандидатах разрешение может отличаться.
Ссылка на временный объект

Из пары f(T&) и f(const T&) временный объект обычного типа можно передать в const T&, но не в неконстантную lvalue-ссылку.

Const на двух уровнях

Не все уровни одинаковы

int** нельзя неявно превратить в const int**. На первом уровне добавление const допустимо, а такое вложенное преобразование могло бы открыть путь к изменению настоящего const int.

Представь: int** даёт доступ к переменной типа int*. Если бы его разрешили выдавать за const int**, можно было бы заменить внутренний указатель на адрес действительно константного объекта.

Потом исходный int* оказался бы обращённым к const int, и запись через него нарушила бы const-корректность.

Многоуровневая квалификация

Правила добавления const на вложенных уровнях указателей; для допустимого преобразования могут потребоваться дополнительные квалификаторы на промежуточных уровнях.

deep_const.cpp
int main() {
    int x = 4;
    int* p = &x;
    int** pp = &p;
    // const int** bad = pp; // CE: опасная квалификация
    const int* const* safe = pp; // OK
    (void)safe;
}
Для вложенной квалификации const учитываются промежуточные уровни.
Визуализация / почему запрещено int** → const int**

Для указателя на указатель ограничения нужны на каждом уровне. Ошибка int** → const int** опасна тем, что через промежуточный указатель можно было бы подменить адрес и затем изменить действительно константный объект. В безопасной форме const int* const* запрещена и такая подмена промежуточного указателя.

int**⇢const int**✕
0. Есть настоящий const int k = 4
1. Вообразим int** → const int**
2. *view = &k
3. *p = 5
Обычный int* изначально не указывает на k.
Компилятор запрещает такое преобразование на шаге 1 и не допускает опасную цепочку действий.
Гипотетический сценарий: запрещённое преобразование НЕ выполняется настоящим C++.
Не путай с const int* const*

Некоторые многоуровневые преобразования корректны: например, int** в const int* const* допустимо. Учитываются квалификаторы на каждом уровне, а не только крайний const.

Дополнительно

Тонкости и полезные дополнения · необязательно для основной лекции
Что полезно знать

Top-level const для параметров по значению не участвует в различении перегрузок. RAII и передача по значению с перемещением расширяют простые рекомендации лекции. const_cast существует, но попытка изменять действительно const-объект через снятую константность приводит к UB.

Современное правило передачи

Для дешёвых в копировании типов выбирайте передачу по значению. Крупные только для чтения часто передают через const T&. Но если функция должна владеть собственной копией, передача по значению может быть предпочтительнее.

Исключения lifetime extension

Срок жизни временного объекта не продлевается до срока жизни ссылки при return и при связывании с параметром функции — там объект существует до конца полного выражения вызова. Инициализация членов-ссылок имеет собственные тонкости.

additional_const.cpp
#include <string>
#include <utility>
void small(int value) { (void)value; }
void large(const std::string& value) { (void)value; }
void owner(std::string value) { value += "!"; }
int main() {
    int x = 4;
    const int* read = &x;
    int* again = const_cast<int*>(read); // допустимо: исходный x не const
    *again = 9;
    const int real_const = 7;
    // *const_cast<int*>(&real_const) = 1; // UB при попытке изменить
    large("hello");
    owner(std::string("hello"));
    small(x);
}
Продвинутые нюансы — const_cast, владение параметром и семантика перемещения.
Const ≠ thread-safe

const сам по себе не гарантирует потокобезопасность и не означает размещение объекта в памяти «только для чтения». Это ограничение операций языка.

Справочник терминов

Основные определения встречаются внутри глав. Здесь они собраны в одном алфавитном поиске.

const

Константность

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

Примеры кода

Строки и слова: обычный, пустой и отсутствующий файл

file-cases.cpp
#include <fstream>
#include <iostream>
#include <sstream>
#include <string>
int main() {
    std::ifstream input("sample.txt");
    if (!input) {
        std::cerr << "Cannot open file\n";
        return 1;
    }
    int lines = 0, words = 0;
    std::string line;
    while (std::getline(input, line)) {
        ++lines;
        std::istringstream row(line);
        std::string word;
        while (row >> word) ++words;
    }
    if (!input.eof()) {
        std::cerr << "Read error\n";
        return 1;
    }
    std::cout << lines << ' ' << words << '\n';
}

Чтение через const& и изменение через & — разные контракты.

code-guided-parameters.cpp
#include <cstddef>
#include <iostream>
#include <string>

std::size_t length(const std::string& text) {
    return text.size();
}

void addMark(std::string& text) {
    text += '!';
}

int main() {
    std::string text = "hello";
    const std::size_t oldLength = length(text);
    addMark(text);
    std::cout << oldLength << ' ' << text << '\n';
}

Обсудить лекцию с ИИ

Скопируйте промпт в свой ИИ-чат. В него уже включены материал этой лекции и примеры кода — можно сразу задавать вопросы и тренироваться.

Посмотреть промпт