vector用erase删除元素时,为什么被删元素的析构函数会被调用更多次?

博文 http://patmusing.blog.163.com/blog/static/13583496020101831514657/  中提到:

vector::erase的原型如下:

iterator erase(iterator position);

iterator erase(iterator first, iterator last);

对应的相关说明如下:

"

...

This effectively reduces the vector size by the number of elements removed, calling each element's destructor before.

...

"

上面的说明中,有下划线的那句的含义是:

这实际上就是减少的容器的大小,减少的数目就是被删除元素的数目,在删除元素之前,将会调用被删元素的析构函数

在该博文后续的一些代码说明中,当调用vectorerase函数时,发现vector中的元素对象的析构函数被调用了多次。按照通常的理解,析构函数被调用一次就可以销毁对象,那为什么vector容器在用erase删除其元素对象时,其被删除的元素对象的析构函数要被多次调用呢?

 

一、 到底发生了什么事情?

如在上面提及的博文中一样,假定定义了一个类如下:

#include <iostream>

#include <algorithm>

#include <string>

#include <vector>

using namespace std;

 

class Student

{

public:

         Student(const string name = "Andrew"const int age = 7) : name(name), age(age)

         {}

 

         ~Student()

         {

                   cout << name << "\tdeleted." << endl;

         }

 

         const string get_name() const

         {

                   return name;

         }

 

         const int get_age() const

         {

                   return age;

         }

 

private:

         string name;

         int age;

};

 

那么当在程序中,类似下面的new表达式(new expression)delete表达式(delete expression),即

Student *stu = new Student("Bob", 7);

delete stu;

背后到底发生了什么事情?

 

在上面的new表达式,实际上做了3步工作:

第一步: 调用库函数operator new,分配可以容纳一个Student类型对象的原始的、无类型的内存;

第二步: 根据给定的实参,调用Student类的构造函数,以构造一个对象;

第三步: 返回在第二步中被构造对象的地址。

 

对应地,上面的delete表达式,也实际上做了2步工作:

第一步: 调用Student类的析构函数,以销毁对象。这一步完成后,对象已经被销毁,但该对象占用的内存此时仍然没有

返还给系统;

第二步: 调用库函数operator delete,将已经被删除对象所占用的内存交回给系统。

 

需要特别注意的是,new表达式和库函数operator new不是一回事情,事实上,new表达式总是会调用更底层的库函数operator newdelete表达式和库函数operator delete之间的关系也与此类似。

 

 

二、 allocator

在一中说明了一些最基本的概念。在这里,我们将简要讨论一下allocator类,因为在STL中,所有的容器关于内存的动态分配,都是通过allocator来完成的。

 

allocator是一个模板类,它将内存分配和对象构造,对象析构和内存回收分别分开执行。主要地,它包含了3个大的功能:

1.       针对特定类型,进行内存分配;

2.       对象构造;

3.       对象析构。

 

具体一点就是提供了下表所列的一些功能:

成员函数

描述

allocator<T> a;

定义了一个allocator对象aa可以用来分配内存或者构造T类型的对象

a.allocate(n)

nT类型的对象,分配原始的、未构建的(unconstructed)内存。

注意:此处的a就是上面所定义的a

a.deallocate(p, n)

将从p开始的nT类型的对象所占用的内存交回给系统。在调用deallocate之前,程序员必须先将有关的对象用destroy予以销毁。

注意:p的类型是T*,其声明如: T* p;

a.construct(p, t)

p所指向的内存中,构造一个新的T类型对象。在这个过程中将调用T的拷贝构造函数,将T类型的对象t的一个拷贝作为无名的,临时的T类型对象复制到p所指定的内存中。

注意:t是一个T类型的对象,其声明如:T t;

a.destroy(p)

调用p所指向对象的析构函数,以销毁该对象。注意,此函数调用后,对象已经被销毁,但其所占用的内存并未交还给系统

uninitialized_copy(b, e, b2)

将有迭代器be所规定范围内的元素复制到由迭代器b2开始的原始的、未构建的内存中。本函数是目标内存中构造函数,而不是将元素赋值到目标内存。因为,将一个对象赋值到一个未构建的内存中这种行为时未定义的。

uninitialized_fill(b, e, t)

t的拷贝初始化由迭代器be指定范围的对象。该范围是原始的、未构建的内存,其中的对象构造是通过其拷贝构造函数来完成的。

uninitialized_fill_n(b, e, t, n)

最多用t的拷贝初始化由迭代器be指定范围的n个对象。be之间的范围至少是n。该范围是原始的、未构建的内存,其中的对象构造是通过其拷贝构造函数来完成的。

由上表可见,拷贝构造函数在allocator中的使用非常频繁。

 

 

三、 定位new表达式(placement new expression)

定位new表达式的写法:

new (place_address) type;

new (place_address) type(initializer_list);

 

下面是一个例子:

int main(void)

{

         allocator<Student> alloc;

         Student *stu = alloc.allocate(3);

 

         Student stu01;

         Student stu02("Bob", 7);

 

         new (stu) Student(stu01);                             // 定位new表达式:使用缺省拷贝构造函数

         new (stu + 1) Student(stu02);                      // 定位new表达式:使用缺省拷贝构造函数

         new (stu + 2) Student("Chris", 7);               // 定位new表达式:使用普通构造函数

 

         cout << stu->get_name() << endl;

         cout << (stu + 1)->get_name() << endl;

         cout << (stu + 2)->get_name() << endl;

 

         alloc.destroy(stu + 2);

         // 绝对不能将上面的语句写成: delete (stu + 2);

         // 因为,用allocator分配内存的对象,必须由对应的destroy来销毁

 

         return 0;

}

上面程序的输出结果:

Andrew

Bob

Chris

Chris         deleted.

Bob           deleted.

Andrew     deleted.

可见对象的构建和析构的顺序刚好是反过来的。

 

前面我们曾经提到construct只能用拷贝构造函数构建对象,从上面代码中,我们可以看到定位new表达式,不仅可以是用拷贝构造函数来构造对象,也可以使用普通的构造函数,这表明:定位new表达式要比construct更加灵活。而且,有些时候,比如当拷贝构造函数是私有的时候,就只能使用定位new表达式了。

 

从效率的角度来看,根据Stanley B. Lippman的观点,定位new表达式和construct没有太大的差别。

 

还有一个必须注意到的现象: 使用普通构造函数构造的对象,在其作用域结束时,并不会自动析构,而必须由destroy来完成这项工作。其他通过拷贝构造函数构造的对象,在其作用域结束时,均会被自动析构。

 

如果使用construct函数,下面是一个例子(可将其拷贝到上面的main函数中)

         Student stu04("Dudley", 8);

         alloc.construct(stu + 3, stu04);

         cout << (stu + 3)->get_name() << endl;

由于construct只能使用拷贝构造函数,因此,stu04在其作用域结束时,也会被自动析构,程序运行结果也应证了这一点。

 

结论:

如果一个对象是通过拷贝构造函数构造的对象,那么在其作用域结束时,该对象会自动析构;而通过非拷贝构造函数构建的对象,则不会自动析构,必须通过destroy函数对其析构。

 

 

四、 vector的模拟实现

为了说明本文开始处提出的问题,我们自然需要对vector的实现机制有所了解。而要了解vector的实现机制,就必须了解allocator及其相关的内容,正如前面所提及的,在STL中,所有的容器都使用allocator来对内存进行管理,这就 是为什么我讲了一、二和三的原因。

 

在这部分内容中,我们将模拟实现一个vector。在后续的内容中,我们将使用这个vector的模拟实现,看看会不会出现本文开始处所提出的现象。

 

好了,既然我们的准备工作已经完成,那么现在开始模拟实现一个vector,不妨将其命名为Vector

 

下图就是Vector内存分配策略。其中,elements指向数组中的第一个元素,first_free指向最后有效元素后面紧接着的位置,end指向数组末尾后面紧接着的位置。

vector用erase删除元素时,为什么被删元素的析构函数会被调用更多次? - 玄机逸士 - 玄机逸士博客

根据这样的假定,很容易可以推知:

Vector中包含元素的数目 = first_free – elementsVector的容量 = end – elementsVector剩余可用空间 = end – first_free

 

下面是Vector的实现代码:

注意:为方便起见,以下各个类以及测试代码,都处于同一个cpp文件中,该cpp文件包含以下头文件:

#include <iostream>

#include <string>

#include <memory>

#include <cstddef>

#include <stdexcept>

using namespace std;

 

// vector的模拟实现 – Vector

template<typename T>

class Vector

{

private:

         T *elements;              // 指向第一个元素的指针

         T *first_free;               // 指向最后有效元素后面紧接着位置的指针

         T *end;                        // 指向数组末尾后面紧接着位置的指针

 

private:     

         // 用于获取为构造内存(unconstructed memory)的对象。在此,它必须是static的,因为:

         // 创建对象之前,必须要为其提供内存。如果非static,那么alloc对象必须是在Vector

         // 对象创建之后才可以使用,而alloc的初衷却是为即将要创建的对象分配内存。因此非

         // static是断然不行的。

         // static成员是类级别的,而非类之对象级别的。也就是说,static成员早于对象存在,

         // 因此,下面的alloc可以为即将要创建的对象分配内存。

         static std::allocator<T> alloc;

 

         // 当元素数量超过容量时,该函数用来分配更多的内存,并复制已有元素到新空间。

         void reallocate();

 

public:

         Vector() : elements(0), first_free(0), end(0)        // 全部初始化为空指针

         {}

 

         void push_back(const T&);                          // 增加一个元素

         void reserve(const size_t);                           // 保留内存大小

         void resize(const size_t);                              // 调整Vector大小

         T& operator[](const size_t);                          // 下标操作符

         size_t size();                                                    // 获取Vector中元素的个数

         size_t capacity();                                             // 获取Vector的容量

         T& erase(const size_t);                                 // 删除指定元素

};

 

// 初始化静态变量。注意,即使是私有成员,静态变量也可以用如下方式初始化

template<typename T>

allocator<T> Vector<T>::alloc;

 

template<typename T>

void Vector<T>::reallocate()

{

         // 计算现有元素数量

         ptrdiff_t size = first_free - elements;

 

         // 分配现有元素大小两倍的空间

         ptrdiff_t new_capacity = 2 * max(size, 1);  //(size == 0) ? 2 : 2 * size;

         T *new_elements = alloc.allocate(new_capacity);

 

         // 在新空间中构造现有元素的副本

         uninitialized_copy(elements, first_free, new_elements);

 

         // 逆序销毁原有元素

         for(T *p = first_free; p != elements; )

         {

                   alloc.destroy(--p);

         }

 

         // 释放原有元素所占内存

         if(elements)

         {

                   alloc.deallocate(elements, end - elements);

         }

 

         // 更新个重要的数据成员

         elements = new_elements;

         first_free = elements + size;

         end = elements + new_capacity;

}

 

template<typename T>

void Vector<T>::push_back(const T &t)

{

         if(first_free == end)                      // 如果没有剩余的空间

         {

                   reallocate();                         // 分配更多空间,并复制已有元素

         }

         alloc.construct(first_free, t);       // t复制到first_free指定的位置

         first_free++;                                  // first_free

}

 

template<typename T>

void Vector<T>::reserve(const size_t n)

{

         // 计算当前Vector的大小

         size_t size = first_free - elements;

         // 如果新分配空间小于当前Vector的大小

         if(n < size)

         {

                   throw custom_exception("所保留的空间不应少于容器中原有元素的个数");

         }

 

         // 分配可以存储nT类型元素的空间

         T *newelements = alloc.allocate(n);

         // 在新分配的空间中,构造现有元素的副本

         uninitialized_copy(elements, first_free, newelements);

 

         // 逆序销毁原有元素,但此时并未将原有元素占用的空间交还给系统

         for(T *p = first_free; p != elements;)

         {

                   alloc.destroy(--p);

         }

 

         // 释放原有元素所占用的内存

         if(elements)

         {

                   alloc.deallocate(elements, end - elements);

         }

 

         // 更新个重要的数据成员

         elements = newelements;

         first_free = elements + size;

         end = first_free + n;

}

 

template<typename T>

void Vector<T>::resize(const size_t n)                          // 调整Vector大小

{

         // 计算当前Vector大小以及容量

         size_t size = first_free - elements;

         size_t capacity = end - elements;

 

         if(n > capacity)  // 如果新空间的大小大于原来的容量

         {

                   reallocate();

                   T temp;

                   uninitialized_fill(elements + size, elements + n, temp);

                   end = elements + n;

         }

         else if(n > size)          // 如果新空间的大小大于原来Vector的大小

         {

                   uninitialized_fill(elements + size, elements + n, temp);

         }

         else // 如果新空间的大小小于或等于原来Vector的大小

         {

                   // 逆序销毁多余元素

                   for(T *p = first_free; p != elements + n;)

                   {

                            alloc.destroy(--p);

                   }

         }

 

         // 更新相关数据成员

         // elements没有改变,无需更新

         first_free = elements + n;

         // end在上面n > capacity时,已经被更改

}

template<typename T>

T& Vector<T>::operator[](const size_t index)               // 下标操作符

{

         size_t size = first_free - elements;

         // 如果接受的参数不在有效的范围内,则抛出异常

         if(index < 0 || index > size)

         {

                   throw custom_exception("给定的索引参数错误");

         }

         return elements[index];

}

 

template<typename T>

size_t Vector<T>::size()                                                   // 获取Vector中元素的个数

{

         size_t temp = first_free - elements;

         return temp;

}

 

template<typename T>

size_t Vector<T>::capacity()                                            // 获取Vector的容量

{

         size_t temp = end - elements;

         return temp;

}

 

Vector中用到的自定义异常类以及Student类分别定义如下:

// 自定义异常类,从std::runtime_error继承而来

// 注意,可以在此基础上,增加更复杂的内容。本例为了方便,使用了最简单的形式。

class custom_exception : public runtime_error

{

public:

         // 定义一个explicit的构造函数,并将参数传递给基类

         explicit custom_exception(const string& s) : runtime_error(s)

         {

         }

 

         // An empty specification list says that the function does not throw any exception

         // 析构函数不抛出任何异常

         virtual ~custom_exception() throw()

         {

         }

};

 

// 这个类将作为Vector中元素的类型

class Student

{

public:

         Student(const string name = "Andrew"const int age = 7) : name(name), age(age)

         {}

 

         ~Student()

         {

                   cout << name << "\tdeleted." << endl;

         }

 

         const string get_name() const

         {

                   return name;

         }

 

         const int get_age() const

         {

                   return age;

         }

 

private:

         string name;

         int age;

};

 

下面是测试代码:

// 测试代码

int main(void)

{

         Vector<Student> svec;

         cout << "size" << svec.size() << "\t\tcapacity" << svec.capacity() << endl;

         //svec.reserve(32);                                                            (1)

 

         //构造9Student对象

         Student stu01;

         Student stu02("Bob", 6);

         Student stu03("Chris", 5);

         Student stu04("Dudley", 8);

         Student stu05("Ely", 7);

         Student stu06("Fiona", 3);

         Student stu07("Greg", 2);

         Student stu08("Howard", 9);

         Student stu09("Iris", 6);

 

         // svec增加一个元素。以下对应各句,与此相同。

         svec.push_back(stu01);

         // 输出svec的大小和容量。以下对应各句,与此相同。

         cout << "size" << svec.size() << "\t\tcapacity" << svec.capacity() << endl;

        

         svec.push_back(stu02);

         cout << "size" << svec.size() << "\t\tcapacity" << svec.capacity() << endl;

        

         svec.push_back(stu03);

         cout << "size" << svec.size() << "\t\tcapacity" << svec.capacity() << endl;

 

         svec.push_back(stu04);

         cout << "size" << svec.size() << "\t\tcapacity" << svec.capacity() << endl;

 

         svec.push_back(stu05);

         cout << "size" << svec.size() << "\t\tcapacity" << svec.capacity() << endl;

 

         svec.push_back(stu06);

         cout << "size" << svec.size() << "\t\tcapacity" << svec.capacity() << endl;

 

         svec.push_back(stu07);

         cout << "size" << svec.size() << "\t\tcapacity" << svec.capacity() << endl;

 

         svec.push_back(stu08);

         cout << "size" << svec.size() << "\t\tcapacity" << svec.capacity() << endl;

 

         svec.push_back(stu09);

         cout << "size" << svec.size() << "\t\tcapacity" << svec.capacity() << endl;

 

         return 0;

}

上面程序输出的结果为:

size0              capacity0

size1              capacity2

size2              capacity2

Bob           deleted.

Andrew     deleted.

size3              capacity4

size4              capacity4

Dudley      deleted.

Chris         deleted.

Bob           deleted.

Andrew     deleted.

size5              capacity8

size6              capacity8

size7              capacity8

size8              capacity8

Howard    deleted.

Greg                   deleted.

Fiona        deleted.

Ely             deleted.

Dudley      deleted.

Chris         deleted.

Bob           deleted.

Andrew     deleted.

size9              capacity16

Iris             deleted.

Howard    deleted.

Greg                   deleted.

Fiona        deleted.

Ely             deleted.

Dudley      deleted.

Chris         deleted.

Bob           deleted.

Andrew     deleted.

 

从上面代码中我们看到,析构函数在同一个对象上被调用了很多次,原因其实很简单:

当元素超过Vector的容量后,Vector会自动增加容量(为原来大小的2),然后Vector会调用元素的拷贝构造函数(本例中为缺省的拷贝构造函数,因为Student类中指针成员变量,所以无需自定义一个重载拷贝构造函数),将已有的元素复制到新分配的内存,然后调用destroy销毁原来的元素对象,destroy会显式调用对象的析构函数。在本例中,Vector容量改变了4次,每次容量的改变都会通过destroy显式调用元素对象的析构函数以销毁对象,这就是为什么析构函数被多次调用的真正原因。

 

在上面的测试代码中,如果我们把(1)的注释去掉,即一开始就为Vector对象svec分配可以存储32Student对象的空间,那么运行结果将是:

size0              capacity0

size1              capacity32

size2              capacity32

size3              capacity32

size4              capacity32

size5              capacity32

size6              capacity32

size7              capacity32

size8              capacity32

size9              capacity32

Iris             deleted.

Howard    deleted.

Greg                   deleted.

Fiona        deleted.

Ely             deleted.

Dudley      deleted.

Chris         deleted.

Bob           deleted.

Andrew     deleted.

 

我们可以看到前面几乎没有析构函数被调用,最后析构函数被调用了9次,是因为测试代码中的9Student对象在其作用域结束时,自动被销毁的结果。关于这点,可以参考:http://patmusing.blog.163.com/blog/static/13583496020101824142699/

 

Vector中,并没有实现erase,这是因为erase要用到Iterator,限于篇幅就不在此列出相关代码了,因为其原理和上面的代码是一样的,而上面的代码已经足以解释博文http://patmusing.blog.163.com/blog/static/13583496020101831514657/ 中提到的关于析构函数被多次调用的问题。

 

最后再次强调一下:显式调用析构函数,可以销毁对象,但不会释放对象所占的内存。

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值