Константность и права доступа
Квалификатор const, четыре формы указателей, константные ссылки, время жизни временных объектов и выбор параметров функций.
Что означает const
const — квалификатор типа, ограничивающий допустимые операции модификации через соответствующее выражение. const int и int — разные cv-квалифицированные формы типа.
Идея лекции: константность — ограничение возможностей доступа, а не обещание, что числовое значение нигде и никогда не изменится.
const int a = 5; можно читать и сравнивать, но нельзя присвоить ему новое значение и даже повторно записать 5.
const дополняет тип. В записи int const x = 5; смысл тот же, что у const int x = 5;.
#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 int snapshot = original создаёт отдельное число и запрещает менять его после инициализации. Изменение original не обновит snapshot автоматически (это сохранённая копия, а не наблюдение за исходной переменной). В визуализации сравните чтение и операции записи для обычного и константного int.
#include <iostream>
int main() {
int original = 5;
const int snapshot = original;
original = 9;
// snapshot = 9; // ошибка: snapshot нельзя присваивать новое значение
std::cout << original << ' ' << snapshot << '\n';
}#include <array>
#include <iostream>
int main() {
const std::array<int, 3> values{2, 4, 6};
int total = 0;
for (int value : values) {
total += value;
}
// values[0] = 10; // ошибка: доступ к элементам только для чтения
std::cout << values.front() << ' ' << total << '\n';
}const int x = 5; создаёт объект с исходным значением. x = 5; — последующее присваивание и ошибка компиляции, хотя значение совпадает.
Константные ссылки
const T& — ссылка, через которую нельзя менять объект. Она не может быть переназначена, как и обычная T&, но позволяет удобно читать уже существующий объект без копирования.
const int& r = x; создаёт ещё одно имя x с доступом только на чтение. Изменение исходного x отражается при чтении r.
const T& — ссылка на cv-квалифицированный объект. Неконстантная T& не может связываться с const T.
#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 со ссылкой на действительно константный объект: ограничение доступа одинаковое, а свойства исходных объектов различаются.
#include <iostream>
int main() {
int value = 5;
const int& view = value;
std::cout << view << '\n';
value = 12;
std::cout << view << '\n';
std::cout << std::boolalpha << (&view == &value) << '\n';
}#include <iostream>
int main() {
const int limit = 7;
const int& view = limit;
int copy = view;
++copy;
std::cout << limit << ' ' << view << ' ' << copy << '\n';
// ++view; // ошибка: изменение через const int& запрещено
// ++limit; // ошибка: сам limit тоже константный
}Создание константного объекта в динамической памяти
new const int(5) возвращает const int*. Поскольку объект константный, его нужно инициализировать сразу. Обычный int* не может принять такой указатель неявно.
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& ограничивается доступ к самому T. Запись T& const некорректна.
Передача параметров
В функцию можно передать объект по значению (T), по изменяемой ссылке (T&) или по константной ссылке (const T&). Выбор определяет копирование и право изменять исходный объект.
Для крупного объекта вроде строки, когда нужно лишь читать, типичный выбор — const std::string&. Если нужно изменить аргумент вызывающего кода — std::string&. Для простых небольших значений (int, double) обычно лучше копирование.
Образуется самостоятельный объект-параметр; его изменения не затрагивают аргумент.
Параметр именует исходный объект без копирования всего объекта.
#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-ссылке. Если функции нужна собственная изменяемая строка, параметр по значению прямо выражает это намерение.
#include <cstddef>
#include <iostream>
#include <string>
int squared(int value) { return value * value; }
std::size_t textLength(const std::string& text) { return text.size(); }
int main() {
int number = 4;
std::string message = "hello";
std::cout << squared(number) << ' ' << textLength(message) << '\n';
std::cout << number << ' ' << message << '\n';
}#include <iostream>
#include <string>
std::string decorate(std::string text) {
text += "!";
return text;
}
int main() {
std::string original = "hello";
std::string result = decorate(original);
std::cout << original << ' ' << result << '\n';
std::cout << decorate(std::string{"ready"}) << '\n';
}Прослеживайте, какой объект обозначает имя параметра внутри функции. При передаче по значению это отдельный объект, а параметр-ссылка обозначает объект вызывающего кода. Константная ссылка по-прежнему обозначает исходный объект, но запрещает изменять его через данный параметр.
#include <iostream>
int incrementCopy(int value) {
++value;
return value;
}
int main() {
int original = 5;
int result = incrementCopy(original);
std::cout << original << ' ' << result << '\n';
}#include <iostream>
void increment(int& value) { ++value; }
int read(const int& value) { return value; }
int main() {
int original = 5;
increment(original);
std::cout << original << ' ' << read(original) << '\n';
}| Форма | Копия объекта | Меняет оригинал | Подходит для |
|---|---|---|---|
void f(T x) | Да (или перемещение / устранение копии) | Нет | Небольшие типы; владение копией |
void f(T& x) | Нет | Может | Изменение исходного объекта |
void f(const T& x) | Нет | Нет через x | Чтение большого объекта без копии |
В лекции это не рекомендуется как стиль для параметров. На практике const T внутри определения функции может защищать локальную копию, но в сигнатуре не меняет механизм передачи и не образует отдельную перегрузку.
Разобранный пример и самостоятельная проверка
Перед написанием функции задайте три вопроса. Нужно ли изменить объект вызывающего кода? Нужно ли только прочитать большой объект? Нужна ли функции собственная копия? Для небольшого int или double обычно выбирают значение. Для изменения исходной строки подойдёт string&. Для чтения уже существующей строки без её копирования удобно const string&. Слово const здесь ограничивает операции через данный параметр, а не делает исходную неконстантную строку навсегда неподвижной. Другой разрешённый путь может её изменить. Если функция должна получить собственный объект, передача по значению остаётся осмысленным выбором даже для строки. Это стартовые рекомендации, а не утверждение, что любая ссылка быстрее любой копии. Сначала выберите нужное поведение и владение, затем проверяйте стоимость. Именованная константа задаётся при создании; последующее присваивание ей недопустимо. Указатели с несколькими уровнями const для этого первого выбора не нужны.
#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 int* p — указатель, через который можно лишь читать int. int* const p — неизменяемый указатель на изменяемый int. Положение const относительно * меняет смысл.
Представь две самостоятельные сущности: объект-указатель, хранящий адрес, и объект по этому адресу. Для каждой может быть своё право на изменение. Наведись на Как читать справа налево?Найди имя p и двигайся наружу по декларации.const int* pуказатель на константный intint* const pконстантный указатель на intconst int* const pконстантный указатель на константный int для краткой памятки.
Константность объекта под указателем: const int*.
Константность самой переменной-указателя: int* const.
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 int* и int* const решают разные задачи. Прочитайте тип, выберите соответствующую форму в визуализации и отдельно попробуйте изменить адрес и значение.
#include <iostream>
int main() {
int first = 5;
int second = 9;
const int* view = &first;
first = 6;
std::cout << *view << '\n';
view = &second;
std::cout << *view << '\n';
// *view = 10; // ошибка: через view изменять объект нельзя
}#include <iostream>
int main() {
int value = 5;
int* const fixed = &value;
const int* const readonlyFixed = &value;
++*fixed;
std::cout << value << ' ' << *readonlyFixed << '\n';
// fixed = nullptr; // ошибка: адрес fixed менять нельзя
// *readonlyFixed = 10; // ошибка: запись через readonlyFixed запрещена
}| Тип | Изменить *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 может изменить его.
Преобразование, меняющее cv-квалификацию типа по правилам языка; простое добавление const на первом уровне безопасно.
#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.
#include <iostream>
int main() {
int value = 4;
int* writable = &value;
const int* readable = writable;
*writable = 9;
std::cout << *readable << '\n';
// int* forbidden = readable; // ошибка: неявно снять const нельзя
}#include <iostream>
int main() {
int first = 3;
int second = 8;
int* const fixed = &first;
int* copy = fixed;
copy = &second;
const int saved = 5;
int editable = saved;
++editable;
std::cout << *fixed << ' ' << *copy << '\n';
std::cout << saved << ' ' << editable << '\n';
}int x = 4; const int* p = &x; x = 9; допустимо, а чтение *p даст 9. Запрет касается операции записи именно через p.
Продление времени жизни
При прямом связывании const T& с временным объектом его время жизни в ряде случаев продлевается до времени жизни ссылки. Но это правило имеет важные исключения.
const std::string& s = "hello"; создаёт временный объект std::string и привязывает к нему ссылку. При такой локальной инициализации объект доживёт до конца области видимости ссылки.
Временный объект — объект с ограниченным временем жизни, обычно создаваемый при вычислении выражения.
Ссылка, которая больше не обозначает живой объект; использование объекта через неё — UB.
#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-ссылка, непосредственно связанная с временным результатом, может продлить его жизнь до конца своего блока. Возврат ссылки из другой функции такого продления не создаёт. Поэтому для самостоятельного результата функции обычно проще вернуть значение.
#include <iostream>
#include <string>
std::string makeMessage() { return "safe result"; }
int main() {
const std::string& message = makeMessage();
std::cout << message << '\n';
std::cout << message.size() << '\n';
} // временная строка уничтожается при выходе из этого блока
#include <cstddef>
#include <iostream>
#include <string>
std::size_t textLength(const std::string& text) {
return text.size();
}
int main() {
std::size_t length = textLength(std::string{"abc"});
std::cout << length << '\n';
}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. Для записи понадобится ещё одна проверка: перед сообщением об успехе убедиться, что поток успешно завершил сохранение. Бинарные числа и изменение исходного файла изучаются отдельным шагом.
#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';
}Открытие, чтение и конец файла
Сразу после открытия проверьте поток: if (!input) означает, что читать нельзя. Проверка eof() до попытки чтения не предсказывает её успех. В обычном цикле while (input >> value) тело выполняется только после успешного извлечения; последнюю переменную нельзя использовать ещё раз после неудачного чтения.
Для строгой проверки файла нужно отличить чистый конец данных от испорченной записи. Пример ниже сначала пропускает пробельные символы через std::ws, проверяет конец, а затем читает число. Поэтому пустой файл даёт нулевую сумму, а текст вместо числа, переполнение типа и обрыв недопустимой записи дают отказ. bad() указывает на серьёзную ошибку ввода-вывода.
Создайте рядом с программой numbers.txt с содержимым 4 -2 7. Результат будет count=3 sum=9. Вариант без завершающего перевода строки тоже должен работать. Константы имени файла и допустимых границ объявлены вне main.
#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';
}Запись и владение ресурсом
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() показывает число реально извлечённых байтов: ненулевой неполный хвост означает повреждённую запись. Чистый конец наступает, когда извлечено ноль байтов.
#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';
}Матрица неизвестного размера без загрузки в массив
Читайте файл построчно через std::getline. std::istringstream из <sstream> позволяет читать числа из одной строки тем же оператором >>. Это строковый поток, а не массив всей матрицы. Число значений первой строки задаёт ожидаемое число столбцов; остальные строки должны иметь столько же значений.
Создайте matrix.txt из двух строк: 1 2 и 3 4. Пример определит размер 2 × 2 и сумму 10 без хранения таблицы. Пустой файл, пустая строка, нечисловой элемент и строки разной длины отклоняются. Для задания со столбцами сделайте повторный проход, выбирая нужную позицию в каждой строке.
#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';
}Повторные проходы и рабочие файлы
Для нового прохода проще создать новый std::ifstream с тем же именем. Если используете прежний поток, сначала вызовите clear(), чтобы снять признаки конца и ошибки, затем seekg(0) и проверьте результат. tellg() возвращает позицию потока, а не порядковый номер числа. Номер записи считайте отдельной переменной с нуля.
Для удаления участка сначала найдите его границы, затем новым проходом перепишите сохраняемые записи в другой файл. Не перезаписывайте исходник до проверки результата. Для учебного отчёта достаточно оставить оба файла и показать их содержимое. Сортировку файла без массива можно выполнить несколькими проходами: на каждом искать следующий минимум и записывать нужное число его повторений.
Перед защитой проверьте пустой файл, одну запись, повторные экстремумы, отсутствие нужного элемента, недоступный путь, испорченный текст и неполную двоичную запись. Состояние операций чтения и gcount() описаны в разделе проекта стандарта C++ о неформатированном вводе.
Перегрузка и const
f(T&) и f(const T&) — разные перегрузки. Для изменяемого объекта обычно предпочтительна T&, для константного — const T&.
Константность аргумента влияет на допустимость и предпочтение перегрузок, принимающих ссылки. Но f(T) и f(const T) по верхнеуровневому const параметра по значению перегружать нельзя.
Правила выбора наиболее подходящей функции среди одноимённых кандидатов по аргументам и преобразованиям.
#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); // одна сигнатура
}Сначала проверьте, может ли аргумент вообще связаться с параметром каждой функции. Затем сравните подходящие варианты. Для пары int& и const int& изменяемая переменная лучше подходит к int&, а константа и временное значение — к const int&. Это правило относится к конкретной паре перегрузок в примере.
#include <iostream>
void show(int&) { std::cout << "mutable\n"; }
void show(const int&) { std::cout << "readonly\n"; }
int main() {
int value = 5;
const int limit = 7;
show(value);
show(limit);
show(9);
}#include <iostream>
void show(int value);
void show(const int value) {
std::cout << value << '\n';
// ++value; // ошибка: внутри этого определения копия защищена const
}
int main() {
int first = 3;
const int second = 8;
show(first);
show(second);
}Из пары f(T&) и f(const T&) временный объект обычного типа можно передать в const T&, но не в неконстантную lvalue-ссылку.
Const на двух уровнях
int** нельзя неявно превратить в const int**. На первом уровне добавление const допустимо, а такое вложенное преобразование могло бы открыть путь к изменению настоящего const int.
Представь: int** даёт доступ к переменной типа int*. Если бы его разрешили выдавать за const int**, можно было бы заменить внутренний указатель на адрес действительно константного объекта.
Потом исходный int* оказался бы обращённым к const int, и запись через него нарушила бы const-корректность.
Правила добавления const на вложенных уровнях указателей; для допустимого преобразования могут потребоваться дополнительные квалификаторы на промежуточных уровнях.
int main() {
int x = 4;
int* p = &x;
int** pp = &p;
// const int** bad = pp; // CE: опасная квалификация
const int* const* safe = pp; // OK
(void)safe;
}Для указателя на указатель ограничения нужны на каждом уровне. Ошибка int** → const int** опасна тем, что через промежуточный указатель можно было бы подменить адрес и затем изменить действительно константный объект. В безопасной форме const int* const* запрещена и такая подмена промежуточного указателя.
#include <iostream>
#include <type_traits>
int main() {
static_assert(!std::is_convertible_v<int**, const int**>);
static_assert(std::is_convertible_v<int**, const int* const*>);
int value = 5;
int* pointer = &value;
const int* const* view = &pointer;
std::cout << **view << '\n';
value = 9;
std::cout << **view << '\n';
// *view = nullptr; // ошибка: промежуточный указатель защищён
}#include <iostream>
int main() {
int value = 4;
int* pointer = &value;
int* const* view = &pointer;
**view = 12;
std::cout << value << '\n';
// *view = nullptr; // ошибка: менять pointer через view нельзя
}const int* const*Некоторые многоуровневые преобразования корректны: например, int** в const int* const* допустимо. Учитываются квалификаторы на каждом уровне, а не только крайний const.
Дополнительно
Тонкости и полезные дополнения · необязательно для основной лекции
Top-level const для параметров по значению не участвует в различении перегрузок. RAII и передача по значению с перемещением расширяют простые рекомендации лекции. const_cast существует, но попытка изменять действительно const-объект через снятую константность приводит к UB.
Современное правило передачи
Для дешёвых в копировании типов выбирайте передачу по значению. Крупные только для чтения часто передают через const T&. Но если функция должна владеть собственной копией, передача по значению может быть предпочтительнее.
Исключения lifetime extension
Срок жизни временного объекта не продлевается до срока жизни ссылки при return и при связывании с параметром функции — там объект существует до конца полного выражения вызова. Инициализация членов-ссылок имеет собственные тонкости.
#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 сам по себе не гарантирует потокобезопасность и не означает размещение объекта в памяти «только для чтения». Это ограничение операций языка.
Справочник терминов
Основные определения встречаются внутри глав. Здесь они собраны в одном алфавитном поиске.
Константность
Квалификация типа, ограничивающая возможность модифицировать объект через соответствующее выражение.
Примеры кода
Строки и слова: обычный, пустой и отсутствующий файл
#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& и изменение через & — разные контракты.
#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';
}Обсудить лекцию с ИИ
Скопируйте промпт в свой ИИ-чат. В него уже включены материал этой лекции и примеры кода — можно сразу задавать вопросы и тренироваться.