(P76)stl(四):ptr_vector实现,通过ptr_vector来避免潜在的内存泄漏

时间:2022-05-06 01:19:08


文章目录

  • 1.ptr_vector实现
  • 2.通过ptr_vector来避免潜在的内存泄漏
  • 3.其他版本的ptr_vector的实现

1.ptr_vector实现

  • eg:P75\CalculatorTest2\main.cpp
#include <iostream>
#include <memory>
#include <vector>

using namespace std;
#include "DebugNew.h"
#include "Node.h"

class Test
{
public:
    Test()
    {
        cout<<"Test ..."<<endl;
    }
    Test(const Test& other)
    {
        cout <<"Copy Test ..."<<endl;
    }
    ~Test()
    {
        cout <<"~Test() ..."<<endl;
    }
};


int main(void)
{
    vector<Test> v;
    Test t1;
    Test t2;
    Test t3;
    //这3个对象会释放

    //容器中的3个对象也会释放,总共调用6次析构
    v.push_back(t1);
    v.push_back(t2);
    v.push_back(t3);        

    return 0;
}
  • 存放的类型是Test为啥会调用拷贝构造函数?
Test t1;
v.push_back(t1);			sizeof(Test)

不是因为push_back调用,才拷贝构造函数的(因为是引用,不会调用拷贝构造函数),而是push_back的时候内部分配了空间,通过operator new来分配空间,它不调用构造函数和拷贝构造函数

接着,placement new不分配空间,调用拷贝构造函数(在原来已经分配的空间的基础上初始化一个对象)

(P76)stl(四):ptr_vector实现,通过ptr_vector来避免潜在的内存泄漏

  • eg:P75\CalculatorTest2\main1.cpp
#include <iostream>
#include <memory>
#include <vector>

using namespace std;
#include "DebugNew.h"
#include "Node.h"

class Test
{
public:
    Test()
    {
        cout<<"Test ..."<<endl;
    }
    Test(const Test& other)
    {
        cout <<"Copy Test ..."<<endl;
    }
    ~Test()
    {
        cout <<"~Test() ..."<<endl;
    }
};



int main(void)
{
    vector<Test*> v;
    Test* t1 = new Test;
    Test* t2 = new Test;
    Test* t3 = new Test;

    v.push_back(t1);
    v.push_back(t2);
    v.push_back(t3);        

    return 0;
}
Test* t1 = new Test;
v.push_bak(t1);				sizeof(Test*)

而如果插入的是Test*类型的对象,placement new时不分配空间,也不会调用拷贝构造函数的,因为将指针插入到向量中是不会调用拷贝构造函数的(相当于初始化的是一个指针,而不是一个对象)。
插入到向量当中,每个元素的大小的sizeof(Test*),不论什么类型的指针, 插入到向量中为每个元素分类的空间都是4个字节。
sizeof(Test)插入的字节数是类的大小。
指针的初始化是不需要调用拷贝构造函数来初始化。

  • 测试:因为初始化的是指针,是不会调用拷贝构造函数来初始化。
    vector向量只是接管了指针本身,而并没有接管指针变量所指向的对象。
    vector容器,stl的所有容器都是值语义的。
    往容器中插入指针,容器拷贝的仅仅只是指针,容器并不负责指针所指向的内存空间的释放。
    往容器中插入的是类对象,容器拷贝整个对象(容器中拷贝的对象与原对象脱离关系,原对象的释放由外部自己负责,容器会负责所拷贝到容器中对象的释放。)
  • 对于指针而言,将其值可以看出是long型的整型。类似vector这玩意会调用析构吗?它又不是对象,所以放到vector中不安全的指针(指向对象的指针是不安全的),需要自己释放

2.通过ptr_vector来避免潜在的内存泄漏

  • 实现一个类模板ptr_vector指针vector
    ptr_vector不仅仅释放指针本身,还用于负责指针所指向空间的释放
  • eg:P75\CalculatorTest2\ptr_vector.h
  • 关于派生类模板调用基类模板中的相关函数见:C++类模板中如何调用其基类模板中的函数,下面的代码因为是模板继承后,重定义了基类的成员函数,但是又想调用基类中重定义的函数的做法,参考:(P17-P18)通过using定义基础类型和函数指针别名,使用using和typedef给模板定义别名
#ifndef _PTR_VECTOR_H_
#define _PTR_VECTOR_H_

#include <vector>
#include <memory>

template<typename T>
class ptr_vector : public std::vector<T*>
{
public:
    ~ptr_vector()
    {
        clear();
    }
    //重定义vector中的clear方法
    void clear()
    {
        //负责指针所指向内存空间的释放
        typename std::vector<T*>::iterator it;
        for (it = std::vector<T*>::begin(); it != std::vector<T*>::end(); ++it)
            delete *it;//it就是T*的指针,取*就是T*
    }

    //基类的clear释放自指针变量本身,
    //using std::vector<T*>::clear;别处使用:clear();因为是值拷贝,基类不释放无所谓,只要delte一次就行
    std::vector<T*>::clear();

    //将指针插入向量中时,执行operator new可能分配内存失败,即指针没有插入到向量中,
    //std::vector<T*>::clear();可能不会释放内存,存在内存泄漏
    //处理方法:将所有权转移到智能指针ptr对象上,即使内部内存空间分配失败。智能指针是一个局部对象
    //捕捉异常时,在栈展开的时候会释放对象,将其持有的val指针释放掉
    void push_back(T* const &val) 
    {
        std::auto_ptr<T> ptr(val);
        std::vector<T*>::push_back(val);
        ptr.release();//程序运行到这里,说明operator new分配内存成功,ptr释放所有权
    }

    //增加一个接口:将一个智能指针插入到向量中
    //原生的vector是不能将指针插入到向量中的,如果_Ty是智能指针类型,意味着智能指针在释放所有权的时候(裸指针、智能指针不能插入到vector)
    //会更改内部的指针,将指针置为0,而这里的修饰是const,指针内部的数据成员是不能更改的
    void push_back(std::auto_ptr<T>& val)
    {
        //这里不要使用std::vector<T*>::push_back(val.release());
        //push_back()的operator new过后,且因为release()会释放所有权,造成这个指针成为野指针,没人管,造成内存泄漏
        std::vector<T*>::push_back(val.get());
        //插入成功后,释放所有权
        val.release();//会更改智能指针内部的数据成员的,release()表示智能指针释放所有权
    }

    //boost也有ptr_vector,考虑的内容更多

};
#endif // _PTR_VECTOR_H_

(P76)stl(四):ptr_vector实现,通过ptr_vector来避免潜在的内存泄漏


(P76)stl(四):ptr_vector实现,通过ptr_vector来避免潜在的内存泄漏


(P76)stl(四):ptr_vector实现,通过ptr_vector来避免潜在的内存泄漏

  • eg:P75\CalculatorTest2\main2.cpp
#include <iostream>
#include <memory>
#include <vector>

using namespace std;
#include "DebugNew.h"
#include "Node.h"
#include "ptr_vector.h"

class Test
{
public:
    Test()
    {
        cout<<"Test ..."<<endl;
    }
    Test(const Test& other)
    {
        cout <<"Copy Test ..."<<endl;
    }
    ~Test()
    {
        cout <<"~Test() ..."<<endl;
    }
};



int main(void)
{
    // vector<Test*> v;
    ptr_vector<Test> v;
    
    Test* t1 = new Test;
    Test* t2 = new Test;
    Test* t3 = new Test;

    //动态内存的所有权转移到了ptr_vector向量中,由向量来负责释放
    v.push_back(t1);
    v.push_back(t2);
    v.push_back(t3);        

    return 0;
}
  • 测试:
  • (P76)stl(四):ptr_vector实现,通过ptr_vector来避免潜在的内存泄漏

  • eg:
    智能指针auto_ptr:使用起来是一个类,类对象在其生命周期结束的时候,会调用他的析构函数,在析构函数中对其所持有的资源进行释放;
    ptr_vector:防止当指针还没有插入到向量的过程中,就分配内存失败了,此时指针是没办法释放的,此外,智能指针对象是没办法插入到向量中的
    P75\Calculator\Node.h
void AppendChild(std::auto_ptr<Node>& node, bool positive)
    {
        //存在push_back的时候,operator new失败,可能存在风险
        //智能指针(原生指针)node.release()释放了所有权之后就是1个裸指针,原生指针还没有插入到向量中,operator new分配内存就出现了异常
        //此时,裸指针还没有插入到向量中,即使析构函数~MultipleNode();调用了,也没办法遍历出来,将其释放掉,
        //那么指针所指向的内存就出现了泄漏
        //解决办法是,使用ptr_vector
        // childs_.push_back(node.release());//元素类型是Node*,不能放入智能指针,而应该放入原生指针:释放所有权后传进容器中
        childs_.push_back(node);//因为重载了push_back方法
        positive_.push_back(positive);
    }
    ~MultipleNode();
private:
    //有很多子代,将其放入向量中
    // std::vector<Node*> childs_;//vector里面存放的是Node*
    ptr_vector<Node> childs_;
    std::vector<bool> positive_;//节点的正负性
};

P75\Calculator\Node.cpp

MultipleNode::~MultipleNode()
{
    // std::vector<Node*>::const_iterator it;
    // for (it = children_.begin(); it != children_.end(); ++it)
    // {
    //     //it实际上是Node*的指针的指针,它存放的类型是Node*,*it取出它里面存放的元素Node*
    //     delete *it;
    // }
}
  • 测试:
将析构函数注释掉,且不使用ptr_vector
MultipleNode::~MultipleNode()
{
    // std::vector<Node*>::const_iterator it;
    // for (it = children_.begin(); it != children_.end(); ++it)
    // {
    //     //it实际上是Node*的指针的指针,它存放的类型是Node*,*it取出它里面存放的元素Node*
    //     delete *it;
    // }
}

会出现内存泄漏

(P76)stl(四):ptr_vector实现,通过ptr_vector来避免潜在的内存泄漏


不再出现内存泄漏的问题了

(P76)stl(四):ptr_vector实现,通过ptr_vector来避免潜在的内存泄漏

3.其他版本的ptr_vector的实现

ptr_vector析构的时候会析构自己开辟出来的存放指针的空间,同时析构指针本身指向的空间而,一般容器不会析构指针本身指向的空间

  • 参考boost::ptr_vector。
  • eg:
#ifndef _PTR_VECTOR_HH
#define _PTR_VECTOR_HH

#include <vector>
#include <memory>
#include <assert.h>

namespace myself
{

#define ASSERT(If, Msg) \
	if(!(If)) \
	{\
		fprintf(stderr, "Error/(%s, %d): %s, abort.\n", __FILE__, __LINE__, Msg); abort();\
	}

template<typename T>
class ptr_vector
{
public:
	typedef unsigned int size_type;
	typedef std::vector<T*> vector;
	typedef T* value_type;
	typedef value_type& reference;

	explicit ptr_vector()
	{
	}

	//ptr_vector析构的时候会析构自己开辟出来的存放指针的空间,同时析构指针本身指向的空间
	//而一般容器不会析构指针本身指向的空间
	~ptr_vector()
	{
		clear();
	}

	void clear()
	{
		if(!m_vector.empty()) {
			//typename vector::iterator it;
			for(auto it = m_vector.begin(); it != m_vector.end(); ++it) {
				delete *it;//释放指针指向的内存.
			}
		}

		m_vector.clear();//释放指针本身.
	}

	//在尾部追加元素
	//unique_ptr对象包装一个原始指针,并负责其生命周期。
	//当该对象被销毁时,会在其析构函数中删除关联的原始指针,防止内存泄漏
	void push_back(T* const &v)
	{
		ASSERT(v, "NULL point at ptr_vector push_back()");
		std::unique_ptr<T> tmp(v);
		m_vector.push_back(v); //使用 unique_ptr 保证push_back失败时,v也能正常释放.
		tmp.release();
	}

	//删除最后一个元素并返回指向删除对象的指针
	std::unique_ptr<T> pop_back()
	{
		ASSERT( !m_vector.empty(), "'pop_back()' on empty container");
		std::unique_ptr<T> tmp(m_vector.back());
		m_vector.pop_back();
		return std::move(tmp);
	}

	//提供像c数组通过下表访问的方法
	reference operator[](size_type n)
	{
		ASSERT(n < size(), "operator[] n out of the border")
		return m_vector[n];
	}

	bool empty()
	{
		return m_vector.empty();
	}

	size_type size()
	{
		return m_vector.size();
	}

	void reserve(size_type n)
	{
		m_vector.reserve(n);
	}

	void resize(size_type s)
	{
		size_type size = this->size();
		if(s < size) {
			for(auto it = m_vector.begin() + s; it != m_vector.end(); ++it) {
				delete *it;//释放指针指向的内存.
			}
			m_vector.resize(s);
		} else if(s > size) {
			for(; size != s; ++size) {
				push_back(new T);
			}
		}
		ASSERT(s == this->size(), "'resize' error size asymmetry");
	}

	void swap(ptr_vector<T>& v)
	{
		m_vector.swap(v.base());
	}

private:
	//将拷贝构造和赋值  设置为私有方法,防止所有权转移
	ptr_vector<T>& operator=(const ptr_vector<T>&);
	ptr_vector<T>(ptr_vector<T>&);

	vector& base()
	{
		return m_vector;
	}

	vector m_vector;
};

}

#endif
  • eg:性能测试:
#include "ptr_vector.h"

#include <ctime>
#include <iostream>
#include <memory>
#include <string>
#include <vector>

using std::cout;
using std::endl;
using std::shared_ptr;
using std::unique_ptr;
using std::string;
using std::vector;

//https://www.bbsmax.com/A/kvJ3pKKw5g/
//https://www.codeproject.com/Articles/7351/ptr-vector-A-Container-For-Pointers

class TSomeData {
  private:
	int data;
  public:
	TSomeData(int d)
		: data(d) {
		// Empty
	}
};

const int TEST_ITERATIONS = 10000000;

typedef vector<unique_ptr<TSomeData>> TVectorOfUnique;
typedef vector<shared_ptr<TSomeData>> TVectorOfShared;
typedef myself::ptr_vector<TSomeData> TPtrVector;


int test_cmp2sharedptr() {

	clock_t start;
	clock_t end;

    /*C 库函数 clock_t clock(void) 返回程序执行起(一般为程序的开头),处
    理器时钟所使用的时间。为了获取 CPU 所使用的秒数,您需要除以 CLOCKS_PER_SEC。*/
	start = ::clock();
	TVectorOfShared vectorOfShared;
	for (int i = 0; i < TEST_ITERATIONS; ++i) {
		// Test vector of shared_ptr
		shared_ptr<TSomeData> data(new TSomeData(i));
		vectorOfShared.push_back(data);
	}
	end = ::clock();

	cout << "Vector of shared:\n CPU occupy Time executed: " 
	     << static_cast<uint32_t>((end - start) / (CLOCKS_PER_SEC/1000)) 
	     << endl;

	// ********************************************************************* //

	start = ::clock();
	TVectorOfUnique vectorOfUnique;
	for (int i = 0; i < TEST_ITERATIONS; ++i) {
	  unique_ptr<TSomeData> data(new TSomeData(i));
	  vectorOfUnique.push_back(std::move(data));
	}
	end = ::clock();

	cout << "Vector of unique:\n CPU occupy Time executed: "
	     << static_cast<uint32_t>((end - start) / (CLOCKS_PER_SEC/1000)) 
	     << endl;

	// ********************************************************************* //

	start = ::clock();
	TPtrVector ptrVector;
	for (int i = 0; i < TEST_ITERATIONS; ++i) {
		// Test ptr_vector
		TSomeData* data = new TSomeData(i);
		ptrVector.push_back(data);
	}
	end = ::clock();

	cout << "PtrVector:\n CPU occupy Time executed: "
	     << static_cast<uint32_t>((end - start) / (CLOCKS_PER_SEC/1000)) 
	     << endl;

	return 0;
}

int test_ptr_vector() {

	myself::ptr_vector<string> vec;
	vec.push_back (new string ("Hello, "));
	vec.push_back (new string ("world! "));

	//cout << vec[0]		 << vec.at(1)
	//     << *vec.begin() << vec.begin()[1]
	//    << vec.front()  << vec.back()
	//     << endl;

	cout << *vec[0] << endl;


	return 0;
}


int main() {

	test_cmp2sharedptr();

	test_ptr_vector();

	return 0;
}
  • 结果:-O0情况下测试结果(unique_ptr性能会好点)
  • (P76)stl(四):ptr_vector实现,通过ptr_vector来避免潜在的内存泄漏

  • 参考:muduo ptr_vector详解