1. 什么是智能指针
C++智能指针是一种用于管理动态分配的内存的特殊类型的指针。智能指针是通过类来实现的,它提供了自动管理内存的功能。
C++中常见的智能指针包括std::unique_ptr、std::shared_ptr和std::weak_ptr
头文件: #include <memory>
auto_ptr | c++98智能指针,已在C++11中弃用。
|
std::unique_ptr | 是一种独占所有权的智能指针,同一时间只能有一个std::unique_ptr指向同一块内存。 当unique_ptr被销毁或转移所有权时,它所指向的内存会被自动释放。 |
std::shared_ptr | 是一种共享所有权的智能指针,可以让多个std::shared_ptr指向同一块内存。 内部通过引用计数来管理内存的释放,当最后一个shared_ptr销毁时,会释放所指向的内存。 |
std::weak_ptr | 是一种弱引用智能指针,它不增加所指对象的引用计数。 std::weak_ptr主要用于解决std::shared_ptr的循环引用问题。 |
2. 为什么要用智能指针
使用智能指针可以帮助我们管理动态分配的内存,避免了手动管理内存时可能出现的内存泄漏和悬垂指针的问题,可以大大提高代码的可靠性和安全性。
2.1 背景
使用指针,如果没有释放,就会造成内存泄露。但是使用普通对象却不会。
如果让有生命周期的对象来管理分配的动态内存,那么在对象过期时,它的析构函数删除指向的内存,就可以解决指针忘记释放的问题。
基于这个原理,C++98 提供了 auto_ptr 模板的解决方案,C++11 增加unique_ptr、shared_ptr 和weak_ptr。
2.2 内存泄漏
分配了内存,但是没释放就会导致内存泄露。导致系统长时间运行后,可用内存越来越少。
C/C++中常见的几种内存泄露例子
1. 函数结束时,没有释放分配的内存
void MemoryLeak( )
{
int *data = new int;
*data = 15;
}
2. 在内存释放前结束函数
void MemoryLeak() {
int* ptr = new int;
// do something which may throw an exception
// we never get here if an exception is thrown
delete ptr;
}
3. 在释放分配的内存a之前,给a赋其他值作为地址,导致原先分配的地址丢失,产生内存泄露
void MemoryLeak() {
int * a = malloc(sizeof(int));
a = 0;
}
4. 不能使用delete去释放malloc分配的内存
void MemoryLeak() {
char *s = (char*) malloc(5);
delete s;
}
5. 使用已经释放的指针
void MemoryLeak() {
delete ptr;
ptr = nullptr;
ptr->show();
}
6. 释放形参中的指针
void MemoryLeak(char* c) {
char* temp = new char [ strlen (c) + 1 ];
strcpy ( temp, c );
delete[] c; //错误,不应该删除形参的指针
return temp;//错误,返回局域变量的指针
}
7. 使用没有分配内存的指针
void MemoryLeak() {
MyWindow* window;
window->show();
}
8. 释放非堆上的指针
void MemoryLeak() {
MyWindow window;
delete window;
}
内存泄露的定位方法:https://www.cnblogs.com/skynet/archive/2011/02/20/1959162.html
2.3 悬垂指针
悬垂指针是指指针在指向的内存释放或者销毁后,依然保留着指向该内存地址的情况。当程序中存在悬垂指针时,如果再次使用这个指针访问该内存地址,可能会导致程序崩溃或者产生未定义的行为。
悬垂指针通常出现在以下情况下:
- 指针指向的内存被释放或者销毁,但指针没有被置为nullptr或者被赋予新的有效地址。
- 指针指向的对象在其生命周期内已经被销毁,但指针继续被使用。
例如:
1. 重复释放,指针释放后,没有赋值为nullptr
void MemoryLeak() {
char* pStr = (char*) malloc(20);
free(pStr);
free(pStr); // results in an invalid deallocation
}
2. 指针指向的对象被销毁:
int* ptr;
{
int num = 10;
ptr = #
}
在这里,指针ptr指向的对象num已经被销毁,ptr成为悬垂指针
3. 返回局部变量的地址
int* getLocalPtr() {
int num = 20;
return #
}
int* ptr = getLocalPtr();
函数调用结束后,局部变量num被销毁,ptr成为悬垂指针
使用智能指针可以有效地避免悬垂指针的问题,因为智能指针会在其所指向的对象不再需要时自动释放内存。
3. 如何用智能指针
3.1 unique_ptr
unique_ptr独享所有权,一个非空的unique_ptr总是拥有它所指向的资源。
转移一个unique_ptr将会把所有权也从源指针转移给目标指针(源指针被置空)。
无法拷贝,也无法用于需要副本的任何标准模板库 (STL) 算法。
1. 创建一个unique_ptr实例
int main()
{
unique_ptr<int> pInt(new int(5));
cout << *pInt;
}
2. 移动,转移所有权
int main()
{
unique_ptr<int> pInt(new int(5));
unique_ptr<int> pInt2 = std::move(pInt); // 转移所有权
//cout << *pInt << endl; // 出错,pInt为空
cout << *pInt2 << endl;
unique_ptr<int> pInt3(std::move(pInt2));
}
3. 从函数中返回unique_ptr
unique_ptr<int> clone(int p)
{
unique_ptr<int> pInt(new int(p));
return pInt; // 返回unique_ptr
}
int main() {
int p = 5;
unique_ptr<int> ret = clone(p);
cout << *ret << endl;
}
4. 不能赋值构造和赋值
int main()
{
unique_ptr<int> pInt(new int(5));
unique_ptr<int> pInt2(pInt); // 报错
unique_ptr<int> pInt3 = pInt; // 报错
}
5. 在容器中保存指针(要用move放到容器中)
int main()
{
vector<unique_ptr<int>> vec;
unique_ptr<int> p(new int(5));
vec.push_back(std::move(p)); // 使用移动语义
}
6. 用unique_ptr管理动态数组
int main()
{
unique_ptr<int[]> p(new int[5] {1, 2, 3, 4, 5});
p[0] = 0; // 重载了operator[]
}
3.2 shared_ptr
shared_ptr采用引用计数的方式来管理内存,即每当有一个shared_ptr指向某段内存时,该内存的引用计数就会加1,当所有shared_ptr都不再指向这段内存时,引用计数就会减1。当引用计数减为0时,内存会自动释放。
shared_ptr内部的引用计数是安全的,但是对象的读取需要加锁。
1. 创建一个shared_ptr实例
使用make_shared创建shared_ptr
#include <iostream>
#include <memory>
// 定义一个结构体
struct Person {
std::string name;
int age;
Person(const std::string& n, int a) : name(n), age(a) {}
};
int main() {
// 使用std::make_shared创建一个指向Person结构体的shared_ptr
std::shared_ptr<Person> personPtr = std::make_shared<Person>("Alice", 25);
// 访问结构体成员变量
std::cout << "Name: " << personPtr->name << std::endl;
std::cout << "Age: " << personPtr->age << std::endl;
//使用std::make_shared创建一个指向int的shared_ptr
std::shared_ptr<int> ptr1 = std::make_shared<int>(10);
// 多个shared_ptr指向同一块内存
std::shared_ptr<int> ptr2 = ptr1;
std::cout << *ptr1 << std::endl; // 输出:10
std::cout << *ptr2 << std::endl; // 输出:10
return 0;
}
2. 避免循环引用
下面的示例中,我们创建了两个Node对象,然后将它们的next指针互相指向,造成循环引用。
当main函数结束时,由于循环引用导致两个Node对象无法释放,最终会造成内存泄漏。要解决这个问题,可以使用weak_ptr来打破循环引用。
#include <iostream>
#include <memory>
class Node {
public:
int value;
std::shared_ptr<Node> next;
Node(int v) : value(v), next(nullptr) {
std::cout << "Node " << value << " created\n";
}
~Node() {
std::cout << "Node " << value << " destroyed\n";
}
};
int main() {
std::shared_ptr<Node> node1 = std::make_shared<Node>(1);
std::shared_ptr<Node> node2 = std::make_shared<Node>(2);
node1->next = node2;
node2->next = node1; // 会导致循环引用
return 0;
}
3. 减少计数
使用reset方法,可以将shared_ptr指向另一个新的Test对象。这将导致原来的Test对象的引用计数减少,当引用计数减少到0时,原来的Test对象将被删除。
注意,reset方法不仅会减少原来对象的引用计数,还会增加新对象的引用计数。因此不需要显式地删除原来的Test对象。
这个示例也展示了shared_ptr的一个重要特性,即它可以自动管理内存,无需程序员显式地调用delete。
#include <iostream>
#include <memory>
class Test {
public:
Test() {
std::cout << "Test Constructor" << std::endl;
}
~Test() {
std::cout << "Test Destructor" << std::endl;
}
};
int main() {
std::shared_ptr<Test> p1(new Test());
p1.reset(new Test());
return 0;
}
3.3 weak_ptr
weak_ptr是为了配合shared_ptr而引入的一种智能指针,它指向一个由shared_ptr管理的对象而不影响所指对象的生命周期,也就是将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。
不论是否有weak_ptr指向,一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放。从而避免了循环引用的问题。
1. 创建weak_ptr、检测对象是否可用
首先创建了一个shared_ptr指向一个Test对象,然后我们创建了一个weak_ptr,它指向同一个Test对象。
然后,使用lock方法,尝试获取一个shared_ptr,指向weak_ptr所指向的对象。如果weak_ptr所指向的对象仍然存在,lock方法将返回一个shared_ptr,否则,lock方法将返回一个空的shared_ptr。
这个示例展示了weak_ptr的一个重要特性,即它可以安全地访问shared_ptr所指向的对象,而不会增加对象的引用计数。在if语句中使用auto关键字,自动推断shared_ptr的类型。
另外,这个示例也展示了weak_ptr的另一个重要特性,即它可以检查shared_ptr所指向的对象是否仍然存在。这就是为什么需要使用if语句,来检查lock方法返回的shared_ptr是否为空。
#include <iostream>
#include <memory>
class Test {
public:
Test() {
std::cout << "Test Constructor" << std::endl;
}
~Test() {
std::cout << "Test Destructor" << std::endl;
}
};
int main() {
std::shared_ptr<Test> p1(new Test());
std::weak_ptr<Test> wp1(p1);
if (auto sp1 = wp1.lock()) {
std::cout << "Test object is still alive" << std::endl;
} else {
std::cout << "Test object has been destroyed" << std::endl;
}
return 0;
}