考虑一个我们动态分配值的函数:
void someFunction()
{
Resource *ptr = new Resource; // Resource是一个结构或类
// 在这里做ptr的东西
delete ptr;
}
虽然上面的代码看起来相当简单,但忘记释放ptr却相当容易。即使您确实记得在函数末尾删除ptr,如果函数提前退出,也有多种方法可能无法删除ptr。这可以通过提前退货来实现:
#include
void someFunction()
{
Resource *ptr = new Resource;
int x;
std::cout << "Enter an integer: ";
std::cin >> x;
if (x == 0)
return; //函数提前返回,ptr不会被删除!
// 在这里做ptr的东西
delete ptr;
}
或通过抛出的异常:
#include
void someFunction()
{
Resource *ptr = new Resource;
int x;
std::cout << "Enter an integer: ";
std::cin >> x;
if (x == 0)
throw 0; // 函数提前返回,ptr不会被删除!
//在这里做ptr的东西
delete ptr;
}
在上面的两个程序中,早期的return或throw语句执行,导致函数终止而不删除变量ptr。因此,为变量ptr分配的内存现在被泄露(每次调用此函数时它将再次泄漏并提前返回)。
从本质上讲,出现这类问题是因为指针变量没有固有的机制可以自行清理。
智能指针类的帮助?
关于类的最好的事情之一是它们包含析构函数,当类的对象超出范围时,它们会自动执行。所以,如果您分配(或回收)存储你的构造函数,你可以释放它在你的析构函数,并保证当类对象被销毁的内存将被释放(不管它是否超出范围,被显式删除,等等…)。这是我们在前面析构函数中讨论过的RAII编程范例的核心。
那么我们可以使用一个类来帮助我们管理和清理指针吗?答案是我们可以!
考虑一个类,其唯一的工作是保持并“拥有”传递给它的指针,然后在类对象超出范围时释放该指针。只要该类的对象仅作为局部变量创建,我们就可以保证该类适当地超出范围(无论我们的函数何时或如何终止)并且拥有的指针将被销毁。
以下是该想法的初稿:
#include
template
class Auto_ptr1
{
T* m_ptr;
public:
// 通过构造函数传入指向“own”的指针
Auto_ptr1(T* ptr=nullptr)
:m_ptr(ptr)
{
}
// 析构函数将确保它被解除分配
~Auto_ptr1()
{
delete m_ptr;
}
// 重载dereference和operator->所以我们可以像使用m_ptr一样使用Auto_ptr1。
T& operator*() const { return *m_ptr; }
T* operator->() const { return m_ptr; }
};
//证明上述功能的一个例子
class Resource
{
public:
Resource() { std::cout << "Resource acquired\n"; }
~Resource() { std::cout << "Resource destroyed\n"; }
};
int main()
{
Auto_ptr1 res(new Resource); // 注意这里的内存分配
// ... 而没有显式删除的必要
// 另请注意,角度大括号中的资源不需要*符号,因为它是由模板提供的
return 0;
} // res超出范围,并为我们销毁分配的资源
该程序打印:
Resource acquired
Resource destroyed
考虑一下这个程序和类如何工作。首先,我们动态创建一个Resource,并将其作为参数传递给我们模板化的Auto_ptr1类。从那时起,我们的Auto_ptr1变量res拥有该Resource对象(Auto_ptr1与m_ptr具有组合关系)。因为res被声明为局部变量并且具有块范围,所以当块结束时它将超出范围并被销毁(不用担心忘记取消分配它)。因为它是一个类,当它被销毁时,将调用Auto_ptr1析构函数。该析构函数将确保它所持有的资源指针被删除!
只要Auto_ptr1被定义为局部变量(具有自动持续时间,因此是类名的“Auto”部分),资源将保证在声明它的块的末尾被销毁,无论如何函数终止(即使它提前终止)。
这样的类称为智能指针。一个智能指针是当智能指针对象超出范围,旨在管理动态分配的内存,并确保内存被删除。(相关地,内置指针有时被称为“哑指针”,因为它们自身无法清理)。
现在让我们回到上面的someFunction()示例,并展示智能指针类如何解决我们的问题:
#include
template
class Auto_ptr1
{
T* m_ptr;
public:
//通过构造函数传入指向“own”的指针
Auto_ptr1(T* ptr=nullptr)
:m_ptr(ptr)
{
}
// 析构函数将确保它被解除分配
~Auto_ptr1()
{
delete m_ptr;
}
// 重载dereference和operator->所以我们可以像使用m_ptr一样使用Auto_ptr1。
T& operator*() const { return *m_ptr; }
T* operator->() const { return m_ptr; }
};
// 证明上述功能的一个例子
class Resource
{
public:
Resource() { std::cout << "Resource acquired\n"; }
~Resource() { std::cout << "Resource destroyed\n"; }
void sayHi() { std::cout << "Hi\n"; }
};
void someFunction()
{
Auto_ptr1 ptr(new Resource); // ptr现在拥有Resource
int x;
std::cout << "Enter an integer: ";
std::cin >> x;
if (x == 0)
return; //该函数提前返回
// 在这里做ptr的东西
ptr->sayHi();
}
int main()
{
someFunction();
return 0;
}
如果用户输入非零整数,则上述程序将打印:
Resource acquired
Hi!
Resource destroyed
如果用户输入零,上述程序将提前终止,打印:
Resource acquired
Resource destroyed
请注意,即使在用户输入零并且函数提前终止的情况下,仍然可以正确地释放资源。
因为ptr变量是局部变量,所以当函数终止时,ptr将被销毁(无论它如何终止)。并且因为Auto_ptr1析构函数将清理资源,所以我们确信资源将被正确清理。
一个关键的bug
Auto_ptr1类在一些自动生成的代码背后隐藏着一个关键缺陷。在进一步阅读之前,看看你是否能够确定它是什么。我们等一下…
(提示:如果您不提供类的哪些部分会自动生成)
(前方危险…)
好的,时间到了。
我们会告诉你,而不是不告诉你。考虑以下程序:
#include
// Same as above
template
class Auto_ptr1
{
T* m_ptr;
public:
Auto_ptr1(T* ptr=nullptr)
:m_ptr(ptr)
{
}
~Auto_ptr1()
{
delete m_ptr;
}
T& operator*() const { return *m_ptr; }
T* operator->() const { return m_ptr; }
};
class Resource
{
public:
Resource() { std::cout << "Resource acquired\n"; }
~Resource() { std::cout << "Resource destroyed\n"; }
};
int main()
{
Auto_ptr1 res1(new Resource);
Auto_ptr1 res2(res1); // Alternatively, don't initialize res2 and then assign res2 = res1;
return 0;
}
该程序打印:
Resource acquired
Resource destroyed
Resource destroyed
很可能(但不一定)你的程序将在此时崩溃。现在看到问题?因为我们没有提供复制构造函数或赋值运算符,所以C ++为我们提供了一个。它提供的功能是浅拷贝。因此,当我们用res1初始化res2时,两个Auto_ptr1变量都指向同一个Resource。当res2超出范围时,它会删除资源,使res1保留悬空指针。当res1去删除它(已经删除)的资源时,崩溃!
你会遇到类似这样的函数的类似问题:
void passByValue(Auto_ptr1 res)
{
}
int main()
{
Auto_ptr1 res1(new Resource);
passByValue(res1)
return 0;
}
在此程序中,res1将按值复制到passByValue的参数res中,从而导致资源指针重复。崩溃!
很明显,这并不好。我们怎么解决这个问题呢?
好吧,我们可以做的一件事是明确定义和删除复制构造函数和赋值运算符,从而防止首先制作任何副本。这会阻止传递值的情况(这很好,我们可能不应该通过值传递这些值)。
但是那么我们如何将一个函数的Auto_ptr1返回给调用者呢?
??? generateResource()
{
Resource *r = new Resource;
return Auto_ptr1(r);
}
我们不能通过引用返回我们的Auto_ptr1,因为本地Auto_ptr1将在函数结束时被销毁,并且调用者将留下悬空引用。按地址返回有同样的问题。我们可以通过地址返回指针r,但之后我们可能会忘记删除r,这是首先使用智能指针的重点。所以那就是了。按值返回Auto_ptr1是唯一有意义的选项 - 但最后我们会得到浅拷贝,重复指针和崩溃。
另一种选择是覆盖复制构造函数和赋值运算符以进行深层复制。这样,我们至少可以保证避免重复指向同一个对象的指针。但是复制可能很昂贵(并且可能不太可取或甚至不可能),并且我们不想仅仅为了从函数返回Auto_ptr1而制作不必要的对象副本。另外,分配或初始化哑指针不会复制指向的对象,那么为什么我们希望智能指针的行为方式不同?
我们做什么?
移动语义
如果我们不是让我们的复制构造函数和赋值运算符复制指针(“复制语义”),而是将指针的所有权从源传输/移动到目标对象,该怎么办?这是移动语义背后的核心思想。 移动语义意味着类将转移对象的所有权而不是复制。
让我们更新我们的Auto_ptr1类来展示如何做到这一点:
#include
template
class Auto_ptr2
{
T* m_ptr;
public:
Auto_ptr2(T* ptr=nullptr)
:m_ptr(ptr)
{
}
~Auto_ptr2()
{
delete m_ptr;
}
// 实现移动语义的复制生成器
Auto_ptr2(Auto_ptr2& a) // note: not const
{
m_ptr = a.m_ptr; // 将我们的哑指针从源传输到我们的本地对象
a.m_ptr = nullptr; // 确保源不再拥有指针
}
// 实现移动语义的赋值运算符
Auto_ptr2& operator=(Auto_ptr2& a) // note: not const
{
if (&a == this)
return *this;
delete m_ptr; // 确保我们释放目标已经占据的任何指针
m_ptr = a.m_ptr; // 然后将我们的哑指针从源传输到本地对象
a.m_ptr = nullptr; //确保源不再拥有指针
return *this;
}
T& operator*() const { return *m_ptr; }
T* operator->() const { return m_ptr; }
bool isNull() const { return m_ptr == nullptr; }
};
class Resource
{
public:
Resource() { std::cout << "Resource acquired\n"; }
~Resource() { std::cout << "Resource destroyed\n"; }
};
int main()
{
Auto_ptr2 res1(new Resource);
Auto_ptr2 res2; // Start as nullptr
std::cout << "res1 is " << (res1.isNull() ? "null\n" : "not null\n");
std::cout << "res2 is " << (res2.isNull() ? "null\n" : "not null\n");
res2 = res1; //res2假设所有权,res1设置为null
std::cout << "Ownership transferred\n";
std::cout << "res1 is " << (res1.isNull() ? "null\n" : "not null\n");
std::cout << "res2 is " << (res2.isNull() ? "null\n" : "not null\n");
return 0;
}
该程序打印:
Resource acquired
res1 is not null
res2 is null
Ownership transferred
res1 is null
res2 is not null
Resource destroyed
请注意,我们的重载operator=将m_ptr的所有权从res1赋予res2!因此,我们最终没有指针的重复副本,并且所有内容都得到了整齐清理。
std :: auto_ptr,以及为什么要避免它
现在是讨论std :: auto_ptr的合适时机。在C ++ 98中引入的std :: auto_ptr是C ++首次尝试使用标准化的智能指针。std :: auto_ptr选择实现移动语义,就像Auto_ptr2类一样。
但是,std :: auto_ptr(和我们的Auto_ptr2类)有许多问题使得使用它很危险。
首先,因为std :: auto_ptr通过复制构造函数和赋值运算符实现移动语义,所以通过值将std :: auto_ptr传递给函数将导致您的资源被移动到函数参数(并在函数末尾被销毁)当函数参数超出范围时)。然后当你从调用者访问你的auto_ptr参数(没有意识到它被传输和删除)时,你突然取消引用空指针。崩溃!
其次,std :: auto_ptr总是使用非数组删其内容。这意味着auto_ptr将无法与动态分配的数组一起正常工作,因为它使用了错误的解除分配方式。更糟糕的是,它不会阻止你将动态数组传递给它,然后它会管理不善,从而导致内存泄漏。
最后,auto_ptr与标准库中的许多其他类不兼容,包括大多数容器和算法。发生这种情况是因为那些标准库类假定当它们复制一个项目时,它实际上是一个副本,而不是执行一个移动。
由于上述缺点,std :: auto_ptr在C ++ 11中已被弃用,因此不应使用它。实际上,std :: auto_ptr将作为C ++ 17的一部分从标准库中完全删除!
规则:不推荐使用std :: auto_ptr,不应使用它。(改为使用std :: unique_ptr或std :: shared_ptr)。。
向前进
设计std :: auto_ptr的核心问题是,在C ++ 11之前,C ++语言根本没有机制来区分“复制语义”和“移动语义”。重写复制语义以实现移动语义会导致奇怪的边缘情况和无意的错误。例如,您可以编写res1 = res2并且不知道res2是否会被更改!
因此,在C ++ 11中,“移动”的概念被正式定义,并且“移动语义”被添加到语言中以正确区分复制和移动。现在我们已经为移动语义的有用性设置了阶段,我们将在本章的其余部分探讨移动语义的主题。我们还将使用移动语义修复Auto_ptr2类。
在C ++ 11中,std :: auto_ptr已被一堆其他类型的“移动感知”智能指针所取代:std :: scoped_ptr,std :: unique_ptr,std :: weak_ptr和std :: shared_ptr。我们还将探索其中最受欢迎的两个:unique_ptr(它是auto_ptr的直接替代品)和shared_ptr。