Derived classes in C - What is your favorite metho

2020-02-10 09:26发布

问题:

In my experience in object oriented C programming I have seen two ways to implement derived classes.


First Method, have a definition of the parent class as a .h file. Then each class that derives from this class will have do:

File parent_class.h:

int member1;
int member2;

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

Second Method, would do:

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

What is your favorite method of doing derived classes in C ( doesn't have to be what I have done )? and why?

Which method would you prefer, first or second method ( or your own )?

回答1:

I'm one of the maintainers of a library that uses method 2. Works just as well as method 1, but without any preprocessor trickery. Or it actually works better, since you can have functions that take the base class as argument and you can just cast to the base struct, C guarantees that this works for the first member.

The more interesting question is, how do you do virtual functions? In our case, the struct has pointers to all the functions, and the initialization set them up. It's slightly simpler, but has more space overhead than the "proper way" with a pointer to a shared vtable.

Anyway, I'd prefer to use C++ rather than kludge it with plain C, but politics..



回答2:

The first method is hideous and it hides important information. I'd never use it or allow it being used. Even using a macro would be better:

#define BODY int member1; \
             int member2; 

struct base_class
{
   BODY
};

But method 2 is much better, for reasons others have pointed out.



回答3:

I know that GNOME uses the 2nd method, and casting pointers was a known thing as well. I don't remember there being any real hoops to jump through to do so. In fact, From a C memory model standpoint, there can't be any semantic difference between the two, since AFAIK the only possible difference would be compiler differences in structure padding, but since the code all runs through the same compiler, that would be a moot point.



回答4:

Second option forces you to write very long names like myobj.parent.grandparent.attribute, which is ugly. First option is better from syntax point of view, but it is a bit risky to cast child to parent - I'm not sure whether is is guaranteed by standard that different structs will have same offsets for similar members. I guess compiler may use different padding for such structs.

There is another option, if you are using GCC - anonymous struct members, which is part of MS extension, so I guess it was originated by some MS compiler and still may be supported by MS.

Declarations look like

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

In your "constructor" function you need to specify correct function for calculating area and the enjoy the inheritance and polymorphism. The only problem that you always need to pass explicit this - you cannot just call 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'

Compile with gcc -fms-extensions. I never used it in real-life project but I tested it some time ago and it worked.



回答5:

The code I worked with used the first method.

The only two reasons I can think of for using the first method is:

  1. Saves you some cycles because you will be doing one less de-referencing
  2. When you cast the derived class pointer to the parent class, i.e. (struct parent_class *)ptr_my_derived_class , you know 100% what the expected result will be.

I prefer the first method because you can cast the derived class pointer to the parent class without any worries.

Can you do the same thing with the second method? ( It seems like you would have to jump through a hoop or two to get the same end result )

If you can do the same with method 2, then I think both methods would be equal.



回答6:

I have used method #2 before and found it works quite ok:

  • you can upcast to the base type anytime if it is the first member in derived type
  • instead of dereferencing all the time to get at base members, just keep two pointers: one for the base interface, one for the derived interface

free() on the pointer to the base structure will of course also free up the derived fields, so that isn't an issue...

Also, I find accessing base fields something I tend to do in a polymorphic situation: I only care about those fields in methods that care about the base type. Fields in the derived type are used in methods only interested in the derived type.



回答7:

The second way has the advantage of typesafety with inherited methods. If you want to have a method foo(struct parent_class bar) and call it with foo((struct parentclass) derived_class), this will work correctly. The C-Standard defines this. Thus, I'd generally prefer method #2. In general, it is guaranteed that casting a structure to its first member will result in a struct containing the data of the first member of the struct, no matter how memory is laid out.



回答8:

At a former job, we used a preprocessor to handle this. We declared classes using a simple C++-style syntax, and the preprocessor generated C headers that were basically equivalent to the First Method, but without the #includes. It also did cool things like generating vtables and macros for upcasting and downcasting.

Note that this was in the days before good C++ compilers existed for all the platforms we targeted. Now, doing this would be stupid.



回答9:

I prefer the first method because you can cast the derived class pointer to the parent class without any worries.

It's the other way round.

The C standard guarantees that the address of a struct is the address of the first member, so in the second case it is safe to cast a pointer to derived to parent, as the first member of derived is the parent struct, and a the struct as a member as the same layout as the same struct when not a member, so casting a pointer to a derived to parent will always work.

The same is not true for the second case. Two structs with some members defined as the same type may have different padding between those members.

It would be reasonable for a 64 bit bigendian compiler to compile

struct A { a uint64_t; b uint32_t; };

such that sizeof(A) is a whole multiple of 8 and b is 64 bit aligned, but compile

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

so that sizeof(B) is a whole multiple of 8, but b is only 32 bit aligned so that it doesn't waste space.



标签: c oop