c++智能指针详解
为什么要使用智能指针
一句话带过:智能指针就是帮我们C++程序员管理动态分配的内存的,它会帮助我们自动释放new出来的内存,从而避免内存泄漏!
如下例子就是内存泄露的例子:
// 动态分配内存,没有释放就return
void memoryLeak1() {
string *str = new string("动态分配内存!");
return;
}
// 动态分配内存,虽然有些释放内存的代码,但是被半路截胡return了
int memoryLeak2() {
string *str = new string("内存泄露!");
// ...此处省略一万行代码
// 发生某些异常,需要结束函数
if (1) {
return -1;
}
/
// 另外,使用try、catch结束函数,也会造成内存泄漏!
/
delete str; // 虽然写了释放内存的代码,但是遭到函数中段返回,使得指针没有得到释放
return 1;
}
int main(void) {
memoryLeak1();
memoryLeak2();
return 0;
}
memoryLeak1函数中,new了一个字符串指针,但是没有delete就已经return结束函数了,导致内存没有被释放,内存泄露!
memoryLeak2函数中,new了一个字符串指针,虽然在函数末尾有些释放内存的代码delete str,但是在delete之前就已经return了,所以内存也没有被释放,内存泄露!
使用指针,我们没有释放,就会造成内存泄露。但是我们使用普通对象却不会!
思考:如果我们分配的动态内存都交由有生命周期的对象来处理,那么在对象过期时,让它的析构函数删除指向的内存,这看似是一个 very nice 的方案?
智能指针就是通过这个原理来解决指针自动释放的问题!
- C++98 提供了 auto_ptr 模板的解决方案
- 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]);
举例不使用智能指针:
class Test {
public:
Test() { cout << "Test的构造函数..." << endl; }
~Test() { cout << "Test的析构函数..." << endl; }
int getDebug() { return this->debug; }
private:
int debug = 20;
};
int main(void) {
Test *test = new Test;
return 0;
}
当我们直接new这个类的对象,却没有释放时。。。

可以看到,只是打印了构造函数这个字符串,而析构函数的字符却没有被打印,说明并没有调用析构函数!这就导致了内存泄露!
解决内存泄露的办法,要么手动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;
}

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

auto_ptr的函数
-
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); } -
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); } -
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; }
缺点
- 复制或者赋值都会改变资源的所有权
// 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;
- 在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; - 不支持对象数组的内存管理
auto_ptr<int[]> array(new int[5]);
auto_ptr智能指针的原理
auto_ptr的底层实现是通过一个简单的指针包装类来实现的。它的定义如下:
template<class T>
class auto_ptr {
public:
explicit auto_ptr(T* ptr = nullptr) : ptr_(ptr) {}
~auto_ptr() {
delete ptr_;
}
auto_ptr(auto_ptr& other) {
ptr_ = other.release();
}
auto_ptr& operator=(auto_ptr& other) {
if (this != &other) {
delete ptr_;
ptr_ = other.release();
}
return *this;
}
T* get() const {
return ptr_;
}
T& operator*() const {
return *ptr_;
}
T* operator->() const {
return ptr_;
}
T* release() {
T* temp = ptr_;
ptr_ = nullptr;
return temp;
}
private:
T* ptr_;
};
上述代码展示了一个简化的auto_ptr实现,它使用了一个指针成员变量ptr_来持有动态分配的对象的地址。在构造函数中,可以传入一个指针来初始化auto_ptr,并接管该指针所指向的对象。析构函数会在auto_ptr对象销毁时自动释放所拥有的对象,即调用delete操作符释放指针指向的内存。
看下面一个例子
void func()
{
auto_ptr pstr (new string);
g(); //如果g()掷出异常,pstr 被自动摧毁
}
auto_ptr 创建的是一个对象,当我们创建一个栈上的变量时,程序在结束是会自动被摧毁,C++保证在堆栈展开过程中,自动存储类型的对象被自动摧毁。而auto_ptr pstr就是一个栈变量(对象),在程序结束时,pstr的析构会删除其绑定的串指针也就是在堆上的内存,所以不会发生内存泄露。
unique_ptr
auto_ptr是用于C++11之前的智能指针。由于 auto_ptr 基于排他所有权模式:两个指针不能指向同一个资源,复制或赋值都会改变资源的所有权。auto_ptr 主要有三大问题:
- 复制和赋值会改变资源的所有权,不符合人的直觉。
- 在 STL 容器中使用auto_ptr存在重大风险,因为容器内的元素必需支持可复制(copy constructable)和可赋值(assignable)。
- 不支持对象数组的操作
以上问题已经在上面体现出来了,下面将使用unique_ptr解决这些问题。
所以,C++11用更严谨的unique_ptr 取代了auto_ptr!
unique_ptr 和 auto_ptr用法几乎一样,除了一些特殊
4. 基于排他所有权模式:两个指针不能指向同一个资源
5. 无法进行左值unique_ptr复制构造,也无法进行左值复制赋值操作,但允许临时右值赋值构造和赋值
6. 保存指向某个对象的指针,当它本身离开作用域时会自动释放它指向的对象。
7. 在容器中保存指针是安全的
- 无法进行左值复制赋值操作,但允许临时右值赋值构造和赋值
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; - 在 STL 容器中使用unique_ptr,不允许直接赋值
当然,运行后是直接报错的,因为vec[1]已经是NULL了,再继续访问就越界了。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; - 支持对象数组管理
// 会自动调用delete [] 函数去释放内存 unique_ptr<int[]> array(new int[5]); // 支持这样定义
auto_ptr 与 unique_ptr智能指针的内存管理陷阱
auto_ptr<string> p1;
string *str = new string("智能指针的内存管理陷阱");
p1.reset(str); // p1托管str指针
auto_ptr<string> p2;
p2.reset(str); // p2接管str指针时,会先取消p1的托管,然后再对str的托管
// 此时p1已经没有托管内容指针了,为NULL,在使用它就会内存报错!
cout << "str:" << *p1 << endl;
这是由于auto_ptr 与 unique_ptr的排他性所导致的!
为了解决这样的问题,我们可以使用shared_ptr指针指针!
shared_ptr
熟悉了unique_ptr 后,其实我们发现unique_ptr 这种排他型的内存管理并不能适应所有情况,有很大的局限!如果需要多个指针变量共享怎么办?
如果有一种方式,可以记录引用特定内存对象的智能指针数量,当复制或拷贝时,引用计数加1,当智能指针析构时,引用计数减1,如果计数为零,代表已经没有指针指向这块内存,那么我们就释放它!

- 引用计数
如上代码,sp1 = sp2; 和 shared_ptr< Person > sp3(sp1);就是在使用引用计数了。shared_ptr<Person> sp1; shared_ptr<Person> sp2(new Person(2)); // 获取智能指针管控的共享指针的数量 use_count():引用计数 cout << "sp1 use_count() = " << sp1.use_count() << endl; cout << "sp2 use_count() = " << sp2.use_count() << endl << endl; // 共享 sp1 = sp2; cout << "sp1 use_count() = " << sp1.use_count() << endl; cout << "sp2 use_count() = " << sp2.use_count() << endl << endl; shared_ptr<Person> sp3(sp1); cout << "sp1 use_count() = " << sp1.use_count() << endl; cout << "sp2 use_count() = " << sp2.use_count() << endl; cout << "sp2 use_count() = " << sp3.use_count() << endl << endl;
