Если у вас несколько лет опыта программирования на языке C, то, вероятно, вы гораздо более уверены в своих знаниях этого языка, чем если бы вы провели столько же времени, работая с C++ или Java.
И язык C, и его стандартная библиотека довольно близки к к минимально возможному размеру.
Текущая наиболее часто используемая версия языка, c99, принесла много новых возможностей, многие из которых совершенно неизвестны большинству программистов на C (в более старых спецификациях, очевидно, тоже есть свои темные уголки).
Вот те, о которых я знаю:
Sizeof может иметь побочные эффекты
int main(void) {
return sizeof(int[printf("ooopsn")]);
}
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("%zun", sizeof(a));
printf("%zun", 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("%zun", sizeof(the_fuck));
}
Это является стандартом с C99. Понятия не имею, как это вообще может быть полезно.
Array designators
struct {
int a[3], b;
} w[] = {
[0].a = {
[1] = 2
},
[0].a[0] = 1,
};
int main() {
printf("%dn", w[0].a[0]);
printf("%dn", 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. Этот урок будет особенно полезен для разработчиков различных встраиваемых систем, например, подсистем умного дома, роботизированных систем.
Автор: Ксения Мосеенкова