В отличие от языков ООП C++ и Objective-C, C не имеет никаких объектно-ориентированных функций. В связи с широким распространением языка и популярностью объектно-ориентированного программирования существуют подходы к использованию ООП в C.
- ООП в C — возможно ли это?
- Как работает ООП в C?
- Объекты модели как структуры данных
- Определить типы для создания объектов
- Инкапсуляция внутреннего состояния
- Замените методы функциями.
- Cоздавать экземпляры объектов
- Как можно снова настроить проект C в объектно-ориентированном стиле?
- Объектно-ориентированные языки в стиле C
- Объектно-ориентированные языки на основе C
ООП в C — возможно ли это?
Язык программирования C сам по себе не предназначен для объектно-ориентированного программирования. Этот язык является ярким примером стиля структурированного программирования в рамках императивного программирования. Тем не менее, можно эмулировать объектно-ориентированные подходы в C. Язык имеет все необходимые компоненты на борту. Так служил C et al. как основу объектно-ориентированного программирования на Python.
С помощью ООП вы можете определить свои собственные «абстрактные типы данных» (ADT). АТД можно рассматривать как набор возможных значений и функций, которые с ними работают. Важно, чтобы внешне видимый интерфейс и внутренняя реализация были отделены друг от друга. Таким образом, как пользователь, вы можете быть уверены, что объекты типа ведут себя в соответствии с описанием.
Объектно-ориентированные языки, такие как Python, Java и C++, используют концепцию «класса» для моделирования абстрактных типов данных. Классы служат шаблоном для создания подобных объектов; также говорят об инстанцировании. C не знает никаких классов по умолчанию, и они также не могут быть воспроизведены в языке. Вместо этого существуют разные подходы к реализации функций ООП в C.
Как работает ООП в C?
Чтобы понять, как ООП работает в C, давайте сначала спросим себя: что такое ООП? Объектно-ориентированное программирование — это распространенный стиль программирования, являющийся проявлением парадигмы императивного программирования. Это контрастирует с декларативным программированием и его специализацией, функциональным программированием.
Основная идея объектно-ориентированного программирования — моделировать объекты и позволять им взаимодействовать друг с другом. Поток программы является результатом взаимодействия объектов и поэтому определяется только во время выполнения. По своей сути ООП включает в себя всего три свойства:
- Объекты инкапсулируют свое внутреннее состояние.
- Объекты получают сообщения через свои методы.
- Методы назначаются динамически во время выполнения.
Объект в чистом языке ООП, таком как Java, является автономным объектом. Это включает в себя сколь угодно сложную структуру данных, а также методы (функции), которые с ней оперируют. Внутреннее состояние объекта, отображаемое в содержащихся в нем данных, может быть считано и изменено только с помощью методов. Языковая функция под названием «сборщик мусора» обычно используется для управления памятью объектов.
Соединение структуры данных и функций с объектами в C не так просто. Вместо этого вы вяжете управляемую систему структур данных, определений типов, указателей и функций. Как обычно в C, программист отвечает за правильное выделение и освобождение памяти.
Получившийся в результате объектно-ориентированный код C не совсем похож на тот, к которому вы привыкли в языках ООП, но он работает. Мы даем обзор основных концепций ООП и их эквивалентов в C:
| Концепция ООП | Эквивалент в C |
| Сорт | тип структуры |
| экземпляр класса | экземпляр структуры |
| метод экземпляра | Функция, которая принимает указатели на структурные переменные |
| эта/самостоятельная переменная | Указатель на структурную переменную |
| создание экземпляра | Размещение и ссылка через указатели |
| новое ключевое слово | вызов malloc |
Объекты модели как структуры данных
Во-первых, давайте посмотрим, как структура данных объекта может быть смоделирована в стиле ООП. C — это компактный язык, который обходится всего несколькими языковыми конструкциями. Для создания структур данных любой сложности используются так называемые «Структуры», название которых происходит от термина «Структура данных».
Структура C определяет структуру данных, которая включает в себя поля, называемые «членами». В других языках такая конструкция также называется «записью», поэтому структуру можно представить как строку в таблице базы данных: комбинацию нескольких полей, возможно, разных типов.
Синтаксис объявления структуры в C очень прост:
struct struct_name;
При желании мы также определяем структуру таким же образом, указывая члены с именами и типами. В качестве стандартного примера рассмотрим точку в двумерном пространстве с координатами x и y. Мы показываем определение структуры:
struct point { /* X-coordinate */ int x; /* Y-coordinate */ int y; }
В обычном коде C затем создается экземпляр структурной переменной. Мы создаем переменную и инициализируем оба поля 0:
struct point origin = {0, 0};
Затем значения полей можно считать и сбросить. Доступ к членам осуществляется через синтаксис origin.x и origin.y, знакомый по другим языкам :
/* Read struct member */ origin.x == 0 /* Assign struct member */ origin.y = 42
Однако это нарушает требование инкапсуляции: доступ к внутреннему состоянию объекта возможен только через определенные для него методы. Так что в нашем подходе все еще чего-то не хватает.
Определить типы для создания объектов
Как уже упоминалось, в C нет понятия класса. Вместо этого типы могут быть определены с помощью оператора typedef. Мы используем typedef, чтобы дать типу данных новое имя:
typedef <old-type-name> <new-type-name>
Вот как можно определить соответствующий тип точки для нашей структуры точек:
typedef struct point Point;
Сочетание typedef с определением структуры примерно эквивалентно определению класса в Java:
typedef struct point { /* X-coordinate */ int x; /* Y-coordinate */ int y; } Point;
Вот соответствующее определение класса в Java:
class Point { private int x; private int y; };
Использование typedef позволяет нам создать точечную переменную без использования ключевого слова struct:
Point origin = {0, 0} /* Instead of */ struct point origin = {0, 0}
Чего все еще не хватает, так это инкапсуляции внутреннего состояния.
Инкапсуляция внутреннего состояния
Объекты отражают свое внутреннее состояние в своей структуре данных. В языках ООП, таких как Java, ключевые слова «частный», «защищенный» и т. д. используются для ограничения доступа к данным объекта. Это предотвращает прямой доступ извне и обеспечивает разделение интерфейса и реализации.
Другой механизм используется для реализации ООП в C. В качестве интерфейса мы используем так называемое форвардное объявление в заголовочном файле и тем самым создаем «неполный тип»:
/* In C header file */ struct point; /* Incomplete type */ typedef struct point Point;
Реализация точечной структуры приведена в отдельном файле исходного кода C, в котором заголовок интегрируется с помощью макроса включения. Этот подход позволяет избежать создания статических переменных типа точки. Также можно использовать указатели типа. Поскольку объекты представляют собой динамически создаваемые структуры данных, на них в любом случае ссылаются с помощью указателей. Указатели на экземпляры структур примерно соответствуют ссылкам на объекты, используемым в Java.
Замените методы функциями.
В языках ООП, таких как Java и Python, объекты включают в себя не только свои данные, но и функции, которые над ними работают. Они называются методами или методами экземпляра. При написании кода ООП на C вместо использования методов мы используем функции, которые принимают указатель на экземпляр структуры:
/* Pointer to `Point` struct */ Point * point;
Поскольку в C нет классов, невозможно сгруппировать функции, принадлежащие одному типу, под общим именем. Вместо этого мы ставим перед именами функций имя типа. Соответствующие сигнатуры функций сначала объявляются в заголовочном файле C:
/* In C header file */ /* Function to move update a point's coordinates */ void Point_move(Point * point, int new_x, int new_y);
Затем мы реализуем функцию в файле исходного кода C:
/* In C source file */ void Point_move(Point * point, int new_x, int new_y) { point->x = new_x; point->y = new_y; };
Этот подход напоминает методы Python, которые представляют собой обычные функции, принимающие self в качестве первого параметра. Кроме того, указатель на экземпляр структуры примерно эквивалентен переменной this в Java или JavaScript. Разница в том, что при вызове функции C указатель передается явно:
/* Call function with pointer argument */ Point_move(point, 42, 51);
При эквивалентном вызове функции в Java объект точки доступен в методе как эта переменная:
// Call instance method from outside of class point.move(42, 51) // Call instance method from within class this.move(42, 51)
Python позволяет вызывать методы как функции с явным аргументом self:
# Call instance method from outside or from within class self.move(42, 51) # Function call from within class move(self, 42, 51)
Cоздавать экземпляры объектов
Отличительной чертой C является ручное управление памятью: программисты несут ответственность за выделение памяти для структур данных. Объектно-ориентированные динамические языки, такие как Java и Python, сделают всю работу за вас. В Java ключевое слово new используется для создания экземпляра объекта. Память автоматически выделяется под капотом:
// Create new Point instance Point point = new Point();
Когда мы пишем ООП-код на C, мы определяем специальную функцию-конструктор для создания экземпляра. Это выделяет память для нашего экземпляра структуры, инициализирует его и возвращает указатель на него:
Point * Point_new(int x, int y) { /* Allocate memory and cast to pointer type */ Point * point = (Point *) malloc(sizeof(Point)); /* Initialize members */ Point_init(point, x, y); // return pointer return point; };
В нашем примере мы отделяем инициализацию членов структуры от инстанцирования. Снова используется функция с префиксом точки:
void Point_init(Point * point, int x, int y) { point->x = x; point->y = y; };
Как можно снова настроить проект C в объектно-ориентированном стиле?
Переписывать существующий проект с описанными методами ООП на C рекомендуется только в исключительных случаях. Следующие подходы имеют больше смысла:
- Перепишите проект на языке C с функциями ООП и используйте существующую кодовую базу C в качестве спецификации.
- Перепишите части проекта на языке ООП и сохраните определенные компоненты C
Пока кодовая база C написана хорошо, второй подход должен давать хорошие результаты. Общепринятой практикой является реализация критичных к производительности частей программы на C и доступ к ним с других языков. Вероятно, нет другого языка, который подходит для этого лучше, чем C. Но какие языки подходят для настройки существующего проекта C с использованием принципов ООП?
Объектно-ориентированные языки в стиле C
Существует большое разнообразие C-подобных языков со встроенной объектной ориентацией. Наиболее известен C++; однако этот язык известен своей сложностью, что в последние годы привело к отказу от C++. Из-за того, что основные языковые конструкции во многом одинаковы, код C можно относительно легко интегрировать в C++.
Objective-C намного легче, чем C++. Диалект C, основанный на оригинальном языке ООП Smalltalk, в основном использовался для программирования приложений на Mac и ранних операционных системах iOS. Позже за этим последовала разработка собственного языка Apple Swift. Функции, написанные на C, можно вызывать из обоих языков.
Объектно-ориентированные языки на основе C
Другие языки программирования ООП, синтаксически не связанные с C, также подходят для переписывания проекта C. Стандартные подходы к интеграции кода C существуют для Python, Rust и Java.
В Python так называемые привязки Python позволяют интегрировать код C. Типы данных Python, возможно, придется преобразовать в соответствующие ctypes. Существует также интерфейс внешних функций C (CFFI), который в определенной степени автоматизирует преобразование типов.
Rust также поддерживает вызов функций C без особых усилий. Интерфейс внешней функции (FFI) можно определить с помощью ключевого слова extern. Функции Rust, которые обращаются к внешним функциям, должны быть объявлены небезопасными:
extern "C" { fn abs(input: i32) -> i32; } fn main() { unsafe { println!("Absolute value of -3 according to C: {}", abs(-3)); } }








