美文网首页程序员C++2.0
C++ 11 常用特性的使用经验总结(二)

C++ 11 常用特性的使用经验总结(二)

作者: Python编程导师 | 来源:发表于2019-02-13 14:18 被阅读19次

    4、智能指针内存管理

    在内存管理方面,C++11的std::auto_ptr基础上,移植了boost库中的智能指针的部分实现,如std::shared_ptr、std::weak_ptr等,当然,想boost::thread一样,C++11也修复了boost::make_shared中构造参数的限制问题。把智能指针添加为标准,个人觉得真的非常方便,毕竟在C++中,智能指针在编程设计中使用的还是非常广泛。

    什么是智能指针?网上已经有很多解释,个人觉得“智能指针”这个名词似乎起得过于“霸气”,很多初学者看到这个名词就觉得似乎很难。

    简单地说,智能指针只是用对象去管理一个资源指针,同时用一个计数器计算当前指针引用对象的个数,当管理指针的对象增加或减少时,计数器也相应加1或减1,当最后一个指针管理对象销毁时,计数器为1,此时在销毁指针管理对象的同时,也把指针管理对象所管理的指针进行delete操作。

    如下图所示,简单话了一下指针、智能指针对象和计数器之间的关系:

    下面的小章节中,我们分别介绍常用的两个智能指针std::shared_ptr、std::weak_ptr的用法。

    4.1、std::shared_ptr

    std::shared_ptr包装了new操作符动态分别的内存,可以自由拷贝复制,基本上是使用最多的一个智能指针类型。

    我们通过下面例子来了解下std::shared_ptr的用法:

    //示例代码1.0 http://www.cnblogs.com/feng-sc/p/5710724.html

    #include class Test

    {

    public:

        Test()

        {

            std::cout "Test()"  std::endl;

        }

        ~Test()

        {

            std::cout "~Test()"  std::endl;

        }

    };

    int main()

    {

        std::shared_ptr p1 = std::make_shared();

        std::cout "1 ref:"  std::endl;

        {

            std::shared_ptr p2 = p1;

            std::cout "2 ref:"  std::endl;

        }

        std::cout "3 ref:"  std::endl;

        return 0;

    }

    运行结果:

    从上面代码的运行结果,需要读者了解的是:

    1、std::make_shared封装了new方法,boost::make_shared之前的原则是既然释放资源delete由智能指针负责,那么应该把new封装起来,否则会让人觉得自己调用了new,但没有调用delete,似乎与谁申请,谁释放的原则不符。C++也沿用了这一做法。

    2、随着引用对象的增加std::shared_ptr p2 = p1,指针的引用计数有1变为2,当p2退出作用域后,p1的引用计数变回1,当main函数退出后,p1离开main函数的作用域,此时p1被销毁,当p1销毁时,检测到引用计数已经为1,就会在p1的析构函数中调用delete之前std::make_shared创建的指针。

    4.2、std::weak_ptr

    std::weak_ptr网上很多人说其实是为了解决std::shared_ptr在相互引用的情况下出现的问题而存在的,C++官网对这个只能指针的解释也不多,那就先甭管那么多了,让我们暂时完全接受这个观点。

    std::weak_ptr有什么特点呢?与std::shared_ptr最大的差别是在赋值是,不会引起智能指针计数增加。

    我们下面将继续如下两点:

    1、std::shared_ptr相互引用会有什么后果;

    2、std::weak_ptr如何解决第一点的问题。

    A、std::shared_ptr 相互引用的问题示例:

    //示例代码1.0 http://www.cnblogs.com/feng-sc/p/5710724.html

    #include class TestB;

    class TestA

    {

    public:

        TestA()

        {

            std::cout "TestA()"  std::endl;

        }

        void ReferTestB(std::shared_ptr test_ptr)

        {

            m_TestB_Ptr = test_ptr;

        }

        ~TestA()

        {

            std::cout "~TestA()"  std::endl;

        }

    private:

        std::shared_ptr m_TestB_Ptr; //TestB的智能指针

    };

    class TestB

    {

    public:

        TestB()

        {

            std::cout "TestB()"  std::endl;

        }

        void ReferTestB(std::shared_ptr test_ptr)

        {

            m_TestA_Ptr = test_ptr;

        }

        ~TestB()

        {

            std::cout "~TestB()"  std::endl;

        }

        std::shared_ptr m_TestA_Ptr; //TestA的智能指针

    };

    int main()

    {

        std::shared_ptr ptr_a = std::make_shared();

        std::shared_ptr ptr_b = std::make_shared();

        ptr_a->ReferTestB(ptr_b);

        ptr_b->ReferTestB(ptr_a);

        return 0;

    }

    运行结果:

    大家可以看到,上面代码中,我们创建了一个TestA和一个TestB的对象,但在整个main函数都运行完后,都没看到两个对象被析构,这是什么问题呢?

    原来,智能指针ptr_a中引用了ptr_b,同样ptr_b中也引用了ptr_a,在main函数退出前,ptr_a和ptr_b的引用计数均为2,退出main函数后,引用计数均变为1,也就是相互引用。

    这等效于说:

    ptr_a对ptr_b说,哎,我说ptr_b,我现在的条件是,你先释放我,我才能释放你,这是天生的,造物者决定的,改不了。

    ptr_b也对ptr_a说,我的条件也是一样,你先释放我,我才能释放你,怎么办?

    是吧,大家都没错,相互引用导致的问题就是释放条件的冲突,最终也可能导致内存泄漏。

    B、std::weak_ptr 如何解决相互引用的问题

    我们在上面的代码基础上使用std::weak_ptr进行修改:

    //示例代码1.0 http://www.cnblogs.com/feng-sc/p/5710724.html

    #include <memory>

    class TestB;

    class TestA

    {

    public:

        TestA()

        {

            std::cout << "TestA()" << std::endl;

        }

        void ReferTestB(std::shared_ptr<TestB> test_ptr)

        {

            m_TestB_Ptr = test_ptr;

        }

        void TestWork()

        {

            std::cout << "~TestA::TestWork()" << std::endl;

        }

        ~TestA()

        {

            std::cout << "~TestA()" << std::endl;

        }

    private:

        std::weak_ptr<TestB> m_TestB_Ptr;

    };

    class TestB

    {

    public:

        TestB()

        {

            std::cout << "TestB()" << std::endl;

        }

        void ReferTestB(std::shared_ptr<TestA> test_ptr)

        {

            m_TestA_Ptr = test_ptr;

        }

        void TestWork()

        {

            std::cout << "~TestB::TestWork()" << std::endl;

        }

        ~TestB()

        {

            std::shared_ptr<TestA> tmp = m_TestA_Ptr.lock();

            tmp->TestWork();

            std::cout << "2 ref a:" << tmp.use_count() << std::endl;

            std::cout << "~TestB()" << std::endl;

        }

        std::weak_ptr<TestA> m_TestA_Ptr;

    };

    int main()

    {

        std::shared_ptr<TestA> ptr_a = std::make_shared<TestA>();

        std::shared_ptr<TestB> ptr_b = std::make_shared<TestB>();

        ptr_a->ReferTestB(ptr_b);

        ptr_b->ReferTestB(ptr_a);

        std::cout << "1 ref a:" << ptr_a.use_count() << std::endl;

        std::cout << "1 ref b:" << ptr_a.use_count() << std::endl;

        return 0;

    }

    运行结果:

    由以上代码运行结果我们可以看到:

    1、所有的对象最后都能正常释放,不会存在上一个例子中的内存没有释放的问题。

    2、ptr_a 和ptr_b在main函数中退出前,引用计数均为1,也就是说,在TestA和TestB中对std::weak_ptr的相互引用,不会导致计数的增加。在TestB析构函数中,调用std::shared_ptr tmp = m_TestA_Ptr.lock(),把std::weak_ptr类型转换成std::shared_ptr类型,然后对TestA对象进行调用。

    5、其他

    本章节介绍的内容如果按照分类来看,也属于以上语法类别,但感觉还是单独拿出来总结好些。

    下面小节主要介绍std::function、std::bind和lamda表达式的一些特点和用法,希望对读者能有所帮助。

    5.1、std::function、std::bind 封装可执行对象

    std::bind和std::function也是从boost中移植进来的C++新标准,这两个语法使得封装可执行对象变得简单而易用。此外,std::bind和std::function也可以结合我们一下所说的lamda表达式一起使用,使得可执行对象的写法更加“花俏”。

    我们下面通过实例一步步了解std::function和std::bind的用法:

    Test.h文件

    //Test.h 示例代码1.0 http://www.cnblogs.com/feng-sc/p/5710724.html

    class Test

    {

    public:

        void Add()

        {

        }

    };

    main.cpp文件

    //main.cpp 示例代码1.0 http://www.cnblogs.com/feng-sc/p/5710724.html

    #include

    #include

    #include "Test.h"

    int add(int a,int b)

    {

        return a + b;

    }

    int main()

    {

        Test test;

        test.Add();

        return 0;

    }

    解释:

    上面代码中,我们实现了一个add函数和一个Test类,Test类里面有一个Test函数也有一个函数Add。

    OK,我们现在来考虑一下这个问题,假如我们的需求是让Test里面的Add由外部实现,如main.cpp里面的add函数,有什么方法呢?

    没错,我们可以用函数指针。

    我们修改Test.h

    //示例代码1.0 http://www.cnblogs.com/feng-sc/p/5710724.html

    class Test

    {

    public:

        typedef int(*FunType)(int, int);

        void Add(FunType fun,int a,int b)

        {

            int sum = fun(a, b);

            std::cout "sum:"  std::endl;

        }

    };

    修改main.cpp的调用

    //示例代码1.0 http://www.cnblogs.com/feng-sc/p/5710724.html

    ....

    ....

    Test test;

    test.Add(add, 1, 2);

    ....

    运行结果:

    到现在为止,完美了吗?如果你是Test.h的提供者,你觉得有什么问题?

    我们把问题升级,假如add实现是在另外一个类内部,如下代码:

    //示例代码1.0 http://www.cnblogs.com/feng-sc/p/5710724.html

    class TestAdd

    {

    public:

        int Add(int a,int b)

        {

            return a + b;

        }

    };

    int main()

    {

        Test test;

        //test.Add(add, 1, 2);

        return 0;

    }

    假如add方法在TestAdd类内部,那你的Test类没辙了,因为Test里的Test函数只接受函数指针。你可能说,这个不是我的问题啊,我是接口的定义者,使用者应该遵循我的规则。但如果现在我是客户,我们谈一笔生意,就是我要购买使用你的Test类,前提是需要支持我传入函数指针,也能传入对象函数,你做不做这笔生意?

    是的,你可以选择不做这笔生意。我们现在再假设你已经好几个月没吃肉了(别跟我说你是素食主义者),身边的苍蝇肉、蚊子肉啊都不被你吃光了,好不容易等到有机会吃肉,那有什么办法呢?

    这个时候std::function和std::bind就帮上忙了。

    我们继续修改代码:

    Test.h

    //示例代码1.0 http://www.cnblogs.com/feng-sc/p/5710724.html

    class Test

    {

    public:

        void Add(std::functionint(int, int)> fun, int a, int b)

        {

            int sum = fun(a, b);

            std::cout "sum:"  std::endl;

        }

    };

    解释:

    Test类中std::function表示std::function封装的可执行对象返回值和两个参数均为int类型。

    main.cpp

    //示例代码1.0 http://www.cnblogs.com/feng-sc/p/5710724.html

    int add(int a,int b)

    {

        std::cout "add"  std::endl;

        return a + b;

    }

    class TestAdd

    {

    public:

        int Add(int a,int b)

        {

            std::cout "TestAdd::Add"  std::endl;

            return a + b;

        }

    };

    int main()

    {

        Test test;

        test.Add(add, 1, 2);

        TestAdd testAdd;

        test.Add(std::bind(&TestAdd::Add, testAdd, std::placeholders::_1, std::placeholders::_2), 1, 2);

        return 0;

    }

    解释:

    std::bind第一个参数为对象函数指针,表示函数相对于类的首地址的偏移量;

    testAdd为对象指针;

    std::placeholders::_1和std::placeholders::_2为参数占位符,表示std::bind封装的可执行对象可以接受两个参数。

    运行结果:

    是的,得出这个结果,你就可以等着吃肉了,我们的Test函数在函数指针和类对象函数中都两种情况下都完美运行。

    5.2、lamda 表达式

    在众多的C++11新特性中,个人觉得lamda表达式不仅仅是一个语法新特性,对于没有用过java或C#lamda表达式读者,C++11的lamda表达式在一定程度上还冲击着你对传统C++编程的思维和想法。

    我们先从一个简单的例子来看看lamda表达式:

    //示例代码1.0 http://www.cnblogs.com/feng-sc/p/5710724.html

    int main()

    {

        auto add= [](int a, int b)->int{

            return a + b;

        };

        int ret = add(1,2);

        std::cout << "ret:" << ret << std::endl;

        return 0;

    }

    解释:

    第3至5行为lamda表达式的定义部分

    []:中括号用于控制main函数与内,lamda表达式之前的变量在lamda表达式中的访问形式;

    (int a,int b):为函数的形参

    ->int:lamda表达式函数的返回值定义

    {}:大括号内为lamda表达式的函数体。

    运行结果:

    我使用lamda表达式修改5.1中的例子看看:

    main.cpp

    //示例代码1.0 http://www.cnblogs.com/feng-sc/p/5710724.html

    .....

    int main()

    {

        Test test;

        test.Add(add, 1, 2);

        TestAdd testAdd;

        test.Add(std::bind(&TestAdd::Add, testAdd, std::placeholders::_1, std::placeholders::_2), 1, 2);

        test.Add([](int a, int b)->int {

            std::cout "lamda add fun"  std::endl;

            return a + b;

        },1,2);

        return 0;

    }

    另外本人从事在线教育多年,将自己的资料整合建了一个QQ群,对于有兴趣一起交流学习c/c++的初学者可以加群:941636044,里面有大神会给予解答,也会有许多的资源可以供大家学习分享,欢迎大家前来一起学习进步!

    相关文章

      网友评论

        本文标题:C++ 11 常用特性的使用经验总结(二)

        本文链接:https://www.haomeiwen.com/subject/eyqkeqtx.html