Недавно снова вернулся к попыткам разобраться в Си и закрыть этот гештальт.
Потому что, откровенно говоря, больше всего удовольствия от языка программирования получаю от языка, который
позволяет почувствовать что мы что-то реально делаем с компьютером, двигаем байтики, копируем память, размещаем структуры в памяти.
По сравнению с этим продолжения, монады, алгебраические типы данных (хотя последние можно связать с 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;
}
До этого в выделенном месте могло находиться всё что угодно - нули, кусочек изначального массива, другие цифры. Никогда раньше меня не сбивали с толку так именно почти “физическая” реализация кода, не именно особенности языка или его виртуальной машины, а именно эти самые байтики в памяти.
Нет, всё-таки Си совсем другой, скорее ассемблер, чем высокоуровневый ЯП. И именно потому что он другой, думаю что он достоин изучения, или хотя бы уважения.