Любую программу можно написать различными способами, и она будет как-то работать, иногда даже правильно. Но лучше все-таки избегать в своем коде определенные конструкции, чтобы не возникало последующих проблем. Эта статья предназначена для начинающих разработчиков на 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 помогает выстроить именно эту базу: от первых принципов и аккуратной работы с памятью до навыков, необходимых для первых рабочих задач и первых собеседований.

Комментарии (10)


  1. OlegZH
    21.11.2025 14:48

    В этой статье мы рассмотрели несколько наиболее распространенных ошибок начинающих разработчиков С++. 

    Лично мне было бы чрезвычайно интересно посмотреть на код настоящих профессионалов и послушать их истории про то, что им им что-то помогло, а что-то не очень.


    1. NickDoom
      21.11.2025 14:48

      Ну я вот я скептически отношусь к критике глобальных переменных, потому что попытка написать «приложение для всего» в любом случае превращается в слабоотлаживаемую кашу. А если разбить его на кучу отдельных бинарников, связанных общим фреймворком (самописным, под данный круг задач), и каждому назначить одну производственно-диагностическую операцию (Рюриковичи приборостроители мы) — глобалы в таких плагинах вполне уместны. Он сам, по сути, функция :) и его глобалы не сказать чтоб очень глобальны…

      Но если архитектура не сложилась и её нельзя привести к чему-то стройному, то это тоже превращается в кашу, а фреймворк — в надстройку над компилятором, не делающую ничего полезного и просто лишний раз запутывающую взаимодействие. В это я тоже влипал :)


  1. Abstraction
    21.11.2025 14:48

    Бледно.

    Об именах переменных: не используйте что-либо вне ASCII-набора, даже если среда это позволяет (потому что нынче она, к сожалению, иногда позволяет). Отладка путаницы между int cap и int сар согреет вашу пятую точку как мало что другое.

    Обращайте внимание на предупреждение "name XXX hides outer variable". C++ позволяет дублирование имён во вложенных блоках:
    int a = 2;
    for(int b = 0; b < a; ) {
    int a = 17;
    //...
    --a;
    }

    - но это почти никогда не идёт на пользу читаемости программы.

    Если вы таки-используете глобальные переменные, называйте их уникальным образом, чтобы при чтении было очень хорошо видно, что вот здесь модифицируется глобальная переменная.
    Аналогичный совет относится к статическим переменным в функциях/методах и данным-членам классов.

    Разделяйте имена переменных и функций (скажем, одни с маленькой буквы, другие с большой), потому что в C++ они умеют конфликтовать.

    Либо не используйте макросы вообще, либо давайте им визуально отличные от всего остального имена (обычное соглашение - ИСПОЛЬЗОВАТЬ_КАПС). Имя макроса может быть распознано в совершенно любом контексте и результат способен порождать очень озадачивающие сообщения компилятора.
    (Да, старайтесь не включать windows.h по крайней мере в заголовочных файлах, эта падла определяет тонну макросов, иногда с очень неудачными именами.)

    Называйте переменные и функции по тому, что они представляют, а не потому, как они реализованы:
    bool IsLessThanMinusTen(int number) {return number <= -10;} // плохо
    bool IsLethalHP(int hp) {return hp <= -10;} // лучше

    Хотя имена могут быть слишком длинными, на современных мониторах "строка не влезает в экран" - отчётливый признак того, что вы делаете что-то кошмарно не то. Найдите в настройках среды визуализацию вертикальной линеечки и выставьте её на какую-нибудь разумную ширину. Есть люди которые рекомендуют 80, на мой вкус это бывает слишком коротко, я предпочитаю 120 (это удачно совпадает со строкой, которая не переносится в интерфейсе нашего gitlab при сравнении side-by-side).


    1. NickDoom
      21.11.2025 14:48

      Называйте переменные и функции по тому, что они представляют

      Воистину. Вот это прямо в точку. КАК функция делает своё дело — по ней самой понятно. А название должно отвечать на вопрос, накукуя она вообще это делает :-D


    1. fire64
      21.11.2025 14:48

      Стиль написания вещь на которую многие увы забивают.

      Для себя всегда пишу так:

      #define MACROS - заглавными буквами.

      Int g_nGlobalValue - в начале пишу g потом нижнее подчёркивание, потом тип данных условно n - number, s - string, p - pointer и т.д. и потом название того, что она в себе содержит, при этом каждое слово начиная с большой буквы.

      Члены классов или структур оформляю похоже.

      m_nMemberValue.

      Считаю, что это красиво, удобно и читаемо.


  1. savostin
    21.11.2025 14:48

    При чем тут C++...


    1. Playa
      21.11.2025 14:48

      Вы что, cout не заметили? /s


  1. goldexer
    21.11.2025 14:48

    Статья ни-о-чем. По сути то, что должны знать даже самые наизеленейшие новички. И в материале о С++ ничего нет. Хотелось бы конкретных примеров из реальных проектов, как вот PVSку развивают и освещают занятные случаи или по игровым движкам - узкие места, утечки памяти, проблемы указателей, может что-то из эмбеда. Статей для новичков пруд пруди. А вот собрать с десяток С++ курьёзов с разбором, вот это бы народ с удовольствием почитал.


  1. WASD1
    21.11.2025 14:48

    Про имена переменных:

    1. Чем идеоматичнее часть, которую вы пишете - тем короче имена переменных, чем "задаче-специфичнее" код, тем длиннее
    2. Чем локальнее время жизни переменной - тем короче имя (т.к. один раз прочитав g /* guess */ в течении 5 минут читающий способен это помнить.

    Дальше статью читать не стал. Если она настолько поверхностна - не стоит тратить на неё своё время.


    1. NickDoom
      21.11.2025 14:48

      Полностью согласен по первому пункту, счётчики по жизни i j k, минимум-максимум внутри небольшой функции часто m и M… но только внутри небольшой. Это прямо по второму пункту в десяточку. По третьему — не согласен, такие статьи часто порождают ценные камменты типа Вашего :) А первые два — всё так.