%%{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: "Четыре оператора приведения C++ разводят ранее смешанные в одной записи семантики"
%%| fig-width: 6.4
%%| fig-height: 3.4
flowchart TB
Casts["Операторы приведения C++"]
Dyn["dynamic_cast<br/>иерархия, проверка в runtime"]
Stat["static_cast<br/>преобразование на этапе компиляции"]
Const["const_cast<br/>const/volatile"]
Reint["reinterpret_cast<br/>биты / адреса как другой тип"]
Casts --> Dyn
Casts --> Stat
Casts --> Const
Casts --> Reint
W4. Приведения типов C++, идентификация типа, удалённые и заданные по умолчанию функции, инициализация членов, делегирующие конструкторы, указатель this, константные методы
1. Краткое содержание
1.1 Проблема приведений в стиле C (C-style casts)
До C++11 для всех преобразований типов обычно использовали C-style casts (приведения в стиле C). Но такие приведения слишком «всеядны» и почти не фиксируют намерение программиста в терминах языка.
1.1.1 Традиционная запись приведений
C++ унаследовал от C две формы записи:
Запись в стиле C:
int x = (int)12.34;Функциональная запись:
int x = int(12.34);Обе формы избыточно универсальны: при одной и той же синтаксической конструкции могут выполняться принципиально разные операции:
int x = (int)12.34; // Преобразование значения: меняются биты (округление)
int* px = &x;
long a = (long)px; // Реинтерпретация: биты не «пересчитываются»
Derived* pd = new Derived();
Base* pb = (Base*)pd; // Навигация по иерархии: нужна проверка в runtime1.1.2 Семантическая проблема
Корневая проблема традиционных приведений — неоднозначность намерения (ambiguity of intent). Увидев (Type)expression, нельзя сразу понять, какое именно преобразование имеется в виду:
- преобразование значения (value conversion)? (например,
double→intс потерей данных); - реинтерпретация (reinterpretation)? (например, указатель → целое без изменения битового образа);
- приведение вверх/вниз по иерархии (upcasting/downcasting)? (например,
Derived*→Base*); - добавление/снятие const (constness)? (например,
const char*→char*).
Из‑за этой неоднозначности код труднее читать, сопровождать и отлаживать: сложнее искать «опасные» приведения по проекту, а компилятору труднее выдавать уместные предупреждения.
1.1.3 Решение в C++
В C++ ввели четыре специализированных оператора приведения (cast operators), у каждого — своя роль и более прозрачная семантика:
dynamic_cast<T>(v)— безопасное приведение в runtime с проверками;static_cast<T>(v)— приведение на этапе компиляции без проверок в runtime;const_cast<T>(v)— добавить или убрать квалификаторы const/volatile;reinterpret_cast<T>(v)— реинтерпретировать битовый образ без его «пересчёта».
У каждого оператора своя зона ответственности: код становится более самодокументируемым, а компилятор может ловить ошибки точнее.
1.2 Статический и динамический тип: напоминание
Прежде чем разбирать операторы приведения, полезно зафиксировать базовое различие:
Static type (статический тип) — тип, который виден в исходном коде и определяется на compile time:
Circle circle;
Shape* figure = &circle;Здесь статический тип figure — это Shape* (как объявлено).
Dynamic type (динамический тип) — фактический тип объекта в runtime, то есть тип того, на что реально указывает указатель/ссылка:
После присваивания выше динамический тип figure соответствует объекту Circle (фактический тип объекта в памяти).
Это различие критично, чтобы понимать, когда уместен dynamic_cast, а когда — static_cast.
%%{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: 3
flowchart LR
Decl["Shape* figure"]
Obj["фактический объект: Circle"]
Decl -- "static type" --> Shape["Shape*"]
Decl -- "dynamic type (runtime)" --> Obj
1.3 dynamic_cast
dynamic_cast<T>(v) выполняет проверяемые в runtime преобразования между указателями или ссылками в иерархии наследования.
1.3.1 Синтаксис и требования
dynamic_cast<T>(v)Требования:
Tдолжен быть типом указателя или ссылки;vдолжен быть указателем или ссылкой на объект классового типа;- у базового класса должен быть хотя бы один виртуальный метод — это включает RTTI (Run-Time Type Information).
1.3.2 Как работает dynamic_cast
Для указателей: при неудаче возвращает nullptr (объект не имеет целевого типа).
Base* pb = new Derived();
Derived* pd = dynamic_cast<Derived*>(pb);
if (pd != nullptr) {
// Приведение удалось: pb указывает на Derived
pd->derivedMethod();
} else {
// Приведение не удалось: pb не указывает на Derived
}Для ссылок: при неудаче выбрасывается исключение std::bad_cast.
Base& rb = /* ссылка на базовый тип */;
try {
Derived& rd = dynamic_cast<Derived&>(rb);
// Приведение удалось
} catch (std::bad_cast& e) {
// Приведение не удалось
}1.3.3 Когда использовать dynamic_cast
Имеет смысл, когда:
- нужно безопасно выполнить downcast от указателя/ссылки на базовый класс к производному;
- вы не уверены в фактическом runtime-типе объекта;
- нужна проверка типа в runtime, чтобы предотвратить ошибки.
Пример:
class Base {
public:
virtual void f() { } // Нужен хотя бы один virtual
};
class Derived : public Base {
public:
void derivedMethod() { cout << "Derived!" << endl; }
};
Base* pb = new Derived();
// C-style cast: ОПАСНО — нет проверки в runtime
Derived* pd1 = (Derived*)pb; // Если pb не на Derived — UB
// dynamic_cast: безопаснее — есть проверка в runtime
Derived* pd2 = dynamic_cast<Derived*>(pb);
if (pd2 != nullptr) {
pd2->derivedMethod(); // Вызов безопасен
}Ключевое преимущество: dynamic_cast выполняет проверки в runtime. Если pb не указывает на объект Derived, для указателя вернётся nullptr, а не немедленный undefined behavior.
%%{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: "dynamic_cast проверяет runtime-тип перед downcast"
%%| fig-width: 6.2
%%| fig-height: 3.2
classDiagram
class Base
class Derived
Base <|-- Derived
1.3.4 Производительность
У dynamic_cast есть накладные расходы в runtime: нужно узнать фактический тип объекта. В узких по времени местах, где тип гарантирован, иногда берут static_cast — но это осознанный компромисс..
1.4 static_cast
static_cast<T>(v) выполняет преобразования, которые компилятор проверяет на compile time, без проверок в runtime — быстрее, но менее безопасно, чем dynamic_cast.
1.4.1 Синтаксис и требования
static_cast<T>(v)Требования:
Tможет быть указателем, ссылкой или скалярным типом;vможет быть указателем, ссылкой или значением;- не требуется наличие виртуальных методов у базового класса.
1.4.2 Как работает static_cast
static_cast описывает преобразования, которые компилятор может согласовать на compile time, но без проверки корректности в runtime:
Base* pb = new Derived();
Derived* pd1 = (Derived*)pb; // C-style: без проверок
Derived* pd2 = static_cast<Derived*>(pb); // То же по смыслу, но явнееОтличие от dynamic_cast:
- нет проверок в runtime — оператор не «поймает» неверный dynamic type;
- быстрее — нет обхода иерархии типов в runtime;
- опаснее — если
pbне указывает наDerived, поведение не определено (UB).
1.4.3 Типовые сценарии
1. Числовые преобразования:
double d = 3.14;
int i = static_cast<int>(d); // Явно: возможна потеря данных2. Приведение вверх по иерархии (обычно безопасно):
Derived* pd = new Derived();
Base* pb = static_cast<Base*>(pd); // Derived → Base3. Приведение вниз (опасно без гарантий):
Base* pb = /* ... */;
Derived* pd = static_cast<Derived*>(pb); // Только если вы УВЕРЕНЫ, что pb → Derived4. Преобразования с void*:
void* vp = /* ... */;
int* ip = static_cast<int*>(vp);1.4.4 Когда использовать static_cast
Уместно, когда:
- нужны числовые преобразования и вы хотите явно зафиксировать возможную потерю данных;
- вы уверены в dynamic type (например, только что создали объект нужного типа);
- критична производительность и типовая безопасность обеспечивается логикой программы;
- нужно приводить к/от
void*.
Практическое правило: если нет 100% уверенности в типе, для downcast предпочтительнее dynamic_cast.
%%{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: "dynamic_cast и static_cast при downcast"
%%| fig-width: 6.2
%%| fig-height: 3.2
flowchart LR
BasePtr["Base* pb"]
Dyn["dynamic_cast<Derived*>(pb)<br/>безопаснее, с проверкой"]
Stat["static_cast<Derived*>(pb)<br/>быстрее, без проверки"]
BasePtr --> Dyn
BasePtr --> Stat
1.5 const_cast
const_cast<T>(v) добавляет или убирает квалификаторы const (или volatile). Это единственный оператор приведения, который может снять constness.
1.5.1 Базовый синтаксис
const_cast<T>(v)- используется, чтобы убрать или добавить квалификаторы
const/volatile; - «вычислений» в runtime не делает — это конструкция системы типов на этапе компиляции;
- битовый образ объекта не меняет — меняется лишь тип в смысле правил языка.
1.5.2 Типовой случай: старый API без const
Иногда нужно передать const-объект в функцию, которая принимает неконстантный указатель, хотя по смыслу данные не меняет:
const char* str = "abcdef";
void legacyFunction(char* s); // Старый API: s не меняется, но const нет
// Ошибка: нельзя передать const char* туда, где ожидается char*
// legacyFunction(str);
// OK: снять const через const_cast
legacyFunction(const_cast<char*>(str));⚠️ Внимание: это небезопасно, если legacyFunction всё-таки пишет в буфер:
- если объект изначально был по-настоящему
const(например, строковый литерал), запись даёт undefined behavior; const_castоправдан только когда вы уверены, что функция данные не модифицирует.
1.5.3 Безопасное и небезопасное использование
Относительно безопасный сценарий:
const int* constPtr = /* ... */;
void readOnly(int* p) {
cout << *p; // Только чтение
}
// Безопасно, если readOnly действительно ничего не меняет
readOnly(const_cast<int*>(constPtr));Небезопасный сценарий:
const int x = 42;
int* px = const_cast<int*>(&x);
*px = 100; // UB: x изначально const1.5.4 Когда использовать const_cast
Имеет смысл, когда:
- стыкуетесь со legacy C-API, где
constрасставлен неверно; - у вас есть
const-объект, но нужно вызвать non-const метод, который по факту состояние не меняет; - вы поэтапно доводите большой код до const-correctness.
Практика: по возможности избегайте const_cast; если API ваш — лучше поправить сигнатуру функции.
1.6 reinterpret_cast
reinterpret_cast<T>(v) трактует битовый образ объекта как другой тип без «пересчёта» значения — самый опасный из стандартных приведений.
1.6.1 Синтаксис и смысл
reinterpret_cast<T>(v)- меняет интерпретацию двоичного представления;
- не выполняет отдельной операции над битами (в смысле преобразования значения);
- не делает проверок в runtime;
- позволяет рассматривать значение «как будто» это совсем другой тип.
1.6.2 Типовые сценарии
1. Указатель как целое (и обратно):
int x = 777;
int* p = &x;
// Указатель → целое (хеширование, отладка низкого уровня)
long internal = reinterpret_cast<long>(p);
// Обратно в указатель
int* back = reinterpret_cast<int*>(internal);2. Type punning: одна и та же память «глазами» разных типов:
unsigned int bits = 0x41200000; // Битовый образ
float* f = reinterpret_cast<float*>(&bits);
// *f читает те же биты как float3. «Несовместимые» указатели:
unsigned* px = /* ... */;
int* py = reinterpret_cast<int*>(px); // Допустимо через reinterpret_castБез reinterpret_cast это ошибка:
unsigned* px = /* ... */;
int* py = px; // ERROR: несовместимые типы1.6.3 Риски
reinterpret_cast крайне опасен:
- полностью обходит типовую безопасность;
- при некорректной интерпретации легко получить undefined behavior;
- поведение зависит от платформы (размер указателя, порядок байт, выравнивание).
Пример опасности:
unsigned x = 777;
unsigned* px = &x;
int y = 999;
int* py = &y;
py = reinterpret_cast<int*>(px); // Синтаксически «можно»
*py = -1; // UB: писать «как int» в unsigned-память так нельзя1.6.4 Когда использовать reinterpret_cast
Только если реально нужен низкий уровень:
- системные API;
- собственные аллокаторы;
- регистры/память устройств (memory-mapped I/O);
- бинарная сериализация с доступом к «сырой» памяти.
Для типичного прикладного кода reinterpret_cast почти никогда не нужен.
1.7 Идентификация типов: typeid
Оператор typeid позволяет узнавать тип в runtime; вместе с dynamic_cast это часть RTTI (Run-Time Type Information).
1.7.1 Базовый синтаксис
typeid(expression) // Тип выражения
typeid(type) // Сам типtypeid возвращает ссылку на объект std::type_info с информацией о типе.
Аналогия с sizeof:
Как sizeof работает и для типа, и для выражения:
sizeof(int) // Размер типа
sizeof(x) // Размер типа выражения xтак же устроен и typeid:
typeid(int) // Информация о типе int
typeid(x) // Информация о типе x1.7.2 Класс type_info
Фрагмент из стандарта ISO C++ (раздел 17.7.3):
namespace std {
class type_info {
public:
virtual ~type_info();
bool operator==(const type_info& rhs) const noexcept;
bool before(const type_info& rhs) const noexcept;
size_t hash_code() const noexcept;
const char* name() const noexcept;
type_info(const type_info&) = delete; // Cannot be copied
type_info& operator=(const type_info&) = delete; // Cannot be copied
};
}Основные операции:
- имя типа:
name()— строковое представление (implementation-defined); - сравнение типов:
operator==— совпадают ли типы; - хеш:
hash_code()— для хеш-таблиц.
Замечание: смысл before() зависит от реализации, на практике используется редко.
1.7.3 Примеры с typeid
Проверка динамического типа:
Base* pb = new Derived();
const std::type_info& info = typeid(*pb); // Dynamic type: Derived
cout << "Type: " << info.name() << endl;Сравнение типов:
Base* pb = new Derived();
if (typeid(*pb) == typeid(Derived)) {
cout << "pb points to a Derived object" << endl;
}
if (typeid(*pb) == typeid(Base)) {
cout << "pb points to a Base object" << endl; // Не выполнится
}Важное различие:
Base* pb = new Derived();
typeid(pb) // Тип: Base* (static type указателя)
typeid(*pb) // Тип: Derived (dynamic type объекта)1.7.4 Когда использовать typeid
Уместно, когда:
- нужно узнать фактический runtime-тип объекта;
- строите свою сериализацию/«отражение»;
- отлаживаете (печать информации о типе);
- делаете dispatch по типу без
dynamic_cast.
Но: частые проверки через typeid часто сигнализируют о слабом OOP-дизайне; предпочтительнее полиморфизм (virtual functions).
1.7.5 Сравнение: typeid и dynamic_cast
| Свойство | typeid |
dynamic_cast |
|---|---|---|
| Задача | узнать тип | привести тип |
| Результат | ссылка на type_info |
указатель/ссылка или nullptr |
| Типичный сценарий | проверка типа | безопасный downcast |
| Производительность | обычно быстрее | медленнее (обход иерархии) |
Часто можно выбрать любой из подходов:
// Через typeid
if (typeid(*pb) == typeid(Derived)) {
Derived* pd = static_cast<Derived*>(pb);
pd->derivedMethod();
}
// Через dynamic_cast (часто предпочтительнее)
if (Derived* pd = dynamic_cast<Derived*>(pb)) {
pd->derivedMethod();
}Вариант с dynamic_cast обычно ближе к идиоматическому C++.
1.8 Удалённые и заданные по умолчанию функции (= delete / = default)
C++11 дал явный контроль над особими функциями-членами через спецификаторы = default и = delete.
1.8.1 Проблема: правила неявной генерации
Компилятор может неявно сгенерировать ряд особых функций-членов:
class T {
// Compiler may generate:
T(); // Default constructor
T(const T&); // Copy constructor
T(T&&); // Move constructor
virtual ~T(); // Destructor
T& operator=(const T&); // Copy assignment
T&& operator=(T&&); // Move assignment
};Но правила того, когда именно что генерируется, крайне сложны:
- если вы объявили любой конструктор, default constructor уже не генерируется автоматически;
- если объявлен move constructor, копирующие операции могут оказаться удалёнными;
- если объявлен деструктор, генерация копирующего присваивания может вести себя неочевидно;
- плюс множество других взаимных ограничений.
Итог: правила трудно держать в голове, и от этого появляются тонкие ошибки.
1.8.2 Мотивация: объекты без копирования
Частый приём — тип, который нельзя копировать:
Старый стиль (до C++11):
class NonCopyable {
public:
NonCopyable() { }
private:
// Declare but don't define - causes linker error if called
NonCopyable(const NonCopyable&);
NonCopyable& operator=(const NonCopyable&);
};Минусы:
- намерение неочевидно:
privateмогут означать и другое; - ошибка часто всплывает на линковке, а не на компиляции;
- если вы объявили конструктор вручную, default constructor уже не «появится сам».
1.8.3 Решение: = delete
Современный вариант:
class NonCopyable {
public:
NonCopyable() = default; // Generate default constructor
NonCopyable(const NonCopyable&) = delete; // Delete copy constructor
NonCopyable& operator=(const NonCopyable&) = delete; // Delete copy assignment
};Плюсы:
- намерение явное: операции запрещены на уровне языка;
- ошибка на compile time, сообщения обычно понятнее;
- не нужен «пустой» конструктор ради генерации — достаточно
= default; = deleteприменим не только к особым функциям-членам.
1.8.4 Использование = default
Пример 1: вернуть генерацию конструктора по умолчанию
class A {
public:
A(int x) { } // Defining this suppresses default constructor
};
A a; // ERROR: no default constructorРешение:
class A {
public:
A(int x) { }
A() = default; // Force generation of default constructor
};
A a; // OK nowПример 2: явно зафиксировать «обычное» поведение
Даже если компилятор и так сгенерировал бы функцию, = default делает договорённость видимой:
class C {
public:
C() = default; // Explicit: this class is default-constructible
C(const C&) = default; // Explicit: this class is copyable
C& operator=(const C&) = default; // Explicit: this class is copy-assignable
~C() = default; // Explicit: destructor is not virtual
};1.8.5 = delete шире, чем «особые функции»
Запрет выделения в куче:
class StackOnly {
public:
void* operator new(size_t) = delete; // Prevent heap allocation
};
StackOnly* ps = new StackOnly(); // ERROR: operator new is deleted
StackOnly s; // OK: stack allocationОтсечь нежелательные преобразования аргументов:
void foo(double x) { /* ... */ }
foo(3.14); // OK: double literal
foo(3); // OK: int converts to double
foo(true); // OK: bool converts to double (unintended!)Оставим только double:
void foo(double x) { /* ... */ }
void foo(int) = delete; // Block int
void foo(bool) = delete; // Block bool
foo(3.14); // OK
foo(3); // ERROR: deleted function
foo(true); // ERROR: deleted functionЕщё жёстче — фактически только double:
template<typename T>
void foo(T) = delete; // Delete ALL other types
void foo(double x) { /* ... */ } // Only this overload allowed
foo(3.14); // OK: exact match for double
foo(3); // ERROR: would instantiate deleted template
foo(2.71F); // ERROR: float would instantiate deleted template1.8.6 Практические рекомендации
- Будьте явны:
= default/= deleteфиксируют намерение лучше «молчаливых» правил. - Не полагайтесь на неявную генерацию «на память»: правила слишком громоздкие.
= deleteполезен для любых перегрузок, не только для special member functions.- Предпочитайте ошибки компиляции:
= deleteобычно информативнее, чемprivateбез определения.
1.9 Инициализация баз и полей
При создании объекта производного класса нужно корректно инициализировать подобъект базы и поля производного класса.
1.9.1 Порядок выполнения конструкторов
При создании объекта производного класса порядок такой:
- конструктор базового класса (инициализирует base subobject);
- инициализаторы полей (member initializers) — в порядке объявления полей в классе;
- тело конструктора производного класса.
class Base {
int m;
public:
Base() { m = 0; }
Base(int i) { m = i; }
};
class Derived : public Base {
int md;
public:
Derived() { md = 7; }
};
Derived d; // Что произойдёт?Вопрос: какой конструктор Base будет вызван?
1.9.2 В чём затык
У базового класса может быть несколько конструкторов. Как выбрать нужный?
class Derived : public Base {
public:
Derived() { md = 7; } // Какой конструктор Base вызовется?
int md;
};Поведение по умолчанию: если в списке инициализации база не указана явно, вызывается default constructor базы. Но что делать, если default constructor отсутствует, или нужен другой конструктор базы?
1.9.3 Список инициализации конструктора (ctor-initializer)
Constructor initializer list (список инициализации конструктора), его же называют member initializer list, задаёт:
- какой конструктор базы вызвать;
- как инициализировать поля до входа в тело конструктора.
class Derived : public Base {
public:
Derived() : Base(1), md(7) { }
// ^^^^^^^^ ^^^^^
// | инициализация поля
// инициализация базы
int md;
};Синтаксис:
DerivedConstructor(parameters) : BaseClass(args), member1(value1), member2(value2) {
// Constructor body
}1.9.4 Инициализация базовых подобъектов
Базовый пример:
class Base {
public:
Base() { m = 0; }
Base(int i) { m = i; }
int m;
};
class Derived : public Base {
public:
Derived() : Base(1) { md = 7; } // Call Base(int) constructor
int md;
};
Derived d; // Calls Base(1), then initializes md = 7Несколько аргументов у базы:
class Base {
public:
Base(int x, int y) { /* ... */ }
};
class Derived : public Base {
public:
Derived() : Base(10, 20) { /* ... */ }
};1.9.5 Инициализация полей данных
Поля стоит (и часто нужно) инициализировать в списке:
class C {
int m1;
int m2;
T m3; // Some class type
public:
C() : m1(5), m2(10), m3(15) { } // Member initialization
};Зачем список, а не тело?
- эффективнее: прямое инициализирование, а не «дефолт + присваивание»;
- обязательно для
const, ссылок и полей без default constructor; - яснее: отделяет инициализацию от прочей логики в теле.
1.9.6 Инициализация и присваивание
Вариант 1: присваивание в теле (хуже для классовых полей)
class C {
int md;
T md2; // Some class type
public:
C() {
md = 7; // Assignment (for primitive types, OK)
md2 = T(5); // Default-construction, then assignment (inefficient)
}
};Вариант 2: инициализация в списке (предпочтительно)
class C {
int md;
T md2;
public:
C() : md(7), md2(5) { // Direct initialization
}
};Для классового md2 вариант 1 обычно означает:
- default-construct
md2; - затем assign
T(5).
Вариант 2 обычно сводится к одному шагу:
- прямое конструирование
md2значением5.
1.9.7 Когда список инициализации обязателен
Поля const:
class C {
const T md2;
public:
C() { md2 = expression; } // ERROR: cannot assign to const
C() : md2(expression) { } // OK: initialization
};Ссылки:
class C {
int& ref;
public:
C(int& r) : ref(r) { } // Must use initializer list
};Поля без конструктора по умолчанию:
class NoDefault {
public:
NoDefault(int x) { } // No default constructor
};
class C {
NoDefault nd;
public:
C() : nd(42) { } // Must initialize in list
};1.10 Делегирующие конструкторы (delegating constructors)
Delegating constructors (C++11) позволяют одному конструктору вызвать другой конструктор того же класса, уменьшая дублирование кода.
1.10.1 Проблема: дублирование кода
Несколько конструкторов часто повторяют одинаковую инициализацию:
class C {
int x, y;
public:
C() {
// Common initialization
x = 0;
y = 0;
// Specific logic
}
C(int val) {
// Common initialization (duplicated!)
x = 0;
y = 0;
// Specific logic
x = val;
}
};Старый приём: вынести общее в private метод:
class C {
int x, y;
private:
void init() { // Common initialization
x = 0;
y = 0;
}
public:
C() {
init();
// Specific actions
}
C(int val) {
init();
// Specific actions
x = val;
}
};1.10.2 Современный приём: делегирование
Вместо отдельного init() один конструктор может делегировать другому:
class C {
int x, y;
public:
C() { // Target constructor
x = 0;
y = 0;
}
C(int val) : C() { // Delegating constructor
x = val; // Specific actions
}
};Синтаксис: в списке инициализации вызывается другой конструктор: ИмяКонструктора(args).
1.10.3 Порядок выполнения
При делегировании:
- полностью отрабатывает target constructor (включая тело);
- затем выполняется тело delegating constructor.
class C {
public:
C() {
cout << "Common initialization" << endl;
}
C(int x) : C() {
cout << "Specific initialization" << endl;
}
};
C c(42);
// Output:
// Common initialization
// Specific initialization1.10.4 Терминология
class C {
public:
C(int) { } // Target constructor
C() : C(42) { } // Delegating constructor
};- target constructor — тот конструктор, которому делегируют;
- delegating constructor — тот, кто делегирует;
- primary constructor — распространённый приём: один конструктор держит «основную» инициализацию, остальные к нему делегируют.
1.10.5 Правила и ограничения
Нельзя смешивать делегирование с инициализацией полей в одном списке так:
class C {
int x;
public:
C(int val) : x(val) { }
C() : C(0), x(10) { } // ERROR: cannot delegate and initialize members
};Нельзя делать циклы делегирования:
class C {
public:
C(int) { }
C(): C(42) { } // Delegates to C(int)
C(char c): C(42.0) { } // ERROR: circular delegation
C(double d): C('a') { } // ERROR: circular delegation
};Делегирование «монопольно»: если вы делегируете, в том же списке нельзя параллельно:
- инициализировать базы;
- инициализировать поля;
- вызывать ещё один конструктор.
1.10.6 Плюсы
- меньше дублирования: общая логика в одном месте;
- проще сопровождать: правки общей части локализованы;
- яснее намерение: видно, что один конструктор «надстраивается» над другим.
1.11 Указатель this
Внутри функций-членов специальный указатель this указывает на объект, для которого функция вызвана.
1.11.1 Что такое this
this — неявный параметр, доступный во всех non-static функциях-членах:
class C {
int member;
public:
void f(int i) {
member = i; // Implicitly: this->member = i
this->member = i; // Explicitly using this
}
};По смыслу эквивалентно:
member = i;
this->member = i;1.11.2 Зачем он нужен
1. Снятие неоднозначности имён
Когда параметр называется так же, как поле:
class Point {
double x, y;
public:
void setX(double x) {
this->x = x; // this->x — поле, x — параметр
}
};2. Вернуть «текущий объект»
Удобно для method chaining:
class Builder {
public:
Builder& setWidth(int w) {
width = w;
return *this; // Return reference to current object
}
Builder& setHeight(int h) {
height = h;
return *this;
}
private:
int width, height;
};
Builder b;
b.setWidth(10).setHeight(20); // Method chaining3. Передать объект во внешнюю функцию
void externalFunction(C* obj);
class C {
public:
void f() {
externalFunction(this); // Pass pointer to current object
}
};4. Проверка самоприсваивания
class C {
public:
C& operator=(const C& other) {
if (this != &other) { // Check if assigning to self
// Perform assignment
}
return *this;
}
};1.11.3 Тип this
Для класса C:
- в non-const функции-члене:
thisимеет типC* const; - в const функции-члене:
thisимеет типconst C* const.
Почему this — константный указатель:
C* const this; // Implicit declarationТак язык запрещает «переназначить» текущий объект:
class C {
public:
void bad() {
this = &other; // ERROR: cannot modify this
}
};1.11.4 Как на самом деле устроены вызовы
Функции-члены получают this как скрытый первый параметр:
class C {
public:
int m;
void f(int i) { m = 7; }
};Компилятор моделирует это примерно так:
void f(C* this, int i) { // Hidden 'this' parameter
this->m = 7;
}Вызов:
C c;
c.f(1); // Becomes: f(&c, 1)
C* p = new C();
p->f(1); // Becomes: f(p, 1)Адрес объекта подставляется автоматически.
1.11.5 Статические функции-члены
У static member functions нет this: они не привязаны к конкретному экземпляру.
class C {
int member;
static int sMember;
public:
static void f() {
member = 5; // ERROR: no 'this' pointer
sMember = 7; // OK: static member
}
};1.12 Константные функции-члены (const после списка параметров)
Constant member functions обещают не менять состояние объекта: this трактуется как указатель на const.
1.12.1 Квалификатор const у метода
const ставится после списка параметров:
class C {
int member;
public:
void f1() { // Non-const member function
member = 5; // OK: can modify
}
void f2() const { // Const member function
member = 5; // ERROR: cannot modify
int x = member; // OK: can read
}
};1.12.2 Тип this в const-методах
Обычный метод:
void f() {
// this has type: C* const
// Can modify object through this
}const-метод:
void f() const {
// this has type: const C* const
// Cannot modify object through this
}Квалификатор const превращает this из «указателя на изменяемый объект» в «указатель на const».
1.12.3 Когда const-методы обязательны по смыслу использования
const-методы можно вызывать у const объектов; «неконстантные» — нет:
class C {
public:
void f1() { }
void f2() const { }
};
C c1;
c1.f1(); // OK
c1.f2(); // OK
const C c2;
c2.f1(); // ERROR: f1 can modify c2
c2.f2(); // OK: f2 cannot modify c2Почему это важно: при передаче по const reference (частый приём ради эффективности) доступны только const-методы:
void process(const C& obj) {
obj.f1(); // ERROR: f1 is not const
obj.f2(); // OK: f2 is const
}1.12.4 Практика: помечайте const, если метод не меняет состояние
class Point {
double x, y;
public:
double getX() const { return x; } // Doesn't modify, should be const
double getY() const { return y; }
void setX(double newX) { x = newX; } // Modifies, cannot be const
};Плюсы:
- документирует контракт «только чтение»;
- работает с
constобъектами; - помогает оптимизациям компилятора;
- расширяет применимость класса в
const-контекстах.
1.12.5 Перегрузка по const
Можно иметь две версии одной функции — const и не-const:
class C {
int* data;
public:
// Non-const version
int* getData() {
return data; // Returns modifiable pointer
}
// Const version
const int* getData() const {
return data; // Returns const pointer
}
};
C c1;
int* p1 = c1.getData(); // Calls non-const version
const C c2;
const int* p2 = c2.getData(); // Calls const versionКомпилятор выбирает версию по constness объекта.
1.13 Объявление и определение функции
Различие declaration vs definition критично для разнесения C++-кода на заголовки и .cpp.
1.13.1 Объявления и определения
Declaration (объявление): вводит имя и тип для компилятора.
Definition (определение): даёт полную реализацию (тело функции, полный класс и т.п.).
Для функций:
int f(int x); // Declaration (function prototype)
int f(int x) { ... } // Definition (includes body)Для классов:
class C; // Declaration (forward declaration)
class C { ... }; // Definition (complete class)1.13.2 Заголовки и исходники
Типичный C++-проект разделяет interface (объявления) и реализации:
Заголовок (Library.h):
// Declarations only
int f(int x);
class C {
int member;
public:
void method(int x); // Declaration
};Исходник (Library.cpp):
#include "Library.h"
// Definitions
int f(int x) {
return x * 2;
}
void C::method(int x) { // Note: C::method syntax
member = x;
}Пользовательский код (main.cpp):
#include "Library.h"
int main() {
f(42);
C obj;
obj.method(10);
}1.13.3 Определения функций-членов вне класса
Вне класса для принадлежности к типу используют scope resolution operator (оператор ::):
// In header
class C {
public:
void f(int x); // Declaration
};
// In source file
void C::f(int x) { // C::f specifies this is a member of C
// Implementation
}Смысл :: для компилятора: это не свободная функция f, а f класса C.
1.13.4 Зачем разделять объявление и определение?
1. Независимая компиляция
- заголовок задаёт interface;
- один заголовок подключают много
.cpp; - единицы трансляции компилируются отдельно;
- пересобирают в основном изменённые файлы.
2. Инкапсуляция
- потребителю виден интерфейс (заголовок);
- детали реализации живут в
.cpp; - реализацию можно менять, не ломая внешний контракт.
3. Меньше зависимостей
- заголовки компактнее и компилируются быстрее;
- тяжёлые реализации не «тащатся» в каждую единицу трансляции.
2. Определения
- C-style cast (приведение в стиле C): традиционная запись
(Type)exprилиType(expr), которая может означать любое преобразование без явной семантической метки. - Dynamic cast /
dynamic_cast:dynamic_cast<T>(v)— приведения в иерархии с проверкой в runtime; для указателей при ошибке даётnullptr, для ссылок бросает исключение. - Static cast /
static_cast:static_cast<T>(v)— приведения, согласуемые на compile time без проверок в runtime; быстрее, но при неверном dynamic type возможен UB. - Const cast /
const_cast:const_cast<T>(v)— добавить/убратьconst/volatile; единственный стандартный способ снять constness через приведение. - Reinterpret cast /
reinterpret_cast:reinterpret_cast<T>(v)— реинтерпретация битового образа как другого типа без «пересчёта» значения; максимально опасный инструмент. - Static type (статический тип): тип в коде на compile time (например,
Shape*у указателя, объявленного какShape*). - Dynamic type (динамический тип): фактический тип объекта в runtime (например,
Circle, еслиShape*указывает наCircle). - Type identification (идентификация типа): определение типа объекта в runtime, часто через
typeid. typeid: оператор, возвращающийconst std::type_info&с информацией о типе выражения/типа.std::type_info: стандартный класс сведений о типе; сравнение,name(),hash_code().- RTTI (Run-Time Type Information): служебная информация компилятора о типах; нужна для
dynamic_castи полиморфногоtypeid; требует полиморфного базового класса (обычно — virtual-функции). = default: явно попросить компилятор сгенерировать особую функцию-член с поведением по умолчанию.= delete: явно запретить использование функции (ошибка на compile time при попытке вызова).- Automatic generation (неявная генерация): автоматическое создание компилятором особых функций-членов при определённых условиях.
- Constructor initializer list (список инициализации конструктора): хвост
: Base(args), member(value)до тела конструктора. - Member initialization list: часть списка, которая инициализирует поля класса.
- Base class initialization: выбор конструктора базы в списке инициализации производного конструктора.
- Delegating constructor (делегирующий конструктор): конструктор, который в списке инициализации вызывает другой конструктор того же класса.
- Target constructor (целевой конструктор): конструктор, которому делегируют.
thispointer: неявный указатель на текущий объект в non-static методах; типC* constилиconst C* constвconst-методе.- Constant member function (const-метод): метод с
constпосле(), не меняющий состояние черезthis. - Const overloading: две перегрузки метода —
constи не-const; выбор по constness объекта. - Function declaration: имя и сигнатура без реализации.
- Function definition: полная спецификация, включая тело.
::(scope resolution operator): привязка имени к классу при определении вне класса (ClassName::functionName).- Header file: файл объявлений (часто
.h/.hpp), подключаемый через#include. - Source file: файл реализаций (часто
.cpp). - Forward declaration:
class C;без полного определения — чтобы использовать указатели/ссылки до полного класса.
3. Примеры
3.1. Класс банковского счёта: полная реализация (Лаба 4, Задание 1)
Реализуйте учебную модель банковских счетов, продемонстрировав:
- базовый класс
Accountс базовыми операциями; - производный класс
SavingsAccountс начислением процентов; - использование указателя
this; - const-методы;
= deleteдля запрета копирования;= defaultдля конструктора по умолчанию.
Нажмите, чтобы увидеть решение
Ключевая идея: цельная иерархия классов, где вместе собраны основные приёмы этой лекции: наследование, this, const-корректность, = default / = delete, корректная инициализация.
#include <iostream>
#include <string>
using namespace std;
class Account {
private:
int accountNumber;
double balance;
string ownerName;
public:
// Defaulted default constructor
Account() = default;
// Parameterized constructor
Account(int accNum, double initialBalance, string owner)
: accountNumber(accNum), balance(initialBalance), ownerName(owner) {
cout << "Account created for " << ownerName << endl;
}
// Deleted copy constructor and assignment operator
Account(const Account&) = delete;
Account& operator=(const Account&) = delete;
// Deposit money (uses this pointer for demonstration)
void deposit(double amount) {
if (amount > 0) {
this->balance += amount; // Explicit use of this
cout << "Deposited: $" << amount << endl;
} else {
cout << "Invalid deposit amount" << endl;
}
}
// Withdraw money (ensures balance doesn't go negative)
void withdraw(double amount) {
if (amount > 0 && this->balance >= amount) {
this->balance -= amount;
cout << "Withdrawn: $" << amount << endl;
} else {
cout << "Invalid withdrawal or insufficient funds" << endl;
}
}
// Constant member functions (don't modify state)
double getBalance() const {
return balance;
}
int getAccountNumber() const {
return accountNumber;
}
string getOwnerName() const {
return ownerName;
}
// Virtual destructor for proper inheritance
virtual ~Account() {
cout << "Account destroyed for " << ownerName << endl;
}
};
class SavingsAccount : public Account {
private:
double interestRate; // Annual interest rate (e.g., 2.5 for 2.5%)
public:
// Constructor delegating to base class
SavingsAccount(int accNum, double initialBalance, string owner, double rate)
: Account(accNum, initialBalance, owner), interestRate(rate) {
cout << "SavingsAccount created with " << interestRate << "% interest" << endl;
}
// Calculate and deposit interest
void calculateInterest() {
double interest = this->getBalance() * (interestRate / 100.0);
cout << "Calculating interest: $" << interest << endl;
this->deposit(interest); // Use inherited deposit method
}
// Constant member function
double getInterestRate() const {
return interestRate;
}
~SavingsAccount() {
cout << "SavingsAccount destroyed" << endl;
}
};
int main() {
cout << "=== Creating Savings Account ===" << endl;
SavingsAccount savings(123456, 1000.0, "John Doe", 2.5);
cout << "\n=== Initial State ===" << endl;
cout << "Account Number: " << savings.getAccountNumber() << endl;
cout << "Owner's Name: " << savings.getOwnerName() << endl;
cout << "Current Balance: $" << savings.getBalance() << endl;
cout << "Interest Rate: " << savings.getInterestRate() << "%" << endl;
cout << "\n=== Performing Transactions ===" << endl;
savings.deposit(500.0);
savings.withdraw(200.0);
cout << "\n=== After Transactions ===" << endl;
cout << "Current Balance: $" << savings.getBalance() << endl;
cout << "\n=== Calculating Interest ===" << endl;
savings.calculateInterest();
cout << "\n=== Final State ===" << endl;
cout << "Final Balance: $" << savings.getBalance() << endl;
// Attempting to copy would cause compile error
// SavingsAccount copy = savings; // ERROR: copy constructor deleted
// Account acc2 = savings; // ERROR: copy constructor deleted
cout << "\n=== Exiting (destructors called) ===" << endl;
return 0;
}Вывод программы:
=== Creating Savings Account ===
Account created for John Doe
SavingsAccount created with 2.5% interest
=== Initial State ===
Account Number: 123456
Owner's Name: John Doe
Current Balance: $1000
Interest Rate: 2.5%
=== Performing Transactions ===
Deposited: $500
Withdrawn: $200
=== After Transactions ===
Current Balance: $1300
=== Calculating Interest ===
Calculating interest: $32.5
Deposited: $32.5
=== Final State ===
Final Balance: $1332.5
=== Exiting (destructors called) ===
SavingsAccount destroyed
Account destroyed for John Doe
Разбор:
= default:Account() = defaultявно оставляет конструктор по умолчанию.= delete: копирующий конструктор и копирующее присваивание запрещены, чтобы нельзя было «дублировать» счёт.this:- в
deposit()/withdraw()используется явно для наглядности; this->balance— доступ к полю через указатель.
- в
- Const-методы:
getBalance(),getAccountNumber(),getOwnerName()помеченыconst;- их можно вызывать у
const Account; - контракт: состояние не меняют.
- Наследование:
SavingsAccountнаследуетAccount;- база инициализируется из списка конструктора;
- добавлена логика процентов.
- Виртуальный деструктор:
- корректное уничтожение при удалении через указатель на базу;
- порядок деструкторов: сначала производный, затем базовый.
Проектные решения:
- счета нельзя копировать (unique resource);
- баланс меняется только через
deposit/withdraw(encapsulation); - const-геттеры безопасно читают состояние;
- проценты кладутся через уже существующий
deposit(code reuse).
Ответ: полная реализация с = default / = delete, this, const-корректностью, инициализацией и наследованием.
3.2. Иерархия фигур и четыре вида приведения (Лаба 4, Задание 2)
Постройте иерархию фигур и покажите на примерах все четыре оператора приведения.
Нажмите, чтобы увидеть решение
Ключевая идея: для разных ситуаций в иерархии выбирается свой «правильный» cast operator.
#include <iostream>
#include <cmath>
using namespace std;
class Shape {
public:
virtual double area() const = 0; // Pure virtual
virtual double perimeter() const = 0; // Pure virtual
virtual ~Shape() { } // Virtual destructor
};
class Rectangle : public Shape {
private:
double width;
double height;
public:
Rectangle(double w, double h) : width(w), height(h) { }
double area() const override {
return width * height;
}
double perimeter() const override {
return 2 * (width + height);
}
// Rectangle-specific method
double diagonal() const {
return sqrt(width * width + height * height);
}
double getWidth() const { return width; }
double getHeight() const { return height; }
};
class Circle : public Shape {
private:
double radius;
public:
Circle(double r) : radius(r) { }
double area() const override {
return M_PI * radius * radius;
}
double perimeter() const override {
return 2 * M_PI * radius;
}
// Circle-specific method
double diameter() const {
return 2 * radius;
}
double getRadius() const { return radius; }
};
int main() {
Rectangle rectangle(5.0, 3.0);
Circle circle(4.0);
Shape* shape = &rectangle;
cout << "=== Demonstrate static casting [1] ===" << endl;
// Static cast: Compile-time downcast (no runtime check)
// Safe here because we KNOW shape points to Rectangle
const Rectangle* rectPtr = static_cast<const Rectangle*>(shape);
cout << "Rectangle width: " << rectPtr->getWidth() << endl;
cout << "Rectangle height: " << rectPtr->getHeight() << endl;
cout << "Rectangle diagonal: " << rectPtr->diagonal() << endl;
cout << "\n=== Demonstrate dynamic casting [2] ===" << endl;
// Dynamic cast: Runtime type checking
// Check if shape is actually a Circle
if (const Circle* circPtr = dynamic_cast<const Circle*>(shape)) {
cout << "Shape is a Circle with radius: " << circPtr->getRadius() << endl;
} else {
cout << "Shape is NOT a Circle" << endl;
}
// Now point to circle and check again
shape = &circle;
if (const Circle* circPtr = dynamic_cast<const Circle*>(shape)) {
cout << "Shape is a Circle with radius: " << circPtr->getRadius() << endl;
cout << "Circle diameter: " << circPtr->diameter() << endl;
} else {
cout << "Shape is NOT a Circle" << endl;
}
cout << "\n=== Demonstrate const casting [3] ===" << endl;
// Const cast: Remove const qualifier
// WARNING: Only safe if the original object wasn't const
const Rectangle* constRectPtr = &rectangle;
// Need to modify through a const pointer (normally not allowed)
// Remove const to call non-const method
Rectangle* mutableRectPtr = const_cast<Rectangle*>(constRectPtr);
// Now can call non-const methods (if they existed)
cout << "Successfully removed const (use with caution!)" << endl;
cout << "Area: " << mutableRectPtr->area() << endl;
cout << "\n=== Demonstrate reinterpret casting [4] ===" << endl;
// Reinterpret cast: Low-level bit reinterpretation
int intValue = 42;
// Treat the integer's bits as if they were a double
// WARNING: This is just for demonstration - meaningless operation!
double* doublePtr = reinterpret_cast<double*>(&intValue);
cout << "Integer value: " << intValue << endl;
cout << "Integer address: " << &intValue << endl;
cout << "Reinterpreted as double pointer: " << doublePtr << endl;
// Don't dereference doublePtr - it doesn't point to a valid double!
// More practical use: store pointer as integer
Shape* shapePtr = &rectangle;
long ptrAsInt = reinterpret_cast<long>(shapePtr);
cout << "Pointer stored as integer: " << ptrAsInt << endl;
// Convert back
Shape* restoredPtr = reinterpret_cast<Shape*>(ptrAsInt);
cout << "Restored pointer area: " << restoredPtr->area() << endl;
cout << "\n=== Summary ===" << endl;
cout << "1. static_cast: Fast compile-time cast (use when type is known)" << endl;
cout << "2. dynamic_cast: Safe runtime cast (use when type is uncertain)" << endl;
cout << "3. const_cast: Remove const (use rarely, with caution)" << endl;
cout << "4. reinterpret_cast: Bit reinterpretation (use for low-level operations)" << endl;
return 0;
}Вывод программы:
=== Demonstrate static casting [1] ===
Rectangle width: 5
Rectangle height: 3
Rectangle diagonal: 5.83095
=== Demonstrate dynamic casting [2] ===
Shape is NOT a Circle
Shape is a Circle with radius: 4
Circle diameter: 8
=== Demonstrate const casting [3] ===
Successfully removed const (use with caution!)
Area: 15
=== Demonstrate reinterpret casting [4] ===
Integer value: 42
Integer address: 0x7ffeefbff5ac
Reinterpreted as double pointer: 0x7ffeefbff5ac
Pointer stored as integer: 140732920755372
Restored pointer area: 15
=== Summary ===
1. static_cast: Fast compile-time cast (use when type is known)
2. dynamic_cast: Safe runtime cast (use when type is uncertain)
3. const_cast: Remove const (use rarely, with caution)
4. reinterpret_cast: Bit reinterpretation (use for low-level operations)
Разбор:
static_cast[1]:- downcast с
Shape*наconst Rectangle*; - приведение на compile time, без проверки в runtime;
- здесь безопасно, потому что известно:
shapeуказывает наRectangle; - быстрее
dynamic_cast, но требует уверенности программиста.
- downcast с
dynamic_cast[2]:- проверка типа в runtime;
- при ошибке для указателя —
nullptr; - первая проверка не срабатывает (
shape— неCircle); - вторая — срабатывает (
shapeуказывает наCircle); - безопаснее, но дороже по времени.
const_cast[3]:- снимает
constу типа указателя; - позволяет вызывать non-const методы через
constуказатель; - безопасно только если объект изначально не был «настоящим»
const; - злоупотреблять не стоит — часто симптом дизайна.
- снимает
reinterpret_cast[4]:- другая интерпретация битов без преобразования значения;
- в примере — хранение указателя как целого и обратно;
- максимально ломает типовую безопасность;
- оставить для низкоуровневых задач.
Когда что брать:
static_cast— если тип гарантирован (и/или критична скорость);dynamic_cast— если нужна безопасность при неопределённом runtime-типе;const_cast— стыковка со legacy API (по возможности — без этого);reinterpret_cast— системное программирование (в прикладном коде почти никогда).
Ответ: показаны все четыре приведения: static_cast для «известно корректных» случаев, dynamic_cast для безопасного downcast, const_cast для снятия const, reinterpret_cast для низкоуровневых манипуляций.
3.3. Класс без копирования (Лекция 4, Пример 1)
Сделайте класс, который можно создавать, но нельзя копировать, корректно используя = default и = delete.
Нажмите, чтобы увидеть решение
Ключевая идея: = delete явно запрещает копирование, а = default — явно просит компилятор сгенерировать «обычную» версию функции.
#include <iostream>
using namespace std;
class UniqueResource {
private:
int* data;
int id;
static int nextId;
public:
// Default constructor - explicitly defaulted
UniqueResource() = default;
// Parameterized constructor
UniqueResource(int value) : data(new int(value)), id(nextId++) {
cout << "Resource " << id << " created with value " << value << endl;
}
// Copy constructor - explicitly deleted
UniqueResource(const UniqueResource&) = delete;
// Copy assignment - explicitly deleted
UniqueResource& operator=(const UniqueResource&) = delete;
// Move constructor - can still move unique resources
UniqueResource(UniqueResource&& other) noexcept
: data(other.data), id(other.id) {
other.data = nullptr;
cout << "Resource " << id << " moved" << endl;
}
// Move assignment
UniqueResource& operator=(UniqueResource&& other) noexcept {
if (this != &other) {
delete data;
data = other.data;
id = other.id;
other.data = nullptr;
cout << "Resource " << id << " move-assigned" << endl;
}
return *this;
}
// Destructor
~UniqueResource() {
if (data != nullptr) {
cout << "Resource " << id << " destroyed" << endl;
delete data;
} else {
cout << "Resource " << id << " (moved-from) destroyed" << endl;
}
}
void print() const {
if (data != nullptr) {
cout << "Resource " << id << ": " << *data << endl;
} else {
cout << "Resource " << id << ": (moved-from state)" << endl;
}
}
};
int UniqueResource::nextId = 1;
int main() {
cout << "=== Creating resources ===" << endl;
UniqueResource r1(42);
UniqueResource r2(100);
cout << "\n=== Printing resources ===" << endl;
r1.print();
r2.print();
// Copy operations are deleted
// UniqueResource r3 = r1; // ERROR: copy constructor deleted
// r2 = r1; // ERROR: copy assignment deleted
cout << "\n=== Moving resource ===" << endl;
UniqueResource r3 = std::move(r1); // OK: move constructor
cout << "\n=== After move ===" << endl;
r1.print(); // r1 is in moved-from state
r3.print(); // r3 now owns the resource
cout << "\n=== Exiting (destructors called) ===" << endl;
return 0;
}Вывод программы:
=== Creating resources ===
Resource 1 created with value 42
Resource 2 created with value 100
=== Printing resources ===
Resource 1: 42
Resource 2: 100
=== Moving resource ===
Resource 1 moved
=== After move ===
Resource 1: (moved-from state)
Resource 1: 42
=== Exiting (destructors called) ===
Resource 2 destroyed
Resource 1 destroyed
Resource 1 (moved-from) destroyed
Разбор:
- Явное удаление копирования:
- копирующий конструктор:
UniqueResource(const UniqueResource&) = delete; - копирующее присваивание:
operator=(const UniqueResource&) = delete; - попытка копирования — ошибка компиляции.
- копирующий конструктор:
- Move semantics:
- move constructor и move assignment остаются доступны;
- можно переносить владение без копирования.
- Ясное намерение:
- по определению класса видно, что ресурс «уникален»;
- читатель сразу понимает политику non-copyable.
- Контроль на этапе компиляции:
- ошибки ловятся при компиляции, а не в runtime;
- сообщения обычно понятнее, чем у старого приёма с
privateбез определения.
Типовые случаи для non-copyable классов:
- дескрипторы файлов;
- сетевые соединения;
- объекты потоков;
- аналоги
std::unique_ptr.
Ответ: копирование запрещают через = delete, а перенос владения оставляют через move-операции.
3.4. Списки инициализации конструкторов (Лекция 4, Пример 2)
Покажите корректное использование списка инициализации: для базы и для полей.
Нажмите, чтобы увидеть решение
Ключевая идея: список инициализации выполняется до тела конструктора; так эффективнее и так обязаны инициализироваться const, ссылки и некоторые поля классовых типов.
#include <iostream>
#include <string>
using namespace std;
class Base {
protected:
int m1, m2;
public:
Base() : m1(0), m2(0) {
cout << "Base default constructor" << endl;
}
Base(int a, int b) : m1(a), m2(b) {
cout << "Base(int, int) constructor: m1=" << m1 << ", m2=" << m2 << endl;
}
};
class Derived : public Base {
private:
int md;
const int constMember;
string name;
public:
// Using initializer list to specify base constructor and initialize members
Derived(int a, int b, int d, string n)
: Base(a, b), // Initialize base class
md(d), // Initialize member
constMember(999), // Initialize const member (REQUIRED)
name(n) // Initialize string member
{
cout << "Derived constructor: md=" << md
<< ", constMember=" << constMember
<< ", name=" << name << endl;
}
void print() const {
cout << "Base: m1=" << m1 << ", m2=" << m2 << endl;
cout << "Derived: md=" << md << ", constMember=" << constMember
<< ", name=" << name << endl;
}
};
// Example showing why initializer lists are required for certain members
class RequiresInitializerList {
private:
const int constValue; // Must be initialized
int& refValue; // Must be initialized
string str; // Has default constructor but more efficient to initialize
public:
// ALL of these MUST use initializer list
RequiresInitializerList(int val, int& ref, string s)
: constValue(val), // const: must be initialized, cannot be assigned
refValue(ref), // reference: must be initialized, cannot be rebound
str(s) // more efficient than default-construct + assign
{
// This would not work:
// constValue = val; // ERROR: cannot assign to const
// refValue = ref; // ERROR: cannot rebind reference
// str = s; // Works but less efficient (default construct + assign)
}
void print() const {
cout << "constValue=" << constValue
<< ", refValue=" << refValue
<< ", str=" << str << endl;
}
};
int main() {
cout << "=== Creating Derived object ===" << endl;
Derived d(10, 20, 30, "MyObject");
d.print();
cout << "\n=== Creating RequiresInitializerList object ===" << endl;
int x = 42;
RequiresInitializerList r(100, x, "Hello");
r.print();
// Modify x to show reference works
x = 999;
cout << "\nAfter modifying x:" << endl;
r.print();
return 0;
}Вывод программы:
=== Creating Derived object ===
Base(int, int) constructor: m1=10, m2=20
Derived constructor: md=30, constMember=999, name=MyObject
Base: m1=10, m2=20
Derived: md=30, constMember=999, name=MyObject
=== Creating RequiresInitializerList object ===
constValue=100, refValue=42, str=Hello
After modifying x:
constValue=100, refValue=999, str=Hello
Разбор:
- Инициализация базы:
Base(a, b)в списке выбирает конструктор базы;- база инициализируется до полей производного класса.
- Инициализация полей:
- фактический порядок — порядок объявления полей в классе, а не порядок в списке;
- для классовых полей это обычно эффективнее, чем «дефолт + присваивание» в теле.
- Обязательные случаи:
const-поля — только инициализация;- ссылки — только привязка при инициализации;
- поля без default constructor — только через список (или in-class инициализаторы).
- Эффективность:
- список — прямое конструирование;
- тело с присваиванием — лишние шаги для классовых типов.
Типичная ошибка — перепутать порядок с порядком в списке:
class Wrong {
int b;
int a;
public:
Wrong() : a(5), b(a + 1) { } // WRONG! b is initialized before a
};Поля всегда инициализируются в порядке объявления, а не в порядке записи в списке. Здесь сначала инициализируется b, затем a, поэтому b(a + 1) использует ещё не инициализированное a — UB / мусор.
Ответ: список инициализации нужен, чтобы 1) вызвать нужный конструктор базы, 2) корректно инициализировать const/ссылки, 3) не платить лишними шагами для полей. Порядок инициализации полей — по объявлению.
3.5. Делегирующие конструкторы (Лекция 4, Пример 3)
Покажите delegating constructors, чтобы убрать дублирование общей инициализации между перегрузками конструктора.
Нажмите, чтобы увидеть решение
Ключевая идея: один конструктор вызывает другой в списке инициализации, концентрируя общую логику.
#include <iostream>
#include <string>
using namespace std;
class Rectangle {
private:
double width;
double height;
string color;
static int objectCount;
int id;
// Private helper for common initialization
void logCreation() {
cout << "Rectangle #" << id << " created: "
<< width << "x" << height << " (" << color << ")" << endl;
}
public:
// Target constructor - performs the main initialization
Rectangle(double w, double h, string c)
: width(w), height(h), color(c), id(++objectCount) {
logCreation();
}
// Delegating constructor - creates a square
Rectangle(double side)
: Rectangle(side, side, "white") { // Delegates to target constructor
cout << " (Created as square)" << endl;
}
// Delegating constructor - default color
Rectangle(double w, double h)
: Rectangle(w, h, "white") { // Delegates to target constructor
cout << " (Used default color)" << endl;
}
// Delegating constructor - default rectangle
Rectangle()
: Rectangle(1.0, 1.0, "white") { // Delegates to target constructor
cout << " (Default 1x1 rectangle)" << endl;
}
double area() const {
return width * height;
}
void print() const {
cout << "Rectangle #" << id << ": " << width << "x" << height
<< ", color=" << color << ", area=" << area() << endl;
}
};
int Rectangle::objectCount = 0;
int main() {
cout << "=== Creating rectangles with different constructors ===" << endl;
cout << "\n1. Full specification:" << endl;
Rectangle r1(5.0, 3.0, "red");
cout << "\n2. Width and height only (default color):" << endl;
Rectangle r2(4.0, 2.0);
cout << "\n3. Square (single dimension):" << endl;
Rectangle r3(3.0);
cout << "\n4. Default constructor:" << endl;
Rectangle r4;
cout << "\n=== Printing all rectangles ===" << endl;
r1.print();
r2.print();
r3.print();
r4.print();
return 0;
}Вывод программы:
=== Creating rectangles with different constructors ===
1. Full specification:
Rectangle #1 created: 5x3 (red)
2. Width and height only (default color):
Rectangle #2 created: 4x2 (white)
(Used default color)
3. Square (single dimension):
Rectangle #3 created: 3x3 (white)
(Created as square)
4. Default constructor:
Rectangle #4 created: 1x1 (white)
(Default 1x1 rectangle)
=== Printing all rectangles ===
Rectangle #1: 5x3, color=red, area=15
Rectangle #2: 4x2, color=white, area=8
Rectangle #3: 3x3, color=white, area=9
Rectangle #4: 1x1, color=white, area=1
Разбор:
- Target constructor:
Rectangle(double, double, string)держит основную инициализацию. - Delegating constructors: остальные конструкторы делегируют ему:
Rectangle(double)— квадрат (одинаковые стороны);Rectangle(double, double)— цвет по умолчанию"white";Rectangle()— дефолтные размеры и цвет.
- Порядок выполнения:
- сначала полностью отрабатывает target (включая тело);
- затем тело delegating конструктора.
- Плюсы:
- общая логика в одном месте;
- проще сопровождать;
- видна «лесенка» конструкторов.
Альтернатива без делегирования (старый стиль):
class Rectangle {
// ...
private:
void init(double w, double h, string c) {
width = w;
height = h;
color = c;
id = ++objectCount;
logCreation();
}
public:
Rectangle(double w, double h, string c) { init(w, h, c); }
Rectangle(double side) { init(side, side, "white"); }
Rectangle(double w, double h) { init(w, h, "white"); }
Rectangle() { init(1.0, 1.0, "white"); }
};Делегирующие конструкторы обычно чище и ближе к современному C++.
Ответ: в списке инициализации вызывается другой конструктор (: Имя(args)), что централизует инициализацию и убирает дублирование.
3.6. Приведения в стиле C: в чём проблема (Туториал 4, Пример 1)
Разберите, почему C-style casts плохо читаются и зачем в C++ появились именованные операторы приведения.
Нажмите, чтобы увидеть решение
Ключевая идея: одна и та же запись (T)expr может означать принципиально разные вещи — намерение теряется.
#include <iostream>
using namespace std;
class Base {
public:
virtual ~Base() { }
};
class Derived : public Base {
public:
void derivedMethod() { cout << "Derived method" << endl; }
};
int main() {
cout << "=== Problem 1: Value Conversion ===" << endl;
// Standard conversion: double → int (data loss)
int x = (int)12.34;
cout << "Converted 12.34 to int: " << x << endl;
cout << "Intent: Value conversion with rounding" << endl;
cout << "\n=== Problem 2: Pointer Reinterpretation ===" << endl;
// Reinterpretation: pointer → long (no bit modification)
int* px = &x;
long a = (long)px;
cout << "Pointer value as long: " << a << endl;
cout << "Intent: View pointer bits as integer" << endl;
cout << "\n=== Problem 3: Unsafe Upcasting ===" << endl;
// Downcasting without runtime checks
Derived* pd = new Derived();
Base* pb = (Base*)pd;
cout << "Upcasted Derived* to Base*" << endl;
cout << "Intent: Type hierarchy navigation (needs safety check)" << endl;
cout << "\n=== The Problem ===" << endl;
cout << "All three use identical syntax: (Type)expression" << endl;
cout << "But they do COMPLETELY DIFFERENT things:" << endl;
cout << " 1. Value conversion (modifies bits)" << endl;
cout << " 2. Reinterpretation (keeps bits, changes view)" << endl;
cout << " 3. Type hierarchy navigation (needs runtime check)" << endl;
cout << "\nSolution: Use specific cast operators!" << endl;
delete pb;
return 0;
}Вывод программы:
=== Problem 1: Value Conversion ===
Converted 12.34 to int: 12
Intent: Value conversion with rounding
=== Problem 2: Pointer Reinterpretation ===
Pointer value as long: 140732920755324
Intent: View pointer bits as integer
=== Problem 3: Unsafe Upcasting ===
Upcasted Derived* to Base*
Intent: Type hierarchy navigation (needs safety check)
=== The Problem ===
All three use identical syntax: (Type)expression
But they do COMPLETELY DIFFERENT things:
1. Value conversion (modifies bits)
2. Reinterpretation (keeps bits, changes view)
3. Type hierarchy navigation (needs runtime check)
Solution: Use specific cast operators!
Разбор:
- Один синтаксис — разная семантика:
(int)12.34— преобразование значения;(long)px— реинтерпретация битов указателя;(Base*)pd— шаг по иерархии типов.
- Почему это плохо:
- читатель не видит намерения;
- компилятору сложнее помогать предупреждениями;
- трудно искать «опасные» приведения в проекте;
- выше цена сопровождения.
- Что даёт C++:
static_cast<int>(12.34)— явное числовое приведение;reinterpret_cast<long>(px)— явная реинтерпретация;dynamic_cast<Derived*>(pb)— безопасная навигация по иерархии (при полиморфной базе).
Ответ: C-style cast скрывает намерение; в C++ четыре именованных приведения с более прозрачной семантикой: static_cast, dynamic_cast, const_cast, reinterpret_cast.
3.7. dynamic_cast на практике (Туториал 4, Пример 2)
Покажите безопасный downcast через dynamic_cast и проверку типа в runtime.
Нажмите, чтобы увидеть решение
Ключевая идея: dynamic_cast проверяет фактический тип; для указателя при ошибке даёт nullptr, снижая риск UB.
#include <iostream>
using namespace std;
class Base {
public:
virtual void f() { cout << "Base::f" << endl; }
virtual ~Base() { }
};
class Derived : public Base {
public:
void f() override { cout << "Derived::f" << endl; }
void derivedMethod() { cout << "Derived-specific method" << endl; }
};
int main() {
cout << "=== C-Style Cast: DANGEROUS ===" << endl;
Base* pb1 = new Base(); // Actually points to Base, not Derived
// C-style cast: NO RUNTIME CHECK
Derived* pd1 = (Derived*)pb1;
cout << "C-style cast succeeded (no checks performed)" << endl;
// pd1->derivedMethod(); // UNDEFINED BEHAVIOR! pb1 doesn't point to Derived
cout << "Calling derivedMethod() would cause undefined behavior!" << endl;
cout << "\n=== Dynamic Cast: SAFE ===" << endl;
Base* pb2 = new Base(); // Actually points to Base, not Derived
// Dynamic cast: PERFORMS RUNTIME CHECK
Derived* pd2 = dynamic_cast<Derived*>(pb2);
if (pd2 != nullptr) {
cout << "Cast succeeded" << endl;
pd2->derivedMethod();
} else {
cout << "Cast failed: pb2 doesn't point to Derived (returned nullptr)" << endl;
cout << "Safe! No undefined behavior." << endl;
}
cout << "\n=== Dynamic Cast: Success Case ===" << endl;
Base* pb3 = new Derived(); // Actually points to Derived
Derived* pd3 = dynamic_cast<Derived*>(pb3);
if (pd3 != nullptr) {
cout << "Cast succeeded: pb3 actually points to Derived" << endl;
pd3->derivedMethod();
} else {
cout << "Cast failed" << endl;
}
cout << "\n=== Comparison ===" << endl;
cout << "C-style cast: Fast but UNSAFE (no checks)" << endl;
cout << "dynamic_cast: Slower but SAFE (runtime checks)" << endl;
cout << " - Returns nullptr if cast fails (for pointers)" << endl;
cout << " - Throws bad_cast if cast fails (for references)" << endl;
cout << " - Requires at least one virtual function in base" << endl;
delete pb1;
delete pb2;
delete pb3;
return 0;
}Вывод программы:
=== C-Style Cast: DANGEROUS ===
C-style cast succeeded (no checks performed)
Calling derivedMethod() would cause undefined behavior!
=== Dynamic Cast: SAFE ===
Cast failed: pb2 doesn't point to Derived (returned nullptr)
Safe! No undefined behavior.
=== Dynamic Cast: Success Case ===
Cast succeeded: pb3 actually points to Derived
Derived-specific method
=== Comparison ===
C-style cast: Fast but UNSAFE (no checks)
dynamic_cast: Slower but SAFE (runtime checks)
- Returns nullptr if cast fails (for pointers)
- Throws bad_cast if cast fails (for references)
- Requires at least one virtual function in base
Разбор:
- Опасность C-style cast:
- «проходит» на этапе компиляции;
- нет проверки в runtime;
- легко получить невалидный указатель и UB.
- Безопасность
dynamic_cast:- сверяется с фактическим типом объекта;
- для указателя возвращает
nullptr, если тип не подходит; - помогает избежать UB при неверном downcast.
- Требования:
- полиморфная база (обычно — virtual-функции), чтобы работал RTTI;
- дороже по времени, чем «слепой»
static_cast.
- Когда уместен:
- если нет полной уверенности в runtime-типе;
- если важнее безопасность, чем микроскопическая экономия;
- в полиморфных иерархиях.
Ответ: dynamic_cast даёт проверку корректности приведения в runtime; для указателя при ошибке возвращает nullptr вместо немедленного UB.
3.8. static_cast для «известно безопасных» случаев (Туториал 4, Пример 3)
Покажите, когда оправдан static_cast для приведений на compile time.
Нажмите, чтобы увидеть решение
Ключевая идея: static_cast быстрее dynamic_cast, но требует уверенности программиста в типах.
#include <iostream>
using namespace std;
class Base {
public:
virtual void f() { cout << "Base::f" << endl; }
virtual ~Base() { }
};
class Derived : public Base {
public:
void f() override { cout << "Derived::f" << endl; }
void derivedMethod() { cout << "Derived method" << endl; }
};
int main() {
cout << "=== Use Case 1: Numeric Conversions ===" << endl;
double d = 3.14159;
int i = static_cast<int>(d); // Explicit about data loss
cout << "static_cast<int>(3.14159) = " << i << endl;
cout << "Explicit: programmer acknowledges data loss" << endl;
cout << "\n=== Use Case 2: Upcasting (Always Safe) ===" << endl;
Derived* pd = new Derived();
Base* pb = static_cast<Base*>(pd); // Derived → Base (safe)
cout << "Upcasted Derived* to Base* (always safe)" << endl;
pb->f(); // Polymorphic call
cout << "\n=== Use Case 3: Downcasting (ONLY if certain) ===" << endl;
Base* pb2 = new Derived(); // We KNOW it points to Derived
// Safe because we're certain pb2 points to Derived
Derived* pd2 = static_cast<Derived*>(pb2);
cout << "Downcasted Base* to Derived* (we're certain of type)" << endl;
pd2->derivedMethod();
cout << "\n=== Comparison with dynamic_cast ===" << endl;
cout << "static_cast: No runtime overhead, no safety checks" << endl;
cout << "dynamic_cast: Runtime overhead, but provides safety" << endl;
cout << "\nWhen to use static_cast:" << endl;
cout << " 1. You're 100% certain of the actual type" << endl;
cout << " 2. Performance is critical" << endl;
cout << " 3. Numeric conversions (explicit data loss)" << endl;
cout << "\n=== DANGER: Wrong Use ===" << endl;
Base* pb3 = new Base(); // Points to Base, NOT Derived
Derived* pd3 = static_cast<Derived*>(pb3); // WRONG! No check performed
cout << "Cast succeeded but UNSAFE - pd3 doesn't point to valid Derived" << endl;
// pd3->derivedMethod(); // UNDEFINED BEHAVIOR!
cout << "Would cause undefined behavior if we call derived methods" << endl;
delete pb;
delete pb2;
delete pb3;
return 0;
}Вывод программы:
=== Use Case 1: Numeric Conversions ===
static_cast<int>(3.14159) = 3
Explicit: programmer acknowledges data loss
=== Use Case 2: Upcasting (Always Safe) ===
Upcasted Derived* to Base* (always safe)
Derived::f
=== Use Case 3: Downcasting (ONLY if certain) ===
Downcasted Base* to Derived* (we're certain of type)
Derived method
=== Comparison with dynamic_cast ===
static_cast: No runtime overhead, no safety checks
dynamic_cast: Runtime overhead, but provides safety
When to use static_cast:
1. You're 100% certain of the actual type
2. Performance is critical
3. Numeric conversions (explicit data loss)
=== DANGER: Wrong Use ===
Cast succeeded but UNSAFE - pd3 doesn't point to valid Derived
Would cause undefined behavior if we call derived methods
Разбор:
- Плюсы
static_cast:- нет накладных расходов RTTI-проверки;
- быстрее
dynamic_castв типичных реализациях; - явно маркирует намерение «привести тип».
- Относительно безопасные применения:
- upcast по иерархии (в типичных случаях);
- числовые приведения с явным допуском потери;
- downcast, если dynamic type доказан логикой программы.
- Опасные применения:
- downcast без гарантий;
- риск UB при вызовах методов производного типа;
- нет «подушки безопасности» как у
dynamic_cast.
- Практическое правило:
- для сомнительного downcast по умолчанию —
dynamic_cast; static_cast— когда профилирование/контракты оправдывают риск;- скорость без измерений редко стоит UB.
- для сомнительного downcast по умолчанию —
Ответ: static_cast делает приведения, согласуемые на compile time, без проверок в runtime; хорош для upcast, явных числовых сужений и редких «жёстко гарантированных» downcast. Если уверенности нет — dynamic_cast.
3.9. const_cast и const-корректность (Туториал 4, Пример 4)
Покажите, когда const_cast неизбежен, а когда он опасен.
Нажмите, чтобы увидеть решение
Ключевая идея: const_cast снимает const у типа доступа; применять редко и только если гарантировано отсутствие записи в объект.
#include <iostream>
#include <cstring>
using namespace std;
// Legacy function that doesn't use const correctly
void legacyPrint(char* str) {
// This function only reads, doesn't modify
cout << "String: " << str << endl;
}
// Another legacy function (pretend it's from old C library)
void legacyProcess(char* str) {
// Actually modifies the string!
for (size_t i = 0; i < strlen(str); i++) {
str[i] = toupper(str[i]);
}
}
int main() {
cout << "=== Safe Use: Read-Only Legacy API ===" << endl;
const char* message = "Hello, World!";
// Error without const_cast:
// legacyPrint(message); // ERROR: can't pass const char* to char*
// OK with const_cast (safe because legacyPrint doesn't modify)
legacyPrint(const_cast<char*>(message));
cout << "Safe: legacyPrint only reads the string" << endl;
cout << "\n=== UNSAFE Use: Modifying Legacy API ===" << endl;
const char* constMessage = "Dangerous";
cout << "Before: " << constMessage << endl;
// DANGEROUS! legacyProcess actually modifies the string
// legacyProcess(const_cast<char*>(constMessage)); // UNDEFINED BEHAVIOR!
cout << "Cannot safely use const_cast here - legacyProcess modifies data" << endl;
cout << "Would cause undefined behavior (string literal is in read-only memory)" << endl;
cout << "\n=== Safe Alternative: Copy First ===" << endl;
char buffer[100];
strcpy(buffer, "Hello");
cout << "Before: " << buffer << endl;
const char* constPtr = buffer; // Now const
char* mutablePtr = const_cast<char*>(constPtr); // Remove const
legacyProcess(mutablePtr); // Safe: original wasn't const
cout << "After: " << buffer << endl;
cout << "\n=== Best Practice ===" << endl;
cout << "1. Avoid const_cast when possible" << endl;
cout << "2. Only use when interfacing with legacy APIs" << endl;
cout << "3. Ensure the underlying object wasn't originally const" << endl;
cout << "4. Document why const_cast is necessary" << endl;
cout << "5. Consider wrapping legacy API with const-correct interface" << endl;
return 0;
}Вывод программы:
=== Safe Use: Read-Only Legacy API ===
String: Hello, World!
Safe: legacyPrint only reads the string
=== UNSAFE Use: Modifying Legacy API ===
Before: Dangerous
Cannot safely use const_cast here - legacyProcess modifies data
Would cause undefined behavior (string literal is in read-only memory)
=== Safe Alternative: Copy First ===
Before: Hello
After: HELLO
=== Best Practice ===
1. Avoid const_cast when possible
2. Only use when interfacing with legacy APIs
3. Ensure the underlying object wasn't originally const
4. Document why const_cast is necessary
5. Consider wrapping legacy API with const-correct interface
Разбор:
- Относительно безопасно:
- снять
constради вызова «только читающей» функции с неверной сигнатурой; - если объект создан как неконстантный, а
constпоявился только на пути доступа.
- снять
- Опасно:
- снять
constсо строкового литерала и писать в память — UB; - снять
constс объекта, который изначальноconst.
- снять
- Правило:
- безопаснее, если реальный объект не
const, аconst— лишь «надпись» на ссылке/указателе; - небезопасно, если объект по смыслу неизменяемый и может жить в read-only памяти.
- безопаснее, если реальный объект не
- Почему это важно:
- компилятор может класть
const-данные в защищённые сегменты; - запись может дать падение или тихую порчу;
- оптимизации исходят из неизменяемости
const-объектов.
- компилятор может класть
Лучше, чем размазывать const_cast по коду:
// Instead of using const_cast, fix the API:
void properPrint(const char* str) { // Now const-correct
cout << "String: " << str << endl;
}
// Or create a wrapper:
void safeLegacyPrint(const char* str) {
legacyPrint(const_cast<char*>(str)); // Encapsulate the cast
}Ответ: const_cast снимает const у типа доступа. Это терпимо только при стыковке со legacy API, если функция не пишет, и если объект не был изначально «настоящим» const. Предпочтительнее поправить API.
3.10. reinterpret_cast для низкоуровневых операций (Туториал 4, Пример 5)
Покажите reinterpret_cast на примерах «битового взгляда» на память и хранения указателя как целого.
Нажмите, чтобы увидеть решение
Ключевая идея: reinterpret_cast меняет интерпретацию битов, не выполняя обычного преобразования значения — инструмент для системного уровня и максимального риска.
#include <iostream>
using namespace std;
int main() {
cout << "=== Use Case 1: Pointer ↔ Integer Conversion ===" << endl;
int x = 777;
int* p = &x;
cout << "Original pointer: " << p << endl;
cout << "Points to value: " << *p << endl;
// Store pointer as integer (e.g., for hashing, debugging)
long ptrAsInt = reinterpret_cast<long>(p);
cout << "Pointer as long: " << ptrAsInt << endl;
// Convert back to pointer
int* pBack = reinterpret_cast<int*>(ptrAsInt);
cout << "Converted back: " << pBack << endl;
cout << "Still points to: " << *pBack << endl;
cout << "\n=== Use Case 2: Type Punning (View Memory Differently) ===" << endl;
unsigned int bits = 0x3F800000; // IEEE 754 representation of 1.0f
cout << "As unsigned int: " << bits << endl;
// View these bits as a float
float* fptr = reinterpret_cast<float*>(&bits);
cout << "Same bits as float: " << *fptr << endl;
cout << "\n=== Use Case 3: Incompatible Pointer Types ===" << endl;
unsigned int ux = 777;
unsigned int* pux = &ux;
// This would be an error without cast:
// int* pix = pux; // ERROR: incompatible types
// reinterpret_cast allows it
int* pix = reinterpret_cast<int*>(pux);
cout << "unsigned* converted to int*" << endl;
cout << "Value: " << *pix << endl;
cout << "\n=== DANGER: Platform-Specific ===" << endl;
cout << "sizeof(void*) = " << sizeof(void*) << endl;
cout << "sizeof(long) = " << sizeof(long) << endl;
if (sizeof(void*) != sizeof(long)) {
cout << "WARNING: Pointer-to-long conversion may lose data!" << endl;
cout << "Use intptr_t or uintptr_t from <cstdint> instead" << endl;
} else {
cout << "OK: Pointer fits in long on this platform" << endl;
}
cout << "\n=== When to Use reinterpret_cast ===" << endl;
cout << "1. Low-level memory operations" << endl;
cout << "2. Hardware register access" << endl;
cout << "3. Binary serialization/deserialization" << endl;
cout << "4. Implementing custom memory allocators" << endl;
cout << "5. Interfacing with C APIs using void*" << endl;
cout << "\nFor application code: Almost NEVER!" << endl;
cout << "\n=== Better Alternatives ===" << endl;
cout << "• For pointer storage: use uintptr_t (from <cstdint>)" << endl;
cout << "• For type punning: use union or memcpy (safer)" << endl;
cout << "• For different pointer types: redesign to avoid need" << endl;
return 0;
}Вывод программы:
=== Use Case 1: Pointer ↔ Integer Conversion ===
Original pointer: 0x7ffeefbff5ac
Points to value: 777
Pointer as long: 140732920755372
Converted back: 0x7ffeefbff5ac
Still points to: 777
=== Use Case 2: Type Punning (View Memory Differently) ===
As unsigned int: 1065353216
Same bits as float: 1
=== Use Case 3: Incompatible Pointer Types ===
unsigned* converted to int*
Value: 777
=== DANGER: Platform-Specific ===
sizeof(void*) = 8
sizeof(long) = 8
OK: Pointer fits in long on this platform
=== When to Use reinterpret_cast ===
1. Low-level memory operations
2. Hardware register access
3. Binary serialization/deserialization
4. Implementing custom memory allocators
5. Interfacing with C APIs using void*
For application code: Almost NEVER!
=== Better Alternatives ===
• For pointer storage: use uintptr_t (from <cstdint>)
• For type punning: use union or memcpy (safer)
• For different pointer types: redesign to avoid need
Разбор:
- Что делает:
- трактует тот же битовый образ как другой тип;
- это не «обычное» значение-преобразование, а смена взгляда компилятора;
- почти полностью обходит статическую типобезопасность.
- Типовые применения:
- хранение указателя в целом и обратно;
- type punning (осторожно: strict aliasing);
- приведения между «несовместимыми» указателями.
- Риски:
- зависимость от платформы;
- выравнивание и размеры;
- легко получить UB;
- ломаются инварианты типовой системы.
- Почему это опасно:
- возможны нарушения strict aliasing;
- невыровненный доступ;
- порядок байт;
- неверные предположения о размерах.
Более безопасные альтернативы:
#include <cstdint>
// Instead of reinterpret_cast<long>(ptr):
uintptr_t ptrAsInt = reinterpret_cast<uintptr_t>(ptr); // Guaranteed to fit
// Instead of reinterpret_cast for type punning:
union FloatInt {
float f;
uint32_t i;
};
FloatInt fi;
fi.i = 0x3F800000;
float value = fi.f; // Safer than reinterpret_castОтвет: reinterpret_cast — для низкоуровневых задач (ОС, железо, бинарные форматы). В прикладном коде чаще уместнее uintptr_t, аккуратные union/memcpy и перепроектирование типов.
3.11. typeid для идентификации типа (Туториал 4, Пример 6)
Покажите typeid для проверки типа в runtime и сравнения типов в полиморфной иерархии.
Нажмите, чтобы увидеть решение
Ключевая идея: typeid даёт сведения о типе в runtime; полезно для отладки и редкого dispatch, но не замена нормальному полиморфизму.
#include <iostream>
#include <typeinfo>
using namespace std;
class Animal {
public:
virtual ~Animal() { } // Virtual destructor (required for RTTI)
};
class Dog : public Animal {
public:
void bark() { cout << "Woof!" << endl; }
};
class Cat : public Animal {
public:
void meow() { cout << "Meow!" << endl; }
};
void identifyAnimal(Animal* a) {
cout << "Type: " << typeid(*a).name() << endl;
// Compare with specific types
if (typeid(*a) == typeid(Dog)) {
cout << "This is a Dog" << endl;
Dog* d = static_cast<Dog*>(a); // Safe because we checked
d->bark();
} else if (typeid(*a) == typeid(Cat)) {
cout << "This is a Cat" << endl;
Cat* c = static_cast<Cat*>(a); // Safe because we checked
c->meow();
} else if (typeid(*a) == typeid(Animal)) {
cout << "This is a generic Animal" << endl;
}
}
int main() {
Dog dog;
Cat cat;
Animal animal;
Animal* ptr;
// Test with different objects
cout << "=== Testing with Dog ===" << endl;
ptr = &dog;
identifyAnimal(ptr);
cout << "\n=== Testing with Cat ===" << endl;
ptr = &cat;
identifyAnimal(ptr);
cout << "\n=== Testing with Animal ===" << endl;
ptr = &animal;
identifyAnimal(ptr);
// Demonstrating difference between static and dynamic type
cout << "\n=== Static vs Dynamic Type ===" << endl;
Animal* ptrToDog = new Dog();
cout << "Static type (pointer): " << typeid(ptrToDog).name() << endl;
cout << "Dynamic type (object): " << typeid(*ptrToDog).name() << endl;
// Can use this for conditional behavior
if (typeid(*ptrToDog) == typeid(Dog)) {
cout << "Confirmed: pointer points to a Dog" << endl;
}
delete ptrToDog;
return 0;
}Возможный вывод программы:
=== Testing with Dog ===
Type: 3Dog
This is a Dog
Woof!
=== Testing with Cat ===
Type: 3Cat
This is a Cat
Meow!
=== Testing with Animal ===
Type: 6Animal
This is a generic Animal
=== Static vs Dynamic Type ===
Static type (pointer): P6Animal
Dynamic type (object): 3Dog
Confirmed: pointer points to a Dog
Примечание: точный текст type_info::name() implementation-defined и на разных компиляторах выглядит по-разному (например, Dog, class Dog, mangled-имя).
Разбор:
- Базовый приём:
typeid(*ptr)отражает dynamic type объекта. - Сравнение:
typeid(*ptr) == typeid(Dog)проверяет «является ли объект именноDog». - Статика vs динамика:
typeid(ptr)— тип выраженияptr(здесьAnimal*);typeid(*ptr)— тип объекта в памяти (например,Dog).
- Практика: можно распознать тип и затем сузить указатель (часто вслед за проверкой используют
static_cast, если логика эквивалентна проверке).
Альтернатива через dynamic_cast (часто предпочтительнее):
void identifyAnimal(Animal* a) {
if (Dog* d = dynamic_cast<Dog*>(a)) {
cout << "This is a Dog" << endl;
d->bark();
} else if (Cat* c = dynamic_cast<Cat*>(a)) {
cout << "This is a Cat" << endl;
c->meow();
}
}Ответ: typeid возвращает const std::type_info& для проверок типа в runtime; typeid(*ptr) — про объект, typeid(ptr) — про сам указатель/ссылку как значение. Сравнение — через == у type_info.