C++ 智能指针 - 全部用法详解

Posted cpp_learner

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++ 智能指针 - 全部用法详解相关的知识,希望对你有一定的参考价值。

为什么要学习智能指针?
咳咳,这个问题不是问大家的,是询问我自己的!
我依稀记得刚离校出来找实习工作那会,去面试一份工作,其中有一个环节需要答题;有一道题目就是问什么是智能指针?卧槽?当时我就懵逼,智能指针我压根就没有听说过…
最后,面试的这份工作理所应当的黄了。
差不多是一年前左右吧,现在趁有闲余时间,学习一下智能指针,丰富一下自己!



一、为什么要使用智能指针

一句话带过:智能指针就是帮我们C++程序员管理动态分配的内存的,它会帮助我们自动释放new出来的内存,从而避免内存泄漏

如下例子就是内存泄露的例子:

#include <iostream>
#include <string>
#include <memory>

using namespace std;


// 动态分配内存,没有释放就return
void memoryLeak1() {
	string *str = new string("动态分配内存!");
	return;
}

// 动态分配内存,虽然有些释放内存的代码,但是被半路截胡return了
int memoryLeak2() {
	string *str = new string("内存泄露!");

	// ...此处省略一万行代码

	// 发生某些异常,需要结束函数
	if (1) {
		return -1;
	}

	delete str;	// 虽然写了释放内存的代码,但是遭到函数中段返回,使得指针没有得到释放
	return 1;
}


int main(void) {

	memoryLeak1();

	memoryLeak2();

	return 0;
} 

memoryLeak1函数中,new了一个字符串指针,但是没有delete就已经return结束函数了,导致内存没有被释放,内存泄露!
memoryLeak2函数中,new了一个字符串指针,虽然在函数末尾有些释放内存的代码delete str,但是在delete之前就已经return了,所以内存也没有被释放,内存泄露!

使用指针,我们没有释放,就会造成内存泄露。但是我们使用普通对象却不会!

思考:如果我们分配的动态内存都交由有生命周期的对象来处理,那么在对象过期时,让它的析构函数删除指向的内存,这看似是一个 very nice 的方案?

智能指针就是通过这个原理来解决指针自动释放的问题!

  1. C++98 提供了 auto_ptr 模板的解决方案
  2. C++11 增加unique_ptr、shared_ptr 和weak_ptr

二、auto_ptr

auto_ptr 是c++ 98定义的智能指针模板,其定义了管理指针的对象,可以将new 获得(直接或间接)的地址赋给这种对象。当对象过期时,其析构函数将使用delete 来释放内存!

用法:
头文件: #include < memory >
用 法: auto_ptr<类型> 变量名(new 类型)

例 如:
auto_ptr< string > str(new string(“我要成为大牛~ 变得很牛逼!”));
auto_ptr<vector< int >> av(new vector< int >());
auto_ptr< int > array(new int[10]);


我们先定义一个类,类的构造函数和析构函数都输出一个字符串用作提示!
定义一个私有成员变量,赋值20.
再定义一个私有成员方法用于返回这个私有成员变量。

class Test {
public:
	Test() { cout << "Test的构造函数..." << endl; }
	~Test() { cout << "Test的析构函数..." << endl; }

	int getDebug() { return this->debug; }

private:
	int debug = 20;
};

当我们直接new这个类的对象,却没有释放时。。。

int main(void) {
	Test *test = new Test;

	return 0;
} 

可以看到,只是打印了构造函数这个字符串,而析构函数的字符却没有被打印,说明并没有调用析构函数!这就导致了内存泄露!
解决内存泄露的办法,要么手动delete,要么使用智能指针!

使用智能指针

// 定义智能指针
auto_ptr<Test> test(new Test);

智能指针可以像普通指针那样使用:

cout << "test->debug:" << test->getDebug() << endl;
cout << "(*test).debug:" << (*test).getDebug() << endl;

这时再试试:

int main(void) {

	//Test *test = new Test;
	auto_ptr<Test> test(new Test);

	cout << "test->debug:" << test->getDebug() << endl;
	cout << "(*test).debug:" << (*test).getDebug() << endl;

	return 0;
} 


自动调用了析构函数。
为什么智能指针可以像普通指针那样使用???
因为其里面重载了 * 和 -> 运算符, * 返回普通对象,而 -> 返回指针对象。

具体原因不用深究,只需知道他为什么可以这样操作就像!
函数中返回的是调用get()方法返回的值,那么这个get()是什么呢?

智能指针的三个常用函数:

  1. get() 获取智能指针托管的指针地址

    // 定义智能指针
    auto_ptr<Test> test(new Test);
    
    Test *tmp = test.get();		// 获取指针返回
    cout << "tmp->debug:" << tmp->getDebug() << endl;
    

    但我们一般不会这样使用,因为都可以直接使用智能指针去操作,除非有一些特殊情况。
    函数原型

    _NODISCARD _Ty * get() const noexcept
    {	// return wrapped pointer
    	return (_Myptr);
    }
    
  2. release() 取消智能指针对动态内存的托管

    // 定义智能指针
    auto_ptr<Test> test(new Test);
    
    Test *tmp2 = test.release();	// 取消智能指针对动态内存的托管
    delete tmp2;	// 之前分配的内存需要自己手动释放
    

    也就是智能指针不再对该指针进行管理,改由管理员进行管理!
    函数原型

    _Ty * release() noexcept
    {	// return wrapped pointer and give up ownership
    	_Ty * _Tmp = _Myptr;
    	_Myptr = nullptr;
    	return (_Tmp);
    }
    
  3. reset() 重置智能指针托管的内存地址,如果地址不一致,原来的会被析构掉

    // 定义智能指针
    auto_ptr<Test> test(new Test);
    
    test.reset();			// 释放掉智能指针托管的指针内存,并将其置NULL
    
    test.reset(new Test());	// 释放掉智能指针托管的指针内存,并将参数指针取代之
    

    reset函数会将参数的指针(不指定则为NULL),与托管的指针比较,如果地址不一致,那么就会析构掉原来托管的指针,然后使用参数的指针替代之。然后智能指针就会托管参数的那个指针了。
    函数原型:

    void reset(_Ty * _Ptr = nullptr)
    {	// destroy designated object and store new pointer
    	if (_Ptr != _Myptr)
    		delete _Myptr;
    	_Myptr = _Ptr;
    }
    

使用建议

  1. 尽可能不要将auto_ptr 变量定义为全局变量或指针;

    // 没有意义,全局变量也是一样
    auto_ptr<Test> *tp = new auto_ptr<Test>(new Test);	
    
  2. 除非自己知道后果,不要把auto_ptr 智能指针赋值给同类型的另外一个 智能指针;

    auto_ptr<Test> t1(new Test);
    auto_ptr<Test> t2(new Test);
    t1 = t2;	// 不要这样操作...
    
  3. C++11 后auto_ptr 已经被“抛弃”,已使用unique_ptr替代!C++11后不建议使用auto_ptr。

  4. auto_ptr 被C++11抛弃的主要原因

    1). 复制或者赋值都会改变资源的所有权

    // auto_ptr 被C++11抛弃的主要原因
    auto_ptr<string> p1(new string("I'm Li Ming!"));
    auto_ptr<string> p2(new string("I'm age 22."));
    
    cout << "p1:" << p1.get() << endl;
    cout << "p2:" << p2.get() << endl;
    
    // p2赋值给p1后,首先p1会先将自己原先托管的指针释放掉,然后接收托管p2所托管的指针,
    // 然后p2所托管的指针制NULL,也就是p1托管了p2托管的指针,而p2放弃了托管。
    p1 = p2;	
    cout << "p1 = p2 赋值后:" << endl;
    cout << "p1:" << p1.get() << endl;
    cout << "p2:" << p2.get() << endl;
    

    2). 在STL容器中使用auto_ptr存在着重大风险,因为容器内的元素必须支持可复制和可赋值

    vector<auto_ptr<string>> vec;
    auto_ptr<string> p3(new string("I'm P3"));
    auto_ptr<string> p4(new string("I'm P4"));
    
    // 必须使用std::move修饰成右值,才可以进行插入容器中
    vec.push_back(std::move(p3));
    vec.push_back(std::move(p4));
    
    cout << "vec.at(0):" <<  *vec.at(0) << endl;
    cout << "vec[1]:" <<  *vec[1] << endl;
    
    
    // 风险来了:
    vec[0] = vec[1];	// 如果进行赋值,问题又回到了上面一个问题中。
    cout << "vec.at(0):" << *vec.at(0) << endl;
    cout << "vec[1]:" << *vec[1] << endl;
    

    访问越界了!

    3). 不支持对象数组的内存管理

    auto_ptr<int[]> array(new int[5]);	// 不能这样定义
    

所以,C++11用更严谨的unique_ptr 取代了auto_ptr!

测试代码

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

using namespace std;

class Test {
public:
	Test() { cout << "Test的构造函数..." << endl; }
	~Test() { cout << "Test的析构函数..." << endl; }

	int getDebug() { return this->debug; }

private:
	int debug = 20;
};

// 不要定义为全局变量,没有意义
//auto_ptr<Test> test(new Test);

void memoryLeak1() {
	//Test *test = new Test;

	// 定义智能指针
	auto_ptr<Test> test(new Test);
	
	cout << "test->debug:" << test->getDebug() << endl;
	cout << "(*test).debug:" << (*test).getDebug() << endl;


	// get方法
	Test *tmp = test.get();		// 获取指针返回
	cout << "tmp->debug:" << tmp->getDebug() << endl;


	// release方法
	Test *tmp2 = test.release();	// 取消智能指针对动态内存的托管
	delete tmp2;	// 之前分配的内存需要自己手动释放


	// reset方法:重置智能指针托管的内存地址,如果地址不一致,原来的会被析构掉
	test.reset();			// 释放掉智能指针托管的指针内存,并将其置NULL
	test.reset(new Test());	// 释放掉智能指针托管的指针内存,并将参数指针取代之


	// 忠告:不要将智能指针定义为指针
	//auto_ptr<Test> *tp = new auto_ptr<Test>(new Test);

	// 忠告:不要定义指向智能指针对象的指针变量
	//auto_ptr<Test> t1(new Test);
	//auto_ptr<Test> t2(new Test);
	//t1 = t2;

	return;
}

int memoryLeak2() {
	//Test *test = new Test();

	// 定义智能指针
	auto_ptr<Test> test(new Test);

	// ...此处省略一万行代码

	// 发生某些异常,需要结束函数
	if (1) {
		return -1;
	}

	//delete test;
	return 1;
}


int main1(void) {

	//memoryLeak1();

	//memoryLeak2();

	//Test *test = new Test;
	//auto_ptr<Test> test(new Test);

	//cout << "test->debug:" << test->getDebug() << endl;
	//cout << "(*test).debug:" << (*test).getDebug() << endl;


	 auto_ptr 被C++11抛弃的主要原因
	//auto_ptr<string> p1(new string("I'm Li Ming!"));
	//auto_ptr<string> p2(new string("I'm age 22."));
	//
	//cout << "p1:" << p1.get() << endl;
	//cout << "p2:" << p2.get() << endl;

	//p1 = p2;
	//cout << "p1 = p2 赋值后:" << endl;
	//cout << "p1:" << p1.get() << endl;
	//cout << "p2:" << p2.get() << endl;



	// 弊端2.在STL容器中使用auto_ptr存在着重大风险,因为容器内的元素必须支持可复制
	vector<auto_ptr<string>> vec;
	auto_ptr<string> p3(new string("I'm P3"));
	auto_ptr<string> p4(new string("I'm P4"));

	vec.push_back(std::move(p3));
	vec.push_back(std::move(p4));

	cout << "vec.at(0):" <<  *vec.at(0) << endl;
	cout << "vec[1]:" <<  *vec[1] << endl;


	// 风险来了:
	vec[0] = vec[1];
	cout << "vec.at(0):" << *vec.at(0) << endl;
	cout << "vec[1]:" << *vec[1] << endl;


	// 弊端3.不支持对象数组的内存管理
	//auto_ptr<int[]> array(new int[5]);	// 不能这样定义
	return 0;
} 

三、unique_ptr

auto_ptr是用于C++11之前的智能指针。由于 auto_ptr 基于排他所有权模式:两个指针不能指向同一个资源,复制或赋值都会改变资源的所有权。auto_ptr 主要有三大问题:

  1. 复制和赋值会改变资源的所有权,不符合人的直觉。
  2. 在 STL 容器中使用auto_ptr存在重大风险,因为容器内的元素必需支持可复制(copy constructable)和可赋值(assignable)。
  3. 不支持对象数组的操作

以上问题已经在上面体现出来了,下面将使用unique_ptr解决这些问题。

所以,C++11用更严谨的unique_ptr 取代了auto_ptr!

unique_ptr 和 auto_ptr用法几乎一样,除了一些特殊。

unique_ptr特性

  1. 基于排他所有权模式:两个指针不能指向同一个资源
  2. 无法进行左值unique_ptr复制构造,也无法进行左值复制赋值操作,但允许临时右值赋值构造和赋值
  3. 保存指向某个对象的指针,当它本身离开作用域时会自动释放它指向的对象。
  4. 在容器中保存指针是安全的

A. 无法进行左值复制赋值操作,但允许临时右值赋值构造和赋值

unique_ptr<string> p1(new string("I'm Li Ming!"));
unique_ptr<string> p2(new string("I'm age 22."));
	
cout << "p1:" << p1.get() << endl;
cout << "p2:" << p2.get() << endl;

p1 = p2;					// 禁止左值赋值
unique_ptr<string> p3(p2);	// 禁止左值赋值构造

unique_ptr<string> p3(std::move(p1));
p1 = std::move(p2);	// 使用move把左值转成右值就可以赋值了,效果和auto_ptr赋值一样

cout << "p1 = p2 赋值后:" << endl;
cout << "p1:" << p1.get() << endl;
cout << "p2:" << p2.get() << endl;

运行截图:

B. 在 STL 容器中使用unique_ptr,不允许直接赋值

vector<unique_ptr<string>> vec;
unique_ptr<string> p3(new string("I'm P3"));
unique_ptr<string> p4(new string("I'm P4"));

vec.push_back(std::move(p3));
vec.push_back(std::move(p4));

cout << "vec.at(0):" << *vec.at(0) << endl;
cout << "vec[1]:" << *vec[1] << endl;

vec[0] = vec[1];	/* 不允许直接赋值 */
vec[0] = std::move(vec[1]);		// 需要使用move修饰,使得程序员知道后果

cout << "vec.at(0):" << *vec.at(0) << endl;
cout << "vec[1]:" << *vec[1] << endl;

当然,运行后是直接报错的,因为vec[1]已经是NULL了,再继续访问就越界了。

C. 支持对象数组的内存管理

// 会自动调用delete [] 函数去释放内存
unique_ptr<int[]> array(new int[5]);	// 支持这样定义

除了上面ABC三项外,unique_ptr的其余用法都与auto_ptr用法一致。

  1. 构造

    class Test {
    public:
    	Test() { cout << "Test的构造函数..." << endl; }
    	~Test() { cout << "Test的析构函数..." << endl; }
    
    	void doSomething() { cout << "do something......" << endl; }
    };
    
    
    // 自定义一个内存释放其
    class DestructTest {
    	public:
    	void operator()(Test *pt) {
    		pt->doSomething();
    		delete pt;
    	}
    };
    
    // unique_ptr<T> up; 空的unique_ptr,可以指向类型为T的对象
    unique_ptr<Test> t1;
    
    // unique_ptr<T> up1(new T());	定义unique_ptr,同时指向类型为T的对象
    unique_ptr<Test> t2(new Test);
    
    // unique_ptr<T[]> up;	空的unique_ptr,可以指向类型为T[的数组对象
    unique_ptr<int[]> t3;
    
    // unique_ptr<T[]> up1(new T[]);	定义unique_ptr,同时指向类型为T的数组对象
    unique_ptr<int[]> t4(new int[5]);
    
    // unique_ptr<T, D> up();	空的unique_ptr,接受一个D类型的删除器D,使用D释放内存
    unique_ptr<Test, DestructTest> t5;
    
    // unique_ptr<T, D> up(new T(

    以上是关于C++ 智能指针 - 全部用法详解的主要内容,如果未能解决你的问题,请参考以下文章

    C++ - 指针和“智能指针”

    c++使用裸指针与智能指针返回数组详解

    C++智能指针详解:智能指针的引入

    C++ 智能指针详解(转)

    C++智能指针详解(真的很经典 )

    C++ Boost 智能指针详解