[Перевод] Малоизвестные возможности языка C
Если у вас несколько лет опыта программирования на языке C, то, вероятно, вы гораздо более уверены в своих знаниях этого языка, чем если бы вы провели столько же времени, работая с C++ или Java.
И язык C, и его стандартная библиотека довольно близки к к минимально возможному размеру.
Текущая наиболее часто используемая версия языка, c99, принесла много новых возможностей, многие из которых совершенно неизвестны большинству программистов на C (в более старых спецификациях, очевидно, тоже есть свои темные уголки).
Вот те, о которых я знаю:
Sizeof может иметь побочные эффекты
int main(void) {
return sizeof(int[printf("ooops\n")]);
}
sizeof
на переменных типах требует исполнения произвольного кода.
Шестнадцатеричный float с экспонентой
int main() {
return assert(0xap-1 == 5.0);
}
p
означает степень, и за ним следует знаковая экспонента, закодированная по основанию 10. Выражение имеет тип double
, но его можно изменить на float
, добавив к литералу символ f
.
Совместимые объявления и массивы как параметры функций
#include
void a(); // 1
void a(long story, int a[*], int b[static 12][*][*]); // 2
void a(long story, int a[42], int b[*][*][64]); // 3
void a(long story, int a[*], int b[const 42][24][*]); // 4
// void a(long story, int a[*], int b[*][666][*]); // 5
// void a(long story, int a[*], int b[*][*][666]); // 6
void a(long story, int a[42], int b[restrict 0 * story + a[0]][24][64]) {
printf("%zu\n", sizeof(a));
printf("%zu\n", sizeof(b));
}
int main() {
a(0, 0, 0);
return 0;
}
Здесь происходит много чего:
Можно объявлять одну и ту же функцию несколько раз, если их объявления совместимы, что означает, что если у них есть параметры, то оба объявления должны иметь совместимые параметры.
Если на момент объявления размер какого-либо массива неизвестен, то вместо него можно написать
[]
.Определители типа можно заключить внутри скобок массива, чтобы добавить информацию о свойствах массива. Если присутствует ключевое слово
static
, размер массива не игнорируется, а интерпретируется как фактический минимальный размер. Квалификаторы типов иstatic
могут находиться только внутри скобок первой размерности массива.Компилятор должен использовать новые объявления для заполнения недостающей информации о прототипе функции. Вот почему раскомментирование любого из объявлений 5 и 6 должно вызвать ошибку: 666 не является известным размером измерения массива. CLang игнорирует это. На самом деле, похоже, что объединение деклараций его совершенно не волнует.
Размер первого измерения не имеет значения, поэтому компилятор его игнорирует. Вот почему объявления 2 и 4 не конфликтуют, хотя их первое измерение имеет разный размер.
Древовидные структуры во время компиляции
struct bin_tree {
int value;
struct bin_tree *left;
struct bin_tree *right;
};
#define NODE(V, L, R) &(struct bin_tree){V, L, R}
const struct bin_tree *tree = \
NODE(4,
NODE(2, NULL, NULL),
NODE(7,
NODE(5, NULL, NULL),
NULL));
Эта фича называется составными литералами. С ними можно проделывать множество других забавных трюков.
VLA typedef
int main() {
int size = 42;
typedef int what[size];
what the_fuck;
printf("%zu\n", sizeof(the_fuck));
}
Это является стандартом с C99. Понятия не имею, как это вообще может быть полезно.
Array designators
struct {
int a[3], b;
} w[] = {
[0].a = {
[1] = 2
},
[0].a[0] = 1,
};
int main() {
printf("%d\n", w[0].a[0]);
printf("%d\n", w[0].a[1]);
}
С помощью данной фичи можно итеративно определить член структуры.
Препроцессор — функциональный язык
#define OPERATORS_CALL(X) \
X(negate, 20, !) \
X(different, 70, !=) \
X(mod, 30, %)
struct operator {
int priority;
const char *value;
};
#define DECLARE_OP(Name, Prio, Op) \
struct operator operator_##Name = { \
.priority = Prio, \
.value = #Op, \
};
OPERATORS_CALL(DECLARE_OP)
Макрос можно передать в качестве параметра другому макросу.
Оператор switch можно мешать с другим кодом
#include
#include
#include
int main(int argc, char *argv[]) {
if (argc != 2)
errx(1, "Usage: %s DESTINATION", argv[0]);
int destination = atoi(argv[1]);
int i = 0;
switch (destination) {
for (; i < 2; i++) {
case 0: puts("0");
case 1: puts("1");
case 2: puts("2");
case 3: puts("3");
case 4: puts("4");
default:;
}
}
return 0;
}
Такие применения известны как устройства Даффа. Помимо прочего, они позволяют легко разворачивать цикл вручную.
Typedef — почти класс хранения
typedef
работает почти так же, как inline
или static
.
Вы можете написать
void typedef name;
a[b] — синтаксический сахар
Знаю, ничего такого безумного. Но, тем не менее, это забавно!
a[b]
буквально эквивалентно (a + b)
. Таким образом, можно написать абсолютное безумие, например 41[yourarray + 1]
.
Вызовы макросов в #include
Это валидный препроцессор:
#define ARCH x86
#define ARCH_SPECIFIC(file)
#include ARCH_SPECIFIC(test.h)
Несуразные объявления указателей
int (*b);
int (*b)(int);
int (*b)[5]; // 1
int *b[5]; // 2
Все это — допустимые декларации.
Скобки полезны для разграничения:
Одиночный # является допустимым препроцессором
Он ничего не делает.
#
#
#
int main() {
return 0;
}
Это все, что я нашел!
Большую часть вышеперечисленного я нашел, читая спецификацию, а часть — читая продакшн код.
Желаю вам счастливых приключений на С :)
Дополнено: Даже не знаю, как я умудрился забыть про устройства Даффа. Спасибо пользователю reddit needadvicebadly за то, что напомнил об этом.
Как встроить экспертную систему в программу на С? Поговорим об этом на открытом уроке 3 июля. Мы обсудим, что такое экспертная система, когда она используется и на чем создается;, а таже рассмотрим язык разработки экспертных систем и библиотеку CLIPS. Этот урок будет особенно полезен для разработчиков различных встраиваемых систем, например, подсистем умного дома, роботизированных систем.