Проблемы безопасности типов
Риски функций с переменным числом аргументов
«Проблемы безопасности типов» — бесплатный урок 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 — локальная установка не требуется.
Все уроки этого курса
- Макросы stdarg
- Написание функций, подобных printf
- Проблемы безопасности типов
- Практические примеры