本文记录我对智能指针的学习,主要分为库学习和自己对于智能指针实现部分的学习。
主演参考链接如下:
[从auto_ptr说起]
[C++ 11 创建和使用 unique_ptr]
[shared_ptr线程安全性分析]
[为什么多线程读写shared_ptr要加锁?]
[C++设计模式——代理模式]
[解引用、判空以及比较]
[cplusplus-shared_ptr]
概述
什么是智能指针?如何理解它?
智能指针是存储指向动态分配(堆)对象指针的类。简言之,对指向堆对象的指针做了一层封装。注意,指针有好多种,文件指针也是指针。所以,要注意区别,智能指针封装的是指向堆对象的指针。
为什么要有之智能指针?
结合上一目对于RAII的学习就很容易理解了,堆空间作用一种系统资源,需要进行管理。在没有智能指针之前,需要程序员自己管理堆空间,可能会导致内存泄露的情形。利用RAII的思路,把资源抽象为类,把对资源的管理封装到类当中,构造时候获取资源,析构的时候释放资源。从而把对资源的管理转化为对对象的管理。那么,指向堆空间的指针,作为句柄,就是我们实际管理的对象。从而有了智能指针类,本质是借助对象,对堆指针所指向的堆空间进行管理。
c++提供了哪些智能指针?
最新的标准c++11当中提供了unique_ptr, shared_ptr, weak_ptr这三种智能指针。当然,还有之前的auto_ptr
库学习
auto_ptr学习
先开宗明义说清楚,c++11的标准已经废弃了该指针,讲该指针的目的是因为之前学习了RAII,所以知识体系上一脉相承。
- auto_ptr的初衷是用来实现智能指针的,实现内存的自动回收。注意它主要是用来实现内存的自动回收。
- auto_ptr的基本思想是RAII。把资源抽象为对象,由于内存资源的句柄是指针。所以,先简单把指针看做资源,从而将该指针抽象为一个类,就有了我们的auto_ptr。
- 但是,auto_ptr离现在的智能指针还是有区别,因为它没有实现引用计数的技术,并且垃圾回收机制也不是当下主流智能指针使用的。
- 当然,好处还是很明显,自动管理资源。
下面讨论下它的bug:
#include <iostream>
#include <memory.h>
using namespace std;
class A
{
public:
A() { cout << "Construct A Object." << endl; }
~A() { cout << "Destroy A Object." << endl; }
void SetA(int value) { m_a = value; }
int GetA() { return m_a; }
private:
int m_a;
};
void TestFunc(auto_ptr<A> Obj)
{
Obj->SetA(20);
cout << Obj->GetA() << endl;
}
int main()
{
auto_ptr<A> pAObj(new A());
//auto_ptr<A> pAObj1 = new A(); **This is wrong expression.
pAObj->SetA(10);
cout << pAObj->GetA() << endl;
TestFunc(pAObj); // 问题出在这!!!
//cout << pAObj->GetA() << endl; ** This is wrong.
return 0;
}
我们之前讨论过,用RAII实现的资源管理类是禁止拷贝和赋值的,因为资源是没有拷贝和赋值的语义的。但是,上面的地方在testFunc完成了复制,此时两个指针指向同一块资源,那么testFunc结束之后会自动释放资源。从而,出来访问失败。
下面看下源码:
template <class _Tp> class auto_ptr {
private:
_Tp* _M_ptr; //实际wrap的指针
public:
typedef _Tp element_type;
// 显示构造函数,防止auto_ptr<A> pAObj1 = new A();隐士构造
explicit auto_ptr(_Tp* __p = 0) __STL_NOTHROW : _M_ptr(__p) {}
// 复制构造函数,知道为什么参数是&吗?
auto_ptr(auto_ptr& __a) __STL_NOTHROW : _M_ptr(__a.release()) {}
template <class _Tp1> auto_ptr(auto_ptr<_Tp1>& __a) __STL_NOTHROW
: _M_ptr(__a.release()) {}
// 赋值构造函数
auto_ptr& operator=(auto_ptr& __a) __STL_NOTHROW {
if (&__a != this) { // 避免自我赋值
delete _M_ptr;
_M_ptr = __a.release(); // 释放了右值,保持左值。虽然实现了赋值操作,但是没有实现赋值语义。上面的复制同样没有实习那赋值语义。
}
return *this;
}
template <class _Tp1>
auto_ptr& operator=(auto_ptr<_Tp1>& __a) __STL_NOTHROW {
if (__a.get() != this->get()) {
delete _M_ptr;
_M_ptr = __a.release();
}
return *this;
}
~auto_ptr() { delete _M_ptr; }
// 智能指针一般都要重载"*"和"->"操作符
_Tp& operator*() const __STL_NOTHROW {
return *_M_ptr;
}
_Tp* operator->() const __STL_NOTHROW {
return _M_ptr;
}
_Tp* get() const __STL_NOTHROW {
return _M_ptr;
}
_Tp* release() __STL_NOTHROW {
_Tp* __tmp = _M_ptr;
_M_ptr = 0;
return __tmp;
}
void reset(_Tp* __p = 0) __STL_NOTHROW {
if (__p != _M_ptr) {
delete _M_ptr;
_M_ptr = __p;
}
}
};
auto_ptr<int> ptr(p);
auto_ptr<int> ptr2(p);
ptr = ptr2; // ptr2会失效!
auto_ptr的一些缺陷:
1. auto_ptr不能共享所有权,即不要让两个auto_ptr指向同一个对象。资源管理类本生没有复制和赋值语义,但是auto_ptr还是实现了,这样会析构掉右值。
2. auto_ptr不能指向数组,因为auto_ptr在析构的时候只是调用delete,而数组应该要调用delete[]。
3. auto_ptr只是一种简单的智能指针,没有实现引用计数。如有特殊需求,需要使用其他智能指针,比如share_ptr。
4. auto_ptr不能作为容器对象,STL容器中的元素经常要支持拷贝,赋值等操作,在这过程中auto_ptr会传递所有权,那么source与sink元素之间就不等价了。
unique_ptr学习
- std::unique_ptr实现了独享所有权的语义。这点它和auto_tr是一脉相承的。即不允许两个unique_ptr指向同一个对象,因为第一个对象用new T()返回的指针初始化之后,第二个对象无法拿到之前的指针,想共享只能通过第一个指针的复制。可以它并不支持。
- 它无法复制到其他 unique_ptr,无法通过值传递到函数,也无法用于需要副本的任何标准模板库 (STL) 算法。语法上就不可行!auto_ptr语法上可行的,但是还是会析构右值。其实没意义,所以unique_ptr改进之后无法复制。
- 只能移动unique_ptr。这意味着,内存资源所有权将转移到另一 unique_ptr,并且原始 unique_ptr 不再拥有此资源。
std::unique_ptr实现了独享所有权的语义。一个非空的std::unique_ptr总是拥有它所指向的资源。转移一个std::unique_ptr将会把所有权也从源指针转移给目标指针(源指针被置空)。拷贝一个std::unique_ptr将不被允许,因为如果你拷贝一个std::unique_ptr,那么拷贝结束后,这两个std::unique_ptr都会指向相同的资源,它们都认为自己拥有这块资源(所以都会企图释放)。因此std::unique_ptr是一个仅能移动(move_only)的类型。当指针析构时,它所拥有的资源也被销毁。默认情况下,资源的析构是伴随着调用std::unique_ptr内部的原始指针的delete操作的。
实例1 unique_ptr的使用
#include <memory>
struct Point {
int x;
int y;
Point() : x(0), y(0) { std::cout << "Point() called." << std::endl; }
Point(int a, int b) : x(a), y(b) { std::cout << "Point(int, int) called." << std::endl; }
~Point() { std::cout << "~Point() called." << std::endl; }
};
int main( void ){
std::unique_ptr<Point> p(new Point(3,4));
std::cout << p->x << " , " << p->y << std::endl;
return 0;
}
/*
Point(int, int) called.
3 , 4
~Point() called.
*/
实例2 unique_ptr移动语义
移动语义意味着要转移所有权!
// unique_ptr::operator->
#include <iostream>
#include <memory>
struct Point {
int a;
int b;
Point() { a = 0; b = 0; std::cout << "Point() called." << std::endl; }
Point(int x, int y) { a = x; b = y; std::cout << "Point(int,int) called." << std::endl; }
~Point() { std::cout << "~Point() called." << std::endl; }
};
int main () {
std::unique_ptr<Point> p (new Point(10,20)); // raii
std::unique_ptr<Point> q; // 没有触发raii,因为没有申请资源,所以也就没有构造。
p->a = 10;
p->b = 20;
q = std::move(p);
if (p) std::cout << "p: " << p->a << ' ' << p->b << '\n';
if (q) std::cout << "q: " << q->a << ' ' << q->b << '\n';
return 0;
}
/*
Point(int,int) called.
q: 10 20
~Point() called.
*/
实例3 unique_ptr可以存储在容器中
这点auto_ptr是禁止的。
// unique_ptr::operator->
#include <iostream>
#include <memory>
#include <vector>
struct Point {
int a;
int b;
Point() { a = 0; b = 0; std::cout << "Point() called." << std::endl; }
Point(int x, int y) { a = x; b = y; std::cout << "Point(int,int) called." << std::endl; }
~Point() { std::cout << "~Point() called." << std::endl; }
};
int main () {
std::unique_ptr<Point> p(new Point(10,20));
std::unique_ptr<Point> q(new Point(1,2));
p->a = 1;
p->b = 2;
q->a = 3;
q->b = 4;
std::vector< std::unique_ptr<Point> > vec;
vec.push_back( std::move( p ) ); // 必须使用move语义
vec.push_back( std::move( q ) ); // 必须使用move语义
int sz = vec.size();
for( int i = 0; i < sz; ++i ){
std::cout << vec[i]->a << "," << vec[i]->b << std::endl;
}
std::cout << "After vector :" << std::endl;
if( p ) std::cout << p->a << "," << p->b << std::endl;
if( q ) std::cout << q->a << "," << q->b << std::endl;
return 0;
}
/*
Point(int,int) called.
Point(int,int) called.
1,2
3,4
After vector :
~Point() called.
~Point() called.
*/
实例4 unique_ptr可以管理动态数组
由于智能指针就封装了一个指针,所以,存储的还是一个动态数组的首地址指针。用法和int* arr = new int[maxn];arr用法一样。arr[i]此时关联的就是对象。
// unique_ptr::operator->
#include <iostream>
#include <memory>
#include <vector>
struct Point {
int a;
int b;
Point() { a = 0; b = 0; std::cout << "Point() called." << std::endl; }
Point(int x, int y) { a = x; b = y; std::cout << "Point(int,int) called." << std::endl; }
~Point() { std::cout << "~Point() called." << std::endl; }
};
int main () {
const int maxn = 8;
std::unique_ptr< Point[] > arr( new Point[maxn] );
for( int i = 0; i < maxn; ++i ){
std::cout << i << ":" << arr[i].a << "," << arr[i].b << std::endl;
}
return 0;
}
/*
Point() called.
Point() called.
Point() called.
Point() called.
Point() called.
Point() called.
Point() called.
Point() called.
0:0,0
1:0,0
2:0,0
3:0,0
4:0,0
5:0,0
6:0,0
7:0,0
~Point() called.
~Point() called.
~Point() called.
~Point() called.
~Point() called.
~Point() called.
~Point() called.
~Point() called.
*/
总结下unique_ptr和auto_ptr的区别:
1. 这二者都实现了独享语义,不支持共享。这是他们的共同点
2. 最大的区别在于unique_ptr支持move语义,并且同时禁止了copy语义。因为独占类的语义就是禁止copy语义的。也就是它有move constructor,而后者没有。
3. unique_ptr由于支持move语义,禁止了copy语义。作为独占资源管理类,本生就应该禁止copy语义。但是auto_ptr则没有禁止,不合理。并且还实现了move语义,使得可以作为容器元素。而后者不行。
4. 最后,前者支持动态数组,后者不支持。
shared_ptr学习
shared_ptr作为智能指针的一种,主要的用途就是来管理动态内存(dynamic memory)。我们知道如果用new和delete来管理会带来很大的问题。他们的行为类似常规指针,重要的区别他们负责自动释放所指向的对象!
shared_ptr和unique_ptr的本质区别在于:前者是基于共享语义实现的,而后者是基于独占语义实现的。这体现在他们管理底层指针的方式,shared_ptr允许多个指针指向同一个对象,并且还提供了引用计数来管理owner的数量。unique_ptr则独占所指向的对象。语义的不同体现在是否可以有复制和赋值等语义。
实例1 shared_ptr的初始化
见如下代码:初始化有两种方式。一种是采用裸指针的形式,一种是采用make_shared的方法。他们两之间在性能上存在区别,如果使用裸指针的方式,则会发生两次动态内存申请,一次是new,一次是shared_ptr为资源管理对象分配的。这两次的内容不一样,但是总归还是两次。c++ primer建议的是后者make_shared.
此函数在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr.
采用make_shared可以一次性分配足够的内存,用来保存这个资源管理者和这个新建对象。
// unique_ptr::operator->
#include <iostream>
#include <memory>
#include <vector>
struct Point {
int a;
int b;
Point() { a = 0; b = 0; std::cout << "Point() called." << std::endl; }
Point(int x, int y) { a = x; b = y; std::cout << "Point(int,int) called." << std::endl; }
~Point() { std::cout << "~Point() called." << std::endl; }
};
int main () {
std::shared_ptr<Point> p; // 默认初始化智能指针保存一个空指针
std::cout << p << std::endl; // 输出0
std::shared_ptr<int> p1 = std::make_shared<int>(8); // make_shared形式
std::shared_ptr<int> p11(new int(8)); // 裸指针形式
//std::shared_ptr<int> p12 = new int(8); // illegal 禁止隐式转换
std::cout << *p1 << std::endl;
std::cout << *p11 << std::endl;
return 0;
}
实例2 shared_ptr的拷贝和赋值
我们可以认为每个shared_ptr都有一个关联的计数器,通常称为引用计数。它表明了拥有堆内存的owner数量。
- 无论何时我们拷贝一个shared_ptr,引用计数都会增加
- 当我们给shared_ptr赋予一个新值或是shared_ptr析构(离开作用域),左值的计数器都会减少。
// unique_ptr::operator->
#include <iostream>
#include <memory>
#include <vector>
struct Point {
int a;
int b;
Point() { a = 0; b = 0; std::cout << "Point() called." << std::endl; }
Point(int x, int y) { a = x; b = y; std::cout << "Point(int,int) called." << std::endl; }
~Point() { std::cout << "~Point() called." << std::endl; }
};
int main () {
std::shared_ptr<Point> p1 = std::make_shared<Point>();
std::cout << p1->a << "," << p1->b << std::endl;
std::cout << "p1 ref count: " << p1.use_count() << std::endl;;
std::shared_ptr<Point> p2 = p1; // copy semantics
std::cout << "p2 ref count: " << p2.use_count() << std::endl;;
std::shared_ptr<Point> p3 = std::make_shared<Point>(10,20);
std::cout << p3->a << "," << p3->b << std::endl;
std::cout << "p3 ref count: " << p2.use_count() << std::endl;;
p2 = p3; // assignment semantics
std::cout << "p1 ref count: " << p1.use_count() << std::endl;;
std::cout << "p2 ref count: " << p2.use_count() << std::endl;;
std::cout << "p3 ref count: " << p2.use_count() << std::endl;;
return 0;
}
/*
Point() called.
0,0
p1 ref count: 1
p2 ref count: 2
Point(int,int) called.
10,20
p3 ref count: 2
p1 ref count: 1
p2 ref count: 2
p3 ref count: 2
~Point() called.
~Point() called.
*/
实例3 shared_ptr指向动态数组
我们先来看看shared_ptr模仿unique_ptr指向数组的形式。
// unique_ptr::operator->
#include <iostream>
#include <memory>
#include <vector>
struct Point {
int a;
int b;
Point() { a = 0; b = 0; std::cout << "Point() called." << std::endl; }
Point(int x, int y) { a = x; b = y; std::cout << "Point(int,int) called." << std::endl; }
~Point() { std::cout << "~Point() called." << std::endl; }
};
int main () {
int maxn = 8;
std::unique_ptr< Point[] > p( new Point[maxn] ); // this will correctly call delete []
//std::shared_ptr< Point[] > arr( new Point[maxn] ); // illegal call delete
return 0;
}
编译时如果不注释,无法通过。也就是说shard_ptr无法采用类似与unique_ptr指向动态数组的形式指向动态数组。
原因在哪?
这是因为前者在对象析构时会调用delete [] ,所以可以析构动态数组空间。但是后者调用delete,只能析构单个元素。如何解决
可以通过定制自己的析构函数对象来实现!
// unique_ptr::operator->
#include <iostream>
#include <memory>
#include <vector>
struct Point {
int a;
int b;
Point() { a = 0; b = 0; std::cout << "Point() called." << std::endl; }
Point(int x, int y) { a = x; b = y; std::cout << "Point(int,int) called." << std::endl; }
~Point() { std::cout << "~Point() called." << std::endl; }
};
template<class T>
struct arr_deleter {
void operator() (T * p) {
delete [] p;
}
};
int main () {
int maxn = 8;
std::shared_ptr<int> arr( new int[maxn], std::default_delete<int>() ); // 系统析构
std::shared_ptr< int > arr1( new int[maxn], arr_deleter< int >() ); // 自定义析构
std::shared_ptr< Point > arr2( new Point[maxn], std::default_delete< Point[] >() ); // 系统系统
std::shared_ptr< Point > arr3( new Point[maxn], arr_deleter< Point >() ); // 自定义析构
return 0;
}
/**
Point() called.
Point() called.
Point() called.
Point() called.
Point() called.
Point() called.
Point() called.
Point() called.
~Point() called.
~Point() called.
~Point() called.
~Point() called.
~Point() called.
~Point() called.
~Point() called.
~Point() called.
*/
上面代码自己定义了deleter,也可以调用系统的deleter。但是,这段代码还是坑很多啊!
- 首先,对于shared_ptr而言,如果要指向数组,必须给出deleter,不管是系统的还是默认的。
- 其次,unique_ptr和shared_ptr对于指针初始化的形式并不一样。unique_ptr是这种形式:
unique_ptr<Point[]> arr(new Point[maxn]);里面给出的是数组类型。而shared_ptr则需要给出每个元素的类型,及时是指向数组的,这点和之前的用法一致。shared_ptr<Point> arr( new Point[maxn], ... ); - shared_ptr的deleter的给出形式也是要区分是调用系统的还是调用自己定制的。如果是系统的,deleter的参数需要给出数组类型,如果是自己定制,则还是给出元素类型的。
std::shared_ptr< Point > arr2( new Point[maxn], std::default_delete< Point[] >() ); // 系统系统
std::shared_ptr< Point > arr3( new Point[maxn], arr_deleter< Point >() ); // 自定义析构
这里小结下:如果要用只能指针指向数组,尽量使用unique_ptr,因为通常不会具备共享语义。如果使用shared_ptr,注意模板参数类型以及deleter模板的参数类型。
实例4 shared_ptr实现共享语义
由于shared_ptr作为智能指针的一种,主要是面对堆空间进行资源的自动管理。c++ primer给出了使用动态内存的三个原因:
- 程序不知道自己要使用多少对象
- 程序不知道所需对象的准确类型
- 程序需要在多个对象之间共享数据
c++primer给出了这样的一种场景:我们希望定义一个Blob,保存一组元素。但是,与容器不一样,我们希望Blob对象具有共享的语义。就是说在进行复制和赋值的时候可以进行共享。
当然,很简单的想法是通过标准容器库来实现,可是这个办法虽然简单,但是标准容器库是不支持共享语义的。所以,不能直接使用容器去保存。
这时,用到上面说的情况,当我们希望实现共享语义的时候,可以考虑使用动态内存。为了避免自己实现控制资源的回收操作,我们采用智能指针来管理这一块动态内存。由于使用共享语义,我们可以采用shared_ptr来管理。那么,这块动态内存由于要存储一组元素,我们可以用标准库容易来实现它。所以,最终的方案就是用shared_ptr来管理动态内存,这个动态内存用标准库容器来实现。所以,借助这两个工具,省掉了我们很多的麻烦。容器帮我们存储一组元素,智能指针帮我们开辟堆空间、自动回收资源、并且实现共享语义。
#include <iostream>
#include <vector>
#include <string>
#include <initializer_list>
#include <memory>
class StrBlob {
public:
StrBlob() {}
StrBlob( const std::initializer_list< std::string >& il ) : data(il) {}
private:
std::vector< std::string > data;
};
class StrBlob1 { // 实现共享语义
public:
StrBlob1() : data( std::make_shared< std::vector< std::string> >() ) {} // 也可以不调用make_shared,但是由于c++ primer建议使用make_shared。我们采用这种方法。
StrBlob1( const std::initializer_list< std::string >& il ) : data( std::make_shared< std::vector< std::string > >(il) ) {}
private:
std::shared_ptr< std::vector<std::string> > data;
};
int main() { return 0; }
完整代码如下:如果写成泛型,那就是一个共享的容器!!!
#include <iostream>
#include <vector>
#include <string>
#include <initializer_list>
#include <memory>
#include <stdexcept>
class StrBlob {
public:
StrBlob() : data( std::make_shared< std::vector< std::string> >() ) {}
StrBlob( const std::initializer_list< std::string >& il ) : data( std::make_shared< std::vector< std::string > >(il) ) {}
public:
typedef std::vector<std::string>::size_type size_type;
typedef std::vector<std::string>::iterator iterator;
public:
size_type size() const { return data->size(); }
bool empty() const { return data->empty(); }
iterator begin() { return data->begin(); }
iterator end() { return data->end(); }
void push_back( const std::string& s ) { data->push_back(s); }
void pop_back() {
check( 0, "empty strblob" );
data->pop_back();
}
public:
std::string& front() {
check( 0, "empty strblob" );
return data->front();
}
const std::string& front() const {
check( 0, "empty strblob" );
return data->front();
}
std::string& back() {
check( 0, "empty strblob" );
return data->back();
}
const std::string& back() const {
check( 0, "empty strblob" );
return data->back();
}
public:
std::string& operator[]( size_type i ) {
check( i, "out of range" );
return (*data)[i];
}
const std::string& operator[]( size_type i ) const {
check( i, "out of range" );
return (*data)[i];
}
private:
void check( size_type i, const std::string& err ) const {
if( i >= data->size() )
throw std::out_of_range( err );
}
private:
std::shared_ptr< std::vector<std::string> > data;
};
int main() {
StrBlob str_list = { "kang", "lucy", "xin" };
int sz = str_list.size();
std::cout << "str_list:" << std::endl;
for( int i = 0; i < sz; ++i ){
std::cout << str_list[i] << std::endl;
}
StrBlob str_list_copy = str_list;
sz = str_list_copy.size();
std::cout << "str_list_copy:" << std::endl;
for( int i = 0; i < sz; ++i ){
std::cout << str_list_copy[i] << std::endl;
}
str_list_copy.push_back( "hello,world" );
std::cout << "----------after copy---------- " << std::endl;
sz = str_list.size();
std::cout << "str_list:" << std::endl;
for( int i = 0; i < sz; ++i ){
std::cout << str_list[i] << std::endl;
}
sz = str_list_copy.size();
std::cout << "str_list_copy:" << std::endl;
for( int i = 0; i < sz; ++i ){
std::cout << str_list_copy[i] << std::endl;
}
return 0;
}
/*
str_list:
kang
lucy
xin
str_list_copy:
kang
lucy
xin
----------after copy----------
str_list:
kang
lucy
xin
hello,world
str_list_copy:
kang
lucy
xin
hello,world
*/
小小总结以下:
- 使用get()方法只有一种情形,就是需要传递给一个不能使用shared_ptr的函数。
- 使用get()方法获得指针之后,一定要小心,当最会一个shared_ptr销毁之后,这个指针就会实效。
- shared_ptr如果要管理new T[maxn]的资源,记住传递给它一个deleter.
实例5 shared_ptr循环引用的问题
首先看一段代码:
// shared_ptr循环引用
#include <memory>
#include <string>
#include <iostream>
class Woman;
class Man {
public:
Man() { std::cout << "Man() called." << std::endl; }
explicit Man( const std::string& s ) : gender(s) { std::cout << "Man(const std::string&) called." << std::endl; }
~Man() { std::cout << "~Man() called." << std::endl; }
void set_wife( std::shared_ptr< Woman >& prhs ) {
pwife = prhs;
}
private:
std::string gender;
std::shared_ptr< Woman > pwife;
};
class Woman {
public:
Woman() { std::cout << "Woman" << std::endl; }
explicit Woman( const std::string& s ) : gender(s) { std::cout << "Woman(const std::string&) called." << std::endl; }
~Woman() { std::cout << "~Woman() called." << std::endl; }
void set_husband( std::shared_ptr< Man >& prhs ){
phusband = prhs;
}
private:
std::string gender;
std::shared_ptr< Man > phusband;
};
int main() {
std::shared_ptr< Man > pman = std::make_shared<Man>("male");
std::shared_ptr< Woman > pwoman = std::make_shared<Woman>("female");
pman->set_wife( pwoman );
pwoman->set_husband( pman );
return 0;
}
/*
Man(const std::string&) called.
Woman(const std::string&) called.
*/
通过执行上面的代码,我们发现Man和Woman的析构函数都没有执行!也就是说堆内存没有得到回收,造成内存泄露!!!这不是我们希望看到的。
下面我们来分析以下,为什么会产生这种情况!
首先,我们先来分析类的构成。Man的内部存在一个指向Woman的shared_ptr,Woman的内部存在一个指向Man的shared_ptr。对于这种情况,当两个类的内部分别有指向彼此的shared_ptr的时候,此时这两个类构成循环引用,这回导致这两个类都无法正常的析构,造成内存泄露!
好了,那么这是为什么呢?我们继续来分析,主函数中pman和pwomen分别指向了两个Man和Women的两个堆对象。下面他们分别设置了自己内部的智能指针,pman内部的pwife指向了Women堆对象,pwomen内部的phusband指向了Man堆对象。现在来分析以下,对于Man堆对象,有pman, pwoman.phusband两个指针关联。对于Woman堆对象,有pwomen, pman.pwife两个堆对象关联。好,现在我们开始来析构,和构造顺序相反。先析构pwomen(注意,pwomen虽然指向队对象,但是它自身是栈对象),它在析构的时候需要析构phusband,首先,它会–ref_count. 此时,判断ref_count是否为0,显然ref_count现在为1,因为Man堆对象没有被析构。那么,不会继续析构它所指向的堆对象。从而,它的析构没有完成。现在,我们再析构pman,同理,它的内部有指向Woman堆对象的shared_ptr,但是由于Woman没有完成析构,所以,shared_ptr一样无法导致Woman析构。再换句话说,Man要析构,必须Woman先析构。可是Woman要析构,必须Man先析构,从而谁都无法析构。也即,构成循环引用的类都需要彼此的所指向的类先析构,自己才能析构,这回导致最后谁也析构不成,造成内存泄露。
那么,这个问题该如何解决呢?且看下一目的weak_ptr.
weak_ptr学习
c++ primer是这样形容的:
- weak_ptr是一种不控制所指向对象生存期的智能指针,它指向一个由shared_ptr管理的对象。(weak_ptr不能单独使用,要和shared_ptr一起使用,或者不使用)
- 将一个weak_ptr绑定到shared_ptr管理的对象,不会增加shared_ptr的引用计数。
- 一旦最后一个指向堆对象的shared_ptr被销毁,堆空间就会被释放。此时weak_ptr失效。(也就是说,及时weak_ptr指向堆对象,堆对象还是会被释放。)
- 由于weak_ptr所指向的对象可能失效,因此不能直接通过它访问,需要首先调用lock,判断weak_ptr所指向的对象是否存在。如果,存在,lock函数返回该对象的shared_ptr
简言之,shared_ptr和weak_ptr都实现了共享语义,前者是一种强共享(强引用),后者是一种弱共享(弱引用)。强弱的区别就在于是否增加资源的属主数量。
所以,上面的代码,修改如下:
#include <memory>
#include <string>
#include <iostream>
class Woman;
class Man {
public:
Man() { std::cout << "Man() called." << std::endl; }
explicit Man( const std::string& s ) : gender(s) { std::cout << "Man(const std::string&) called." << std::endl; }
~Man() { std::cout << "~Man() called." << std::endl; }
void set_wife( std::shared_ptr< Woman >& prhs ) {
pwife = prhs;
}
std::string get_gender() const { return gender; }
private:
std::string gender;
std::shared_ptr< Woman > pwife;
//std::weak_ptr< Woman > pwife;
};
class Woman {
public:
Woman() { std::cout << "Woman" << std::endl; }
explicit Woman( const std::string& s ) : gender(s) { std::cout << "Woman(const std::string&) called." << std::endl; }
~Woman() { std::cout << "~Woman() called." << std::endl; }
void set_husband( std::shared_ptr< Man >& prhs ){
phusband = prhs;
}
std::string get_gender() const { return gender; }
void print_husband_gender() {
std::shared_ptr<Man> ret = phusband.lock();
if( ret ){ // 检查有效性
std::cout << ret->get_gender() << std::endl;
}
}
private:
std::string gender;
//std::shared_ptr< Man > phusband;
std::weak_ptr< Man > phusband;
};
int main() {
std::shared_ptr< Man > pman = std::make_shared<Man>("male");
std::shared_ptr< Woman > pwoman = std::make_shared<Woman>("female");
pman->set_wife( pwoman );
pwoman->set_husband( pman );
return 0;
}
/*
Man(const std::string&) called.
Woman(const std::string&) called.
~Man() called.
~Woman() called.
*/
分析上面代码,发现对象正常析构。但是有一点疑惑是,栈对象的析构顺序不是和构造顺序相反嘛?怎么没有相反。
那是因为循环引用导致的,强引用的条件都是只有对方析构了,我才析构。所以,women引入若引用之后,由于不对Man引用计数增加,所以Man可以正常析构,之后才是Woman。当然,如果交换这二者的强弱关系,析构顺序交换。
小小总结下:weak_ptr主要是作为shared_ptr的若引用版本,辅助shared_ptr进行工作的。
shared_ptr安全性讨论
为了,保证循环引用问题讨论的一致性,我把shared_ptr的线程安全性放在此处讨论。主要参考了两位大神的博客。现在直接给出结论:
- shared_ptr的引用计数本生是安全无锁的!
问:引用计数 count++的时候,就是两条机器指令: read count,write count+1。如果两个线程同时增加shared_ptr的引用技术,应该会引起丢失一个引用计数的问题。在释放时,就会出现问题。
陈硕答:不会有问题,shared_ptr 的 count++ 是一个原子操作。在 x86 上是一条指令,lock xadd 或 lock incl
2 . 对象的读写则不是线程安全的,具体分为以下三种情况:
同一个shared_ptr对象可以被多线程同时读取。【A shared_ptrinstance can be “read” (accessed using only const operations)simultaneously by multiple threads.】
不同的shared_ptr对象可以被多线程同时修改(即使这些shared_ptr对象管理着同一个对象的指针)。【Different shared_ptr instances can be “written to”(accessed using mutable operations such as operator= or reset) simultaneouslyby multiple threads (even when these instances are copies, and share the samereference count underneath.) 】
任何其他并发访问的结果都是无定义的。【Any other simultaneous accesses result in undefined behavior.】
智能指针注意点
- 当指针不具有共享语义的时候,使用了shared_ptr。此时使用unique_ptr即可
- 既然shared_ptr是具有共享语义的,那么对于多线程访问的安全性如何保证!
- auto_ptr禁止使用,作为独占语义的智能指针,我们使用unique_ptr即可
- 关于shared_ptr,尽量使用make_shared来进行初始化。
- 智能指针在创建对象时,如果使用裸指针,应该确保裸指针之后都不会再使用。
- 关于shared_ptr,使用get方法获得裸指针是非常危险的行为!禁止!
- 关于shared_ptr,当使用shared_ptr指向动态数组的时候,需要定义自己的deleter.
- 在使用shared_ptr时形成循环引用,使用weak_ptr来解决这个问题。
- 关于unique_ptr,如果使用release方法之后,unique_ptr从销毁指针的责任中解脱出来,此时还需要手动销毁裸指针。
- 关于weak_ptr,使用的时候,必须调用lock方法来判断它的有效性。
智能指针是一种工具,具体来说是一种类型,用来管理动态内存(dynamic memory or heap memory)。当然,你也可以用new和delete来管理,但是后者比较容易出bug。智能指针的行为类似常规指针,重要的区别它负责自动释放所指向的对象。C++11提供的三种智能指针分别是unique_ptr,shared_ptr和weak_ptr。区别是,前者实现了独占语义,后两者则实现了共享语义。语义上的具体体现就是是否允许复制和赋值的语义。在底层实现上是否允许多个指针指向同一个对象。shared_ptr和weak_ptr的区别是,强引用和弱引用的区别。具体表现为是否增加所指向对象的ref count.
智能指针实现
这一目记录了我对智能指针的学习。说一下我的学习过程吧,上一周还是不错的,基本上c++一些比较重要的问题都搞定了。最后还实现了智能指针作为项目实现,感觉这一块编程还是有提高的。
说一下智能指针的实现过程,我觉得我还是牛逼的,因为我没有看其他人的任何实现。就是单纯的学习了代理模式之后实现的智能指针,我的代码原创性还是比较高的。我准备先说一下代理模式,然后给出自己的实现,以及实现过程中的一些问题。
代理模式
先给出定义:
为其他对象提供一种代理以控制对这个对象的访问.
举个例子:
比如说下载服务器,每次向下载服务器发起请求之后,后者都需要去磁盘准备数据然后发送给客户端。哪怕是重复的请求,下载服务器也会这么做。但是,如果增加一个代理,代理服务器每次先判断客户端的请求的资源之前是否请求过,如果没有,则向下载服务器进行请求,然后,代理服务器回送给客户端,并进行缓存。下次如果有同样的资源请求,则直接会送给客户端。UML如下:
- Proxy
- 保存一个引用使得代理可以访问实体。若RealSubject和Subject的接口相同,Proxy会引用Subject,就相当于在代理类中保存一个Subject指针,该指针会指向RealSubject;
- 提供一个与Subject的接口相同的接口,这样代理就可以用来替代实体;
- 控制对实体的存取,并可能负责创建和删除它;
- 其它功能依赖于代理的类型,例如:
远程代理负责对请求及其参数进行编码,并向不同地址空间中的实体发送已编码的请求;
虚代理可以缓存实体的附加信息,以便延迟对它的访问;
保护代理检查调用者是否具有实现一个请求所必须的访问权限。- Subject:定义RealSubject和Proxy的共用接口,这样就在任何使用RealSubject的地方都可以使用Proxy;
- RealSubject:定义Proxy所代理的实体。
为什么要设计一个subject出来,因为在client看了,realsubject以及proxysubject,都是提供服务的。所以,可以抽象出一个公共的接口。
优点
对外部提供同一个的接口方法,代理类在对对象操作的同时,可以附加别的操作,从而在不影响外部调用的情况下,对系统进行扩展。
代码
// Server.h
#ifndef SERVER_H_
#define SERVER_H_
class Server {
public:
Server() {}
virtual ~Server() {}
public:
virtual void request() = 0;
};
#endif
// RealServer.h
#ifndef REAL_SERVER_H_
#define REAL_SERVER_H_
#include <iostream>
#include "Server.h"
class RealServer : public Server {
public:
RealServer() {};
virtual ~RealServer() {}
public:
virtual void request() {
std::cout << "RealServer request." << std::endl;
}
};
#endif
// ProxyServer.h
#ifndef PROXY_SERVER_H_
#define PROXY_SERVER_H_
#include <iostream>
#include <memory>
#include "Server.h"
#include "RealServer.h"
class ProxyServer : public Server {
public:
ProxyServer() {}
virtual ~ProxyServer() {}
public:
virtual void request() {
std::cout << "ProxyServer request." << std::endl;
// construct a real server
if( !p_realserver ){
p_realserver = std::unique_ptr<RealServer>( new RealServer() ) ; // move semantics
}
p_realserver->request();
}
private:
std::unique_ptr< RealServer > p_realserver;
};
#endif
// main.cpp
#include <memory>
#include <iostream>
#include "ProxyServer.h"
int main( void ) {
ProxyServer proxy;
proxy.request();
return 0;
}
智能指针-代理模式实现
思路
我自己在最初设计的时候,先没想过为什么要用代理。我就只是想用raii对指针进行封装即可,这样一样可以实现对资源的回收。但是,简单的raii无法实现共享的语义。看下面的代码:
SmartPointer<int> p(new int());
SmartPointer<int> p1(p) ;
SmartPointer<int> q(new int());
p = q;
此时,会导致p的析构,但是p1对这一切一无所知。所以,简单的raii无法实现共享的语义。unique_ptr应该可以这样实现。但是shared_ptr肯定是不行的。那么,此时,引入引用计数的思路,为每一个pointer关联一个cnt。这样我把他们两进行封装,如下:
template<class T>
class Pointer{ public:...private: int cnt; T* ptr; };这么写乍一看也没有问题,我在复制和赋值同时修改cnt即可,可是对于上面的那种情形,p=q执行之后,p的引用计数是可以减少,但是p1的引用计数还是没有减少,它对这一切任然一无所知。
问题的本质出在,cnt是一个栈变量,它没有办法共享。考虑到动态内存的第三个作用就是可以实现共享语义,那么,我果断把Pointer类开到了堆空间上面,此时,我再引入一个SmartPointer的代理类,代理Pointer类的行为即可。这就是整体的设计思路。
Pointer类相当于完成了对堆空间的管理以及引用计数,SmartPointer类则对外代理了普通指针的行为。
当然,这里的理解大家可能有点不同。因为我实现之后看了比人的方法,基本上都是把SmartPointer实现了slot模型,一个指向对堆空间,一个指向ref的堆空间。而我的这种实现方法则被称为辅助类。大家出发的角度不同吧。
代码
// Ptr.h
#ifndef PTR_H_
#define PTR_H_
// Abstract Pointer Class
template<class T>
class Ptr {
public:
// Deference object
virtual T& operator*() const = 0 ;
// Defence object member
virtual T* operator->() const = 0;
};
#endif
// Pointer.h
#ifndef POINTER_H_
#define POINTER_H_
#include <iostream>
#include "Ptr.h"
// Real Pointer Class
template<class T>
class Pointer : public Ptr<T> {
public:
Pointer() : ptr(NULL), ref_cnt(0) {}
explicit Pointer( T* pp ) : ptr(pp), ref_cnt(1) { std::cout << "Pointer(int*) called." << std::endl; }
~Pointer() {
if(ptr){
delete ptr;
ptr = NULL;
std::cout << "~Pointer() called." << std::endl;
}
}
public:
void add_ref() { ++ref_cnt; }
void release_ref() { --ref_cnt; }
int get_ref() const { return ref_cnt; }
T* get() const { return ptr; }
public:
virtual T& operator*() const { return *ptr; }
virtual T* operator->() const { return ptr; }
private:
T* ptr;
int ref_cnt;
};
#endif
// SmartPointer.h
#ifndef SMARTPOINTER_H_
#define SMARTPOINTER_H_
#include <cassert>
#include "Ptr.h"
#include "Pointer.h"
// Proxy pointer class
template<class T>
class SmartPointer : public Ptr<T> {
public:
SmartPointer() : ppointer(NULL) {}
explicit SmartPointer( T* pp ) {
ppointer = new Pointer<T>( pp );
assert(ppointer);
}
SmartPointer( const SmartPointer& rhs ) {
ppointer = rhs.ppointer;
// add ref
ppointer->add_ref();
}
SmartPointer& operator=( const SmartPointer& rhs ) {
if( this != &rhs ){
if( !ppointer ){
ppointer = rhs.ppointer;
// add ref
ppointer->add_ref();
}
else{
// release ref
ppointer->release_ref();
if(!ppointer->get_ref() )
delete ppointer;
// add ref
ppointer = rhs.ppointer;
ppointer->add_ref();
}
}
return *this;
}
~SmartPointer() {
if( ppointer ){
// release ref
ppointer->release_ref();
if(!ppointer->get_ref()){
delete ppointer;
ppointer = NULL;
}
}
}
public:
virtual T& operator*() const {
assert(ppointer);
return ppointer->operator*();
}
virtual T* operator->() const {
assert(ppointer);
return ppointer->operator->();
}
public:
T* get() const {
if( ppointer )
return ppointer->get();
else
return NULL;
}
int use_count() const {
if( ppointer )
return ppointer->get_ref();
else
return 0;
}
public:
operator bool() const {
return ppointer != NULL;
}
bool operator==( const T* p_rhs ) const {
return ppointer->get() == p_rhs;
}
bool operator==( const SmartPointer<T>& rhs ) const {
return ppointer->get() == rhs.get();
}
bool operator!=( const T* p_rhs ) const {
return ppointer->get() != p_rhs;
}
bool operator!=( const SmartPointer<T>& rhs ) const {
return ppointer->get() != rhs.get();
}
private:
Pointer<T>* ppointer;
};
#endif
#include <iostream>
#include "SmartPointer.h"
void test_case1() {
SmartPointer< int > p1( new int(10) ); // constructor
std::cout << " p1->use_count " << p1.use_count() << std::endl;
SmartPointer< int > p2(p1); // copy constructor
std::cout << " p2->use_count " << p2.use_count() << std::endl;
SmartPointer< int > p3; // default constructor
std::cout << " p3->use_count " << p3.use_count() << std::endl;
p3 = p1; // assignment operator
std::cout << "before modify : " << std::endl;
std::cout << *p1 << std::endl;;
std::cout << *p2 << std::endl;;
std::cout << *p3 << std::endl;;
*p2 = 8;
std::cout << "after modify : " << std::endl;
std::cout << *p1 << std::endl;;
std::cout << *p2 << std::endl;;
std::cout << *p3 << std::endl;;
SmartPointer<int> p4( new int(32) );
std::cout << "*p4 = " << *p4 << std::endl;
p4 = p1;
}
void test_case2(){
SmartPointer<int> p1(new int(8));
SmartPointer<int> p2;
if( p1 ) std::cout << *p1 << std::endl;
if( p2 ) std::cout << *p2 << std::endl;
if( p1 == p2 ) std::cout << "p1==p2" << std::endl;
if( p1 != p2 ) std::cout << "p1!=p2" << std::endl;
SmartPointer<int> p3(p1);
if( p1 == p3 ) std::cout << "p1==p3" << std::endl;
if( p1 != p3 ) std::cout << "p1!=p3" << std::endl;
}
int main( void ) {
//test_case1();
test_case2();
return 0;
}
上面的代码也并不是没有问题,比如,上面的代码只能管理普通指针,而不能管理动态数组的指针,主要是delete ptr。这里不是销毁动态数组的形式,按照shared_ptr的做法,可以传入一个deleter解决这个问题。
后记
这两天又复习了下智能指针,再实现部分有了不太一样的理解。对于之前的实现,解读有不同。分别说下。
- Ptr.h
这个类没有什么好说的,实现最基本的接口。也就是指针的两个行为。
#ifndef PTR_H_
#define PTR_H_
template< class T >
class Ptr {
public:
// dereference object
virtual T& operator*() = 0;
// dereference object member
virtual T* operator->() = 0;
};
#endif
- Pointer.h
1.这个类要好好说下,首先这个类实现了对动态资源最底层的管理。相当于最底层的资源管理类。由于上层是实现共享的语义,所以底层的资源是没有复制和赋值的语义。所以,资源管理类也没有复制和赋值的语义。显然,复制构造和赋值函数要禁止。这点很清楚。
2.上层提供了共享语义,所以肯定是要支持复制和赋值语义的,但是具体实现并不是拷贝和赋值资源。而是通过引用计数实现。所以,引用计数本质上应该放在上层。但是,由于上层的都是栈对象,所以,引用技术如果在上层实现,没有办法共享。那么,把引用技术也放入资源管理层。表示引用资源的数量。然后这个资源管理类开在堆空间上实现共享。
3.上面的思路说清楚了。再说下最初的思路,最初我是想直接在资源管理类里面管理资源和引用技术。那么,此时资源管理类变成栈对象,应用计数没法共享。所以,我把资源管理类开在了堆对象上面。再提供一个栈对象的上层。这是最初的思路。其实,比较常见的思路是在资源管理这一层,把引用计数分出去,实现一个slot模型也是可以的。
目前,就还是按照我最初的思路设计。Pointer类实现底层资源的管理,上层的引用计数也封装到Pointer里面,向高层提供支持。然后把Pointer类开到堆空间上面。实现共享。
#ifndef POINTER_H_
#define POINTER_H_
#include <iostream>
#include "Ptr.h"
template< class T >
class Pointer : public Ptr<T> {
public:
Pointer() : ptr_(NULL), ref_cnt_(0) {}
explicit Pointer( T* pp ) : ptr_(pp), ref_cnt_(1) {}
virtual ~Pointer() { release_ref(); }
public:
void add_ref() { ref_cnt_++; }
void release_ref() {
ref_cnt_--;
if(!ref_cnt_){
delete ptr_;
ptr_ = NULL;
}
}
int get_ref() const { return ref_cnt_; }
T* get() const { return ptr_; }
public:
T& operator*() { return *ptr_; }
T* operator->() { return ptr_; }
private:
// No copying allowed
Pointer( const Pointer& );
void operator=( const Pointer& );
private:
T* ptr_;
int ref_cnt_;
};
#endif
- SmartPointer.h
底层提供了资源控制类,Pointer类当中含有资源的句柄,以及资源的属主(owner)数量。提供了ref();unref();方法给上层应用来控制资源的共享语义。所以上层要充分利用底层的方法。由于要实现共享语义,所以,底层Pointer对象要开成堆对象。最后还要提供判空以及判断是否相等的接口。
#ifndef SMART_POINTER_H_
#define SMART_POINTER_H_
#include <iostream>
#include <assert.h>
#include "Ptr.h"
#include "Pointer.h"
template< class T >
class SmartPointer : public Ptr<T> {
public:
SmartPointer() : ppointer_(NULL) {}
explicit SmartPointer( T* pp ) {
ppointer_ = new Pointer<T>(pp);
assert(ppointer_);
}
SmartPointer( const SmartPointer<T>& rhs ){
ppointer_ = rhs.ppointer_;
assert(ppointer_);
ppointer_->add_ref();
}
void operator=( const SmartPointer<T>& rhs ){
if( this != &rhs ){
assert(ppointer_);
ppointer_->release_ref();
ppointer_ = rhs.ppointer_;
assert(ppointer_);
ppointer_->add_ref();
}
}
virtual ~SmartPointer(){
if(ppointer_){
ppointer_->release_ref();
}
}
public:
T& operator*() {
assert( ppointer_ );
return ppointer_->operator*();
}
T* operator->() {
assert( ppointer_ );
return ppointer_->operator->();
}
public:
T* get() const {
assert( ppointer_ );
return ppointer_->get();
}
int use_count() const {
assert( ppointer_ );
return ppointer_->get_ref();
}
public:
operator bool() const { return ppointer_ != NULL; }
bool operator==( const T* p_rhs ) const {
assert( ppointer_ );
return ppointer_->get() == p_rhs;
}
bool operator==( const SmartPointer<T>& rhs ) const {
assert(ppointer_);
return ppointer_->get() == rhs.get();
}
bool operator!=( const T* p_rhs ) const {
return !operator==(p_rhs);
}
bool operator!=( const SmartPointer<T>& rhs ) const {
return !operator==(rhs);
}
private:
Pointer<T>* ppointer_;
};
#endif
- main.cc
这个文件提供方法的测试函数。
#include "Pointer.h"
#include "SmartPointer.h"
struct Point{
int x;
int y;
Point(){ std::cout << "Point() called." << std::endl; }
Point( int xx, int yy ) : x(xx), y(yy) { std::cout << "Point(int,int)" << std::endl; }
~Point(){ std::cout << "~Point() called." << std::endl; }
};
void test_Pointer(){
Pointer<Point> p;
Pointer<Point> p1(new Point());
p1->x = 5;
p1->y = 3;
}
void test_SmartPointer1(){
SmartPointer<Point> p1( new Point(3, 4) );
std::cout << p1->x << "," << p1->y << std::endl;
SmartPointer<Point> p2(p1);
std::cout << p2->x << "," << p2->y << std::endl;
std::cout << "p1 use count :" << p1.use_count() <<std::endl;
std::cout << "p2 use count :" << p2.use_count() <<std::endl;
SmartPointer<Point> p3( new Point(7, 8) );
std::cout << p3->x << "," << p3->y << std::endl;
p2 = p3;
std::cout << p2->x << "," << p2->y << std::endl;
SmartPointer<int> pp( new int(3) );
std::cout << *pp << std::endl;
}
void test_SmartPointer2(){
SmartPointer<Point> p1( new Point(3,4) );
if( p1 ){
std::cout << p1->x << "," << p1->y << std::endl;
}
Point* p2 = new Point(3,4);
Point* p3 = p1.get();
if( p1 == p2 )
std::cout << "p1==p2" << std::endl;
if( p1 != p2 )
std::cout << "p1!=p2" << std::endl;
if( p1 == p3 )
std::cout << "p1==p3" << std::endl;
if( p1 != p3 )
std::cout << "p1!=p3" << std::endl;
SmartPointer<Point> p4(p1);
if( p1 == p4 )
std::cout << "p1==p4" << std::endl;
if( p1 != p4 )
std::cout << "p1!=p4" << std::endl;
SmartPointer<Point> p5(new Point(7,8));
if( p1 == p5 )
std::cout << "p1==p5" << std::endl;
if( p1 != p5 )
std::cout << "p1!=p5" << std::endl;
}
int main(void){
test_SmartPointer2();
return 0;
}
2537

被折叠的 条评论
为什么被折叠?



