Производные классы в C - Какой ваш любимый метод? - PullRequest
4 голосов
/ 19 марта 2009

В своем опыте объектно-ориентированного программирования на Си я видел два способа реализации производных классов.


Первый метод , определение родительского класса в виде файла .h. Тогда каждый класс, производный от этого класса, будет делать:

Файл parent_class.h:

int member1;
int member2;

Файл testing.c:

struct parent_class {
    #include "parent_class.h" // must be first in the struct
}

struct my_derived_class {
    #include "parent_class.h" // must be first in the struct
    int member3;
    int member4;
}

Второй метод , будет делать:

Файл testing.c:

struct parent_class {
    int member3;
    int member4;
}

struct my_derived_class {
    struct parent_class; // must be first in the struct
    int member3;
    int member4;
}

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

Какой метод вы бы предпочли, первый или второй (или ваш)?

Ответы [ 9 ]

3 голосов
/ 19 марта 2009

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

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

В любом случае, я бы предпочел использовать C ++, а не класть его с простым C, но политикой ..

3 голосов
/ 19 марта 2009

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

#define BODY int member1; \
             int member2; 

struct base_class
{
   BODY
};

Но метод 2 намного лучше, по причинам, указанным другими.

1 голос
/ 19 марта 2009

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

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

Объявления выглядят как

struct shape {
    double  (*area)(struct shape *);
    const char    *name;
};

struct square {
    struct shape;           // anonymous member - MS extension
    double          side;
};

struct circle {
    struct shape;           // anonymous member - MS extension
    double          radius;
};

В вашей функции "конструктор" вам нужно указать правильную функцию для вычисления площади и наследования и полиморфизма. Единственная проблема, которую вам всегда нужно передать явно this - вы не можете просто позвонить shape[i]->area().

shape[0] = (struct shape *)new_square(5);
shape[1] = (struct shape *)new_circle(5);
shape[2] = (struct shape *)new_square(3);
shape[3] = (struct shape *)new_circle(3);

for (i = 0; i < 4; i++)
    printf("Shape %d (%s), area %f\n", i, shape[i]->name,
            shape[i]->area(shape[i]));    // have to pass explicit 'this'

Компилировать с gcc -fms-extensions. Я никогда не использовал его в реальных проектах, но я протестировал его некоторое время назад, и это сработало.

1 голос
/ 19 марта 2009

Я знаю, что GNOME использует 2-й метод, и использование указателей также было известно. Я не помню, чтобы были какие-то реальные обручи, чтобы прыгать, чтобы сделать это. Фактически, с точки зрения модели памяти C, между ними не может быть никакой семантической разницы, поскольку AFAIK единственная возможная разница будет разницей компилятора в заполнении структуры, но, поскольку весь код выполняется через тот же компилятор, это был бы спорный вопрос.

0 голосов
/ 19 марта 2009

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

Это наоборот.

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

То же самое не верно для второго случая. Две структуры с некоторыми элементами, определенными как один и тот же тип, могут иметь разные отступы между этими элементами.

Было бы разумно, чтобы 64-битный бигендовский компилятор компилировал

struct A { a uint64_t; b uint32_t; };

такой, что sizeof (A) является целым кратным 8, а b выровнен на 64 бита, но компилируется

struct B { a uint64_t; b uint32_t; c uint32_t; };

так, чтобы sizeof (B) был целым кратным 8, но b выровнен только на 32 бита, чтобы не тратить пространство.

0 голосов
/ 19 марта 2009

На прежней работе мы использовали препроцессор для обработки этого. Мы объявили классы, используя простой синтаксис в стиле C ++, и препроцессор сгенерировал заголовки C, которые в основном были эквивалентны первому методу, но без #include. Он также делал классные вещи, такие как генерация vtables и макросов для апскейтинга и апскейтинга.

Обратите внимание, что это было в те дни, когда хорошие компиляторы C ++ существовали для всех платформ, на которые мы нацеливались. Теперь делать это было бы глупо.

0 голосов
/ 19 марта 2009

Второй способ обладает преимуществом безопасности типов с унаследованными методами. Если вы хотите иметь метод foo (struct parent_class bar) и вызывать его с помощью foo ((struct parentclass) output_class), это будет работать правильно. Стандарт C определяет это. Таким образом, я бы предпочел метод № 2. В общем, гарантируется, что приведение структуры к ее первому члену приведет к структуре, содержащей данные первого члена структуры, независимо от того, как распределена память.

0 голосов
/ 19 марта 2009

Я уже использовал метод # 2 и обнаружил, что он работает вполне нормально:

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

free() по указателю на базовую структуру, конечно, также освободит производные поля, так что это не проблема ...

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

0 голосов
/ 19 марта 2009

Код, с которым я работал, использовал первый метод.

Единственные две причины, по которым я могу придумать использование первого метода:

  1. Сохраняет некоторые циклы, потому что вы будете делать меньше разыменования
  2. Когда вы приводите указатель производного класса к родительскому классу, то есть (struct parent_class *) ptr_my_derived_class, вы на 100% знаете ожидаемый результат.

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

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

Если вы можете сделать то же самое с методом 2, то я думаю, что оба метода будут равны.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...