如何C++的vector中删除指针
一、简介
正如《4招轻松从C++序列容器中移除元素,你学会了吗?》文章中所看到的,为了基于谓词删除vector
中的元素,C++使用了erase-remove
联合语句:
vector<int> vec{2, 3, 5, 2};
vec.erase(std::remove_if(vec.begin(), vec.end(), [](int i){ return i % 2 == 0;}), vec.end());
可以将其封装在一个更具表现力的函数调用中:
vector<int> vec{2, 3, 5, 2};
erase_if(vec, [](int i){ return i % 2 == 0; });
在这两个例子中,调用算法后得到的vec
包含{3,5}
。
这可以很好地用于值的vector
,例如vector<int>
。但是对于指针的vector
,这就不那么简单了,因为要考虑到内存管理问题。
二、从vector中移除智能指针 unique_ptr
C++ 11引入了std::unique_ptr
和其他智能指针,它们包装了一个普通指针,并通过在析函数中对指针调用delete
来处理内存管理。
这允许更容易地操作指针,特别是允许在std::unique_ptr
向量上调用std::remove
和std::remove_if
而不会出现问题。
auto vec = std::vector<std::unique_ptr<int>>{};
vec.push_back(std::make_unique<int>(2));
vec.push_back(std::make_unique<int>(3));
vec.push_back(std::make_unique<int>(5));
vec.push_back(std::make_unique<int>(2));
也可以使用std::initializer_list
来初始化vector
的unique_ptr
;但本文主要是说明如何从C++的vector中删除指针,这里就没有这么做。
vec.erase(std::remove_if(vec.begin(), vec.end(), [](auto const& pi){ return *pi % 2 == 0; }), vec.end());
或者使用自己封装的erase-remove
语句函数:
erase_if(vec, [](auto const& pi){ return *pi % 2 == 0; });
这段代码有效地删除了指向偶数的vector
的第一个和最后一个元素。
需要注意的是,由于std::unique_ptr
不能复制,只能移动,因此这段代码能够编译通过,这表明std::remove_if
不会复制集合中的元素,而是将它们移动到其他位置。我们知道,将一个std::unique_ptr<T> u1
对象移动到另一个std::unique_ptr<T> u2
对象中,会将u1
指向的底层原始指针的所有权转移到u2
中,使u1
变为空指针。因此,算法将元素放置在集合开头(在上面的例子中是指向3和5的unique_ptr
对象)的位置,可以保证这些元素是其底层指针的唯一所有者。所有这些内存处理都是通过unique_ptr
对象实现的。那么,如果使用一个指向所有者的vector
又会怎样呢?
三、从拥有原始指针的vector中移除
在现代C++中,使用拥有裸指针的vector
是不推荐的(即使在现代C++中,不使用vector
而直接使用裸指针也是不推荐的)。自C++11以来,std::unique_ptr
和其他智能指针提供了更安全、更直观的替代方案。但是,尽管现代C++不断创新,但并不是世界上的所有代码库都能以相同的速度跟进。这使得有可能遇到拥有裸指针的vector
。这可能出现在C++03的代码库中,也可能出现在使用现代编译器但其遗留代码中仍包含旧模式的代码库中。
另外一种会让人感到担忧的情况是编写库代码。如果代码接受一个类型为std::vector<T>
的参数,而对T
没有任何假设,那么可能会被从旧代码中调用,该代码传递的是一个包含拥有指针的vector
的参数。
本文其余部分假定有时需要处理包含拥有指针的vector
的情况,并且需要从中删除元素。在这种情况下,使用std::remove
和std::remove_if
是非常不明智的。
3.1、原始指针上的std::remove问题
为了说明这个问题,这里创建一个拥有原始指针的vector
:
auto vec = std::vector<int*>{ new int(2), new int(3), new int(5), new int(2) };
如果调用通常的erase-remove
模式:
vec.erase(std::remove_if(vec.begin(), vec.end(), [](int* pi){ return *pi % 2 == 0; }), vec.end());
将会内存泄漏:vector
不再包含指向2
的指针,但是没有人对它们调用delete
。
因此,我们可能会试图将std::remove_if
从erase
调用中分离出来,以便在调用时可以删除vector
末端的指针:
auto firstToErase = std::remove_if(vec.begin(), vec.end(), [](int* pi){ return *pi % 2 == 0; });
for (auto pointer = firstToErase; pointer != vec.end(); ++pointer)
delete *pointer;
vec.erase(firstToErase, vec.end());
但是这也行不通,因为这会创建悬空指针。要理解为什么,必须考虑 std::remove
和 std::remove_if
的一个要求(或者说,缺失):它们在vector
末尾留下的元素是不确定的。它可能是在调用算法之前就在那里的元素,也可能是满足谓词的元素,或者其他任何元素。
在特定的STL实现中,在调用std::remove_if
算法后,容器末尾剩下的元素竟然是在调用算法之前存在的那些。比如,在调用std::remove
之前,vector
中有指向2、3、5、2的指针,调用之后,指针变成了指向3、5、5、2
。
例如,在调用std::remove
之前打印向量内的值可能输出如下:
0x55c8d7980c20
0x55c8d7980c40
0x55c8d7980c60
0x55c8d7980c80
调用std::remove
后输出如下:
0x55c8d7980c40
0x55c8d7980c60
0x55c8d7980c60
0x55c8d7980c80
调用erase
删除了第三个位置的指针,使得第二个位置的指针(与之相等)成为一个危险的悬空指针!
3.2、替换方案
可以使用std::stable_partition
代替std::remove_if
,使用一个反转的谓词。实际上,std::stable_partition
根据谓词对集合进行分区。即将满足谓词的元素放在开头,而不满足谓词的元素放在末尾。不再有相等的指针。
这里的分区是将不需要移除的元素放在开头,因此需要反转谓词:
std::stable_partition(vec.begin(), vec.end(), [](int* pi){ return *pi % 2 != 0; });
std::stable_partition
返回集合的分区点,即在分区后第一个不满足谓词的元素的迭代器。因此,必须从这一点开始直到向量的末尾删除指针。之后,可以从向量中删除元素:
auto firstToRemove = std::stable_partition(vec.begin(), vec.end(), [](int* pi){ return *pi % 2 != 0; });
std::for_each(firstToRemove, vec.end(), [](int* pi){ delete pi; });
vec.erase(firstToRemove, vec.end());
另一种解决方法是删除要移除的指针,并将它们设置为nullptr
,然后才对nullptr
执行std::remove
:
for(auto& pointer : vec)
{
if (*pointer % 2 == 0)
{
delete pointer;
pointer = nullptr;
}
}
vec.erase(std::remove(vec.begin(), vec.end(), nullptr), vec.end());
由于删除操作是在调用std::remove
之前执行的,因此不再存在悬空指针的问题。但是,这种解决方案仅在vector
不包含空指针时才有效。否则,空指针将与for
循环设置的指针一起被移除。
四、总结
要小心拥有原始指针。优先选择unique_ptr
或其他智能指针而不是拥有原始指针。这将使代码更简洁、更具表现力。如果确实需要使用拥有原始指针的vector
,请选择正确的STL算法来正确处理内存管理!