c++ ctor and dtor don't work the same manner
我试图找出C++中类继承的技巧,并且我已经建立了一个示例项目:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| #include"stdafx.h"
#include <iostream>
using namespace std;
class A
{
public:
A()
{
cout <<"Class A initialized" << endl;
}
~A()
{
cout <<"Class A destructed" << endl;
}
};
class B : public A
{
public:
B()
{
cout <<"Class B initialized" << endl;
}
~B()
{
cout <<"Class B destructed" << endl;
}
};
int _tmain(int argc, _TCHAR* argv[])
{
cout <<"A* a = new A()" << endl;
A* a = new A();
cout <<"B* b = new B ()" << endl;
B* b = new B ();
cout <<"A* ab = new B()" << endl;
A* ab = new B();
cout <<"delete a" << endl;
delete a;
cout <<"delete b" << endl;
delete b;
cout <<"delete ab" << endl;
delete ab;
int i;
cin >> i;
return 0;
} |
我得到的输出是:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| A* a = new A()
Class A initialized
B* b = new B ()
Class A initialized
Class B initialized
A* ab = new B()
Class A initialized
Class B initialized
delete a
Class A destructed
delete b
Class B destructed
Class A destructed
delete ab
Class A destructed |
我可以理解类B作为派生类的行为——首先它构造基类,然后是派生类。当它调用析构函数时,它的工作方式与之相反。似乎合乎逻辑。
我不明白的是a b的行为(b的分配,我把它放在a指针上)。为什么构造函数的行为与纯b相同,但析构函数只在a上运行?
谢谢。
- 因为你没有做过A的析构函数virtual。
- 你是对的。是我的错。谢谢。
编译器调用与指针的静态类型相对应的类的成员函数。指针ab的类型是A *,因此编译器调用类a的析构函数。例如,如果将析构函数声明为虚的
1 2 3 4 5 6 7 8 9
| class A
{
public:
//...
virtual ~A()
{
cout <<"Class A destructed" << endl;
}
}; |
然后编译器将使用vitual函数指针表。在这种情况下,也就是在删除ab的情况下,表将包含引用派生类的析构函数的指针。
对于构造函数,则当使用operator new b()时,表达式中使用的静态类型为b。因此,B的constructor与a的构造函数一起调用,作为基类的构造函数。
- 这个答案实际上是错误的;析构函数遵循稍微不同的规则,事实上,他的程序具有未定义的行为。
- 重读:这个答案实际上是错误的(任何没有指出未定义行为的答案也是错误的)。
- @詹姆斯坎兹,你能详细说明我的回答有什么问题吗?
- 几乎所有的东西,除了析构函数应该是虚拟的。无论在理论上还是在实践中,它都无法描述发生了什么。
- @詹姆斯坎兹又读了一遍这个问题。问题是"为什么构造函数的行为与纯b相同,但析构函数只在a上运行?"所以我的回答是完全正确的。它解释了为什么称为b的构造函数,同时又称a的析构函数,是你不理解这个问题。我们这里不讨论未定义的行为。我们正在讨论为什么调用B的构造函数和A的析构函数。
- 关于为什么析构函数只在上运行的答案是因为他的代码包含未定义的行为。其他答案都不正确。
- @詹姆斯坎兹,这是完全错误的。调用类A的析构函数是一种已定义的行为。另外,它是C++标准定义的行为。你甚至不明白这里的未定义行为是什么。这里未定义的行为是,派生类的析构函数不被调用,也不被调用。
- 不,这是未定义的行为。所以我才说你的回答是错的。参见&167;5.3.5/3:"[…]如果要删除的对象的静态类型与其动态类型不同,则静态类型应为要删除的对象的动态类型的基类,静态类型应具有虚拟析构函数或行为未定义。"在这种情况下,程序崩溃或内存崩溃并不罕见。损坏,导致稍后崩溃。
- @詹姆斯坎兹,再一次,你不会低估这里所说的未定义的行为。调用基类析构函数是定义很好的行为。在这种情况下,未定义的行为只意味着派生类对象不会被正确地销毁和删除。
- 标准并不是这么说的。标准规定行为是不确定的,句号。你不明白什么是未定义的。(也不总是只调用基类析构函数;我也看到过这种情况下的系统崩溃和内存损坏。)
- @标准的詹姆斯坎兹清楚地回答了什么是析构函数。没有任何未定义的行为。未定义的行为发生在调用基类的析构函数之后。
- 哪个标准?我直接引用C++ 11,这里说这是未定义的行为。我引用的文本在标准的早期版本中是相同的。我引用的文字清楚而准确;我看不出你对它有什么不理解。
- @詹姆斯坎兹,你似乎不明白你读了什么。标准中明确定义了将调用什么构造函数这个问题的答案。没有任何未定义的行为。我重复的所有编译器都将调用基类的析构函数,因为此行为在标准中定义良好。所以你的答案与线程作者的问题无关。此外,您对标准的理解也有一些问题。
- 我放弃了。标准清楚地说"未定义的行为",我引用了这段话。如果你不理解未定义的行为,我就无能为力了。我还看到了使用sun cc、g++和vc++的崩溃和内存损坏,这显然与您对所有编译器所做的声明相矛盾,所以您的声明显然是错误的。
- @我说过的詹姆斯坎兹,你甚至不明白你读的是什么。未定义的行为是只为派生类的对象调用基类析构函数的结果。但标准明确规定了在这种情况下,将调用destractor。因此,将调用基类的析构函数这一事实在标准中得到了很好的定义。
- 关于"未定义的行为",你不明白什么?标准规定这是未定义的行为。该标准称,"未定义行为"是"本国际标准不要求的行为"。任何事情都会发生(在这种情况下,确实会发生)。在标准中,你从哪里找到其他的东西?
- @毫无疑问,标准的JamesKanze说在这种情况下,将调用基类的析构函数。这是这种调用行为不明确的结果。这是不同的事情。线程的作者问为什么要调用基类析构函数而不是派生类析构函数。所以除了你,一切都很清楚,因为你甚至不理解你读到的东西。
- 你到底不明白"未定义"是什么意思?你继续说标准是这样说的,但是你没有引用标准中的任何东西来支持这一点。我引用的这段话清楚地说了"未定义的行为",标准将未定义的行为定义为对发生的事情没有约束。
- @问题是为什么调用了基类的析构函数。它与未定义的行为没有任何共同之处。很清楚。如果你不理解这个问题,那么你可以根据需要反复阅读。
- 调用基类析构函数的原因是程序具有未定义的行为,因此可能发生任何事情。不能保证将调用基类析构函数。如果你不懂C++,我建议你选一门好的课程。
- @詹姆斯·坎兹,这是你的严重错误。没有什么未定义的,我正在重复所有编译器插入调用基类构造函数的代码。指针的类型定义了可以调用的一组函数。此规则应用于类的所有成员函数,析构函数不是异常的。未定义的行为发生在调用析构函数之后,因为指针指向的对象的状态未定义。
- 你可以随心所欲地重复,但你所说的完全是错误的。对于初学者来说,在这种情况下,至少vc++、g++和sun cc有时会崩溃或损坏内存,所以您关于所有编译器的声明是错误的。你还没有在标准中引用任何与我引用的段落相矛盾的代码。
- @JamesKanze您在拼写调用的结果,但问题是为什么调用了基类构造函数。还有一件事是不同的。未定义的行为没有什么共同之处。因为没有任何行为。调用基CLSS构造函数的代码在编译时生成。程序尚未运行。编译器必须定义它必须调用的函数,标准的clear说明在这种情况下什么函数将被clled。
- 在我引用的这段话中,我谈到了标准的内容。没有提到调用基类析构函数之类的。它简单地说:如果动态类型不同,并且静态类型的析构函数不是虚拟的,那么行为是未定义的。时期。就这些了。
构造函数和析构函数之间有一个根本的区别(或构造器和其他功能):何时构造对象时,必须在源中指定其确切类型代码。对于所有其他函数(包括析构函数),它是只要满足一定的条件,就可以只提到一个基地。其中一个条件是函数(或析构函数)是在基类中是虚拟的。
对于析构函数,有一个额外的约束,因为析构函数与delete有关,而delete又要求完整对象的地址,以便正确释放内存。因此,在给定A* pA;的情况下,像pA->f()这样的表达式将调用如果不是虚拟的,则为基类中的函数f,但函数如果派生类是虚拟的,则派生类中的f(),以及派生类超越它。另一方面,delete pA;将调用析构函数如果基中的析构函数是虚拟的,但却是如果pA指向派生类,并且基中的析构函数不是虚拟的。不存在公正的问题9调用基类的析构函数;尽管这可能是实际行为在简单情况下,行为在所有情况下都是未定义的。
出于这个原因,通常建议设计为用作基类,析构函数应为虚拟或受保护。不知道,这取决于班级:如果有人错误地使用了std::exception<>到了写作的地步,比如:
1 2 3
| std::exception<...>* pIter = new std::vector<...>::iterator;
// ...
delete pIter; |
没有希望,也不值得费心定义一个析构函数对于std::iterator来说,只是为了保护它(在pre-c++11中,派生迭代器不可能是pod)。