2016-08-15 3 views
1

Я получил простую программу, как это:Почему результат отличается от того, что я ожидаю?

#include "stdafx.h" 
#include <iostream> 

using namespace std; 

int main() 
{ 
    class B { 
    protected: 
     int  data = 0; 
    public: 
     B() { cout << "B() ctor\n";} 
     virtual ~B() { cout << "~B()\n"; } 
     virtual void method() { cout << "data in B: " << data << "\n"; } 
    }; 

    class A : public B 
    { 
     int dataA = 2; 
    public: 
     A() { cout << "A() ctor\n"; } 
     ~A() { cout << "~A()\n"; } 
     void method() { cout << "data in A: " << dataA << "\n"; } 
    }; 

    { 
     B* fptrList[]{ &B{}, &A{}}; 
     for (auto& itr : fptrList) 
      itr->method(); 
    } 

    cin.get(); 
    return 0; 
} 

Вот результат я ожидаю:

B() ctor 
B() ctor 
A() ctor 
data in B: 0 
data in A: 2 
~A() 
~B() 
~B() 

Вот реальный результат, когда я запустил эту программу:

B() ctor 
~B() 
B() ctor 
A() ctor 
~A() 
~B() 
data in B: 0 
data in B: 0 

Мои вопросы:

  • Почему результат отличается от ожидаемого?
  • Как вызывать метод() после вызова ~ A() и ~ B()?
  • Почему метод() класса B вызывается дважды?
+1

Это просто UB, попробуйте объяснить, что поведение бессмысленно. – songyuanyao

+0

Я так не думаю. Вы видите, что вызовы метода() получили успех и распечатали правильные данные. Если эти объекты фактически уничтожены, вызов метода(), который обращается к члену «данные», должен вызывать ошибку времени выполнения. –

+1

Это UB, ничего не гарантируется. Это может привести к ошибке выполнения, может работать хорошо (к сожалению). – songyuanyao

ответ

5

Эта программа не может быть объяснена, поскольку она демонстрирует неопределенное поведение.

Перевод: это багги. Он принимает адрес временных объектов, а затем пытается разыменовать их после временного уничтожения.

Хороший компилятор C++ будет даже сказать, что программа не работает, и отказаться от участия в этой катастрофе:

t.C: In function ‘int main()’: 
t.C:26:27: error: taking address of temporary [-fpermissive] 
     B* fptrList[]{ &B{}, &A{}}; 
         ^
t.C:26:33: error: taking address of temporary [-fpermissive] 
     B* fptrList[]{ &B{}, &A{}}; 
           ^

Любой выход из этой программы не имеет смысла мусора.

5

Вот что происходит:

  • Вы инициализировать fptrList адресам временных переменных A и B
  • Временные переменные разрушаются сразу же после того, как их адреса будут приняты, так что ваш код будет иметь неопределенное поведение.
  • Правильный способ сделать то, что вы пытаетесь сделать, это использовать оператор new с интеллектуальными указателями или сделать экземпляры вне инициализатора.

Вот один из возможных решений:

{ 
    B b; 
    A a; 
    B* fptrList[]{ &b, &a }; 
    for (auto& itr : fptrList) 
     itr->method(); 
} 
+0

Быстрый вопрос: почему временные файлы уничтожены сразу после их адреса, а не после завершения заявления? – Rakete1111

+0

@ Rakete1111 Поскольку временные данные не нужны после их адреса, компилятор может решить, когда нужно вызвать деструктор. Похоже, ваш компилятор решил немедленно их уничтожить. – dasblinkenlight

+0

Я знаю, что ваше «исправление» - это правильный способ. Я просто попробовал и увидел, как это странно. –

0

Ok, это неопределенное поведение, но вопрос до сих пор интересно, почему это неопределенного поведения.

  • Почему конструкторы/деструкторы вызываются в таком порядке? Как уже установлено, вы создаете временные объекты, которые создаются/уничтожаются один за другим.

  • Почему я могу вызвать методы уже несуществующих объектов? Ваш временный объект живет в стеке, и, таким образом, память будет освобождена только в конце функции main, так что вы все еще можете получить доступ к этой памяти, и она не сбивается вызовами других функций (например, печать на терминал).Если вы создадите объект с new, чем удалите его и попробуйте его использовать - шансы будут выше, что система уже восстановила эту память, и вы получите ошибку сегментации.

  • Почему я вижу 2 раза метод B, называемый? Это смешно. Чтобы вызвать виртуальную функцию объекта, компилятор делегирует решение, метод которого должен быть вызван в виртуальную таблицу (его адрес занимает первый 8-байтовый номер такого объекта (по крайней мере, для моего компилятора и 64-битного)). Не то, что хорошо известно о виртуальных методах, заключается в том, что во время вызова деструктора все виртуальные методы называются так, как если бы они не были виртуальными. Но как это связано с вашим кодом? Вы видите его побочный эффект: не виртуальное поведение обеспечивается деструктором, перезаписывая виртуальную таблицу текущего объекта виртуальной таблицей текущего класса. Поэтому после вызова деструктора B память содержит виртуальную таблицу класса B, которую вы можете видеть, потому что B::method вызывается дважды.

Давайте отслеживать значение виртуальной таблицы его в вашей программе:

  1. зову A{}: На первом суперкласс B -constructor это называется - (еще не полностью завершен) объект имеет виртуальная таблица класса B (этот адрес перемещается в первый 8-байтовый объект, занятый объектом), чем A -constructor вызывается - теперь объект имеет виртуальную таблицу класса A.
  2. звонок ~A(): после его выполнения деструктор A автоматически вызывает деструктор B. Первое, что деструктор B делает, это перезаписать виртуальную таблицу объекта виртуальной таблицей класса B.
  3. Итак, после уничтожения память все еще существует и интерпретируется как объект, будет иметь виртуальную таблицу класс B.
  4. itr->method(); находит виртуальную таблицу класса B по адресу itr указывает на B::method().
Смежные вопросы