当前位置: 首页 > 知识库问答 >
问题:

C中的OO多态性,混淆现象?

韩志专
2023-03-14

我和一位同事正试图实现一个简单的多态类层次结构。我们正在开发嵌入式系统,并且仅限于使用C编译器。我们有一个基本的设计理念,可以在没有警告的情况下编译(-Wall -Wextra -fstrict-aliasing -pedantic),并且在gcc 4.8.1下运行良好。

然而,我们有点担心别名问题,因为我们并不完全了解这何时会成为一个问题。

为了演示,我们编写了一个带有“接口”IHello和两个实现该接口的类“Cat”和“dog”的玩具html" target="_blank">示例

#include <stdio.h>

/* -------- IHello -------- */
struct IHello_;
typedef struct IHello_
{
    void (*SayHello)(const struct IHello_* self, const char* greeting);
} IHello;

/* Helper function */
void SayHello(const IHello* self, const char* greeting)
{
    self->SayHello(self, greeting);
}

/* -------- Cat -------- */
typedef struct Cat_
{
    IHello hello;
    const char* name;
    int age;
} Cat;

void Cat_SayHello(const IHello* self, const char* greeting)
{
    const Cat* cat = (const Cat*) self;
    printf("%s I am a cat! My name is %s and I am %d years old.\n",
           greeting,
           cat->name,
           cat->age);
}

Cat Cat_Create(const char* name, const int age)
{
    static const IHello catHello = { Cat_SayHello };
    Cat cat;

    cat.hello = catHello;
    cat.name = name;
    cat.age = age;

    return cat;
}

/* -------- Dog -------- */
typedef struct Dog_
{
    IHello hello;
    double weight;
    int age;
    const char* sound;
} Dog;

void Dog_SayHello(const IHello* self, const char* greeting)
{
    const Dog* dog = (const Dog*) self;
    printf("%s I am a dog! I can make this sound: %s I am %d years old and weigh %.1f kg.\n",
           greeting,
           dog->sound,
           dog->age,
           dog->weight);
}

Dog Dog_Create(const char* sound, const int age, const double weight)
{
    static const IHello dogHello = { Dog_SayHello };
    Dog dog;

    dog.hello = dogHello;
    dog.sound = sound;
    dog.age = age;
    dog.weight = weight;

    return dog;
}

/* Client code */
int main(void)
{
    const Cat cat = Cat_Create("Mittens", 5);
    const Dog dog = Dog_Create("Woof!", 4, 10.3);

    SayHello((IHello*) &cat, "Good day!");
    SayHello((IHello*) &dog, "Hi there!");

    return 0;
}

输出:

日安!我是一只猫!我的名字是连指手套,我今年5岁。

嘿,你好!我是一只狗!我可以发出这样的声音:哇!我今年4岁,体重10.3公斤。

我们非常确定从猫和狗到IHello的“向上投射”是安全的,因为IHello是这两个结构的第一个成员。

我们真正关心的是在SayHello的相应接口实现中从IHello到Cat和Dog的“降级”。这会导致任何严格的别名问题吗?我们的代码是否保证能按照C标准工作,或者我们只是幸运地在gcc中工作?

更新

我们最终决定使用的解决方案必须是标准的C,不能依赖例如gcc扩展。代码必须能够使用各种(专有)编译器在不同的处理器上编译和运行。

这种“模式”的目的是客户端代码应接收指向IHello的指针,因此只能调用接口中的函数。但是,这些调用的行为必须不同,具体取决于接收到的IHello的实现。简而言之,我们希望与实现此接口的接口和类的OOP概念的行为相同。

我们知道只有当IHello接口结构被放置为实现接口的结构的第一个成员时,代码才有效。这是我们愿意接受的限制。

根据:通过C转换访问结构的第一个字段是否违反严格的别名?

§6.7.2.1/13:

在结构对象中,非位字段成员和位字段所在的单元具有按声明顺序增加的地址。指向经过适当转换的结构对象的指针指向其初始成员(或者,如果该成员是位字段,则指向它所在的单元),反之亦然。结构对象中可能存在未命名的填充,但在其开头没有。

混淆现象规则如下(§6.5/7):

对象的存储值只能由具有以下类型之一的左值表达式访问:

  • 与对象的有效类型兼容的类型。
  • 与对象的有效类型兼容的类型的限定版本。
  • 一种类型,它是与对象的有效类型相对应的有符号或无符号类型。
  • 一种类型,它是与对象有效类型的限定版本相对应的有符号或无符号类型。
  • 在其成员中包括上述类型之一的聚合或联合类型(递归地包括子聚合或包含的联合的成员),或
  • 字符类型

根据上面的第五点以及结构顶部不包含填充的事实,我们相当确定将实现接口的派生结构“向上转换”为指向接口的指针是安全的,即

Cat cat;
const IHello* catPtr = (const IHello*) &cat; /* Upcast */

/* Inside client code */
void Greet(const IHello* interface, const char* greeting)
{
    /* Users do not need to know whether interface points to a Cat or Dog. */
    interface->SayHello(interface, greeting); /* Dereferencing should be safe */
}

最大的问题是接口函数实现中使用的“向下转换”是否安全。如上所示:

void Cat_SayHello(const IHello* hello, const char* greeting)
{
    /* Is the following statement safe if we know for
     * a fact that hello points to a Cat?
     * Does it violate strict aliasing rules? */
    const Cat* cat = (const Cat*) hello;
    /* Access internal state in Cat */
}

另请注意,将实现函数的签名更改为

Cat_SayHello(const Cat* cat, const char* greeting);
Dog_SayHello(const Dog* dog, const char* greeting);

注释掉“向下转换”也可以编译和运行良好。但是,这会生成函数签名不匹配的编译器警告。

共有2个答案

印劲
2023-03-14

从您引用的标准部分:

一个指向结构对象的指针,经过适当的转换,指向它的初始成员(或者如果该成员是位字段,则指向它所在的单元),反之亦然

转换像cat这样的指针绝对是安全的 -

调用站点,您正在做相反的事情:将指向结构的指针转换为指向第一个元素的指针。这也保证起作用。

宰父保臣
2023-03-14

多年来,我一直在用c做对象,做的那种构图正是你在这里做的那种。我建议你不要做你所描述的简单演员,而是证明我需要一个例子。例如,与分层实现一起使用的计时器回调机制:

typedef struct MSecTimer_struct MSecTimer;
struct MSecTimer_struct {
     DoubleLinkedListNode       m_list;
     void                       (*m_expiry)(MSecTimer *);
     unsigned int               m_ticks;
     unsigned int               m_needsClear: 1;
     unsigned int               m_user: 7;
};

当这些计时器之一到期时,管理系统调用m_expiry函数并传入指向对象的指针:

timer->m_expiry(timer);

然后拿一个有惊人表现的基础物体来说:

typedef struct BaseDoer_struct BaseDoer;
struct BaseDoer_struct
{
     DebugID      m_id;
     void         (*v_beAmazing)(BaseDoer *);  //object's "virtual" function
};

//BaseDoer's version of BaseDoer's 'virtual' beAmazing function
void BaseDoer_v_BaseDoer_beAmazing( BaseDoer *self )
{
    printf("Basically, I'm amazing\n");
}

我的命名系统在这里是有目的的,但这不是重点。我们可以看到可能需要的各种面向对象的函数调用:

typedef struct DelayDoer_struct DelayDoer;
struct DelayDoer_struct {
     BaseDoer     m_baseDoer;
     MSecTimer    m_delayTimer;
};

//DelayDoer's version of BaseDoer's 'virtual' beAmazing function
void DelayDoer_v_BaseDoer_beAmazing( BaseDoer *base_self )
{
     //instead of just casting, have the compiler do something smarter
     DelayDoer *self = GetObjectFromMember(DelayDoer,m_baseDoer,base_self);

     MSecTimer_start(m_delayTimer,1000);  //make them wait for it
}

//DelayDoer::DelayTimer's version of MSecTimer's 'virtual' expiry function
void DelayDoer_DelayTimer_v_MSecTimer_expiry( MSecTimer *timer_self )
{
    DelayDoer *self = GetObjectFromMember(DelayDoer,m_delayTimer,timer_self);
    BaseDoer_v_BaseDoer_beAmazing(&self->m_baseDoer);
}

大约从1990年开始,我就一直对GetObjectFromMember使用相同的宏,Linux内核也创建了相同的宏,并将其命名为container_of(不过参数的顺序不同):

  #define GetObjectFromMember(ObjectType,MemberName,MemberPointer) \
              ((ObjectType *)(((char *)MemberPointer) - ((char *)(&(((ObjectType *)0)->MemberName)))))

它依赖于(技术上)未定义的行为(解引用一个空对象),但是可以移植到我测试过的所有旧的(和新的)c编译器上。新版本需要宏的offsetof,它现在是标准的一部分(显然是从C89开始):

#define container_of(ptr, type, member) ({ \
            const typeof( ((type *)0)->member ) *__mptr = (ptr); 
            (type *)( (char *)__mptr - offsetof(type,member) );})

当然,我更喜欢我的名字,但无论如何。使用此方法可以使您的代码不依赖于将基对象放在第一位,并且还可以使第二个用例成为可能,我发现这在实践中非常有用。所有别名编译器问题都在宏中管理(我认为是通过char * 进行转换,但我并不是真正的标准律师)。

 类似资料:
  • 我已经运行这个程序好几次了,每次最后打印的p和g的值都是一样的。我不太确定为什么会这样——malloc不能在理论上选择内存中的任何位置让p和g指向吗?为什么p 8总是等于g?如果能澄清,将不胜感激。 谢谢

  • 问题内容: 我是使用属性的新手,因此我进行了如下所示的简单测试。在测试中,我创建了两个类“ Test1”和“ Test2”,每个类都持有一个值。我正在尝试使用属性来控制对伪隐藏的“ val”属性的访问。当前测试不限制“ val”属性的任何输入或输出,因为该程序仅是概念证明。下面显示的两个测试类产生相同的结果,并被认为代表了构造属性的不同方法。我要引用的属性的示例使用在python docs上找到。

  • “有四种多态性:参数、包含、强制和重载”。 在注释中,它指用具有不同参数的方法重载,也指重载运算符,例如在ints和floats意义上的+。 Wikipedia还指出,“在许多语言中,使用函数重载支持ad hoc多态性。”

  • 我已经成功地用Maven配置了Proguard来混淆jar及其依赖jar。我已经设法让两个混淆使用相同的映射文件,这样一个jar就可以调用另一个jar的方法。我面临的问题是,Proguard没有在模糊的jar中保留唯一的名称;两个模糊的jar都包含一个名为 由于有两个名为F.B.class的类(每个jar中有一个),因此优先级被赋予了调用jar中的类,这就造成了问题。 也被应用,但它显然只将此应用

  • 我有一个具有登录功能的控制器类。当我输入用户名和密码并按submit时,它将调用此控制器并在会话中存储客户信息。但有一件事让我感到困惑,那就是@model属性 我将使用@ModelAttribute Customer存储我输入的用户名和密码,并使用Customer c存储我从customService获得的所有信息,并将其存储到会话中。但是会话存储的是客户。 如果我这样改变论点。它工作正常

  • 我一直在摆弄Proguard配置,我想测试只是为了优化 但我仍然会遇到这样的错误: java.lang.IllegalArgumentExc0019:找不到[org/apache/log/log4j/core/jackson/Log4jXmlMoules](有1个已知的超级类)和[org/apache/log/log4j/core/jackson/Log4jJsonMoules](有4个已知的超级