Еше раз о C в виде «заметок на полях»
Знакомство
Честно говоря, язык Си был одним из тех языков, которые я начал учить просто потому, что мне это срочно понадобилось. Я работал в одной компании, где в одном из программных компонентов использовался Си, и мне надо было дописать функционал этого компонента. Ну вот так и случилось, что мне впервые пришлось довольно быстро разобраться с сишным кодом.
Через несколько лет, я решил заполнить пробелы и более серьезно занялся его изучением. Книжка K&R в одну руку, gcc под другую и через 2 недели я уже переписал в учебных целях сниппет из руководства по библиотеке libcurl. В том примере, содержимое страницы загружалось в буфер, который представлял из себя структуру из указателя на строку и целого числа для указания ее длины. Для лучшего понимания работы с указателями, я решил переписать этот пример без использования структуры. Я взял указатель на строку, который указывал на динамически выделяемую и реаалоцируемую память и писал туда, попутно реаллоцируя когда это было нужно. Учитывая, что моей задачей было загрузить только текстовые данные, размер буфера я контролировал через функцию strlen (). Как упражнение это было очень полезно т. к. попутно нужно было понять что такое «указатель на указатель на строку» (char**), ну и на практике понять что такое «нуль-терминированная строка» и как с ней работать. Кстати, вот этот сниппет — https://curl.se/libcurl/c/getinmemory.html
Я не могу похвастаться какими-то большими проектами на Си т.к. в основном приходилось заниматься мелким системным программированием, а также написанием и правкой всяких служебных тулзов в основном связанных с сетевым стэком в Linux и Windows. Например, доставшийся мне однажды по наследству самописный bgp-route-коллектор. Не могу сказать, что я к этому стремился — писать Си-код, но во многом я нохожу это интересным и познавательным.
В виду того, что тема языка Си остается актуальной — за последнее время видел много видео на ютубе и недавно статью здесь про указатели типа void*, да и много других материалов — решил изложить несколько существеных, на мой взгляд, замечаний о Си в наши дни, и почему не стоит чураться писать на нем
Что касается его особенностей, то я бы его сравнил со французским: когда ты учишь его, то подчас ты просто в ужасе от количества нюансов, его сложной грамматики с кучей времен, которые нужно знать, периодически тебя подбешивает орфография, и сколько всяких условностей и нюансов в произношении. Иногда сложно понять значение тех или иных фразеологических оборотов, которые, как оказывается уходят корнями в века…В общем, полно моментов, которые реально кажутся невероятным усложнением после английского. Но когда ты через это все проходишь, то вдруг ты понимаешь всю выразительность и красоту этого языка. И потом, ты хочешь чаще говорить на нем. Вот точно также и с Си — чем больше пишешь, чем больше узнаешь, уже после прохождения этапа первых мук, тем больше хочется и дальше писать на нем.
Указатели типа void*
Будем честны — это своего рода «синтасический сахар». В первом издании K&R его нет, и эти указатели появляются позже (в третьем издании они уже есть). И из объяснения там вполне можно понять причину по которой они появились. В противном случае нам бы пришлось пользоваться указателями на строку, которые в этом смысле удобны. На x86 один байт это char, вот и массив байт будет char[]. Так ведь? Но такой синтаксис может вводить в заблуждение, ведь, на уровне чтения кода сложно различить char* который указывает на нуль-терминированную строку и такой же char* который указывает на набор данных, который необязательно может быть ею представлен. Кстати, в сниппете, о котором я говорил выше, указатель void* используется для передачи указателя на структуру в callback-функцию, где этот указатель разыменовывается в указатель на структуру.
WriteMemoryCallback(void *contents, size_t size, size_t nmemb, void *userp)
{
size_t realsize = size * nmemb;
struct MemoryStruct *mem = (struct MemoryStruct *)userp;
Согласитесь, синтаксис более наглядный, чем если бы мы использовали char* ? Конечно, можно возразить, что все равно, синтаксис не дает представления о том, на что ссылается указатель типа void*, но с другой стороны это дает свободу передавать в функцию указатель на что угодно.
Указатели на void, на мой взгляд вносят возможности некоторой унификации — неважно, указатель на что ты передаешь, ведь потом в месте его использования, ты можешь его разыменовать в нужный тебе тип данных.
Длина массивов
О, да! Здесь было сломано немало копьев. Читать или писать за границами массивов — это то, что сделать очень легко в Си. Но на практике я использую два очевидных правила, которые помогают ввести достаточный контроль:
массив объявленный в стэке должен быть фиксированной длины (соответственно запись и чтение в массив контролируется с помощью числовой константы, которая задает размерность массива)
массив объявленный в куче лучше поместить в структуру, в которой будет два поля — ссылка на сам массив и целое число с помощью которого будем контролировать размерность массива
Кстати, в итоге я пришел к системному подходу, и к каждой структуре, в которую оборачиваю массив динамически выделяемой памяти, добавляю еще поля с ссылками на функции аллокации/реаллокации/освобождения памяти в массивах. Ну и проверка значений возвращаемых функциями работы с памятью является умолчательной нормой.
Батарейки в комплект не входят
Тут все просто — или приходится писать самому, или искать библиотеку. Моя установка в этом случае такова, что если пишешь на Си, то будь готов принять и сишное понимание свободы. Здесь у меня есть практический совет и мечта. Практический совет — нарабатывайте свою библиотеку часто используемого кода, найдите и пользуйтесь чужими там, где это возможно и разумно. Это и организует и упростит вашу работу. Мечтаю же я все же о подобии CPAN для Си кода. Конечно, есть поиск по Интернету и по гитхабу, но все же централизованный каталог типа CPAN для Perl — это было бы классно. В некоторых моментах помогала библиотека Apache Portable Runtime, но большого опыта в ее использовании у меня нет, поэтому не могу гарантировать, что там все работает классно и быстро.
Коллаборация — это добро
Во многих языках есть возможности взаимодействия с Си-кодом. Еще когда-то давно для меня было удивительно найти в perl-коде, который подготавливал логи телефонной станции к отправке в биллинг, участок кода, который вызывал функцию в написанной на Си библиотеке. Функция, кстати выполняла нормализацию телефонных номеров и приведения их к единому виду. И у такого решения была причина — этот код работал очень быстро.
На мой взгляд, при взвешенном подходе, такие «гибриды» это реальный способ ускорить исполнение интерпретируемого кода.
Развивайтесь и исследуйте
Я взял себе за правило, что если даже мне не нужно написать что-то на Си, то я обязательно изучу код чего-то, что мне интересно на Си. В свое время читал монографию Роберта Лава про ядро Linux, было интересно посмотреть как работает планировщик. В вещах менее специфических тоже можно почерпнуть много нового интересного, а может и, наконец, понять как что работает. Я например, долгое время не понимал в чем разница между симметричным и асимметричным шифрованием, и что такое режим работы симметричных алгоритов. Посмотрев как работать с API криптографических библиотек я понял то, что для меня было непонятно. В целом существует множество интересных и полезных тем для изучения. Любое исследование чужого кода, особенно если он общеиспользуемый — это полезно (например libc).
Не верьте рекламе
Не торопитесь нырять в Си, до тех пор пока в этом нет нужды. Если вы только начинаете программировать, то Си — это точно не ваш выбор. На своем опыте, могу сказать, что изучение Си для меня совпало с моментом, когда нужно было и немного понять ассемблер. Три первых года — это вам точно не понадобится. Но далее, когда станете взрослее, и если не бросите кодить, то хотя бы для общего развития рекомендую базово разобраться в Си и ассемблере для x86. Это поможет начать путь к оптимизации кода. Конечно, ассемблерные вставки вы не начнете писать, но хотя бы более глубоко поймете как компьютер исполняет код.
* * *
Пожалуй это все, в рамках того, чтоб не вызвать споров о том какой язык «лучше». Надеюсь, кому-то, кто только начинает свой путь в Си, эти заметки пригодятся. Си для меня это та база, на которой стоит многое с чем я работаю, поэтому, какой-бы он ни был, это почти необходимость понимать его. Есть ситуации, где невозможно сбежать на какой-то другой язык, а надо решить задачу на том языке, на котором написан код. У меня так случилось, что даже в моих прикладных задачах часто встречается Си.