Объектно-ориентированное программирование: использование ООП в C

использование ООП в C Изучение

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

ООП в C — возможно ли это?

Язык программирования C сам по себе не предназначен для объектно-ориентированного программирования. Этот язык является ярким примером стиля структурированного программирования в рамках императивного программирования. Тем не менее, можно эмулировать объектно-ориентированные подходы в C. Язык имеет все необходимые компоненты на борту. Так служил C et al. как основу объектно-ориентированного программирования на Python.

С помощью ООП вы можете определить свои собственные «абстрактные типы данных» (ADT). АТД можно рассматривать как набор возможных значений и функций, которые с ними работают. Важно, чтобы внешне видимый интерфейс и внутренняя реализация были отделены друг от друга. Таким образом, как пользователь, вы можете быть уверены, что объекты типа ведут себя в соответствии с описанием.

Объектно-ориентированные языки, такие как Python, Java и C++, используют концепцию «класса» для моделирования абстрактных типов данных. Классы служат шаблоном для создания подобных объектов; также говорят об инстанцировании. C не знает никаких классов по умолчанию, и они также не могут быть воспроизведены в языке. Вместо этого существуют разные подходы к реализации функций ООП в C.

Как работает ООП в C?

Чтобы понять, как ООП работает в C, давайте сначала спросим себя: что такое ООП? Объектно-ориентированное программирование — это распространенный стиль программирования, являющийся проявлением парадигмы императивного программирования. Это контрастирует с декларативным программированием и его специализацией, функциональным программированием.

Основная идея объектно-ориентированного программирования — моделировать объекты и позволять им взаимодействовать друг с другом. Поток программы является результатом взаимодействия объектов и поэтому определяется только во время выполнения. По своей сути ООП включает в себя всего три свойства:

  1. Объекты инкапсулируют свое внутреннее состояние.
  2. Объекты получают сообщения через свои методы.
  3. Методы назначаются динамически во время выполнения.

Объект в чистом языке ООП, таком как 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, ключевые слова «частный», «защищенный» и т. д. используются для ограничения доступа к данным объекта. Это предотвращает прямой доступ извне и обеспечивает разделение интерфейса и реализации.

Читайте также:  Создание надежного и легко поддерживаемого кода через применение принципов SOLID

Другой механизм используется для реализации ООП в 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 рекомендуется только в исключительных случаях. Следующие подходы имеют больше смысла:

  1. Перепишите проект на языке C с функциями ООП и используйте существующую кодовую базу C в качестве спецификации.
  2. Перепишите части проекта на языке ООП и сохраните определенные компоненты C
Читайте также:  Превращаем список в строку с помощью метода join - Подробное пошаговое руководство

Пока кодовая база 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));
    }
}

Оцените статью
Блог о программировании
Добавить комментарий