В этом посте разобраны некоторые фокусы, причуды и фичи языка C (некоторые из них – весьма фундаментальные!), которые, казалось бы, могут сбить с толку даже опытного разработчика. Поэтому я потрудился сделать за вас грязную работу и (в произвольном порядке) собрал некоторые из них в этом посте. Примеры сопровождаются ещё более вольными краткими пояснениями и/или листингами (некоторые из них цитируются).
Конечно же, здесь я не берусь перечислять абсолютно всё, так как факты из разряда «функция nan()
не может устанавливать errno
, поскольку в определённых ситуациях поведёт себя как strtod()
» не слишком интересны.
ВНИМАНИЕ: сам факт попадания тех или иных вещей в эту подборку не означает автоматически, что я рекомендую или, наоборот, не рекомендую ими пользоваться! Некоторые из приведённых примеров никогда не должны просачиваться за пределы списков наподобие этого, тогда как другие примеры невероятно полезны! Уверен, что могу положиться на ваш здравый смысл, дорогие читатели.
Источники
Указатели на массив
При спуске к указателю обычные указатели на массив, как правило, оказываются не нужны:
int arr[10];
int *ap0 = arr; // спуск к указателю
// ap0[2] = ...
int (*ap1)[10] = &arr; // правильный указатель на массив
// (*ap1)[2] = ...
Но как приятно, когда можешь выделить в куче большой многомерный массив:
int (*ap3)[90000][90000] = malloc(sizeof *ap3);
С указателями могут пригодиться даже массивы переменной длины
int (*ap4)[n] = malloc(sizeof *ap4);
Оператор запятая
Запятая применяется в качестве оператора и ставится как разделительный знак между двумя и более выражениями в случаях, когда ожидается всего одно выражение. Когда для того, чтобы получить значение, требуется интерпретировать целый набор выражений, то учитывается только крайнее правое.
Например: b = (a=3, a+2); – этот код сначала присвоит выражение 3 переменной a, затем a+2 будет присвоено переменной b. Таким образом, в итоге b будет содержать значение 5, тогда как в переменной a будет записано 3.
В Википедии приводится ещё несколько таких примеров.
Диграфы, триграфы и альтернативные токены
Код на C не всегда поддаётся портированию, но сам язык C, пожалуй, приспособлен к портированию лучше любого другого. Например, есть системы, в которых вместо ASCII используется другая кодировка, скажем, EBCDIC. Для поддержки таких систем в C предусмотрены диграфы и триграфы – многосимвольные последовательности, трактуемые компилятором как другие символы.
Диграф |
|
| Триграф |
|
| iso646.h |
|
<: | [ |
| ??= | # |
| and | && |
:> | ] |
| ??( | [ |
| and_eq | &= |
<% | { |
| ??/ | \ |
| bitand | & |
%> | } |
| ??) | ] |
| bitor | | |
%: | # |
| ??' | ^ |
| compl | ~ |
%:%: | ## |
| ??< | { |
| not | ! |
——– | ———– |
| ??! | | |
| not_eq | != |
——– | ———– |
| ??> | } |
| or | || |
——– | ———– |
| ??- | ~ |
| or_eq | |= |
——– | ———– |
| ——– | ———– |
| xor | ^ |
——– | ———– |
| ——– | ———– |
| xor_eq | ^= |
Хотя и пришлось преодолеть вялое сопротивление, Комитет постановил убрать поддержку триграфов, начиная с версии C23.
Скрытый текст
Выделенный инициализатор
При помощи выделенных инициализаторов вы можете указывать, какие именно элементы объекта (массива, структуры, объединения) должны инициализироваться следующими значениями. Порядок неважен!
struct Foo {
int x, y;
const char *bar;
};
void f(void)
{
int arr[] = { 1, 2, [5] = 9, [9] = 5, [8] = 8 };
struct Foo f = { .y = 23, .bar = "barman", .x = -38 };
struct Foo arr[] = {
[10] = { 8, 8, 9 },
[8] = { 1, 8, bar3 },
[12] = { .x = 9, .z = 8 },
};
struct {
int sec, min, hour, day, mon, year;
} z = {
.day = 31, 12, 2014,
.sec = 30, 15, 17
}; // инициализирует z в { 30, 15, 17, 31, 12, 2014 }
}
Скрытый текст
Составные литералы
Составной литерал выглядит как приведение списка инициализаторов, заключённого в скобки. Его значение — это объект того типа, что указан при приведении, и в нём содержатся элементы, указанные в инициализаторе.
#include <stdio.h>
struct Foo { int x, y; };
void bar(struct Foo p)
{
printf("%d, %d", p.x, p.y);
}
int main(void)
{
bar((struct Foo){2, 3});
return 0;
}
Составные литералы — это адреса
(struct Foo){};
((struct Foo){}).x = 4;
&(struct Foo){};
func(&(struct Foo){.x = 2});
Даже если вы уже знаете о составных литералах, возможно, вы даже не замечали, что они представляют собой адреса (lvalues). А это важно, поскольку, когда перед нами lvalue, мы можем получить его адрес (и, например, передать его функции).
Защита от затенения
Следующий код вернёт 42, а не 3840!
int x = 42;
int func() {
int x = 3840;
{
extern int x;
return x;
}
}
Многосимвольные константы
Они зависят от реализации и даже от самого стандарта, так что обычно лучше их избегать. При этом они достаточно удобны в качестве самодокументируемых перечислений, если впоследствии вам придётся иметь дело с необработанными дампами памяти.
enum state {
waiting = 'WAIT',
running = 'RUN!',
stopped = 'STOP',
};
Например, у меня на машине я могу локализовать 'WAIT' как показано здесь:
00001120: c3 66 66 2e 0f 1f 84 00 00 00 00 00 0f 1f 40 00 .ff...........@.
00001130: f3 0f 1e fa e9 67 ff ff ff 55 48 89 e5 48 83 ec .....g...UH..H..
00001140: 10 c7 45 fc 54 49 41 57 8b 45 fc 89 c6 48 8d 05 ..E.TIAW.E...H..
00001150: b0 0e 00 00 48 89 c7 b8 00 00 00 00 e8 cf fe ff ....H...........
00001160: ff b8 00 00 00 00 c9 c3 f3 0f 1e fa 48 83 ec 08 ............H...
Битовые поля
Объявляется член с явно заданной шириной в битах. Прилегающие к нему члены битового поля могут быть упакованы для совместного использования и растягивания отдельных битов.
struct cat {
unsigned int legs : 3; // 3 бита на лапы (0-4 умещается в 3 бита)
unsigned int lives : 4; // 4 бита на жизни (0-9 умещается в 4 бита)
};
Скрытый текст
Битовые поля нулевой длины
Скрытый текст
Описание из документации к Arm Compiler 6:
При помощи битового поля нулевой длины можно вносить следующие изменения:
Разграничивать любые битовые поля, расположенные до и после битового поля нулевой длины. Любые битовые поля по разные стороны от этой границы трактуются как неперекрывающиеся участки памяти. Это сказывается на структуре программ C и C++. В стандартах C и C++ требуется, чтобы обращения к битовому полю на загрузку и на сохранение с одной стороны границы никак не затрагивали битовые поля по другую сторону границы.
Пользуйтесь заполняющими нулями, чтобы все битовые поля после битового поля нулевой длины выравнивались по ближайшей доступной естественной границе в зависимости от того, каков тип битового поля нулевой длины. Например, при помощи char:0 можно выполнить выравнивание по ближайшей доступной байтовой границе, а при помощи int:0 – по ближайшей доступной границе слов.
Рассмотрим пример, приведённый в качестве ответа на Stackoverflow (с небольшими изменениями):
struct bar {
unsigned char x : 5;
unsigned short : 0;
unsigned char y : 7;
}
Вот как будет выглядеть в памяти вышеприведённый код (предполагается, что мы имеем 16-разрядные short и не учитываем порядок битов – от младшего к старшему или наоборот):
char pad pad short boundary
| | | |
v v v v
xxxxx000 00000000 yyyyyyy0
Битовое поле нулевой длины расположено так, что позиция сдвигается до следующей границы short (иными словами: расположится на ближайшем естественном рубеже, предусмотренном на целевой платформе). Мы определили short как 16-разрядное число. Соответственно, 16 минус 5 равно 11, значит, 11 разрядов нужно заполнить нулями.
Квалификатор типа volatile
Этот квалификатор сообщает компилятору, что к переменной можно обратиться не только через данный код, но и иначе (например, если мы имеем дело с устройством для ввода/вывода через память). Поэтому нельзя допускать, чтобы в процессе оптимизации убирались операции чтения и записи, связанные с этим ресурсом.
Скрытый текст
Квалификатор типа restrict
Добавляя такой квалификатор типа, программист сообщает компилятору, что на время жизни данного указателя ни один другой указатель не предназначен для обращения к объекту, на который направлен рассматриваемый указатель. Таким образом, компилятор может выполнять оптимизации (например, векторизовать код), которые в ином случае были бы невозможны.
Скрытый текст
Квалификатор типа register
Подсказывает, что компилятор хранит объявленную переменную в регистре ЦП (или в каком-то другом месте, где к ней можно быстро обратиться), а не в памяти с произвольным доступом. Если переменная объявлена с таким квалификатором, то к её местоположению обратиться нельзя (правда, можно применить оператор sizeof).
В настоящее время register обычно не имеет смысла, так как современные компиляторы сами ставят переменные в регистр, если это уместно – независимо от того, есть ли соответствующая подсказка. Иногда такой квалификатор может пригодиться во встраиваемых системах, но даже в таких случаях компилятор, вероятно, предложит оптимизации получше.
Элемент массива с динамическим размером
Из Википедии:
struct vectord {
short len; // здесь должен быть, как минимум, ещё один член данных
double arr[]; // элемент массива с динамическим размером должен идти последним
// Здесь компилятор может зарезервировать дополнительное место для заполнения нулями,
// как и при работе с членами структур.
};
struct vectord *vector = malloc(...);
vector->len = ...;
for (int i = 0; i < vector->len; ++i) {
vector->arr[i] = ...; // прозрачно использует требуемый тип (число double)
}
Скрытый текст
Описатель формата %n
В этом ответе на StackOverflow о нём рассказано достаточно хорошо:
%n возвращает ту позицию, в которой сейчас находится воображаемый курсор, когда форматируется вывод printf().
int pos1, pos2;
const char *str_of_unknown_len = "we don't care about the length of this";
printf("Write text of unknown %n(%s)%n length\n", &pos1, str_of_unknown_len, &pos2);
printf("%*s\\%*s/\n", pos1, " ", pos2-pos1-2, " ");
printf("%*s", pos1+1, " ");
for (int i = pos1+1; i < pos2-1; ++i) {
putc('-', stdout);
}
putc('\n', stdout);
Вывод будет таким:
Write text of unknown (we don't care about the length of this) length
\ /
-------------------------------------
Конечно, этот пример немного надуманный, но сами приёмы могут пригодиться, когда нужно аккуратно вывести текст.
Описатель формата %.* (минимальная ширина поля)
В случаях, когда хотите заполнять память переменным количеством символов, вместо
char fmt_buf[MAX_BUF];
snprintf(fmt_buf, MAX_BUF, "%%.%df", prec);
printf(fmt_buf, num);
попробуйте
printf("%.*f", prec, num);
Другие относительно малоизвестные описатели форматов
Загляните в §7.21.6.1 и §7.21.6.2 в черновом стандарте C11. Там вы увидите %#, %e, %-, %+, %j, %g, %a и ещё несколько интересных описателей.
Пересекающиеся синтаксические конструкции
Следующий код на C является синтаксически корректным:
#include <stdio.h>
int main()
{
int n = 3;
int i = 0;
switch (n % 2) {
case 0:
do {
++i;
case 1:
++i;
} while (--n > 0);
}
printf("%d\n", i); // 5
}
Знаю, что программисты, опасающиеся goto, могли бы написать так:
switch (x) {
case 1:
// 1 специфичный код
if (0) {
case 2:
// 2 специфичный код
}
// общее для 1 и 2
}
Самый известный пример использования этой причуды/«фичи» — это метод Даффа:
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);
}
}
"оператор" -->
Следующий код на C корректен:
size_t n = 10;
while (n --> 0) {
printf("%d\n", n);
}
Уместен вопрос: с каких это пор в C есть такой оператор? И я отвечу: ни с каких. --> - это не один, а два разных оператора, -- и >, записываемых друг за другом. Поэтому они выглядят как один. Это допустимо, поскольку C снисходительно относится к пробелам.
n --> 0 эквивалентно (n--) > 0
idx[arr]
Нотация с квадратными скобками для обращения к элементам массива – это синтаксический сахар, применяемый в арифметике указателей:
arr[5] ≡ *(arr + 5) ≡ *(5 + arr) ≡ 5[arr]
Категорически недопустимо использовать такое в реальном коде … а вообще выглядит довольно забавно!
// массив[индекс]
boxes[products[myorder.product].box].weight;
// индекс[массив]
myorder.product[products].box[boxes].weight;
Отрицательные индексы массива
Однажды, когда мне требовалось по-быстрому провести отладку, я решил проверить, корректными ли значениями заполнено пространство в конце массива. Но я не знал, откуда именно начинается заполнение. Поэтому я сделал так:
int *end = arr + (len - 1);
if (end[0] == VAL && end[-1] == VAL && end[-5] == VAL) {
puts("Correct padding");
}
Конкатенация строковых литералов
Вам не требуется ни sprintf() (ни strcat()!) для конкатенации строковых литералов:
#define WORLD "World!"
const char *s = "Hello " WORLD "\n"
"It's a lovely day, "
"innit?";
Сращивание строк при помощи обратного слэша
Удаляются все экземпляры обратного слэша \, за которыми непосредственно следует символ перехода на новую строку. Таким образом, физические строки исходного кода сращиваются, образуя логические.
#define I_AM_O\
NE_MACRO 123
// Я комментарий \
Я всё тот же комментарий. \
Я – так называемый комментарий-ОДНОСТРОЧНИК!
int fun()
{
if (drive == 2) // дисковод 2 это C:\
return 1; <-- мой дружок вот тут тоже входит в состав КОММЕНТАРИЯ!!
writestuff();
return 0;
}
int main()
{
int x = I_AM_ONE_MACRO; // корректно расширяется до 123
int same_\
variable = 1;
same_variable = 1;
const char *p = "String with\
so many spaces in the MIDDLE!";
puts(p); // Строка с таким множеством пробелов в середине!"
return 0;
}
Использование && и || в качестве условных операторов
Если вы пишете шелл-скрипты, то понимаете, о чём я.
#include <ctype.h>
#include <stdio.h>
#include <stdbool.h>
int main(void)
{
1 && puts("Hello");
0 && puts("I won't");
1 && puts("World!");
0 && puts("be printed");
1 || puts("I won't be printed either");
0 || puts("But I will!");
true && (9 > 2) && puts("9 is bigger than 2");
isdigit('9') && puts("9 is a digit");
isdigit('n') && puts("n is a digit") || puts("n is NOT a digit!");
return 0;
}
Вероятно, компилятор будет сильно ругаться, так как в коде C такая практика очень нетипична.
Проверка допущений при компиляции при помощи перечислений
#define D 1
#define DD 2
enum CompileTimeCheck
{
MAKE_SURE_DD_IS_TWICE_D = 1/(2*(D) == (DD)),
MAKE_SURE_DD_IS_POW2 = 1/((((DD) - 1) & (DD)) == 0)
};
Может пригодиться при работе с библиотеками, константы в которых можно конфигурировать во время выполнения.
Ситуативное определение struct в возвращаемом типе функции
Можно определять структуры (struct) в самых (на первый взгляд) произвольных местах:
#include <stdio.h>
struct Foo { int a, b, c; } make_foo(void) {
struct Foo ret = { .c = 3 };
ret.a = 11 + ret.c;
ret.b = ret.a * 3;
return ret;
}
int main()
{
struct Foo x = make_foo();
printf("%d\n", x.a + x.b + x.c);
return 0;
}
«Вложенное» определение struct необязательно держать вложеннным
#include <stdio.h>
struct Foo {
int x;
struct Bar {
int y;
};
};
int main()
{
struct Bar s = { 34 }; // правильно
// struct Foo.Bar s; // неправильно
printf("%d\n", s.y);
return 0;
}
Плоские списки инициализации
int arr[3][3] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
// = { {1,2,3}, {4,5,6}, {7,8,9} };
struct Foo {
const char *name;
int age;
};
struct Foo records[] = {
"John", 20,
"Bertha", 40,
"Andrew", 30,
};
Неявное приведение указателей void
C11 §6.3.2.3 ¶1:
Указатель на void можно преобразовывать в указатель на объект любого типа и обратно. Указатель на объект любого типа можно преобразовывать в указатель на void и обратно. При сравнении результат получится равным исходному указателю.
C11 §6.5.16.1 ¶1:
—
У левого операнда атомарный, квалифицированный или неквалифицированный указатель и (с учётом того типа, который будет у левого операнда после преобразования lvalue) один из операндов указывает на объектный тип, а другой – на квалифицированную или неквалифицированную версию void. Тот тип, на который указывается слева, обладает всеми теми же квалификаторами, что и тип, на который указывается справа;
void* был добавлен в C89, поскольку требовался обобщённый тип указателя, который поддаётся неявному преобразованию в обе стороны.
На самом деле, при явном преобразовании указателей void возникают следующие проблемы:
Это просто не нужно, так как void* автоматически и безопасно расширяется до указателя любого другого типа;
В таком случае код замусоривается, приведения не очень удобно читать (особенно если указатель относится к типу long);
Такой подход приводит к самоповторам;
Могут возникать скрытые ошибки, если возвращаемый тип изменится с void* на что-либо более конкретное.
Статические индексы массивов в объявлениях параметров функций
За исключением некоторых контекстов, если имя массива не сопровождается индексом (например, region вместо region[4]), то перед нами указатель, значением которого является адрес первого элемента в массиве – при условии, что ранее этот массив был объявлен. Тип массива в списке параметров функции также преобразуется в тип соответствующего указателя. Информация о том, какого размера был массив аргументов, теряется, если обратиться к массиву извне тела функции.
Чтобы сохранять эту информацию, которая может пригодиться при оптимизации, в C99 разрешено объявлять индекс массива аргументов при помощи ключевого слова static. В константном выражении указывается минимальный размер указателя, и на эту информацию можно опираться при оптимизациях как на допущение. Крайне рекомендуется именно так использовать ключевое слово static. Это ключевое слово может фигурировать только в самой внешней операции приведения типа массива и только в объявлениях параметров функций. Если та сторона, которая вызывает функцию, не соблюдает этих ограничений, то возникает неопределённое поведение.
В следующих примерах показано, как может использоваться данная возможность:
int n;
void foo(int arr[static 10]); // arr указывает на первое целое число, а всего таких чисел не менее 10
void foo(int arr[const 10]); // arr - это константный указатель
void foo(int arr[const]); // константный указатель на целое число
void foo(int arr[static const n]); // arr указывает на не менее чем n целых чисел (массив переменной длины)
void foo(int p[static1]); — это, фактически, стандартный вариант объявления, что p должен указывать не на null.
Перегрузка макросов путём регулирования длины списка аргументов
Скрытый текст
#include <stdio.h>
#include "cmoball.h"
#define NoA(...) CMOBALL(FOO, __VA_ARGS__)
#define FOO_3(x,y,z) "Three"
#define FOO_2(x,y) "Two"
#define FOO_1(x) "One"
#define FOO_0() "Zero"
int main()
{
puts(NoA());
puts(NoA(1));
puts(NoA(1,1));
puts(NoA(1,1,1));
return 0;
}
При работе с typedef применяется такой же синтаксис, как и с любыми другими спецификаторами
Обычно при объявлении новых типов применяется следующий синтаксис:
typedef unsigned char byte;
typedef struct {
int x;
int y;
const char *p;
} Record;
Но ключевое слово typedef может располагаться и иначе:
unsigned typedef char byte;
struct {
int x;
int y;
const char *p;
} typedef Record;
Причём у нас всё равно сохраняется возможность объявить множество типов за один ход:
struct {
int x;
int y;
const char *p;
} typedef Record, record, *record_ptr;
Типы функций
Указатели функций широко известны, но не менее известно, что их синтаксис немного неуклюжий. С другой стороны, не столь известно, что typedef можно создавать не только для большинства типов объектов, но и для типов функций.
#include <stdio.h>
int main()
{
typedef double fun_t(double);
fun_t sin, cos, sqrt;
fun_t *ftpt = &sqrt;
printf("%lf\n", ftpt(4)); // 2.000000
return 0;
}
Странности во взаимосвязях между обозначениями функций и указателями
Пример от u/AnonymouX47, приведённый на Reddit в посте What your weirdest C feature?:
Допустим, у нас есть простой прототип функции: void f(void);
Следующие строки эквивалентны друг другу:
void (*fp)(void) = f;
void (*fp)(void) = *f;
void (*fp)(void) = &f;
void (*fp)(void) = ******f;
void (*fp)(void) = &***********f;
void (*fp)(void) = ***&***f;
void (*fp)(void) = &**&***&***&f;
Следующие строки также эквивалентны друг другу:
f();
(*f)();
(&f)();
(*&f)();
fp();
(*fp)();
(*&fp)();
(****fp)();
(&******fp)();
(**&**fp)();
(*&*&*&*fp)();
Но (&fp)() или (&*&*&fp)() работать не будет.
X-макросы
Скрытый текст
Именованные параметры функций
struct _foo_args {
int num;
const char *text;
};
#define foo(...) _foo((struct _foo_args){ __VA_ARGS__ })
int _foo(struct _foo_args args)
{
puts(args.text);
return args.num * 2;
}
int main(void)
{
int result = foo(.text = "Hello!", .num = 8);
return 0;
}
Сочетание аргументов по умолчанию, именованных и позиционных аргументов
Используем составные литералы и макросы для создания именованных аргументов:
typedef struct { int a,b,c,d; } FooParam;
#define foo(...) foo((FooParam){ __VA_ARGS__ })
void (foo)(FooParam p);
Добавить аргументы по умолчанию также не составляет труда:
#define foo(...) foo((FooParam){ .a=1, .b=2, .c=3, .d=4, __VA_ARGS__})
Но после этого позиционные аргументы перестанут действовать; кроме того, могут возникать ситуации, в которых вам захочется поддерживать оба варианта. Но недавно я обнаружил, что можно заставить такие комбинации работать, добавив формальный параметр:
typedef struct { int _; int a,b,c,d; } FooParam;
#define foo(...) foo((FooParam){ .a=1, .b=2, .c=3, .d=4, ._=0, __VA_ARGS__})
Теперь foo можно вызывать следующими способами:
foo(); // a=1, b=2, c=3, d=4
foo(.a=4, .b=5); // a=4, b=5, c=3, d=5
foo(4, 5); // a=4, b=5, c=3, d=5
foo(4, 5, .d=8); // a=4, b=5, c=3, d=8
Формальный параметр не требуется, если у вас есть аргументы, которые требуется передавать по имени:
typedef struct { int alwaysNamed; int a,b,c,d; } FooParam;
#define foo(...) foo((FooParam){.a=1,.b=2,.c=3,.d=4, .alwaysNamed=5, __VA_ARGS__})
Злоупотребляем объединениями, чтобы группировать сущности по пространствам имён
Допустим, у вас есть структура struct с набором полей, и некоторые из них вы хотите обрабатывать одновременно, как группу под некоторым именем. Возможно, вы хотите, чтобы можно было с удобством копировать их единым блоком, применяя при этом операцию присваивания struct.
Работая с объединениями, можно обращаться одновременно к a.field2 и a.sub (причём, a.field2 равноценно a.sub.field2) без каких-либо макросов.
struct a {
int field1;
union {
struct {
int field2;
int field3;
};
struct {
int field2;
int field3;
} sub;
};
};
Единичные сборки
Поскольку механизм #include сводится к примитивному копированию и вставке содержимого включённого файла в актуальный код, в C допускается создание так называемых единичных сборок, где мы складываем весь код в одну единицу трансляции.
Пользуясь такой техникой, иногда можно добиться ускорения компиляции, упростить процесс сборки или создать новые возможности для оптимизаций. К сожалению, этот подход не очень хорошо масштабируется, а также не сочетается с параллельными и инкрементными сборками. Кроме того, при нём усложняется модуляризация/инкапсуляция кода.
Также будет сложно генерировать compile_commands.json для инструментов, работающих на базе LLVM.
Сопоставление классов символов при помощи sscanf()
Из этого комментария на Reddit:
sscanf() может применяться в качестве эрзаца «регулярных выражений» — но не любых, а только таких, которые используются для сопоставления символов. Например, можно написать код вроде следующего, чтобы проверить, присутствуют ли во входной информации буквы или нижние подчёркивания:
int len = 0;
char buf[256];
int read_token = sscanf(input, "%255[a-zA-Z_]", buf, &len);
if (read_token) { /* что-то делаем */ }
или пропустить символы пробелов:
int len = 0;
char buf[256];
sscanf(input, "%255[\r\n]%n", buf, &len);
input += len;
Сборщик мусора
Boehm GC – это библиотека, обеспечивающая сборку мусора в C и C++
Cosmopolitan Libc
Описание с сайта проекта:
Благодаря Cosmopolitan Libc, язык C переходит в состояние «собрал один раз — запустил везде», подобно Java. Но при этом не требуется не интерпретатора, ни виртуальной машины. Вместо этого инструмент реконфигурирует имеющиеся GCC и Clang, давая на вывод POSIX-совместимый многоязычный формат, который нативно выполняется на Linux + Mac + Windows + FreeBSD + OpenBSD + NetBSD + BIOS при максимально возможной производительности. При этом сборка занимает в памяти такой минимум места, какой только можно представить.
Ассемблерные вставки
Язык C, будучи высокоуровневым, достаточно хорошо коммуницирует с низкоуровневым миром. Можно создать код на ассемблере и без труда связать его так, чтобы он работал в программе, написанной на C. Кроме того, во многих компиляторах в качестве расширения (указано в приложении J к стандарту C) предлагается такая возможность, как ассемблерная вставка, обычно предваряемая в коде ключевым словом asm.
Скрытый текст
Вычисление sizeof во время компиляции с возникновением сопутствующей ошибки duplicate case
Допустим, вы разрабатываете встраиваемую систему или вообще любой проект, где получить вывод printf() может быть не так просто.
int foo(int c)
{
switch (c) {
case sizeof (struct Foo): return c + 1;
case sizeof (struct Foo): return c + 2;
}
}
Если добавить такую простую функцию где-либо в вашем коде, то (в зависимости от компилятора) можно получить сообщение об ошибке, в котором содержится результат операции sizeof.
error: duplicate case value '16'
case sizeof(struct Foo): return c + 2;
Обнаружение константных выражений
#define ICE_P(x) _Generic((1 ? ((void*)((x)*(uintptr_t)0)) : &(int){1}), int*: 1, void*: 0)
Отсюда: I think I found a C11 compliant way to detect constant expressions : r/C_Programming
TL;DR: Вызов ICE_P результирует в true, если аргументом служит константное выражение, а в противном случае результирует в false. Поэтому следующий код:
int x = 3;
printf("%d %d\n", ICE_P(x), ICE_P(3));
должен дать вывод 0 1.
Объектно-ориентированное программирование
Скрытый текст
Безопасные функции с переменным количеством аргументов
Из-за функций с переменным количеством аргументов часто возникают трудноуловимые баги, поскольку типы их аргументов не проверяются, а также с ними могут применяться нелогичные правила расширения или преобразования.
Существует и такая проблема: подобная функция не знает, сколько элементов будет ей передано, поэтому тот, кто вызывает функцию, должен уточнять это вручную – либо при помощи некого сторожевого значения, обозначающего конец списка аргументов (напр., в execl() в таком качестве используется NULL), либо передавая размер в другом параметре.
Если грамотно пользоваться макросами для работы с такими функциями — эти макросы появились в C99 — сочетая их с составными литералами, то все эти недостатки можно легко обойти.
Нижеприведённый пример работает лишь в том случае, если все параметры относятся к одному и тому же типу, а бывает так, что могут потребоваться разные типы. Но должна быть предусмотрена возможность расширять его, применяя «размеченные объединения» и _Generic, предусмотренный в C11.
#include <stdio.h>
void print_vec(FILE *f, const int *v, size_t n)
{
for (size_t i = 0; i < n; ++i) {
fprintf(f, "%d\n", p[i]);
}
}
#define print_vec(fstream, ...) \
print_vec((fstream), \
(const int[]){ __VA_ARGS__ }, \
(sizeof (int[]){ __VA_ARGS__ } / sizeof (int)) )
int main(void)
{
print_vec(stdout, 1);
print_vec(stdout, 1, 2, 3);
print_vec(stdout, 1, 2, 3, 4, 5);
return 0;
}
Метапрограммирование
В C11 добавилась возможность _Generic, но оказывается, что метапрограммирование возможно даже на чистом C99 (правда, для этого требуется бесчеловечно надругаться над препроцессором). Знакомьтесь с библиотекой Metalang99.
#include <datatype99.h>
datatype(
BinaryTree,
(Leaf, int),
(Node, BinaryTree *, int, BinaryTree *)
);
int sum(const BinaryTree *tree) {
match(*tree) {
of(Leaf, x) return *x;
of(Node, lhs, x, rhs) return sum(*lhs) + *x + sum(*rhs);
}
return -1;
}
Препроцессор — это полноценный язык
Я уже упоминал некоторые фокусы с препроцессором, но на самом деле их гораздо больше! На самом деле, я мог бы написать ещё такую же статью об одних только фокусах препроцессора. В конце концов, это самодостаточный Тьюринг-полный язык с собственными правилами, грамматикой и подводными камнями. Да что там, он даже применяется не только с C – есть горячие головы, сочетающие его, например, с JavaScript
К счастью, Тима Кинсарт, разработчик вышеупомянутой библиотеки Metalang99, уже собрал список awesome-c-preprocessor, где описаны всевозможные разумные и неразумные вещи, которые можно проделать с препроцессором C.