C++中,智能指针与普通指针不同,是包含指针的一种类模板,用于管理动态分配的内存。智能指针的行为类似于常规指针,但是能够自动地释放所指向的对象,避免内存的泄露。智能指针通过对被引用对象进行计数的方式,或者其他机制,限制被引用的次数,避免形成循环引用。
相较于常规指针,在使用完以后,通常需要使用free或者delete释放指针内容,智能指针会自动释放其所管理的内存,防止程序员忘记释放动态分配的内存而导致内存泄漏。在发生异常的情况下,智能指针可以确保资源被正确地释放,提高程序的健壮性。例如,在函数执行过程中抛出异常,智能指针可以在栈展开时释放资源。
std::unique_ptr
std::unique_ptr
是一种独占式智能指针,它所管理的对象只能有一个unique_ptr
指向它。当unique_ptr
被销毁时,它所指向的对象也会被销毁。unique_ptr
不能被复制,但可以被移动(例如,通过返回函数或传递给函数)。
- 独占所有权:
unique_ptr
所指向的对象在同一时刻只能有一个unique_ptr
拥有其所有权,当该unique_ptr
被销毁时,它所管理的对象也会被自动销毁,从而确保资源不会泄露。 - 轻量级:
unique_ptr
对象本身通常只占用一个指针的空间,不会带来过多的额外开销,其大小通常与一个原生指针相当,这使得它在性能敏感的场景中非常高效。 - 不可复制:
unique_ptr
不支持普通的复制语义,因为复制会导致多个unique_ptr
指向同一个对象,从而破坏独占所有权的语义。但它支持移动语义,可以通过移动操作将所有权从一个unique_ptr
转移到另一个unique_ptr
。
#include<iostream>
#include<memory>
using namespace std;class Resource {
public:Resource(int val = 0) : value(val) {std::cout << "Resource constructed: " << value << std::endl;}void setValue(int val){value = val;}void showValue(){cout<<"This object value is "<<value<<endl;}~Resource() {std::cout << "Resource destroyed: " << value << std::endl;}int value;
};void example01()
{//unique_prt的基本使用// 1. 创建unique_ptr的几种方式std::unique_ptr<int> p1(new int(10)); // 直接构造auto p2 = std::make_unique<int>(20); // 推荐方式(C++14)std::unique_ptr<int[]> arr = std::make_unique<int[]>(5); // 数组形式std::unique_ptr<Resource[]> arr_class(new Resource[3]); //在有默认构造函数情况下,且不需要参数的情况下,可以由编译器给初值std::unique_ptr<Resource[]> arr_Resource(new Resource[3]{Resource(1), Resource(2), Resource(3)}); //在给定参数的情况下,相当于是使用数组初始化的方式构造类的对象std::unique_ptr<Resource[]> arr_Resource2(new Resource[3]{{4},{5},{6}}); //在给定参数的情况下,给出初值,再调用类的构造函数,构造出类数组元素的对象//解引用操作与普通一致cout <<"p1指向的内容是: "<<*p1<<endl;//数组操作与普通数组一致,但是无法使用加减操作来寻址arr[2] = 100;//cout << "arr数组的3号元素值为 : "<<*(arr + 2)<<endl; 报错cout <<"arr数组3号元素指向的内容是: "<<arr[2]<<endl;//调用智能指针访问类数组的对象修改值//(arr_Resource + 2)->showValue(); 使用->调用方式会失败cout<<"修改前后的对象值: "<<endl;arr_Resource[2].showValue();arr_Resource[2].setValue(100);arr_Resource[2].showValue();//获取智能指针的原始指针cout<<"通过原始指针访问值: "<<endl;Resource* p_resource = arr_Resource.get();(p_resource+2)->showValue();//unique_ptr只能转移,不能复制std::unique_ptr<Resource[]> p2_resource = std::move(arr_Resource);cout<<"获取控制权后的智能指针访问数组: "<<endl;p2_resource[2].showValue();if(arr_Resource != nullptr){arr_Resource[2].showValue();}else{cout<<"arr_Resource指针已经被置空"<<endl;}}
int main()
{example01();return 0;
}
程序的输出为:
Resource constructed: 0
Resource constructed: 0
Resource constructed: 0
Resource constructed: 1
Resource constructed: 2
Resource constructed: 3
Resource constructed: 4
Resource constructed: 5
Resource constructed: 6
p1指向的内容是: 10
arr数组3号元素指向的内容是: 100
修改前后的对象值:
This object value is 3
This object value is 100
通过原始指针访问值:
This object value is 100
获取控制权后的智能指针访问数组:
This object value is 100
arr_Resource指针已经被置空
Resource destroyed: 100
Resource destroyed: 2
Resource destroyed: 1
Resource destroyed: 6
Resource destroyed: 5
Resource destroyed: 4
Resource destroyed: 0
Resource destroyed: 0
Resource destroyed: 0
不建议使用unique_ptr指向一个已经存在的变量,这是因为一旦超出了变量的作用域,变量会被释放,造成指针空置而报错或者重复释放。
void example02()
{int data = 100; std::unique_ptr<int> p1(&data); //使用unique_ptr指向一个已经存在的变量std::unique_ptr<int> p2(&data); //另一个unique_ptr指针cout<<"使用p1访问内存变量: data is "<<*p1<<endl;cout<<"使用p2访问内存变量: data is "<<*p2<<endl;
}
使用p1访问内存变量: data is 100
使用p2访问内存变量: data is 100
munmap_chunk(): invalid pointer
Aborted (core dumped)
因为智能指针中,已经包含了对象自动释放机制,避免内存泄漏的情况,所以重复释放也会造成程序报错,类似于普通指针的重复释放:
void example03()
{// int data = 100; int data2 = 101;// std::unique_ptr<int> p1(&data); //使用unique_ptr指向一个已经存在的变量// std::unique_ptr<int> p2(&data); //另一个unique_ptr指针// cout<<"使用p1访问内存变量: data is "<<*p1<<endl;// cout<<"使用p2访问内存变量: data is "<<*p2<<endl;int* p3 = &data2;cout<<"使用普通指针p3访问内存变量: data2 is "<<*p3<<endl;int* p4 = &data2;cout<<"使用普通指针p4访问内存变量: data2 is "<<*p4<<endl;delete p3;
}
使用普通指针p3访问内存变量: data2 is 101
使用普通指针p4访问内存变量: data2 is 101
munmap_chunk(): invalid pointer
Aborted (core dumped)
unique_str的独占性
前面说到过,unique_str管理的对象只能保证被一个智能指针所管理,但是这个独占性还需要进一步理解。unique_ptr
的"独占性"主要体现在所有权(ownership)而不是访问权限(accessibility)上。通过几个例子来说明:
#include <memory>
#include <iostream>int main() {// 1. 错误示范:两个unique_ptr试图拥有同一块内存int* raw = new int(42);std::unique_ptr<int> ptr1(raw);std::unique_ptr<int> ptr2(raw); // 严重错误!会导致双重释放// 2. 错误示范:复制unique_ptrstd::unique_ptr<int> ptr3 = std::make_unique<int>(42);// std::unique_ptr<int> ptr4 = ptr3; // 编译错误!unique_ptr不能被复制// 3. 合法但危险:普通指针访问unique_ptr管理的内存std::unique_ptr<int> ptr5 = std::make_unique<int>(42);int* dangerous_ptr = ptr5.get(); // 获取原始指针*dangerous_ptr = 100; // 可以访问和修改数据// 当ptr5被销毁时,dangerous_ptr变成悬空指针!return 0;
} // ptr1和ptr2都会尝试删除同一块内存,导致未定义行为
unique_ptr具有匹配的内存释放机制,不需要类似普通指针借助delete关键字:
void ownership_example() {auto ptr = std::make_unique<int>(42);int* raw_ptr = ptr.get(); // raw_ptr只是借用访问权// ptr负责资源的释放,而raw_ptr不应该尝试删除// delete raw_ptr; // 严重错误!
} // ptr自动释放资源
unique_ptr
的"独占性"是指资源所有权的独占,而不是访问权限的独占- 只有拥有所有权的
unique_ptr
才能(也必须)负责资源的释放 - 其他指针可以访问同一资源,但:
- 不能删除资源
- 必须确保访问时资源仍然有效
- 需要注意生命周期管理
- 使用
get()
获取原始指针时要特别小心,确保不会在unique_ptr
释放后继续使用
建议:
- 优先使用
unique_ptr
来管理资源 - 谨慎使用
get()
- 明确资源的所有权
- 如果需要共享所有权,考虑使用
shared_ptr
std::shared_ptr
- 基本用法
在unique_ptr的基础上,其他两种指针相对比较好理解。std::shared_ptr是一种共享所有权的智能指针,允许多个shared_ptr
实例共同拥有同一个对象。对象会在最后一个拥有它的shared_ptr
被销毁时自动释放。
shared_ptr的声明和使用方式与unique_ptr类似:
void example03()
{//shared_ptr初始化方式与unique_prt还是比较一致的std::shared_ptr<Resource> p1(new Resource(100));p1->showValue();p1->setValue(99);p1->showValue();cout<<"当前p1管理的对应引用为: "<<p1.use_count()<<endl;std::shared_ptr<Resource> p2(p1); //使用p1初始化p2cout<<"当前p1管理的对应引用为: "<<p2.use_count()<<endl;std::shared_ptr<Resource> p3 = std::move(p1); //通过移动构造,初始化p3,p1成为空指针cout<<"当前p2管理的对应引用为: "<<p2.use_count()<<endl;
}
被shared_ptr管理的内存数据,每多一次引用,就会增加一次引用次数。但是每释放一次智能指针,引用次数会减少。
Resource constructed: 100
This object value is 100
This object value is 99
当前p1管理的对应引用为: 1
当前p1管理的对应引用为: 2
当前p2管理的对应引用为: 2
Resource destroyed: 99
- 循环引用情况
使用shared_ptr如果不是很注意,会造成循环引用的情况,内存无法被真正释放,造成内存泄漏。下面对几种循环引用的情况进行说明,典型的循环引用案例包括菱形继承、相互引用这些。
- 直接循环引用
// A.h
class B; // 前向声明
class A {B* b_ptr; // 使用指针
};// B.h
class A; // 前向声明
class B {A* a_ptr; // 使用指针
};auto a = std::make_shared<A>();
auto b = std::make_shared<B>();a->ptr_b = b; // A holds a shared pointer to B
b->ptr_a = a; // B holds a shared pointer to A
每当一个新的 std::shared_ptr 被创建或复制时,引用计数器会增加。当一个 std::shared_ptr 被销毁或重新赋值时,引用计数器会减少。如果引用计数器变为零,则删除所管理的对象及其控制块。
而在直接循环引用中,上面代码的计数过程如下:
(1) 创建智能指针a时,A的引用次数加1,从0到1;
(2) 创建智能指针b时,B的引用次数加1,从0到1;
(3) a->ptr_b = b; 使得,b又被a的ptr复制了一次,B的引用次数加1,从1到2;
(4) b->ptr_a = a; 使得,a又被b的ptr复制了一次,A的引用次数加1,从1到2;
当我们离开 main 函数时,会发生以下事情:
局部变量 a 被销毁,这将导致 a 的引用计数减1。但是,由于 b 中的 ptr_a 仍然持有对 a 的引用,所以 a 的引用计数从2变为1,而不是0。同样地,局部变量 b 被销毁,这将导致 b 的引用计数减1。但是,由于 a 中的 ptr_b 仍然持有对 b 的引用,所以 b 的引用计数从2变为1,而不是0。因此,a 和 b 的引用计数都保持为1,而不是降为0。结果是,这两个对象永远不会被销毁,因为它们的引用计数始终大于0,从而导致内存泄漏。
- 父子关系中的循环引用
类的继承和包含关系也是经常被混淆的类设计关系,在使用shared_ptr不注意的话,也容易造成循环引用。
class Child;
class Parent {
public:std::shared_ptr<Child> child;~Parent() { std::cout << "Parent destroyed\n"; }
};class Child {
public:std::shared_ptr<Parent> parent;~Child() { std::cout << "Child destroyed\n"; }
};void parent_child_cycle() {auto parent = std::make_shared<Parent>();auto child = std::make_shared<Child>();// 建立双向引用parent->child = child;child->parent = parent;
} // 内存泄漏!
- 复杂对象关系中的循环引用
class Component;
class Object {
public:std::string name;std::shared_ptr<Component> component;Object(const std::string& n) : name(n) {std::cout << "Object " << name << " created\n";}~Object() {std::cout << "Object " << name << " destroyed\n";}
};class Component {
public:std::string name;std::shared_ptr<Object> owner;Component(const std::string& n) : name(n) {std::cout << "Component " << name << " created\n";}~Component() {std::cout << "Component " << name << " destroyed\n";}
};void complex_cycle() {auto obj = std::make_shared<Object>("MainObject");auto comp = std::make_shared<Component>("MainComponent");obj->component = comp;comp->owner = obj;
} // 内存泄漏!
- 使用weak_ptr解决循环引用
class SafeNode {
public:std::shared_ptr<SafeNode> next;std::weak_ptr<SafeNode> weak_next; // 使用weak_ptr~SafeNode() {std::cout << "SafeNode destroyed\n";}
};void safe_cycle() {auto node1 = std::make_shared<SafeNode>();auto node2 = std::make_shared<SafeNode>();// 一个方向使用shared_ptr,另一个方向使用weak_ptrnode1->next = node2;node2->weak_next = node1; // 不会增加引用计数// 使用weak_ptr时需要检查if (auto shared = node2->weak_next.lock()) {std::cout << "Node1 still exists\n";}
} // 正确释放内存