轻松解决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::removestd::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来初始化vectorunique_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::removestd::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_iferase调用中分离出来,以便在调用时可以删除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算法来正确处理内存管理!

在这里插入图片描述

  • 17
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Lion Long

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值