Pull to refresh
739.3
OTUS
Цифровые навыки от ведущих экспертов

Малоизвестные возможности языка C

Reading time4 min
Views24K
Original author: multun.net

Если у вас несколько лет опыта программирования на языке 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. Этот урок будет особенно полезен для разработчиков различных встраиваемых систем, например, подсистем умного дома, роботизированных систем.

Tags:
Hubs:
Total votes 72: ↑68 and ↓4+79
Comments22

Articles

Information

Website
otus.ru
Registered
Founded
Employees
101–200 employees
Location
Россия
Representative
OTUS