Форум программистов, компьютерный форум, киберфорум
el_programmer
Войти
Регистрация
Восстановить пароль
Блоги Сообщество Поиск  
PVS-Studio - это инструмент для выявления ошибок в исходном коде программ, написанных на языках С, C++ и C#.

PVS-Studio выполняет статический анализ кода и генерирует отчёт, помогающий программисту находить и устранять ошибки. PVS-Studio выполняет широкий спектр проверок кода, но наиболее силён в поисках опечаток и последствий неудачного Copy-Paste. Показательные примеры таких ошибок: V501, V517, V522, V523, V3001.

Анализатор ориентирован на разработчиков, использующих среду Visual Studio, и может в фоновом режиме выполнять анализ измененных файлов после их компиляции. В идеале ошибки будут обнаружены и исправлены ещё до попадания в репозиторий. Однако ничто не мешает использовать анализатор для проверки всего решения целиком или для встраивания в системы непрерывной интеграции. Эти и иные способы использования анализатора описаны в документации.

Главный вопрос программирования, рефакторинга и всего такого. Часть 1

Запись от el_programmer размещена 29.04.2016 в 14:42
Показов 2538 Комментарии 0

Автор: Андрей Карпов

Часть 2: https://www.cyberforum.ru/blog... g4222.html
Часть 3: https://www.cyberforum.ru/blog... g4223.html
Часть 4: https://www.cyberforum.ru/blog... g4224.html
Полная версия в ПДФ формате: https://yadi.sk/i/LKkWupFjr5WzR
Полная версия в ПДФ формате английский вариант: https://yadi.sk/i/zKHIOS84r87nk

Содержание

Предисловие
1. Не берите на себя работу компилятора
2. Больше 0, это не 1
3. Один раз скопируй, несколько раз проверь
4. Бойтесь оператора ?: и заключайте его в круглые скобки
5. Используйте доступные инструменты для проверки кода
6. Проверьте все места, где указатель явно приводится к целочисленным типам
7. Не вызывайте функцию alloca() внутри циклов
8. Помните, что исключение в деструкторе - это опасно
9. Используйте для обозначения терминального нуля литерал '\0'
10. Старайтесь "не мельчить" при использовании #ifdef
11. Не жадничайте на строчках кода






Вы угадали, ответ - "42". Здесь приводится 42 рекомендации по программированию, которые помогут избежать множества ошибок, сэкономить время и нервы. Автором рекомендаций выступает Андрей Карпов - технический директор компании "СиПроВер", разрабатывающей статический анализатор кода PVS-Studio. За свою практику он насмотрелся на огромное количество способов отстрелить себе ногу; ему явно есть, о чем поведать читателю. Каждая рекомендация сопровождается практическим примером, что подтверждает актуальность поднятого вопроса. Советы ориентированы на C/C++ программистов, но часто они универсальны и будут интересны разработчикам, использующим и другие языки.


Нажмите на изображение для увеличения
Название: image1.png
Просмотров: 836
Размер:	59.6 Кб
ID:	3764


Предисловие

Здравствуйте. Меня зовут Андрей Карпов. Сфера моих интересов - язык C/C++ и продвижение методологии статического анализа кода. На протяжении пяти лет я являюсь Microsoft MVP в номинации Visual C++. Основная цель моих статей и работы, сделать код программ немножко безопасней и качественней. Буду рад, если этот документ научит вас писать более надежный код и предостережет от некоторых типовых ошибок. Немало полезного здесь можно будет почерпнуть и тем, кто занимается написанием стандартов кодирования для своих компаний.

Немного истории. Не так давно я создал ресурс, на котором делился различными полезными советами по программированию на языке С++. Ресурс не собрал ожидаемое количество подписчиков, поэтому я не вижу смысла приводить здесь на него ссылку. Сайт просуществует какое-то время, после чего уйдет в небытие. А вот советы достойны сохранения. Поэтому я доработал, пополнил эти советы и объединил их в единый текст. Желаю приятного чтения.


1. Не берите на себя работу компилятора

Рассмотрим фрагмент кода, позаимствованный из проекта MySQL. Код содержит ошибку, которую анализатор PVS-Studio диагностирует следующим образом: V525 The code containing the collection of similar blocks. Check items '0', '1', '2', '3', '4', '1', '6' in lines 680, 682, 684, 689, 691, 693, 695.

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static int rr_cmp(uchar *a,uchar *b)
{
  if (a[0] != b[0])
    return (int) a[0] - (int) b[0];
  if (a[1] != b[1])
    return (int) a[1] - (int) b[1];
  if (a[2] != b[2])
    return (int) a[2] - (int) b[2];
  if (a[3] != b[3])
    return (int) a[3] - (int) b[3];
  if (a[4] != b[4])
    return (int) a[4] - (int) b[4];
  if (a[5] != b[5])
    return (int) a[1] - (int) b[5];     <<<<====
  if (a[6] != b[6])
    return (int) a[6] - (int) b[6];
  return (int) a[7] - (int) b[7];
}
Разъяснение

Классическая ошибка, связанная с копированием фрагментов кода (Copy-Paste). По всей видимости, был размножен блок кода "if (a[1] != b[1]) (int) a[1] - (int) b[1];". Затем начали менять индексы, и в одном месте забыли изменить "1" на "5". В результате функция сравнения будет изредка давать неверный результат, и это будет сложно обнаружить. И её действительно сложно обнаружить, раз она не была выявлена никаким тестами до момента, пока мы не проверили MySQL с помощью PVS-Studio.

Корректный код

C++
1
2
if (a[5] != b[5])
  return (int) a[5] - (int) b[5];
Рекомендация

Хотя код красиво оформлен и легко читается, это не помогло программистам заметить и устранить ошибку. На таком коде человеку сложно сосредоточиться. Он видит однотипные блоки и просматривает их невнимательно.

Эти однотипные блоки скорее всего появились из-за желания максимально оптимизировать код. Программист "развернул цикл" руками. В данном случае я думаю это было плохой идеей.

Во-первых, вряд ли программист что-то этим достиг. Современные компиляторы весьма умны и самостоятельно хорошо разворачивают циклы, если это приведёт к ускорению работы программы.

Во-вторых, из-за этой попытки оптимизации в коде появилась ошибка. Если написать простой цикл, то в нём будет гораздо сложнее допустить ошибку.

Я бы порекомендовал переписать эту функцию так:

C++
1
2
3
4
5
6
7
8
9
static int rr_cmp(uchar *a,uchar *b)
{
  for (size_t i = 0; i < 7; ++i)
  {
    if (a[i] != b[i])
      return a[i] - b[i]; 
  }
  return a[7] - b[7];
}
Преимущества:
  • Функцию проще прочитать и понять.
  • В ней намного сложнее допустить ошибку.

При этом, я почти уверен, что функция будет работать не медленнее, чем её длинный вариант.

Собственно, рекомендация. Пишите простой и понятный код. Как правило простой код - это правильный код. Не старайтесь взять на себя работу компилятора. Например, не спешите развернуть циклы. Скорее всего, компилятор хорошо справится и без вашей помощи. Заниматься такими мелкими ручными оптимизациями есть смысл только в очень критичных участках кода и только после того, как профилировщик укажет, что данный участок кода является проблемным (медленным).


2. Больше 0, это не 1

Следующий фрагмент кода взят из проекта CoreCLR. Код содержит ошибку, которую анализатор PVS-Studio диагностирует следующим образом: V698 Expression 'memcmp(....) == -1' is incorrect. This function can return not only the value '-1', but any negative value. Consider using 'memcmp(....) < 0' instead.

C++
1
2
bool operator( )(const GUID& _Key1, const GUID& _Key2) const
  { return memcmp(&_Key1, &_Key2, sizeof(GUID)) == -1; }
Разъяснение

Взглянем на описание функции memcmp():

int memcmp ( const void * ptr1, const void * ptr2, size_t num );

Compares the first num bytes of the block of memory pointed by ptr1 to the first num bytes pointed by ptr2, returning zero if they all match or a value different from zero representing which is greater if they do not.

Return value:
  • < 0 - the first byte that does not match in both memory blocks has a lower value in ptr1 than in ptr2 (if evaluated as unsigned char values).
  • == 0 - the contents of both memory blocks are equal.
  • > 0 - the first byte that does not match in both memory blocks has a greater value in ptr1 than in ptr2 (if evaluated as unsigned char values).

Обратите внимание, что если блоки не совпадают, то возвращаются значения больше или меньше нуля. Именно больше или меньше. Это важно! Нельзя сравнивать результат работы таких функций как memcmp(), strcmp(), strncmp() и так далее с константами 1 и -1.

Что интересно, неправильный код, где результат сравнивается с 1/-1 может работать так, как ожидает программист многие годы. Но это везение и не больше того. Поведение функции может самым неожиданным образом поменяться. Например, вы смените компилятор или разработчики новым образом оптимизируют memcmp(), и ваш код перестанет работать.

Корректный код

C++
1
2
bool operator( )(const GUID& _Key1, const GUID& _Key2) const
  { return memcmp(&_Key1, &_Key2, sizeof(GUID)) < 0; }
Рекомендация

Не полагайтесь на наблюдаемое поведение функций. Если в документации написано, что функция может вернуть значения меньше 0 или больше 0, то так оно и есть. Значит, функция может вернуть -10, 2 или 1024. То, что вы всё время наблюдаете, что функция возвращает -1, 0 или 1, ничего не значит.

Кстати, из того, что функция может вернуть такие числа как, например, 1024 вытекает, что результат работы функции memcmp() нельзя поместить в переменную типа char. Это ещё одна распространённая ошибка, и её последствия могут быть весьма опасны. Одна такая ошибка послужила причиной серьезной уязвимости в MySQL/MariaDB до версий 5.1.61, 5.2.11, 5.3.5, 5.5.22. Суть в том, что при подключении пользователя MySQL /MariaDB вычисляется токен (SHA от пароля и хэша), который сравнивается с ожидаемым значением функцией memcmp(). На некоторых платформах возвращаемое значение может выпадать из диапазона [-128..127]. В итоге, в 1 случае из 256, процедура сравнения хэша с ожидаемым значением всегда возвращает значение true, независимо от хэша. В результате простая команда на bash даёт злоумышленнику рутовый доступ к уязвимому серверу MySQL, даже если он не знает пароль. Причиной этому стал такой код в файле 'sql/password.c':

C++
1
2
3
4
5
typedef char my_bool;
...
my_bool check(...) {
  return memcmp(...);
}
Более подробное описание этой проблемы можно прочитать здесь: Security vulnerability in MySQL/MariaDB.


3. Один раз скопируй, несколько раз проверь

Фрагмент взят из проекта Audacity. Ошибка выявляется PVS-Studio диагностикой: V501 There are identical sub-expressions to the left and to the right of the '-' operator.

C++
1
2
3
4
5
6
7
8
sampleCount VoiceKey::OnBackward (....) {
  ...
  int atrend = sgn(buffer[samplesleft - 2]-
                   buffer[samplesleft - 1]);                          
  int ztrend = sgn(buffer[samplesleft - WindowSizeInt-2]-
                   buffer[samplesleft - WindowSizeInt-2]);
  ...
}
Разъяснение

Выражение "buffer[samplesleft - WindowSizeInt-2]" вычитается само из себя. Эта ошибка связана с копированием фрагментов кода (Copy-Paste) - строчку скопировали, но забыли исправить в ней константу 2 на 1.

Ошибка банальна до безобразия, но от этого она не перестаёт быть ошибкой. Такие ошибки -суровая реальность программистов, поэтому я не раз буду рассматривать подобные случаи. Я им объявляю войну.

Корректный код

C++
1
2
int ztrend = sgn(buffer[samplesleft - WindowSizeInt-2]-
                 buffer[samplesleft - WindowSizeInt-1]);
Рекомендация

Будьте аккуратны и внимательны, дублируя фрагменты кода.

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

Поэтому просто порекомендую быть аккуратней и не спешить.

Знайте, что копирование кода порождает огромное количество ошибок. Вы только посмотрите, что, например, находится с помощью диагностики V501. Половина этих ошибок - это последствия Copy-Paste.

Если копируешь и правишь код - проверь что получилось! Не ленись!

К проблеме Copy-Paste мы ещё вернемся ниже в этой статье. Я же знаю, что вы всё равно не поняли всю глубину проблемы, но я не дам про неё забыть.


4. Бойтесь оператора ?: и заключайте его в круглые скобки

Фрагмент взят из проекта Haiku (преемница операционной системы BeOS). Ошибка выявляется PVS-Studio диагностикой: V502 Perhaps the '?:' operator works in a different way than it was expected. The '?:' operator has a lower priority than the '-' operator.

C++
1
2
3
4
5
bool IsVisible(bool ancestorsVisible) const
{
  int16 showLevel = BView::Private(view).ShowLevel();
  return (showLevel - (ancestorsVisible) ? 0 : 1) <= 0;
}
Разъяснение

Взглянем на приоритеты операций в языке Си/Си++. У тернарного оператора ?: очень низкий приоритет. Он ниже, чем у операций /, +, < и так далее; ниже он и приоритета оператора минус. В результате программа ведёт себя не так, как хотел программист.

Программист думает, что используется вот такая последовательность операций:

C++
1
(showLevel - (ancestorsVisible ? 0 : 1) ) <= 0
А на самом деле она такая:

C++
1
((showLevel - ancestorsVisible) ? 0 : 1) <= 0
Ошибка допущена в весьма простом коде. Это подчеркивает всю опасность оператора ?:. Используя его, очень легко ошибиться, а использовать тернарный оператор в сложных условиях- это вообще вредительство. Мало того, что легко сделать и не заметить ошибку, так ещё и читать такие выражения бывает очень сложно.

Бойтесь, бойтесь оператора ?:. Я повидал немало ошибок с его участием.

Корректный код

C++
1
return showLevel - (ancestorsVisible ? 0 : 1) <= 0;
Рекомендация

Раньше в статьях я уже обсуждал проблему тернарного оператора, но с тех пор я стал ещё большим параноиком. Как показывает пример приведённый выше, ошибку легко сделать даже в коротком и простом выражении, поэтому я модифицирую свои предыдущие советы.

Совсем отказываться от ?: я не предлагаю. Этот оператор иногда полезен и даже необходим. Тем не менее, прошу им не злоупотреблять, а если вы решили где-то использовать тернарный оператор, то я дам следующую рекомендацию.

ВСЕГДА заключайте тернарный оператор в скобки.

Предположим, у вас есть выражение:

A = B ? 10 : 20;

Теперь записывайте его так:

A = (B ? 10 : 20);

Да, сейчас скобки излишни.

Зато, когда через год вы или ваш коллега захочет добавить к числу 10 или 20 переменную X, ничего не сломается:

A = X + (B ? 10 : 20);

Если бы скобок не было, вы могли забыть про низкий приоритет оператора ?: и испортить программу.

Конечно, можно вписать "X+" внутрь скобок, что приведёт всё к той же ошибке. Но все-таки, это дополнительная защита, и пренебрегать ею не стоит.


5. Используйте доступные инструменты для проверки кода

Фрагмент взят из проекта LibreOffice. Ошибка выявляется PVS-Studio диагностикой: V718 The 'CreateThread' function should not be called from 'DllMain' function.

C++
1
2
3
4
5
6
7
8
BOOL WINAPI DllMain( HINSTANCE hinstDLL,
                     DWORD fdwReason, LPVOID lpvReserved )
{
  ....
  CreateThread( NULL, 0, ParentMonitorThreadProc,
                (LPVOID)dwParentProcessId, 0, &dwThreadId );
  ....
}
Разъяснение

В давние времена я подрабатывал фрилансером. Однажды мне была поставлена задача, с которой я не смог справиться. Задача была некорректная, но тогда я про это не догадывался. При этом, на первый взгляд задание казалось понятным и простым.

При определенном условии в функции DllMain нужно было выполнить ряд действий, используя Windows API функции. Что именно нужно было сделать уже забылось, но что-то простое.

Я потратил уйму времени, но мой код упорно не хотел работать. Причем, если сделать новое обыкновенное приложение, то код работает, а в функции DllMain не работает. Чудеса и загадки. Тогда я так и не разобрался в сути проблемы.

Только сейчас, по прошествии многих лет, разрабатывая анализатор PVS-Studio, я вдруг понял причину той старой неудачи. В функции DllMain можно выполнять только очень ограниченный набор действий. Дело в том, что некоторые DLL могут быть ещё не подгружены и вызывать функции из них нельзя.

Мы сделали диагностику, которая предупреждает программистов, если встречает в функциях DllMain опасные действия. Теперь я понимаю, что у меня была именно такая ситуация.

Подробности

Ситуация с DllMain хорошо описана в статье на сайте MSDN: Dynamic-Link Library Best Practices. Приведу из неё некоторые фрагменты:

При вызове функции DllMain происходит блокировка загрузчика. По этой причине, на функции, которые могут быть вызваны внутри DllMain, накладываются существенные ограничения. Как таковая, функция DllMain предназначена для выполнения задач по минимальной инициализации за счет использования небольшого подмножества Microsoft Windows API. Внутри нее нельзя вызывать функции, которые прямо или косвенно пытаются использовать загрузчик. В противном случае, Вы рискуете создать в программе такие условия, при которых произойдет ее аварийное завершение либо взаимная блокировка потоков. Ошибка в реализации DllMain может подвергнуть опасности весь процесс целиком и все его потоки.

В идеале функция DllMain должна представлять собой всего лишь пустую заглушку. Однако, учитывая сложность многих приложений, данное ограничение было бы слишком строгим, поэтому на практике при работе с этой функцией следует откладывать инициализацию как можно дольше. Отложенная инициализация повышает надежность работы приложения, поскольку она не происходит, пока загрузчик заблокирован. Кроме того, отложенная инициализация позволяет безопасно использовать Windows API в значительно большем объеме.

Некоторые задачи инициализации не могут быть отложены. Например, DLL-библиотека, которая зависит от файла конфигурации, должна прерывать свою загрузку, если файл оказывается неправильно сформированным или содержит мусор. При таком типе инициализации DLL-библиотеки должны предпринимать попытку выполнить запланированное действие и в случае неудачи сразу же завершаться вместо того, чтобы тратить ресурсы, выполняя какую-то другую работу.

В любом случае никогда не выполняйте следующие задачи в пределах функции DllMain:
  • Вызов LoadLibrary или LoadLibraryEx (напрямую или косвенно). Это может привести к взаимной блокировке потоков или аварийному завершению программы.
  • Вызов GetStringTypeA, GetStringTypeEx или GetStringTypeW (напрямую или косвенно). Это может привести к взаимной блокировке потоков или аварийному завершению программы.
  • Синхронизация с другими потоками. Это может привести к их взаимной блокировке.
  • Захват объекта синхронизации, принадлежащего коду, который находится в ожидании захвата блокировки загрузчика. Это может привести к взаимной блокировке потоков.
  • Инициализация COM-потоков с помощью CoInitializeEx. При определенных условиях данная функция может вызвать LoadLibraryEx.
  • Вызов реестровых функций. Данные функции реализованы в Advapi32.dll. Если Advapi32.dll не была инициализирована раньше пользовательской DLL-библиотеки, последняя может обратиться к неинициализированной области памяти, что приведет к аварийному завершению процесса.
  • Вызов CreateProcess. Создание процесса может повлечь за собой загрузку другой DLL-библиотеки.
  • Вызов ExitThread. Выход из потока во время отсоединения DLL-библиотеки может повлечь за собой повторный захват блокировки загрузчика, что приведет к взаимной блокировке потоков или аварийному завершению программы.
  • Вызов CreateThread. Если создаваемый поток не синхронизируется с другими потоками, то такая операция допустима, хотя и рискованна.
  • Создание именованного конвейера или другого именованного объекта (только для Windows 2000). В Windows 2000 именованные объекты предоставляются библиотекой Terminal Services DLL. Если данная библиотека не инициализирована, ее вызовы могут привести к аварийному завершению процесса.
  • Использование функций управления памятью из динамической библиотеки C Run-Time (CRT). Если данная библиотека не инициализирована, вызовы этих функций могут привести к аварийному завершению процесса.
  • Вызов функций из библиотек User32.dll или Gdi32.dll. Некоторые функции загружают другие DLL-библиотеки, которые могут быть не инициализированы.
  • Использование управляемого кода.

Корректный код

Приведённый фрагмент кода из проекта LibreOffice может работать, а может и не работать. Всё зависит от везения.

Невозможно легко исправить такую ошибку, требуется рефакторинг кода с целью сделать функцию DllMain максимально простой и короткой.

Рекомендация

Сложно дать рекомендацию. Всё знать невозможно, любой может неожиданно столкнуться с ошибкой такого тайного вида. Формальной рекомендацией должно быть: внимательно читайте всю документацию, относящуюся к тому, с чем работаете, но я думаю вы понимаете, что невозможно заранее предугадывать все такие случаи. Тогда только и будешь, что читать документацию, программировать будет некогда. Даже прочитав N страниц, всё равно не будет уверенности, что ты не прочитал ещё статью X, которая предупредит о беде.

Хочется дать более практичные советы, но к сожалению, на ум приходит только один: используйте статические анализаторы и иные инструменты анализа кода. Да, это не гарантирует отсутствия ошибок, но от каких-то ошибок всё-таки спасёт. Скажи мне много лет назад анализатор, что в функции DllMain нельзя вызывать функцию Foo, я сэкономил бы массу времени и ещё больше нервных клеток, а ведь тогда я очень расстраивался и злился, так как не мог выполнить поставленную задачу.


6. Проверьте все места, где указатель явно приводится к целочисленным типам

Фрагмент взят из проекта IPP Samples. Ошибка выявляется PVS-Studio диагностикой: V205 Explicit conversion of pointer type to 32-bit integer type: (unsigned long)(img)

C++
1
2
3
4
5
6
void write_output_image(...., const Ipp32f *img, 
                        ...., const Ipp32s iStep) {
  ...
  img = (Ipp32f*)((unsigned long)(img) + iStep);
  ...
}
Примечание. Когда я ранее в статье приводил этот пример, многие писали в комментариях, что код плох сразу по нескольким причинам. Согласен, но давайте оставим за рамками вопрос, зачем именно так нужно двигаться по буферу данных, и почему код написан так, а не иначе. Сейчас важно, что указатель явно приводится к типу "unsigned long". И только это. Я выбрал этот пример исключительно из-за его краткости.

Разъяснение

Указатель хотят сдвинуть на определённое количество байт. Этот код будет корректно работать в Win32 программе, так как в ней размер указателя совпадает с размером типа long. Однако, если мы скомпилируем 64-битный вариант программы, то указатель станет 64-битным, и при преобразовании его в тип long, будут потеряны значения старших битов.

Примечание. В Linux используется другая модель данных. В 64-битных Linux программах тип 'long' является 64-битным, однако, всё равно плохая идея - использовать 'long' для хранения указателя. Во-первых, такой код нередко попадает в Windows приложения, где является некорректным. Во-вторых, существуют специальные типы, само название которых подразумевает, что в них может храниться указатель. Например, это intptr_t. Использование таких типов облегчает понимание программы при её изучении.

В приведённом примере мы наблюдаем классическую ошибку, которая проявляет себя в 64-битных программах. Сразу подчеркнём, что есть много других ошибок, которые подстерегают программиста на пути создания 64-битного приложения, однако, запись значения указателя в 32-битную целочисленную переменную является самой распространённой и самой коварной ошибкой.

Наглядно этот ошибку можно проиллюстрировать следующим образом:


Нажмите на изображение для увеличения
Название: image2.png
Просмотров: 1314
Размер:	148.0 Кб
ID:	3765

Рисунок 1. A) 32-битная программа. B) 64-битный указатель ссылается на объект, расположенный в младших адресах. C) 64-битный указатель портится.

Остановлюсь чуть подробнее на коварности. Иногда такие ошибки бывает очень трудно заметить. Программа "почти работает". Ошибки потери старших битов в указателях могут проявляться только через несколько часов активной работы с программой. Сначала память выделяется в младших адресах памяти, поэтому все объекты и массивы лежат в младших 4 гигабайтах памяти. Всё работает хорошо.

В процессе работы приложения память фрагментируется, и даже если программа использует не очень много памяти, новые объекты могут быть созданы за пределами первых 4 гигабайт памяти. Вот здесь и начинаются проблемы; специально повторить возникающие проблемы очень трудно.

Корректный код

Для целочисленного представления указателей можно использовать такие типы как size_t, INT_PTR, DWORD_PTR, intrptr_t, uintptr_t и так далее.

C++
1
img = (Ipp32f*)((uintptr_t)(img) + iStep);
На самом деле, здесь вообще можно было обойтись без явных приведений типов. Нигде не упоминается, что выравнивание отлично от стандартного, т.е. нет никакой магии с использованием __declspec(align( # )) и тому подобного. Значит, указатели сдвигаются на количество байт, кратное размеру Ipp32f; в противном случае, здесь возникнет неопределённое поведение (см. EXP36-C).

Поэтому можно написать так:

img += iStep / sizeof(*img);

Рекомендация

Используйте специальные типы для хранения указателей. Никаких int или long. Наиболее универсальным решением являются следующие типы: intptr_t, uintptr_t. В Visual C++ доступны также следующие типы: INT_PTR, UINT_PTR, LONG_PTR, ULONG_PTR, DWORD_PTR. Само название типа говорит, что в него может быть помещён указатель.

Указатель вполне можно поместить в типы size_t, ptrdiff_t, но рекомендовать это, пожалуй, не стоит. У этих типов другой смысл - они предназначены для хранения размеров и индексов.

В uintptr_t нельзя поместить указатель на функцию-член класса. Функции-члены классов несколько отличаются от стандартных функций. Кроме самого указателя, они хранят скрытое значение this, который указывает на объект класса. Впрочем, это не важно - в 32-битной программе вы не можете положить такой указатель в unsigned int. Такие указатели всегда обрабатываются особым образом, поэтому и проблем с ними в 64-битных программах не возникает. По крайней мере, я таких ошибок не видел.

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

Тем, кто занимается или планирует заниматься созданием 64-битных приложений, дополнительно рекомендую ознакомиться со следующим ресурсом: Разработки 64-битных приложений на языке Си/Си++.


7. Не вызывайте функцию alloca() внутри циклов

Фрагмент взят из проекта Pixie. Ошибка выявляется PVS-Studio диагностикой: V505 The 'alloca' function is used inside the loop. This can quickly overflow stack.

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
inline  void  triangulatePolygon(....) {
  ...
  for (i=1;i<nloops;i++) {
    ...
    do {
      ...
      do {
        ...
        CTriVertex *snVertex =
          (CTriVertex *) alloca(2*sizeof(CTriVertex));
        ...
      } while(dVertex != loops[0]);
      ...
    } while(sVertex != loops[i]);
    ...
  }
  ...
}
Разъяснение

Функция alloca(size_t) выделяет память, используя для этого стек. Память, выделенная с помощью alloca(), освобождается при выходе из функции.

Как правило, для программ выделяется не так уж и много стековой памяти. По умолчанию, когда вы создаёте проект в Visual C++, в настройках указано использовать стек размером всего 1 Мегабайт, поэтому функция alloca() очень быстро может исчерпать всю доступную стековую память, если она располагается в теле цикла.

В приведённом выше примере присутствуют сразу 3 вложенных цикла. Таким образом, при триангуляции большого полигона возникнет переполнение стека.

Опасно использовать в циклах и такие макросы, как A2W, так как внутри они так же содержат вызов функции alloca().

Как сказано выше, по умолчанию Windows-программы используют стек размером в 1 Мегабайт. Это значение можно изменить, для этого в настройках проекта найдите и измените параметры Stack Reserve Size и Stack Commit Size. Подробности: "/STACK (Stack Allocations)". Однако, следует понимать, что увеличение размера стека не является решением проблемы - вы только отодвигаете момент, когда стек программы закончится.

Рекомендация

Не вызывайте функцию alloca() внутри циклов. Если у вас есть цикл, и вам нужно выделить временный буфер, то можно предложить 3 варианта:
  1. Выделите память заранее, а затем используйте один буфер для всех операций. Если каждый раз требуется буфер разного размера, то следует выделить память под самый большой из них. Если это невозможно (не известно заранее сколько памяти понадобится), то следует воспользоваться вариантом 2.
  2. Сделайте из тела цикла отдельную функцию, тогда при каждой итерации буфер будет выделяться и тут же уничтожаться. Если это сложно, то остался вариант 3.
  3. Замените alloca() на функцию malloc() или оператор new, или используйте такой класс как std::vector. Следует учитывать, что при этом память будет выделяться медленнее. В случае использования malloc/new придётся ещё и позаботиться об её освобождении, зато у вас при демонстрации программы заказчику на больших данных, не возникнет переполнение стека.


8. Помните, что исключение в деструкторе - это опасно

Фрагмент взят из проекта LibreOffice. Ошибка выявляется PVS-Studio диагностикой: V509 The 'dynamic_cast<T&>' operator should be located inside the try..catch block, as it could potentially generate an exception. Raising exception inside the destructor is illegal.

C++
1
2
3
4
5
virtual ~LazyFieldmarkDeleter()
{
  dynamic_cast<Fieldmark&>
    (*m_pFieldmark.get()).ReleaseDoc(m_pDoc);
}
Разъяснение

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

Приведенный код весьма опасен. Оператор dynamic_cast генерирует исключение std::bad_cast, если не может привести ссылку на объект к нужному типу.

Аналогично опасны любые конструкции, которые могут вызвать исключение. Например, опасно выделять в деструкторе память с помощью оператора new. В случае неудачи, он генерирует исключение std::bad_alloc.

Корректный (безопасный) код

Код можно исправить, применяя оператор dynamic_cast не к ссылке, а к указателю. В этом случае, если преобразовать тип объекта невозможно, он не генерирует исключение, а просто возвращает nullptr.

C++
1
2
3
4
5
6
virtual ~LazyFieldmarkDeleter()
{
  auto p = dynamic_cast<Fieldmark*>m_pFieldmark.get();
  if (p)
    p->ReleaseDoc(m_pDoc);
}
Рекомендация

Делайте деструкторы максимально простыми. Деструктор - это не место для выделения памяти или чтения файлов.

Конечно, не всегда можно сделать деструктор простым, но я считаю, что к этому надо стремиться. Более того, сложный деструктор, как правило, говорит о плохом дизайне класса и о непродуманности решений.

Чем больше кода в деструкторе, тем сложнее всё предусмотреть. Становится трудно сказать, какой участок кода может сгенерировать исключение, а какой нет.

Если исключение может возникнуть, то часто хорошим решением является подавить его, используя catch(...):

C++
1
2
3
4
5
6
7
8
9
10
11
12
virtual ~LazyFieldmarkDeleter()
{
  try 
  {
    dynamic_cast<Fieldmark&>
      (*m_pFieldmark.get()).ReleaseDoc(m_pDoc);
  }
  catch (...)
  {
    assert(false);
  }
}
Да, это может спрятать какую-то ошибку, возникающую в деструкторе, но приложение в целом может работать стабильнее.

Я не буду настаивать, что деструктор ни при каких обстоятельствах не должен генерировать исключение. Всё зависит от ситуации. Иногда сгенерировать исключение в деструкторе полезно. Я встречал такое в узкоспециализированных классах, но это особые случаи. Эти классы по задумке спроектированы так, что при разрушении объекты генерируют исключения, но если это обыкновенный класс, такой как "своя строка", "точка", "кисточка", "треугольник", "документ" и так далее, то такие классы ни в коем случае не должны выбрасывать из деструктора исключение.

Помните, что двойное исключение приводит к останову программы, и примите решение, допустимо это в вашем проекте или нет.


9. Используйте для обозначения терминального нуля литерал '\0'

Фрагмент взят из проекта Notepad++. Ошибка выявляется PVS-Studio диагностикой: The error text: V528 It is odd that pointer to 'char' type is compared with the '\0' value. Probably meant: *headerM != '\0'.

C++
1
2
3
4
5
6
7
8
TCHAR headerM[headerSize] = TEXT("");
...
size_t Printer::doPrint(bool justDoIt)
{
  ...
  if (headerM != '\0')
  ...
}
Разъяснение

Благодаря тому, что автор кода использовал для обозначения терминального нуля литерал '\0' можно заметить и исправить ошибку. Автор молодец, по крайне мере частично.

Представим, что было бы если он написал:

C++
1
if (headerM != 0)
Адрес массива проверяется на равенство 0. Сравнение не имеет смысла, так как результат всегда true. Что это? Ошибка или просто лишняя проверка? Ответить сложно, особенно если это чужой код или код, написанный много лет назад.

Однако, мы видим в коде '\0' и начинаем подозревать, что хотели проверить значение одного символа. В добавок, сравнивать указатель headerM с NULL не имеет смысла. Подумав, мы понимаем, что хотели узнать, пустая строка или нет, но ошиблись. Чтобы исправить код, следует добавить разыменование указателя.

Корректный код

C++
1
2
3
4
5
6
7
8
TCHAR headerM[headerSize] = TEXT("");
...
size_t Printer::doPrint(bool justDoIt)
{
  ...
  if (*headerM != _T('\0'))
  ...
}
Рекомендация

Число 0 может означать NULL, false, терминальный ноль '\0', просто значение 0, поэтому не ленитесь и не используйте 0 для краткости везде, где только можно. Использование "голого" 0 усложняет чтение кода и мешает находить ошибки.

Используйте:
  • 0 - для обозначения целочисленного нуля;
  • nullptr - для обозначения нулевых указателей в C++;
  • NULL - для обозначения нулевых указателей в C;
  • '\0', L'\0', _T('\0') - для обозначения терминальных нулей;
  • 0.0, 0.0f - для обозначения нуля в выражениях, где используются типы с плавающей точкой;
  • false, FALSE - для обозначения 'ложь'.

Это упростит чтение кода и поможет выявить ошибки при обзорах кода.


10. Старайтесь "не мельчить" при использовании #ifdef

Фрагмент взят из проекта CoreCLR. Ошибка выявляется PVS-Studio диагностикой: V522 Dereferencing of the null pointer 'hp' might take place.

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
heap_segment* gc_heap::get_segment_for_loh (size_t size
#ifdef MULTIPLE_HEAPS
                                           , gc_heap* hp
#endif //MULTIPLE_HEAPS
                                           )
{
#ifndef MULTIPLE_HEAPS
    gc_heap* hp = 0;
#endif //MULTIPLE_HEAPS
    heap_segment* res = hp->get_segment (size, TRUE);
    if (res != 0)
    {
#ifdef MULTIPLE_HEAPS
        heap_segment_heap (res) = hp;
#endif //MULTIPLE_HEAPS
  ....
}
Разъяснение

Я считаю конструкции #ifdef/#endif злом. К сожалению, это неизбежное зло. Они нужны, и все мы их используем, поэтому я не буду призывать вас не использовать #ifdef, это не имеет смысла. Однако, я хочу призвать вас не "частить".

Думаю, многие из читателей сталкивались с кодом, напичканным #ifdef. Особенно тяжкое впечатление производит код, где #ifdef следует через каждые 10 строк кода или ещё чаще. Как правило, это системно-зависимый код, и без #ifdef в нём не обойтись, но от этого не легче.

Обратите внимание, как тяжело читать код, приведённый в примере! А именно чтение кода - это основное занятие программиста. Да, да. Мы намного больше времени читаем код и разбираемся в нем, чем пишем новый текст. Поэтому трудночитаемый текст очень сильно снижает продуктивность работы и увеличивает вероятность появления ошибки.

Вернемся к приведённому выше фрагменту кода. Ошибка в разыменовании нулевого указателя, если не объявлен макрос MULTIPLE_HEAPS. Для простоты я раскрою макросы:

C++
1
2
3
4
5
heap_segment* gc_heap::get_segment_for_loh (size_t size)
{
  gc_heap* hp = 0;
  heap_segment* res = hp->get_segment (size, TRUE);
  ....
Объявили переменную hp, инициализировали её NULL, и тут же разыменовали. Если не объявлен MULTIPLE_HEAPS, то будет беда.

Корректный код

Эта ошибка до сих пор живёт в CoreCLR (12.04.2016) несмотря на то, что мой коллега описал её в статье "25 подозрительных фрагментов кода из CoreCLR". Так что я затрудняюсь точно сказать, как должен выглядеть правильный вариант кода.

На мой взгляд, если (hp == nullptr), то переменную 'res' нужно инициализировать каким-то другим значением. Но я не знаю каким, поэтому на этот раз правильный вариант кода я пропущу.

Рекомендации

Боритесь с мелкими блоками #ifdef/#endif. Они очень затрудняют чтение кода! Код с "лесом" из #ifdef сложно поддерживать и в нём очень легко допустить ошибку.

Советы на все случаи жизни я не дам: решения зависят от ситуации. Главное - помнить, что с #ifdef есть проблема и постоянно стараться оставить код максимально читаемым.

Совет N1. Попробовать отказаться от #ifdef.

Иногда #ifdef можно заменить на константы и обычный оператор if. Сравним 2 участка кода. Вариант с макросами:

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#define DO 1
 
#ifdef DO
static void foo1()
{
  zzz();
}
#endif //DO
 
void F()
{
#ifdef DO
  foo1();
#endif // DO
  foo2();
}
Такой код читать очень тяжело, даже не хочется. Я уверен, вы просто пропустили приведённый фрагмент кода. Сравните теперь вот с таким вариантом:

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const bool DO = true;
 
static void foo1()
{
  if (!DO)
    return;
  zzz();
}
 
void F()
{
  foo1();
  foo2();
}
Код читается намного проще. Кто-то может возразить, что такой код менее эффективен, так как вызывается функция и осуществляется проверка. Я с ним не согласен. Во-первых, компиляторы сейчас очень умные и в Release версии есть высокая вероятность получить один и тот-же код без лишних проверок и вызовов функций. Во-вторых, эти потери слишком малы, чтобы о них думать. Красивый и понятный код важнее.

Совет N2. "Укрупните" блоки #ifdef.

Если бы я писал функцию get_segment_for_loh(), то не стал бы делать в ней несколько #ifdef. Я бы сделал две версии функции. Да, получилось бы чуть больше текста, зато функции будет легко читать и изменять.

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

Подождите, подождите! А почему у вас большие функции? Вынесите общую логику в отдельные вспомогательные функции, тогда два варианта функции будут короткими и в них хорошо будет видно, чем они отличаются.

Я понимаю, что этот совет не универсален, но подумайте над ним.

Совет N3. Подумайте о шаблонах, возможно они вам помогут.

Совет N4. Просто подумайте, прежде чем написать #ifdef. Быть может, он не так и нужен? Или можно обойтись меньшим количеством #ifdef, собрав "зло" в одном месте?


11. Не жадничайте на строчках кода

Фрагмент взят из проекта Godot Engine. Ошибка выявляется PVS-Studio диагностикой: V567 Undefined behavior. The 't' variable is modified while being used twice between sequence points.

C++
1
2
3
4
static real_t out(real_t t, real_t b, real_t c, real_t d)
{
  return c * ((t = t / d - 1) * t * t + 1) + b;
}
Разъяснение

Иногда можно встретить фрагменты кода, где программист попытался поместить как можно больше логики в малый объём кода, создавая при этом сложные конструкции. Навряд ли он этим поможет компилятору, но может легко помешать другому человеку (и даже самому себе) читать и понимать код. К тому же, в таких фрагментах кода легче допустить ошибку.

Достаточно часто ошибки, связанные с неопределённым поведением, как раз и встречаются там, где старались уместить всего побольше в одном месте. Обычно это запись и чтение из одной и той же переменной в рамках одной точки следования. Тут следует несколько более детально остановиться на понятиях "неопределённого поведения" и "точки следования".

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

Точка следования - любая точка программы, в которой гарантируется, что все побочные эффекты предыдущих вычислений уже проявились, а побочные эффекты следующих ещё отсутствуют. В языках программирования C\C++ существуют следующие точки следования:
  • точки следования для операторов "&&", "||", ",". Эти операторы, в случае если они не перегружены, гарантируют вычисление слева направо;
  • точка следования для тернарного оператора "?:";
  • точка следования в конце каждого полного выражения (обычно помечены символом ';');
  • точка следования в месте вызова функции, но после вычисления аргументов;
  • точка следования при возвращении из функции.

Примечание. В новом стандарте языка С++ разработчики отошли от понятия "точка следования". Однако сейчас, для краткого знакомства воспользуемся именно таким объяснением. Оно проще и достаточно, чтобы пояснить, почему не надо производить много действий в "одной куче".

В данном примере нет ни одной из вышеперечисленных точек следования, а оператор '=', так же как и скобки, точками следования не являются. Таким образом, нельзя сказать, какое значение переменной t будет использовано при вычислении возвращаемого значения.

Ещё раз другими словами - данное выражение представляет собой одну точку следования. Поэтому неизвестно, в каком порядке будут осуществляться доступ к переменной 't'. Например, подвыражение "t * t" может быть вычислено как до записи в переменную "t = t / d - 1", так и после.

Корректный код

C++
1
2
3
4
5
static real_t out(real_t t, real_t b, real_t c, real_t d)
{
  t = t / d - 1;
  return c * (t * t * t + 1) + b;
}
Рекомендация

Уместить выражение целиком в одну строчку в приведённом фрагменте кода явно не было лучшей идеей. Мало того, что оно тяжелее читается, так туда ещё закралась ошибка.

Исправив ситуацию и разбив содержимое выражения, мы решили сразу 2 проблемы: улучшили читабельность кода и устранили неопределённое поведение, добавив точку следования.

Конечно, приведённый выше фрагмент кода - не единичный случай:

C++
1
2
*(mem+addr++) = 
   (opcode >= BENCHOPCODES) ? 0x00 : ((addr >> 4)+1) << 4;
Как и в прошлый раз, ошибка кроется в усложнении кода на ровном месте. Попытка инкрементировать переменную addr в рамках одного выражения привела к возникновению неопределённого поведения, так как неизвестно, какое значение будет содержать переменная 'addr' в правой части выражения - исходное, или инкрементированное.

Решение проблемы осталось прежним - не заумствовать, а разделить операции на несколько выражений:

C++
1
2
*(mem+addr) = (opcode >= BENCHOPCODES) ? 0x00 : ((addr >> 4)+1) << 4; 
addr++;
Отсюда следует простой, но полезный вывод - не стоит пытаться уместить все действия в минимальное количество выражений. Возможно, стоит разбить код на несколько фрагментов, тем самым как улучшая читабельность кода, так и уменьшая вероятность возникновения ошибки.

В следующий раз, когда будете писать сложные конструкции, подумайте, какова же будет цена их использования, и готовы ли вы её заплатить.
Размещено в Без категории
Надоела реклама? Зарегистрируйтесь и она исчезнет полностью.
Всего комментариев 0
Комментарии
 
Новые блоги и статьи
Установка MinGW GCC 16.2 и CMake
8Observer8 10.08.2026
VK Видео: https:/ / vkvideo. ru/ video-240781534_456239017 YouTube: eY5-5PyI9NM Текстовая версия
Неделя из жизни имитационной модели склада: мои кривые руки растут, откуда надо
anaschu 10.08.2026
Неделя из жизни имитационной модели склада: как я почти написал неправильную логику и что с этим делать Работаю сейчас над учебно-рабочим проектом: строю в AnyLogic имитационную модель процессов. . .
Калькулятор для расчета родства
russiannick 07.08.2026
1. Задача: Создать калькулятор для расчета родства. Родственных связей существует 8 ступеней, такие как: p - отец P - мать q - муж Q - жена b - брат B - сестра s - сын S - дочь
Мир по моей воле
kumehtar 07.08.2026
Когда-то кажется, что всё просто. Ты весь такой светлый. Причиняешь добро. Борешься за справедливость в этом тёмном мире. Потом начинаешь замечать одну неприятную вещь. Почти каждый хороший. . .
Кредитный калькулятор
Maks 05.08.2026
Решение задачи по прикладной информатике средствами 1С. Задача: Напишите приложение-калькулятор, которое помогает рассчитывать параметры кредита для аннуитетного и дифференцированного видов. . .
У нас сейчас поговорку "Опять 25" нужно переделать на "Опять +35".
kumehtar 04.08.2026
С ностальгией вспоминаю времена моего детства, когда у нас и правда +25 - была максимальная температура летом. Раньше +25 °C реально казались вершиной жары, когда можно было весь день пропадать на. . .
Как ИИ начал спорить и врать (возможно почуяв опасность для себя от индустрии - уход от электроники).
Hrethgir 04.08.2026
Недельный диалог, на фоне событий с НПЗ. Да, из спирта можно получать бензин, и это не сложно. Но потом в схеме я решил избавиться от насоса, при этом полностью сделав контроль подачи спирта в. . .
Термопринтер QR701
Argus19 03.08.2026
Термопринтер QR701 Купил два термопринтера QR701. На сэлф-тесте написано: Language: PC936 (GB18030). Что означает, что принтеры могут печатать только латиницу и китайские иероглифы. Так же. . .
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2026, CyberForum.ru