文章目录
C++的智能指针
智能指针是C++中的一个强大特性,它能帮助我们更安全、更方便地管理动态分配的内存。让我们通过一些生动的比喻和例子来深入了解智能指针。
1. 为什么需要智能指针?
想象你在玩一个魔法世界的游戏。在这个世界里,你可以创造各种物品(对象),但如果不及时清理,这些物品就会堆积成垃圾,占据宝贵的空间。普通指针就像是没有魔法的普通人,他们可以创造物品,但经常忘记清理。而智能指针就像是具有自动清理能力的魔法师,它们不仅可以创造物品,还能在物品不再需要时自动清理。
2. 智能指针的类型
C++提供了三种主要的智能指针:
std::unique_ptr
:独占所有权的魔法师std::shared_ptr
:共享所有权的魔法师std::weak_ptr
:观察者魔法师
让我们逐一了解这些"魔法师"。
3. std::unique_ptr
unique_ptr
就像一个独占欲很强的魔法师。它创造的物品只能由它自己管理,不能被其他魔法师共享。
#include <memory>
#include <iostream>
class Treasure {
public:
Treasure(std::string n) : name(n) {
std::cout << "Treasure " << name << " created!" << std::endl;
}
~Treasure() {
std::cout << "Treasure " << name << " destroyed!" << std::endl;
}
std::string name;
};
int main() {
{
std::unique_ptr<Treasure> goldCoin = std::make_unique<Treasure>("Gold Coin");
// 使用 goldCoin
std::cout << "I have a " << goldCoin->name << std::endl;
} // goldCoin 在这里自动销毁
std::cout << "End of the spell" << std::endl;
return 0;
}
输出:
Treasure Gold Coin created!
I have a Gold Coin
Treasure Gold Coin destroyed!
End of the spell
注意 goldCoin
是如何在离开作用域时自动被销毁的。这就是 unique_ptr
的魔力!
4. std::shared_ptr
shared_ptr
就像一群友好的魔法师,他们一起管理同一个物品。只有当所有魔法师都不再需要这个物品时,它才会被清理。
#include <memory>
#include <iostream>
class Treasure {
public:
Treasure(std::string n) : name(n) {
std::cout << "Treasure " << name << " created!" << std::endl;
}
~Treasure() {
std::cout << "Treasure " << name << " destroyed!" << std::endl;
}
std::string name;
};
int main() {
std::shared_ptr<Treasure> sharedGem = std::make_shared<Treasure>("Magic Gem");
{
std::shared_ptr<Treasure> anotherGem = sharedGem;
std::cout << "Gem is shared by " << sharedGem.use_count() << " wizards" << std::endl;
}
std::cout << "Now Gem is shared by " << sharedGem.use_count() << " wizard" << std::endl;
return 0; // sharedGem 在这里被销毁
}
输出:
Treasure Magic Gem created!
Gem is shared by 2 wizards
Now Gem is shared by 1 wizard
Treasure Magic Gem destroyed!
看到了吗?Magic Gem
在被两个魔法师共享时并没有被销毁,只有当最后一个魔法师不再需要它时,它才被清理。
解析代码
在这个代码示例中,sharedGem
是一个 std::shared_ptr<Treasure>
,它管理一个名为 “Magic Gem” 的 Treasure
对象。理解 shared_ptr
的生命周期和销毁时机,涉及到 C++ 中作用域和引用计数的概念。
代码解读
-
创建和初始化
shared_ptr
:std::shared_ptr<Treasure> sharedGem = std::make_shared<Treasure>("Magic Gem");
这行代码创建了一个
shared_ptr
,并将其初始化为指向一个名为 “Magic Gem” 的Treasure
对象。此时,shared_ptr
的引用计数为 1。 -
共享
shared_ptr
:{ std::shared_ptr<Treasure> anotherGem = sharedGem; std::cout << "Gem is shared by " << sharedGem.use_count() << " wizards" << std::endl; }
在这个代码块中,
sharedGem
被赋值给anotherGem
,使得anotherGem
也指向同一个Treasure
对象。这会使sharedGem
的引用计数增加到 2。当这个代码块结束时,
anotherGem
超出作用域并被销毁,其持有的引用也会被释放,引用计数将减少为 1。 -
检查引用计数:
std::cout << "Now Gem is shared by " << sharedGem.use_count() << " wizard" << std::endl;
这行代码在
anotherGem
销毁后打印出当前sharedGem
的引用计数,应该显示为 1。 -
返回语句:
return 0;
在程序结束时,
main
函数的最后一个语句是return 0
。由于sharedGem
是在main
函数作用域内定义的局部变量,它会在main
函数的作用域结束时自动销毁。当sharedGem
销毁时,其引用计数会降到 0,因此它管理的 “Magic Gem” 对象也会被销毁。
作用域的理解
sharedGem
的生命周期贯穿整个main
函数。当main
函数执行完毕(即执行return 0
时),sharedGem
超出作用域并被销毁。- 在 C++ 中,局部变量的生命周期受其所在的代码块(或函数)的作用域控制。当程序执行到作用域的结束(通常是右花括号
}
处)时,该作用域内定义的局部变量会自动被销毁。
为什么 sharedGem
在 return 0
之后才被销毁?
sharedGem
是在main
函数中定义的局部变量,它的生命周期与main
函数的执行周期一致。- 在
main
函数即将结束时,所有在main
函数作用域内定义的局部变量都会被销毁。因此,sharedGem
会在return 0
之后销毁,因为此时main
函数的作用域结束。 - 当
sharedGem
销毁时,其引用计数降为 0,导致 “Magic Gem” 对象被销毁。
总结
shared_ptr
对象在其作用域结束时会自动销毁,这通常发生在函数结束或显式块(用{}
包裹的代码块)结束时。- 当
shared_ptr
的引用计数降为 0 时,它所管理的对象会自动释放。在这个例子中,sharedGem
在return 0
后才被销毁,因为它是main
函数的局部变量,而main
函数结束意味着作用域的结束。
5. std::weak_ptr
weak_ptr
像是一个没有实际魔力的观察者。它可以查看 shared_ptr
管理的物品,但不会增加引用计数。
#include <memory>
#include <iostream>
class Treasure {
public:
Treasure(std::string n) : name(n) {
std::cout << "Treasure " << name << " created!" << std::endl;
}
~Treasure() {
std::cout << "Treasure " << name << " destroyed!" << std::endl;
}
std::string name;
};
int main() {
std::weak_ptr<Treasure> weakGem;
{
std::shared_ptr<Treasure> sharedGem = std::make_shared<Treasure>("Magic Gem");
weakGem = sharedGem;
if (auto temp = weakGem.lock()) {
std::cout << "I can see the " << temp->name << std::endl;
}
}
if (weakGem.expired()) {
std::cout << "The gem has vanished!" << std::endl;
}
return 0;
}
输出:
Treasure Magic Gem created!
I can see the Magic Gem
Treasure Magic Gem destroyed!
The gem has vanished!
weak_ptr
允许我们观察一个对象,而不会影响它的生命周期。
6. 智能指针的使用建议
- 优先使用
unique_ptr
,除非你确实需要共享所有权。 - 使用
make_unique
和make_shared
来创建智能指针,而不是直接使用new
。 - 避免使用裸指针,尽可能使用智能指针。
- 使用
weak_ptr
来解决循环引用问题。
7. 注意事项
- 不要手动
delete
智能指针管理的对象。 - 避免将同一个裸指针赋值给多个智能指针。
- 小心使用
get()
方法,不要用它返回的裸指针来创建另一个智能指针。
总结
智能指针就像是C++世界中的自动清洁魔法师。它们帮助我们管理内存,防止内存泄漏,使得代码更安全、更易于维护。通过正确使用 unique_ptr
、shared_ptr
和 weak_ptr
,我们可以创建出更加健壮和高效的C++程序。记住,在这个魔法世界中,让智能指针来处理你的"垃圾",你就可以专注于创造更多精彩的"魔法"了!
另一种讲解
shared_ptr 是 C++ 标准库中的一种智能指针,主要用于管理动态分配的内存。它可以自动管理对象的生命周期,避免内存泄漏。当多个 shared_ptr 指向同一个对象时,该对象的引用计数会增加。当最后一个 shared_ptr 被销毁或重置时,引用计数降为零,所指向的对象会被自动释放。
为了更好地理解 shared_ptr
的工作原理及其引用计数机制,下面通过一个简单的示例来详细说明。
示例代码
#include <iostream>
#include <memory>
class MyClass {
public:
MyClass() {
std::cout << "MyClass Constructor\n";
}
~MyClass() {
std::cout << "MyClass Destructor\n";
}
void display() {
std::cout << "Hello from MyClass\n";
}
};
int main() {
std::shared_ptr<MyClass> ptr1(new MyClass()); // 创建一个shared_ptr,引用计数为1
{
std::shared_ptr<MyClass> ptr2 = ptr1; // ptr2也指向同一个对象,引用计数增加到2
std::cout << "Reference Count: " << ptr1.use_count() << std::endl; // 输出引用计数
ptr2->display(); // 使用ptr2访问对象的成员函数
} // 离开作用域,ptr2被销毁,引用计数减少到1
std::cout << "Reference Count: " << ptr1.use_count() << std::endl; // 输出引用计数
ptr1.reset(); // 手动重置ptr1,引用计数减少到0,MyClass对象被销毁
return 0;
}
逐步解析
-
创建
shared_ptr
:std::shared_ptr<MyClass> ptr1(new MyClass());
- 这里我们创建了一个
shared_ptr
对象ptr1
,指向一个动态分配的MyClass
实例。此时,引用计数为 1。 - 输出结果:
MyClass Constructor
- 这里我们创建了一个
-
复制
shared_ptr
:std::shared_ptr<MyClass> ptr2 = ptr1;
- 现在我们创建了另一个
shared_ptr
对象ptr2
,它指向同一个MyClass
实例。引用计数增加到 2。 - 通过
ptr1.use_count()
或ptr2.use_count()
可以得到当前的引用计数,即 2。 - 输出结果:
Reference Count: 2
- 现在我们创建了另一个
-
使用
shared_ptr
访问对象成员:ptr2->display();
- 我们可以使用
ptr2
访问MyClass
对象的成员函数display()
。 - 输出结果:
Hello from MyClass
- 我们可以使用
-
超出作用域销毁
shared_ptr
:- 当代码块执行完毕后,
ptr2
离开作用域被销毁,引用计数自动减少到 1。 - 再次输出引用计数,即 1。
- 输出结果:
Reference Count: 1
- 当代码块执行完毕后,
-
手动重置
shared_ptr
:ptr1.reset();
- 调用
reset()
方法将ptr1
重置为空指针。此时,引用计数减少到 0。 - 当引用计数为 0 时,
shared_ptr
会自动销毁所管理的对象,即MyClass
实例的析构函数被调用。 - 输出结果:
MyClass Destructor
- 调用
完整输出结果
MyClass Constructor
Reference Count: 2
Hello from MyClass
Reference Count: 1
MyClass Destructor
关键点总结
- 引用计数的变化: 每当一个新的
shared_ptr
指向同一个对象时,引用计数增加;每当一个shared_ptr
被销毁或重置时,引用计数减少。 - 对象的生命周期管理: 当引用计数降为 0 时,
shared_ptr
会自动释放所管理的对象,避免内存泄漏。 - 作用域的影响:
shared_ptr
的生命周期与作用域紧密相关,当超出作用域时,指针会被自动销毁,并更新引用计数。
这个简单的示例说明了 shared_ptr
的基本用法以及它如何帮助管理动态内存。