Введение: почему C++ — это сложно, но интересно
C++ — один из самых мощных и производительных языков программирования. Но с большой силой приходит и большая ответственность. Начинающие разработчики часто сталкиваются с ошибками, которые могут привести к падению программы, утечкам памяти и неопределённому поведению. В этой статье мы разберём 10 самых распространённых ошибок новичков в C++ — от А до Я. Вы узнаете, как их распознавать, исправлять и, главное, не допускать в будущем. Материал будет полезен как студентам, так и тем, кто только начинает свой путь в системном программировании.
1. Игнорирование неопределённого поведения (UB)
Одна из самых коварных ошибок — это неопределённое поведение (Undefined Behavior, UB). Компилятор не выдаст предупреждение, программа может работать «вроде бы нормально», но при смене оптимизаций или платформы всё рухнет.
Пример: выход за границы массива
int arr[5] = {1, 2, 3, 4, 5};
arr[5] = 42; // UB: выход за границы
std::cout << arr[10]; // UB: чтение мусора
Как избежать: Всегда используйте std::array или std::vector с проверкой через .at(), а также контейнеры STL. Никогда не полагайтесь на «везение» — UB может проявиться в самый неподходящий момент.
2. Утечки памяти и сырые указатели
В C++ нет сборщика мусора. Каждый new должен иметь парный delete. Новички часто забывают освобождать память, особенно при исключениях.
Пример утечки
void leak() {
int* p = new int(100);
// забыли delete p;
// при выходе из функции память потеряна
}
Решение: Используйте умные указатели: std::unique_ptr для единоличного владения и std::shared_ptr для разделяемого. Они автоматически освободят память при выходе из области видимости.
void no_leak() {
auto p = std::make_unique<int>(100);
// delete вызывается автоматически
}
3. Неправильное использование ссылок и указателей
Путаница между ссылками (&) и указателями (*) — частая беда.
- Ссылка — это псевдоним. Она не может быть
nullptrи не переназначается. - Указатель может быть нулевым и может указывать на разные объекты.
Типичная ошибка: возврат ссылки на локальный объект
int& bad_ref() {
int x = 10;
return x; // x уничтожится после выхода
}
// использование: int& r = bad_ref(); // висячая ссылка
Правило: Никогда не возвращайте ссылки или указатели на локальные переменные. Только на данные, время жизни которых гарантировано (например, переданные по ссылке аргументы или статические переменные).
4. Пренебрежение RAII (Resource Acquisition Is Initialization)
RAII — это ключевая идиома C++. Ресурс (память, файл, мьютекс) захватывается в конструкторе и освобождается в деструкторе. Новички часто пишут ручное управление, что приводит к утечкам при исключениях.
Плохой пример
void handle_file() {
FILE* f = fopen("data.txt", "r");
// ... работа с файлом ...
if (some_error) {
return; // забыли fclose!
}
fclose(f);
}
Хороший пример с RAII
void handle_file_safe() {
std::ifstream f("data.txt");
if (!f) return;
// ... работа ...
// файл автоматически закроется при выходе из scope
}
Вывод: Всегда оборачивайте ресурсы в RAII-обёртки. Для файлов — std::fstream, для памяти — умные указатели, для мьютексов — std::lock_guard.
5. Игнорирование const и constexpr
Многие начинающие не используют const там, где это возможно. Это ухудшает читаемость и лишает компилятор возможности оптимизации.
// Плохо: параметр можно изменить случайно
void print_vector(std::vector<int>& v) {
for (int x : v) std::cout << x;
}
// Хорошо: обещаем не менять
void print_vector(const std::vector<int>& v) {
for (int x : v) std::cout << x;
}
Также используйте constexpr для вычислений на этапе компиляции — это ускорит программу.
6. Злоупотребление макросами вместо constexpr/inline
Старая школа: #define PI 3.14. Современный C++ предлагает более безопасные альтернативы.
// Вместо макроса
constexpr double PI = 3.1415926535;
// Вместо макроса-функции
inline int square(int x) { return x * x; }
Почему макросы плохи: они не имеют области видимости, не участвуют в проверке типов и могут привести к трудноуловимым ошибкам.
7. Неправильное копирование и присваивание
Если вы пишете свой класс с динамической памятью, нужно следовать правилу трёх (или пяти в C++11). Иначе — двойное освобождение или утечка.
Пример ошибки: поверхностное копирование
class String {
char* data;
public:
String(const char* str) {
data = new char[strlen(str) + 1];
strcpy(data, str);
}
// Нет конструктора копирования и оператора присваивания!
~String() { delete[] data; }
};
void foo() {
String s1("hello");
String s2 = s1; // копия указателя, двойной delete в деструкторах
}
Решение: Либо запретите копирование (= delete), либо реализуйте глубокое копирование. Лучше всего — используйте