您的位置:首页 > 其它

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

2014-11-05 13:54 357 查看


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

分类: C++2011-08-18
14:55 720人阅读 评论(0) 收藏 举报

vectoriteratorexceptiondeletestringdestructor

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/ 中提到的关于析构函数被多次调用的问题。

最后再次强调一下:显式调用析构函数,可以销毁对象,但不会释放对象所占的内存。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐