C/C++指针&智能指针二
文章目录
1.智能指针简介
C++智能指针是为了解决原始指针可能导致的内存泄漏、悬挂指针等问题而引入的。它们是C++标准库中模板类,自动管理指针所指向对象的生命周期,以RAII(Resource Acquisition Is Initialization)原则为基础,确保了对象在不再需要时能被正确地销毁和释放内存。C++11引入了三种主要的智能指针:std::unique_ptr
、std::shared_ptr
和 std::weak_ptr
。
-
std::unique_ptr(独占智能指针):
std::unique_ptr
表示对一个对象的独占所有权。一个对象只能有一个unique_ptr
拥有者,当这个unique_ptr
离开作用域或者被显式重置时,它所指向的对象会被自动删除。- 它不支持拷贝构造函数和赋值操作符,但支持移动语义,可以通过移动构造函数或移动赋值将所有权转移给另一个
unique_ptr
。
-
std::shared_ptr(共享拥有权智能指针):
std::shared_ptr
允许多个智能指针共享同一个对象的所有权。它内部维护了一个引用计数器,每当一个新的shared_ptr
被创建指向同一个对象时,引用计数增加;当一个shared_ptr
被销毁或被重置时,引用计数减少。当引用计数降至0时,对象被自动删除。- 由于其共享性质,
shared_ptr
支持拷贝构造和赋值操作。
-
std::weak_ptr(弱引用智能指针):
std::weak_ptr
是一种不增加引用计数的智能指针,它观测由某个shared_ptr
管理的对象。主要用于解决循环引用问题,因为weak_ptr
不影响对象的生命周期,即使持有该weak_ptr
的所有对象都存在,也不会阻止被观测的对象被销毁。- 使用
weak_ptr
需要通过lock()
成员函数转换为临时的shared_ptr
才能访问对象,以检查对象是否还存在。
这些智能指针的使用简化了资源管理,减少了内存泄漏的风险,并且提高了代码的安全性和可维护性。在现代C++编程中,它们被广泛推荐用于原生指针的替代品。
2.独占智能指针unique_ptr
1.基本概念
std::unique_ptr
是C++中的一种独占式智能指针,它体现了唯一拥有权的概念。这意味着在同一时间内,只能有一个 unique_ptr
实例指向某个动态分配的内存区域。这种设计旨在防止多个指针同时修改或释放同一块内存,从而避免了资源竞争和潜在的内存泄漏问题。
-
所有权转移 :
unique_ptr
支持移动语义(通过移动构造函数和移动赋值运算符),允许将所有权从一个unique_ptr
转移到另一个unique_ptr
上,但不支持普通的拷贝(没有拷贝构造函数和拷贝赋值运算符)。 -
自动管理内存 :当
unique_ptr
离开其作用域时,它会自动删除所管理的动态分配的对象,无须手动调用delete
。 -
初始化与重置:
- 初始化:可以通过直接初始化或赋值初始化创建
unique_ptr
,并立即让它管理一个新分配的内存。 - 重置:可以使用
reset()
方法来让unique_ptr
管理一个新的对象或释放当前管理的对象。
- 初始化:可以通过直接初始化或赋值初始化创建
-
裸指针访问 :提供了
get()
方法来获取底层的原始指针,便于与不接受智能指针的旧代码交互。
示例
cpp
#include <iostream>
#include <memory>
int main() {
// 创建并初始化unique_ptr
std::unique_ptr<int> ptr(new int(42));
// 访问unique_ptr管理的对象
std::cout << "Value: " << *ptr << std::endl;
// 通过移动构造函数转移所有权
std::unique_ptr<int> anotherPtr(std::move(ptr));
// 此时ptr不再拥有任何对象
if (!ptr) {
std::cout << "ptr is now empty." << std::endl;
}
// 可以继续使用anotherPtr
*anotherPtr = 100;
std::cout << "New value: " << *anotherPtr << std::endl;
return 0;
}
在这个例子中,unique_ptr
ptr
初始化后管理一个整数值为42的动态分配的内存。然后,通过移动构造函数,这个管理权被转移到了 anotherPtr
,之后 ptr
就不再拥有任何对象。最后,我们修改了 anotherPtr
指向的值并打印出来,展示了所有权转移和自动内存管理的特性。
为什么要使用智能指针呢?
看下面这个例子:
cpp
//
// Created by 86189 on 2024/7/14.
//
#include <iostream>
using namespace std;
class student{
public:
string name;
student(){
cout << "nothing func" << endl;
}
student(string name){
this->name = name;
cout << "have func" << endl;
}
~student(){
cout << "delete func" << endl;
}
};
int main(){
student *p = new student;
p->name = "jack";
return 0;
}
运行程序发现只用类的无参构造函数被调用 ,而析构函数并没有被调用,也就是说我们new出来的空间并没用被释放,当我们在程序的结尾添加上delete p
时,析构函数才被调用。而当我们使用智能指针时,如下:
cpp
//
// Created by 86189 on 2024/7/14.
//
#include <iostream>
#include <utility>
#include <memory>
using namespace std;
class student{
public:
string name;
student(){
cout << "nothing func" << endl;
}
explicit student(string name){
this->name = std::move(name);
cout << "have func" << endl;
}
~student(){
cout << "delete func" << endl;
}
};
int main(){
auto *p = new student;
// p->name = "jack";
// delete p;
unique_ptr<student>ptr(p);
ptr->name = "jack";
return 0;
}
并不需要我们主动释放内存,编译器会自动帮我们释放。
2.使用方法
1.初始化
方法一:
cpp
unique_str<typet>ptr(new typet(value)); //分配内存并初始化
例如:
unique_str<student>ptr(new student("jack"));
方法二:
cpp
unique_str<typet>ptr = make_ptr<typet>(value)
例如:
unique_str<student>ptr = make_ptr<student>("jack");
方法三:
cpp
auto *p = new type;
unique_str<typet>ptr(p); //使用已知的地址初始化
例如:
auto *p = new student;
unique_ptr<student>ptr(p);
ptr.get(); // 返回原始指针p
注意:
不能普通指针赋值给智能指针
不能使用智能指针的拷贝构造函数
不能使用=对智能指针进行赋值
智能指针重载*操作符,可以使用解引用操作
在智能指针作为函数参数进行传递时,不能使用值传递,可使用传递引用或者地址
void func(unique_ptr<int> &ptr)
智能指针不支持指针的运算
3.使用技巧
-
利用构造函数初始化 :最好在定义
unique_ptr
时就直接初始化它,这样可以确保资源从一开始就得到妥善管理。避免先声明后初始化,这可能会导致资源泄露。 -
使用make_unique :自C++14起,推荐使用
std::make_unique
函数来创建unique_ptr
,这样可以更安全地处理异常情况,并且代码更简洁易读。cppauto ptr = std::make_unique<MyClass>(args...);
-
利用移动语义:通过移动构造函数和移动赋值操作符,可以在函数之间高效地传递所有权。这在需要将资源所有权从一个作用域转移到另一个作用域时特别有用。
cppvoid foo(std::unique_ptr<MyClass> ptr) { // ... } int main() { auto ptr = std::make_unique<MyClass>(); foo(std::move(ptr)); // 移动所有权 // 此处ptr不再拥有对象 }
-
避免裸指针操作 :尽量使用
unique_ptr
管理对象,避免直接使用裸指针,以减少内存泄漏的风险。 -
使用get()与释放所有权 :在需要将
unique_ptr
管理的对象传递给API,该API期望裸指针时,可以使用get()
方法获取原始指针。但是,务必确保不会因此导致对象的生命周期管理出现问题。 -
利用reset()进行重置 :当需要改变
unique_ptr
管理的对象时,可以使用reset()
方法。如果之前管理了对象,它会被正确销毁。cppptr.reset(new MyClass()); // 重新分配内存 ptr.reset(); // 释放当前管理的内存,使unique_ptr变为nullptr
-
避免循环依赖 :由于
unique_ptr
不支持共享所有权,所以在设计类的成员时,应避免通过unique_ptr
形成循环依赖,这可能导致资源无法释放。 -
模板与多态 :
unique_ptr
可以用于持有基类指针,实现多态性,并且能够安全地管理派生类对象的生命周期。这在设计灵活且类型安全的系统时非常有用。 -
单元测试 :在编写使用
unique_ptr
的类或函数的单元测试时,可以先在栈上创建对象进行测试,以确保逻辑正确,然后再使用智能指针进行集成测试。 -
文档化所有权转移:在代码中明确注释所有权的转移,特别是当通过函数参数或返回值进行传递时,有助于提高代码的可读性和维护性。
3.共享智能指针
std::shared_ptr
是C++中一种基于引用计数的智能指针,它允许多个 shared_ptr
实例共享同一个对象的所有权。当最后一个指向该对象的 shared_ptr
销毁时,该对象会被自动删除,从而有效防止内存泄漏。
1.基本概念
-
引用计数 :
shared_ptr
内部维护一个引用计数器,记录有多少个shared_ptr
指向同一个对象。每当创建一个新的shared_ptr
指向该对象时,引用计数增加;当一个shared_ptr
被销毁或被重置指向其他对象时,引用计数减少。当引用计数降至0,对象将被自动析构。 -
共享所有权 :多个
shared_ptr
可以同时拥有同一个对象的所有权,任何一个shared_ptr
的析构都会减少引用计数,当引用计数为0时,对象被释放。 -
线程安全性 :
shared_ptr
的引用计数操作通常是线程安全的(原子操作),这意味着在多线程环境下增加或减少引用计数不会引发数据竞争。然而,如果多个线程同时修改shared_ptr
(例如通过赋值或重置),则需要外部同步机制来保证线程安全。
2.使用方法
-
创建与初始化:
-
直接初始化:
std::shared_ptr<int> sptr(new int(42));
-
使用
make_shared
:推荐做法,因为更高效且能确保内存分配与构造函数调用的原子性。cppstd::shared_ptr<int> sptr = std::make_shared<int>(42);
-
-
拷贝与赋值 :
shared_ptr
支持拷贝构造函数和赋值运算符,拷贝后两个shared_ptr
实例将共享同一个对象的所有权。cppstd::shared_ptr<int> sptr2 = sptr; // sptr和sptr2现在共享所有权
-
访问原始指针 :使用
get()
方法获取原始指针。cppint* rawPtr = sptr.get();
-
检查是否为空 :使用
bool
类型转换或expired()
方法检查shared_ptr
是否为空或所指向的对象是否已被释放。cppif (sptr) { /* 非空 */ }
-
重置与交换:
reset()
方法可以用来释放当前指向的对象(如果引用计数允许的话),并可选择指向新的对象。swap()
方法可以交换两个shared_ptr
的管理对象。
-
自定义删除器 :在创建
shared_ptr
时,可以提供一个自定义删除器,用于在对象被删除时执行特定的操作。
3.注意事项
-
循环引用 :
shared_ptr
最大的风险在于可能引起循环引用,导致对象无法释放。在涉及循环引用的场景中,应该考虑使用std::weak_ptr
来断开引用链。 -
性能考量 :由于引用计数的维护,
shared_ptr
相比于原始指针或unique_ptr
会有一定的性能开销,尤其是在频繁拷贝或分配大量小对象时。 -
资源管理 :虽然
shared_ptr
自动管理内存,但正确使用它仍需开发者对对象生命周期有清晰的认识,避免不必要的资源占用。
4.智能指针删除器
智能指针删除器是在C++中使用智能指针(如std::unique_ptr
和std::shared_ptr
)时,提供的一种机制,用于自定义管理所指向对象生命周期结束时的清理行为。默认情况下,智能指针使用delete
操作符来释放内存,但通过指定删除器,你可以改变这一行为,以适应更复杂的资源管理需求,比如管理通过malloc
分配的内存、关闭文件描述符、释放自定义资源等。
1.如何使用删除器
-
对于std::unique_ptr:
cppvoid customDeleter(int* ptr) { // 自定义释放内存或其他操作 free(ptr); // 或其他清理操作 } std::unique_ptr<int, decltype(&customDeleter)> uniquePtr(new int(42), customDeleter);
在这里,
decltype(&customDeleter)
是删除器类型的显式指定,customDeleter
是实际的删除器函数。 -
对于std::shared_ptr:
cppstd::shared_ptr<int> sharedPtr(new int(42), customDeleter);
同样,
customDeleter
是自定义的删除器函数,但在这里类型推导会自动确定删除器的类型,所以不需要显式指定。 -
使用lambda作为删除器:
cppauto deleter = [](int* ptr) { // 自定义释放逻辑 delete ptr; }; std::unique_ptr<int, decltype(deleter)> uniquePtrWithLambda(new int(42), deleter);
-
使用std::make_shared与删除器 :
对于
std::shared_ptr
,虽然std::make_shared
不直接支持传递删除器,但可以通过创建一个没有删除器的shared_ptr
,然后使用std::shared_ptr
的拷贝构造函数和自定义删除器来创建一个新的shared_ptr
实例。
2.应用场景
- 当对象不是通过
new
分配的,而是通过其他方式(如malloc
)分配时,需要自定义删除器来释放资源。 - 管理非内存资源,如文件句柄、网络连接等,需要在对象生命周期结束时执行特定的清理操作。
- 在需要执行额外清理工作或者遵循特定清理协议的情况下。
通过使用自定义删除器,智能指针变得更加灵活,能够适应更广泛的资源管理需求,增强了C++代码的安全性和可维护性。
5.弱智能指针weak_ptr
std::weak_ptr
是C++智能指针家族中的一个成员,专门设计用于解决由std::shared_ptr
引起的循环引用问题。它是对shared_ptr
所管理对象的一种非拥有(或者说不增加引用计数)的引用方式,主要用于监控对象的存在性而不影响对象的生命周期管理。
1.基本概念
-
非拥有性 :
weak_ptr
不增加它所指向的shared_ptr
管理对象的引用计数。这意味着,即使存在多个weak_ptr
指向同一对象,该对象也仅当最后一个关联的shared_ptr
销毁时才会被释放。 -
监测作用 :它主要用于在不延长对象生命周期的前提下,安全地访问可能由
shared_ptr
管理的对象。这对于避免因循环引用导致的内存泄漏尤为重要。 -
生存确认 :由于
weak_ptr
不控制对象生命周期,使用前必须通过lock()
方法检查对象是否仍然存在,lock()
会返回一个指向相同对象的临时shared_ptr
,若对象已被销毁,则返回一个空的shared_ptr
。
2.使用方法
-
从
shared_ptr
创建:cppstd::shared_ptr<int> sptr = std::make_shared<int>(42); std::weak_ptr<int> wptr(sptr); // 从shared_ptr构造weak_ptr
-
检查对象有效性:
cppif(auto sp = wptr.lock()) { // 尝试锁定并检查对象是否存在 std::cout << "*wptr: " << *sp << std::endl; // 安全访问对象 } else { std::cout << "Object no longer exists." << std::endl; }
-
访问对象 :通过
lock()
方法获得的临时shared_ptr
来访问对象,确保了对象存在的同时,也遵循了RAII原则。 -
循环引用解决方案 :在设计含有相互引用的类时,可以使用
weak_ptr
替代一个方向上的shared_ptr
引用,以打破循环,确保没有不必要的生命周期扩展。
3.注意事项
-
非直接访问 :
weak_ptr
本身不支持类似*
或->
这样的直接访问操作,必须先转换为shared_ptr
。 -
生命周期管理 :尽管
weak_ptr
不直接影响对象生命周期,但应谨慎使用,避免不经意间延长了对象的生命周期,尤其是当使用lock()
后未及时释放对应的shared_ptr
时。