第九课.指针(四)


使用指针是危险的行为,可能存在野指针问题,可能造成内存泄漏事故,但指针是高效的,所以需要一种更安全的方式使用指针,一般有两种方案:

  • 使用更安全的指针:智能指针;
  • 不使用指针,使用更安全的方式:引用;

C++中有四种常用的智能指针:unique_ptr,shared_ptr,weak_ptr,auto_ptr(但auto_ptr存在缺陷,已经在C++17中被废弃);

智能指针auto_ptr

auto_ptr比较简单直接,在auto_ptr销毁时,其所管理的对象也会自动delete,对于auto_ptr指向的对象,如果让另一个智能指针指向该对象,则原来的指针指针就失去了对该对象的控制权(自动指向NULL),实例如下:

#include<string>
#include<iostream>
#include<memory>
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管理的对象只能被其所指,在当前指针不转让权限的情况下,不支持其他unique_ptr指向它,转让权限被称为移动语义,通过方法std::move()完成,移动语义后,该对象的使用权限将不会停留在原始指针,而是移动到新指针;

实例如下:

#include<iostream>
#include<memory>
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<iostream>
#include<memory>
using namespace std;

int main()
{
	//局部范围
	{
		//另一种定义方式
		auto unip = unique_ptr<int>(new int(10));

		//离开局部范围后,栈区域的变量 unip 会被释放,其指向的堆区域内存也会被回收
	}
}

unique_ptr释放堆区内存的来源:
原因在于析构函数中,对指向的堆区域对象进行了释放delete,析构函数(destructor) 与构造函数相反,当对象结束其生命周期,系统自动执行析构函数;析构函数往往用来做“清理善后” 的工作


shared_ptr和weak_ptr

从前面的内容得知,不管是auto_ptr还是unique_ptr,在同一时刻,内存的权限只能保留在一个指针上,在使用上存在局限性,所以需要shared_ptr,shared_ptr可以共享同一个对象;

shared_ptr通过引用计数共享同一个对象,shared_ptr是为了解决auto_ptr在对象权限上的局限性,在使用引用计数机制上提供了可以共享权限的智能指针,所以需要额外的开销;当引用计数为0时,该对象没有被使用,将自动被回收:
fig1
实例如下:

#include<iostream>
#include<memory>
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<iostream>
#include<memory>
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<string>
#include<iostream>
#include<memory>
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中的AWBW确实被回收了

引用

本质来说,引用是一种特殊的指针,一种不允许修改的指针,比如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<iostream>
#include<assert.h>
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语言;


  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值