智能指针验证性实验

初步了解智能指针,以及浅显地验证其基本功能。

1 起因

1.1 动态内存管理

运算符new和delete在自由存储区上进行动态内存管理。如果在程序中使用new从自由存储区中分配了内存,那么当不在需要它时,应当使用delete将其释放。

动态内存管理不当时会出现问题,比如:忘记delete,造成内存泄漏;在有指针引用内存的情况下进行了delete,形成野指针产生非法引用。

内存泄漏的例子:

1
2
3
4
5
void swap(int& x, int& y) {
int* temp = new int{ x };
x = y;
y = *temp;
}

由于没有使用delete,所以在调用完swap函数后,temp所引用的内存没有释放,但temp在作用域接收后被销毁,因而再也没有办法访问先前由new申请的这块内存,也无法将其释放,造成内存泄漏。

1.2 启发

在之前的例子中,当swap函数终止,局部变量temp占据的内存从栈中被释放,若temp指向内存也被释放就更好了。如果temp不是一个常规指针,而是一个类的对象,在函数作用域结束后调用的析构函数中,就能实现将temp指向的内存释放。因此有了智能指针模板,它们都定义了类似指针的对象,智能指针过期时,析构函数调用delete释放内存。

2 智能指针

智能指针可以帮助在不需要时动态内存的自动释放,用于避免上述问题的产生。智能指针用法和普通指针类似,但它能自动释放所指对象。C++11中支持3种智能指针:unique_ptr, shared_ptrweak_ptr,需要include<memory>

简单的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
#include <memory>
class Test {
private:
std::string str;
public:
Test(const std::string str_ = "defualt string") :str{ str_ } { std::cout << "Created! " << str << std::endl; }
~Test() { std::cout << "Deleted! " << str << std::endl; }
};
int main() {
{
std::shared_ptr<Test> TestPtr{ new Test("abc") };
}
{
std::unique_ptr<Test> TestPtr{ new Test("ABC") };
}
return 0;
}

运行结果为

1
2
3
4
Created! abc
Deleted! abc
Created! ABC
Deleted! ABC

说明在作用域结束后,不仅析构了智能指针对象intPtr,还析构了intPtr对象所指向Test类对象。

2.1 auto_ptr

最初版本的auto_ptr已被弃用,它在同类型的互相拷贝时会错误地将自己析构。

1
2
3
4
5
6
7
8
9
10
#include <iostream>
#include <memory>
int main() {
std::auto_ptr<int> intPtr1{ new int{8} };
std::auto_ptr<int> intPtr2{ new int };
intPtr2 = intPtr1;
std::cout << *intPtr2 << std::endl;
std::cout << *intPtr1 << std::endl;
return 0;
}

上述程序在输出8后就会出现异常,此时*intPtr1是无效参数。如果第4行的拷贝后intPtr1仍然有效,那么intPtr1intPtr2将指向同一整型对象8,当二者过期时,将删除对象8两次,这是不可接受的。

2.2 shared_ptr

简单的验证

实际情况下我们通常需要多个智能指针指向同一对象,shared_ptr采用”引用计数“的方法,记录指向某一对象的指针数,当且仅当计数值为0时才析构该对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int main() {
using namespace std;
shared_ptr<Test> ptr1{ new Test("ABC") };
shared_ptr<Test> ptr2{ new Test("default") };
ptr2 = ptr1;
cout << "Copy Completed!" << endl;

shared_ptr<Test> ptr3{ new Test("xyz") };
ptr2 = ptr3;
cout << "ptr2 has moved" << endl;
ptr1 = ptr3;
cout << "ptr1 has moved" << endl;
return 0;
}

其运行结果为:

1
2
3
4
5
6
7
8
9
Created! ABC
Created! defualt string
Deleted! defualt string
Copy Completed!
Created! xyz
ptr2 has moved
Deleted! ABC
ptr1 has moved
Deleted! xyz

可以看出,第5行ptr2拷贝ptr1的值后,已经没有指针指向值为“default”的对象,此对象被自动析构。第5行运行结束后,有两个指针指向值为“ABC”的对象。第11行执行完毕后,已经没有指针指向值为”ABC”的对象,此对象被自动析构。shared_ptr使用引用计数的方法记录指向当前对象的指针数,当计数器归0时析构该对象。

使用不当的shared_ptr

在有些情况下,不恰当地使用shared_ptr会产生错误。由于缺乏实际工程经历,只能强行创建了一个例子。

假定一个结构体类,其中一个成员变量的类型为该结构体的指针。在自由存储区new出2个该类对象,用2个智能指针(局部变量)指向它们,此时自由存储区中两个对象的计数器值均为1。再使它们的指针类型成员相互指向另一个对象,此后两个对象的计数器都为2。这种情况下撤除先前的两个局部变量智能指针,此时已经没有指针再指向自由存储区上的两个对象,但两个对象的计数器均为1,没有被析构。而这两个对象再也服务被访问或删除,造成内存泄漏。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <iostream>
#include <memory>

struct LinkNode {
int value;
std::shared_ptr<LinkNode> next;

LinkNode(int value_) :value(value_) {
std::cout << "Created! " << value << std::endl;
}
~LinkNode() {
std::cout << "Deleted! " << value << std::endl;
}
};

//循环引用
int main() {
{
std::shared_ptr<LinkNode> ptr1{ new LinkNode(1) };
std::shared_ptr<LinkNode> ptr2{ new LinkNode(2) };
ptr1->next = ptr2;
ptr2->next = ptr1;
ptr1 = nullptr;
ptr2 = nullptr;
}
std::cout << "There's no pointer available now!" << std::endl;
return 0;
}

运行结果为:

1
2
3
Created! 1
Created! 2
There's no pointer available now!

可以看出,即使已经没有智能指针(shared_ptr类型)指向它们,值为1和值为2的两个LinkNode类对象也始终没有被析构,因为二者的引用计数器均为1,仍旧造成了内存泄漏。

2.3 weak_ptr

使用weak_ptr不会使引用计数值改变,它只能读取所指对象的计数值。它只能观测对象,而不能改变。将前个例子结构体中的成员指针改为weak_ptr类型。这样的情况下,即使有weak_ptr类型的指向自由存储区上的对象,但没有shared_ptr类型的指针指向它(计数器值为0),也会调用其析构函数并释放内存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <iostream>
#include <memory>

struct LinkNode {
int value;
std::weak_ptr<LinkNode> next;

LinkNode(int value_) :value( value_ ){
std::cout << "Created! " << value << std::endl;
}
~LinkNode(){
std::cout << "Deleted! " << value << std::endl;
}
};

int main() {
{
std::shared_ptr<LinkNode> ptr1{ new LinkNode(1) };
std::shared_ptr<LinkNode> ptr2{ new LinkNode(2) };
ptr1->next = ptr2;
ptr2->next = ptr1;
ptr1 = nullptr;
ptr2 = nullptr;
}
std::cout << "There's no pointer available now!" << std::endl;
return 0;
}

运行结果为

1
2
3
4
5
Created! 1
Created! 2
Deleted! 1
Deleted! 2
There's no pointer available now!

可见,没有智能指针(shared_ptr类型)指向自由存储区上的这些对象时,这些对象被正常析构了。这也间接证明了weak_ptr类型智能指针不能改变计数器的值。

3.小结

合理地借助智能能够进行自动地动态内存管理,避免内存泄漏和野指针的出现。

  • shared_ptr借助引用计数,在计数值为0时自动释放所指对象的内存。
  • weak_ptr是前者的弱化本,只能观测所指对象,不能进行修改计数值等操作。
  • unique_ptr如其字面意思所示,是唯一的指向某对象的指针,有新的智能指针指向此对象时,先前的unique_ptr会失效,而使用失效的unique_ptr会在编译时报错(同样使用auto_ptr则只会在运行时出错)。