C++11新特性:智能指针/STL容器
一、智能指针
C++程序设计中使用堆内存是非常频繁的操作,堆内存的申请和释放都由程序员自己管理。程序员自己管理堆内存可以提高了程序的效率,但是整体来说堆内存的管理是麻烦的,C++11中引入了智能指针的概念,方便管理堆内存。使用普通指针,容易造成堆内存泄露(忘记释放),二次释放,程序发生异常时内存泄露等问题等,使用智能指针能更好的管理堆内存。
理解智能指针需要从下面三个层次:
(1)从较浅的层面看,智能指针是利用了一种叫做RAII(资源获取即初始化)的技术对普通的指针进行封装,这使得智能指针实质是一个对象,行为表现的却像一个指针。
(2)智能指针的作用是防止忘记调用delete释放内存和程序异常的进入catch块忘记释放内存。另外指针的释放时机也是非常有考究的,多次释放同一个指针会造成程序崩溃,这些都可以通过智能指针来解决。
(3)智能指针还有一个作用是把值语义转换成引用语义。C++和Java有一处最大的区别在于语义不同,在Java里面下列代码:
Animal a = new Animal();
Animal b = a;
这里其实只生成了一个对象,a和b仅仅是把持对象的引用而已。但在C++中不是这样,
Animal a;
Animal b = a;
这里却是就是生成了两个对象。
简单地说,智能指针只是用对象去管理一个资源指针,同时用一个计数器计算当前指针引用对象的个数,当管理指针的对象增加或减少时,计数器也相应加1或减1,当最后一个指针管理对象销毁时,计数器为1,此时在销毁指针管理对象的同时,也把指针管理对象所管理的指针进行delete操作。
1、auto_ptr指针
C++11之前的智能指针是auto_ptr,一开始它的出现是为了解决指针没有释放导致的内存泄漏。比如忘了释放或者在释放之前,程序throw出错误,导致没有释放。所以auto_ptr在这个对象声明周期结束之后,自动调用其析构函数释放掉内存。 int t = 3, m =4;
auto_ptr<int> p1(&t);
auto_ptr<const int> p2(&m);
//注意这里一定是[5]而不是(5),因为(5)表示申请了一个里面存着数字5的地址,
//不要记混了
auto_ptr<int> p3(new int[5]);
注意:这里只是阐述了怎么用,p1,p2一般不能那么定义,因为一般不用智能指针去指向非堆内存中的地址,因为自行释放非堆地址很有可能出现问题。所以上述程序运行会报错。相当于如下操作:
int t = 3;
int *p = &t;
delete p;
这样是不行的,运行时候会报错。所以千万不要用一块非new分配的动态内存去初始化一个智能指针。
auto_ptr被弃用的原因:
(1)避免潜在的内存崩溃
智能指针auto_ptr在被赋值操作的时候,被赋值的取得其所有权,去赋值的丢失其所有权。如下面的例子:
auto_ptr< string> ps (new string ("I reigned lonely as a cloud.");
auto_ptr<string> vocation;
vocaticn = ps;
执行完上面这步之后,ps就不再指向原来的string串了,变成了空串,vocation指向了原来的string串。但是会出下如下的错误:
auto_ptr<string> films[5] =
{
auto_ptr<string> (new string("Fowl Balls")),
auto_ptr<string> (new string("Duck Walks")),
auto_ptr<string> (new string("Chicken Runs")),
auto_ptr<string> (new string("Turkey Errors")),
auto_ptr<string> (new string("Goose Eggs"))
};
auto_ptr<string> pwin;
pwin = films[2]; // films[2] loses ownership. 将所有权从films[2]转让给pwin,此时films[2]不再引用该字符串从而变成空指针
for(int i = 0; i < 5; ++i)
cout << *films[i] << endl;
以上的程序编译正常,但是运行到输出环节的时候就会出现错误。因为films[2]此时已经丢掉了控制权。而如果用unique_ptr的时候就会在编译期间发现这个错误,因为unique_ptr是不允许直接赋值的。
(2)不够方便–没有移动语义的后果
比如auto_ptr不能够作为函数的返回值和函数的参数,也不能在容器中保存autp_ptr。
而这些unique_ptr都可以做到。因为C++11之后有了移动语义的存在,这里调用的是移动构造函数。因为移动语义它可以接管原来对象的资源,同时让原来对象的资源置为空。
C++11之后智能指针分为了三种:shared_ptr, unique_ptr,weak_ptr,包含在头文件中,而weak_ptr相当于shared_ptr的一个辅助指针, 所以正式的智能指针只有shared_ptr和unique_ptr。
explict关键字
C++11之后的智能指针的构造函数都有explict关键词修饰,表明它不能被隐式的类型转换。即如下p1的形式是不行的:
shared_ptr<int> p1 = new int(1024); //这种是不行的,
//因为等号右边是一个int*的指针,
//因为有explict修饰,
//所以它不能被隐式的转换为shared_ptr<int>的类型
shared_ptr<int> p2(new int(1024)); //这种是直接采用了初始化的形式
2、shared_ptr指针
shared_ptr多个指针指向相同的对象。shared_ptr使用引用计数,每一个shared_ptr的拷贝都指向相同的内存。每使用他一次,内部的引用计数加1,每析构一次,内部的引用计数减1,减为0时,自动删除所指向的堆内存。shared_ptr内部的引用计数是线程安全的,但是对象的读取需要加锁。
(1)初始化。智能指针是个模板类,可以指定类型,传入指针通过构造函数初始化。也可以使用make_shared函数初始化。不能将指针直接赋值给一个智能指针,一个是类,一个是指针。例如std::shared_ptr p4 = new int(1);的写法是错误的
(2)拷贝和赋值。拷贝使得对象的引用计数增加1,赋值使得原对象引用计数减1,当计数为0时,自动释放内存。后来指向的对象引用计数加1,指向后来的对象。
(3)get函数获取原始指针
(4)注意不要用一个原始指针初始化多个shared_ptr,否则会造成二次释放同一内存
(5)注意避免循环引用,shared_ptr的一个最大的陷阱是循环引用,循环,循环引用会导致堆内存无法正确释放,导致内存泄漏。循环引用在weak_ptr中介绍。
#include <iostream>
#include <memory>
int main()
{
{
int a = 10;
//使用make_shared初始化
std::shared_ptr<int> ptra = std::make_shared<int>(a);
std::shared_ptr<int> ptra2(ptra); //copy,使得对象ptra的引用次数加1
std::cout << ptra.use_count() << std::endl;//cout 2
int b