【问题标题】:Derived classes in C - What is your favorite method?C 中的派生类 - 你最喜欢的方法是什么?
【发布时间】:2009-03-18 21:01:12
【问题描述】:

根据我在面向对象的 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 中做派生类的方法是什么(不一定是我所做的)?为什么?

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

【问题讨论】:

  • 如果您确实使用第一种方法,则不应使用 .h 作为 parent_class 的文件扩展名,因为这是格式良好的头文件的惯例。使用 .inc 或类似的,所以很明显文件不是真正的标题。

标签: c oop


【解决方案1】:

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

更有趣的问题是,虚函数是怎么做的?在我们的例子中,结构有指向所有函数的指针,并且初始化设置它们。它稍微简单一些,但比使用指向共享 vtable 的指针的“正确方式”具有更多的空间开销。

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

【讨论】:

  • “我更喜欢使用 C++,而不是把它和普通的 C 混为一谈,但是政治”,这就是为什么它只被标记为 c。不带 c 和 c++ 标签。
【解决方案2】:

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

#define BODY int member1; \
             int member2; 

struct base_class
{
   BODY
};

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

【讨论】:

    【解决方案3】:

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

    【讨论】:

      【解决方案4】:

      第二个选项迫使你写很长的名字,比如myobj.parent.grandparent.attribute,这很难看。从语法的角度来看,第一个选项更好,但是将 child 转换为 parent 有点冒险 - 我不确定标准是否保证不同的结构对相似的成员具有相同的偏移量。我猜编译器可能会对此类结构使用不同的填充。

      如果您使用的是 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 编译。 我从未在实际项目中使用过它,但我前段时间对其进行了测试,它确实有效。

      【讨论】:

      • 访问嵌套结构不涉及任何额外的地址间接寻址,因此编译器可以直接计算嵌套结构中字段的地址,而不会产生任何性能损失。
      【解决方案5】:

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

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

      1. 为您节省一些周期,因为您将减少一次取消引用
      2. 当您将派生类指针强制转换为父类时,即 (struct parent_class *)ptr_my_derived_class ,您 100% 知道预期的结果。

      我更喜欢第一种方法,因为您可以毫无顾虑地将派生类指针强制转换为父类。

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

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

      【讨论】:

      • 是的,将任何结构转换为其第一个成员的类型,您将获得指向其第一个成员的指针。这是有保证的行为。
      • 我怀疑“拯救你来周期”并不是真正的胜利,因为几乎每个编译器都会在常量折叠期间消除额外的算术。
      • 方法 2 没有额外的取消引用。基本类型的字段是内联存储的,而不是通过引用存储的。
      【解决方案6】:

      我之前用过方法#2,发现效果很好:

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

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

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

      【讨论】:

        【解决方案7】:

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

        【讨论】:

          【解决方案8】:

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

          请注意,这是在我们针对所有目标平台都存在好的 C++ 编译器之前的日子。现在,这样做是愚蠢的。

          【讨论】:

            【解决方案9】:

            我更喜欢第一种方法,因为您可以毫无顾虑地将派生类指针强制转换为父类。

            反之亦然。

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

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

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

            【讨论】:

              猜你喜欢
              • 2011-05-24
              • 1970-01-01
              • 2010-09-13
              • 1970-01-01
              • 2010-09-05
              • 2010-10-08
              • 2011-07-18
              • 1970-01-01
              • 2010-09-06
              相关资源
              最近更新 更多