析构函数查询

6
我有一个程序,其中我通过引用将向函数myFunc传递一个向量,并在函数内部向该向量添加了一些元素。
目前我没有释放使用new创建的对象,请忽略由此导致的内存泄漏。
在完成myFunc()执行后,我会打印变量ctor和dtor以知道构造函数和析构函数被调用的次数。
输出为:
Before Exiting 5 7

我正在创建5个对象,因此ctor5。但是为什么dtor7?从哪里来的额外两个计数?我有什么遗漏吗?

#include
#include
using namespace std;

static int ctor = 0;
static int dtor = 0;

class MyClass
{
public:
    MyClass(int n)
    {
        i = n;
        ctor++;
        // cout << "Myclass ctor " << ctor << endl; 
    } 

    ~MyClass()
    {
        dtor++; 
        // cout << "Myclass dtor" << dtor << endl;
    }

private: 
    int i;
}; 

void myFunc(vector<MyClass> &m);

void myFunc(vector<MyClass> &m)
{
    MyClass *mc;

    for(int i = 0; i < 5; i++)
    {
        mc = new MyClass(i);
        m.push_back(*mc);
    }
}

int main()
{

    vector<MyClass> m;
    vector<MyClass>::iterator it;

    myFunc(m);

    cout << "Before Exiting " << ctor << " " << dtor << endl;
}

3
你是否尝试在构造函数和析构函数中设置断点,以查看发生了什么? - Demian Brecht
5个回答

8
向量会围绕物体复制,但只有你的 `int` 构造函数会增加 `ctor`。这并没有考虑到复制构造的对象,因为你没有提供它,所以编译器为你提供了它。
添加:
MyClass(const MyClass& rhs) i(rhs.i) { ++ctor; }

将其添加到您的类中,以查看是否平衡计数。


2
向量以较小的大小开始。当您将元素推入其中时,它们使用复制构造函数对其进行复制,因此您不会看到正常构造函数被调用。当向量大小超过其限制时,它将通过其当前大小的倍数(例如,将其加倍)来增加其限制。
向量保证始终在连续的内存中保留对象,因此如果添加新对象超出了向量的容量(即 size() + 1 > capacity()),则向量将在某个地方分配新内存并将所有元素复制到其中。这将再次使用复制构造函数。因此,在向量预调整大小之前,其中的元素将在使用其复制构造函数将其复制到新分配的空间后调用其析构函数。
所以,析构函数的调用次数比正常构造函数的调用次数多 :)

我现在明白了。虽然我知道复制构造函数,但我没有想到 vector 内部执行的复制操作。谢谢。 - irappa

1
向量有时候会调用另一个构造函数,拷贝构造函数,这个函数是编译器隐式为您的类生成的。这就是为什么有些ctor++调用会丢失的原因:并不是所有的对象都使用您定义的构造函数构造,有些是使用其他构造函数构造的。
为了确保类型(类)在向量中的正确行为,必须为其实现拷贝构造函数:
MyClass(const MyClass& rhs) { i = rhs.i; ++ctor; } // copy constructor

......因为编译器生成的代码什么也没有做。


1

正如其他人所指出的,你的结果之所以如此,是由于复制构造函数和调整vector大小的原因。一个vector有大小和容量两个属性。当vector需要调整大小以容纳新元素时,通常会将容量加倍,这样调整大小的次数就不会太多。

在每个push_back之间添加一些跟踪代码以打印出向量容量,可以更清楚地了解这种行为。

m.capacity(): 0
m.capacity(): 1
m.capacity(): 2
m.capacity(): 4
m.capacity(): 4
m.capacity(): 8
Before Exiting 5 7

这里的实际情况是,析构函数只有在向量调整大小时才会被调用(请看下面为什么)。第一次调整大小时,它没有元素,因此析构函数从未被调用。第二次,容量为1,因此析构函数调用一次。第三次调用两次,第四次调用四次。总共调用七次,正如计数器所示。

myFunc 中动态分配的元素从未被释放,因此析构函数从未运行,而最终的输出("Before Exiting...")是在分配 vector 的作用域内离开之前完成的,因此最后一个“vector reincarnation”的析构函数直到该打印出来之后才被调用。 因此,只有在调整向量大小时才会调用MyClass 的析构函数。


又一个琐碎的问题。在添加了几个printf语句后,我观察到了以下情况。我在创建对象时打印了new运算符返回的对象地址。稍后在调用myFunc()之后,我遍历了向量,并再次打印了每个对象的地址。我发现这些地址现在是不同的。这可能是因为在向量被重新调整大小时,对象被销毁和重建的缘故吗?由于push_back方法将对象引用作为参数,那么在该方法返回后,调用者中的对象是否正确地更改(在内存位置方面)? - irappa
@irappa 新地址有两个原因。首先,vector 持有实际对象,而不是指向 push_back 传递的对象的指针。当调用 push_back(foo) 时,foo 中的内容被复制到 vector 内部另一个实例(foo 所属类的实例)中。此外,当 vector 被调整大小时,支持数组往往(总是?)也会移动到新位置,从而给 vector 中包含的元素新地址。 - Mikael Auno

0

C++标准的§12.8.8规定:如果类定义没有显式声明复制构造函数,没有用户声明的移动构造函数和移动赋值运算符,则会隐式声明复制构造函数(8.4.2)。如果该类具有用户声明的复制赋值运算符或用户声明的析构函数,则此类隐式声明已被弃用。

基本上,由于您的结构违反了五个规则,编译器为您创建了一个复制构造函数和赋值运算符。那个其他的构造函数不会增加ctor,但使用您定义的析构函数。然后向量使用该替代构造函数作为速度提升。

如果您在类声明中添加protected: MyClass(const MyClass& b);,则可以解决此问题。


实际上,关于用户析构函数的最后一点似乎暗示编译器正在违反规则。嗯。 不过,那行代码应该可以解决问题。 - Mooing Duck

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接