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

C++模板与STL核心实战:从泛型编程到容器高效应用 泛型编程是C++实现代码复用和类型安全的核心范式,其核心机制是模板。模板通过类型参数化,允许编写独立于数据类型的通用代码,编译器在编译时根据具体类型进行实例化。这一原理奠定了C++标准模板库(STL)的基础,使其能够提供一套高效、通用的数据结构和算法。STL的价值在于将算法与数据结构分离,通过迭代器作为粘合剂,极大地提升了代码的复用性、可维护性和性能。在实际开发中,STL容器如vector(动态数组)和string(字符串类)是最常用的组件。理解vector的连续存储与动态扩容机制,以及string的内部实 阅读详情

最近在做一个线程类,考虑到将来会用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_back和emplace_back操作可能导致的扩容开销,还有当我们在vector的任意位置插入删除元素(非尾部),都会导致O(n)复杂度的元素移动,这种移动也会导致大量的复制开销。
  4. **后来又想了一下,貌似顺序型容器只有vector有这种问题,其他的顺序型容器应该不存在扩容的情况。比如deque就不会因扩容导致大量元素复制,但是在deque中间插入删除元素也会导致大量元素移位而产生复制/移动开销。
  • 因此,当我们使用vector的时候,最好提前使用reserve方法,预留足够的空间,防止扩容导致的复制操作。如果对象本身提供了移动构造的话,相对来说开销没那么大。
  • 另一方面,如果用vector来存对象,看起来貌似不是特别好,因此正如我开头所说,如果使用智能指针来管理对象资源,那么vector内只需要存放智能指针对象,而非裸对象。
    当容量不够时,扩容操作仅会对智能指针对象发生复制或移动(如果是std::unique_ptr那么应该是移动)。
    特别是当你需要管理的对象占据空间比较大的时候,用智能指针来管理对象资源并用容器来收集这些资源,会更加节省开销。
  • 另外,如果一定要管理裸对象,我认为用std:list会更好,同样属于顺序型容器,std::list相当于链表,不存在所谓的固定容量,当所需管理的对象资源数量不确定的情况下,用std::list相对会节省开销。只是std::list不提供随机访问的operator[],面对具体问题需要做一些取舍。
vector的扩容原理、resize与reserve的区别 vector 存储的空间在内存中是连续的,如果 vector 现有空间已存满元素,在 push_back 新增数据的时候就要分配一块更大的内存,将原来的数据 copy 过来,接着释放之前的内存,再在新的内存空间中存入新增的元素。不同编译器对 vector 的扩容方式实现不一样,在 vs 中以 1.5 倍扩容,而在 gcc 中以 2 倍扩容,后面我们会看到以 1.5 倍扩容的方式效果更好。 阅读详情

相关推荐

C++实现KTV点歌系统:核心数据结构与项目实战解析

C++是系统开发中常用的编程语言,掌握数据结构与文件操作是构建完整应用的基础。KTV点歌系统作为经典的项目案例,综合运用链表、动态数组、查找排序算法和面向对象设计,帮助开发者理解如何将零散语法整合成可运行的工程。本文从歌曲库建模、播放队列管理、排行榜统计等核心模块出发,详解vector与链表的选型、二分查找与模糊匹配的实现、CSV文件持久化及常见内存异常排查技巧,并通过实际代码演示控制台交互的完整流程。无论是课程设计还是想巩固C++工程能力,这个项目都能提供高价值的实战参考。

weixin_29597551的博客 293

c++中vector在push_back时调用移动构造函数

c++构造函数的调用 c++11中构造函数包括: 默认构造函数 拷贝构造函数 移动构造函数 拷贝赋值函数 移动赋值函数 那么请思考下面的一个案例中其调用的顺序是怎样的: class A { public: A() { printf("A()--默认构造\n"); } ~A() { printf("~A()--析构函数\n"); } A(const A &a) { printf("A(const A&

li123_123_的博客 2631

Windows下Cppcheck静态测试全流程:从安装到生成HTML报告的避坑指南

本文详细介绍了在Windows平台下使用Cppcheck进行静态代码分析的全流程,包括环境搭建、插件配置、自动化脚本编写及HTML报告生成。重点解析了MISRA插件集成、常见问题排查以及性能优化技巧,帮助C/C++开发者高效提升代码质量。文章特别针对Windows环境下的路径处理、空格问题等细节提供了实用解决方案。

weixin_29313547的博客 200

vector/移动构造(C++11)/左值右值引用

vector :可是存放任何类型的数组vector 对象的定义:vector &lt;元素类型&gt; 数组对象名(数组长度例子: vector &lt;int&gt; ary(5) //建立大小为5的数组可以利用 vector对象名发 [下标表达式]对数组元素进行饮用获取数组长度:用size函数:vector对象名.size()移动构造:1. 使用深层复制构造函数: 返回时构造临时对象, 动态分...

weixin_42380877的博客 3116

RAG在医药行业为什么80%都翻车了?

这篇文章不是要劝你别做RAG,而是把坑说清楚。医药行业有它的特殊性——监管压力、专业术语密度、多语言混排、文档结构复杂——这些因素叠加在一起,让标准RAG教程里的那套方法论几乎直接失效

qq_31808385的博客 635

大白话系列:什么是微服务

用大白话和实际例子来通俗易懂地告诉你什么是微服务~

2401_87403086的博客 1242

深入分析移动构造函数及其原理

std::move

u012159565的专栏 1561

全网最全的AItium Designer 16下载资源与安装步骤

Altium Designer 除了全面继承包括Protel 99SE、Protel DXP在内的先前一系列版本的功能和优点外,还增加了许多改进和很多高端功能。该平台拓宽了板级设计的传统界面,全面集成了FPGA设计功能和SOPC设计实现功能,从而允许工程设计人员能将系统设计中的FPGA与PCB设计及嵌入式设计集成在一起。...

WQ的博客 1万+

vetcor使用移动构造取代拷贝构造实现push_back

智能指针,C++容器操作,移动语义结合容器操作,大大提升效率, 智能指针常见错误,移动语义常见写法与错误分析

weixin_45119096的博客 472

Redmi K30 5G刷 LineageOS17.1

前言 买手机前就观望了Xdr论坛,看Redmi K30 5G有LineageOS17.1的Rom并且还挺热,应该还不错,就入手了。晚上就刷机了。买了小米就是为了折腾,为发烧而生。 在开始前还请先安装adb工具,因为我自己是做Android开发的,自带adb,不会安装的可以百度“Android安装adb工具”。还有下载刷机文件:百度云 密码:2a0f 刷机的系统是刚买来的低版本,没有升级过Rom,如果高版本慎刷。 还有这个Rom是5G版本的,4G版本的不要刷。 建议没有刷过机器的最好不要尝试,变砖后果自负。

u010436867的博客 1万+

PX4飞控CAN总线通信深度配置:从menuconfig到驱动加载的完整指南

本文详细解析了PX4飞控中CAN总线通信的深度配置流程,从menuconfig选项设置到驱动加载的完整指南。涵盖硬件抽象层、驱动层和应用层的配置细节,提供实际验证方法和故障排查技巧,帮助开发者高效实现CAN通信,特别适用于无人机和机器人底盘运动控制等场景。

weixin_30374009的博客 499

C++ Lambda表达式:从语法到实战,提升代码简洁性与STL算法效率

在C++编程中,函数对象(Functor)和函数指针是传递自定义逻辑的常见方式,但它们在处理临时、简单的操作时往往显得代码臃肿且意图割裂。为了解决这一问题,C++11引入了lambda表达式,它本质上是一个由编译器生成的匿名类,允许开发者以内联方式定义轻量的、可调用的代码块。lambda的核心价值在于其便捷的语法和高效的性能,它通过捕获列表(capture list)灵活地访问外部变量,并可与STL算法(如std::sort、std::for_each)无缝结合,极大地提升了代码的表达力和局部封装能力。在异

weixin_34198453的博客 365

AI测试入门:如何测试AI对话模型/智能体?「详细介绍」

作为测试人员,我们需要掌握特定的技能和方法来有效地测试这些复杂的AI对话模型和智能体。传统的软件测试方法在这里可能不再完全适用,我们需要开发新的测试策略和技术来应对AI系统的独特特性。在这篇文章中,我们将深入探讨如何测试AI对话模型和智能体。

blues_C的博客 6856

搜狗C++校招笔试题解析:考点梳理与实战模板

C++工程师的校招笔试往往考察的不只是语法记忆,更是对底层原理和工程实践的掌握。从对象模型、内存布局到STL迭代器失效,再到字符串处理、链表反转、多线程并发等高频考点,企业的笔试题设计始终围绕真实业务场景展开。理解虚函数、智能指针、条件变量等概念的本质,才能在限定时间内写出健壮且高效的代码。本文以搜狗2017年C++试卷为切入点,系统拆解考点结构,提炼可复用的代码模板与应试策略,帮助准备校招的开发者查漏补缺,同时将基础理论映射到搜索引擎、输入法等实际应用后端的技术选型中,具有较强的自测与复习参考价值。

470

手把手教你学simulink实例--基于Simulink的船舶动力定位系统建模与仿真

船舶动力定位系统(Dynamic Positioning System, DPS)是一种通过控制推进器来保持船舶在指定位置和航向的技术。它广泛应用于海上钻井平台、海洋调查船、起重船等需要精确位置控制的场景。DPS的核心任务是实时感知环境干扰(如风、浪、流),并通过闭环控制算法调整推进器输出,以抵消外界扰动并维持船舶的位置和姿态。本项目旨在基于MATLAB/Simulink平台,构建船舶动力定位系统的数学模型,并进行仿真分析,验证其性能和稳定性。

MHD0815的博客 1082

ET2012新版免狗免安装绿色软件

ET2012新版免狗免安祖昂绿色软件。,新界面,新视觉,

贝壳找房C++秋招笔试复盘:核心考点与高分备考指南

在C++工程师的求职路上,语言底层机制与高频算法模板始终是笔试考核的两大基石。从constexpr的版本演进到多线程并发中的ABA问题,扎实的语言功底决定了代码的可靠性;而快速幂、单调栈等经典算法,则考验着候选人对时间复杂度的敏感度与工程抽象能力。理解这些概念背后的原理,不仅有助于应对在线笔试中的ACM模式输入输出,更能为后续处理真实业务场景中的性能瓶颈打下基础。本文以贝壳找房秋招笔试为切入点,系统梳理了C++核心语法、STL使用陷阱、树形动态规划及贪心策略等高频考点,并结合实际考场经验与常见失误,提供了一

381

VC中sqlite的使用

在VC中对sqlite的使用,实现数据库大批数据的快速插入,读取数据库中的数据在ListCtrl中显示

奇安信C++校招笔试复盘:核心考点与实战经验详解

C++底层开发中,内存管理和并发控制是衡量工程师基本功的核心指标。智能指针(如shared_ptr/weak_ptr)解决了资源释放问题,移动语义则提升了容器操作性能;而多线程编程中的条件变量、回调机制,直接影响安全产品的稳定性和响应效率。这些技术点不仅是企业级C++开发的高频考点,也是保障终端安全软件常驻运行、避免内存泄漏和崩溃的关键。结合安全厂商的真实业务场景,笔试往往围绕这些底层细节展开,考察候选人对语言特性和工程实践的熟悉程度。以奇安信C++校招笔试题为切入点,系统复盘了语言特性、内存模型、并发控制

weixin_30291791的博客 411

蓝牙模块HM-10手册

蓝牙模块HM-10手册,该手册对蓝牙模块HM-10进行了详细的介绍和使用方法以及一些AT指令。

win10 跳过U8环境检测的IE WebControls.rar

win10 安装u8的时候 提示IE WebControls 不通过,这个工具可以欺骗u8的检查。 跳过U8环境检测的IE WebControls。提示导入失败后,重新检测通过。

Momenta 2019秋招技术岗笔试题复盘:自动驾驶算法岗备考指南

自动驾驶技术岗笔试不仅考察编程能力,更检验候选人对深度学习、C++、SLAM及决策规划等核心知识的综合掌握程度。从基础概念出发,理解反向传播、Batch Normalization、智能指针、状态估计等原理,是构建完整知识体系的关键。这些技术在实际应用中支撑着感知、定位与路径规划等模块的工程落地,也是算法岗位笔试与面试的高频考点。本文以Momenta 2019秋招技术岗笔试题为切入点,复盘选择、编程、简答三类题型的考察逻辑与备考策略,帮助求职者更有针对性地准备自动驾驶算法岗、感知岗与SLAM岗的笔试环节。

weixin_30619101的博客 278

C++高阶知识:深入分析移动构造函数及其原理

移动构造函数是C++11中新增加的一种构造函数,其作用是提高程序性能。今天我们就细扒一下它的工作原理,看看它是怎么提高性能的。

木牛的博客 1161

C++:移动构造函数与vector动态内存分配

探究c++中移动构造函数在vector动态内存分配时的表现

qq_32810257的博客 1234

浅析vector容器(3)-使用移动语义提高性能

文章目录定义一个用于测试的类没有移动构造时构造vector对象向vector中推入对象a提供移动构造时构造vector对象向vector中推入对象a移动构造必须加上noexcept 在上一篇文章中,我们可知,在vector进行扩容或缩容的时候,元素的拷贝是不可避免的。既然拷贝是不可避免的,那么能不能较低拷贝的开销哪? C++11中很大的一个特性是移动语义,移动语义可以将资源“偷”过来,避免了资源的拷贝和释放。在类有很大的资源时,使用移动语义可以大幅的提升类构造和赋值的性能。 接下来分析一下,移动语义是如何提

专注音视频基础、WebRTC、ffmpeg、流媒体服务器 3945

vector里面存放指针的注意事项

vector存放指针

燃犀的博客 1282

关于c++中vector的push_back、拷贝构造copy constructor和移动构造move constructor

问题来自C++ Primer的第十三章练习题的13.48.是这样说的: 定义一个vector&lt;String&gt;并在其上多次调用push_back运行你的程序,并观察String被拷贝了多少次。 其中String是在习题中自己写的一个类似标准库string的类。(第一次看,自己写的String类确实有点挫,于是直接用了github上找到的CppPrimer答案中的String类。 ...

正善有的博客 5880

C++移动语义-移动构造函数和移动赋值运算符重载

本文介绍了C++移动语义-移动构造函数和移动赋值运算符重载

hubing_hust的专栏 6507

C++ 移动构造函数在vector扩容时的场景

这是为什么呢,原来STL在选择调用移动构造函数还是拷贝构造函数时,首先要考虑的是,已经存在的元素能否安全地从老的内存空间转移到新的内存空间,一般来说拷贝会比移动更加安全一点,所以在同等情况下会优先考虑用拷贝构造函数。考虑一个场景:vector里面的元素是类的对像,并且该类的对象提供移动构造函数,那么当vector扩容时,需要把元素从老的内存空间移动到新的内存空间,那么此时STL会调用移动构造函数吗?编译:g++ test.cpp -o test。

Nemo的博客 769
上一篇: WSL无法访问网络的解决办法
下一篇: C++设计模式由浅入深(一)—— 继承和多态
wbvalid
博客等级 码龄10年 35粉丝 15原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值