【C++:C++11】详解C++11右值引用与移动语义:从性能瓶颈到零拷贝优化

【C++:C++11】详解C++11右值引用与移动语义:从性能瓶颈到零拷贝优化

🔥艾莉丝努力练剑:个人主页

专栏传送门:《C语言》《数据结构与算法》C/C++干货分享&学习过程记录Linux操作系统编程详解笔试/面试常见算法:从基础到进阶测试开发要点全知道

⭐️为天地立心,为生民立命,为往圣继绝学,为万世开太平


🎬艾莉丝的简介:


🎬艾莉丝的C++专栏简介:


目录

C++学习阶段的三个参考文档

前情提示

1  C++学习的三个参考文档

2  {}初始化

3  C++11中的{}

4  引用

5  fmin

6  左值引用和右值引用

1  ~>  C++11的历史发展

1.1  历史发展

1.2  拓展讨论

2  ~>  列表初始化:{}

2.1  C++98中的{}

2.2  C++11中的{}

2.3  C++11中的std:initializer_list(初始化列表)

3  ~>  右值引用  &&  移动语义

3.1  左值和右值

3.2  左值引用和右值引用

3.2.1  概念

3.2.2  左值引用、右值引用可以相互交叉

3.2.3  引用延迟生命周期

3.2.4  左值和右值的参数匹配问题

3.3  右值引用和移动语义的使用场景

3.3.1  左值引用主要使用场景

3.3.2  传值返回需要拷贝

3.3.3  移动构造和移动赋值

3.3.4  左值拷贝和右值拷贝:拷贝构造和移动构造

3.4  右值引用和移动语义解决传值返回问题

3.4.1  两种场景实践

3.4.2  传值返回已经没有拷贝了,是否意味上面的右值引用和移动语义就没意义?

3.4.3  右值对象构造,只有拷贝构造,没有移动构造的场景

3.4.4  右值对象构造,既有拷贝构造,也有移动构造的场景

3.4.5  以上两种情况结合局部对象生命周期和栈帧的角度的理解

3.4.6  右值对象赋值,只有拷贝构造和拷贝赋值,没有移动构造和移动赋值的场景

3.4.7  右值对象赋值,既有拷贝构造和拷贝赋值,也有移动构造和移动赋值的场景

3.5  右值引用和移动语义在传参中的提效

完整代码示例与实践演示

list.h:

Test.cpp:

最终运行结果

结尾


C++学习阶段的三个参考文档

看库文件(非官方文档):cplusplus



这个文档在C++98、C++11时候还行,之后就完全没法用了……

还可以看语法——准官方文档(同步更新):C++准官方参考文档



这个行,包括C++26都同步了,我们以后会看这个。

有大佬——官方文档(类似论坛):Standard C++



这个网站上面会有很多大佬。

总结一下就是——



前情提示

1  C++学习的三个参考文档

2  {}初始化

3  C++11中的{}

4  引用

5  fmin

6  左值引用和右值引用



1  ~>  C++11的历史发展

1.1  历史发展

既然谈到C++11的历史发展,那不得不再次献出这种经典老图——

C++11是C++的第二个主要版本,并且是自从C++98版本开始的最重要的更新。C++11引入了大量更改,标准化了既有的一些实践,并改进了对C++程序员可用的抽象。在它最终由ISO在2011年8月12日采纳前,人们曾使用名称“C++0x”——因为它曾被期待在2010年之前发布(结果一直拖到2011年)。C++03(没出什么新特征)与C++11期间花了8年时间!这是迄今为止最长的版本间隔——C++08烂尾了(五年计划),因为C++08的规划太大、特性太多——C++标准委员会想做的东西太多,结果来不及了,这导致委员会此后调整了发布策略:

从那时起,C++有规律地每3年更新一次——能出多少是多少。

1.2  拓展讨论

语言被公司使用是有一个过程的。

C++标准委员会:制定理论语法。

编译器:支持C++语法——可进行有原则性的支持(常用的肯定会支持,否则编译器不好用就会被淘汰的)

举个例子,C语言的C99标准更新了变长数组(用变量),VS就不支持(编译器不支持的体现)。

包括像C++23,大多数编译器还不完全支持——

上面这张图,uu们可以了解一下:编译器对几个版本的C++的支持情况。

语言的新的语法标准被大规模使用的缓冲期:5~10年。

(1)大多数公司就是使用到C++11,当然也有40%~50%的公司在使用C++14(小版本)、C++17(中版本,不少公司用)。

(2)C++23(公司使用偏少)是个大版本:特性还不够成熟(上层的库还不够完善),学的不错的程序员不多;C++23就更少了。

VS编译器:MSVC(微软);苹果编译器:Clang(支持苹果的ObjectC,也支持C / C++)。

看编译器是否支持语法特性。

2  ~>  列表初始化:{}

2.1  C++98中的{}

C++98中一般数组和结构体可以用0进行初始化。

2.2  C++11中的{}

C++11以后,想统一初始化方式:试图实现一切对象皆可用{}初始化{}初始化也叫列表初始化

内置类型和自定义类型都支持,自定义类型本质是类型转换,中间会产生临时对象,最后优化了以后变成直接构造;

{}初始化的过程中,可以省略掉=;

C++11列表初始化的本意是想实现一个大统一的初始化方式,其次在有些场景下列表初始化会带来不少便利,如容器push / inset多参数构造的对象时,{}初始化会很方便。

2.3  C++11中的std:initializer_list(初始化列表)

上面的{}初始化已经很方便了,但是对于对象容器初始化还是不太方便,比如一个vector对象,如果我们想用N个值去构造初始化,那么我们得实现很多个构造函数才能支持——

vector<int>v1 = {1 , 2 , 3}; vector<int>v2 = {1 , 2 , 3 , 4 , 5};

C++11库中提出了一个std:initializer_list的类——

autoil={10,20,30};//the typeofilisaninitializer_list
std:initializer_list这个类的本质是:底层开一个数组,将数据拷贝过来,std:initializer_list内部有两个指针分别指向数组的开始和结束。

文档:initializer_list,std:initializer_list支持迭代器遍历。

容器支持一个std:initializer_list的构造函数,也就支持任意多个值构成的{x1 , x2 , x3...}进行初始化。STL中的容器支持任意多个值构成的{x1 , x2 , x3 , ……}进行初始化,就是std::initializer_list的构造函数支持的。如下图所示——


3  ~>  右值引用  &&  移动语义

C++98的C++语法中就有引用的语法,我们前面介绍的就是啦,而C++11中新增了的右值引|用语法特性,C++11之后,我们之前学习的引用就叫做左值引用。无论是左值引用还是右值引用,都是给对象取别名(给对象取别名,不开空间)。

3.1  左值和右值

左值,可以取地址——左值是一个表示数据的表达式(如变量名或解引用的指针),一般有持久存在,存储在内存中,可以获取它的地址(区别),左值可以出现赋值符号的左边,也可以出现在赋值符号右边。定义时const修饰符后的左值,不能给他赋值。
右值,不能取地址(区别)——右值也是一个表示数据的表达式,要么是字面值常量、要么是表达式求值过程中创建的临时对象右值可以出现在赋值符号的右边,但是一般不能出现出现在赋值符号的左边。

这里是会编译报错的,根据提示,注释掉后面的cout部分就能通过了——

3.2  左值引用和右值引用

3.2.1  概念

Type& r1 = x; Type&& rr1 = y;

第一个语句就是左值引用,左值引用就是给左值取别名;第二个语句就是右值引用,同样的道理,右值引用就是给右值取别名——这个左值和右值就是上面说的能不能取到地址的区别。

左值引用不能直接引用右值,但是const左值引用可以引用右值;
右值引用不能直接引用左值,但是右值引用可以引用move(左值)。
template typename remove_reference::type&& move (T&& arg);

move(强转)是库里面的一个函数模板,本质内部是进行强制类型转换,当然这里还涉及一些引用折叠的知识,这个我们后面会详细介绍的,现在先了解一下。

值得注意的是变量表达式都是左值属性,也就意味着一个右值被右值引用绑定后,右值引用变量变
量表达式的属性是左值

语法层面看,左值引用和右值引用都是取别名,不开空间。

从汇编底层的角度看下面代码中r1和rr1汇编层实现,底层都是用指针实现的,没什么区别。底层汇编等实现和上层语法表达的意义有时是背离的,所以不要放到一起去理解,很容易搞混!uu们在学习的时候有时候喜欢互相佐证,这样反而是陷入迷途。

3.2.2  左值引用、右值引用可以相互交叉

左值引用、右值引用可以“相互交叉”,如下图所示——

3.2.3  引用延迟生命周期

右值引用可用于为临时对象延长生命周期,const的左值引用也能延长临时对象生存期,但这些对象无法被修改。如下图,艾莉丝演示了右值引用延长匿名对象的生命周期——

3.2.4  左值和右值的参数匹配问题

C++98中,我们实现一个const左值引用作为参数的函数,那么实参传递左值和右值都可以匹配。

C++11以后,分别重载左值引用、const左值引用、右值引用作为形参的f函数,那么实参是左值会
匹配f(左值引用),实参是const左值会匹配f(const左值引用),实参是右值会匹配f(右值引用)。

右值引用变量在用于表达式时属性是左值(天才的设计!),这个设计这里会感觉有点怪,等我们介绍右值引用的使用场景时,就能体会这样设计的价值了,这里先买个关子。

3.3  右值引用和移动语义的使用场景

3.3.1  左值引用主要使用场景

左值引用主要使用场景:函数中左值引用传参和左值引用传返回值时减少拷贝,同时还可以修改实参和修改返回对象的价值。左值引用已经解决大多数场景的拷贝效率问题,但是有些场景不能使用传左值引用返回:如addStrings和generate函数,C++98中的解决方案只能是被迫使用输出型参数解决。C++11以后这里可以使用右值引用作为返回值来解决吗?显然这是不可能的,因为这里的本质是返回对象是一个局部对象,函数结束这个对象就析构销毁了,右值引用返回也无法概念对象已经析构销毁的事实。

3.3.2  传值返回需要拷贝

3.3.3  移动构造和移动赋值

移动构造函数是一种构造函数,类似拷贝构造函数,移动构造函数要求第一个参数是该类类型的引用,但是不同的是要求这个参数是右值引用,如果还有其他参数,额外的参数必须有缺省值。

移动赋值是一个赋值运算符的重载,跟拷贝赋值构成函数重载,类似拷贝赋值函数,移动赋值函数要求第一个参数是该类类型的引用,但是不同的是要求这个参数是右值引用。

对于像string / vector这样的深拷贝的类或者包含深拷贝的成员变量的类,移动构造和移动赋值才有
意义,因为移动构造和移动赋值的第一个参数都是右值引用的类型,本质是要“窃取”引用的右值对象的资源,而不是像拷贝构造和拷贝赋值那样去拷贝资源,从提高效率。下面的jqj:string样例实现了移动构造和移动赋值,我们需要结合场景理解。

3.3.4  左值拷贝和右值拷贝:拷贝构造和移动构造

3.4  右值引用和移动语义解决传值返回问题

3.4.1  两种场景实践

3.4.2  传值返回已经没有拷贝了,是否意味上面的右值引用和移动语义就没意义?

3.4.3  右值对象构造,只有拷贝构造,没有移动构造的场景

如下图展示了vs2019的debug环境下编译器对拷贝的优化,左边为不优化的情况下,两次拷贝构造;右边为编译器优化的场景下连续步骤中的拷贝合二为一变为一次拷贝构造。

需要注意的是在vs2019的release版本和vs2022的debug和release版本,下面代码优化为非常恐怖——会直接将str对象的构造,str拷贝构造临时对象,临时对象拷贝构造ret对象,合三为一,变为直接构造。要理解这个优化要结合局部对象生命周期和栈帧的角度理解。

linux下可以将下面代码拷贝到test.cpp文件,编译时用的方式关闭构造优化——

g++ test.cpp-fno-elide-constructors

运行结果可以看到下图中左边没有优化的两次拷贝。

3.4.4  右值对象构造,既有拷贝构造,也有移动构造的场景

如下图展示了vs2019的debug环境下编译器对拷贝的优化,左边为不优化的情况下,两次移动构造;右边为编译器优化的场景下连续步骤中的拷贝合二为一变为一次移动构造。

需要注意的是在vs2019的release版本和vs2022的debug和release版本,下面代码优化为非常恐怖——会直接将str对象的构造,str拷贝构造临时对象,临时对象拷贝构造ret对象,合三为一,变为直接构造。要理解这个优化要结合局部对象生命周期和栈帧的角度理解。

linux下可以将下面代码拷贝到test.cpp文件,编译时用的方式关闭移动优化——

g++ test.cpp-fno-elide-constructors

运行结果可以看到下图中左边没有优化的两次移动——

3.4.5  以上两种情况结合局部对象生命周期和栈帧的角度的理解

3.4.6  右值对象赋值,只有拷贝构造和拷贝赋值,没有移动构造和移动赋值的场景

下图的左边展示了vs2019的debug版本以及——

g++test.cpp-fno-elide-constructors

关闭优化环境下编译器的处理,一次拷贝构造,一次拷贝赋值。

需要注意的是在vs2019的release版本和vs2022的debug和release版本,下面代码会进一步优化,直接构造要返回的临时对象,str本质是临时对象的引用,底层角度用指针实现。运行结果的角度,可以看到str的析构是在赋值以后,说明str就是临时对象的别名。

3.4.7  右值对象赋值,既有拷贝构造和拷贝赋值,也有移动构造和移动赋值的场景

下图的左边展示了vs2019的debug版本以及——

g++test.cpp-fno-elide-constructors

关闭优化环境下编译器的处理,一次移动构造,一次移动赋值。

需要注意的是在vs2019的release版本和vs2022的debug和release版本,下面代码会进一步优化,直接构造要返回的临时对象,str本质是临时对象的引用,底层角度用指针实现。运行结果的角度,可以看到str的析构是在赋值以后,说明str就是临时对象的别名。

3.5  右值引用和移动语义在传参中的提效

查看STL文档我们发现C++11以后容器的push和insert系列的接口否增加的右值引用版本;

当实参是一个左值时,容器内部继续调用拷贝构造进行拷贝,将对象拷贝到容器空间中的对象;

当实参是一个右值,容器内部则调用移动构造,右值对象的资源到容器空间的对象上;

把我们之前模拟实现的bit:list拷贝过来,支持右值引用参数版本的push_back和insert;

其实这里还有一个emplace系列的接口(终于来啦),但是这个涉及可变参数模板,我们需要把可变参数模板介绍完以后再介绍emplace系列的接口——涉及引用折叠等内容——


完整代码示例与实践演示

list.h:

#pragma once namespace jqj { // --------------链表节点结构-------------- template<class T> struct list_node { list_node<T>* _next; // 指向下一个节点的指针 list_node<T>* _prev; // 指向前一个节点的指针 T _data; // 节点存储的数据 // --------------节点构造函数-------------- // 左值 list_node(const T& x = T()) :_next(nullptr) , _prev(nullptr) , _data(x) // x 节点数据,默认为T类型的默认值 {} // 右值 list_node(T&& x) :_next(nullptr) , _prev(nullptr) , _data(move(x)) // x 节点数据,默认为T类型的默认值 {} }; // --------------链表迭代器-------------- // 实现双向迭代器功能,支持前向和后向遍历 template<class T, class Ref,class Ptr> // T 数据类型 // Ref 引用类型(T& 或 const T&) struct list_iterator { // using还具有tepedef没有的功能 // 使用类型别名(C++11新特性) using Self = list_iterator<T, Ref, Ptr>; // 自身类型 using Node = list_node<T>; // 节点类型 Node* _node; // 当前指向的节点指针 // 迭代器构造函数 list_iterator(Node* node) :_node(node) {} // 迭代器解引用操作 // *it = 1 // Ref 返回节点数据的引用(可读或可写) Ref operator*() // 解引用,Ref就是reference,引用的意思 { return _node->_data; } // operator*()返回对应数据类型的引用 Ptr operator->() // 返回对应数据类型的指针 { return &_node->_data; } // ++it // 前向迭代操作 Self& operator++() // Self& 返回递增后的迭代器引用 { _node = _node->_next; return *this; } Self operator++(int) // Self 返回递增前的迭代器副本 { Self tmp(*this); _node = _node->_next; return tmp; } // --it // 后向迭代操作 Self& operator--() // Self& 返回递减后的迭代器引用 { _node = _node->_prev; return *this; } Self operator--(int) // Self 返回递减前的迭代器副本 { Self tmp(*this); _node = _node->_prev; return tmp; } // 迭代器比较操作 bool operator!=(const Self& s) const // bool 两个迭代器是否不指向同一节点 { return _node != s._node; } bool operator==(const Self& s) const // bool 两个迭代器是否不指向同一节点 { return _node == s._node; } }; //template<class T> //struct list_const_literator //{ // using Self = list_const_literator<T>; // using Node = list_node<T>; Node* _node; // Node* _node; // list_const_iterator(Node* node) // :_node(node) // { } // // *it // const T& operator*() // { // return _node->_data;; // } // // ++it // Self& operator++() // { // _node = _node->_next; // return *this; // } // Self operator++(int) // { // Self tmp(*this); // _node = _node->_next; // return *this; // } // // --it // Self& operator--() // { // _node = _node->_prev; // return *this; // } // Self operator--(int) // { // Self tmp(*this); // _node = _node->_prev; // return tmp; // } // bool operator!=(const Self& s) const // { // return _node != s._node; // } // bool operator==(const Self& s) const // { // return _node == s._node; // } //}; // --------------链表主体类-------------- template<class T> class list { using Node = list_node<T>; // 节点类型别名 public: // 迭代器类型定义 using iterator = list_iterator<T, T&, T*>; // 普通迭代器 using const_iterator = list_iterator<T, const T&, const T*>; // 常量迭代器 // const T* 只能读数据,不能修改数据 //using iterator = list_iterator<T>; //using const_iterator = list_const_iterator<T>; // --------------迭代器访问接口-------------- // 获取指向第一个元素的迭代器 // iterator 指向首元素的迭代器 iterator begin() { return iterator(_head->_next); } // iterator 指向哨兵节点的迭代器 iterator end() { return iterator(_head); } // 获取指向第一个元素的常量迭代器 // const_iterator 指向首元素的常量迭代器 const_iterator begin() const { return const_iterator(_head->_next); } // const_iterator 指向哨兵节点的常量迭代器 const_iterator end() const { return const_iterator(_head); } // ----------------链表初始化相关----------------- void empty_init() // 初始化空链表(创建哨兵节点) { _head = new Node; _head->_next = _head; _head->_prev = _head; } // 默认构造函数 list() { empty_init(); } // 初始化列表构造函数 // il 初始化列表 list(initializer_list<T> il) { empty_init(); for (auto& e : il) { push_back(e); } } // 范围构造函数 // InputIterator 输入迭代器类型 template <class InputIterator> list(InputIterator first, InputIterator last) // first 范围起始迭代器 // last 范围结束迭代器 { empty_init(); while (first != last) { push_back(*first); ++first; } } // 数量构造函数(size_t版本) list(size_t n, T val = T()) // val 元素值,默认为T的默认值 { empty_init(); for (size_t i = 0; i < n; ++i) { push_back(val); } } // 数量构造函数(int版本) list(int n, T val = T()) // val 元素值,默认为T的默认值 { empty_init(); for (size_t i = 0; i < n; ++i) { push_back(val); } } // -------------析构函数------------- // 清理所有节点并释放哨兵节点 ~list() { clear(); delete _head; _head = nullptr; _size = 0; } // -----------拷贝控制函数---------- // lt 要拷贝的源链表 // lt2(lt1) list(const list<T>& lt) { empty_init(); for (auto& e : lt) { push_back(e); } } // 拷贝赋值运算符 // lt 要拷贝的源链表 // list<T>& 返回当前链表的引用 // lt1 = lt3 list<T>& operator=(const list<T>& lt) { if (this != &lt) { clear(); for (auto& e : lt) { push_back(e); } } return *this; } //// //list(list<T>& lt) // list(const list& lt) //{ // empty_init(); // list tmp(lt.begin(), lt.end()); // swap(tmp); //} //// lt1 = lt3 ////list<T>& operator=(list<T> tmp) //list& operator=(list tmp) //{ // swap(tmp); //} // ----------------容量操作--------------- // 交换两个链表的内容 void swap(list<T>& lt) // lt 要交换的另一个链表 { std::swap(_head, lt._head); std::swap(_size, lt._size); } // 清空链表中的所有元素 // 保留哨兵节点,删除所有数据节点 void clear() { iterator it = begin(); while (it != end()) { it = erase(it); } } //void push_back(const T& x) //{ // Node* newnode = new Node(x); // Node* tail = _head->_prev; // tail->_next = newnode; // newnode->_prev = tail; // newnode->_next = _head; // _head->_prev = newnode; //} // -----------------元素访问操作-------------------- // 复用 void push_back(const T& x) // 在链表尾部插入元素,x:要插入的元素值 { insert(end(), x); } // 右值:尾插 void push_back(T&& x) { insert(end(), x); } // 在链表头部插入元素,x:要插入的元素值 void push_front(const T& x) { insert(begin(), x); } // // 右值 //void push_front(T&& x) //{ // insert(begin(), x); //} // 删除链表尾部元素 void pop_back() { erase(--end()); } // 删除链表头部元素 void pop_front() { erase(begin()); } // ---------------- 指定位置操作 ---------------- // 在指定位置前插入元素 void insert(iterator pos, const T& x) // pos:插入位置的迭代器,x:要插入的元素值 { Node* cur = pos._node; Node* prev = cur->_prev; Node* newnode = new Node(x); // // 连接新节点:prev -> newnode -> cur // prev newnode cur prev->_next = newnode; newnode->_prev = prev; newnode->_next = cur; cur->_prev = newnode; ++_size; } // 删除指定位置的元素 iterator erase(iterator pos) // iterator 返回指向被删除元素后一个元素的迭代器 { Node* cur = pos._node; Node* prev = cur->_prev; Node* next = cur->_next; // 跳过被删除节点:prev -> next prev->_next = next; next->_prev = prev; delete cur; --_size; /*return iterator(next);*/ return next; /// 返回下一个节点的迭代器 //两种写法都可以 } // -------------- 容量信息 ------------------ size_t size() const { //size_t n = 0; //for (auch e : *this) //{ // ++n; //} //return n; return _size; } private: Node* _head; // 哨兵头节点指针 size_t _size = 0; // 链表元素个数计数器 }; }

Test.cpp:

#define _CRT_SECURE_NO_WARNINGS 1 #include<iostream> #include<vector> #include<map> #include<list> #include<string> using namespace std; // ========================列表初始化=========================== // ------Point——C++98中的{}------ //struct Point //{ // int _x; // int _y; //}; // //// C++98中的{} //int main() //{ // // C++98 // int array1[] = { 1,2,3,4,5 }; // int array2[] = { 0 }; // Point p = { 1,2 }; // // return 0; //} //// ------Date日期类——C++11中的{}------ //class Date //{ //public: // Date(int year = 1,int month = 1,int day = 1) // :_year(year) // ,_month(month) // ,_day(day) // { // cout << "Date(int year,int month,int day)" << endl; // } // // Date(const Date& d) // :_year(d._year) // , _month(d._month) // , _day(d._day) // { // cout << "Date(const Date& d)" << endl; // } // //private: // int _year; // int _month; // int _day; //}; // //void Insert(const Date& d) //{ // // ... //} // //Date func() //{ // //Date d(2025, 11, 15); // //return d; // // //return { 2025, 11, 15 }; // // //Date d; // //return d; // // return {}; //} // //// C++11中的{} //int main() //{ // Date d1(2025, 11, 15); // // Date d2 = 2025; // // // 自定义类型支持 // // 这里本质是用{ 2025, 11, 15 }构造一个Date临时对象 // // 临时对象再去拷贝构造d1,编译器优化后合二为一变成{ 2025, 11, 15 }直接构造初始化d1 // // 运行一下,我们可以验证上面的理论,发现是没调用拷贝构造的 // Date d5 = { 2025, 11, 15 }; // Date d6{ 2025, 11, 15 }; // Date d7{}; // 不传参数,调用默认构造;固定个数,可以传不同类型 // // Insert(2025); // Insert({ 2025, 11, 15 }); // // 一切对象都可用{}初始化 // // 内置类型 // int i = 0; // int j = { 1 }; // int k{ 2 }; // int m{}; // 现代C++初始化趋向于用{}进行初始化 // // // 输出结果: // // Date(int year, int month, int day) // // Date(int year, int month, int day) // // Date(int year, int month, int day) // // Date(int year, int month, int day) // // Date(int year, int month, int day) // // Date(int year, int month, int day) // // Date(int year, int month, int day) // // return 0; //} //// -------------{}:initializer_list <T>---------------- //int main() //{ // // vector // vector<int> v1 = { 1,2,3,4,5 }; // 需求不固定,1~n个,必须传同一个类型 // vector<int> v2{ 1,2,3,4,4,5,6,7,8,8,9 }; // // // map // map<string, string> dict = { { "sort","排序" }, { "string","字符串" } }; // // v1 = { 10,20,30 ,40 }; // // // 验证: // auto il = { 10,20,30 ,40 }; // cout << typeid(il).name() << endl; // 打印结果 // // 输出:class std::initializer_list<int> // // 如"v1 = { 10,20,30 ,40 };",如果是走隐式类型转换传给vinitializer_list<T>:支持任意多个值初始化容器 // // std::initializer_list<int> mylist; // mylist = { 10,20,30,40 }; // cout << sizeof(mylist) << endl; // // 输出:16 // // // 这里begin和end返回的值是initializer_list对象中存的两个指针 // // 这两个指针的值跟i的地址接近,说明数组存在栈上——数据量一多数组会存到堆上 // int i = 0; // cout << mylist.begin() << endl; // cout << mylist.end() << endl; // cout << &i << endl; // // 输出: // // 0000006EBAAFFB18 // // 0000006EBAAFFB28 // // 0000006EBAAFF6A4 // // return 0; //} // =============右值引用和移动语义============== //int main() //{ // // -------左值,可以取地址------- // // 左值:左值是一个表示数据的表达式(如变量名或解引用的指针),一般是有持久状态,存储在内存中, // // 我们可以获取它的地址,左值可以出现赋值符号的左边,也可以出现在赋值符号右边。 // // 定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址(区别)。 // // // 验证左值都可以取地址 // // 以下的p、b、c、*p、s、s[0]就是常见的左值 // int* p = new int(0); // int b = 1; // const int c = b; // *p = 10; // string s("11111111111111111111111111111"); // s[0] = 'x'; // // cout << &p << endl; // cout << &b << endl; // cout << &c << endl; // cout << &(*p) << endl; // cout << &s << endl; // //cout << &s[0] << endl; // 输出:x1111111111111111111111111111,强转成void*返回 // cout << (void*)&s[0] << endl; // // 输出: // // 000000CB1E7EF4B8 // // 000000CB1E7EF4D4 // // 000000CB1E7EF4F4 // // 00000270883D7EB0 // // 000000CB1E7EF518 // // 00000270883E1930 // // // -------右值,不能取地址------- // // 右值:右值也是一个表示数据的表达式,要么是字面值常量、要么是表达式求值过程中创建的临时对象 // // 右值可以出现在赋值符号的右边,但是一般不能出现出现在赋值符号的左边,右值不能取地址(区别)。 // // double x = 1.1, y = 2.2, z = 3.3; // // // 以下几个10、x + y、fmin(x, y)、string("11111")都是常见的右值 // 10; // x + y; // fmin(x, y); // string("11111"); // 数据量小存栈上,大存到堆上 // // //// 编译报错 // //cout << &10 << endl; // 编译报错:常量上的“&” // //// 表达式必须为左值或函数指示符 // //cout << &(x + y) << endl; // //cout << &(fmin(x, y)) << endl; // //cout << &string("11111") << endl; // // return 0; //} //// -------左值引用、右值引用可以相互交叉------- //int main() //{ // // -------左值,可以取地址------- // // 以下的p、b、c、*p、s、s[0]就是常见的左值 // int* p = new int(0); // int b = 1; // const int c = b; // *p = 10; // string s("11111"); // s[0] = 'x'; // double x = 1.1, y = 2.2, z = 3.3; // // // 左值引用给左值取别名 // int& r1 = b; // int*& r2 = p; // int& r3 = *p; // string& r4 = s; // char& r5 = s[0]; // // // 右值引用给右值取别名 // int&& rr1 = 10; // double&& rr2 = x + y; // double&& rr3 = fmin(x, y); // string&& rr4 = string("11111"); // // // 左值引用不能直接引用右值,但是const左值引用可以直接引用右值 // // 语法上不能修改,但是可以间接修改 // const int& rx1 = 10; // const double& rx2 = x + y; // const double& rx3 = fmin(x, y); // const string& rx4 = string("11111"); // // // 右值引用不能直接引用左值,但是右值引用可以直接引用move(左值) // int&& rrx1 = move(b); // int*&& rrx2 = move(p); // int&& rrx3 = move(*p); // string&& rrx4 = move(s); // string&& rrx5 = (string&&)s; // // // 右值引用绑定后,右值引用的属性是左值(本身属性是左值)->要move一下 // int&& rrrx1 = move(rrx1); // 右值引用不能直接修改(没有赋值什么的),通过右值引用修改(左值属性) // // return 0; //} //// -------右值引用延长匿名对象的生命周期------- //class A //{ //public: // A() // { // cout << "A()" << endl; // } // // ~A() // { // cout << "~A()" << endl; // } //}; // //int main() //{ // //A aa1; // //// 输出: // //// A() // //// ~A() // // A aa1; // // // 延长匿名对象的生命周期 // const A& ref1 = A(); // A&& ref2 = A(); // 右值引用延长匿名对象的生命周期 // // cout << "main end" << endl; // // 输出: // // A() // // A() // // A() // // main end // // ~A() // // ~A() // // ~A() // // return 0; //} //// -------左值和右值的参数匹配问题:左值引用重载和右值引用重载验证------- //void f(int& x) //{ // std::cout << "左值引用重载 f(" << x << ")\n"; //} // //void f(const int& x) //{ // std::cout << "const的左值引用重载 f(" << x << ")\n"; //} // //void f(int&& x) //{ // std::cout << "右值引用重载 f(" << x << ")\n"; //} // //int main() //{ // f(10); // // int a = 20; // f(a); // // const int b = 20; // f(b); // // // 右值引用本身的属性是左值 // int&& x = 1; // f(x); // 调用f(int& x) // f(std::move(x)); // 调用f(int&& x) // // 输出: // // 右值引用重载 f(10) // // 左值引用重载 f(20) // // const的左值引用重载 f(20) // // 左值引用重载 f(1) // // 右值引用重载 f(1) // // return 0; //} #include<iostream> #include<assert.h> #include<algorithm> #include<string.h> using namespace std; namespace Alice { class string { public: typedef char* iterator; typedef const char* const_iterator; iterator begin() { return _str; } iterator end() { return _str + _size; } const_iterator begin() const { return _str; } const_iterator end() const { return _str + _size; } string(const char*) :_size(strlen(str)) ,_capacity(_size) { cout << "string(char* str)-构造" << endl; _str = new char[_capacity + 1]; strcpy(_str, str); } void swap(string& s) { std::swap(_str, s._str); std::swap(_size, s._size); std::swap(_capacity, s._capacity); } // 拷贝构造 string(const string& s) { cout << "string(char* str)-构造" << endl; reserve(s._capacity); for (auto ch : s) { push_back(ch); } } // 移动构造 string(string&& s) { cout << "string(char* str)-构造" << endl; swap(s); // 交换后s持有原对象的资源 } // s析构时会释放原对象的资源,但原对象现在持有什么? string& operator=(const string& s) { cout << "string(char* str)-构造" << endl; if (this != &s) { _str[0] = '\0'; _size = 0; reserve(s._capacity); for (auto ch : s) { push_back(ch); } } return *this; } // 移动赋值 string& operator=(string&& s) { cout << "string(char* str)-构造" << endl; swap(s); // 和上面同样的问题 return *this; } ~string() { //cout << "~string() -- 析构" << endl; delete[] _str; _str = nullptr; } char& operator[](size_t pos) { assert(pos < _size); return _str[pos]; } void reserve(size_t new_capacity) { //int n = 0; // 触发assert的报错机制 if (new_capacity > _capacity) { char* tmp = new char[new_capacity + 1]; if (_str) { strcpy(tmp, _str); // 包括null终止符 delete[] _str; } _str = tmp; _capacity = new_capacity; } } void push_back(char ch) { if (_size >= _capacity) { size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2; reserve(newcapacity); } _str[_size] = ch; ++_size; _str[_size] = '\0'; } string& operator+=(char ch) { push_back(ch); return *this; } const char* c_str() const { return _str; } size_t size() const { return _size; } private: char* _str = nullptr; size_t _size = 0; size_t _capacity = 0; }; // 右值引用和移动语义解决传值返回问题 // 传值返回需要拷贝 string addStrings(string num1, string num2) { string str; int end1 = num1.size() - 1, end2 = num2.size() - 1; // 进位 int next = 0; while (end1 >= 0 || end2 >= 0) { int val1 = end1 >= 0 ? num1[end1--] - '0' : 0; int val2 = end2 >= 0 ? num2[end2--] - '0' : 0; int ret = val1 + val2 + next; next = ret / 10; ret = ret % 10; str += ('0' + ret); } if (next == 1) str += '1'; reverse(str.begin(), str.end()); cout << &str << endl; return str; } } //int main() //{ // Alice::string ret = Alice::addStrings("11111", "2222"); // cout << &ret << endl; // // 输出: // // string(char* str) - 构造 // // string(char* str) - 构造 // // string(char* str) - 构造 // // 0000002A676FF898 // // 0000002A676FF898 //} //int main() //{ // Alice::string ret; // // ... // ret = Alice::addStrings("11111", "2222"); // cout << &ret << endl; // ////assert断言报错了 // // 运行成功,输出: // // string(char* str) - 构造 // // string(char* str) - 构造 // // string(char* str) - 构造 // // string(char* str) - 构造 // // 000000CEA432FAF8 // // string(char* str) - 构造 // // 000000CEA432F968 //} // 右值引用和移动语义在传参中的提效 #include"list.h" int main() { jqj::list<Alice::string> lt; cout << "**************************" << endl; Alice::string s1("111111111111111111"); lt.push_back(s1); cout << "**************************" << endl; lt.push_back("2222222222222222222222222222222222"); cout << "**************************" << endl; lt.push_back("3333333333333333333333333333333"); cout << "**************************" << endl; // 左值move本质是授予别人转移你的数据资源的权限,所以要谨慎,move过的左值以后就不能再用了 lt.push_back(move(s1)); cout << "**************************" << endl; // 运行结果如下: //string(char* str) - 构造 //string(char* str) - 构造 //* ************************* //string(char* str) - 构造 //string(char* str) - 构造 //* ************************* //string(char* str) - 构造 //string(char* str) - 构造 //* ************************* //string(char* str) - 构造 //string(char* str) - 构造 //* ************************* //string(char* str) - 构造 //* ************************* // 右值引用绑定后,右值引用的属性是左值(本身属性是左值)->要move一下 // 为什么要这样设计?为了让它能够修改! return 0; }

最终运行结果


结尾

uu们,本文的内容到这里就全部结束了,艾莉丝再次感谢您的阅读!

往期回顾:

【C++:哈希表封装】用哈希表封装unordered_map和unordered_set

结语:既然都看到这里啦!请大佬不要忘记给博主来个“一键四连”哦! 

🗡博主在这里放了一只小狗,大家看完了摸摸小狗放松一下吧!🗡

૮₍ ˶ ˊ ᴥ ˋ˶₎ა


Read more

Scrapling 终极指南:5分钟掌握Python网页抓取技术

Scrapling是一个强大的Python网页抓取库,专为解决现代网页反爬机制而设计。无论你是数据分析师、研究人员还是开发者,都能通过这个指南快速上手网页数据提取。 【免费下载链接】Scrapling🕷️ Undetectable, Lightning-Fast, and Adaptive Web Scraping for Python 项目地址: https://gitcode.com/gh_mirrors/sc/Scrapling 🚀 快速入门:从零到第一个网页抓取 环境准备与安装 首先克隆项目到本地: git clone https://gitcode.com/gh_mirrors/sc/Scrapling cd Scrapling pip install -e . 基础网页抓取实战 Scrapling提供了多种抓取方式,最简单的静态页面抓取只需要几行代码: from scrapling import get # 获取网页内容并自动解析 page

By Ne0inhk

Python 真实世界的数据科学(九)

原文:Python: Real-World Data Science 协议:CC BY-NC-SA 4.0 三十一、新闻文章聚类 在前面的大多数章节中,我们都是在了解数据的基础上进行数据挖掘的。 通过使用目标类,我们可以了解我们的变量如何在训练阶段对这些目标进行建模。 我们有目标要针对的这种学习类型称为监督学习。 在本章中,我们考虑了没有这些目标的情况。 这是无监督学习,更多的是探索性任务。 无需使用我们的模型进行分类,无监督学习的目标更多是关于探索数据以寻找见解。 在本章中,我们着眼于对新闻文章进行聚类以发现数据中的趋势和模式。 我们研究如何使用链接聚合网站显示不同的新闻故事,从不同的网站提取数据。 本章涵盖的关键概念包括: * 从任意网站获取文本 * 使用 Reddit API 收集有趣的新闻故事 * 用于无监督数据挖掘的聚类分析 * 从文档中提取主题 * 在线学习以更新模型而无需重新训练 * 集群整合以结合不同的模型 获得新闻文章 在本章中,我们将构建一个系统,该系统获取新闻文章的实时供稿并将其分组,其中各组的主题相似。 您可以在数周(或更长时间)

By Ne0inhk

迅投实盘1:一个简单的下单程序,跑通python实盘全流程

博主使用迅投投研的程序开始要写实盘的代码了,这个系列的博客就是记录使用迅投的各种方法,一边学习一边记录。 迅投把python分为两个版本: 1. 内置python:指的是使用迅投客户端,客户端可以新建python脚本写策略 2. 原生python:使用xtquant包就可以通过本地python操作客户端执行策略 内置python依赖客户端编辑器且无法调试,因此这个系列博主尽可能的使用本地的python,在Pycharm中进行开发。 迅投官方知识库:https://dict.thinktrader.net/ xtquant官方说明文档:https://dict.thinktrader.net/nativeApi/start_now.html 下单的示例程序 程序逻辑说明 这里我们就执行一个最简单的操作,看是否打通了整个流程: 1. 需要先安装好python环境,并且把xtquant的压缩包放到根目录,使用如下代码判断环境是否配置好 import time, datetime, sys from xtquant.xttrader import XtQuantTr

By Ne0inhk
【2025 最新】 Python 安装教程 以及 Pycharm 安装教程(超详细图文指南,附常见问题解决)

【2025 最新】 Python 安装教程 以及 Pycharm 安装教程(超详细图文指南,附常见问题解决)

前言         Python 作为目前最热门的编程语言之一,在数据分析、人工智能、Web 开发等领域应用广泛。而 PyCharm 作为 JetBrains 推出的 Python 集成开发环境(IDE),以其强大的功能和友好的界面成为开发者的首选工具。         本文针对 2025 年最新版 Python(3.13.x)和 PyCharm(202x.x.x),提供Windows 10或11和macOS Sonoma双系统安装教程,从官网下载到环境配置一步到位,同时整理了安装过程中最常见的 10 类问题及解决方案,确保新手也能顺利完成环境搭建。 一、Python 安装教程(2025 最新版) 1. 下载 Python 安装包 步骤 1:访问 Python 官网

By Ne0inhk