Тег «C»

Переменное число аргументов в функциях C

Полезная, но редко используемая и часто недопонимаемая возможность языка C — переменное число аргументов функции. Это такая особая сишная магия, позволяющая писать функции наподобие printf(). Обычно такие функции пишутся именно для форматированного вывода текста, хотя возможны и другие применения. В учебниках любят приводить в качестве примера функцию сложения произвольного числа аргументов, но вряд ли такое можно увидеть в реальном коде.

Выглядеть определение такой безразмерной функции может как-то так (три точки в списке формальных аргументов следует воспринимать буквально, то есть там именно три точки):

int myfunct(int arg1, double arg2, ...) { /* какой-то код */ }

Так как в языке C обычно придерживаются соглашения о вызовах cdecl, то аргументы, передаваемые функции, передаются через стек, причем кладутся туда, начиная с последнего (правого) аргумента и заканчивая первым. Вот вам примерный портрет стекового фрейма для вызова функции с N аргументами на процессоре x86:

Стековый фрейм

Все наши знания о фрейме ограничены значениями регистров ESP и EBP. Та точка отсчета, от которой мы можем отталкиваться для доступа к аргументам — текущее значение регистра EBP. Например, первый аргумент лежит по адресу EBP+8, а второй — по адресу EBP+12. Ладно, а сколько нужно прибавить к EBP, чтобы получить адрес N-го аргумента в стеке? Для этого нужно знать:

  • сколько всего передано аргументов;
  • размеры аргументов (кто сказал, что они должны быть одинаковы?).

Ни то, ни другое в доступных нам ESP и EBP не хранится. Знаем мы только то, что именованные аргументы имеют меньшие адреса. Выходит, что размеры аргументов должны быть жестко определены в теле функции или же получены на основе значений именованных аргументов. Но как определить их количество? Рассчитывать на форматную строку, как в printf? Но кто гарантирует, что эта строка правильная?

Что, страшно уже? К счастью, всю низкоуровневую рутину с жоглированием указателями и размерами готов взять на себя компилятор, предоставляя нам удобный интерфейс в заголовочном файле stdarg.h. Правда, количество аргументов и их типы нам все-таки придется выяснять самостоятельно.

Правила нехитрые:

  • функция должна иметь по крайней мере один именованный параметр (именованные идут в списке формальных параметров перед неименованными);
  • перед доступом к аргументам нужно вызвать va_start, чтобы инициализировать объект контекста (va_list);
  • используем va_arg для последовательного доступа к неименованным аргументам;
  • по окончанию вызываем va_end в той же функции, что и va_start.

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

#include <stdio.h>
#include <stdarg.h>
int sum(int count, ...) {
    va_list args;
    int total = 0;
    va_start(args, count);
    while (count--)
        total += va_arg(args, int);
    va_end(args);
    return total;
}
int main() {
    printf("%d\n", sum(1, 33));
    printf("%d\n", sum(4, 1, 2, 3, 4));
    return 0;
}

Пояснения здесь излишни. Кстати, va_list можно передавать в качестве аргумента другой функции. Надо только не забыть вызвать для него va_end в той же функции (а точнее, в том же вызове функции), где был вызван va_start. Не знаю точно, что делает va_end, но если он манипулирует значением EBP, то лучше бы это значение было тем же, что и раньше.

Зачем может понадобиться передача va_list? Обычно для написания оберток для функций семейства printf, например, в каком-нибудь логгере:

int msgOut(enum msgLevel level, const char *fmt, ...) {
    va_list ap;
    int ret;
    va_start(ap, fmt);
    ret = msgOutL(level, fmt, ap);
    va_end(ap);
    return ret;
}
int msgOutL(enum msgLevel level, const char *fmt, va_list vl) {
    if (level >= s_level)
        return vfprintf(s_stream ? s_stream : stdout, fmt, vl);
    else
        return 0;
}

Напоследок хочется сказать вот что. Если вы пишете функцию с переменным числом аргументов не от нечего делать, а в реальном проекте, то использование stdarg.h — единственный вариант. Организация стека в x86, описанная выше, — это штука, на постоянство которой не стоит рассчитывать. Оптимизирующие компиляторы могут организовать передачу аргументов через регистры, а уж про перенос программы на другие платформы и говорить не приходится. Еще одна рекомендация — пишите такие функции только для форматирования текста. Другие применения обычно создают больше проблем, чем решают.

УжасноПлохоНормальноХорошоОтлично (3 голосов, средний: 3,33 из 5)
Loading ... Loading ...

Ссылки для C-гиков

Такая вот подборка в дополнение к опубликованному ранее:

  • C @ Interview Mantra — подборка задач на C, предлагаемых на собеседованиях.
  • C Puzzles — набор сишных головоломок, неплохо прокачивает знание тонких моментов языка.
  • C Extensions — описание того, что GCC привносит в C помимо требований стандарта.
  • Coroutines in C — статья о реализации сопрограмм на C.
  • How to Use the restrict Qualifier — что такое квалификатор restrict и когда его нужно использовать.
  • Ampifying C — совершенно сносящая крышу статья, описывающая применение генерации C-кода из Lisp-кода.
УжасноПлохоНормальноХорошоОтлично (1 голосов, средний: 5,00 из 5)
Loading ... Loading ...

Ссылки для изучающих C

Вчера была нетленка, поэтому сегодня расслаблюсь и просто дам несколько ссылок на бесплатные, но очень полезные ресурсы по языку C.

  • C Elements of Style — сокращенный вариант старой доброй книжки лохматого года; слегка устарела, но все равно содержит массу полезного;
  • Learning GNU C — туториал про одноименный диалект C;
  • An Introduction to GCC — руководство по gcc и g++;
  • C FAQ — огромный FAQ по языку;
  • Writing Bug-free C Code — оригинальная методология программирования на C с элементами ООП, позволяющая вроде бы упростить обнаружение ошибок; почитать интересно, но применять стремно;
  • The GNU C Library — мануал по стандартной библиотеке C в исполнении GNU;
  • Top 10 Ways to be Screwed by C — топ 10 сишных граблей; будет особенно полезно начинающим;
  • (UPD) Programming in C — курс лекций по С с упором на UNIX-среду;
  • (UPD) C Traps and Pitfalls — статья про скользкие места C;
  • (UPD) Notes on Writing Portable Programs in C — статья про написание переносимых программ на C; категорически рекомендую;
  • (UPD) Indian Hill: Recommended C Style and Coding Standards — рекомендации по стилю C-программ от ребят из одной лаборатории AT&T.

Интересные ссылки из комментов переносятся в основной пост, так что пишите.

УжасноПлохоНормальноХорошоОтлично (2 голосов, средний: 5,00 из 5)
Loading ... Loading ...

Обработка ошибок и освобождение ресурсов на C

Некая усредненная функция обычно занимается чем-то таким:

  1. Получает/захватывает/выделяет необходимые для работы ресурсы.
  2. Выполняет содержательные действия.
  3. Освобождает ресурсы.

На каждом из предыдущих этапов вдобавок должно выполняться обнаружение и обработка ошибок. И вот здесь-то и начинается самое интересное. Казалось бы, нет ничего сложного. Ну, обнаружили ошибку. Ну, обработали. В чем проблема-то? А проблема в деталях. Могут иметь место такие, например, случаи:

  1. Функция получает не один ресурс, а N. Соответственно, все полученные ресурсы нужно освободить. А если при получении i-го ресурса произошла ошибка, то нужно освободить только ресурсы 0…i-1.
  2. Если в содержательных действиях происходит ошибка, то перед освобождением ресурсов может понадобиться эту ошибку как-то обработать. Хорошо если такая ошибка может возникнуть только в одном месте — прямо там и обработаем. А что если в разных местах могут возникнуть ошибки, требующие одинаковой обработки? Еще после возникновения ошибки надо как-то покинуть содержательный код.
  3. Если ошибка происходит в цикле со вложенностью 2 и более, то надо как-то все эти циклы покинуть.
  4. Будем предполагать, что обработка ошибки не ограничивается возвратом кода ошибки, а включает какую-то более сложную обработку: запись в лог, выдача сообщений и т.п.

А еще мы хотим, чтобы наш код нормально читался, быстро работал и был пригоден для дальнейшего расширения. Такие вот мы противоречивые. Что делать будем? Есть несколько вариантов.

Вариант 0, «тупой»

Что думаю, то и пишу.

int function(...) {
    // ...здесь объявления переменных...
    if (get_res1(&res1))
        return ERROR1;
    if (get_res2(&res2)) {
        release_res1(&res1);
        return ERROR2;
    {
    // ...
    if (get_res25(&res25)) {
        release_res1(&res1);
        release_res2(&res2);
        // ...
        release_res24(&res24);
    }
    // ...какой-то содержательный код...
    // ...ошибка!
    if (error) {
        release_res1(&res1);
        release_res2(&res2);
        // ...
        release_res25(&res25);
        return SOME_ERROR;
    }
 
    release_res1(&res1);
    release_res2(&res2);
    // ...
    release_res25(&res25);
 
    return 0;
}

Плюсы: не обнаружены.

Минусы:

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

Вариант 1, «правильный»

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

int function(...) {
    struct data data;
    int r_code;
 
    r_code = get_resources(&data);
    if (!r_code) do_work(&data);
    if (r_code) handle_error(r_code, &data);
    release_resources(&data);
 
    return r_code;
}

Что там делают функции get_resources(), do_work(), handle_error() и release_resources() — нам неинтересно. Конечно, приведенный выше код — всего лишь демонстрация идеи. Очевидно, если все функции писать в таком духе, то до нижнего уровня, который, собственно, и делает фактическую работу, мы никогда не дойдем. Так и погрязнем в бесконечных слоях абстракций. Поэтому вместо do_work() обычно пишется компактный код, выполняющий требуемую работу, но без излишнего углубления в детали.

Плюсы:

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

Минусы:

  • относительно низкая эффективность;
  • наличие синтетической структуры данных (struct data), хранящей состояние ресурсов.

Вариант 2, «тру-хакерский»

Настоящие хакеры не боятся goto. Настоящие хакеры плевать хотели на предрассудки. Мнение о вредности goto было специально распространено ими среди начинающих; goto в коде хакера — как катана в руках самурая: новичок может покалечить себя, а мастер — всех остальных.

int function(...) {
    // ...здесь объявления переменных...
 
    if (get_res1(&res1)) {
        ret = ERROR1;
        goto exit;
    }
    if (get_res2(&res2)) {
        ret = ERROR2;
        goto exit1;
    }
    // ...
    if (get_res25(&res25)) {
        ret = ERROR25;
        goto exit24;
    }
    // ...какой-то содержательный код...
    // ...ошибка!
    if (error) {
        ret = SOME_ERROR;
        goto error;
    }
    // ...
    goto exitOK;
 
error:
    // ...
 
exitOK:
    release_res25(&res25);
exit24:
    release_res24(&res24);
// ...
exit2:
    release_res2(&res2);
exit1:
    release_res1(&res1);
exit:
    return ret;
}

Идея схожа с предыдущим вариантом: локализовать места обработки ошибок и освобождения ресурсов. За счет fallthrough-стиля освобождение происходит очень изящно. Главная проблема при использовании goto для обработки ошибок и освобождения ресурсов — соблюдать меру. Не зря ведь в моем примере 25 ресурсов: в случае использования варианта №1 размер кода не зависит от их количества, здесь же мы получаем спагетти. Использование goto позволяет сократить код по сравнению с вариантом №0, но таит в себе опасность бесконечного увеличения размера функции, так как возможности рефакторинга здесь ограничены.

Такой подход можно встретить, например, в ядрах Linux и FreeBSD.

Плюсы:

  • неплохая читабельность при аккуратной реализации;
  • высокая эффективность;
  • одна точка выхода.

Минусы:

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

Вариант 3, «gotoфобский»

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

int function(...) {
    // ...здесь объявления переменных...
 
    do {
        if (get_res1(&res1)) {
            ret = ERROR1;
            break;
        }
        if (get_res2(&res2)) {
            ret = ERROR2;
            break;
        }
        // ...
        if (get_res25(&res25)) {
            ret = ERROR25;
            break;
        }
        // ...какой-то содержательный код...
        // ...ошибка!
        if (error) {
            ret = SOME_ERROR;
            break;
        }
        // ...
    } while(0);
 
    if (ret)
        ret = handle_error(ret, ...);
 
    if (res1_allocated(&res1))
        release_res1(&res1);
    if (res2_allocated(&res2))
        release_res2(&res2);
    // ...
    if (res25_allocated(&res25))
        release_res25(&res25);
 
    return ret;
}

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

Плюсы:

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

Минусы:

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

Итого

Конечно, можно придумать еще мульён вариантов, один другого краше. Есть даже отдельная когорта любителей «метапрограммирования» (тыц, тыц). Я такие вещи не понимаю и не одобряю. Если уж есть такая тяга к синтаксису try-catch, то почему бы не пользоваться C++? В подавляющем большинстве случаев нет причины, которая позволяла бы пользоваться C, но исключала возможность использования C++ (хотя у меня именно такой редкий случай).

Чем же пользоваться? Мое мнение такое:

  • всегда писать согласно варианту 1;
  • если в результате работает слишком медленно, переписать отдельные функции (или даже модули) по варианту 2.

Жду гневные, но конструктивные комментарии.

УжасноПлохоНормальноХорошоОтлично (1 голосов, средний: 5,00 из 5)
Loading ... Loading ...

Развлечения на Си

FallenGamer своим постом вызвал у меня острый приступ ностальгии:

int i = 10;
while (i --> 0) // "оператор -->"
    foo();

Хотя его пост был про C#, но такая штука сработает практически во всех C-подобных языках. Я сразу вспомнил еще пару подобных забавных конструкций. Например, интересное определение макросов TRUE и FALSE:

#define TRUE ('/'/'/')
#define FALSE ('-'-'-')

или «оператор приведения к bool»:

int a = 0, b = 123;
a = !!a; // 0
b = !!b; // 1

А еще вот — логический XOR:

!a != !b

Очень вас прошу, не пользуйтесь в своем коде чем-либо подобным… Я уж и не говорю про примеры из книги «Алгоритмические трюки для программистов».

УжасноПлохоНормальноХорошоОтлично (2 голосов, средний: 5,00 из 5)
Loading ... Loading ...

Книга: «Программирование в стандарте POSIX. Часть 1»

Галатенко В.А. Программирование в стандарте POSIX. Часть 1Название: Программирование в стандарте POSIX. Курс лекций. Учебное пособие. Часть 1.
Автор: В.А. Галатенко
Год выхода: 2004
Издательство: Интернет-Университет Информационных Технологий
Тираж: 2000
Объем: 560 стр.
Обложка: твердая
Где покупал: нигде (подарок)

Книгу мне подарил лично Владимир Антонович, поэтому я посчитал себя обязанным ее всю прочитать и поделиться впечатлениями с «уважаемым all».

Книга является дословным бумажным воплощением одноименного курса на Интуите. Так что все сказанное в равной степени справедливо и для оного курса.

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

Лично я теперь пользуюсь этим курсом как прекрасно откомментированным и снабженным примерами man’ом. Программирую, например, что-то связанное со взаимодействием процессов — сразу лезу в соответствующий раздел (кстати, книга в плане поиска нужного места гораздо удобнее онлайн-версии), читаю все подряд и дальше уже с полнейшей уверенностью пишу все как надо. Что выгодно отличает книгу от man’а, так это комплексная подача материала: описываются не отдельные функции, а их работа в контексте общей проблемы. Да еще и примеры достаточно объемные.

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

Существует еще и вторая часть книги, в которой рассказывается о «продвинутых» возможностях: потоки, средства реального времени, асинхронный ввод-вывод, трассировка и т.д. Уже взялся ее читать, ждите обзор.

Минусы:

  • код набран слишком крупным шрифтом, ширины страницы часто не хватает, да и по вертикали на странице помещается мало строк.

Плюсы:

  • подробное, но при этом очень сжатое изложение; никакой воды, только информация;
  • рассматриваются многие неочевидные моменты — видно глубокое понимание материала;
  • отличное качество печати и переплета.
УжасноПлохоНормальноХорошоОтлично (1 голосов, средний: 3,00 из 5)
Loading ... Loading ...