13、【C++】C++11新特性:智能指针/STL容器

C++11新特性:智能指针/STL容器

一、智能指针

    1、auto_ptr指针

    2、shared_ptr指针

    3、unique_ptr指针

    4、weak_ptr指针

    5、weak_ptr指针解决循环引用

    6、智能指针的设计和实现

二、STL新容器

    1、C++11 STL容器分类

    2、array

    3、forward_list

    4、unordered_map/unordered_set

    5、begin/end

一、智能指针

    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';
}

猜你喜欢

转载自blog.csdn.net/sinat_33924041/article/details/83783412