Почему адрес этого указателя в деструкторе отличается от ожидаемого (c ++) - PullRequest
4 голосов
/ 07 декабря 2010

У меня странная проблема с указателем this в деструкторе базового класса.

Описание проблемы:

У меня есть 3 класса: A1 , A2 , A3

A2 наследуется публично от A1 и наследуется от A3

class A2:private A3, public A1 {...}

A3 имеет функцию getPrimaryInstance () ..., которая возвращает ссылку типа A1 на A2 экземпляр:

A1& A3::getPrimaryInstance()const{
    static A2 primary;
    return primary;
}

И конструктор A3 выглядит следующим образом:

A3(){
    getPrimaryInstance().regInst(this);
}

(где regInst (...) - это функция, определенная в A1 , которая сохраняет указатели на все A3 экземпляры)

Аналогично деструктор A3 :

~A3(){
    getPrimaryInstance().unregInst(this);
}

^ Вот где возникает проблема!

Когда статический A2 -экземпляр с именем primary разрушается при завершении программы, вызывается деструктор A3 , но внутри ~ A3 Я пытаюсь получить доступ к функции в том же экземпляре, который уничтожаю. => Нарушение прав доступа во время выполнения!

Поэтому я подумал, что это можно исправить с помощью простого оператора if, например:

~A3(){
    if(this != (A3*)(A2*)&getPrimaryInstance()) //Original verison
    //if (this != dynamic_cast<A3*>(static_cast<A2*>(&getPrimaryInstance())))
    //Not working. Problem with seeing definitions, see comments below this post

        getPrimaryInstance().unregInst(this);
}

(Причиной двойного приведения является наследство:)
A1 A3
, \ /
, A2
(Но это не важно, могло бы быть просто (int) или как-то еще)

Кикер в том, что он все еще падает. Выполнение кода с помощью отладчика показывает, что когда мой A2 primary -instance получает уничтожает указатель this в деструкторе и адрес, который я получаю при вызове getPrimaryInstance () по какой-то причине не совпадает вообще! Я не могу понять, почему адрес, на который указывает этот , всегда отличается от того, каким он должен быть (насколько мне известно). (

Делаем это в деструкторе:

int test = (int)this - (int)&getPrimaryInstance();

Также показал мне, что разница не постоянна (у меня была краткая теория, что существует некоторое постоянное смещение), так что это как два совершенно разных объекта, когда они должны быть одинаковыми. (

Я пишу в VC ++ Express (2008). И, немного погуглив, нашел следующую статью MS:
ИСПРАВЛЕНИЕ: Указатель «this» неверен в деструкторе базового класса

Это не та же проблема, что и у меня (и она также была якобы исправлена ​​в C ++. Net 2003). Но несмотря на то, что симптомы казались одинаковыми и , они представляли собой простой обходной путь, поэтому я решил попробовать:
Удалил неработающее - if -статейство и добавил в virtual перед вторым наследованием к A2 , например так:

class A2:private A3, public A1 {...} // <-- old version
class A2:private A3, virtual public A1 {...} //new, with virtual!

И ЭТО РАБОТАЛО! Указатель this по-прежнему неверен, но больше не дает нарушения доступа.

Итак, мой главный вопрос: почему?
Почему этот -пункт не указывает на то, где он должен (?)?
Почему добавление virtual к наследованию, как описано выше, решает его (несмотря на то, что this по-прежнему указывает куда-то еще, чем & getPrimaryInstance () )?
Это ошибка? Может кто-нибудь попробовать это в среде без MS?
И самое главное: Это безопасно ?? Конечно, он больше не жалуется, но я все еще волнуюсь, что он не делает то, что должен. : S

Если кто-то знает об этом или имеет опыт и может помочь мне понять это, я был бы очень благодарен, так как я все еще изучаю C ++, и это совершенно за пределами моих текущих знаний.

Ответы [ 8 ]

4 голосов
/ 07 декабря 2010

Вы используете C бросает, убивает вас.
Особенно подвержен разрывам в ситуациях с множественным наследованием.

Вам необходимо использовать dynamic_cast <>, чтобы свергнуть иерархию классов. Хотя вы можете использовать static_cast <> для продвижения вверх (как я это сделал), но иногда я думаю, что более динамично использовать dynamic_cast <> для перемещения в обоих направлениях.

Избегайте приведения в стиле C в коде C ++

C ++ имеет 4 различных типа приведения, разработанных для замены приведения в стиле C. Вы используете эквивалент reinterpret_cast <> и используете его неправильно (любой хороший C ++ разработчик, увидев reinterpret_cast <>, собирается задержаться здесь на секунду).

~A3(){
    if(this != (A3*)(A2*)&getPrimaryInstance())
        getPrimaryInstance().unregInst(this);
}

Should be:

~A3()
{
   if (this != dynamic_cast<A3*>(static_cast<A2*>(&getPrimaryInstance())))
   {    getPrimaryInstance().unregInst(this);
   }
}

Расположение объекта A2: (возможно, что-то вроде этого).

     Offset   Data
A2   0x00   |------------------
     0x10   * A3 Stuff
            *------------------
     0x20   * A1 Stuff
            *------------------
     0x30   * A2 Stuff

В getPrimaryInstance ()

 // Lets assume:
 std::cout << primary; // has an address of 0xFF00

Возвращенная ссылка будет указывать на часть А1 объекта:

std::cout << &getPrimaryInstancce();
// Should now print out 0xFF20

Если вы используете приведение в стиле C, оно ничего не проверяет, просто меняет тип:

std::cout << (A2*)&getPrimaryInstancce();
// Should now print out 0xFF20
std::cout << (A3*)(A2*)&getPrimaryInstancce();
// Should now print out 0xFF20

Хотя, если вы используете приведение C ++, оно должно корректно компенсировать:

std::cout << static_cast<A2*>(&getPrimaryInstance());
// Should now print out 0xFF00
std::cout << dynamic_cast<A3*>(static_cast<A2*>(&getPrimaryInstance()));
// Should now print out 0xFF10

Конечно, фактические значения очень зависят от компилятора и будут зависеть от макета реализации. Выше приведен лишь пример того, что может произойти.

Хотя, как указывалось выше, вероятно, небезопасно вызывать dynamic_cast <> для объекта, который в настоящее время находится в процессе уничтожения.

Так как насчет

Измените regInst (), чтобы он возвращал true для первого зарегистрированного объекта. GetPrimaryInstance () всегда будет первым созданным объектом, поэтому он всегда будет первым, кто зарегистрирует себя.

сохранить этот результат в локальной переменной-члене и отменить регистрацию, только если вы не первый:

A3()
{
    m_IamFirst = getPrimaryInstance().regInst(this);
}

~A3()
{
    if (!m_IamFirst)
    {
         getPrimaryInstance().unregInst(this);
    }
}

Вопросы:

Почему указатель this не указывает на то, где он должен (?)?

Да. Просто с помощью C-Cast привинтите указатели.

Почему добавление виртуального к наследованию, как описано выше, решает эту проблему (несмотря на то, что это все еще указывает куда-то еще, кроме & getPrimaryInstance ())?

Потому что он меняет расположение в памяти таким образом, что C-Cast больше не портит указатель.

Это ошибка?

Нет. Неправильное использование C-Cast

Может кто-нибудь попробовать его в среде не-MS?

Это будет делать нечто подобное.

И самое главное: это безопасно ??

Нет. Это реализация, определяемая тем, как выложены виртуальные члены. Тебе просто повезло.

Решение: прекратить использование бросков в стиле C. Используйте соответствующее приведение C ++.

3 голосов
/ 07 декабря 2010
class A2 : private A3, public A1 {...} // <-- old version
class A2 : private A3, virtual public A1 {...} //new, with virtual!  

Почему добавление виртуального к наследованию, как описано выше, решает эту проблему (несмотря на то, что это указывает на что-то еще, кроме & getPrimaryInstance ())?

Причина, по которой это имеет значение, заключается в том, что virtual наследование влияет на порядок вызова конструкторов базового класса и деструкторов базового класса.

Причина, по которой числовые значения ваших this указателей не совпадают, заключается в том, что разные "подобъекты базового класса" вашего полного объекта A2 primary; могут и должны иметь разные адреса. Перед вызовом любых деструкторов вы можете использовать dynamic_cast, чтобы получить значение от A1* до A2*. Когда вы уверены, что объект A3 действительно является частной базовой частью A2, вы можете использовать приведение в стиле C, чтобы получить от A3* до A2*.

Но как только тело деструктора ~A2 завершено, как в случае с деструктором ~A3, dynamic_cast с A1* до A2* завершится неудачно, а стиль C приведен с A3* to A2* приведет к неопределенному поведению, поскольку больше нет A2 объекта.

Так что, вероятно, нет способа сделать то, что вы пытаетесь, если вы не измените способ хранения / доступа к A2 primary;.

2 голосов
/ 07 декабря 2010

Виртуальный базовый класс должен вступать в игру только в том случае, если у вас есть структура наследования бриллиантов, т. Е. Вы наследуете несколько классов, имеющих общий базовый класс. Показываете ли вы фактическое дерево наследования A1, A2 и A3 в своем фактическом коде?

1 голос
/ 07 декабря 2010

OP @AnorZaken прокомментировал:

... Это была одна из первоначальных проблем, которые я пытался решить: я хотел бы, чтобы getPrimaryInstance () возвращал ссылку A2 напрямую, но я не могу!A3 не видел определения A2!Так как getPrimaryInstance () объявлен в базовом классе A3 (не упомянуто выше), вы получите: error C2555: «A3 :: getPrimaryInstance»: тип возвращаемой переменной виртуальной функции отличается и не является ковариантным относительно «A3Base :: getPrimaryInstance»даже если я заявляю о существовании A2, я не знаю, как сказать компилятору, что у A2 есть A1 в качестве базы, прежде чем я объявлю A2.:( Если бы я мог решить эту проблему, это было бы здорово!

Так что, похоже, у вас есть что-то вроде:

class A3Base {
public:
  virtual A1& getPrimaryInstance();
};

И поскольку class A2 нельзя определить раньше class A3, я бы просто пропустил ковариантный тип возвращаемого значения. Если вам нужен способ получить ссылку A2& из A3, добавьте его как другой метод .

// A3.hpp
class A2;

class A3 : public A3Base {
public:
  virtual A1& getPrimaryInstance();
  A2& getPrimaryInstanceAsA2();
};

// A3.cpp
#include "A3.hpp"
#include "A2.hpp"

A1& A3::getPrimaryInstance() {
    return getPrimaryInstanceAsA2(); // no cast needed for "upward" public conversion
}
1 голос
/ 07 декабря 2010

Проблема может заключаться в том, что когда A3 :: ~ A3 () вызывается для объекта A2, объект A2 уже уничтожается, поскольку ~ A3 () вызывается в конце уничтожения A2. Вы не можете снова вызвать getPrimary, поскольку он уже уничтожен. Примечание: это относится к случаю первичной статической переменной

0 голосов
/ 07 декабря 2010

Есть 2 гораздо больших вопроса, которые вы должны задать:

  1. Почему мне нужно использовать личное наследование?
  2. Почему мне нужно использовать множественное наследование для неабстрактногобазовые классы?

В большинстве случаев ответом на # 1 является то, что вы не знаете.Объявление класса, который содержит член данных для другого класса, обычно обрабатывает эту же ситуацию намного проще и проще в обслуживании кодовой базы.

В большинстве случаев ответом на вопрос №2 также является то, что вы этого не делаете.Это особенность языка, который вы используете на свой страх и риск.

Я рекомендую вам прочитать книги Мейерса и пересмотреть ваш шаблон проектирования.

0 голосов
/ 07 декабря 2010

Когда статический A2 -экземпляр с именем primary разрушается при завершении программы, вызывается деструктор A3, но внутри ~ A3 я пытаюсь получить доступ к функции в том же экземпляре что я уничтожаю. => Нарушение прав доступа во время выполнения!

Когда статический A2 -экземпляр с именем primary равен уничтожен , указатель на primary будет указывать на " случайное место в памяти. Поэтому вы пытаетесь получить доступ к произвольной ячейке памяти и получаете нарушение времени выполнения. Все это связано с тем, в каком порядке вы вызываете деструкторы и делаете вызов в деструкторе.

попробуйте что-то вроде этого:

delete a3;
delete a2-primary;

вместо

delete a2-primary;
delete a3;

Я также думаю, что вы могли бы найти этот учебник по типизации полезным.

Надеюсь, я смогу вам помочь.

0 голосов
/ 07 декабря 2010

Краткий ответ. Это происходит, потому что неверный деструктор вызывает. Для длинного ответа проверьте это и это И проверьте Эффективный C ++ Скотта Мейера. Он охватывает такие вопросы.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...