C中的派生类 – 您最喜欢的方法是什么?

根据我在面向对象的C编程方面的经验,我已经看到了两种实现派生类的方法。


第一种方法 ,将父类定义为.h文件。 然后,从这个类派生的每个类都将执行:

文件parent_class.h:

int member1; int member2; 

文件测试。:

 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; } 

第二种方法 ,会做:

文件测试。:

 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语言做派生类的方法是什么(不一定是我做过的)? 为什么?

您更喜欢哪种方法,第一种或第二种方法(或您自己的方法)?

我是使用方法2的库的维护者之一。与方法1一样工作,但没有任何预处理器技巧。 或者它实际上工作得更好,因为你可以拥有将基类作为参数的函数,并且你可以只转换为基础结构,C保证这适用于第一个成员。

更有趣的问题是,你如何做虚函数? 在我们的例子中,struct有指向所有函数的指针,初始化设置它们。 它稍微简单一点,但比使用指向共享vtable的指针的“正确方法”有更多的空间开销。

无论如何,我更喜欢使用C ++而不是用普通的C语言来克服它,但政治……

第一种方法很可怕,它隐藏了重要的信息。 我永远不会使用它或允许它被使用。 即使使用宏也会更好:

 #define BODY int member1; \ int member2; struct base_class { BODY }; 

但是方法2要好得多,原因是其他人指出的。

我知道GNOME使用第二种方法,而且指针也是一个已知的东西。 我不记得有任何真正的箍要跳过这样做。 事实上,从C内存模型的角度来看,两者之间不存在任何语义差异,因为AFAIK唯一可能的区别在于结构填充中的编译器差异,但由于代码都运行在同一个编译器中,因此没有实际意义。

第二个选项强制你编写很长的名字,比如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; }; 

在“构造函数”函数中,您需要指定正确的函数来计算区域并享受inheritance和多态。 唯一的问题是你总是需要明确地传递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. 为您节省一些周期,因为您将少做一次解引用
  2. 当您将派生类指针强制转换为父类时,即(struct parent_class *)ptr_my_derived_class,您将100%知道预期结果。

我更喜欢第一种方法,因为您可以将派生类指针强制转换为父类,而不必担心。

你能用第二种方法做同样的事情吗? (看起来你必须跳过一两圈才能获得相同的最终结果)

如果你可以用方法2做同样的事情,那么我认为两种方法都是平等的。

我之前使用过方法#2,发现它工作得很好:

  • 如果它是派生类型中的第一个成员,则可以随时向上转换为基类型
  • 而不是解除引用所有时间来获得基本成员,只需保留两个指针:一个用于基本接口,一个用于派生接口

指向基础结构的指针上的free()当然也会释放派生字段,所以这不是问题……

此外,我发现在多态情况下我倾向于访问基本字段:我只关心那些关心基类型的方法中的字段。 派生类型中的字段用于仅对派生类型感兴趣的方法。

第二种方式具有inheritance方法的类型安全的优点。 如果你想要一个方法foo(struct parent_class bar)并用foo((struct parentclass)derived_class)调用它,这将正常工作。 C标准定义了这一点。 因此,我通常更喜欢方法#2。 通常,无论内存如何布局,都可以保证将结构转换为第一个成员将导致包含结构的第一个成员的数据的结构。

在以前的工作中,我们使用预处理器来处理这个问题。 我们使用简单的C ++样式语法声明了类,并且预处理器生成的C头基本上等同于First Method,但没有#includes。 它也做了很酷的事情,比如为upcasting和downcasting生成vtable和宏。

请注意,这是在我们针对的所有平台都存在优秀C ++编译器的前几天。 现在,这样做会很愚蠢。

我更喜欢第一种方法,因为您可以将派生类指针强制转换为父类,而不必担心。

反过来说。

C标准保证结构的地址是第一个成员的地址,因此在第二种情况下,将指向派生到父项的指针是安全的,因为derived的第一个成员是父结构和结构作为一个成员,作为与不是成员时相同结构的相同布局,因此将指向派生到父对象的指针始终有效。

第二种情况也是如此。 将一些成员定义为相同类型的两个结构可能在这些成员之间具有不同的填充。

编译64位bigendian编译器是合理的

 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位对齐,因此不会浪费空间。