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

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

分类: C++   720人阅读  评论(0)  收藏  举报
 

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.

...

"

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

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

在该博文后续的一些代码说明中,当调用vector的erase函数时,发现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 new。delete表达式和库函数operator delete之间的关系也与此类似。

 

 

二、 allocator类

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

 

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

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

2.       对象构造;

3.       对象析构。

 

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

成员函数

描述

allocator<T> a;

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

a.allocate(n)

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

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

a.deallocate(p, n)

将从p开始的n个T类型的对象所占用的内存交回给系统。在调用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)

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

uninitialized_fill(b, e, t)

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

uninitialized_fill_n(b, e, t, n)

最多用t的拷贝初始化由迭代器b和e指定范围的n个对象。b和e之间的范围至少是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中包含元素的数目 = first_free – elements,Vector的容量 = end – elements,Vector剩余可用空间 = 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("所保留的空间不应少于容器中原有元素的个数");

         }

 

         // 分配可以存储n个T类型元素的空间

         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)

 

         //构造9个Student对象

         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;

}

上面程序输出的结果为:

size:0              capacity:0

size:1              capacity:2

size:2              capacity:2

Bob           deleted.

Andrew     deleted.

size:3              capacity:4

size:4              capacity:4

Dudley      deleted.

Chris         deleted.

Bob           deleted.

Andrew     deleted.

size:5              capacity:8

size:6              capacity:8

size:7              capacity:8

size:8              capacity:8

Howard    deleted.

Greg                   deleted.

Fiona        deleted.

Ely             deleted.

Dudley      deleted.

Chris         deleted.

Bob           deleted.

Andrew     deleted.

size:9              capacity:16

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分配可以存储32个Student对象的空间,那么运行结果将是:

size:0              capacity:0

size:1              capacity:32

size:2              capacity:32

size:3              capacity:32

size:4              capacity:32

size:5              capacity:32

size:6              capacity:32

size:7              capacity:32

size:8              capacity:32

size:9              capacity:32

Iris             deleted.

Howard    deleted.

Greg                   deleted.

Fiona        deleted.

Ely             deleted.

Dudley      deleted.

Chris         deleted.

Bob           deleted.

Andrew     deleted.

 

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

 

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

 

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


评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值