Вновь хочется вернуться к вопросу использования магических констант в коде. Можно сколько угодно говорить, что для вычисления корректного размера выделяемой памяти необходимо использовать оператор sizeof(). Но эти знания и корректное написание нового кода не помогут в диагностировании уже существующей ошибки в дебрях старого кода в больших проектах.
Приведем классический пример ошибки:
size_t nCount = 10;
int **poinerArray = (int **)malloc(nCount * 4);
Код некорректен, но в рамках одной 32-битной системы он будет стабильно работать. Ошибка может проявить себя при адаптации программы к другой программной/аппаратной среде. Важность обнаружения подобного кода стала по настоящему актуальна в связи с массовым переносом программного обеспечения на 64-битные системы. Изменение размеров ряда базовых типов делает код, аналогичный выше приведенному, крайне опасным.
На приведенный выше код статический анализатор Viva64, входящий в состав PVS-Studio, выдаст предупреждение об использование магической константы "4″ и ошибка будет обнаружена в ходе просмотра диагностических сообщений. Но код может быть более запутан:
#define N_COUNT 100
#define POINTER_SIZE 4
#define NSIZE (N_COUNT * POINTER_SIZE)
int **pArray = (int **)malloc(NSIZE);
Ошибку в подобном коде, написанном в стиле языка Си с использованием #define, диагностировать сложнее. Хотя в коде присутствует константа 4, заданная через макрос, в анализаторе Viva64 сознательно заблокирован вывод предупреждения на подобные конструкции. Анализатор игнорирует магические константы, задаваемые макросами (#define) по двум причинам. Во-первых, если программист задает константы через макросы, то, скорее всего, он знает, что делает и очень высока вероятность ложного срабатывания. Во-вторых, если реагировать на опасные с точки зрения 64-битности константы (4, 8, 32 и т.д.), то возникнет лавина ложных срабатываний связанная с использованием Windows API. В качестве примера приведем безобиднейший код:
MessageBox("Are you sure ?",
"Some question",
MB_YESNO | MB_ICONQUESTION);
Если анализировать магические числа, скрывающиеся за макросами MB_YESNO и MB_ICONQUESTION, то на приведенную строчку необходимо выдать два предупреждения об использовании магических констант 4 и 32. Естественно это будет неприемлемо высоким уровнем ложных срабатываний.
Можно при анализе функции malloc() выводить информацию о всех опасных магических константах, независимо, макрос это или нет. Но и этого будет недостаточно для следующего случая:
int **pArray = (int **)malloc(400);
Если пойти дальше и считать любое число, используемое в выражении для функции malloc() опасным, то это тоже повлечет к ложным срабатываниям на корректном коде:
int **pArray = (int **)malloc(400 * sizeof(int *));
Проанализировав ситуацию, мы решили реализовать новое правило, для верификации выражений, результат которых передается в функцию malloc(). На данный момент правило сформулировано следующим образом:
Опасным следует считать использование числовых литералов в выражении, передаваемом в функцию malloc().
Исключения:
1) В выражении присутствует оператор sizeof()
2) Все числовые литералы делятся на 4 c остатком
Благодаря данному правилу мы сможем предупредить об ошибке в следующем коде:
1) Первый пример:
void *p = malloc(nCount * 4);
2) Второй пример:
#define N_COUNT 100
#define POINTER_SIZE 4
#define NSIZE (N_COUNT * POINTER_SIZE)
int **pArray = (int **)malloc(NSIZE);
А также не выдать ложное предупреждение на код вида:
1) Первый пример:
void *p = malloc(sizeof(double) * 4);
2) Второй пример:
#define N_COUNT 100
#define POINTER_SIZE sizeof(int *)
#define NSIZE (N_COUNT * POINTER_SIZE)
int **pArray = (int **)malloc(NSIZE);
Скорее всего, это новое диагностическое правило появится в следующей версии PVS-Studio 3.30.
Рассмотрим теперь другую ситуацию, также связанную с функцией malloc() и неверным предположением о выравнивании данных. Это не совсем связано с магическими числами, но проблема очень родственная. Рассмотрим пример кода:
struct MyBigStruct {
unsigned m_numberOfPointers;
void *m_Pointers[1];
};
unsigned n = 10000;
void *ptr = malloc(sizeof(unsigned) +
n * sizeof(void *));
Хотя в данном коде не используется магических чисел, размер типов определяется через sizeof(), код все равно не корректен. В нем неучтено изменение способа выравнивания данных, различного для 32-битных и 64-битных систем. Корректным будет следующий код:
void *ptr = malloc(
offsetof(MyBigStruct, m_Pointers) +
n * sizeof(void *));
Чтобы предупредить пользователя о возможной допущенной ошибке мы планируем реализовать еще одно правило:
Опасным следует считать использование более одного оператора sizeof() в выражении, передаваемом в функцию malloc. Возможно, при вычислении размера структуры не учитывается изменение выравнивания.
В ряде случаев такое правило будет давать ложные срабатывания, но подобные места в любом случае стоит тщательно проверить.
Описанные выше опасные выражения с участием магических констант актуальны не только для функции malloc(), но и для класса таких функций как fread, fwrite и так далее. Но эти функции требуют отдельного изучения, и к их анализу мы приступим позже, когда отработаем диагностику, связанную с malloc().