C++中有四种常用的智能指针:unique_ptr,shared_ptr,weak_ptr,auto_ptr(但auto_ptr存在缺陷,已经在C++17中被废弃);
auto_ptr比较简单直接,在auto_ptr销毁时,其所管理的对象也会自动delete,对于auto_ptr指向的对象,如果让另一个智能指针指向该对象,则原来的指针指针就失去了对该对象的控制权(自动指向NULL),实例如下:
#include
#include
#include
using namespace std;
int main()
{
//该区域结束后,auto_ptr作为栈区变量会被释放,因此其指向的对象将被自动回收
{
//auto_ptr来自头文件memory
auto_ptr<int>pl(new int(10));
cout << *pl << endl;
auto_ptr<string>language[5] = {
auto_ptr<string>(new string("C")),
auto_ptr<string>(new string("Java")),
auto_ptr<string>(new string("C++")),
auto_ptr<string>(new string("Python")),
auto_ptr<string>(new string("Rust"))
};
auto_ptr<string>pc;
pc = language[2]; //此后language[2]将指向NULL
//cout << *language[2] << endl; //会报错,因为NULL没有引用对象
cout << *language[3] << endl;
};
return 0;
}
unique_ptr具有专属权,unique_ptr管理的对象只能被其所指,在当前指针不转让权限的情况下,不支持其他unique_ptr指向它,转让权限被称为移动语义,通过方法std::move()
完成,移动语义后,该对象的使用权限将不会停留在原始指针,而是移动到新指针;
实例如下:
#include
#include
using namespace std;
int main()
{
//unique_ptr
//auto是C++11之后支持的,可以自动检测变量的数据类型
//w指向堆中开辟的 int(10)
auto w = std::make_unique<int>(10);
//间接引用指针的值
cout << *(w.get()) << endl;
//C++赋值后的对象指向的内存不同
int a = 1;
int b = a;
cout << &a << endl; //00FBF82C
cout << &b << endl; //00FBF820
//auto w2 = w; //报错,unique_ptr不可以直接赋值
auto w2 = std::move(w); //w2获得权限,w此时指向NULL
//三目运算符---表达式1?表达式2:表达式3---若表达式1为True(1),返回表达式2,否则False(0)返回表达式3
cout << ((w.get() != nullptr) ? (*w.get()) : -1) << endl; //-1
cout << ((w2.get() != nullptr) ? (*w2.get()) : -1) << endl; //10
return 0;
}
补充一下,C++中的赋值不同于Python中的赋值,C++赋值后的对象指向的内存不同,但Python的赋值指向内存相同(关于Python的赋值,浅拷贝,深拷贝回顾Python笔记本第二课):
a=1
b=a
print(id(a),id(b))
# 10919328 10919328
关于内存释放,实例如下:
#include
#include
using namespace std;
int main()
{
//局部范围
{
//另一种定义方式
auto unip = unique_ptr<int>(new int(10));
//离开局部范围后,栈区域的变量 unip 会被释放,其指向的堆区域内存也会被回收
}
}
unique_ptr释放堆区内存的来源:
原因在于析构函数中,对指向的堆区域对象进行了释放delete
,析构函数(destructor) 与构造函数相反,当对象结束其生命周期,系统自动执行析构函数;析构函数往往用来做“清理善后” 的工作
从前面的内容得知,不管是auto_ptr还是unique_ptr,在同一时刻,内存的权限只能保留在一个指针上,在使用上存在局限性,所以需要shared_ptr,shared_ptr可以共享同一个对象;
shared_ptr通过引用计数共享同一个对象,shared_ptr是为了解决auto_ptr在对象权限上的局限性,在使用引用计数机制上提供了可以共享权限的智能指针,所以需要额外的开销;当引用计数为0时,该对象没有被使用,将自动被回收:
实例如下:
#include
#include
using namespace std;
int main()
{
//shared_ptr
{
auto wA = shared_ptr<int>(new int(20));
{
//wA2和wA共同指向一块内存
auto wA2 = wA;
cout << ((wA2.get() != nullptr) ? (*wA2.get()) : -1) << endl; //20
cout << ((wA.get() != nullptr) ? (*wA.get()) : -1) << endl; //20
// 打印内存对象的引用计数
cout << wA2.use_count() << endl; //2
cout << wA.use_count() << endl; //2
}
//cout << wA2.use_count() << endl; //报错,wA2已离开作用域,被消毁
cout << wA.use_count() << endl; //1
//shared_ptr内部利用引用计数实现内存自动管理,每复制一个shared_ptr,
//引用计数会+1,当一个shared_ptr离开作用域后,引用计数-1,
//引用计数为0时,delete内存
}
return 0;
}
shared_ptr也可以使用move方法转让权限:
#include
#include
using namespace std;
int main()
{
//move方法
auto wAA = std::make_shared<int>(30);
auto wAA2 = std::move(wAA); //此时wAA为NULL,wAA2指向原内存,该内存的引用计数依然为1
cout << ((wAA.get() != nullptr) ? (*wAA.get()) : -1) << endl; //-1
cout << ((wAA2.get() != nullptr) ? (*wAA2.get()) : -1) << endl; //30
cout << wAA.use_count() << endl; //0
cout << wAA2.use_count() << endl; //1
//将wAA指向的对象move给wAA2,意味着wAA放弃了对内存的管理,指向NULL,
//wAA2获得了对象的权限,但由于wAA不再管理对象,所以该对象的引用计数依然为1
return 0;
}
shared_ptr会引起循环引用问题:从堆区域申请两个对象A和B(类或结构体),初始时总是需要有两个shared_ptr(比如tA和tB)指向A和B;A对象中包含一个shared_ptr(pB)指向B对象,B对象中包含一个shared_ptr(pA)指向A对象,此时A和B的引用计数都为2,注意到比如虽然有两个指针指向了内存A,但一种指针tA在栈区域,另一种pB却在堆区域,离开作用域后,tA和tB会自动被系统回收,A和B的引用计数从2变成1,没有成为0,即堆区内存A和B依然存在,造成内存泄漏;
为了避免循环引用,所以使用weak_ptr协助shared_ptr,weak_ptr以观察者模式工作,观察者意味着weak_ptr只对shared_ptr进行引用,而不改变其引用计数;依然使用上面的例子:从堆区域申请两个对象A和B(类或结构体),初始时用两个shared_ptr(比如tA和tB)指向A和B;A对象中包含一个shared_ptr(pB)指向B对象,B对象中包含一个weak_ptr(pA)指向A对象,此时A的引用计数为1,B的引用计数为2,当离开作用域后,tA和tB被系统回收,A的引用计数为0,B的为1,由于A的引用计数为0,则A被回收(包括A中的pB也被回收),由于pB被回收,则B的引用计数减1而变成0,B也被回收;
上述过程如下:
#include
#include
#include
using namespace std;
//C语言中的结构体,对应面向对象中的类
struct B;
struct A {
shared_ptr<B>pb;
//在析构函数中进行打印,显式地判断变量有没有被回收
~A()
{
cout << "~A()" << endl;
}
};
struct B {
shared_ptr<A>pa;
~B()
{
cout << "~B()" << endl;
}
};
//pa和pb存在循环引用,根据shared_ptr引用计数原理,pa和pb都无法被正常释放,
//weak_ptr用于解决循环引用
struct BW;
struct AW {
shared_ptr<BW>pb;
~AW()
{
cout << "~AW()" << endl;
}
};
struct BW {
weak_ptr<AW>pa;
~BW()
{
cout << "~BW()" << endl;
}
};
void test()
{
cout << "test shared_ptr and shared_ptr:" << endl;
shared_ptr<A>tA(new A()); //tA指向A对象
shared_ptr<B>tB(new B()); //tB指向B对象
cout << tA.use_count() << endl; //1
cout << tB.use_count() << endl; //1
tA->pb = tB;
tB->pa = tA;
cout << tA.use_count() << endl; //2
cout << tB.use_count() << endl; //2
}
void test2()
{
cout << "test weak_ptr and shared_ptr:" << endl;
shared_ptr<AW>tA(new AW());
shared_ptr<BW>tB(new BW());
cout << tA.use_count() << endl; //1
cout << tB.use_count() << endl; //1
tA->pb = tB;
tB->pa = tA;
cout << tA.use_count() << endl; //1
cout << tB.use_count() << endl; //2
}
int main()
{
test();
test2();
/*
test shared_ptr and shared_ptr:
1
1
2
2
test weak_ptr and shared_ptr:
1
1
1
2
~AW()
~BW()
*/
return 0;
}
在结果中,打印了~AW()
和~BW()
,说明test2
中的AW
和BW
确实被回收了
本质来说,引用是一种特殊的指针,一种不允许修改的指针,比如Java,其实Java也有指针,这种指针就是引用;一旦初始化了引用这种指针,这个指针就不能再修改指向其他对象,所以使用引用必须初始化,并永远指向初始化的那个对象;
引用可以认为是指定变量的别名,使用时可以认为是变量本身:
int x=1,x2=3;
int& rx=x; //引用:将rx与x绑定
rx=2;
cout<<x<<endl; //2
cout<<rx<<endl; //2
rx=x2; //相当于x=x2
cout<<x<<endl; //3
cout<<rx<<endl; //3
使用引用可以实现两个变量的值交换:
#include
#include
using namespace std;
//用引用的方式交换a和b的值
void swap(int& a, int& b)
{
auto temp = a;
a = b;
b = temp;
}
int main()
{
int a = 3, b = 4;
swap(a, b);
//用断言测试结果
assert((a == 4) && (b == 3));
return 0;
}
有了指针为什么还要引用
引用完全可以被指针替代,但基于引用方式定义的函数,在参数的传递上看起来会更加自然;
有了引用为什么还要指针
C++不是Java,使用指针是为了完全兼容C语言;