在 C++ 的编程世界里,内存管理一直是一个让人又爱又恨的话题。手动管理内存,就像是在走钢丝,稍有不慎就会陷入内存泄漏、悬空指针等可怕的陷阱。不过,C++ 为我们提供了智能指针这一强大的工具,它就像是一位贴心的内存管家,能够帮助我们更安全、更轻松地管理内存。今天,就让我们一起深入探秘 C++ 智能指针的奥秘。
在传统的 C++ 编程中,我们使用 new
来动态分配内存,使用 delete
来释放内存。例如:
int* ptr = new int(10);
// 使用 ptr
delete ptr;
看似简单的代码,却隐藏着许多风险。如果在 delete
之前程序因为异常而提前退出,那么 ptr
所指向的内存就无法被释放,从而造成内存泄漏。而且,如果不小心多次释放同一块内存,或者使用已经被释放的内存(悬空指针),程序就会出现未定义行为,导致崩溃或产生难以调试的错误。
智能指针的出现,就是为了解决这些问题。它利用 C++ 的对象生命周期管理机制,自动释放所管理的内存,避免了手动管理内存带来的风险。
std::unique_ptr
是一种独占式的智能指针,它确保同一时间只有一个 unique_ptr
可以指向某块内存。就像一位专一的守护者,它全心全意地守护着自己所指向的内存,不允许其他指针来分享。
#include
#include
int main() {
std::unique_ptr ptr1 = std::make_unique(20);
// std::unique_ptr ptr2 = ptr1; // 错误,不能复制
std::unique_ptr ptr2 = std::move(ptr1); // 可以移动所有权
if (ptr1 == nullptr) {
std::cout << "ptr1 已失去所有权" << std::endl;
}
if (ptr2 != nullptr) {
std::cout << "ptr2 拥有所有权,值为: " << *ptr2 << std::endl;
}
return 0;
}
在上面的代码中,std::make_unique
是一个便捷的函数,用于创建 std::unique_ptr
。注意,不能直接将一个 unique_ptr
赋值给另一个 unique_ptr
,因为这会违反独占式的原则。但可以使用 std::move
来转移所有权。
- 错误地进行复制操作,会导致编译错误。
- 转移所有权后,原指针不再拥有内存,若继续使用会导致悬空指针问题。
std::shared_ptr
是一种共享式的智能指针,它可以让多个 shared_ptr
共同指向同一块内存。它使用引用计数的机制来管理内存,每增加一个指向该内存的 shared_ptr
,引用计数就加 1;每减少一个指向该内存的 shared_ptr
,引用计数就减 1。当引用计数变为 0 时,自动释放内存。
#include
#include
int main() {
std::shared_ptr ptr1 = std::make_shared(30);
std::shared_ptr ptr2 = ptr1; // 可以复制
std::cout << "引用计数: " << ptr1.use_count() << std::endl; // 输出 2
ptr2.reset(); // 释放 ptr2 的所有权
std::cout << "引用计数: " << ptr1.use_count() << std::endl; // 输出 1
return 0;
}
std::make_shared
同样是创建 std::shared_ptr
的便捷函数。通过 use_count
方法可以查看当前内存的引用计数。
循环引用问题:当两个或多个
shared_ptr
相互引用,形成一个循环时,引用计数永远不会变为 0,导致内存泄漏。例如:#include
class B; class A { public: std::shared_ptr b_ptr; ~A() { std::cout << "A destroyed" << std::endl; } }; class B { public: std::shared_ptr a_ptr; ~B() { std::cout << "B destroyed" << std::endl; } }; int main() { std::shared_ptr a = std::make_shared(); std::shared_ptr b = std::make_shared(); a->b_ptr = b; b->a_ptr = a; return 0; }
- 在
main
函数中,首先创建了两个std::shared_ptr
对象a
和b
,分别指向A
和B
类型的对象。此时,a
指向的A
对象的引用计数为 1,b
指向的B
对象的引用计数也为 1。- 接着,执行
a->b_ptr = b;
和b->a_ptr = a;
语句,这使得A
对象中的b_ptr
指向B
对象,B
对象中的a_ptr
指向A
对象。此时,A
对象的引用计数变为 2(a
和b->a_ptr
都指向它),B
对象的引用计数也变为 2(b
和a->b_ptr
都指向它)。- 当
main
函数结束时,a
和b
超出作用域,它们所指向的对象的引用计数会减 1。但是,由于A
对象的b_ptr
仍然指向B
对象,B
对象的a_ptr
仍然指向A
对象,所以A
和B
对象的引用计数都变为 1,而不是 0。因此,这两个对象的内存不会被释放,造成了内存泄漏。
std::weak_ptr
是一种弱引用的智能指针,它可以指向 std::shared_ptr
所管理的内存,但不会增加引用计数。它就像是一个默默的观察者,不影响内存的生命周期。std::weak_ptr
主要用于解决 std::shared_ptr
的循环引用问题。(std::weak_ptr在赋值时可以使用
std::shared_ptr
赋值给std::weak_ptr,也可以的使用另一个std::weak_ptr赋值)
#include
#include
class B;
class A {
public:
std::weak_ptr b_ptr;
~A() { std::cout << "A destroyed" << std::endl; }
};
class B {
public:
std::shared_ptr a_ptr;
~B() { std::cout << "B destroyed" << std::endl; }
};
int main() {
std::shared_ptr a = std::make_shared();
std::shared_ptr b = std::make_shared();
a->b_ptr = b;
b->a_ptr = a;
return 0;
}
A
类中的 std::shared_ptr
改为 std::weak_ptr
。当执行 a->b_ptr = b;
时,B
对象的引用计数不会增加,仍然为 1(只有 b
指向它)。main
函数结束时,a
和 b
超出作用域,A
对象的引用计数减 1 变为 0(因为只有 b->a_ptr
指向它,且 b
超出作用域),A
对象的内存被释放。随着 A
对象被释放,A
对象中的 b_ptr
不再指向 B
对象,但由于 b_ptr
是 std::weak_ptr
,不影响 B
对象的引用计数。接着,B
对象的引用计数也减 1 变为 0,B
对象的内存也被释放。将 A
类中的 std::shared_ptr
改为 std::weak_ptr
后,就打破了循环引用。因为 std::weak_ptr
不会增加引用计数,当 main
函数结束时,a
和 b
的引用计数会正常变为 0,内存会被正确释放。
由于
std::weak_ptr
不拥有对象的所有权,它不能直接解引用(即不能像普通指针那样使用*
或->
操作符来访问所指向的对象)。这是因为std::weak_ptr
所指向的对象可能已经被销毁,如果直接解引用,会导致未定义行为。
lock()
是std::weak_ptr
提供的一个成员函数,它的作用是将std::weak_ptr
转换为一个std::shared_ptr
。当调用lock()
方法时,它会检查std::weak_ptr
所指向的对象是否还存在(即引用计数是否大于 0):
对象存在:如果对象存在,
lock()
会创建一个新的std::shared_ptr
,该std::shared_ptr
指向同一个对象,并且对象的引用计数会加 1。这样,在新的std::shared_ptr
的生命周期内,对象不会被销毁,我们可以安全地通过这个std::shared_ptr
来访问对象。对象已销毁:如果对象已经被销毁(即引用计数为 0),
lock()
会返回一个空的std::shared_ptr
(即std::shared_ptr
的operator bool()
会返回false
)。通过检查返回的std::shared_ptr
是否为空,我们可以避免对已销毁对象的访问。#include
#include class MyClass { public: void doSomething() { std::cout << "Doing something..." << std::endl; } }; int main() { std::shared_ptr sharedPtr = std::make_shared (); std::weak_ptr weakPtr = sharedPtr; // 使用 lock() 方法获取一个 shared_ptr std::shared_ptr newSharedPtr = weakPtr.lock(); if (newSharedPtr) { newSharedPtr->doSomething(); } else { std::cout << "The object has been destroyed." << std::endl; } // 释放 sharedPtr,对象被销毁 sharedPtr.reset(); // 再次尝试使用 lock() 方法 newSharedPtr = weakPtr.lock(); if (newSharedPtr) { newSharedPtr->doSomething(); } else { std::cout << "The object has been destroyed." << std::endl; } return 0; }
- 首先,创建一个
std::shared_ptr
对象sharedPtr
,它指向一个MyClass
类型的对象。- 然后,创建一个
std::weak_ptr
对象weakPtr
,并将sharedPtr
赋值给它。此时,weakPtr
指向同一个对象,但对象的引用计数不会增加。- 调用
weakPtr.lock()
方法,将返回一个std::shared_ptr
对象newSharedPtr
。由于对象还存在,newSharedPtr
不为空,我们可以通过它调用doSomething()
方法。- 调用
sharedPtr.reset()
释放sharedPtr
,对象的引用计数变为 0,对象被销毁。- 再次调用
weakPtr.lock()
方法,此时返回的newSharedPtr
为空,会输出 “The object has been destroyed.”。
int* raw_ptr = new int(40);
std::shared_ptr shared_ptr(raw_ptr);
// 不要再次使用 raw_ptr,否则会导致重复释放或悬空指针问题
delete
来释放内存,但在某些情况下,可能需要自定义删除器。例如,对于使用 malloc
分配的内存,需要使用 free
来释放,可以通过自定义删除器来实现:#include
#include
void my_deleter(int* ptr) {
std::cout << "Custom deleter called" << std::endl;
free(ptr);
}
int main() {
int* raw_ptr = (int*)malloc(sizeof(int));
std::shared_ptr shared_ptr(raw_ptr, my_deleter);
return 0;
}
C++ 智能指针为我们提供了一种安全、高效的内存管理方式。通过深入理解和正确使用三种智能指针,我们可以避免许多内存管理方面的问题,让我们的代码更加健壮和可靠。