0Pricing
C Academy · Урок

Проблемы безопасности типов

Риски функций с переменным числом аргументов

«Проблемы безопасности типов» — бесплатный урок C Academy на CoddyKit. Это урок 3 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения C Academy, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс C Academy содержит 4 уроков всего.

Переменное количество аргументов обходит проверку типов

Обычные функции C проверяют соответствие аргументов типам параметров. Аргументы переменного количества полностью пропускают эту проверку.

Компилятор не может проверить, что Вы передаёте после ..., поэтому ошибки легко допустить, и они могут быть опасными.

#include <stdio.h>
#include <stdarg.h>

int add(int n, ...) {
    va_list a; va_start(a, n);
    int s = 0;
    for (int i = 0; i < n; i++) s += va_arg(a, int);
    va_end(a);
    return s;
}

int main(void) {
    printf("%d\n", add(2, 3, 4));
    return 0;
}

Неверный тип в va_arg

Если прочитать аргумент с неверным типом, результатом будет неопределённое поведение.

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

#include <stdio.h>
#include <stdarg.h>

int read_int(int n, ...) {
    va_list a; va_start(a, n);
    int v = va_arg(a, int);
    va_end(a);
    return v;
}

int main(void) {
    printf("%d\n", read_int(1, 100));
    return 0;
}

Несоответствие строк формата

В функциях в стиле printf неверный спецификатор — классическая ошибка. printf("%d", 3.14) читает значение double так, будто это int.

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

#include <stdio.h>

int main(void) {
    double pi = 3.14;
    printf("%f\n", pi);
    return 0;
}

Неожиданные результаты продвижения целых типов

В части с переменным количеством аргументов малые типы продвигаются до int. Поэтому даже если Вы передаёте char, прочитать его обратно нужно как int.

Чтение как char с помощью va_arg(a, char) приводит к неопределённому поведению.

#include <stdio.h>
#include <stdarg.h>

void show(int n, ...) {
    va_list a; va_start(a, n);
    int c = va_arg(a, int);
    printf("char is %c\n", c);
    va_end(a);
}

int main(void) {
    show(1, 'A');
    return 0;
}

Float преобразуется в double

Аргумент типа float всегда продвигается до double в функциях с переменным количеством аргументов. Вы должны читать его как double, а не как float.

Это одна из самых распространённых ошибок при работе с переменным количеством аргументов.

#include <stdio.h>
#include <stdarg.h>

void show(int n, ...) {
    va_list a; va_start(a, n);
    double d = va_arg(a, double);
    printf("%.2f\n", d);
    va_end(a);
}

int main(void) {
    float f = 1.5f;
    show(1, f);
    return 0;
}

Чтение слишком большого количества аргументов

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

Именно поэтому необходимо точно знать количество аргументов или использовать маркер.

#include <stdio.h>
#include <stdarg.h>

int sum(int n, ...) {
    va_list a; va_start(a, n);
    int s = 0;
    for (int i = 0; i < n; i++) s += va_arg(a, int);
    va_end(a);
    return s;
}

int main(void) {
    printf("%d\n", sum(3, 1, 2, 3));
    return 0;
}

Маркеры с указателем NULL требуют осторожности

Использовать NULL как маркер рискованно: на некоторых платформах NULL имеет тип 0 как int, а его размер отличается от размера указателя.

Явно приводите его к нужному типу, например (char *)NULL, чтобы гарантировать передачу значения правильного типа.

#include <stdio.h>
#include <stdarg.h>

void print_all(const char *first, ...) {
    va_list a; va_start(a, first);
    const char *s = first;
    while (s) { printf("%s\n", s); s = va_arg(a, const char *); }
    va_end(a);
}

int main(void) {
    print_all("a", "b", (char *)NULL);
    return 0;
}

Атрибуты формата компилятора

GCC и Clang предоставляют __attribute__((format(printf, 1, 2))), чтобы сообщить компилятору, что Ваша функция использует форматирование в стиле printf.

После этого компилятор проверяет строки формата во время компиляции и обнаруживает множество ошибок.

#include <stdio.h>
#include <stdarg.h>

void logf(const char *fmt, ...)
    __attribute__((format(printf, 1, 2)));

void logf(const char *fmt, ...) {
    va_list a; va_start(a, fmt);
    vprintf(fmt, a);
    va_end(a);
}

int main(void) {
    logf("%d items\n", 3);
    return 0;
}

Отдавайте предпочтение более безопасным альтернативам

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

Используйте переменное количество аргументов только при настоящей необходимости в стиле printf.

#include <stdio.h>

int sum_array(const int *a, int n) {
    int s = 0;
    for (int i = 0; i < n; i++) s += a[i];
    return s;
}

int main(void) {
    int nums[] = {1, 2, 3, 4};
    printf("%d\n", sum_array(nums, 4));
    return 0;
}

Документируйте соглашение

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

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

#include <stdio.h>
#include <stdarg.h>

/* sum_ints(n, ...): pass exactly n int values */
int sum_ints(int n, ...) {
    va_list a; va_start(a, n);
    int s = 0;
    for (int i = 0; i < n; i++) s += va_arg(a, int);
    va_end(a);
    return s;
}

int main(void) {
    printf("%d\n", sum_ints(2, 5, 5));
    return 0;
}

Тщательное тестирование

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

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

#include <stdio.h>
#include <stdarg.h>

int sum(int n, ...) {
    va_list a; va_start(a, n);
    int s = 0;
    for (int i = 0; i < n; i++) s += va_arg(a, int);
    va_end(a);
    return s;
}

int main(void) {
    printf("%d\n", sum(0));
    printf("%d\n", sum(1, 9));
    return 0;
}

Быстрая проверка

Проверьте, насколько хорошо Вы поняли риски типов при работе с переменным количеством аргументов.

Итоги

Вы изучили опасности функций с переменным количеством аргументов:

  • Компилятор не проверяет типы аргументов переменного количества.
  • Неверные типы, неверное количество и неправильные маркеры приводят к неопределённому поведению.
  • Помните о продвижении: малые целые числа преобразуются в int, а float — в double.
  • Используйте атрибуты формата компилятора, документируйте соглашение и по возможности отдавайте предпочтение типобезопасным альтернативам.

Часто задаваемые вопросы

Урок «Проблемы безопасности типов» бесплатный?

Да — полный текст урока «Проблемы безопасности типов» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс C Academy, подпишись на CoddyKit PRO. Курс C Academy содержит 4 уроков всего.

Чему я научусь в уроке «Проблемы безопасности типов»?

Риски функций с переменным числом аргументов Ты практикуешь C Academy с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.

Нужен ли мне опыт, чтобы начать C Academy?

Предыдущий опыт не требуется. C Academy на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 3 из 4.

Сколько времени занимает урок «Проблемы безопасности типов»?

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

Можно ли писать и запускать код в этом уроке C Academy?

Да. Каждый урок C Academy включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.

Все уроки этого курса

  1. Макросы stdarg
  2. Написание функций, подобных printf
  3. Проблемы безопасности типов
  4. Практические примеры
← Назад к C Academy