Такой странный Си


Недавно снова вернулся к попыткам разобраться в Си и закрыть этот гештальт. Потому что, откровенно говоря, больше всего удовольствия от языка программирования получаю от языка, который позволяет почувствовать что мы что-то реально делаем с компьютером, двигаем байтики, копируем память, размещаем структуры в памяти. По сравнению с этим продолжения, монады, алгебраические типы данных (хотя последние можно связать с struct и union) кажутся чем-то далёким от реального мира. Даже странно, что и то и другое существует в одном пространстве языков программирования.

В замечательной статье Some Were Meant for C подробно объясняется почему Си такой, и почему он всё имеет место в мире, где полно более “безопасных” языков. Си на самом деле это ассемблер, пытающийся замаскироваться под типичные языки программирования. Если так получилось, что вы как и я, пришли к Си после какого-то типичного ЯП вроде Python, это откровение может снизойти не сразу. Потому что по первой это очень похоже на типичный ЯП (правильнее конечно, что все они похожи на Си :)) - вот переменная, вот функция, вот массив (за его границы, правда можно вывалиться и получить segmentation fault, или ещё хуже, какое-то валидное значение).

Но со временем неизбежно приходит момент осознания, обычно очень болезненно.

Недавно пытался сделать несколько вариантов сортировок, включая тот самый “правильный” quick sort, который происходит in-place, без копирования партиций массива, что немного сложнее.

void qs_inner(int *numbers, int left, int right, compare_cb cmp)
{
    int left_original = left;
    int right_original = right;
    int tmp = 0;

    // base case
    if (left >= right) return;
    int mid = numbers[(left + right) / 2];

    do {
        while (cmp(numbers[left], mid) < 0) left++;
        while (cmp(mid, numbers[right]) < 0) right--;

        if (left <= right) {
            tmp = numbers[left];
            numbers[left] = numbers[right];
            numbers[right] = tmp;
            left++;
            right--;
        }
    } while (left <= right);
    // recursive call
    qs_inner(numbers, left_original, right, cmp);
    qs_inner(numbers, left, right_original, cmp);
}

int * quick_sort(int *numbers, int count, right, cmp)
{
    int *target = malloc(count * sizeof(int));
    if (!target) die("Memory error");

    /* qs_inner - реализация алгоритма сортировки */
    qs_inner(target, 0, count - 1, cmp);
    return target;
}

Окей, выглядит нормально, попробуем запустить:

./qs_test  3 0 4 2

Получаем:

> 3 4 19 12312312

Ну, оно сортировано, но выглядит как-то неправильно. А главное - как понять, что вообще происходит? Потому что такую ошибку мы обычно никогда не получим в Python. Провозившись с gdb больше часа, понимаю, что после malloc я собственно забыл скопировать изначальный массив в выделенное место:

int * quick_sort(int *numbers, int count, right, cmp)
{
    int *target = malloc(count * sizeof(int));
    if (!target) die("Memory error");

    /* потерянная строчка */
    memcpy(target, numbers, count * sizeof(int));
    /* qs_inner - реализация алгоритма сортировки */
    qs_inner(target, 0, count - 1, cmp);
    return target;
}

До этого в выделенном месте могло находиться всё что угодно - нули, кусочек изначального массива, другие цифры. Никогда раньше меня не сбивали с толку так именно почти “физическая” реализация кода, не именно особенности языка или его виртуальной машины, а именно эти самые байтики в памяти.

Нет, всё-таки Си совсем другой, скорее ассемблер, чем высокоуровневый ЯП. И именно потому что он другой, думаю что он достоин изучения, или хотя бы уважения.