Введение
В заключительной статье 50-статейного цикла синтаксиса C/C++ собираем итоговый, практический пример — разбираем фрагмент намеренно «плохо» написанного, но реалистично выглядящего C++ кода, и последовательно рефакторим его, применяя знания, накопленные на протяжении всего цикла: от базового синтаксиса через память, ООП, шаблоны, и идиомы современного C++.
Концепция
Итоговый рефакторинг демонстрирует совместное применение множества отдельных тем цикла к единому, целостному примеру — переход от «сырых» указателей и ручного управления памятью (статья 524) к умным указателям (статья 547) и RAII (статья 552); от нарушения правила пяти (статья 553) к его соблюдению через делегирование стандартным RAII-классам (правило нуля); от неконтролируемых исключений и отсутствия const-корректности (статьи 530, 540) к их продуманному, согласованному использованию; и от устаревшего, многословного синтаксиса к современным удобствам (auto, диапазонные циклы, структурированные привязки, статьи 544/550).
Пример кода: Итоговый мини-проект
// ИСХОДНЫЙ, "плохой" код — содержит МНОЖЕСТВО проблем, разобранных на протяжении всего цикла
class data_holder // нарушение стиля именования (статья 559), отсутствие явного назначения класса
{
public:
data_holder(int s) { size = s; data = (int*)malloc(s * sizeof(int)); } // malloc в C++ (статья 524)!
~data_holder() { free(data); } // free вместо delete[] — несогласовано с malloc, но технически совместимо
// ЗАБЫТО правило пяти (статья 553) — компилятор сгенерирует НЕКОРРЕКТНОЕ копирование!
int get(int i) { return data[i]; } // НЕТ проверки границ (статья 518), НЕТ const (статья 530)
void set(int i, int v) { data[i] = v; } // та же проблема отсутствия проверки
int *data; // ПУБЛИЧНОЕ поле — нарушение инкапсуляции (статья 531)
int size;
};
void process(data_holder *holder) // указатель там, где более уместна ссылка (статья 517)
{
for (int i = 0; i <= holder->size; i++) { // ОШИБКА НА ГРАНИЦЕ (off-by-one) — выход за границы!
holder->data[i] = i * 2;
}
}
// РЕФАКТОРЕННЫЙ код — применение знаний всего цикла
#include <vector>
#include <stdexcept>
class DataHolder // явное, согласованное именование (статья 559)
{
public:
explicit DataHolder(size_t size) : m_data(size) {} // std::vector — правило НУЛЯ (статья 553),
// память управляется автоматически (статья 552)
// НЕ требуется явный деструктор/копирование/перемещение — std::vector обрабатывает ВСЁ корректно
int get(size_t index) const // const-метод (статья 530), size_t для индекса
{
return m_data.at(index); // .at() выполняет ПРОВЕРКУ границ, выбрасывая исключение (статья 540)
}
void set(size_t index, int value)
{
m_data.at(index) = value; // та же безопасная, проверяемая индексация
}
size_t size() const { return m_data.size(); } // явный, инкапсулированный метод вместо публичного поля
private:
std::vector<int> m_data; // ПРИВАТНОЕ поле — инкапсуляция (статья 531)
};
void process(DataHolder &holder) // ССЫЛКА вместо указателя (статья 517) — holder гарантированно не null
{
for (size_t i = 0; i < holder.size(); ++i) { // КОРРЕКТНОЕ условие — строго МЕНЬШЕ, без off-by-one
holder.set(i, static_cast<int>(i) * 2); // явный static_cast (статья 556) вместо неявного преобразования
}
}
Пояснения к коду
Исходный data_holder концентрирует сразу несколько классических проблем, разобранных в цикле — смешение malloc/free (стиль C) с философией C++ классов; полное нарушение правила пяти (компилятор сгенерировал бы поверхностное копирование указателя data, приводящее к двойному освобождению при копировании объекта, статья 553); публичное поле data, разрушающее инкапсуляцию; отсутствие проверки границ в get/set; и классическая ошибка off-by-one в process (i <= holder->size вместо корректного i < holder->size), приводящая к выходу за границы массива. Рефакторенный DataHolder систематически устраняет каждую из этих проблем — делегирование управления памятью std::vector (правило нуля) автоматически решает проблему правила пяти; .at() вместо прямой индексации обеспечивает проверку границ с понятным исключением при нарушении; приватное поле с явными, инкапсулированными методами доступа восстанавливает контроль над инвариантами класса; и исправленное условие цикла (i < holder.size()) устраняет исходную ошибку off-by-one, дополнительно используя ссылку вместо указателя в сигнатуре process, что устраняет необходимость в проверке на nullptr, поскольку ссылка по своей природе не может быть нулевой (статья 517).
Подводные камни
- Рефакторинг существующего, работающего (хотя и проблемного) кода без достаточного покрытия тестами (основной цикл статей про тестирование) перед началом изменений — без тестов, фиксирующих текущее, даже если несовершенное поведение кода до рефакторинга, легко непреднамеренно изменить наблюдаемое поведение системы в процессе «улучшения» внутренней реализации, и для реального, production-кода (в отличие от этого учебного, иллюстративного примера) написание тестов, фиксирующих ожидаемое поведение, должно обычно предшествовать существенному рефакторингу, а не выполняться после него или вовсе игнорироваться.
- Попытка рефакторить весь проблемный код «целиком и сразу» вместо постепенного, поэтапного улучшения — для реальных, больших и давно существующих кодовых баз с множеством подобных проблем (а не для одного небольшого, иллюстративного примера, как в этой статье) попытка единовременного, всеобъемлющего рефакторинга существенно увеличивает риск внесения новых ошибок и затрудняет code review (статья 444 DevOps-цикла) такого крупного, многоаспектного изменения — постепенный, разбитый на логические, проверяемые шаги рефакторинг (с явным фиксированием и тестированием на каждом промежуточном этапе) значительно безопаснее.
- Применение всех изученных техник цикла механически, без понимания, действительно ли каждая из них нужна именно для конкретного, рассматриваемого фрагмента кода — аналогично общему принципу, неоднократно подчёркивавшемуся на протяжении всего плана статей (в частности, в итоговых статьях предыдущих циклов), осознанное, целенаправленное применение конкретных техник именно там, где они решают реальную, присутствующую в коде проблему, остаётся центральным принципом качественного, не избыточно усложнённого рефакторинга, а не бездумное, механическое применение «максимума всех изученных приёмов» без разбора их реальной применимости к конкретной ситуации.
- Недостаточное понимание того, что даже после столь основательного рефакторинга, разобранного в этой статье, итоговый код всё ещё может содержать менее очевидные, более тонкие проблемы, не покрытые конкретно разобранными в этой статье изменениями (специфичные вопросы производительности, потокобезопасности при использовании из нескольких потоков, более сложные сценарии использования, не отражённые в этом упрощённом, учебном примере) — данный 50-статейный цикл синтаксиса C/C++ дал основательную, систематическую базу знаний, но реальное, профессиональное мастерство написания качественного C++ кода продолжает развиваться через практический опыт, code review, изучение конкретных, специфичных областей применения (как уже разобранные в предыдущих частях общего плана embedded-разработка, сетевое программирование, многопоточность) far за пределами одного, даже весьма обширного цикла статей.
Углублённый цикл синтаксиса C/C++ (50 статей, 511-560) полностью завершён — пять блоков провели систематический путь от базового синтаксиса C (типы, операторы, управляющие конструкции, функции, указатели, строки) через структуры данных и память (структуры, объединения, перечисления, динамическая память, препроцессор, линковка), объектно-ориентированное программирование C++ (классы, наследование, полиморфизм, перегрузка операторов, исключения), шаблоны и современный C++ (обобщённое программирование, move-семантика, умные указатели, constexpr, concepts), до идиом, стандартов и практики (RAII, правило пяти/нуля, неопределённое поведение, касты типов, стиль кода). Общий план статей теперь включает 560 тем, давая исчерпывающую, систематическую основу для понимания самого языка C/C++ в дополнение ко всем ранее разобранным прикладным направлениям — desktop/embedded разработке на Qt, сетям, базам данных, DevOps, алгоритмам, мультимедиа, QML, и Python-биндингам.