关于STL容器vector与移动构造函数的小坑

最近在做一个线程类,考虑到将来会用STL容器来存放线程类的对象(后来思考了一下存智能指针也许会更好,详见后文分析),因此在设计线程类的时候,就主动声明了复制构造函数和左值引用赋值运算符为delete。然后手动实现了移动构造函数和右值引用赋值运算符,大概如下:

//因为考虑到线程类可能没有动态多态的需求,就用CRTP实现了静态多态
//头文件就略了
template<class Derived>
class ThreadBase
{
protected:
    std::unique_ptr<std::thread> thread_;
    std::string threadName_;
    bool isRunning_;

    Derived* cast()
    {
        return static_cast<Derived*>(this);
    }
    Derived* cast() const
    {
        return static_cast<const Derived*>(this);
    }
    ThreadBase(std::string name)
        : threadName_(name)
        , isRunning_(false)
    {}
    ThreadBase(const ThreadBase &) = delete;
    ThreadBase &operator=(const ThreadBase &) = delete;
    ThreadBase(ThreadBase&& rhs)
        : thread_(std::move(rhs.thread_))
        , threadName_(std::move(rhs.threadName_))
        , isRunning_(rhs.isRunning_)
    {
        std::cout << "thread base moved\n";
    }
    ThreadBase &operator=(ThreadBase&& rhs)
    {
        std::cout << "thread base move assigned\n";
        thread_ = std::move(rhs.thread_);
        threadName_ = std::move(rhs.threadName_);
        isRunning_ = rhs.isRunning_;
        return *this;
    }
    ~ThreadBase()
    {
        if (!thread_)
        {
            std::cout << "thread_ null ptr\n";
            return ;
        }
        if (thread_->joinable())
        {
            thread_->join();
        }
        else
        {
            std::cout << "thread not joinable\n";
        }
    }
    void routine()
    {
        cast()->routine();
    }
public:
    friend Derived; //以便派生类使用基类的构造函数,基类不允许构造对象
    void start()
    {
        isRunning_ = true;
        thread_.reset(new std::thread(std::bind(&ThreadBase<Derived>::routine, this)));
    }
    void stop()
    {
        isRunning_ = false;
    }
};

std::mutex globalMutex; //测试需要,保证输出的顺序不乱

class D : public ThreadBase<D>
{
public:
    friend ThreadBase<D>; //允许基类调用派生类的非公开方法
    D(const std::string& name)
        : ThreadBase<D>(name)
    {}
    D(D&&) = default;

protected:
    void routine()
    {
        while (isRunning_)
        {
            {
                std::lock_guard<std::mutex> lg(globalMutex);
                std::cout << threadName_ << " is working 1s...\n";
            }
            std::this_thread::sleep_for(std::chrono::seconds(1));
        }
    }
};

测试用的代码如下:

int main(int argc, const char** argv) 
{
    std::vector<D> vThreads;
    for (int i = 0; i < 5; ++i)
    {
        std::string name = "thread[$]";
        name[name.find('$')] = static_cast<char>('0' + i);
        //vThreads.push_back(D(name));
        vThreads.emplace_back(name);
        std::cout << "emplace end\n";
    }
    std::cout << "vector end\n";
    for (auto& th : vThreads)
    {
        th.start();
    }
    std::cout << "start end\n";
    std::cout << "main thread sleeping for 5 seconds\n";
    std::this_thread::sleep_for(std::chrono::seconds(5));
    for (auto& th : vThreads)
    {
        th.stop();
    }
    std::cout << "stop end\n";
    return 0;
}

编译运行后惊奇的发现,每次调用emplace_back的时候,之前每插入过一个元素,移动构造函数就会多调用一次。

╰─± ./a.out 
thread base moved
thread_ null ptr
emplace end
thread base moved
thread base moved
thread_ null ptr
thread_ null ptr
emplace end
thread base moved
thread base moved
thread base moved
thread_ null ptr
thread_ null ptr
thread_ null ptr
emplace end
thread base moved
thread_ null ptr
emplace end
thread base moved
thread base moved
thread base moved
thread base moved
thread base moved
thread_ null ptr
thread_ null ptr
thread_ null ptr
thread_ null ptr
thread_ null ptr
emplace end 

当for循环第五次调用emplace_back的时候,屏幕会输出5次thread base move assigned并且输出5次析构函数的内容。
这个结果让我很是奇怪,按理说,emplace_back每次只会移动构造一个线程对象,结果却并非想象的那样。那么到底哪里出了问题呢?
通过gdb跟踪每次调用移动构造函数发现,当容器中的元素多与1个的时候,调用栈中都出现了一个奇怪的东西:
在这里插入图片描述
就是vector中的realloc,莫非是因为容器的初始大小不够,在扩容的时候又产生了移动?
在StackOverflow上找了一下相关的问题,发现确实如此。由于vector的初始容量不够,因此在扩容的时候产生了复制/移动操作。于是我就在for循环之前,提前调用了vector<T>::reserve方法,预留了5个空间给线程类,然后编译再次运行。结果就很正常了,每次emplace_back都只调用了一次移动构造函数,符合预期。

总结思考

由于容器容量不够而导致扩容的问题,可能会引发很严重的后果:

  1. 以vector为例,如果调用push_back或者emplace_back时,由于容量不够,而触发动态扩容的时候,会将原来存储的对象全部复制或者移动(如果对象有移动构造函数)到扩容后的空间中去,此之谓"reallocation";
  2. 那么当容器内的对象数量非常多的时候,这些复制的开销将会非常大。
  3. 不仅push_backemplace_back操作可能导致的扩容开销,还有当我们在vector的任意位置插入删除元素(非尾部),都会导致O(n)复杂度的元素移动,这种移动也会导致大量的复制开销。
  4. **后来又想了一下,貌似顺序型容器只有vector有这种问题,其他的顺序型容器应该不存在扩容的情况。比如deque就不会因扩容导致大量元素复制,但是在deque中间插入删除元素也会导致大量元素移位而产生复制/移动开销。
  • 因此,当我们使用vector的时候,最好提前使用reserve方法,预留足够的空间,防止扩容导致的复制操作。如果对象本身提供了移动构造的话,相对来说开销没那么大。
  • 另一方面,如果用vector来存对象,看起来貌似不是特别好,因此正如我开头所说,如果使用智能指针来管理对象资源,那么vector内只需要存放智能指针对象,而非裸对象。
    当容量不够时,扩容操作仅会对智能指针对象发生复制或移动(如果是std::unique_ptr那么应该是移动)。
    特别是当你需要管理的对象占据空间比较大的时候,用智能指针来管理对象资源并用容器来收集这些资源,会更加节省开销。
  • 另外,如果一定要管理裸对象,我认为用std:list会更好,同样属于顺序型容器,std::list相当于链表,不存在所谓的固定容量,当所需管理的对象资源数量不确定的情况下,用std::list相对会节省开销。只是std::list不提供随机访问的operator[],面对具体问题需要做一些取舍。
<think>嗯,用户现在的情况是会C语言,想要在春招期间快速学习C++,达到能通过笔试和面试的水平。每天有两个小时的空闲时间,需要制定一个高效的学习计划。首先,我得分析一下C++和C的主要区别,以及春招笔试面试中常见的C++考点。 用户已经会C语言,所以基础语法部分可能不需要从头学起,但C++有很多特有的概念,比如面向对象编程、STL、智能指针、RAII等。这些是C中没有的,但又是面试中经常考察的重点。需要确定哪些知识点是必须掌握的,哪些可以暂时跳过。 接下来要考虑时间安排。用户每天只有两小时,春招可能已经临近,所以计划需要在短时间内覆盖关键内容。可能需要分阶段进行,比如先掌握基础语法和OOP,然后深入学习STL和内存管理,最后刷题和模拟面试。 另外,用户的目标是通过笔试和面试,所以实践和刷题非常重要。光有理论不够,必须结合题目来巩固知识,尤其是LeetCode或剑指Offer中的常见题目,可能还需要针对C++的特性进行练习,比如使用STL容器解决问题。 还要注意常见面试问题,比如虚函数、多态、智能指针的实现原理等。这些概念可能需要深入理解,而不仅仅是表面知识。同时,项目经验或者实战练习可能会加分,但时间紧张的话,可能需要用一些小项目或案例来展示C++的能力。 可能还需要推荐一些资源,比如书籍、在线教程、刷题网站等。考虑到时间有限,应该选择速成材料,比如《C++ Primer》虽然经典,但可能太厚,可以选择《Effective C++》或者网上的一些速成教程。 最后,计划需要具体到每天的学习内容,确保在有限的时间内覆盖所有关键点,并且留出时间进行复习和模拟面试。可能需要每周安排复习,避免遗忘,同时通过错题和难点加强练习。 总结下来,学习计划应该分为几个阶段:基础语法和OOP(1周)、STL和高级特性(1周)、刷题和实战(2周)、模拟面试和查漏补缺(1周)。每个阶段明确目标,每天安排具体的学习任务和练习,确保高效利用时间。</think>根据你的情况和时间限制,我为你设计了一套4周高效学习方案(每天2小时,总学习时间约56小时),聚焦笔试面试核心考点,重点利用C语言基础加速掌握C++特性: ### 一、学习阶段规划(3周) **🌟 第一周:C++核心特性突破** 1. 类对象(3天) - 类定义/构造函数/析构函数(对比C结构体) - 继承体系(对比C模块化编程) - 虚函数表原理(重点!面试高频) - 多态实现机制(vptr底层原理) - 抽象类/接口设计模式 **🚀 第二周:现代C++内存管理** 1. 关键新特性(3天) - 引用vs指针(对比C参数传递) - const/constexpr深度理解 - 移动语义(右值引用重点突破) - 类型推导(auto/decltype) 2. 内存管理(4天) - 智能指针(unique/shared/weak) - 练习:实现简易智能指针 **💻 第三周:STL实战编程** 1. 标准模板库(4天) - 容器vector/deque/map/set(重点) - 迭代器失效问题(笔试高频坑点) - 算法:sort/find/copy - 练习:LeetCode容器相关题目 2. 模板基础(3天) - 函数模板/类模板 - 模板特化偏特化 - 练习:实现泛型链表 ### 二、实战强化阶段(1周) **🎯 第四周:笔试面试冲刺** 1. 高频考点专训(3天) - 虚函数表手绘(重点!) - 对象内存布局分析 - 智能指针循环引用问题 - STL源码常见考点 2. 真题实战(4天) - 每日2道LeetCode中等题(优先队列/链表/树) - 牛客网C++专项练习 - 剑指Offer经典题C++实现 ### 三、学习资源推荐 1. 速成教程: - 慕课网《C++面试通关攻略》 2. 刷题平台: - LeetCode企业题库(筛选C++标签) - 牛客网往年真题 3. 调试工具: - OnlineGDB快速验证 - CLion调试内存布局 ### 四、每日学习模板 ```markdown 19:00-19:40 核心概念学习(配合代码示例) 19:40-20:00 手写代码验证 20:00-20:40 针对性刷题 20:40-21:00 错题复盘/面试题默写 ``` ### 五、重点注意事项 1. 面试必杀技: - 资源管理使用智能指针 - 掌握vector底层扩容机制 2. 简历亮点: - 用C++11/14特性改造C项目 - 实现过简易STL组件(如智能指针) - 对比分析C/C++实现差异 建议每周留出1小时进行模拟面试(可用牛客网模拟面试功能),重点训练白板编程和底层原理阐述能力。遇到难点时,优先保证核心概念(OOP/STL/智能指针)的深度理解,再扩展其他知识点。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值