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

时间:2009-03-18 21:01:13

标签: c oop

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


第一种方法,将父类定义为.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 个答案:

答案 0 :(得分:3)

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

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

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

答案 1 :(得分:3)

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

#define BODY int member1; \
             int member2; 

struct base_class
{
   BODY
};

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

答案 2 :(得分:1)

我知道GNOME使用第二种方法,而且指针也是众所周知的。我不记得有任何真正的箍要跳过这样做。事实上,从C内存模型的角度来看,两者之间不存在任何语义差异,因为AFAIK唯一的可能的差异将是编译器在结构填充方面的差异,但由于代码全部贯穿相同的编译器,这将是一个有争议的问题。

答案 3 :(得分:1)

第二个选项强制你写出很长的名字,比如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汇编。 我从未在现实生活中使用过它,但是我前段时间对它进行了测试并且有效。

答案 4 :(得分:0)

我使用的代码使用了第一种方法。

我能想到使用第一种方法的唯一两个原因是:

  1. 为您节省一些周期,因为您将少做一次解除引用
  2. 当您将派生类指针强制转换为父类时,即(struct parent_class *)ptr_my_derived_class,您就知道预期结果将是100%。
  3. 我更喜欢第一种方法,因为您可以将派生类指针强制转换为父类,而不必担心。

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

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

答案 5 :(得分:0)

之前我使用过方法#2,发现它的工作正常:

  • 如果它是派生类型
  • 中的第一个成员,您可以随时向上转换为基本类型
  • 而不是一直取消引用以获取基本成员,只需保留两个指针:一个用于基本接口,一个用于派生接口
指向基础结构的指针上的

free()当然也会释放派生字段,所以这不是问题......

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

答案 6 :(得分:0)

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

答案 7 :(得分:0)

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

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

答案 8 :(得分:0)

  

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

反过来说。

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

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

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