突破编程_C++_基础教程(指针(二))

1 智能指针的引入

C++中,使用传统指针进行动态内存操作时,在使用完所申请的内存后,需要手动释放对应的内存空间。如果忘记正确释放内存或者释放了无效的指针,就会导致内存泄漏;如果指针指向的内存被释放后仍然使用,就会产生野指针。这些问题都会导致程序出现错误或者崩溃。

1.1 容易忘记正确释放内存的场景

(1)释放堆上的数组
在释放内存的处理上,在堆上申请的数组和其他类型的对象是不一样的,数组的释放需要使用 delete[] ,其他类型的对象使用 delete 。如下为样例代码:

int* val = new int;
delete val;			//OK
int* vals = new int[6];
delete vals;		//错误:这样只会释放 vals 数组的第一个元素所占据的空间,从而造成内存泄漏
delete[] vals;		//OK

在实际开发过程中, delete 后面的 [] 经常容易忘记写,从而导致内存泄漏。而更为麻烦的是,很多内存泄漏的检测工具无法检测出这种形式的内存泄漏(比如在 Windows 下最常使用第三方工具 VLD),这样就失去了最后一层内存安全的保障。
(2)在释放内存前有返回操作
如果申请内存和释放内存之间还有很多代码,则很有可能因为中间就 return 结束,导致释放内存的代码没有执行到。如下为样例代码:

#include 

void doSomething(int type)
{
	int* val = new int;
	if (0 != type)
	{
		return;	 //错误: 一旦入参值为 0 ,则不会执行到释放 val 内存的语句
	}
	delete val;
}

int main()
{
	doSomething(0);

	return 0;
}

这种场景在实际开发过程中也很常见,因为一旦代码的逻辑比较复杂,截断返回就很重要,不然就会增加大量的 if-else 嵌套。
上面的两种容易忘记释放内存的场景会给程序带来很多潜在的风险点,解决这类问题需要使用到 C++ 的 RAII 编程方法。

1.2 RAII 编程方法

RAII(Resource Acquisition Is Initialization)是C++的发明者 Bjarne Stroustrup 提出的概念,也称为资源获取就是初始化,是一种管理资源、避免泄漏的编程方法。 它的基本思想是在对象的构造函数中获取资源,并在对象的析构函数中释放资源。通过这种方式,资源管理被封装在对象的生命周期中,从而简化了资源的获取和释放,避免了手动管理资源时可能出现的错误。
RAII 的核心思想是将资源的生命周期与对象的生命周期绑定在一起。当对象被创建时,它会自动获取所需的资源;当对象被销毁时,它会自动释放所拥有的资源。这种方式可以确保资源的正确获取和释放,避免了资源泄漏和内存泄漏等问题。
比如针对上面章节中在释放内存前有返回操作的场景,使用 RAII 编程方法可以作如下代码调整:

#include 

class SmartVal
{
public:
	SmartVal()
	{
		printf("automatically apply for memory\n");
		m_val = new int;
	}

	~SmartVal()
	{
		if (nullptr != m_val)
		{
			printf("automatically release for memory\n");
			delete m_val;
			m_val = nullptr;
		}
	}

public:
	int* getVal()
	{
		return m_val;
	}

private:
	int* m_val = nullptr;
};

void doSomething(int type)
{
	SmartVal val;
	if (0 != type)
	{
		return;			//OK:val 申请的内存会自动释放
	}
}

int main()
{
	doSomething(0);

	return 0;
}

上面代码的输出为:

automatically apply for memory
automatically release for memory

通过在构造函数自动申请内存,在析构函数中自动释放内存,从而避免了由于忘记正确释放内存导致内存泄漏。除了对于内存的自动管理,RAII 还可以应用于其他类型的资源管理,如文件句柄、网络连接等。通过将资源的获取和释放封装在相应的对象中,可以简化资源的管理,提高代码的可读性和可维护性。根据 RAII 编程方法, C++11 标准引入了能够自动管理动态内存的智能指针。

1.3 使用智能指针

智能指针是实际上是一个模板类,它具有自动管理内存的机制。当智能指针超出作用域时,它会自动释放所指向的内存,无需程序员手动释放。这样就可以避免内存泄漏和野指针等问题。
C++11 标准引入了三种智能指针: shared_ptr (共享指针)、 unique_ptr (独享指针)和 weak_ptr (弱引用指针)。shared_ptr 表示共享所有权的智能指针,它可以由多个智能指针共享同一个对象的所有权,当最后一个共享所有权的智能指针被销毁时,对象才会被删除。 unique_ptr 表示独占所有权的智能指针,它只允许一个智能指针拥有对对象的所有权,当所有权被转移时,原来的智能指针会自动删除对象。 weak_ptr 表示弱引用智能指针,它不能拥有对象的所有权,只能与 shared_ptr 配合使用,用来解决共享所有权带来的循环引用问题。
使用智能指针可以大大简化内存管理的工作,减少出错的可能性,提高代码的可读性和可维护性。因此,在 C++ 编程中,强烈建议使用智能指针而不是原始指针(性能上两者相差无几)。
注意:使用智能指针需要引入头文件 #include

2 shared_ptr (共享指针)

shared_ptr 用于实现共享所有权的内存管理,在初始化一个 shared_ptr 之后,可以将其赋值给其他变量,也可以按值将其传入函数参数,然后将其分配给其他 shared_ptr 实例(每多一个实例,则引用计数 +1 )。 所有实例均指向同一个对象,这一个对象被多个 shared_ptr 共享所有权。 当引用计数达到零时,控制块将删除内存资源和自身。

2.1 shared_ptr 的初始化

shared_ptr 的初始化有四种方式:构造函数、 make_shared 辅助函数、 reset 方法以及赋值操作。如下为样例代码:

#include 
#include 

using namespace std;

int main()
{
	shared_ptr<int> ptr1(new int);		//构造函数

	shared_ptr<int> ptr2 = make_shared<int>(0);	//make_shared

	shared_ptr<int> ptr3;
	ptr3.reset(new int);				//reset

	shared_ptr<int> ptr4;
	ptr4 = ptr3;						//赋值操作

	return 0;
}

其中,优先推荐 make_shared 辅助函数来进行初始化,make_shared 的一个特性是能提升效率。使用 make_shared 允许编译器产生更小,更快的代码。上面代码中的第 16 行 ptr4 = ptr3;,这两个指针指向的是同一个对象,其引用计数 +1 。
与传统指针不同的是,不能将一个 new 出来的内存地址直接赋值给智能指针,如下就是一个错误样例:

shared_ptr<int> ptr = new int;	//错误:不允许将一个 new 出来的内存地址直接赋值给智能指针

2.2 shared_ptr 获取原始指针

可以通过 get 方法获取原始指针,如下为样例代码:

#include 
#include 

using namespace std;

int main()
{
	shared_ptr<int> ptr = make_shared<int>(0);
	int* orgPtr = ptr.get();

	*orgPtr = 2;
	printf("value = %d\n", *ptr);

	return 0;
}

上面代码的输出为:

value = 2

上面代码中 11 行 int* orgPtr = ptr.get(); 便是通过 get 方法获取原始指针,代码中的 14 行 printf("value = %d\n", *ptr); 表明 shared_ptr 的 operator*() 已经被重载,可以解引用得到其指向的对象。

2.3 shared_ptr 的自定义删除函数

智能指针支持自定义删除函数,以替代默认的删除操作。这在处理特殊类型的资源时非常有用(例如需要调用特定析构函数的资源)。样例代码如下:

#include 
#include 

using namespace std;

void customDeleteFunc(int* val)
{
	printf("custom delete\n");
	delete val;
}

int main()
{
	shared_ptr<int> ptr(new int, customDeleteFunc);

	return 0;
}

上面代码的输出为:

custom delete

在 ptr 的引用计数为 0 时,系统会自动调用自定义的删除函数 void customDeleteFunc(int* val) 进行内存释放。删除函数也可以使用 Lambda 表达式:shared_ptr ptr(new int, [](int* val) {delete val; });
**注意:由于 shared_ptr 的默认删除器不支持数组对象,所以这种场景需要自定义删除函数。**如下为样例代码:

shared_ptr<int> ptr(new int[6], [](int* vals) {delete[] vals; });

2.4 使用 shared_ptr 时 this 指针的返回

this 指针指向对象自身的地址,该指针是可以作为返回值传递给其他对象的。不要直接使用 shared_ptr(this) 将 this 指针返回,因为它可能导致循环引用和内存泄漏。循环引用是指两个或多个对象相互引用,形成了一个无法被打破的引用循环,导致这些对象都无法被正确销毁。当一个对象通过 shared_ptr(this)) 返回其自身的 this 指针时,就会造成重复析构。如下为样例代码:

#include   
#include   

using namespace std;

class A 
{
public:
	A() {}
	~A() {}
public:
	std::shared_ptr<A> getSharedPtr() {
		return shared_ptr<A>(this);
	}
};

int main() {

	shared_ptr<A> ptr1 = make_shared<A>();
	shared_ptr<A> ptr2 = ptr1->getSharedPtr();
	
	return 0;
}

这段代码执行时会出现程序崩溃的现象,原因就是离开作用域后的 this 指针会被 ptr1 与 ptr2 各自析构。
正确的返回 this 指针的方法是让目标类继承 enable_shared_from_this ,然后使用基类的成员函数 shared_from_this 来返回 this 指针。如下为样例代码:

#include   
#include   

using namespace std;

class A : public enable_shared_from_this<A>
{
public:
	A() {}
	~A() {}
public:
	std::shared_ptr<A> getSharedPtr() {
		return shared_from_this();
	}
};

int main() {

	shared_ptr<A> ptr1 = make_shared<A>();
	shared_ptr<A> ptr2 = ptr1->getSharedPtr();
	
	return 0;
}

2.5 使用 shared_ptr 的注意点

(1)避免同一块内存绑定到多个独立创建的 shared_ptr 上,否则会造成内存泄漏。如下为样例代码:

int* val = new int(0);
shared_ptr<int> ptr1(val);
shared_ptr<int> ptr2(val);

这段代码执行时会出现程序崩溃的现象,原因就是离开作用域后的变量 val 会被 ptr1 与 ptr2 各自析构。
这个注意点实际上也提醒我们:不要混合使用智能指针和普通指针,坚持只用智能指针。
(2)当使用 shared_ptr 来管理一个数组时,需要注意 shared_ptr 的默认删除器不支持数组对象,所以这种场景需要自定义删除函数。
(3)避免循环引用。智能指针最大的一个陷阱是循环引用,循环引用会导致内存泄漏。如下为样例代码:

#include   
#include   

using namespace std;

class A;
class B;

class A
{
public:
	A() {}
	~A() {}

public:
	void setB(shared_ptr<B> b)
	{
		m_b = b;
	}

private:
	shared_ptr<B> m_b;
};

class B
{
public:
	B() {}
	~B() {}

public:
	void setA(shared_ptr<A> a)
	{
		m_a = a;
	}

private:
	shared_ptr<A> m_a;
};

int main() {

	shared_ptr<A> a(new A);
	shared_ptr<B> b(new B);
	
	a->setB(b);
	b->setA(a);

	return 0;
}

上述代码在运行后,两个指针 a 与 b 都不会被删除,从而造成了内存泄漏,这是由于 a 与 b 的循环引用导致这两者的引用计数都为 2 ,在离开作用域后, a 与 b 的引用计数减为 1 ,并不为 0 ,导致这两个指针都不会被析构,于是变产生了内存泄漏。解决办法是使用 weak_ptr ,具体实现方式在下面讲解 weak_ptr 会详细说明。

3 unique_ptr (独享指针)

unique_ptr 用于表示对动态分配对象的独占所有权。一个 unique_ptr 在任何时候都拥有其指向对象的唯一所有权。这意味着不能有两个 unique_ptr 同时指向同一个对象。因此一个 unique_ptr 不支持复制操作。

3.1 unique_ptr 的独享特性

unique_ptr 不允许将一个 unique_ptr 赋值给另外一个 unique_ptr:

#include   
#include   

using namespace std;

int main() {
	std::unique_ptr<int> ptr1(new int);  // 创建 unique_ptr,并初始化它指向一个新创建的整形对象  
	std::unique_ptr<int> ptr2 = ptr1;    // 错误:独享指针不允许复制

	return 0;
}

unique_ptr 不允许复制,但是可以使用 move 函数将其转移给其他 unique_ptr 。(原有的 unique_ptr 将会成为 nullptr ):

#include   
#include   

using namespace std;

int main() {
	std::unique_ptr<int> ptr1(new int);
	std::unique_ptr<int> ptr2 = move(ptr1); 

	if (nullptr == ptr1)
	{
		printf("ptr1 value = nullptr\n");
	}

	return 0;
}

上面代码的输出为:

ptr1 value = nullptr

3.2 unique_ptr 的自定义删除函数

unique_ptr 自定义删除函数与 shared_ptr 自定义删除函数不同,比如下面的这种方法是错误的:

unique_ptr<int> ptr(new int, [](int* vals) {delete[] vals; }); //错误

unique_ptr 自定义删除函数需要将删除函数指定在模板中,如下:

#include   
#include   

using namespace std;

class customDeleteFunc
{
public:
	void operator()(int* vals) const {
		delete[] vals;
	}
};

int main() {
	unique_ptr<int[], customDeleteFunc> ptr(new int[6]);

	return 0;
}

在上面代码中,customDeleteFunc 类定义了一个函数调用操作符来删除一个整数数组。这个删除器被传递给 unique_ptr,这个独享指针负责管理一个通过 new[] 分配的整数数组。当 vals 离开其作用域时,customDeleteFunc 的操作符将被自动调用以释放数组。
unique_ptr 的自定义删除函数还可以使用函数包装器 function :

#include   
#include   
#include   

using namespace std;

int main() {
	function<void(int*)> customDeleteFunc = [](int* vals) {
		delete vals;
	};

	unique_ptr<int[], function<void(int*)>> ptr(new int[6], customDeleteFunc);


	return 0;
}

4 weak_ptr (弱引用指针)

weak_ptr 是用来监视 shared_ptr 的。它指向一个由 shared_ptr 管理的对象,但不控制该对象的生存期。即当多个 shared_ptr 指向同一个对象时,该对象会有多个引用计数,但当 weak_ptr 指向这个对象时,并不会增加该对象的引用计数。weak_ptr 没有重载操作符 * 以及 ->

4.1 weak_ptr 的基本用法

(1)使用 use_count() 获取当前观测对象的引用计数:

#include   
#include   
#include   

using namespace std;

int main() {
	shared_ptr<int> ptr = make_shared<int>(0);
	weak_ptr<int> wPtr = ptr;

	printf("wPtr count = %ld\n", wPtr.use_count());

	return 0;
}

上面代码的输出为:

wPtr count = 1

(2)使用 expired() 获取当前观测对象的的资源是否已经被释放:

#include   
#include   
#include   

using namespace std;

int main() {
	shared_ptr<int> ptr = make_shared<int>(0);
	weak_ptr<int> wPtr(ptr);

	printf("wPtr.expired = %d\n", wPtr.expired());

	return 0;
}

上面代码的输出为:

wPtr.expired = 0

(3)使用 lock() 从 weak_ptr 创建一个 shared_ptr 。如果 weak_ptr 观察的对象仍然存在(即没有被销毁),则 lock 会成功并返回一个新的 shared_ptr ,该 shared_ptr 拥有对对象的共享所有权。如果对象已经被销毁, lock 将返回一个空的 shared_ptr 。如下为样例代码:

#include   
#include   
#include   

using namespace std;

int main() {
	shared_ptr<int> ptr = make_shared<int>(0);
	weak_ptr<int> wPtr = ptr;
	shared_ptr<int> ptr2 = wPtr.lock();

	printf("wPtr count = %ld\n", wPtr.use_count());

	return 0;
}

上面代码的输出为:

wPtr count = 2

4.2 weak_ptr 解决循环引用问题

循环引用是指两个或更多智能指针相互引用,形成一个闭环,导致它们的引用计数永远不会降到0,从而使得它们管理的内存无法得到释放。
使用 weak_ptr 可以打破这个循环,因为它不增加所指向对象的引用计数。当一个 shared_ptr 和一个 weak_ptr 相互引用时,只有当 shared_ptr 的引用计数变为 0 时,对象才会被销毁。而 weak_ptr 可以通过调用 lock() 方法来尝试获取一个临时的 shared_ptr,以安全地访问对象。如果对象已经被销毁,lock() 方法将返回一个空的 shared_ptr。如下为样例代码:

#include   
#include   

using namespace std;

class A;
class B;

class A
{
public:
	A() {}
	~A() 
	{
		printf("destroy A\n");
	}

public:
	void setB(shared_ptr<B> b)
	{
		m_b = b;
	}

private:
	shared_ptr<B> m_b;
};

class B
{
public:
	B() {}
	~B()
	{
		printf("destroy B\n");
	}

public:
	void setA(shared_ptr<A> a)
	{
		m_a = a;
	}

private:
	weak_ptr<A> m_a;		//这里使用 weak_ptr 不会增加引用计数,解决了循环依赖的问题
};

int main() {

	shared_ptr<A> a(new A);
	shared_ptr<B> b(new B);

	a->setB(b);
	b->setA(a);

	return 0;
}

上面代码的输出为:

destroy A
destroy B

你可能感兴趣的:(突破编程_C++_基础教程,c++)