C++ std::shared_ptr 的引用计数原理
C++ std::shared_ptr 的引用计数原理
什么是 std::shared_ptr
在C++ 中,std::shared_ptr
是智能指针的一种,它提供了一种自动管理动态分配对象生命周期的机制。std::shared_ptr
通过引用计数的方式来决定所指向对象的销毁时机。当指向某个对象的最后一个 std::shared_ptr
被销毁时,该对象也会被自动释放。这种机制极大地简化了内存管理,有效地避免了内存泄漏等问题。
引用计数的基本概念
引用计数是一种跟踪对象被引用次数的技术。对于 std::shared_ptr
而言,每个 std::shared_ptr
对象都维护着一个引用计数,该计数记录了当前有多少个 std::shared_ptr
对象指向同一个动态分配的对象。每当创建一个新的 std::shared_ptr
指向某个对象时,引用计数会增加;而当一个 std::shared_ptr
被销毁(例如超出作用域)时,引用计数会减少。当引用计数变为 0 时,意味着没有任何 std::shared_ptr
再指向该对象,此时该对象就会被释放。
引用计数的实现方式
- 独立的控制块
std::shared_ptr
通常通过一个独立的控制块来管理引用计数。这个控制块不仅包含引用计数,还可能包含其他信息,例如弱引用计数(用于std::weak_ptr
,后面会详细介绍)等。当使用std::make_shared
或者直接通过std::shared_ptr
的构造函数来创建一个std::shared_ptr
时,会同时分配一个控制块。- 例如,假设有以下代码:
#include <iostream>
#include <memory>
class MyClass {
public:
MyClass() { std::cout << "MyClass constructor" << std::endl; }
~MyClass() { std::cout << "MyClass destructor" << std::endl; }
};
int main() {
std::shared_ptr<MyClass> ptr1 = std::make_shared<MyClass>();
std::shared_ptr<MyClass> ptr2 = ptr1;
return 0;
}
- 在上述代码中,
std::make_shared<MyClass>()
不仅分配了MyClass
对象的内存,还创建了一个控制块。ptr1
指向MyClass
对象,并且控制块中的引用计数初始化为 1。当std::shared_ptr<MyClass> ptr2 = ptr1;
执行时,ptr2
也指向了同一个MyClass
对象,控制块中的引用计数增加到 2。当ptr1
和ptr2
超出作用域时,引用计数会依次减少,当引用计数变为 0 时,MyClass
对象和控制块都会被释放。
- 控制块的内存分配
- 控制块的内存分配方式有多种。在
std::make_shared
的情况下,控制块和对象的内存通常是在一次内存分配中完成的,这被称为“一次分配”策略。这种策略可以提高内存分配效率,减少内存碎片。 - 例如:
- 控制块的内存分配方式有多种。在
#include <iostream>
#include <memory>
class BigObject {
char data[1024 * 1024];
public:
BigObject() { std::cout << "BigObject constructor" << std::endl; }
~BigObject() { std::cout << "BigObject destructor" << std::endl; }
};
int main() {
auto sp1 = std::make_shared<BigObject>();
return 0;
}
-
这里,
std::make_shared<BigObject>()
会分配一块足够大的内存,既包含BigObject
对象本身的内存,也包含控制块的内存。这种方式相比于分别分配对象和控制块的内存,减少了内存分配的次数,提高了效率。 -
而当通过
std::shared_ptr
的构造函数直接传递一个裸指针时,可能会采用不同的内存分配策略。例如:
#include <iostream>
#include <memory>
class SmallObject {
public:
SmallObject() { std::cout << "SmallObject constructor" << std::endl; }
~SmallObject() { std::cout << "SmallObject destructor" << std::endl; }
};
int main() {
SmallObject* rawPtr = new SmallObject();
std::shared_ptr<SmallObject> sp2(rawPtr);
return 0;
}
- 在这种情况下,对象和控制块的内存可能是分开分配的。这种灵活性在某些场景下是必要的,例如当你需要对对象的内存分配进行特殊控制时。
引用计数的线程安全性
- 基本线程安全特性
std::shared_ptr
的引用计数操作在多线程环境下是基本线程安全的。这意味着在不同线程中对同一个std::shared_ptr
进行引用计数的增减操作不会导致数据竞争。例如:
#include <iostream>
#include <memory>
#include <thread>
class ThreadSafeClass {
public:
ThreadSafeClass() { std::cout << "ThreadSafeClass constructor" << std::endl; }
~ThreadSafeClass() { std::cout << "ThreadSafeClass destructor" << std::endl; }
};
void threadFunction(std::shared_ptr<ThreadSafeClass>& sharedPtr) {
std::shared_ptr<ThreadSafeClass> localPtr = sharedPtr;
// 这里可以对 localPtr 进行操作,引用计数的增减是线程安全的
}
int main() {
std::shared_ptr<ThreadSafeClass> globalPtr = std::make_shared<ThreadSafeClass>();
std::thread t1(threadFunction, std::ref(globalPtr));
std::thread t2(threadFunction, std::ref(globalPtr));
t1.join();
t2.join();
return 0;
}
- 在上述代码中,
t1
和t2
线程都对globalPtr
进行操作,std::shared_ptr
的引用计数操作保证了不会出现数据竞争问题。
- 注意事项
- 然而,虽然引用计数操作是线程安全的,但对
std::shared_ptr
所指向对象的访问本身并不是线程安全的。如果多个线程同时访问std::shared_ptr
所指向对象的成员变量或成员函数,可能会导致数据竞争。例如:
- 然而,虽然引用计数操作是线程安全的,但对
#include <iostream>
#include <memory>
#include <thread>
class UnsafeClass {
public:
int value;
UnsafeClass() : value(0) { std::cout << "UnsafeClass constructor" << std::endl; }
~UnsafeClass() { std::cout << "UnsafeClass destructor" << std::endl; }
};
void accessObject(std::shared_ptr<UnsafeClass>& sharedPtr) {
for (int i = 0; i < 1000; ++i) {
sharedPtr->value++;
}
}
int main() {
std::shared_ptr<UnsafeClass> globalPtr = std::make_shared<UnsafeClass>();
std::thread t1(accessObject, std::ref(globalPtr));
std::thread t2(accessObject, std::ref(globalPtr));
t1.join();
t2.join();
std::cout << "Final value: " << globalPtr->value << std::endl;
return 0;
}
- 在这个例子中,
t1
和t2
线程同时对UnsafeClass
对象的value
成员变量进行递增操作,由于没有同步机制,这会导致数据竞争,最终value
的值可能并不是预期的 2000。为了避免这种情况,需要使用同步机制,如互斥锁等。
引用计数与循环引用
- 循环引用的产生
- 循环引用是
std::shared_ptr
使用中可能出现的一个问题。当两个或多个std::shared_ptr
对象相互引用,形成一个环时,就会产生循环引用。例如:
- 循环引用是
#include <iostream>
#include <memory>
class B;
class A {
public:
std::shared_ptr<B> ptrToB;
A() { std::cout << "A constructor" << std::endl; }
~A() { std::cout << "A destructor" << std::endl; }
};
class B {
public:
std::shared_ptr<A> ptrToA;
B() { std::cout << "B constructor" << std::endl; }
~B() { std::cout << "B destructor" << std::endl; }
};
int main() {
std::shared_ptr<A> a = std::make_shared<A>();
std::shared_ptr<B> b = std::make_shared<B>();
a->ptrToB = b;
b->ptrToA = a;
return 0;
}
- 在上述代码中,
a
指向A
对象,b
指向B
对象,然后a->ptrToB
使得A
对象中的std::shared_ptr
指向B
对象,b->ptrToA
使得B
对象中的std::shared_ptr
指向A
对象,形成了循环引用。
-
循环引用的影响
- 由于循环引用的存在,
A
对象和B
对象的引用计数永远不会变为 0。当a
和b
超出作用域时,A
对象和B
对象的引用计数只会从 2 减到 1,因为它们相互引用。这就导致A
和B
对象无法被释放,从而造成内存泄漏。
- 由于循环引用的存在,
-
解决循环引用的方法 - 使用 std::weak_ptr
std::weak_ptr
是解决循环引用问题的有效手段。std::weak_ptr
不增加引用计数,它只是观察std::shared_ptr
所指向的对象。当std::shared_ptr
所指向的对象被释放时,std::weak_ptr
会自动变为空。- 对上述代码进行修改,使用
std::weak_ptr
来打破循环引用:
#include <iostream>
#include <memory>
class B;
class A {
public:
std::weak_ptr<B> ptrToB;
A() { std::cout << "A constructor" << std::endl; }
~A() { std::cout << "A destructor" << std::endl; }
};
class B {
public:
std::weak_ptr<A> ptrToA;
B() { std::cout << "B constructor" << std::endl; }
~B() { std::cout << "B destructor" << std::endl; }
};
int main() {
std::shared_ptr<A> a = std::make_shared<A>();
std::shared_ptr<B> b = std::make_shared<B>();
a->ptrToB = b;
b->ptrToA = a;
return 0;
}
- 在这个修改后的代码中,
A
中的ptrToB
和B
中的ptrToA
都变为std::weak_ptr
。当a
和b
超出作用域时,A
和B
对象的引用计数会减为 0,从而被正确释放,避免了内存泄漏。
引用计数与自定义删除器
-
自定义删除器的概念
std::shared_ptr
允许用户提供自定义删除器。默认情况下,当引用计数变为 0 时,std::shared_ptr
会调用delete
来释放所指向的对象。但在某些情况下,需要使用自定义的释放逻辑。例如,当对象是通过malloc
分配的,就需要使用free
来释放;或者当对象涉及到资源管理(如文件句柄、数据库连接等),需要特殊的关闭操作。- 自定义删除器是一个可调用对象(函数指针、函数对象或 lambda 表达式),它接受一个指向对象的指针,并负责释放该对象。
-
使用自定义删除器的示例 - 函数指针
#include <iostream>
#include <memory>
void customDelete(void* ptr) {
std::cout << "Custom delete function" << std::endl;
free(ptr);
}
int main() {
void* rawPtr = std::malloc(100);
std::shared_ptr<void> sp(rawPtr, customDelete);
return 0;
}
- 在上述代码中,
std::shared_ptr<void> sp(rawPtr, customDelete);
使用了自定义删除器customDelete
。当sp
超出作用域,引用计数变为 0 时,会调用customDelete
函数来释放rawPtr
所指向的内存。
- 使用自定义删除器的示例 - lambda 表达式
#include <iostream>
#include <memory>
#include <fstream>
class FileWrapper {
public:
std::fstream file;
FileWrapper(const std::string& filename) : file(filename, std::ios::out) {
if (!file) {
std::cerr << "Failed to open file" << std::endl;
}
}
};
int main() {
std::shared_ptr<FileWrapper> filePtr(new FileWrapper("test.txt"), [](FileWrapper* ptr) {
ptr->file.close();
delete ptr;
});
// 在 filePtr 超出作用域时,会调用 lambda 表达式中的自定义删除逻辑
return 0;
}
- 这里,
std::shared_ptr<FileWrapper>
使用了一个 lambda 表达式作为自定义删除器。当filePtr
超出作用域时,lambda 表达式会先关闭文件,然后释放FileWrapper
对象。
引用计数的性能考量
- 引用计数操作的开销
- 虽然
std::shared_ptr
极大地简化了内存管理,但引用计数操作本身是有开销的。每次std::shared_ptr
的构造、赋值或销毁操作,都需要对引用计数进行原子操作(以保证线程安全)。这些原子操作在性能敏感的场景下可能会成为瓶颈。 - 例如,在一个频繁创建和销毁
std::shared_ptr
的循环中:
- 虽然
#include <iostream>
#include <memory>
#include <chrono>
class SimpleClass {
public:
SimpleClass() { }
~SimpleClass() { }
};
int main() {
auto start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < 1000000; ++i) {
std::shared_ptr<SimpleClass> sp = std::make_shared<SimpleClass>();
}
auto end = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
std::cout << "Duration: " << duration << " ms" << std::endl;
return 0;
}
- 在这个例子中,频繁的引用计数操作会增加循环执行的时间。如果对性能要求极高,可以考虑使用其他内存管理方式,如对象池等。
- 减少引用计数开销的方法
-
一种减少引用计数开销的方法是尽量减少
std::shared_ptr
的创建和销毁次数。例如,可以使用对象池来复用对象,而不是每次都创建新的std::shared_ptr
。另外,在性能敏感的代码段中,避免不必要的std::shared_ptr
赋值操作,因为这也会导致引用计数的变化。 -
例如,假设有一个对象池的实现:
-
#include <iostream>
#include <memory>
#include <vector>
#include <queue>
class Object {
public:
Object() { std::cout << "Object constructor" << std::endl; }
~Object() { std::cout << "Object destructor" << std::endl; }
};
class ObjectPool {
private:
std::queue<std::shared_ptr<Object>> pool;
std::vector<std::shared_ptr<Object>> allObjects;
public:
ObjectPool(int initialSize) {
for (int i = 0; i < initialSize; ++i) {
auto obj = std::make_shared<Object>();
pool.push(obj);
allObjects.push_back(obj);
}
}
std::shared_ptr<Object> getObject() {
if (pool.empty()) {
auto newObj = std::make_shared<Object>();
allObjects.push_back(newObj);
return newObj;
}
auto obj = pool.front();
pool.pop();
return obj;
}
void returnObject(std::shared_ptr<Object> obj) {
pool.push(obj);
}
};
int main() {
ObjectPool pool(10);
auto obj1 = pool.getObject();
auto obj2 = pool.getObject();
pool.returnObject(obj1);
return 0;
}
- 在这个对象池的实现中,通过复用
std::shared_ptr
对象,减少了引用计数的创建和销毁次数,从而在一定程度上提高了性能。
引用计数与动态类型转换
- 使用 std::dynamic_pointer_cast 进行动态类型转换
- 在使用
std::shared_ptr
时,有时需要进行动态类型转换。std::dynamic_pointer_cast
可以实现这一功能。它不仅进行类型转换,还会正确处理引用计数。 - 例如,假设有一个继承体系:
- 在使用
#include <iostream>
#include <memory>
class Base {
public:
virtual void print() { std::cout << "Base" << std::endl; }
virtual ~Base() { }
};
class Derived : public Base {
public:
void print() override { std::cout << "Derived" << std::endl; }
};
int main() {
std::shared_ptr<Base> basePtr = std::make_shared<Derived>();
std::shared_ptr<Derived> derivedPtr = std::dynamic_pointer_cast<Derived>(basePtr);
if (derivedPtr) {
derivedPtr->print();
}
return 0;
}
- 在上述代码中,
std::dynamic_pointer_cast<Derived>(basePtr)
将basePtr
从std::shared_ptr<Base>
转换为std::shared_ptr<Derived>
。如果转换成功,derivedPtr
会指向Base
指针所指向的Derived
对象,并且引用计数会被正确处理。
- 动态类型转换与引用计数的关系
-
std::dynamic_pointer_cast
的实现会保持原std::shared_ptr
的引用计数不变。如果转换成功,新的std::shared_ptr
会与原std::shared_ptr
共享控制块,引用计数增加。如果转换失败,会返回一个空的std::shared_ptr
,不会影响原std::shared_ptr
的引用计数。 -
例如:
-
#include <iostream>
#include <memory>
class Base {
public:
virtual void print() { std::cout << "Base" << std::endl; }
virtual ~Base() { }
};
class Derived1 : public Base {
public:
void print() override { std::cout << "Derived1" << std::endl; }
};
class Derived2 : public Base {
public:
void print() override { std::cout << "Derived2" << std::endl; }
};
int main() {
std::shared_ptr<Base> basePtr = std::make_shared<Derived1>();
std::shared_ptr<Derived2> derived2Ptr = std::dynamic_pointer_cast<Derived2>(basePtr);
if (derived2Ptr) {
derived2Ptr->print();
} else {
std::cout << "Dynamic cast failed" << std::endl;
}
return 0;
}
- 在这个例子中,
std::dynamic_pointer_cast<Derived2>(basePtr)
由于basePtr
实际指向的是Derived1
对象,转换失败,derived2Ptr
为空,basePtr
的引用计数不受影响。
引用计数在实际项目中的应用场景
- 模块间对象传递
- 在大型项目中,不同模块之间经常需要传递对象。使用
std::shared_ptr
可以方便地管理对象的生命周期,避免在模块边界处出现内存泄漏。例如,一个图形渲染模块可能会将一个渲染资源对象传递给另一个特效处理模块。
- 在大型项目中,不同模块之间经常需要传递对象。使用
#include <iostream>
#include <memory>
class RenderResource {
public:
RenderResource() { std::cout << "RenderResource constructor" << std::endl; }
~RenderResource() { std::cout << "RenderResource destructor" << std::endl; }
};
class EffectModule {
public:
void processEffect(std::shared_ptr<RenderResource> resource) {
// 处理特效
std::cout << "Processing effect with resource" << std::endl;
}
};
class RenderModule {
public:
std::shared_ptr<RenderResource> createResource() {
return std::make_shared<RenderResource>();
}
};
int main() {
RenderModule renderModule;
EffectModule effectModule;
auto resource = renderModule.createResource();
effectModule.processEffect(resource);
return 0;
}
- 在这个例子中,
RenderModule
创建了一个RenderResource
对象,并通过std::shared_ptr
传递给EffectModule
。两个模块都不需要关心资源的释放,std::shared_ptr
会自动管理其生命周期。
- 容器中存储对象
- 当在容器(如
std::vector
、std::list
等)中存储对象时,std::shared_ptr
可以保证对象在容器中的生命周期管理。例如,一个游戏开发项目中,可能有一个容器存储各种游戏对象:
- 当在容器(如
#include <iostream>
#include <memory>
#include <vector>
class GameObject {
public:
GameObject() { std::cout << "GameObject constructor" << std::endl; }
~GameObject() { std::cout << "GameObject destructor" << std::endl; }
};
int main() {
std::vector<std::shared_ptr<GameObject>> gameObjects;
gameObjects.push_back(std::make_shared<GameObject>());
gameObjects.push_back(std::make_shared<GameObject>());
// 当 gameObjects 超出作用域时,所有 GameObject 对象会被正确释放
return 0;
}
- 在这个例子中,
std::vector<std::shared_ptr<GameObject>>
存储了GameObject
对象的std::shared_ptr
。当gameObjects
超出作用域时,std::shared_ptr
的引用计数机制会确保所有GameObject
对象被正确释放。
- 资源管理
- 对于一些需要特殊资源管理的对象,如数据库连接、网络套接字等,
std::shared_ptr
结合自定义删除器可以有效地管理资源的生命周期。例如:
- 对于一些需要特殊资源管理的对象,如数据库连接、网络套接字等,
#include <iostream>
#include <memory>
#include <mysql/mysql.h>
class MySQLConnection {
public:
MYSQL* conn;
MySQLConnection() {
conn = mysql_init(nullptr);
if (!conn) {
std::cerr << "mysql_init() failed" << std::endl;
}
}
~MySQLConnection() {
mysql_close(conn);
}
};
void customMySQLDelete(MySQLConnection* conn) {
mysql_close(conn->conn);
delete conn;
}
int main() {
std::shared_ptr<MySQLConnection> mysqlConn(new MySQLConnection(), customMySQLDelete);
// 使用 mysqlConn 进行数据库操作
return 0;
}
- 在这个例子中,
std::shared_ptr<MySQLConnection>
使用自定义删除器customMySQLDelete
来正确关闭 MySQL 连接并释放对象。这样可以确保在std::shared_ptr
生命周期结束时,数据库连接资源被正确释放。
通过深入理解 std::shared_ptr
的引用计数原理,开发者可以更加有效地使用它来管理内存和资源,避免常见的内存管理问题,提高程序的稳定性和性能。在实际项目中,根据不同的场景合理运用 std::shared_ptr
,结合自定义删除器、线程同步等技术,可以构建出高效、可靠的 C++ 程序。同时,要注意 std::shared_ptr
的性能开销以及循环引用等问题,在合适的地方采用优化策略和解决方案,以满足项目的需求。