Любой программист на C, который использовал GCC, должен быть знаком, по крайней мере, с некоторыми базовыми флагами компиляции, предназначенными для помощи в написании правильного и надежного кода.

В течение десятилетий многие использовали параметры предупреждения, такие как -Wall, -Wextra и даже -Wpedantic, чтобы помочь выявить тривиальные и не очень тривиальные проблемы в своих программах. Но это не вся история...

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

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

Каковы предметы первой необходимости?

Хорошие программисты на C должны знать (и, надеюсь, использовать) по крайней мере -Wall и -Wextra, а также указывать соответствующий уровень оптимизации (например, -O2 или -O3) для дальнейшего расширения диапазона сообщаемых предупреждений.

Оптимизация — довольно интересная тема сама по себе, и причина, по которой она имеет отношение к генерации предупреждений компилятора, ясно изложена на собственной странице руководства GCC:

Эффективность некоторых предупреждений зависит от включенной оптимизации.

Отступление от оптимизации

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

// fencepost.c
int main(void) {
    int x[10];
    for (int i = 0; i <= 10; ++i) {
        x[i] = i;
    }
    return 0;
}

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

Компиляция с:

gcc -O2 fencepost.c

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

-Wall, -Wextra и -Werror

Мне всегда казалось, что имя -Wall вводит в заблуждение. Это, конечно, не включает «все» предупреждения, а, скорее, большой выбор полезных. На собственной странице руководства GCC -Wall «включает все предупреждения о конструкциях, которые некоторые пользователи считают сомнительными и которых легко избежать». Другими словами, -Wall включает предупреждения, которые хорошие программисты считают полезными для выявления возможных проблем в своем коде.

Я не буду углубляться в предупреждения, включенные -Wall, так как их довольно много, и это было бы контрпродуктивно, учитывая, что многие наверняка привыкли к этим предупреждениям.

Между тем, -Wextra (ранее -W) активирует несколько дополнительных предупреждений, не включенных -Wall. Например, -Wunused-parameter, -Wsign-compare, -Wtype-limits и -Wmissing-field-initializers и многие другие. Хотя вполне корректный код может не скомпилироваться под -Wextra (то же самое можно сказать и о -Wall), хорошо написанный код, как правило, не должен вызывать проблем.

Давайте подробно рассмотрим одну из таких опций предупреждения, включенных -Wextra, хорошо известную -Wunused-parameter.

-Wunused-parameter

Это предупреждает всякий раз, когда параметр функции не используется, кроме его объявления, и включается -Wextra, но не -Wall.

Такое предупреждение будет вызвано для следующей функции, так как параметр a не используется в теле функции.

void foo(int a) {
}

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

Тем не менее, хорошо спроектированный код, как правило, должен полностью избегать этого, а наличие неиспользуемого параметра часто может быть признаком запаха кода. Это мало чем отличается от других параметров «семейства -Wunused», таких как -Wunused-function и -Wunused-variable, оба из которых активированы с помощью -Wall.

В любом случае, сопровождающие GCC решили, что -Wall не будет запрашивать -Wunused-parameter.

Включение -Wunused-parameter весьма полезно, чтобы избежать запахов кода. Также тривиально можно подавить предупреждение для определенных параметров, если в этом возникнет необходимость.

Собственная страница руководства GCC предлагает:

Чтобы скрыть это предупреждение, используйте атрибут «unused».

Это будет выглядеть следующим образом:

void bar(__attribute__((unused)) int a) {
}

У этого есть два недостатка:

  1. Это подробный синтаксис.
  2. Это зависит от компилятора, что делает код непереносимым.

Гораздо более очевидным способом подавить предупреждение было бы фиктивное приведение к типу void, обычно выполняемое в самом начале тела функции для удобочитаемости:

void foobar(int a) {
    (void)a;
}

Это делает код более переносимым, менее подробным и более простым для понимания.

Я лично также рекомендую -Werror. Кое-что о превращении всех предупреждений в «ошибки» имеет тенденцию быть более убедительным, что что-то определенно не так. Это, конечно, предотвращает создание ошибочного двоичного файла в целом. Достаточно сказать, что если код не компилируется под -Wall и -Wextra, то вы, вероятно, делаете что-то очень неправильно.

Что, если вы хотите быть педантичным?

Несколько более спорным является вариант -Wpedantic (эквивалентно -pedantic). Если важна переносимость (на мой взгляд, так должно быть всегда), то и -Wpedantic тоже. Конечно, из-за -Wpedantic сообщается о дополнительной диагностике — вещи, которые просто не будут обнаружены с -Wall и -Wextra, и это может быть полезно для обнаружения более тонких проблем.

Истинная цель -Wpedantic всегда заключалась в том, чтобы выдавать все предупреждения, требуемые строгим стандартом ISO C. Однако есть оговорка, что он не гарантирует строгого соответствия стандарту ISO C. На собственной странице руководства GCC -Wpedantic «находит некоторые практики, не относящиеся к ISO, но не все — только те, для которых ISO C требует диагностику, и некоторые другие, для которых диагностика была добавлена».

Часто предупреждения, выдаваемые -Wpedantic, превращаются в ошибки под -Werror, но это не всегда так. Как объясняется на собственной странице руководства GCC, использование -pedantic-errors действительно делает это, но «не эквивалентно -Werror=pedantic, поскольку есть ошибки, разрешенные этой опцией и не разрешенные последней, и наоборот».

Компиляция по стандарту

В контексте параметров предупреждения уместно упомянуть флаг -std= GCC для указания стандарта C. Если не указано, используемый стандарт по умолчанию будет отличаться в зависимости от версии компилятора. В GCC версии 5 используемый стандарт по умолчанию был изменен с -std=gnu90 на -std=gnu11. Затем значение по умолчанию изменилось на -std=gnu17 с выпуском GCC Version 8. Поскольку значение по умолчанию может меняться по мере появления новых стандартов, рекомендуется всегда указывать стандарт при компиляции.

Обратите также внимание на то, что стандартом по умолчанию, используемым GCC, является диалект GNU соответствующего стандарта ISO C. Такие стандарты поддерживают расширения GNU, которые не соответствуют ISO. Компиляция с использованием соответствующего базового стандарта, такого как -std=c99 или -std=c11, позволит полностью избежать расширений GNU. В частности, -Wpedantic и -pedantic-errors будут использовать базовый стандарт, чтобы определить, какие предупреждения/ошибки следует выдавать.

Какие еще существуют предупреждающие флаги?

Любой, кто видел справочную страницу GCC, знает о ее длине и обилии доступных опций.

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

Однако стоит рассмотреть некоторые дополнительные предупреждающие флаги, которые может предложить GCC, и включить их наряду с уже упомянутыми.

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

Далее следует исследование набора менее известных опций предупреждений, доступных в GCC — тех, которые не активируются ни одной из вышеупомянутых опций.

Объясняется их назначение вместе с соответствующими примерами.

-Walloc-zero

Это предупреждает о вызовах функций распределения, украшенных атрибутом alloc_size, которые указывают нулевые байты, например. malloc(0).

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

Компиляция с -Walloc-zero выдаст предупреждение в обеих этих строках:

int *x = malloc(0);
x = realloc(x, 0);

-Wcast-qual

Это предупреждает в двух разных случаях:

  • Всякий раз, когда указатель приводится для удаления квалификатора типа из целевого типа.
  • При создании приведения, которое вводит квалификатор типа небезопасным образом.

Компиляция с -Wcast-qual вызовет такое предупреждение в обоих следующих случаях.

Пример 1:

Преобразование y в char * отбрасывает квалификатор const. Объявление x как char * отключит предупреждение.

const char *x = "x";
char *y = (char *)x;

Пример 2:

Преобразование y в const char ** небезопасно. В целях безопасности все промежуточные указатели в приведении от char ** к const char ** должны иметь квалификацию const. Если y объявлено как const char **x как const char *), это будет безопасно, и предупреждение будет отключено.

char *x = malloc(sizeof *x);
char **y = &x;
const char **z = (const char **)y;

-Wconversion

Это предупреждает о неявных преобразованиях, которые могут изменить значение. В свою очередь, он включает как -Wsign-conversion, так и -Wfloat-conversion.

Для целых чисел -Wsign-conversion обеспечивает создание предупреждения, если знак целого числа может измениться.

Для вещественных чисел -Wfloat-conversion гарантирует, что будет сгенерировано предупреждение, если точность значения может измениться.

Пример 1:

Значение со знаком преобразуется в значение без знака, вызывая предупреждение. Явное приведение формы (unsigned int)-1 отключит предупреждение.

unsigned int x = -1;

Пример 2:

Точность double может быть снижена при преобразовании в float. Явное приведение типа (float)x заглушит сгенерированное предупреждение.

double x = 3.14;
float y = x;

Пример 3:

Преобразование действительного значения в целое число может изменить его значение. Явное приведение типа (int)x заглушит сгенерированное предупреждение.

float x = 42;
int y = x;

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

-Wdouble-promotion

Это предупреждает, когда float неявно повышается до double.

Это часто возникает при выполнении вычислений с использованием литералов с плавающей запятой, которые неявно относятся к типу double.

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

Компиляция с -Wdouble-promotion вызовет такое предупреждение для следующего кода:

float radius = 5;
float area = radius * radius * 3.14; // this line generates a warning

Компиляция вместе с -Wconversion или -Wfloat-conversion вызовет предупреждение в той же строке, поскольку результирующий double в результате вычисления будет преобразован обратно в float, что может привести к потере точности.

Явное приведение литерала 3.14 к типу (float)3.14 отключит оба предупреждения, поскольку вычисления будут выполняться с использованием значений float.

-Wduplicated-branches

Это предупреждает, когда оператор if-else имеет идентичные ветви.

Предупреждение не генерируется, если обе ветки содержат оператор null.

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

int x = 0;
if (x == 0) {
    return x;
} else {
    return x;
}

-Wduplicated-cond

Это предупреждает о повторяющихся условиях в цепочке if-else if.

Тривиальные случаи, такие как следующие, легко обнаруживаются и могут помочь предотвратить простые опечатки в коде.

int x = 0;
if (x == 0) {
} else if (x == 1) {
} else if (x == 0) {
}

-Wfloat-equal

Это предупредит, когда значения с плавающей запятой используются в сравнениях на равенство.

Это обычно считается небезопасным, поскольку значения с плавающей запятой по существу являются приближениями к бесконечно точным действительным числам.

Компиляция с -Wfloat-equal вызовет предупреждение в обоих следующих случаях.

Пример 1:

Две переменные double сравниваются на равенство.

double x = 3.14;
double y = 3.14;
if (x == y) {
    printf("Equal\n");
}

Пример 2:

Переменная double сравнивается с литералом с плавающей запятой на равенство.

double x = 3.14;
if (x == 3.14) {
    printf("Equal\n");
}

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

#define EPSILON 0.00001
double x = 3.14;
double y = 3.14;
if (fabs(x - y) < EPSILON) {
    printf("Equal\n");
}

-Wformat=2

Семейство параметров предупреждения -Wformat предупреждает о неправильно сформированных аргументах форматированных функций ввода и вывода, таких как printf и scanf.

Опция -Wall включает только -Wformat=1.

Дополнительные проверки безопасности выполняются -Wformat=2.

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

Одним из таких примеров является передача переменной в качестве строки формата.

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

В следующем примере строка формата представляет собой переменную, содержащую последовательные спецификаторы формата %s.

В некоторых системах это приведет к сбою программы (просто добавьте больше %s в строку формата, если она не сработает).

Это происходит потому, что printf попытается интерпретировать числа из стека как адреса для строк и разыменовать адреса для печати содержимого. При достаточном количестве %s в строке формата более вероятно, что встречающиеся числа не являются допустимыми адресами, что приводит к сбою.

Предупреждение будет вызвано -Wformat-security, который, в свою очередь, активируется -Wformat=2, но не -Wformat=1.

char *username = "%s%s%s%s%s%s%s%s%s%s%s";
printf(username);

На момент написания -Wformat=2 эквивалентно -Wformat -Wformat-nonliteral -Wformat-security -Wformat-y2k.

Это было изменено с -Wformat -Wformat-nonliteral -Wformat-security в GCC Version 3.4.

На момент написания -Wformat-security является подмножеством -Wformat-nonliteral. Таким образом, любого варианта достаточно, чтобы предупредить о приведенном выше коде. Однако в будущих выпусках могут выполняться дополнительные проверки под -Wformat-security по сравнению с -Wformat-nonliteral.

-Wformat-signedness

Это предупреждает, когда строка формата требует беззнакового аргумента, но аргумент подписан, и наоборот.

Он не включается -Wformat=1 или -Wformat=2, и его полезно указать вместе с -Wformat=2.

Компиляция с -Wformat-signedness вызовет такое предупреждение для следующего кода:

int x = -1;
printf("%u", x);

-Winit-self

Это предупреждает о неинициализированных переменных, которые инициализируются самими собой.

Он должен использоваться вместе с -Wuninitialized, который включен -Wall и -Wextra.

Этот сценарий явно игнорируется -Wuninitialized по умолчанию; -Winit-self включает эту проверку.

Следующий минимальный пример вызовет такое предупреждение:

int x = x;

-Wlogical-op

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

Пример 1:

Сравнение x > 0 используется дважды как операнд одного и того же логического оператора. Такой сценарий вызовет предупреждение, обычно идентифицирующее либо избыточную проверку, либо вероятную опечатку.

int x = 0;
if (x > 0 && x > 0) {
}

Пример 2:

Логический оператор && применяется к переменной и небулевой константе. Это вызовет предупреждение с -Wlogical-op, учитывая, что, скорее всего, имелось в виду побитовое &.

int x = 0;
if (x && 0x00042) {
}

-Wmissing-declarations

Это предупреждает всякий раз, когда глобальная функция определяется без предварительного объявления.

Как указано на собственной странице руководства GCC, его можно использовать «для обнаружения глобальных функций, которые не объявлены в файлах заголовков».

Например, эта функция:

int foo(int i) {
    return i;
}

потребуется как минимум такое объявление, как:

int foo();

для предотвращения срабатывания предупреждения.

Если компилируется вместе с -Wstrict-prototypes, объявление прототипа, например:

int foo(int i);

было бы необходимо. Но, с точки зрения -Wmissing-declaration, оба заявления были бы приемлемыми.

-Wmissing-prototypes

Это предупреждает всякий раз, когда глобальная функция определяется без предыдущего объявления прототипа.

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

Например, эта функция:

int foo(int i) {
    return i;
}

потребуется объявление прототипа, например:

int foo(int i);

для предотвращения срабатывания предупреждения.

Это отличается от -Wmissing-declarations, который не требует, чтобы объявление было прототипом. Следовательно,

int foo();

будет недостаточно, чтобы подавить предупреждение под -Wmissing-prototypes.

-Wpadded

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

Простая перестановка полей struct потенциально может уменьшить и/или устранить заполнение.

Однако не всегда это может быть тривиально возможно сделать.

Рассмотрим следующую надуманную структуру, содержащую две переменные int и одну double.

Под -Wpadded код вызовет два предупреждения — одно для выравнивания double и одно для выравнивания всей структуры.

struct foo {
    int int1;
    double double1;
    int int2;
};

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

struct foo {
    int int1;
    int int2;
    double double1;
};

-Wshadow

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

В следующем простом примере x повторно объявляется в новой области, которая затеняет исходное определение x, вызывая предупреждение с включенным -Wshadow.

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

int x = 0;
{
    int x = 1;
}

-Wstrict-prototypes

Это предупреждает, если функция объявлена ​​или определена без указания типов аргументов.

Пример 1:

Функция main определена без типов аргументов. В C функция main должна быть либо void, как в int main(void), либо иметь 2 аргумента, как в int main(int argc, char *argv[]).

int main() {
    return 0;
}

Пример 2:

Функция return_number определена без типов аргументов. Это должно быть void, но без его указания пользовательский код может непреднамеренно передавать аргументы, как показано.

int return_number() {
    return 42;
}
int main(void) {
    return_number("foobar");
    return 0;
}

-Wswitch-default

Это предупреждает всякий раз, когда оператор switch не имеет регистра default.

Компиляция с -Wswitch-default создаст такое предупреждение для следующего кода:

int i = 0;
switch (i) {
case 0:
    printf("Zero\n");
    break;
case 1:
    printf("One\n");
    break;
}

Эта опция может быть «зашумленной», поскольку допустимый код не обязательно должен иметь регистр default для всех операторов switch.

-Wswitch-enum

Это предупреждает всякий раз, когда оператор switch имеет индекс перечисляемого типа и не имеет case для одного или нескольких именованных кодов этого перечисления.

Это отличается от параметра -Wswitch, включенного параметром -Wall, тем, что этот параметр выдает предупреждение независимо от наличия метки default.

Чтобы лучше различать эти два похожих варианта, давайте рассмотрим три связанных примера с использованием показанного enum EXAMPLE:

enum EXAMPLE {
    FOO,
    BAR,
    FOOBAR
};
enum EXAMPLE foo = FOO;

Этот switch будет генерировать предупреждение при любой включенной опции, поскольку у него нет ни case для FOOBAR, ни default.

switch (foo) {
case FOO:
    printf("foo\n");
    break;
case BAR:
    printf("bar\n");
    break;
}

Этот switch будет генерировать предупреждение только с -Wswitch-enum, поскольку случая default достаточно для удовлетворения требований -Wswitch.

switch (foo) {
case FOO:
    printf("foo\n");
    break;
case BAR:
    printf("bar\n");
    break;
default:
    printf("foobar\n");
    break;
}

Этот switch не будет генерировать предупреждение ни для одного из вариантов, поскольку для всех трех кодов перечисления существует явный case.

switch (foo) {
case FOO:
    printf("foo\n");
    break;
case BAR:
    printf("bar\n");
    break;
case FOOBAR:
    printf("foobar\n");
    break;
}

-Wundef

Это предупреждает, если неопределенный идентификатор оценивается в директиве #if. Такие идентификаторы заменяются нулем.

Предупреждение о неопределенном идентификаторе может указывать на опечатку или другую проблему с кодом, как в следующем случае.

#define M 42
#if N
#endif

Если намерение состоит в том, чтобы проверить, определен ли идентификатор, то его следует явно проверить с помощью оператора defined, как показано здесь.

#if defined N
#endif

-Wunused-macros

Это предупреждает о неиспользуемых макросах, определенных в основном файле.

Под «основным файлом» подразумевается файл .c, в котором определен макрос.

О встроенных макросах, макросах, определенных в командной строке, и макросах, определенных во включаемых файлах, не предупреждают.

В следующей программе константа X определена, но никогда не используется.

Это мало чем отличается от знакомой опции -Wunused-variable с поддержкой -Wall для локальных и статических переменных.

#define X 42
int main(void) {
    return 0;
}

-Wwrite-strings

Это дает строковым константам тип const char[length], так что копирование адреса единицы в указатель, отличный от const char *, приводит к предупреждению.

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

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

В следующем случае выдается предупреждение, поскольку x явно не объявлено как const.

char *x = "foobar";

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

Исходники доступны на GitHub.

Пожалуйста, позаботьтесь об окружающей среде перед печатью.