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
}
目录
打赏
0
0
0
0
32
分享
相关文章
【c++丨STL】基于红黑树模拟实现set和map(附源码)
本文基于红黑树的实现,模拟了STL中的`set`和`map`容器。通过封装同一棵红黑树并进行适配修改,实现了两种容器的功能。主要步骤包括:1) 修改红黑树节点结构以支持不同数据类型;2) 使用仿函数适配键值比较逻辑;3) 实现双向迭代器支持遍历操作;4) 封装`insert`、`find`等接口,并为`map`实现`operator[]`。最终,通过测试代码验证了功能的正确性。此实现减少了代码冗余,展示了模板与仿函数的强大灵活性。
58 2
【c++丨STL】map/multimap的使用
本文详细介绍了STL关联式容器中的`map`和`multimap`的使用方法。`map`基于红黑树实现,内部元素按键自动升序排列,存储键值对,支持通过键访问或修改值;而`multimap`允许存在重复键。文章从构造函数、迭代器、容量接口、元素访问接口、增删操作到其他操作接口全面解析了`map`的功能,并通过实例演示了如何用`map`统计字符串数组中各元素的出现次数。最后对比了`map`与`set`的区别,强调了`map`在处理键值关系时的优势。
137 73
【c++丨STL】set/multiset的使用
本文深入解析了STL中的`set`和`multiset`容器,二者均为关联式容器,底层基于红黑树实现。`set`支持唯一性元素存储并自动排序,适用于高效查找场景;`multiset`允许重复元素。两者均具备O(logN)的插入、删除与查找复杂度。文章详细介绍了构造函数、迭代器、容量接口、增删操作(如`insert`、`erase`)、查找统计(如`find`、`count`)及`multiset`特有的区间操作(如`lower_bound`、`upper_bound`、`equal_range`)。最后预告了`map`容器的学习,其作为键值对存储的关联式容器,同样基于红黑树,具有高效操作特性。
69 3
C++ 容器全面剖析:掌握 STL 的奥秘,从入门到高效编程
C++ 标准模板库(STL)提供了一组功能强大的容器类,用于存储和操作数据集合。不同的容器具有独特的特性和应用场景,因此选择合适的容器对于程序的性能和代码的可读性至关重要。对于刚接触 C++ 的开发者来说,了解这些容器的基础知识以及它们的特点是迈向高效编程的重要一步。本文将详细介绍 C++ 常用的容器,包括序列容器(`std::vector`、`std::array`、`std::list`、`std::deque`)、关联容器(`std::set`、`std::map`)和无序容器(`std::unordered_set`、`std::unordered_map`),全面解析它们的特点、用法
C++ 容器全面剖析:掌握 STL 的奥秘,从入门到高效编程
【c++丨STL】priority_queue(优先级队列)的使用与模拟实现
本文介绍了STL中的容器适配器`priority_queue`(优先级队列)。`priority_queue`根据严格的弱排序标准设计,确保其第一个元素始终是最大元素。它底层使用堆结构实现,支持大堆和小堆,默认为大堆。常用操作包括构造函数、`empty`、`size`、`top`、`push`、`pop`和`swap`等。我们还模拟实现了`priority_queue`,通过仿函数控制堆的类型,并调用封装容器的接口实现功能。最后,感谢大家的支持与关注。
106 1
|
3月前
|
【c++丨STL】stack和queue的使用及模拟实现
本文介绍了STL中的两个重要容器适配器:栈(stack)和队列(queue)。容器适配器是在已有容器基础上添加新特性或功能的结构,如栈基于顺序表或链表限制操作实现。文章详细讲解了stack和queue的主要成员函数(empty、size、top/front/back、push/pop、swap),并提供了使用示例和模拟实现代码。通过这些内容,读者可以更好地理解这两种数据结构的工作原理及其实现方法。最后,作者鼓励读者点赞支持。 总结:本文深入浅出地讲解了STL中stack和queue的使用方法及其模拟实现,帮助读者掌握这两种容器适配器的特性和应用场景。
83 21
深入浅出 C++ STL:解锁高效编程的秘密武器
C++ 标准模板库(STL)是现代 C++ 的核心部分之一,为开发者提供了丰富的预定义数据结构和算法,极大地提升了编程效率和代码的可读性。理解和掌握 STL 对于 C++ 开发者来说至关重要。以下是对 STL 的详细介绍,涵盖其基础知识、发展历史、核心组件、重要性和学习方法。
【c++丨STL】list模拟实现(附源码)
本文介绍了如何模拟实现C++中的`list`容器。`list`底层采用双向带头循环链表结构,相较于`vector`和`string`更为复杂。文章首先回顾了`list`的基本结构和常用接口,然后详细讲解了节点、迭代器及容器的实现过程。 最终,通过这些步骤,我们成功模拟实现了`list`容器的功能。文章最后提供了完整的代码实现,并简要总结了实现过程中的关键点。 如果你对双向链表或`list`的底层实现感兴趣,建议先掌握相关基础知识后再阅读本文,以便更好地理解内容。
84 1
【c++丨STL】list的使用
本文介绍了STL容器`list`的使用方法及其主要功能。`list`是一种双向链表结构,适用于频繁的插入和删除操作。文章详细讲解了`list`的构造函数、析构函数、赋值重载、迭代器、容量接口、元素访问接口、增删查改操作以及一些特有的操作接口如`splice`、`remove_if`、`unique`、`merge`、`sort`和`reverse`。通过示例代码,读者可以更好地理解如何使用这些接口。最后,作者总结了`list`的特点和适用场景,并预告了后续关于`list`模拟实现的文章。
129 7
深入解析C++中的函数指针与`typedef`的妙用
本文深入解析了C++中的函数指针及其与`typedef`的结合使用。通过图示和代码示例,详细介绍了函数指针的基本概念、声明和使用方法,并展示了如何利用`typedef`简化复杂的函数指针声明,提升代码的可读性和可维护性。
153 1
AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等