[Перевод] Мифы и реальность языка программирования C

Интернет — первое, что построило человечество и чего человечество не понимает, крупнейший эксперимент в анархии за всю нашу историю .-- Эрик Шмидт

Контекст

1e82e70d74405f04f11496982e19d05d.jpeg

Очень часто кто-то где-нибудь на каком-нибудь форуме жалуется на нехватку инкапсуляции и изоляции в языке программирования C. Это происходит с такой регулярностью, что я намерен раз и навсегда разрушить этот миф. Благодаря этому когда в следующий раз кто-то будет делать подобные заявления, я смогу просто дать ссылку на эту страницу, а не писать объяснение заново.

Нужно сказать, что C — это старый язык, в котором не хватает множества современных возможностей. Но чего в нём хватает, так это инкапсуляции и изоляции.

Миф: поля структур не могут быть скрытыми

Давайте взглянем на определение класса, имеющего члены private; в конце-концов, ведь именно так возникает изоляция, правда? Если бы все поля были публичными, то это был бы просто C, но с наследованием1.

  1. Это сарказм, так что расслабьтесь.

class StringBuilder {
   private String payload;
   public void Append(String snippet) {
      payload = payload + snippet;
   }
};

Любая попытка вызывающей стороны получить доступ к полю payload приведёт к ошибке компиляции. Довольно удобно. Вы понимаете, когда это может быть полезно.

8cf330c56882d41c3d92d5be14b2d974.png

В языке программирования C нет классов, но есть struct, которые выглядят так:

struct StringBuilder {
   char *payload;
};

void Append(struct StringBuilder *obj, const char *snippet);

В нём нет модификаторов доступа; все поля в структуре публичны. Именно поэтому кто-то всегда жалуется на отсутствие в C инкапсуляции или изоляции: всё в структуре видно всем и всегда, даже вызывающей стороне функции Append().

Реальность

Но эта жалоба не всегда справедлива. Хотя вы конечно можете скинуть всё в один файл исходного кода и на этом закончить, чаще всего код разделяется на разные файлы и модули.

Когда код находится в разных файлах исходного кода, они инкапсулированы в модуль. Каждый »модуль» в C состоит из файла интерфейса, который вызывающие могут использовать для вызова функций, находящихся в файле реализации.

Файл интерфейса, называемый файлом заголовка (с расширением .h) — это контракт, сообщающий пользователю модуля, какие функции и типы нужны для использования реализации, достаточно часто называемой файлом исходного кода. После компиляции у вас получается скомпилированная реализация, и таким образом вы можете не иметь доступа к исходному коду Append().

И… та-да!

Вызывающие программы обязаны использовать файл заголовка для выполнения законных (по стандартам C) вызовов Append(); в конечном итоге, реализация может быть доступна только в скомпилированном виде, а не в виде исходного кода.

Поэтому в заголовке мы делаем следующее:

typedef struct StringBuilder StringBuilder;

void Append(StringBuilder *obj, const char *snippet);

А в реализации следующее:

struct StringBuilder {
   char *payload;
};

void Append(StringBuilder *obj, const char *snippet)
{
   ...
}

Вот и всё!

Теперь любой код, использующий структуру типа StringBuilder, сможет использовать всё нужное ему для создания строки, но никогда не увидит строки внутри неё.

Да он даже не сможет выполнить malloc() для своего собственного экземпляра StringBuilder, потому что скрыт даже размер StringBuilder. Ему придётся использовать функции создания и удаления, предоставленные в реализации и указанные в интерфейсе.

Но и это ещё не всё…

Итак, теперь у нас есть возможность создать экземпляр объекта, все поля которого скрыты от любой вызывающей стороны. Одновременно вы запретили всем вызывающим сторонам вмешиваться в поля своего объекта — весь доступ к объекту защищён функциями в реализации (как указано в заголовке).

У нас нет наследования, зато есть одна очень важная характеристика: из этого класса можно создавать объекты и использовать их, даже в Python.

Или в PHP.

И даже в Ruby.

Это будет работать с большинством реализаций Lisp.

Их можно вызывать из Java.

На самом деле, я не думаю, что какой-то из распространённых языков программирования не сможет использовать этот объект. Во многих случаях программисту на этом языке даже не придётся особо трудиться, чтобы использовать этот класс2.

  1. См. swig

В моих Makefiles уже есть правила автоматической генерации интерфейса, чтобы написанный в этой манере код на C можно было вызывать из приложений для Android.

Ладно, вы всё поняли, пора заканчивать

Вот так можно получить инкапсуляцию и изоляцию в C с надёжными гарантиями. Не верьте всему, что читаете в Интернете3.

  1. Кроме моего блога, конечно. Очевидно, что я образец честности и мудрости, за исключением случаев, когда это не так.

© Habrahabr.ru