C++STL容器和智能指针

简介: C++STL容器和智能指针

C++11特性

g++的编译指令

g++ -o test test.cpp -std=c++11

1.1智能指针的分类

  • unique_ptr:独占所有权,没有引用计数,性能好
  • shared_ptr:共享所有权,性能略差
  • weak_ptr:配合shared_ptr解决循环引用的问题

1.2智能指针的好处

  • 自动释放内存,防止内存泄漏
  • 共享所有权指针的传播和释放,比如多线程同时使用一个指针的析构问题

1.3智能指针的场景

  1. 使用智能指针自动释放内存
//Buffer对象分配在堆上,但是可以自动释放
std::shared_ptr<Buffer> buf = std::make_shared<Buffer>("auto free memory");
//Buffer对象分配在堆上,但是需要手动delete
Buffer *buf = new Buffer("auto free memory");
  1. 共享所有权指针的传播和释放
#include <iostream>
#include <memory>
#include <thread>
#include <queue>
#include <string.h>
#include <string>
#include <functional>
#include <atomic>
#include <mutex>
#include <condition_variable>
#include <binders.h>
class Buffer
{
public:
    Buffer(const char *str)
    {
        size = strlen(str);
        ptr_ = new char[size + 1];
        memcpy(ptr_, str, size);
        ptr_[size] = '\0';
        std::cout << "Buffer Construct, ptr:" << ptr_ << std::endl;
    }
    const char *get()
    {
        return ptr_;
    }
    ~Buffer()
    {
        std::cout << "Buffer Destructor, ptr" << ptr_ <<std::endl;
        if(ptr_)
        {
            delete [] ptr_;
        }
    }
private:
    char *ptr_ = nullptr;
    size_t size = 0;
};
//封装一个线程
class Thread
{
public:
    Thread(std::string name):name_(name)
    {
        std::cout << "Thread Constructor" << std::endl;
    }
    virtual ~Thread()
    {
        std::cout << "Thread Destructor" << std::endl;
        if(!IsTerminate())
            this->Stop();
    }
    void Start()
    {
        std::thread thr(std::bind(&Thread::Run, this));
        thread_ = std::move(thr);
    }
    std::thread::id GetId()
    {
        return thread_.get_id();
    }
    void Stop()
    {
        {
            std::unique_lock<std::mutex> lock(mutex_);
            terminate_ = true;
            condition.notify_one();
        }
        std::cout << "Stop terminate_:" << terminate_ << std::endl;
        if(thread_.joinable())
        {
            std::cout << "wait thread exit" << std::endl;
            thread_.join();
        }
    }
    virtual void Run() = 0;
    bool IsTerminate()
    {
        return terminate_;
    }
protected:
    std::string name_;
    bool terminate_ = false;
    std::thread thread_;
    std::mutex  mutex_;
    std::condition_variable condition;
};
class MyThread :
        public Thread
{
public:
    MyThread(std::string name): Thread(name)
    {
        std::cout << "Thread name :" << name_ << std::endl;
    }
    virtual  ~MyThread()
    {
    }
    void Push(std::shared_ptr<Buffer> buf)
    {
        std::unique_lock<std::mutex> lock(mutex_);
        shared_queue_.push(buf);
        condition.notify_one();
    }
    void Stop2()
    {
        std::unique_lock<std::mutex> mutex;
        terminate_ = true;
        condition.notify_one();
        if(thread_.joinable())
            thread_.join();
    }
    virtual void Run() override
    {
        while(!IsTerminate())
        {
            std::shared_ptr<Buffer> buf;
            bool ok = get(buf);
            if(ok)
                std::cout << name_ << " handle " << buf->get() << std::endl;
        }
    }
    bool get(std::shared_ptr<Buffer>& buf)
    {
        std::unique_lock<std::mutex> mutex;
        if(shared_queue_.empty())
        {
            std::cout << "wait into" <<std::endl;
            condition.wait(mutex, [this]{
                std::cout << "wait check terminate_:" << terminate_ << ", queue:" << !shared_queue_.empty() << std::endl;
                return terminate_ || !shared_queue_.empty();
            });
        }
        if(terminate_)
            return false;
        if(!shared_queue_.empty())
        {
            buf = std::move(shared_queue_.front());
            shared_queue_.pop();
            return true;
        }
        return false;
    }
private:
    std::queue<std::shared_ptr<Buffer>> shared_queue_;
};
int main()
{
    std::shared_ptr<Buffer> buf = std::make_shared<Buffer>("auto free memory");
    MyThread thread_a("Thread A");
    MyThread thread_b("Thread B");
    std::shared_ptr<Buffer> buf1 = std::make_shared<Buffer>("01234");
    std::shared_ptr<Buffer> buf2 = std::make_shared<Buffer>("56789");
    std::shared_ptr<Buffer> buf3 = std::make_shared<Buffer>("abcde");
    thread_a.Start();
    thread_b.Start();
    thread_a.Push(buf1);
    thread_b.Push(buf1);
    thread_a.Push(buf2);
    thread_b.Push(buf2);
    thread_a.Push(buf3);
    thread_b.Push(buf3);
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::cout << "sleep_for end" << std::endl;
    thread_a.Stop();
    thread_b.Stop();
    std::cout << "main end" << std::endl;
    return 0;
}

1.4shared_ptr共享智能指针

std::shared_ptr使用引用计数,每一个shared_ptr的拷贝构造都指向同一个内存,,再最后的shared_ptr析构的时候,内存才会被释放。

std::shared_ptr共享被管理对象,当最后一个 std::shared_ptr对象销毁时,被管理对象自动销毁

所以shared_ptr包含了两部分:

  • 指向堆上创建的对象的裸指针,raw_ptr
  • 指向内部影藏的、共享的管理对象,shared_count_object(就是统计堆上对象被多少次引用,即“引用计数”)

1.5shared_ptr的基本用法

s.get();        //返回保存的裸指针
s.reset(...);   //无参数:若智能指针是指向该对象的唯一指针,则释放置空。若不是,引用计数-1同事P置空
                //有参数:若唯一,则释放并且指向新对象,若不唯一,减少引用计数指向新对象。
auto s = make_shared<int>(100);
s.reset(new int(200));

1.6shared_ptr初始化

//1
std::shared_ptr<int> = p1(new int(100));
//2
std::shared_ptr<int> p2 = p2;
//3
std::shared_ptr<int> p3;
p3.reset(new int(100));

优先使用make_shared,因为效率高

auto s = make_ptr<int>(100);
shared_ptr<int> s1 = make_shared<int>(100);
shared_ptr<int> s2(new int(100));

不能将原始指针直接赋值给智能指针shared_ptr<int> s = new int(100);

2.1unique_ptr独占智能指针

  • unique_ptr是一个独占型的智能指针,不能将其赋值给另外一个unique_ptr
  • unique_ptr可以指向一个数组
  • unique_ptr需要确定删除器的类型
unique_ptr<T> my_ptr(new T);
unique_ptr<T> my_other_ptr = my_ptr         //error
unique_ptr<T> My_ptr(new T);
unique_ptr<T> My_othre_ptr = std::move(My_ptr);//right

2.2unique_ptr指向数组

std::unique_ptr<int []> ptr(new, int[10]);
ptr[9] = 9;
std::shared_ptr<int []> ptr1(new, int[10]);//不合法

2.3unique_ptr的删除器

std::shared_ptr<int> ptr3(new int(1), [](int *p){delete p;});//right
std::unique_ptr<int> ptr4(new int(1), [](int *p){delete p;});//error

unique_ptr需要确定删除器类型,所以不能向shared_ptr那样直接指定删除器,可以这么写

std::unique_ptr<int, void(*)(int*)> ptr5(new int(1), [](int *p){delete p;});

3.1weak_ptr弱引用智能指针

  • share_ptr虽然已经很好用了,但是有一点share_ptr智能指针还是有内存泄露的情 况,当两个对象相互使用一个shared_ptr成员变量指向对方,会造成循环引用,使引用计数失效,从而导致内存泄漏。
  • weak_ptr 是一种不控制对象生命周期的智能指针, 它指向一个 shared_ptr 管理的对象. 进行该对象的内存管理的是那个强引用的shared_ptr, weak_ptr只是提供了对管理对象的一个访问手段。
  • weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作, 它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少。

3.2weak_ptr基本用法

  1. 通过use_count()方法获取当前观察资源的引用计数:
shared_ptr<int> sp(new int(10));
weak_ptr<int> wp(sp);
printf("%d", wp.use_count());
  1. 通过expired()方法判断观察资源是否释放:
shared_ptr<int> sp(new int(10));
weak_ptr<int> wp(sp);
if(wp.expired())
    printf("wp无效,资源已释放");
else
printf("wp有效");
  1. 通过lock监视shared_ptr:
std::weak_ptr<int> gw;
void f()
{
    auto spt = gw.lock();
    if(gw.expired())
    {
        //gw资源无效,已释放
    }
    else
    {
        std::cout << *spt << std::endl;
    }
    int main()
    {
        {
        auto sp = std::make_shared<int>(42);
        gw = sp;
        f();
        }
        f();
        return 0;
    }
}

4.1智能指针的安全问题

引用计数本身是安全的,但是以下几种情况需要另外讨论

情况1:多线程代码操作同一个shared_ptr的对象,此时是不安全的。

比如std::thread的回调函数,是一个lambda表达式,其中引用一个shared_ptr

std::thread td([&sp1]()){....}

又或者通过回调函数的参数传入的shared_ptr对象,参数类型引用

void fn(shared_ptr<A> sp)
{
}
std::thread td(fn, sp1);

情况2:多线程代码操作的不是同一个shared_ptr对象

5.1右值引用和移动语义

  • 左值:左值可以取地址,位于等号左边
  • 右值:右值不可以取地址,位于等号右边

5.1左值引用和右值引用

左值引用:能指向左值,不能指向右值

int a = 5;
int &ref_a = a; //左值引用指向左值,对
int &ref_a = 5;//左值引用指向右值,错

引用是变量的的别名,由于右值没有地址,没法被修改,所以左值引用无法指向右值

但是const左值引用可以指向右值const int &ref_a = 5;

const左值引用不会修改指向值,因此可以指向右值,这也是为什么用const &作为函数参数的原因之一,如std::vector的push_back

void push_back(const value_type& val);

5.2右值引用

右值引用的标志是&&,可以指向右值,不能指向左值

int &&ref_a_right = 5;//ok
int a = 5;
int &&ref_a_left = a;//错误,右值引用不能指向左值
ref_a_right = 6;//右值引用的用途,修改右值

5.3右值引用指向左值

std::move()

int a = 5;
int &ref_a_left = a;
int &&ref_a_right = std::move(a);
cout << a;
  • 不理解左右值概念的人们往往以为它能把一个变量里的内容移动到另一个变量;
  • 但事实上std::move移动不了什么,唯一的功能是把左值强制转化为右值,让右值引用可以指向左值。其实现等同于一个类型转换: static_cast<T&&>(lvalue) 。 所以,单纯的std::move(xxx)不会有性能提升。
  • 同样的,右值引用能指向右值,本质上也是把右值提升为一个左值,并定义一个右值引用通过std::move指向该左值:

6.1forward完美转发

forward完美转发实现了参数再传递过程中保持其值属性的功能,即若是左值转发后依然为左值,若是右值,转发后为右值。

int &&a = 10;
int &&b = a; //error

虽然a是一个右值引用,但是a有内存名字,所以a本身是一个左值,再用右值引用a是不对的。

因此需要std::forward完美转发,这种T && val中的val是左值,但是如果用std::forward(val),就会按照参数原来的类型转发;

int &&a = 10;
int &&b = std::forward<int>(a);

6.2emplace_back减少内存拷贝和移动

对于语句

vector<string> testvec;
testvec.push_back(string(16, 'a));

上述代码的底层实现:

  1. 首先,string(16, ‘a’)会创建一个string类型的临时对象,这涉及到一次string构造过程。
  2. 其次,vector内会创建一个新的string对象,这是第二次构造。
  3. 最后在push_back结束时,最开始的临时对象会被析构。加在一起,这两行代码会涉及到两次string构造和一次析构。

优化:

c++11可以用emplace_back代替push_back,emplace_back可以直接在vector中构建一个对象,而非创建一个临时对象,再放进vector,再销毁emplace_back可以省略一次构建和一次析构,从而达到优化的目的。

7.1匿名函数lambda

  • 语法
    [捕获列表](参数列表) mutable(可选) 异常属性 -> 返回类型 {}
  • 规则
    lambda表达式可以看成是一般函数的函数名被略去,返回值使用了一个 -> 的形式表示。唯一与普通函数不同的是增加了“捕获列表”。
int main()
{
    auto Add = [](int a, int b)->int{
        return a+b;
    };
    printf("%d", Add(1, 2);)//3
    return 0;
}

一般情况下,编译器可以自动推断出lambda表达式的返回类型,所以我们可以不指定返回类型,即:

int main()
{
    auto Add = [](int a, int b) {
        return a+b;
    };
    printf("%d", Add(1, 2);)//3
    return 0;
}

但是如果函数体内有多个return语句时,编译器无法自动推断出返回类型,此时必须指定返回类型。

7.2捕获列表

7.2.1值捕获

类似参数传递,值捕获的前提是变量可以被拷贝,不同之处在于:被捕获变量再lambda创建时拷贝,而非调用时拷贝:

void test3()
{
    int c = 12;
    int d = 30;
    auto Add = [c, d](int a, int b)->int{
        printf("d = %d", d);
        return c;
    }
    d = 20;
    printf("%d", Add(1, 2));
}

7.2.2引用捕获

与引用传参类似,是引用,值变化。

void test5()
{
    int c = 12;
    int d = 30;
    auto Add = [&c, &d](int a, int b)->int{
        c = a;
        printf("d = %d\r\n", d);
        return c;
    }
    d = 20;
    printf("%d\r\n", Add(1, 2));
};

7.2.3隐式捕获

手动书写捕获列表有时候很复杂,这种机械的工作可以交给编译器,这时候可以在捕获列表中写一个&或者=向编译器声明采用某种捕获。

void test7()
{
    int c = 12;
    int d = 30;
    auto Add = [&](int a, int b)->int{
        c = a;
        printf("d = %d\r\n", d);
        return c;
    };
    d = 20;
    printf("%d\r\n", Add(1, 2));
    printf("c:%d", c);
}

7.2.4空捕获列表

捕获列表’[]'中为空,表示Lambda不能使用所在函数中的变量。

void test8()
{
cout << "test7" << endl;
int c = 12;
int d = 30;
// 把捕获列表的&改成=再测试
// [] 空值,不能使用外面的变量
// [=] 传值,lambda外部的变量都能使用
// [&] 传引用值,lambda外部的变量都能使用
auto Add = [&](int a, int b)->int {
cout << "d = " << d << endl; // 编译报错
return c;// 编译报错
};
d = 20;
std::cout << Add(1, 2) << std::endl;
std::cout << "c:" << c<< std::endl;
}

7.2.5

  • 上面提到的值捕获、引用捕获都是已经在外层作用域声明的变量,因此这些捕获方式捕获的均为左值,而不能捕获右值。
  • C++14之后支持捕获右值,允许捕获的成员用任意的表达式进行初始化,被声明的捕获变量类型会根据表达式进行判断,判断方式与使用 auto 本质上是相同的:
void test9()
{
    auto important = std::make_unique<int>(1);
    auto add = [v1 = 1, v2 = std::move(important)](int x, int y)->int{
        return x + y + v1 + (*v2);
    };
    printf("%d", add( 3, 4));
}

7.2.6泛型lambda

在C++14之前,lambda表示的形参只能指定具体的类型,没法泛型化。从 C++14 开始, Lambda 函数的形式参数可以使用 auto关键字来产生意义上的泛型:

void test10()
{
    auto add = [](auto x, auto y){
        return x + y;
    };
    printf("%d", add(1, 2));
    printf("%d", add(1.1, 2.2));
}

7.2.7可变lambda

  • 采用值捕获的方式,lambda不可以修改其值,如果要修改使用mutable修饰
  • 采用引用捕获的方式,lambda可以修改其值
void test12()
{
    int v = 5;
    auto ff = [v]() mutable {return ++v};
    v = 0;
    auto j = ff();//j = 6
}
void test13()
{
    int v = 5;
    auto ff = [&v]() mutable {return ++v};
    v = 0;
    auto j = ff();//j = 1
}
相关文章
|
7天前
|
存储 算法 C++
C++ STL 初探:打开标准模板库的大门
C++ STL 初探:打开标准模板库的大门
61 10
|
7天前
|
存储 搜索推荐 C++
【C++篇】深度剖析C++ STL:玩转 list 容器,解锁高效编程的秘密武器2
【C++篇】深度剖析C++ STL:玩转 list 容器,解锁高效编程的秘密武器
31 2
【C++篇】深度剖析C++ STL:玩转 list 容器,解锁高效编程的秘密武器2
|
3天前
|
算法 安全 Linux
【C++STL简介】——我与C++的不解之缘(八)
【C++STL简介】——我与C++的不解之缘(八)
|
7天前
|
存储 C++ 容器
【C++篇】深度剖析C++ STL:玩转 list 容器,解锁高效编程的秘密武器1
【C++篇】深度剖析C++ STL:玩转 list 容器,解锁高效编程的秘密武器
33 5
|
5天前
|
存储 编译器 C语言
C++入门2——类与对象1(类的定义和this指针)
C++入门2——类与对象1(类的定义和this指针)
15 2
|
7天前
|
存储 编译器 C++
【C++篇】揭开 C++ STL list 容器的神秘面纱:从底层设计到高效应用的全景解析(附源码)
【C++篇】揭开 C++ STL list 容器的神秘面纱:从底层设计到高效应用的全景解析(附源码)
27 2
|
6天前
|
传感器 数据处理 定位技术
多线程;顺序容器;智能指针
多线程的创建创建线程比较简单,C++提供头文件thread,使用std的thread实例化一个线程对象创建。 std::thread 在 #include 头文件中声明,因此使用 std::thread 时需要包含 #include 头文件。 #include &lt;iostream&gt; #include &lt;thread&gt; #include &lt;stdlib.h&gt; //sleep using namespace std; void t1() //普通的函数,用来执行线程 { for (int i = 0; i &lt; 10; ++i)
9 0
多线程;顺序容器;智能指针
|
4天前
|
存储 C++
c++的指针完整教程
本文提供了一个全面的C++指针教程,包括指针的声明与初始化、访问指针指向的值、指针运算、指针与函数的关系、动态内存分配,以及不同类型指针(如一级指针、二级指针、整型指针、字符指针、数组指针、函数指针、成员指针、void指针)的介绍,还提到了不同位数机器上指针大小的差异。
8 0
|
3天前
|
消息中间件 NoSQL Kafka
Flink-10 Flink Java 3分钟上手 Docker容器化部署 JobManager TaskManager Kafka Redis Dockerfile docker-compose
Flink-10 Flink Java 3分钟上手 Docker容器化部署 JobManager TaskManager Kafka Redis Dockerfile docker-compose
15 4