Чистый C для C++-ника |
[Sep. 19th, 2012|01:20 am] |
На работе сейчас мне приходится в основном писать на языке C++. Я, видимо, настолько громко ругался по этому поводу, что один коллега меня спросил, не могу ли я
ему посоветовать текст, как для программиста, привыкшего к плюсам, перейти на чистый C. Я пошарил по сети, ничего в этом роде не нашел и решил написать такой ликбезик сам. Возможно, он будет пополняться и улучшаться со временем; с радостью учту ваши комментарии.
Разумеется, многое здесь для приличного C-программиста будет очевидным; я прошу прощения за тривиальность. С другой стороны, если мои собственные предпочтения расходятся с общепринятой практикой, я тоже буду это указывать.
Вопрос, почему переходить именно на C, а не на какой-нибудь более продвинутый язык1, здесь не ставится2.
Итак:
- Книга Кернигана и Ритчи великолепна. Если Вы ее еще не прочитали, сделайте это.
- Помимо нее, есть различные style guides. Вот, например, рекомендации Пайка; а
вот правила
оформления кода в ядре Linux.
- Наконец, вокруг много хорошо написанного кода (например,
многие опенсорсные библиотеки). Этот код полезно читать
и имитировать.
- После того, как я пережил увлечение C++, мой код на C стал
лучше. ООП — небесполезный набор методик и эвристик. При
отказе от C++ необязательно забывать их все.
- Для каждой структуры желательно иметь функцию-фабрику, которая ее
выделяет и инициализирует, и функцию-деструктор, которая освобождает
ресурсы. Имена функций, работающих со структурой, должны иметь
соответствующий префикс.
qz_t *make_qz(int a, float b);
void qz_free(qz_t *qz);
int qz_foo(qz_t *qz, qq_t *qq, int n);
- Как только возникает потребность в полиморфном (в ООП смысле) поведении, полезно
завести структуру-"поведение" из указателей на функции, и держать ее
первым полем у соответствующего типа. При этом указатель на
структуру-"потомок" можно спокойно преобразовывать в указатель на
структуру-предок (а внутри "методов" — обратно).
struct qz_ops {
int (*foo)(qz_t *qz, int a);
double (*bar)(qz_t *qz, char *s);
};
struct qz {
qz_ops *ops;
}
struct qz_ops qqq_ops = { // стиль объявления C99
.foo = qqq_foo,
.bar = qqq_bar
};
struct qqq {
qz_t qz;
int other_field;
}
и потом
qz_t *q = make_qqq(); // проинициализирует qqq и установит поле ops
q->ops->foo(q, 33);
Грубо говоря, эти два правила имитируют C++-классы на коленке, и
оказывается, что весь плюсовый синтаксический сахар повышает удобство
очень ненамного.
- Желательно активно пользоваться ограничением видимости. То есть,
почти любой тип определяется в хедере просто как
typedef struct qz qz_t;
в публичных функциях (перечисленных в том же хедере) используется
qz_t*, а реальное определение структуры сидит уже в C-шнике. При этом
все неэкспортируемые через хедер функции в C-шном файле обозначены как
static.
В результате, в отличие от C++, заголовочные файлы на чистом C
вполне можно читать подряд; мусора, касающегося деталей реализации,
там нет или почти нет.
Это правило вступает в противоречие с предыдущим: если мы
устраиваем полиморфную структуру, то, по крайней мере, ее
член ops должен быть виден извне.
- Желательно взять какую-нибудь стандартную библиотеку со структурами
данных. Я
пользовался glib,
причем в основном ее низкоуровневой частью
(то есть, например, списки и хэши оттуда, а какие-нибудь GString или
объекты -- нафиг). Но примерно ко времени, когда на предыдущей работе
стал активно перелезать на Scheme, уже хотелось поискать что-то
другое. Вроде в проекте Apache есть базовая библиотека APR.
Я никогда с ней не работал, но выглядит как разумная альтернатива.
Имеет смысл посмотреть и выбрать.
- Я сначала пользовался своей доморощенной библиотечкой со счетчиками
ссылок, потом перелез
на Boehm gc.
(для пользования Boehm есть свой набор правил, чтобы получалось
безопасно и эффективно).
Использовать сборку мусора, разумеется, можно только в том
случае, если Вы пишете приложение или такую библиотеку, которая
может навязать правила обращения с памятью своим клиентам. В
противном случае придется отслеживать указатели
самим. Желательно в библиотеке позволять пользователю
задать свои malloc/free (разумеется, через
указатели).
Так или иначе, иметь продуманную политику работы с памятью надо обязательно.
-
- Исключений, понятно, нет. Можно либо возвращать ошибку, если
функция исходно была бы void, либо передавть указатель на ошибку
отдельным аргументом:
qq_t* qz_make_qq(qz_t *qz, int a1, char *a2, err_t **err);
и потом
{
err_t *err = NULL;
qq_t *qq;
qq = qz_make_qq(qz, 33, "xxx", &err);
if (err) {
...
}
}
Это, надо сказать, не слишком красиво. Очень жаль, что в C нет
множественных возвращаемых значений.
-
Обрабатывать ошибки можно так:
int foo() {
int fd1 = -1;
int fd2 = -1;
int fd3 = -1;
fd1 = open(...);
if (fd1 < 0) goto OUT;
fd2 = open(...);
if (fd2 < 0) goto OUT;
fd3 = open(...);
if (fd3 < 0) goto OUT;
...
OUT:
close(fd1);
close(fd2);
close(fd3);
}
При этом деструктороподобные функции должны справляться с -1 или NULL.
(close и free это умеют, кстати.)
-
Лично я в своем коде не использовал модификатор const. По-моему, он
очень коряво встроен в общую систему типов.
То есть, правила, по которым возможны/невозможны присваивания,
например, между типами
int (*)(int **);
int (*)(int *const *);
int (*)(int const **);
int (* const)(int const *const *);
и пр. — эти правила, конечно, оправданы, но отслеживать правильность
всех подобных типов мне оказывается тяжелее, чем вылавливать
относительно редкие ошибки из-за того, что что-то лишнее присвоилось
переменной, которая должна была быть const. И первый из этих типов
читается проще, хоть и дает наименьшие гарантии.
С другой стороны, проблема
таки реальная, так что я рекомендую эту политику не
"вообще", а просто как деталь моего собственного вкуса.
Кстати, в C, поскольку передача параметров только по значению, часто
сигналом модифицируемости оказывается амперсанд перед параметром.
Т.е., в функции
int foo1(qz_t *qz);
qz не поменяется (хотя может измениться содержимое памяти
по указателю). по крайней
мере, он останется "тем же" объектом. А в
int foo2(qz_t **qzp);
c qzp может случиться что угодно, и это видно по вызову foo2(&qz).
- Вообще-то C позволяет передавать структуры по значению в функции и возвращать их как результаты,
а также присваивать их друг другу. Но лично я стараюсь, чтобы всякое присваивание и передача параметра в моих программах приводили к пересылке
одного слова и не больше. То есть, если есть
struct qz {
int i;
double d;
};
struct qz a, b;
то я вместо
a = b;
напишу уж лучше
memmove(&a, &b, sizeof(a));
Мне так читать код оказывается проще.
-
В уже
упомянутых рекомендациях
Пайка, среди прочего, есть замечательное и очень нестандартное правило
`include files should never include include files'.
Я его в свое время у себя в проектах принял и не пожалел. Т. е., вместо
#ifndef qz_H
# define qz_H
...
#endif // qz_H
я писал
#ifdef qz_H
# error "qz.h doubly included"
#endif
#define qz_H
То есть, поскольку в C (и C++) нет никакой разумной системы модулей,
правильнее заставлять программиста в начале каждого C-файла явно
перечислять все зависимости.
Но это практика и для языка C очень необычная. Я бы не решился
ее применять в библиотеке, если я не властен над стилем ее пользователей.
1
Для меня это будет, в зависимости от задачи и настроения, один из Go,
OCaml, Haskell, Scheme, Python или Erlang.
2
Можно, например, считать, что мы пишем переносимый код для
нескольких сразу мобильных платформ, и усилий по перетаскиванию
продвинутой среды слишком много, а сама среда отъест неприемлемо много места.
Или требуется написать библиотеку, чей интерфейс будет доступен из многих высокоуровневых языков ( yushi, спасибо за напоминание!). |
|
|