Тег «C»

Ссылки для 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 --> 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

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

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

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

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

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

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

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

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

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

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

Минусы:

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

Плюсы:

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

Duff’s Device

9 ноября 1983 года Том Дафф изобрел «устройство» имени себя. Вот так оно выглядело:

send(to, from, count)
	register short *to, *from;
	register count;
	{
		register n=(count+7)/8;
		switch(count%8){
		case 0:	do{	*to = *from++;
		case 7:		*to = *from++;
		case 6:		*to = *from++;
		case 5:		*to = *from++;
		case 4:		*to = *from++;
		case 3:		*to = *from++;
		case 2:		*to = *from++;
		case 1:		*to = *from++;
			}while(--n>0);
		}
	}

Такая конструкция предназначалась для раскрутки цикла копирования фрагмента памяти в регистр пословно. Развертывание сокращает количество итераций цикла и, как следствие, количество операций сравнения (в данном случае в 8 раз), тем самым слегка повышая производительность.

Чтобы лучше понять, что происходит, предлагаю читателям запустить следующую программу (я немного изменил изначальный вариант):

#include 
 
int c = 0;
int a = 10;
 
void foo()
{
    printf("%d ", c++);
}
 
void bar()
{
    printf("%d\n", a);
}
 
int main()
{
    switch (a&3) {
    case 0: do { foo(); bar();
    case 3:      foo(); bar();
    case 2:      foo(); bar();
    case 1:      foo(); bar();
	       } while((a -= 4) >= 0);
    }
    return 0;
}

Самое удивительное в этом всем — вовсе не корявое «столбчатое» форматирование. Удивительно то, что язык позволяет размещать цикл внутри блока switch. Интуитивно нам кажется, что каждый case начинает новый блок, и что нельзя соорудить блок, заключающий в себя несколько case’ов. А приведенный выше пример вообще представляется надругательством над синтаксисом C и здравым смыслом. Но стоит задуматься о том, как именно реализована конструкция switch-case, как сразу все становится на свои места:

// исходный код
switch (a) {
    case 0:
        a++;
        break;
    case 1:
        a--;
}
 
// что происходит на самом деле (примерно)
case_0:
    if (a == 1) goto case_1;
    a++;
    goto end;
case_1:
    a--;
end:

Вот так! Теперь понятно, что внутри конструкции switch можно развернуться от души, чем и не преминул воспользоваться Дафф.

Вынужден разочаровать любителей микрооптимизаций: подобное развертывание циклов выполняется современными компиляторами автоматически, и, скорее всего, намного эффективнее. Компилятор ведь может себе позволить не заботиться о чистоте кода. Так что устройство Даффа теперь интересно разве что любителям истории программирования (вроде меня).

Жаждущих подробностей отправляю к оригинальному сообщению Даффа и статье в Википедии.

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