Любую программу можно написать различными способами, и она будет как-то работать, иногда даже правильно. Но лучше все-таки избегать в своем коде определенные конструкции, чтобы не возникало последующих проблем. Эта статья предназначена для начинающих разработчиков на C++, и в ней мы рассмотрим несколько вредных советов по написанию кода и пояснений почему так делать не нужно.
О коротких именах переменных
Стиль именования переменных является важной частью написания кода на любом языке, в том числе и С++. На просторах сети можно встретить рекомендации по использованию коротких имен переменных, например состоящих из одной или двух букв. Объясняется это тем, что таким образом можно уместить более сложное выражение в одну строку на экране и код будет более читаемым.
Да, здесь есть определенная логика, так как чем короче имена переменных, тем проще выражения с ними уместить в одну строку (хотя эту проблему можно решить, используя экран большего размера). Но, имя переменной состоящее из одной или двух букв вряд ли будет информативно.
Ниже приведен пример простой программы Угадай число на C++. В ней есть две переменные g (сокращение от guess) и n (number).
#include <iostream> #include <cstdlib> #include <ctime> int main() { std::srand(std::time(0)); int n = std::rand() % 100 + 1; int g = 0; std::cout << "Угадайте число от 1 до 100: "; while (g != n) { std::cin >> g; if (g < n) { std::cout << "Слишком мало! Попробуйте снова: "; } else if (g > n) { std::cout << "Слишком много! Попробуйте снова: "; } else { std::cout << "Поздравляем! Вы угадали число!" << std::endl; } } return 0; }
В процессе написания своего кода разработчик конечно будет помнить, что означает каждая из этих переменных, но если по прошествии нескольких месяцев ему или другому программисту необходимо будет изменить этот код, он должен будет потратить дополнительное время на то, чтобы разобраться, что значит каждая из переменных. Еще хуже, если разработчик не полностью разберется в работе алгоритма из-за непонятных имен переменных и в результате допустит ошибку в коде.
Так что, для нашего примера с игрой лучше все таки назвать переменные guess и number, как показано в примере ниже.
#include <iostream> #include <cstdlib> #include <ctime> int main() { std::srand(std::time(0)); int number = std::rand() % 100 + 1; int guess = 0; std::cout << "Угадайте число от 1 до 100: "; while (guess != number) { std::cin >> guess; if (guess < number) { std::cout << "Слишком мало! Попробуйте снова: "; } else if (guess > number) { std::cout << "Слишком много! Попробуйте снова: "; } else { std::cout << "Поздравляем! Вы угадали число!" << std::endl; } } return 0; }
Но всегда ли стоит ли всегда избегать коротких имён переменных? На самом деле счетчики в небольших циклах можно называть как i, j, k. Это распространённая практика, и любой разработчик поймет код с такими названиями.
for (int i = 0; i < 5; i++) { cout << "Cycle: " << i << endl; }
Таким образом, иногда короткие имена переменных уместны, но лучше их делать осмысленными.
Магические числа
Вообще, жестко “приколачивать” значения в коде - это очень плохая практика, которой тем не менее грешат многие начинающие программисты. Например, в вузе все мы писали различные математические программы для выполнения различных вычислений. Конечно, когда вся программа состоит из различных формул, тогда конструкции вида:
gr = ts / 65 – 9.81 * s
являются нормой. И здесь для понимания того, что делается в той или иной строке проще всего воспользоваться комментариями.
Но если ваш код состоит не только из математических формул, то конструкции с различными числовыми значениями (их еще называют магическими числами) могут вызвать сложности при чтении и модификации кода. Например, не всегда понятно почему в этой формуле используются именно эти числа. Да, каждую формулу можно и нужно сопроводить комментарием, но лучше всего создать константы, в которых указать все эти магические числа. Тогда, в самих формулах у вас будут использоваться только константы, что позволит сделать код более читаемым.
И кроме этого, в случае, если вам нужно будет изменить какое-либо из этих магических значений, вам достаточно будет просто поменять значение в константе, а не править все формулы в коде.
Int не всегда хорош
Было время, когда большинство приложений было 32 битными… В те далекие времена в книгах по C++ рекомендовалось использовать переменные типа int для хранения размеров массивов и построения циклов. Долгое время на многих распространённых платформах, где использовался язык C++, массив не мог содержать более INT_MAX элементов.
Например, 32-битная программа для Windows имеет ограничение памяти в 2 ГБ (на самом деле, даже меньше). Таким образом, 32-битного типа int было более чем достаточно для хранения размеров массивов или индексации массивов.
Однако на самом деле размера таких типов, как int, unsigned и даже long, может быть недостаточно. В связи с этим программисты, использующие Linux, могут задаться вопросом: почему размера long недостаточно? И вот почему: например, для сборки приложения для платформы Windows x64 компилятор MSVC использует модель данных LLP64. В этой модели длинный тип остается 32-битным.

Итак, какие типы следует использовать? Memsize-типы, такие как ptrdiff_t, size_t, intptr_t, uintptr_t, безопасны для хранения индексов или размеров массивов.
Давайте рассмотрим простой пример кода, где 32-битный счётчик, используемый для обработки большого массива в 64-битном приложении, приводит к ошибке.
std::vector<char> &bigArray = get(); size_t n = bigArray.size(); for (int i = 0; i < n; i++) bigArray[i] = 0;
Если в контейнере больше INT_MAX элементов, знаковая переменная int переполняется, что приводит к неопределённому поведению. Более того, никогда не знаешь, где это неопределённое поведение проявится.
Вот один из примеров правильного кода:
size_t n = bigArray.size(); for (size_t i = 0; i < n; i++) bigArray[i] = 0;
Или такой, улучшенный вариант:
std::vector<char>::size_type n = bigArray.size(); for (std::vector<char>::size_type i = 0; i < n; i++) bigArray[i] = 0;
Очень глобальные переменные
При написании достаточно больших программ у многих начинающих разработчиков возникает соблазн везде где только можно использовать глобальные переменные. Действительно, ведь к ним можно получать доступ из любого места в коде.
Однако, наличие такого глобального доступа приводит к излишнему запутыванию логики кода, когда непонятно, где и для чего они используются. Эта путаница приводит к ошибкам, которые сложно обнаружить при отладке. Кроме того, сложно проверять функции, использующие глобальные переменные, с помощью модульных тестов, поскольку разные функции связаны друг с другом.
Например, возможно возникновение ситуации «затенения глобальной переменной», когда объявляется локальная переменная с тем же именем, что и у глобальной. Это может привести к неожиданному поведению программы, поскольку вы можете подумать, что изменяете глобальную переменную, хотя на самом деле работаете не с ней. К счастью, современные компиляторы могут выдавать предупреждение об этом, но если это предупреждение отключено, и затенение происходит незаметно, что приводит к ошибочному поведению приложения.
Также, невозможно предотвратить создание псевдонима глобальной переменной, например, передачу указателя на глобальную переменную в качестве входных данных функции. Если глобальные переменные считаются вредоносными, то указатели на глобальные переменные — чистое зло. Они затрудняют отслеживание того, что на самом деле происходит с переменной — вы можете искать все случаи записи в глобальную переменную, совершенно упуская из виду тот факт, что существуют функции, изменяющие переменную через указатель. Это ещё больше затрудняет отслеживание изменений глобальной переменной в нашей программе.
В многопоточных системах глобальные переменные приводят к состоянию гонки. Методы синхронизации (такие как блокировки и критические секции) становятся обязательными, но ими слишком часто пренебрегают. Когда доступ к глобальным переменным распределён по всей системе (а не ограничен одним модулем), легко некорректно защитить переменную или пропустить место её обновления – нет гарантии, что синхронизация будет использоваться в каждом месте применения, за исключением дисциплины разработчика (которая может быть непредсказуемой).
Само наличие глобальных переменных в достаточном количестве может фактически ограничить программу однопоточной из-за необходимости надлежащей защиты чтения/записи каждой переменной.
Ну и в завершении темы вреда бездумного использования глобальных переменных. В C++ порядок инициализации глобальных и статических файловых переменных является распространённым источником проблем в системе. Стандарт гласит, что глобальные переменные в одном исходном файле создаются и инициализируются в порядке их появления в файле. Однако глобальный порядок во всех исходных файлах не гарантируется. Когда глобальный объект в одном файле ссылается на глобальный объект в другом файле, порядок инициализации может быть не таким, как ожидалось, что приводит к сбою. Эти сбои могут меняться в зависимости от того, как связаны объектные файлы в сборке, что приводит к ложным проблемам, которые появляются и исчезают со временем.
Аргументы командной строки и не только
Аргументы командной строки как классика уязвимостей переполнения буфера. Разработчик не должен бездумно доверять пользовательскому вводу.
Например, конструкция вида:
char buf[100]; strcpy(buf, argv[1]);
однозначно уязвима для атак переполнения буфера, так как если мы передадим в качестве аргументы более 100 байт программа затрет служебную область памяти. И на самом деле здесь речь не только в возможном переполнении буфера. Обработка данных без предварительной проверки открывает ящик Пандоры, полный уязвимостей. Так что не ленитесь проверять те данные, которые вы получаете от пользователя. Это касается не только аргументов командной строки, но и других способов получения данных от пользователя.
Заключение
В этой статье мы рассмотрели несколько наиболее распространенных ошибок начинающих разработчиков С++. Однако, это далеко не исчерпывающий список и, возможно мы еще вернемся к этой теме, в последующих статьях о разработке на C++. Пишите код хорошо, плохо получится само :)
Если после разбора анти-паттернов хочется собрать более устойчивый фундамент, можно пойти дальше — в сторону системного освоения языка. Курс C++ Developer. Basic помогает выстроить именно эту базу: от первых принципов и аккуратной работы с памятью до навыков, необходимых для первых рабочих задач и первых собеседований.
