ОО лучшие практики для программ на С [закрыто]

19

«Если вы действительно хотите ОО-сахар - используйте C ++», - был немедленный ответ от одного из моих друзей, когда я спросил об этом. Я знаю, что две вещи здесь совершенно неверны. Во-первых, OO НЕ является «сахаром», а во-вторых, C ++ НЕ поглощает C.

Нам нужно написать сервер на C (внешний интерфейс которого будет в Python), и поэтому я изучаю более эффективные способы управления большими программами на C.

Моделирование большой системы с точки зрения объектов и взаимодействия объектов делает ее более управляемой, поддерживаемой и расширяемой. Но когда вы пытаетесь перевести эту модель на C, в котором нет объектов (и всего остального), вы сталкиваетесь с некоторыми важными решениями.

Вы создаете пользовательскую библиотеку для обеспечения абстракций OO, в которых нуждается ваша система? Такие вещи, как объекты, инкапсуляция, наследование, полиморфизм, исключения, pub / sub (события / сигналы), пространства имен, самоанализ и т. Д. (Например, GObject или COS ).

Или вы просто используете базовые конструкции C ( structи функции) для аппроксимации всех ваших классов объектов (и других абстракций) специальными способами. (например, некоторые ответы на этот вопрос на SO )

Первый подход дает вам структурированный способ реализации всей вашей модели на C. Но он также добавляет уровень сложности, который вы должны поддерживать. (Помните, что сложность была то, что мы хотели уменьшить, используя объекты в первую очередь).

Я не знаю о втором подходе и о том, насколько он эффективен в аппроксимации всех абстракций, которые могут вам понадобиться.

Итак, мои простые вопросы: каковы лучшие практики в реализации объектно-ориентированного проектирования в C. Помните, я не спрашиваю, КАК это сделать. Этот и этот вопросы говорят об этом, и даже об этом есть книга . Что меня больше интересует, так это некоторые реалистичные советы / примеры, которые касаются реальных проблем, возникающих при этом.

Примечание: пожалуйста, не советуйте, почему C не следует использовать в пользу C ++. Мы хорошо прошли этот этап.

treecoder
источник
3
Вы можете написать сервер C ++ так, чтобы его внешний интерфейс был extern "C"и мог использоваться из python. Вы можете сделать это вручную или воспользоваться SWIG . Так что стремление к интерфейсу Python - это не причина не использовать C ++. Это не так, говорят, что нет веских причин, чтобы хотеть остаться с C.
Ян Худек
1
Этот вопрос требует уточнения. В настоящее время параграфы 4 и 5 в основном спрашивают, какой подход следует использовать, но затем вы говорите, что «не спрашиваете, КАК это сделать», а вместо этого хотите (список?) Лучших практик. Если вы не ищете, КАК сделать это в C, то вы запрашиваете список «лучших практик», связанных с ООП в целом? Если это так, скажите это, но имейте в виду, что вопрос, скорее всего, будет закрыт за субъективность .
Калеб
:) Я прошу реальные примеры (код или иное), где это было сделано - и проблемы, с которыми они столкнулись при этом.
Treecoder
4
Ваши требования кажутся запутанными. Вы настаиваете на использовании ориентации объекта без какой-либо причины, которую я вижу (в некоторых языках это делает программы более удобными в обслуживании, но не на C), и настаиваете на использовании C. Ориентация объекта - это средство, а не цель или панацея , Кроме того, это значительно выигрывает от языковой поддержки. Если вы действительно хотели ОО, вы должны были учитывать это при выборе языка. Вопрос о том, как сделать большую программную систему с C, будет иметь гораздо больше смысла.
Дэвид Торнли
Возможно, вы захотите взглянуть на «Объектно-ориентированное моделирование и дизайн». (Rumbaugh et al.): Есть раздел, посвященный отображению ОО-конструкций на такие языки, как C.
Джорджио

Ответы:

16

Из моего ответа на Как я должен структурировать сложные проекты в C (не OO, а об управлении сложностью в C):

Ключ модульность. Это проще для разработки, реализации, компиляции и обслуживания.

  • Определите модули в вашем приложении, например, классы в приложении OO.
  • Отдельный интерфейс и реализация для каждого модуля, вставьте в интерфейс только то, что нужно другим модулям. Помните, что в C нет пространства имен, поэтому вы должны сделать все в своих интерфейсах уникальным (например, с префиксом).
  • Скрыть глобальные переменные в реализации и использовать функции доступа для чтения / записи.
  • Не думайте с точки зрения наследования, но с точки зрения состава. Как правило, не пытайтесь имитировать C ++ в C, это будет очень трудно читать и поддерживать.

Из моего ответа на Каковы типичные соглашения об именах для публичных и частных функций OO C (я думаю, что это лучшая практика):

Соглашение, которое я использую:

  • Открытая функция (в заголовочном файле):

    struct Classname;
    Classname_functionname(struct Classname * me, other args...);
  • Частная функция (статическая в файле реализации)

    static functionname(struct Classname * me, other args...)

Более того, многие инструменты UML могут генерировать C-код из диаграмм UML. Один с открытым исходным кодом Topcased .

mouviciel
источник
1
Отличный ответ. Цель модульности. Предполагается, что ОО это обеспечит, но 1) на практике слишком часто бывает так, что в итоге получаются спагетти ОО, и 2) это не единственный способ. Некоторые примеры из реальной жизни можно найти в ядре linux (модульность в стиле C) и проектах, в которых используется glib (OO в стиле C). У меня была возможность поработать с обоими стилями, и модульность IMO C-style выигрывает.
Joh
И почему именно композиция является лучшим подходом, чем наследование? Обоснование и вспомогательные ссылки приветствуются. Или вы имели в виду только программы на C?
Александр Блех
1
@AleksandrBlekh - Да, я имею в виду только Си.
Mouviciel
16

Я думаю, что вы должны различать ОО и С ++ в этой дискуссии.

Реализация объектов возможна в C, и это довольно просто - просто создайте структуры с помощью указателей на функции. Это твой «второй подход», и я бы пошел с ним. Другой вариант - не использовать указатели функций в структуре, а передавать структуру данных функциям в виде прямых вызовов в качестве указателя «контекста». Это лучше, IMHO, так как он более читабелен, более легко отслеживается и позволяет добавлять функции без изменения структуры (легко наследовать, если данные не добавляются). Это на самом деле, как C ++ обычно реализуетthis указатель.

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

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

Что касается пространств имен, исключений, шаблонов и т. Д. - я думаю, что если вы ограничены C - вы должны просто отказаться от них. Я работал над написанием OO на C, не стал бы делать это, если бы у меня был выбор (на этом месте я буквально пробился к внедрению C ++, и менеджеры были «удивлены», насколько легко было его интегрировать с остальными модулями C в конце.).

Само собой разумеется, что если вы можете использовать C ++ - используйте C ++. Нет реальной причины не делать этого.

littleadv
источник
На самом деле, вы можете наследовать от структуры и добавлять данные: просто объявите первый элемент дочерней структуры как переменную, тип которой является родительской структурой. Тогда бросай как тебе нужно.
Mouviciel
1
@mouviciel - да. Я сказал это. « ... так что вам придется либо включить родительскую структуру в ваш дочерний класс, либо ... »
littleadv
5
Нет причин пытаться реализовать наследование. Как средство для повторного использования кода, это ошибочная идея для начала. Композиция объектов проще и лучше.
KaptajnKold
@KaptajnKold - согласен.
littleadv
8

Вот основы того, как создать объектную ориентацию в C

1. Создание объектов и инкапсуляция

Обычно - каждый создает объект как

object_instance = create_object_typex(parameter);

Методы могут быть определены одним из двух способов здесь.

object_type_method_function(object_instance,parameter1)
OR
object_instance->method_function(object_instance_private_data,parameter1)

Обратите внимание, что в большинстве случаев object_instance (or object_instance_private_data)возвращается тип void *.. Приложение не может ссылаться на отдельные элементы или функции этого.

В дополнение к этому каждый метод использует эти object_instance для последующего метода.

2. Полиморфизм

Мы можем использовать множество функций и указателей на функции для переопределения определенных функций во время выполнения.

например - все object_methods определены как указатель на функцию, который может быть расширен как для открытых, так и для частных методов.

Мы также можем применить перегрузку функций в ограниченном смысле, использование var_args которого очень похоже на то, как переменное число аргументов определяется в printf. да, это не так гибко в C ++ - но это самый близкий путь.

3. Определение наследования

Определить наследование немного сложно, но со структурами можно сделать следующее.

typedef struct { 
     int age,
     int sex,
} person; 

typedef struct { 
     person p,
     enum specialty s;
} doctor;

typedef struct { 
     person p,
     enum subject s;
} engineer;

// use it like
engineer e1 = create_engineer(); 
get_person_age( (person *)e1); 

здесь doctorи engineerпроисходит от человека, и его можно привести, например, к более высокому уровню person.

Лучший пример этого используется в GObject и производных от него объектах.

4. Создание виртуальных классов. Я цитирую реальный пример из библиотеки libjpeg, используемой всеми браузерами для декодирования jpeg. Он создает виртуальный класс с именем error_manager, который приложение может создать конкретный экземпляр и предоставить обратно -

struct djpeg_dest_struct {
  /* start_output is called after jpeg_start_decompress finishes.
   * The color map will be ready at this time, if one is needed.
   */
  JMETHOD(void, start_output, (j_decompress_ptr cinfo,
                               djpeg_dest_ptr dinfo));
  /* Emit the specified number of pixel rows from the buffer. */
  JMETHOD(void, put_pixel_rows, (j_decompress_ptr cinfo,
                                 djpeg_dest_ptr dinfo,
                                 JDIMENSION rows_supplied));
  /* Finish up at the end of the image. */
  JMETHOD(void, finish_output, (j_decompress_ptr cinfo,
                                djpeg_dest_ptr dinfo));

  /* Target file spec; filled in by djpeg.c after object is created. */
  FILE * output_file;

  /* Output pixel-row buffer.  Created by module init or start_output.
   * Width is cinfo->output_width * cinfo->output_components;
   * height is buffer_height.
   */
  JSAMPARRAY buffer;
  JDIMENSION buffer_height;
};

Обратите внимание, что JMETHOD раскрывается в указателе функции через макрос, который должен быть загружен правильными методами соответственно.


Я пытался сказать так много вещей без особых индивидуальных объяснений. Но я надеюсь, что люди могут попробовать свои собственные вещи. Тем не менее, я намерен показать, как все складывается.

Кроме того, будет много аргументов, что это не будет точно истинным свойством эквивалента C ++. Я знаю, что ОО в Си не будет настолько строгим к своему определению. Но, работая так, можно понять некоторые из основных принципов.

Важно не то, чтобы ОО был таким строгим, как в C ++ и JAVA. Дело в том, что можно структурно организовать код с учетом ОО-мышления и управлять им таким образом.

Я настоятельно рекомендую людям увидеть реальный дизайн libjpeg и следующие ресурсы

а. Объектно-ориентированное программирование на C
b. это хорошее место, где люди обмениваются идеями
в. и вот полная книга

Дипан Мехта
источник
3

Объектная ориентация сводится к трем вещам:

1) Модульный дизайн программы с автономными классами.

2) Защита данных с частной инкапсуляцией.

3) Наследование / полиморфизм и другой полезный синтаксис, такой как конструкторы / деструкторы, шаблоны и т. Д.

1, безусловно, наиболее важен, и он также полностью независим от языка, все дело в разработке программы. В C вы делаете это путем создания автономных «программных модулей», состоящих из одного файла .h и одного файла .c. Рассматривайте это как эквивалент класса OO. Вы можете решить, что следует размещать внутри этого модуля, используя здравый смысл, UML или любой другой метод проектирования ОО, который вы используете для программ на C ++.

2 также очень важна не только для защиты от преднамеренного доступа к частным данным, но также для защиты от непреднамеренного доступа, то есть от «беспорядка пространства имен». C ++ делает это более изящными способами, чем C, но все еще может быть достигнуто в C с помощью ключевого слова static. Все переменные, которые вы объявили бы закрытыми в классе C ++, должны быть объявлены как статические в C и помещены в область видимости файла. Они доступны только из собственного модуля кода (класса). Вы можете написать «сеттеры / геттеры» так же, как и в C ++.

3 полезно, но не обязательно. Вы можете писать ОО-программы без наследования или без конструкторов / деструкторов. Эти вещи приятно иметь, они, безусловно, могут сделать программы более элегантными и, возможно, также более безопасными (или наоборот, если их использовать небрежно). Но они не нужны. Поскольку C не поддерживает ни одну из этих полезных функций, вам просто придется обойтись без них. Конструкторы могут быть заменены функциями init / destruct.

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

Наконец, каждый трюк ОО может быть сделан в книге С. Акселя-Тобиаса Шрайнера «Объектно-ориентированное программирование с ANSI C» начала 90-х, это доказывает. Однако я не рекомендовал бы эту книгу никому: она добавляет неприятную, странную сложность вашим программам на Си, которая просто не стоит суеты. (Книга доступна бесплатно здесь для тех , кто по- прежнему заинтересованы , несмотря на мое предупреждение.)

Поэтому я советую реализовать 1) и 2) выше и пропустить остальные. Это способ написания программ на Си, который успешно работает уже более 20 лет.


источник
2

Заимствуя некоторый опыт из различных сред выполнения Objective-C, написание динамической полиморфной ОО-возможности в C не так уж сложно (с другой стороны, быстрое и простое использование, по-видимому, все еще продолжается через 25 лет). Однако если вы реализуете объектную возможность в стиле Objective-C без расширения синтаксиса языка, то код, с которым вы в итоге работаете, будет довольно грязным:

  • каждый класс определяется структурой, объявляющей его суперкласс, интерфейсами, которым он соответствует, сообщениями, которые он реализует (как карта «селектора», имени сообщения, «реализации», функции, обеспечивающей поведение) и экземпляром класса переменная компоновка.
  • каждый экземпляр определяется структурой, которая содержит указатель на свой класс, а затем переменные экземпляра.
  • отправка сообщений осуществляется (дайте или возьмите некоторые особые случаи) с помощью функции, которая выглядит следующим образом objc_msgSend(object, selector, …). Зная, к какому классу относится экземпляр объекта, он может найти реализацию, соответствующую селектору, и, таким образом, выполнить правильную функцию.

Это все часть ОО-библиотеки общего назначения, разработанной для того, чтобы позволить нескольким разработчикам использовать и расширять классы друг друга, поэтому может оказаться излишним для вашего собственного проекта. Я часто проектировал C-проекты как «статические» ориентированные на классы проекты, используя структуры и функции: - каждый класс является определением структуры C, определяющей макет ivar - каждый экземпляр является просто экземпляром соответствующей структуры - объекты не могут быть "сообщение", но методы, похожие на методы MyClass_doSomething(struct MyClass *object, …), определены. Это делает вещи более понятными в коде, чем подход ObjC, но обладает меньшей гибкостью.

Где компромисс лжи, зависит от вашего собственного проекта: похоже, другие программисты не будут использовать ваши интерфейсы C, поэтому выбор зависит от внутренних предпочтений. Конечно, если вы решите, что хотите что-то вроде библиотеки времени выполнения objc, то есть кроссплатформенные библиотеки времени выполнения objc, которые будут обслуживать.


источник
1

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

С COS все немного по-другому, так как он поставляется с препроцессором, который расширяет синтаксис C некоторыми конструкциями OO. Существует аналогичный препроцессор для GObject, G Object Builder .

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

Ян Худек
источник
1

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

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

Если у вас еще нет дизайна, чтобы принять решение, сделайте всплеск и посмотрите, что говорит вам код.

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

РЕДАКТИРОВАТЬ: Таким образом, это решение по праву управления позволяет игнорировать первый пункт тогда.

JK.
источник