【问题标题】:Segmentation fault when calling derived class method调用派生类方法时出现分段错误
【发布时间】:2016-05-05 11:22:56
【问题描述】:

我有一个与使用数组参数设计派生类有关的问题。我有从 A 派生的 B 类。从 AA 派生的 BB 类分别具有 B 和 A 数组...

#include <iostream>

class A
{
public:
    A(){}
    virtual void foo(){std::cout<<"foo A\n";}
    int idx[3];
};

class B: public A
{
public:
    B():A(){}
    void foo(){std::cout<<"foo B\n";}
    int uidx[3];
};

class AA
{
public:
    AA(){}
    AA(int count){
        m_count = count;
        m_a = new A[count];
    }
    virtual A* getA(){return m_a;}
    ~AA(){ delete[] m_a;}
protected:
    A* m_a;
    int m_count;
};

class BB: public AA
{
public:
    BB(int count):AA()
    {
        m_count = count;
        m_a = new B[count];
    }
    B* getA(){return dynamic_cast<B*>(m_a);}
};

int main()
{
    AA* aa = new AA(2);
    BB* bb = new BB(2);
    B* b = bb->getA();
    B& b0 = *b;
    b0.idx[0] = 0;
    b0.idx[1] = 1;
    b0.idx[2] = 2;

    B& b1 = *(b+1);
    b1.idx[0] = 2;
    b1.idx[1] = 3;
    b1.idx[2] = 4;

    std::cout<<bb->getA()[1].idx[0]<<"\n"; //prints 2
    std::cout<<bb->getA()[1].idx[1]<<"\n"; //prints 3
    std::cout<<bb->getA()[1].idx[2]<<"\n"; //prints 4

    AA* cc = static_cast<AA*>(bb);
    cc->getA()[0].foo();  //prints foo B

    std::cout<<cc->getA()[1].idx[0]<<"\n"; //prints 4198624 ??
    std::cout<<cc->getA()[1].idx[1]<<"\n"; //prints 0 ??
    std::cout<<cc->getA()[1].idx[2]<<"\n"; //prints 2 ??

    cc->getA()[1].foo();  //segmentation fault
    delete aa;
    delete bb;
    return 0;
}

在将 BB 静态转换为 AA 后,我无法访问索引大于 0 的 A。 如何解决这个问题? 谢谢。

【问题讨论】:

标签: c++ class pointers segmentation-fault derived


【解决方案1】:

请注意,cc-&gt;getA() 在语义上等于cc-&gt;A::getA()(不是cc-&gt;B::getA())并返回指向A(而不是B*)的指针。

现在,由于AB 的子类,但后者还包含一些额外的字段,那么sizeof(B) &gt; sizeof(A)。由于cc-&gt;getA()[n] 基本上是*(cc-&gt;getA() + n)

cc->getA()[1].foo();

做同样的事情:

A * const tmp = cc->getA();
A & tmp2 = *(tmp + 1); // sizeof(A) bytes past tmp
tmp2.foo();

由于 C++ 标准的§5.7.6 [expr.add] 导致未定义的行为:

对于加法或减法,如果表达式 P 或 Q 的类型为“指向 cv T 的指针”,其中 T 和数组元素类型不相似 ([conv.qual]),则行为未定义。 [ 注意: 特别是,当数组包含派生类类型的对象时,指向基类的指针不能用于指针运算。 — 尾注 ]

您可能想要类似于以下的行为:

A * const tmp = cc->getA();
A & tmp2 = *(static_cast<B *>(tmp) + 1); // sizeof(B) bytes past tmp
tmp2.foo();

为此,您需要使用以下内容:

std::cout<<static_cast<B*>(cc->getA())[1].idx[0]<<"\n"; // prints 2
std::cout<<static_cast<B*>(cc->getA())[1].idx[1]<<"\n"; // prints 3
std::cout<<static_cast<B*>(cc->getA())[1].idx[2]<<"\n"; // prints 4

static_cast<B*>(cc->getA())[1].foo();  // prints foo B

但是,最好为AA 实现一个虚拟的A &amp; operator[](std::size_t) 运算符并在BB 中覆盖它。

【讨论】:

    【解决方案2】:

    我可以在您的代码中看到 2 个问题:

    1. 由于您的类负责内存管理,我建议您将析构函数设为virtual,因为如果您在任何时候尝试通过基指针删除派生类对象,派生类的析构函数将不会调用。这在您的当前代码中应该不是问题,但将来可能会成为问题。

    即:

    int main ()
        {
        AA* aa = new BB (2);
        delete aa;
        }
    

    在您的情况下不会调用BB::~BB()

    1. 您注意到的问题,并写下这个问题。

    在您将类型变量从BB* 转换为AA* 之后(即使不需要转换,由于类型是协变的,您也可以直接分配):

    AA* cc = dynamic_cast<AA*>(bb);
    

    您的变量cc 被视为AA* 类型(通常情况下,它具有BB* 的运行时类型并不重要 - 您不知道,也不应该关心关于确切的运行时类型)。在任何虚拟方法调用中,它们通过使用 vtable 被分派到正确的类型。

    现在,为什么控制台/分段错误中打印出奇怪的值? cc-&gt;getA () 的结果是什么?由于变量cc被视为AA*,因此返回值为A*(如前所述,实际类型为B*,但由于is-a继承关系被视为A*)。有什么问题,你可能会问:数组m_a 在两种情况下都是一样的大小,对吧?

    嗯,不是真的,要解释这一点,我需要解释数组索引在 C++ 中的工作原理,以及它与对象大小的关系。

    我想,我不会让你感到震惊,说明 B (sizeof (B)) 类型的对象的大小大于 A (sizeof (A)) 类型的对象的大小,因为 B 具有A 拥有的所有东西(由于继承),以及它自己的一些东西。在我的机器上sizeof(A) = 16 字节,sizeof(B) = 28 字节。

    因此,当您创建一个数组时,该数组占用的空间总量为[element_count] * [size of the element] 字节,这似乎是合乎逻辑的。但是,当您需要从数组中获取一个元素时,它需要计算出该元素在内存中的确切位置,在该数组占用的所有空间中,因此它通过计算它来做到这一点。这样做如下:[start of the array] + [index] * [size of element]

    而且,现在我们找到了问题的根源。你正在尝试做cc-&gt;getA ()[1],但是,因为cc,在引擎盖下,是BB*,所以AA::m_a变量的大小是2 * sizeof (B)(在我的机器上是= 2 * 28 = 56;第一个对象从偏移开始0 (0 * sizeof (B); 第二个偏移量28 (1 * sizeof(B))),但由于cc-&gt;getA () 被视为A*, and you are trying to fetch second element from the array (index 1), it tries to fetch the object from the offset of1 * sizeof (A)`,不幸的是,它位于为对象保留空间,但可以打印任何值/任何事情都可能发生 - 调用未定义的行为。

    如何解决?我将通过实现虚拟索引运算符来解决它,而不是 AA/BB 类上的 GetA 方法,如下所示:

    class AA
        {
        public:
            ...
            virtual A& operator[] (int idx)
                {
                return m_a[idx];
                }
            ...
        };
    
    class BB : public AA
        {
        public:
            ...
            virtual B& operator[] (int idx)
                {
                return dynamic_cast<B*>(m_a)[idx];
                }
            ...
        };
    

    但是,你需要小心调用对象本身的操作符,而不是指向对象的指针:

    std::cout << cc->operator[](1).idx[0] << "\n";
    std::cout << cc->operator[](1).idx[1] << "\n";
    std::cout << cc->operator[](1).idx[2] << "\n";
    

    【讨论】:

    • 感谢您的详细回复。我也有类似的想法。参考 1。如果我将打印添加到 AA 析构函数,它会告诉我它在删除 bb 时被调用,甚至没有将其标记为虚拟。至于2.有没有其他方法可以解决?我有一个辅助 API,如果可能的话,我想在不修改大部分 API 的情况下添加一些像 B 和 BB 这样的覆盖。
    • @哦,是的,没错。我把析构函数调用的顺序搞混了。会暂时解决这个问题。是的,还有另一种解决方法,但在某种意义上,与 jotik 建议的方法相同,但使用dynamic_cast 而不是static_cast,但在我看来,这个解决方案更干净。
    猜你喜欢
    • 2016-07-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-03-31
    • 1970-01-01
    • 2018-03-17
    • 2017-08-05
    • 2015-10-03
    相关资源
    最近更新 更多