C 中的继承和方法覆盖 - 如何使其定义行为

问题描述 投票:0回答:1

我有我自定义的小 OOP 式继承功能,像这样:

// base class
struct BaseTag;

typedef struct {
    int (*DoAwesomeStuff)(struct BaseTag* pInstance);
} S_BaseVtable;

typedef struct BaseTag{
    S_BaseVtable* pVtable;
    int AwesomeValue;
} S_Base;

// child class
struct ChildTag;

typedef struct {
    S_BaseVtable Base;
    void (*SomeOtherStuff)(struct ChildTag* pInstance);
} S_ChildVTable;

typedef struct ChildTag {
    S_Base BaseClass;
    int EvenAwesomerValue;
} S_Child;

现在假设我有一个子类构造函数,其中基类 vtable 被子 vtable 覆盖:

void Child_ctor(S_Child* pInstance) {
    Base_ctor((S_Base*) pInstance);
    pInstance.BaseClass.pVtable = (S_BaseVtable*) &MyChildVTable;
}

同样在这个子vtable中,我想用这样的方法覆盖基类的

DoAwesomeStuff()
方法:

int Child_DoAwesomeStuff(struct BaseTag* pInstance) {
    S_Child* pChild = (S_Child*) pInstance; // undefined behaviour
    return pChild->EvenAwesomerValue;
}

我偶尔会看到这种模式的变化,但我发现它存在一些问题。我的主要问题是

  • 如何从隐藏在
    S_ChildVtable
    指针后面的子实例访问
    S_BaseVtable
  • 如何正确地将
    pInstance
    Child_DoAwesomeStuff()
    参数转换为
    S_Child*
    类型?

据我了解 C 标准,从

S_Child*
S_Base*
(以及相应的 vtable 类型)是可以的,因为
S_Child
的第一个成员是一个
S_Base
实例。但反之亦然,这是未定义的行为。

S_Child* pChild = (S_Child*)((char*) pInstance)
这样的东西是合法的和定义的吗?

c oop language-lawyer dynamic-cast strict-aliasing
1个回答
0
投票

只要传递给

pInstance
Child_DoAwesomeStuff()
指针实际上最初构造为
S_Child
对象,然后将指针强制转换回
S_Child
指针 (
S_Child*
) 是 not 未定义的行为。如果您尝试将指向最初构造为
struct BaseTag
(aka
S_Base
)类型的对象的指针转换为子指针类型,这只会是未定义的行为。

这也是 C++ 的工作方式,使用

dynamic_cast<>()
(我在这里的回答中提到)。

来自https://cplusplus.com/doc/tutorial/typecasting/“dynamic_cast”部分下的示例 C++ 代码如下。

注意

pba
pbb
都是指向基类型(
Base *
)的指针,然而,
pba
实际上是constructed作为
Derived
(子)类型通过
new Derived
,而
pbb
实际上是 constructed 通过
Base
.
因此,将 

new Base

 转换为 
pba 是完全有效的,因为它确实是那种类型,但是将
Derived*
 转换为 
pbb
not
有效的,因为它 not 确实是那种类型. C++ 的 Derived* 调用在运行时捕捉到这个未定义的行为,检测到返回的类型不是完全形成的
dynamic_cast<Derived*>(pbb)
类型,并返回一个
Derived
,它等于
nullptr
,所以你得到打印那说
0
(代码下方继续讨论)

Null pointer on second type-cast.

输出:

// dynamic_cast #include <iostream> #include <exception> using namespace std; class Base { virtual void dummy() {} }; class Derived: public Base { int a; }; int main () { try { Base * pba = new Derived; Base * pbb = new Base; Derived * pd; pd = dynamic_cast<Derived*>(pba); if (pd==0) cout << "Null pointer on first type-cast.\n"; pd = dynamic_cast<Derived*>(pbb); if (pd==0) cout << "Null pointer on second type-cast.\n"; } catch (exception& e) {cout << "Exception: " << e.what();} return 0; }

同样,您的代码具有相同的行为。

这样做是有效的:

Null pointer on second type-cast.

但是这样做是

好的: // create a child S_Child child; // treat it like a base (ok since `S_Base` is at the beginning of it--since the // child contains a base object) S_Base* pBase = (S_Base*)&child; // Now obtain the child back from the base pointer S_Child* pChild = (S_Child*)pBase; // ok, since pBase really points to a // child object

只是一个警告:传递指针并将指针存储到 vtables 和函数以及 C 结构中的东西将使跟踪您的代码并试图理解它
非常

困难!没有人知道的索引器(包括 Eclipse,并且 Eclipse 有我见过的最好的索引器)可以追溯到代码中哪个函数或类型被分配给了一个指针。除非你做这些只是为了学习练习,或者是为了在 C 中从头开始引导你自己的 C++ 语言(同样,为了学习),否则我建议不要使用这些模式。 如果你想要“面向对象

”具有继承和所有功能的 C,请不要这样做。如果你想要“object-based”C,通过不透明的指针/结构进行基本的私有成员封装和数据隐藏,那很好!以下是我更喜欢这样做的方式:Option 1.5(“基于对象的”C 架构).

© www.soinside.com 2019 - 2024. All rights reserved.