WEBKT

C++智能指针避坑指南?原理、场景与循环引用全解析

56 0 0 0

1. 智能指针是个啥?解决了什么问题?

1.1 为什么需要智能指针?

1.2 智能指针的种类

2. unique_ptr:我的地盘我做主

2.1 unique_ptr 的特点

2.2 unique_ptr 的使用场景

2.3 unique_ptr 的代码示例

2.4 unique_ptr 的所有权转移

2.5 unique_ptr 与自定义删除器

3. shared_ptr:大家好才是真的好

3.1 shared_ptr 的特点

3.2 shared_ptr 的使用场景

3.3 shared_ptr 的代码示例

3.4 shared_ptr 的循环引用问题

4. weak_ptr:打破循环的利器

4.1 weak_ptr 的特点

4.2 weak_ptr 的使用场景

4.3 weak_ptr 的代码示例

4.4 如何使用 weak_ptr 访问对象

5. 总结

作为一名C++老鸟,内存管理绝对是绕不开的话题。手动管理内存?那简直是噩梦,一不小心就内存泄漏、野指针满天飞。还好,C++11引入了智能指针,让咱们摆脱了手动 newdelete 的苦海。但是!智能指针用不好,照样会翻车!今天就来跟大家聊聊 C++ 里的智能指针,保证让你看完之后,腰不酸了,腿不疼了,代码也更自信了。

1. 智能指针是个啥?解决了什么问题?

简单来说,智能指针就是个类,它能像指针一样指向内存,最关键的是,它能自动管理所指内存的生命周期。当智能指针销毁时,会自动释放它所管理的内存。这样一来,咱们就不用手动 delete 了,大大降低了内存泄漏的风险。

1.1 为什么需要智能指针?

在C++中,动态内存管理是个老大难问题。如果我们使用 new 来分配内存,就必须使用 delete 来释放内存。但稍有不慎,就可能出现以下问题:

  • 内存泄漏(Memory Leak):分配了内存,但忘记释放,导致内存越用越少。
  • 野指针(Dangling Pointer):指针指向的内存已经被释放,但指针仍然存在,访问它会导致程序崩溃。
  • 重复释放(Double Free):同一块内存被释放多次,导致程序崩溃。

智能指针的出现,就是为了解决这些问题。它通过RAII(Resource Acquisition Is Initialization)机制,将资源的获取和释放与对象的生命周期绑定在一起,从而实现了自动内存管理。

1.2 智能指针的种类

C++11 提供了三种智能指针:

  • unique_ptr:独占式指针,同一时间只能有一个 unique_ptr 指向同一块内存。
  • shared_ptr:共享式指针,多个 shared_ptr 可以指向同一块内存,内部使用引用计数来跟踪有多少个 shared_ptr 指向该内存,当引用计数为 0 时,自动释放内存。
  • weak_ptr:弱引用指针,不增加引用计数,主要用于解决 shared_ptr 的循环引用问题。

2. unique_ptr:我的地盘我做主

unique_ptr 是个独占式指针,它拥有它所指向的内存的唯一所有权。这意味着,同一时间只能有一个 unique_ptr 指向同一块内存。当 unique_ptr 被销毁时,它所管理的内存也会被自动释放。

2.1 unique_ptr 的特点

  • 独占性:保证了资源只能被一个对象拥有,避免了多个对象同时操作同一资源带来的风险。
  • 自动释放:当 unique_ptr 离开作用域时,会自动释放它所管理的内存,无需手动 delete
  • 禁止拷贝unique_ptr 禁止拷贝构造和赋值操作,避免了所有权转移不明确的问题。但允许移动构造和移动赋值操作,可以显式地转移所有权。

2.2 unique_ptr 的使用场景

  • 管理动态分配的内存:这是 unique_ptr 最常见的用途,可以确保动态分配的内存被正确释放。
  • 工厂模式:在工厂函数中返回 unique_ptr,可以避免手动管理内存的麻烦。
  • Pimpl 模式:将类的实现细节隐藏在私有成员中,使用 unique_ptr 管理这些私有成员的内存。

2.3 unique_ptr 的代码示例

#include <iostream>
#include <memory>
class MyClass {
public:
MyClass() {
std::cout << "MyClass constructor called" << std::endl;
}
~MyClass() {
std::cout << "MyClass destructor called" << std::endl;
}
void doSomething() {
std::cout << "MyClass is doing something" << std::endl;
}
};
int main() {
// 创建一个 unique_ptr,指向 MyClass 对象
std::unique_ptr<MyClass> ptr(new MyClass());
// 使用 unique_ptr 访问 MyClass 对象
ptr->doSomething();
// unique_ptr 离开作用域时,会自动释放 MyClass 对象的内存
return 0;
}

代码解释:

  1. 我们使用 std::unique_ptr<MyClass> ptr(new MyClass()); 创建了一个 unique_ptr,它指向一个 MyClass 对象。
  2. 我们可以像使用普通指针一样,使用 ptr->doSomething(); 访问 MyClass 对象。
  3. ptr 离开 main 函数的作用域时,unique_ptr 会自动调用 delete 释放 MyClass 对象的内存。可以看到 MyClass 的析构函数被调用了。

2.4 unique_ptr 的所有权转移

由于 unique_ptr 是独占式的,所以不能进行拷贝构造和赋值操作。但是,我们可以使用移动构造和移动赋值操作来转移所有权。

#include <iostream>
#include <memory>
int main() {
std::unique_ptr<int> ptr1(new int(10));
// 移动构造
std::unique_ptr<int> ptr2 = std::move(ptr1);
// 现在 ptr1 不再拥有所有权,访问它会导致程序崩溃
// std::cout << *ptr1 << std::endl; // 错误!
// ptr2 拥有所有权,可以正常访问
std::cout << *ptr2 << std::endl; // 输出 10
return 0;
}

代码解释:

  1. 我们使用 std::move(ptr1)ptr1 的所有权转移给了 ptr2
  2. 现在 ptr1 不再拥有所有权,访问它会导致程序崩溃。
  3. ptr2 拥有了所有权,可以正常访问它所指向的内存。

2.5 unique_ptr 与自定义删除器

unique_ptr 默认使用 delete 来释放内存。但有时我们需要使用自定义的删除器,例如,使用 delete[] 释放数组内存,或者使用特定的函数来释放资源。unique_ptr 允许我们指定自定义的删除器。

#include <iostream>
#include <memory>
int main() {
// 使用 delete[] 释放数组内存
std::unique_ptr<int[], std::default_delete<int[]>> array(new int[10]);
// 使用自定义函数释放资源
auto deleter = [](FILE* fp) { fclose(fp); };
std::unique_ptr<FILE, decltype(deleter)> file(fopen("test.txt", "w"), deleter);
if (file) {
fprintf(file.get(), "Hello, world!");
}
// unique_ptr 离开作用域时,会自动调用删除器释放资源
return 0;
}

代码解释:

  1. 对于数组,我们需要使用 delete[] 来释放内存,因此我们需要指定 std::default_delete<int[]> 作为删除器。
  2. 对于文件资源,我们需要使用 fclose 来关闭文件,因此我们定义了一个 lambda 表达式作为删除器,并在创建 unique_ptr 时指定它。

3. shared_ptr:大家好才是真的好

shared_ptr 是个共享式指针,多个 shared_ptr 可以指向同一块内存。它内部使用引用计数来跟踪有多少个 shared_ptr 指向该内存,当引用计数为 0 时,自动释放内存。

3.1 shared_ptr 的特点

  • 共享所有权:允许多个 shared_ptr 共享同一块内存的所有权。
  • 引用计数:使用引用计数来跟踪有多少个 shared_ptr 指向该内存,当引用计数为 0 时,自动释放内存。
  • 线程安全:引用计数的操作是线程安全的,可以在多线程环境中使用。

3.2 shared_ptr 的使用场景

  • 多个对象需要共享同一资源:例如,多个对象需要访问同一个数据库连接。
  • 循环数据结构:例如,双向链表或树结构,可以使用 shared_ptr 来管理节点之间的关系。
  • 缓存:可以使用 shared_ptr 来管理缓存中的对象,当没有对象使用缓存时,自动释放缓存。

3.3 shared_ptr 的代码示例

#include <iostream>
#include <memory>
class MyClass {
public:
MyClass() {
std::cout << "MyClass constructor called" << std::endl;
}
~MyClass() {
std::cout << "MyClass destructor called" << std::endl;
}
void doSomething() {
std::cout << "MyClass is doing something" << std::endl;
}
};
int main() {
// 创建一个 shared_ptr,指向 MyClass 对象
std::shared_ptr<MyClass> ptr1(new MyClass());
// 多个 shared_ptr 可以指向同一个 MyClass 对象
std::shared_ptr<MyClass> ptr2 = ptr1;
std::shared_ptr<MyClass> ptr3 = ptr1;
// 引用计数为 3
std::cout << "Reference count: " << ptr1.use_count() << std::endl;
// 当 shared_ptr 离开作用域时,引用计数减 1
return 0;
}

代码解释:

  1. 我们使用 std::shared_ptr<MyClass> ptr1(new MyClass()); 创建了一个 shared_ptr,它指向一个 MyClass 对象。
  2. 我们可以将 ptr1 赋值给其他 shared_ptr,例如 ptr2ptr3,这样多个 shared_ptr 就可以指向同一个 MyClass 对象。
  3. ptr1.use_count() 可以获取引用计数,当前引用计数为 3。
  4. shared_ptr 离开作用域时,引用计数减 1,当引用计数为 0 时,会自动释放 MyClass 对象的内存。

3.4 shared_ptr 的循环引用问题

shared_ptr 虽然好用,但也有个坑:循环引用。当两个或多个对象互相持有对方的 shared_ptr 时,就会形成循环引用,导致引用计数永远不为 0,从而造成内存泄漏。

#include <iostream>
#include <memory>
class A;
class B;
class A {
public:
std::shared_ptr<B> b;
~A() { std::cout << "~A()" << std::endl; }
};
class B {
public:
std::shared_ptr<A> a;
~B() { std::cout << "~B()" << std::endl; }
};
int main() {
std::shared_ptr<A> a = std::make_shared<A>();
std::shared_ptr<B> b = std::make_shared<B>();
a->b = b;
b->a = a;
return 0;
}

代码解释:

  1. A 中包含一个指向类 Bshared_ptr,类 B 中包含一个指向类 Ashared_ptr,这就形成了循环引用。
  2. ab 离开 main 函数的作用域时,它们的引用计数都为 1,不会被释放,从而造成内存泄漏。
  3. 运行这段代码,你会发现 ~A()~B() 没有被调用,说明 AB 对象没有被释放。

4. weak_ptr:打破循环的利器

weak_ptr 是一种弱引用指针,它不增加引用计数,主要用于解决 shared_ptr 的循环引用问题。weak_ptr 可以指向由 shared_ptr 管理的对象,但它不拥有所有权。当 weak_ptr 指向的对象被释放时,weak_ptr 会自动失效。

4.1 weak_ptr 的特点

  • 不增加引用计数:不会影响对象的生命周期。
  • 解决循环引用:可以打破 shared_ptr 的循环引用,避免内存泄漏。
  • 可以检查有效性:可以使用 expired() 方法检查 weak_ptr 指向的对象是否仍然有效。
  • 需要转换为 shared_ptr 才能访问:需要使用 lock() 方法将 weak_ptr 转换为 shared_ptr 才能访问它所指向的对象。

4.2 weak_ptr 的使用场景

  • 解决 shared_ptr 的循环引用问题:这是 weak_ptr 最常见的用途。
  • 观察者模式:可以使用 weak_ptr 来观察对象的状态,当对象被释放时,自动取消观察。
  • 缓存:可以使用 weak_ptr 来引用缓存中的对象,当缓存中的对象被释放时,自动失效。

4.3 weak_ptr 的代码示例

#include <iostream>
#include <memory>
class A;
class B;
class A {
public:
std::weak_ptr<B> b;
~A() { std::cout << "~A()" << std::endl; }
};
class B {
public:
std::shared_ptr<A> a;
~B() { std::cout << "~B()" << std::endl; }
};
int main() {
std::shared_ptr<A> a = std::make_shared<A>();
std::shared_ptr<B> b = std::make_shared<B>();
a->b = b;
b->a = a;
return 0;
}

代码解释:

  1. 我们将类 A 中的 shared_ptr<B> 改为 weak_ptr<B>,这样就打破了循环引用。
  2. ab 离开 main 函数的作用域时,AB 对象都会被正确释放,不会造成内存泄漏。
  3. 运行这段代码,你会发现 ~A()~B() 被调用了,说明 AB 对象被释放了。

4.4 如何使用 weak_ptr 访问对象

由于 weak_ptr 不拥有所有权,所以不能直接访问它所指向的对象。需要使用 lock() 方法将 weak_ptr 转换为 shared_ptr 才能访问对象。如果对象已经被释放,lock() 方法会返回一个空的 shared_ptr

#include <iostream>
#include <memory>
int main() {
std::shared_ptr<int> ptr = std::make_shared<int>(10);
std::weak_ptr<int> weakPtr = ptr;
// 使用 lock() 方法将 weak_ptr 转换为 shared_ptr
std::shared_ptr<int> sharedPtr = weakPtr.lock();
if (sharedPtr) {
// 对象仍然有效,可以访问
std::cout << *sharedPtr << std::endl; // 输出 10
} else {
// 对象已经被释放
std::cout << "Object has been released" << std::endl;
}
// 释放 ptr,使对象失效
ptr.reset();
// 再次尝试将 weak_ptr 转换为 shared_ptr
sharedPtr = weakPtr.lock();
if (sharedPtr) {
// 对象仍然有效,可以访问
std::cout << *sharedPtr << std::endl;
} else {
// 对象已经被释放
std::cout << "Object has been released" << std::endl; // 输出 Object has been released
}
return 0;
}

代码解释:

  1. 我们首先创建了一个 shared_ptr,然后创建了一个 weak_ptr 指向同一个对象。
  2. 我们使用 weakPtr.lock()weak_ptr 转换为 shared_ptr,如果对象仍然有效,lock() 方法会返回一个有效的 shared_ptr,我们可以通过它来访问对象。
  3. 如果对象已经被释放,lock() 方法会返回一个空的 shared_ptr
  4. 我们调用 ptr.reset() 释放了 shared_ptr,使对象失效。再次尝试将 weak_ptr 转换为 shared_ptr 时,lock() 方法会返回一个空的 shared_ptr

5. 总结

智能指针是 C++ 中管理动态内存的利器,可以有效避免内存泄漏、野指针等问题。unique_ptr 适用于独占所有权的情况,shared_ptr 适用于共享所有权的情况,weak_ptr 适用于解决 shared_ptr 的循环引用问题。掌握智能指针的使用方法,可以编写出更加安全、可靠的 C++ 代码。

记住以下几点:

  • 尽量使用智能指针代替原始指针。
  • unique_ptr 独占所有权,禁止拷贝,允许移动。
  • shared_ptr 共享所有权,使用引用计数管理内存。
  • weak_ptr 不增加引用计数,用于解决循环引用问题。
  • 使用 lock() 方法将 weak_ptr 转换为 shared_ptr 才能访问对象。

希望这篇文章能帮助你更好地理解和使用 C++ 智能指针,写出更健壮的代码!

内存管理大师兄 C++智能指针内存管理

评论点评

打赏赞助
sponsor

感谢您的支持让我们更好的前行

分享

QRcode

https://www.webkt.com/article/9288