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 = 20;
int *pb = &a;
//std::shared_ptr<int> ptrb = pb; //error,不能直接将一个指针赋值给智能指针
std::shared_ptr<int> ptrb = std::make_shared<int>(b);
ptra2 = ptrb; //assign,赋值使得原对象ptra的引用次数减1
pb = ptrb.get(); //获取原始指针赋值给pb使得ptrb的引用次数加1
std::cout << ptra.use_count() << std::endl;
std::cout << ptrb.use_count() << std::endl;
}
}
3、unique_ptr指针
unique_ptr“唯一”拥有其所指对象,同一时刻只能有一个unique_ptr指向给定对象(通过禁止拷贝语义、只有移动语义来实现)。相比与原始指针unique_ptr用于其RAII的特性,使得在出现异常的情况下,动态资源能得到释放。
unique_ptr指针本身的生命周期:从unique_ptr指针创建时开始,直到离开作用域。离开作用域时,若其指向对象,则将其所指对象销毁(默认使用delete操作符,用户可指定其他操作)。
unique_ptr指针与其所指对象的关系:在智能指针生命周期内,可以改变智能指针所指对象,如创建智能指针时通过构造函数指定、通过reset方法重新指定、通过release方法释放所有权、通过移动语义转移所有权。
#include <iostream>
#include <memory>
int main() {
{
//传入指针通过构造函数初始化
std::unique_ptr<int> uptr(new int(10)); //绑定动态对象
//std::unique_ptr<int> uptr2 = uptr; //不能赋值
//std::unique_ptr<int> uptr2(uptr); //不能拷贝
std::unique_ptr<int> uptr2 = std::move(uptr); //转换所有权
uptr2.release(); //释放所有权
}
//超过uptr作用域,内存释放
}
4、weak_ptr指针
weak_ptr是为了配合shared_ptr而引入的一种智能指针,因为它不具有普通指针的行为,没有重载operator*和->,它的最大作用在于协助shared_ptr工作,像旁观者那样观测资源的使用情况。
weak_ptr可以从一个shared_ptr或者另一个weak_ptr对象构造,获得资源的观测权。但weak_ptr没有共享资源,它的构造不会引起指针引用计数的增加。使用weak_ptr的成员函数use_count()可以观测资源的引用计数,另一个成员函数expired()的功能等价于use_count()==0,但更快,表示被观测的资源(也就是shared_ptr的管理的资源)已经不复存在。
weak_ptr可以使用一个非常重要的成员函数lock()从被观测的shared_ptr获得一个可用的shared_ptr对象, 从而操作资源。但当expired()==true的时候,lock()函数将返回一个存储空指针的shared_ptr。
#include <iostream>
#include <memory>
int main() {
{
std::shared_ptr<int> sh_ptr = std::make_shared<int>(10);
std::cout << sh_ptr.use_count() << std::endl;
//从shared_ptr对象构造,获取sh_ptr的观测权
std::weak_ptr<int> wp(sh_ptr);
std::cout << wp.use_count() << std::endl;
if(!wp.expired()){
std::shared_ptr<int> sh_ptr2 = wp.lock(); //get another shared_ptr
*sh_ptr = 100;
std::cout << wp.use_count() << std::endl;
}
}
//delete memory
}
5、weak_ptr指针解决循环引用问题
请看下面这个示例:#include <iostream>
#include <memory>
class Child;
class Parent;
class Parent {
private:
std::shared_ptr<Child> ChildPtr;
public:
void setChild(std::shared_ptr<Child> child) {
this->ChildPtr = child;
}
void doSomething() {
if (this->ChildPtr.use_count()) {
}
}
~Parent() {
}
};
class Child {
private:
std::shared_ptr<Parent> ParentPtr;
public:
void setPartent(std::shared_ptr<Parent> parent) {
this->ParentPtr = parent;
}
void doSomething() {
if (this->ParentPtr.use_count()) {
}
}
~Child() {
}
};
int main() {
std::weak_ptr<Parent> wpp;
std::weak_ptr<Child> wpc;
{
std::shared_ptr<Parent> p(new Parent);
std::shared_ptr<Child> c(new Child);
p->setChild(c);
c->setPartent(p);
wpp = p;
wpc = c;
std::cout << p.use_count() << std::endl; // 2
std::cout << c.use_count() << std::endl; // 2
}
std::cout << wpp.use_count() << std::endl; // 1
std::cout << wpc.use_count() << std::endl; // 1
return 0;
}
存在循环引用,内存泄露的问题。正确的做法是在Parent中使用weak_ptr来解决循环的问题。
#include <iostream>
#include <memory>
class Child;
class Parent;
class Parent {
private:
//std::shared_ptr<Child> ChildPtr;
std::weak_ptr<Child> ChildPtr;
public:
void setChild(std::shared_ptr<Child> child) {
this->ChildPtr = child;
}
void doSomething() {
//new shared_ptr
if (this->ChildPtr.lock()) {
}
}
~Parent() {
}
};
class Child {
private:
std::shared_ptr<Parent> ParentPtr;
public:
void setPartent(std::shared_ptr<Parent> parent) {
this->ParentPtr = parent;
}
void doSomething() {
if (this->ParentPtr.use_count()) {
}
}
~Child() {
}
};
int main() {
std::weak_ptr<Parent> wpp;
std::weak_ptr<Child> wpc;
{
std::shared_ptr<Parent> p(new Parent);
std::shared_ptr<Child> c(new Child);
p->setChild(c);
c->setPartent(p);
wpp = p;
wpc = c;
std::cout << p.use_count() << std::endl; // 2
std::cout << c.use_count() << std::endl; // 1
}
std::cout << wpp.use_count() << std::endl; // 0
std::cout << wpc.use_count() << std::endl; // 0
return 0;
}
6、智能指针的设计和实现
下面是一个简单智能指针的demo。智能指针类将一个计数器与类指向的对象相关联,引用计数跟踪该类有多少个对象共享同一指针。每次创建类的新对象时,初始化指针并将引用计数置为1;当对象作为另一对象的副本而创建时,拷贝构造函数拷贝指针并增加与之相应的引用计数;对一个对象进行赋值时,赋值操作符减少左操作数所指对象的引用计数(如果引用计数为减至0,则删除对象),并增加右操作数所指对象的引用计数;调用析构函数时,构造函数减少引用计数(如果引用计数减至0,则删除基础对象)。
智能指针就是模拟指针动作的类。所有的智能指针都会重载 -> 和 * 操作符。智能指针还有许多其他功能,比较有用的是自动销毁。**这主要是利用栈对象的有限作用域以及临时对象(有限作用域实现)析构函数释放内存。
#include <iostream>
#include <memory>
template<typename T>
class SmartPointer {
private:
T* _ptr;//普通指针
size_t* _count;//计数器
public:
//构造函数
SmartPointer(T* ptr = nullptr) :
_ptr(ptr) {
if (_ptr) {
_count = new size_t(1);
} else {
_count = new size_t(0);
}
}
//拷贝构造函数
SmartPointer(const SmartPointer& ptr) {
if (this != &ptr) {
this->_ptr = ptr._ptr;
this->_count = ptr._count;
(*this->_count)++;
}
}
//赋值运算符重载
SmartPointer& operator=(const SmartPointer& ptr) {
if (this->_ptr == ptr._ptr) {
return *this;
}
if (this->_ptr) {
(*this->_count)--;
if (this->_count == 0) {
delete this->_ptr;
delete this->_count;
}
}
this->_ptr = ptr._ptr;
this->_count = ptr._count;
(*this->_count)++;
return *this;
}
//*运算符重载
T& operator*() {
assert(this->_ptr == nullptr);
return *(this->_ptr);
}
//->运算符重载
T* operator->() {
assert(this->_ptr == nullptr);
return this->_ptr;
}
//析构函数
~SmartPointer() {
(*this->_count)--;
if (*this->_count == 0) {
delete this->_ptr;
delete this->_count;
}
}
size_t use_count(){
return *this->_count;
}
};
int main() {
{
SmartPointer<int> sp(new int(10));
SmartPointer<int> sp2(sp);
SmartPointer<int> sp3(new int(20));
sp2 = sp3;
std::cout << sp.use_count() << std::endl;
std::cout << sp3.use_count() << std::endl;
}
//delete operator
}
二、STL新容器
1、C++11 STL容器分类
C++11 STL在原有基础上增加了一些新的容器:forward_list、array等,增加后STL的容器分类如下:
(1)顺序容器
1、vector:可变数组。支持快速随机访问。在尾部之外的位置插入或删除元素可能很慢;
2、deque:双端队列。支持快速随机访问。在头尾位置插入/删除速度很快;
3、list:双向链表。只支持双向顺序访问。在list任何位置进行插入/删除操作速度都很快;
4、forward_list:单向链表。只支持单向顺序访问。在链表任何位置进行插入/删除操作速度都很快;
5、array:固定大小数组。支持快速随机访问。不能添加或删除元素;
6、string:与vector相似的容器,但专门用于保存字符。随机访问快。在尾部插入/删除速度快;
(2)关联容器
有序集合:
1、map:关联数组。保存关键字-值对;
2、set:关键字即值,即只保存关键字的容器;
3、multimap:关键字可重复出现的map;
4、multiset:关键字可重复出现的set;
无序集合:
1、unordered_map:用哈希函数组织的map;
2、unordered_set:用哈希函数组织的set;
3、unordered_multimap:用哈希函数组织的map,关键字可以重复出现;
4、unordered_multiset:用哈希函数组织的set,关键字可以重复出现。
2、array
array最早是在boost中出现,当时的初衷是希望提供一个在栈上分配的,定长数组,而且可以使用STL中个模板。
array的用法如下:
#include <string>
#include <iterator>
#include <iostream>
#include <algorithm>
#include <array>
using namespace std;
int main()
{
// construction uses aggregate initialization
array<int, 3> a1{ {1, 2, 3} }; // double-braces required in C++11 (not in C++14)
array<int, 3> a2 = {1, 2, 3}; // never required after =
array<string, 2> a3 = { string("a"), "b" };
// container operations are supported
sort(a1.begin(), a1.end());
reverse_copy(a2.begin(), a2.end(),
ostream_iterator<int>(cout, " "));
cout << '\n';
// ranged for loop is supported
for(const auto& s: a3)
cout << s << ' ';
}
3、forward_list
为了尽可能地提高效率,forward_list只提供单方向的遍历方式,同时不提供取得链表大小的操作。其他方面与一般的链表没有什么区别。
【示例】
struct Node{
Node(int v):value(v),next(nullptr){}
Node* next;
int value;
};
DWORD begin = GetTickCount();
//std::forward_list<int> list;
/*
std::list<int> list;
for(int i = 0; i < 10000000; ++i)
list.push_front(i);
*/
Node* head = nullptr;
for(int i = 0; i < 10000000; ++i){
Node* new_node = new Node(i);
new_node->next = head;、
head = new_node;
}
DWORD end = GetTickCount();
std::cout << end - begin << std::endl;
代码分别使用forward_list,list,手写链表进行了确认,结果如下:
插入速度的比较结果:手写链表的速度最快,list容器次之,forward_list容器最慢。
仅从forward_list和手写链表的比较结果来讲,使用尽量小的内存,牺牲了一些性能,换来了便利。因此,只有在你真正对内存敏感时考虑使用forward_list吧。
4、unordered_map/unordered_set
同样是来至boost的组件,在早期的标准库STL中是只有红黑树map,而没有hash map的。所以boost提供了unordered这个组件,并且在c++11中进入了标准库。unordered_map提供了和map类似的接口,只是map是有序,而unordered_map因为采用hash map的数据结构,所以是无序的。
另外,因为map采用的是红黑树,所以查找性能是O(log(n))。而unordered_map采用hash map,所以查找性能是O(1)。所以一般来说小规模的数据适合采用map(百W以下),而大规模的数据适合unordered_map(百W以上)。
unordered_map使用如下:
#include <iostream>
#include <string>
#include <unordered_map>
int main()
{
// Create an unordered_map of three strings (that map to strings)
std::unordered_map<std::string, std::string> u = {
{"RED","#FF0000"},
{"GREEN","#00FF00"},
{"BLUE","#0000FF"}
};
// Iterate and print keys and values of unordered_map
for( const auto& n : u ) {
std::cout << "Key:[" << n.first << "] Value:[" << n.second << "]\n";
}
// Add two new entries to the unordered_map
u["BLACK"] = "#000000";
u["WHITE"] = "#FFFFFF";
// Output values by key
std::cout << "The HEX of color RED is:[" << u["RED"] << "]\n";
std::cout << "The HEX of color BLACK is:[" << u["BLACK"] << "]\n";
return 0;
}
5、begin/end
std::begin/std::end并不是容器,但是因为设计std::begin/std::end的目的应该是为了让传统的C风格数组可以使用stl中的模板算法,所以也放在这里介绍。std::begin/std::end使用如下:
#include <iostream>
#include <vector>
#include <iterator>
int main()
{
std::vector<int> v = { 3, 1, 4 };
auto vi = std::begin(v);
std::cout << *vi << '\n';
int a[] = { -5, 10, 15 };
auto ai = std::begin(a);
std::cout << *ai << '\n';
}