Разве плохо писать объектно-ориентированный C? [закрыто]

14

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

struct foo {
    int x;
};

struct foo* createFoo(); // mallocs foo

void destroyFoo(struct foo* foo); // frees foo and its things

Это плохая практика? Как мне научиться писать С "правильным образом".

mosmo
источник
10
Большая часть Linux (ядра) написана таким образом, фактически она даже эмулирует даже более OO-подобные концепции, такие как диспетчеризация виртуальных методов. Я считаю это довольно правильным.
Килиан Фот
13
« [T] он определил, что настоящий программист может писать программы на Фортране на любом языке». - Эд Пост, 1983
Росс Паттерсон,
4
Есть ли причина, по которой вы не хотите переходить на C ++? Вам не нужно использовать части, которые вам не нравятся.
svick
5
Это действительно вызывает вопрос: «Что такое« объектно-ориентированный »?» Я бы не назвал этот объект ориентированным. Я бы сказал, что это процедурно. (У вас нет наследования, нет полиморфизма, нет инкапсуляции / возможности скрыть состояние, и, возможно, вам не хватает других отличительных признаков ОО, которые не выходят из головы.) Хорошая или плохая практика не зависит от этой семантики , хоть.
jpmc26
3
@ jpmc26: Если вы являетесь лингвистическим специалистом по предписаниям, вам следует послушать Алана Кея, он изобрел термин, он должен сказать, что он означает, и он говорит, что ООП - это все о сообщениях . Если вы являетесь лингвистическим дескриптивистом, вы бы изучили использование этого термина в сообществе разработчиков программного обеспечения. Кук сделал именно это, он проанализировал особенности языков, которые утверждают или считаются ОО, и обнаружил, что у них есть одна общая черта: обмен сообщениями .
Jörg W Mittag

Ответы:

24

Нет, это неплохая практика, ее даже поощряют, хотя можно даже использовать такие соглашения, как struct foo *foo_new();иvoid foo_free(struct foo *foo);

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

Префикс foo_является соглашением, которому следует множество библиотек, поскольку он защищает от конфликтов с именами других библиотек. Другие функции часто имеют соглашение об использовании foo_<function>(struct foo *foo, <parameters>);. Это позволяет вам struct fooбыть непрозрачным типом.

Посмотрите документацию libcurl для соглашения, особенно с "подпространствами имен", так что вызов функции curl_multi_*выглядит неправильно с первого взгляда, когда первый параметр был возвращен curl_easy_init().

Есть еще более общие подходы, см. Объектно-ориентированное программирование с ANSI-C

отстой
источник
11
Всегда с оговоркой "где это уместно". ООП это не серебряная пуля.
Дедупликатор
Разве в C нет пространств имен, в которых вы могли бы объявить эти функции? Похоже std::string, не могли бы вы foo::create? Я не использую C. Может быть, это только в C ++?
Крис Cirefice
@ChrisCirefice В C нет пространств имен, поэтому многие авторы библиотек используют префиксы для своих функций.
Residuum
2

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

gnasher729
источник
4
Не то, чтобы я мог не согласиться, но ваше мнение действительно должно быть подкреплено некоторыми уточнениями.
Дедупликатор
1

Это не плохо. Он рекомендует использовать RAII, который предотвращает многие ошибки (утечки памяти, использование неинициализированных переменных, использование после освобождения и т. Д., Которые могут вызвать проблемы безопасности).

Итак, если вы хотите скомпилировать свой код только с помощью GCC или Clang (а не с помощью компилятора MS), вы можете использовать cleanupатрибут, который будет должным образом разрушать ваши объекты. Если вы объявите свой объект так:

my_str __attribute__((cleanup(my_str_destructor))) ptr;

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

Кроме того, не забывайте использовать my_str_в именах ваших методов, потому что Cу них нет пространств имен, и легко столкнуться с некоторыми другими именами функций.

Marqin
источник
2
Afaik, RAII об использовании неявного вызова деструкторов для объектов в C ++ для обеспечения очистки, избегая необходимости добавлять явные вызовы освобождения ресурса. Так что, если я не сильно ошибаюсь, RAII и C являются взаимоисключающими.
cmaster - восстановить монику
@cmaster Если вы используете #defineваши typenames, __attribute__((cleanup(my_str_destructor)))вы получите их как неявные во всей #defineобласти видимости (они будут добавлены во все объявления переменных).
Маркин
Это работает, если а) вы используете gcc, б) если вы используете тип только в локальных переменных функции, и в) если вы используете тип только в голой версии (нет указателей на #defineтип d или его массивы). Короче говоря, это не стандарт C, и вы платите с большой гибкостью в использовании.
cmaster - восстановить монику
Как уже упоминалось в моем ответе, это также работает в Clang.
Маркин
Ах, я этого не заметил. Это действительно делает требование а) значительно менее строгим, поскольку это делает в __attribute__((cleanup()))значительной степени квази-стандарт. Тем не менее, б) и в) все еще стоят ...
cmaster - восстановить монику
-2

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

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

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

Например, на традиционных компиляторах исторические компиляторы интерпретируют следующий код:

struct pair { int i1,i2; };
struct trio { int i1,i2,i3; };

void hey(struct pair *p, struct trio *t)
{
  p->i1++;
  t->i1^=1;
  p->i1--;
  t->i1^=1;
}

выполняя следующие шаги по порядку: увеличивать первый член *p, дополнять младший бит первого члена *t, затем уменьшать первый член *pи дополнять младший бит первого члена *t. Современные компиляторы переставлять последовательность операций в моде , какой код , который будет более эффективным , если pи tидентификации различных объектов, но изменить поведение , если они этого не делают.

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

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

void swap_pointers(void **p1, void **p2)
{
  void *temp = *p1;
  *p1 = *p2;
  *p2 = temp;
}

В Стандарте C, однако, нужно использовать:

#include "string.h"
#include "stdlib.h"
void swap_pointers2(void **p1, void **p2)
{
  void **temp = malloc(sizeof (void*));
  memcpy(temp, p1, sizeof (void*));
  memcpy(p1, p2, sizeof (void*));
  memcpy(p2, temp, sizeof (void*));
  free(temp);
}

Если *p2он хранится в выделенном хранилище, а временный указатель не хранится в выделенном хранилище, эффективным типом *p2станет тип временного указателя и код, который пытается использовать в *p2качестве любого типа, который не соответствует временному указателю. Тип вызовет неопределенное поведение. Крайне маловероятно, что компилятор заметит такую ​​вещь, но поскольку современная философия компилятора требует, чтобы программисты избегали Undefined Behavior любой ценой, я не могу придумать никаких других безопасных способов написания вышеуказанного кода без использования выделенного хранилища ,

Supercat
источник
Downvoters: Хотите прокомментировать? Ключевым аспектом объектно-ориентированного программирования является возможность иметь несколько типов, совместно использующих общие аспекты, и иметь указатель на любой такой тип, который можно использовать для доступа к этим общим аспектам. Пример OP не делает этого, но он лишь царапает поверхность «объектно-ориентированного». Исторические компиляторы C позволяли бы писать полиморфный код гораздо чище, чем это возможно в сегодняшнем стандарте C. Таким образом, для разработки объектно-ориентированного кода на C требуется определить, на какой именно язык он ориентирован. С каким аспектом люди не согласны?
суперкат
Хм ... не могли бы вы показать, что гарантии, которые дает стандарт, не позволяют вам получить чистый доступ к членам общей начальной подпоследовательности? Потому что я думаю, что это то, что твоя напыщенная речь о пороках смелости оптимизировать в рамках договорного поведения зависит от этого времени? (Это мое предположение, что два downvoters нашли нежелательным.)
Дедупликатор
ООП не обязательно требует наследования, поэтому совместимость между двумя структурами не является большой проблемой на практике. Я могу получить настоящий ОО, поместив указатели на функции в структуру и вызывая эти функции определенным образом. Конечно, этот foo_function(foo*, ...)псевдо-OO в C - это просто особый стиль API, который выглядит как классы, но его более правильно назвать модульным программированием с абстрактными типами данных.
Amon
@Deduplicator: см. Указанный пример. Поле «i1» является членом общей начальной последовательности обеих структур, но современные компиляторы потерпят неудачу, если код попытается использовать «struct pair *» для доступа к начальному члену «struct trio».
суперкат
Какой современный компилятор C потерпел неудачу в этом примере? Какие-нибудь интересные варианты нужны?
Дедупликатор
-3

Следующим шагом является скрытие объявления структуры. Вы помещаете это в файл .h:

typedef struct foo_s foo_t;

foo_t * foo_new(...);
void foo_destroy(foo_t *foo);
some_type foo_whatever(foo_t *foo, ...);
...

А затем в файле .c:

struct foo_s {
    ...
};
Соломон Медленный
источник
6
Это может быть следующим шагом, в зависимости от цели. Так или иначе, это даже не пытается ответить на вопрос.
Дедупликатор