Тег «C»

Реализация cond на операторе ?:

Если кто будет спрашивать, cond — это из мира Лиспа.

Подобную же конструкцию можно сделать и в C:

var = (cond1 ? expression1 :
       cond2 ? expression2 :
       /* ... */
       else-expression);

Переменной var присваивается значение выражения expression1, если истинно значение выражения cond1, присваивается значение expression2, если истинно значение cond2, и т.д. Если ни один condX не истинен, присваивается значение else-expression.

По-моему, красиво. Для пущего сходства с Лиспом можно добавить скобочек.

УжасноПлохоНормальноХорошоОтлично (Еще не оценили)
Loading ... Loading ...

Переменное число аргументов в функциях 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, описанная выше, — это штука, на постоянство которой не стоит рассчитывать. Оптимизирующие компиляторы могут организовать передачу аргументов через регистры, а уж про перенос программы на другие платформы и говорить не приходится. Еще одна рекомендация — пишите такие функции только для форматирования текста. Другие применения обычно создают больше проблем, чем решают.

УжасноПлохоНормальноХорошоОтлично (Еще не оценили)
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-кода.
УжасноПлохоНормальноХорошоОтлично (Еще не оценили)
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.

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

УжасноПлохоНормальноХорошоОтлично (Еще не оценили)
Loading ... Loading ...

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

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

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

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

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

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

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

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

!a != !b

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

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