Интересно сравнить ваши рекомендации по созданию открытых/закрытых членов класса/экземпляра, виртуализации/наследованию, рефакторингу и тестированию, TDD и пр. на С.
Столь детально разбирать вопрос, чтобы была методичка, у меня времени не хватает - всё же основная тема ООП, а не то, как в рамках ООП использовать C. Т.е. я даю этот вопрос для общего развития студентов и углубления понимания ими того, как работают объектные программы - это средство, а не самостоятельная цель в курсе ООП.
По этой же причине рефакторинг и тестирование идёт мимо.
Инкапсуляция. Я советую каждый класс оформлять отдельной парой h- и c-файлов с соответствующими названиями (в духе Java/C#). В h-файл выносить открытое, в c-файл - закрытое. Есть некоторые сложности со структурами. Чтобы не показывать их содержимое, нужно использовать только указатели на них - не всегда это удобно.
Атрибуты и методы класса задаются как глобальные переменные и обычные функции.
Атрибуты объекта задаются в структуре, названием совпадающей с названием класса (и файла). Методы объекта - это функции, первым параметром которых является указатель на структуру с атрибутами, идентификатор этого параметра - всегда this.
Наследование. Наследование данных - через агрегацию структур. Наследование функций - через делегирование вызовов.
/*
class A
{
private:
int a;
public:
A();
void f();
};
class B: public A
{
private:
int b;
public:
B();
};
*/
struct A
{
int a;
};
struct A *A_constructor()
{
struct A *instance = (struct A)malloc(sizeof(struct A));
instance->a = 0;
return instance;
}
void A_destructor(struct A *this)
{
free(this);
}
void A_f(struct A *this)
{
printf("%i", this->a);
}
struct B
{
struct A *baseA;
int b;
}
struct B *B_constructor()
{
struct B *instance = (struct B)malloc(sizeof(struct B));
instance->baseA = A_constructor();
instance->b = 0;
return instance;
}
void B_destructor(struct B *this)
{
A_destructor(this->baseA);
free(this);
}
void B_f(struct B *this)
{
A_f(this->baseA);
}
Поскольку конструкции здесь во многом "регулярные", при желании их можно оформить макросами. А договорившись об отказе от множественного наследования, и ссылку на предка можно сделать унифицированной.
Виртуальные методы - вручную реализовать таблицы виртуальных функций.
Вышеприведённый пример с контролем типов годится лишь для таких потомков, которые не имеют своих данных и поэтому не используют агрегацию структуры предка в структуре потомка.
struct A;
typedef void (*A_F)(struct A *this);
struct A
{
int a;
A_F f;
};
void A_f(struct A *this);
struct A *A_constructor()
{
struct A *instance = (struct A)malloc(sizeof(struct A));
instance->a = 0;
instance->f = A_f;
return instance;
}
void A_destructor(struct A *this)
{
free(this);
}
void A_f(struct A *this)
{
printf("%i", this->a);
}
typedef struct B struct A;
void B_f(struct B *this);
struct B *B_constructor()
{
struct A *instance = A_constructor();
instance->f = B_f;
return instance;
}
void B_destructor(struct B *this)
{
A_destructor(this);
}
void B_f(struct B *this)
{
printf("Hello world");
}
int main()
{
struct B *b = B_constructor();
b->f(b);
B_destructor(b);
return 0;
}
Если же надо комбинировать наследование данных и виртуальные функции, существуют разные способы сделать это. Как правило, таблица виртуальных функций выделяется в отдельную структуру, и пишется вспомогательная функция-селектор нужного указателя на функцию. Сами функции можно унифицировать, отказавшись от контроля типа параметра this - использовать void *.
Но вообще-то комбинирование наследования данных и виртуальных функций не особенно часто нужно. В C++ и других языках, где синтаксически записать наследование очень просто, злоупотребляют этим средством. Классы в иерархии логически должны относиться к одной сущности как общее и частное. В большинстве случаев: либо имеется ассоциативная связь двух сущностей - тогда правильнее использовать делегирование, а не виртуальные функции; либо требуется обеспечить динамический полиморфизм необобщающихся по реализации методов - тогда нужны абстрактные методы или в общем случае интерфейсы. А интерфейс есть ни что иное, как выделенная в отдельную структуру таблица виртуальных функций, которой придали самостоятельный смысл - она описывает отдельный протокол взаимодействия с объектом в некоторой ассоциации, в которой этот объект играет определённую роль.