Если у вас несколько лет опыта программирования на языке 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 <stdio.h> 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 <stdio.h> #include <stdlib.h> #include <err.h> 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) <ARCH/file> #include ARCH_SPECIFIC(test.h)
Несуразные объявления указателей
int (*b); int (*b)(int); int (*b)[5]; // 1 int *b[5]; // 2
Все это — допустимые декларации.
Скобки полезны для разграничения:
-
Объявление 1 — указатель на массив из 5 int
-
Объявление 2 — массив из 5 указателей на int
Одиночный # является допустимым препроцессором
Он ничего не делает.
# # # int main() { return 0; }
Это все, что я нашел!
Большую часть вышеперечисленного я нашел, читая спецификацию, а часть — читая продакшн код.
Желаю вам счастливых приключений на С 🙂
Дополнено: Даже не знаю, как я умудрился забыть про устройства Даффа. Спасибо пользователю reddit needadvicebadly за то, что напомнил об этом.
Как встроить экспертную систему в программу на С? Поговорим об этом на открытом уроке 3 июля. Мы обсудим, что такое экспертная система, когда она используется и на чем создается; а таже рассмотрим язык разработки экспертных систем и библиотеку CLIPS. Этот урок будет особенно полезен для разработчиков различных встраиваемых систем, например, подсистем умного дома, роботизированных систем.
ссылка на оригинал статьи https://habr.com/ru/companies/otus/articles/744542/
Добавить комментарий