• C++11 -------- 类的新功能+可变参数模板+emplace接口


    目录

    类的新功能

    1.默认成员函数

    (1)八个默认成员函数

    (2)默认移动构造和移动赋值的生成条件

    (3)默认生成的移动构造和移动赋值会做什么

    (4)验证默认生成的移动构造和移动赋值所做的工作

    2.类变量成员初始化

    3.强制生成默认函数的关键字default

    4.禁止生成默认函数的关键字delete

    5.继承和多态中final与override关键字

    可变参数模板

    1.可变参数模板概念

    2.可变参数模板的定义方式

    (1)定义方式

    (2)举例使用

    3.参数包的展开方式

    (1)错误的展开方式

    (2)递归展开参数包

    (3)判断参数包中参数的个数 , 终止递归调用(×)

    (4)逗号表达式展开参数包

     STL容器中的emplace相关接口函数

    1.emplace版本的插入接口

     2.emplace系列接口的使用方式

     3.emplace系列接口的工作流程

    4.emplace系列接口的意义

    5.测试代码验证


             

    类的新功能

    1.默认成员函数

    (1)八个默认成员函数

    ①在C++11之前,一个类中有如下六个默认成员函数:

    • 构造函数。
    • 析构函数。
    • 拷贝构造函数。
    • 拷贝赋值函数。
    • 取地址重载函数。
    • const取地址重载函数。

                     

    ②其中前四个成员函数最重要,后面两个成员函数一般不会用到,这里“默认”的意思就是你不写编译器会自动生成。在C++11标准中又增加了两个默认成员函数,分别是移动构造和移动赋值重载函数。

                            

    (2)默认移动构造和移动赋值的生成条件

    ①C++11中新增的移动构造函数和移动赋值函数的生成条件如下:

    • 移动构造函数的生成条件:没有自己实现移动构造函数,并且没有自己实现析构函数、拷贝构造函数和拷贝赋值函数。
    • 移动赋值重载函数的生成条件:没有自己实现移动赋值重载函数,并且没有自己实现析构函数、拷贝构造函数和拷贝赋值函数。

                     

    ②移动构造和移动赋值的生成条件与之前六个默认成员函数不同,并不是单纯的没有实现移动构造和移动赋值编译器就会默认生成。

                     

    ③ 如果我们自己实现了移动构造或者移动赋值,就算没有实现拷贝构造和拷贝赋值,编译器也不会生成默认的拷贝构造和拷贝赋值。
                    

    (3)默认生成的移动构造和移动赋值会做什么

    • 默认生成的移动构造函数:对于内置类型的成员会完成值拷贝(浅拷贝),对于自定义类型的成员,如果该成员实现了移动构造就调用它的移动构造,否则就调用它的拷贝构造。
    • 默认生成的移动赋值重载函数:对于内置类型的成员会完成值拷贝(浅拷贝),对于自定义类型的成员,如果该成员实现了移动赋值就调用它的移动赋值,否则就调用它的拷贝赋值。

    (4)验证默认生成的移动构造和移动赋值所做的工作

    ①模拟实现一个简化版的string类

    1. namespace XM
    2. {
    3. class string
    4. {
    5. public:
    6. //构造函数
    7. string(const char* str = "")
    8. {
    9. _size = strlen(str); //初始时,字符串大小设置为字符串长度
    10. _capacity = _size; //初始时,字符串容量设置为字符串长度
    11. _str = new char[_capacity + 1]; //为存储字符串开辟空间(多开一个用于存放'\0')
    12. strcpy(_str, str); //将C字符串拷贝到已开好的空间
    13. }
    14. //交换两个对象的数据
    15. void swap(string& s)
    16. {
    17. //调用库里的swap
    18. ::swap(_str, s._str); //交换两个对象的C字符串
    19. ::swap(_size, s._size); //交换两个对象的大小
    20. ::swap(_capacity, s._capacity); //交换两个对象的容量
    21. }
    22. //拷贝构造函数(现代写法)
    23. string(const string& s)
    24. :_str(nullptr)
    25. , _size(0)
    26. , _capacity(0)
    27. {
    28. cout << "string(const string& s) -- 深拷贝" << endl;
    29. string tmp(s._str); //调用构造函数,构造出一个C字符串为s._str的对象
    30. swap(tmp); //交换这两个对象
    31. }
    32. //移动构造
    33. string(string&& s)
    34. :_str(nullptr)
    35. , _size(0)
    36. , _capacity(0)
    37. {
    38. cout << "string(string&& s) -- 移动构造" << endl;
    39. swap(s);
    40. }
    41. //拷贝赋值函数(现代写法)
    42. string& operator=(const string& s)
    43. {
    44. cout << "string& operator=(const string& s) -- 深拷贝" << endl;
    45. string tmp(s); //用s拷贝构造出对象tmp
    46. swap(tmp); //交换这两个对象
    47. return *this; //返回左值(支持连续赋值)
    48. }
    49. //移动赋值
    50. string& operator=(string&& s)
    51. {
    52. cout << "string& operator=(string&& s) -- 移动赋值" << endl;
    53. swap(s);
    54. return *this;
    55. }
    56. //析构函数
    57. ~string()
    58. {
    59. //delete[] _str; //释放_str指向的空间
    60. _str = nullptr; //及时置空,防止非法访问
    61. _size = 0; //大小置0
    62. _capacity = 0; //容量置0
    63. }
    64. private:
    65. char* _str;
    66. size_t _size;
    67. size_t _capacity;
    68. };
    69. }

                             

    ②简单的Person类,Person类中的成员name的类型就是我们模拟实现的string类。

    1. class Person
    2. {
    3. public:
    4. //构造函数
    5. Person(const char* name = "", int age = 0)
    6. :_name(name)
    7. , _age(age)
    8. {}
    9. //拷贝构造函数
    10. Person(const Person& p)
    11. :_name(p._name)
    12. , _age(p._age)
    13. {}
    14. //拷贝赋值函数
    15. Person& operator=(const Person& p)
    16. {
    17. if (this != &p)
    18. {
    19. _name = p._name;
    20. _age = p._age;
    21. }
    22. return *this;
    23. }
    24. //析构函数
    25. ~Person()
    26. {}
    27. private:
    28. XM::string _name; //姓名
    29. int _age; //年龄
    30. };

                     

    ③Person类当中没有实现移动构造和移动赋值,但拷贝构造、拷贝赋值和析构函数Person类都实现了,因此Person类中不会生成默认的移动构造和移动赋值

    1. int main()
    2. {
    3. Person s1("张三", 100);
    4. Person s2 = std::move(s1); //想要调用Person默认生成的移动构造
    5. return 0;
    6. }

                                     

    • 上述代码中用一个右值去构造s2对象,但由于Person类没有生成默认的移动构造函数,因此这里会调用Person的拷贝构造函数(拷贝构造既能接收左值也能接收右值),这时在Person的拷贝构造函数中就会调用string的拷贝构造函数对name成员进行深拷贝。
    • 如果要让Person类生成默认的移动构造函数,就必须将Person类中的拷贝构造、拷贝赋值和析构函数全部注释掉,这时用右值去构造s2对象时就会调用Person默认生成的移动构造函数。
    • Person默认生成的移动构造,对于内置类型成员age会进行值拷贝,而对于自定义类型成员name,因为我们的string类实现了移动构造函数,因此它会调用string的移动构造函数进行资源的转移。
    • 而如果我们将string类当中的移动构造函数注释掉,那么Person默认生成的移动构造函数,就会调用string类中的拷贝构造函数对name成员进行深拷贝。
       

                     

    ④ 验证Person类中默认生成的移动赋值函数

    1. int main()
    2. {
    3. Person s1("张三", 100);
    4. Person s2;
    5. s2 = std::move(s1); //想要调用Person默认生成的移动赋值
    6. return 0;
    7. }

                    

    ⑤补充

    • 我们在模拟实现的string类的拷贝构造、拷贝赋值、移动构造和移动赋值函数中都打印了一条提示语句,因此可以通过控制台输出判断是否调用了对应的函数。
    • 由于VS2013没有完全支持C++11,因此上述代码无法在VS2013当中验证,需要使用更新一点的编译器进行验证,比如VS2019。

                                

                        

    2.类变量成员初始化

    (1)默认生成的构造函数,对于自定义类型的成员会调用其构造函数进行初始化,但并不会对内置类型的成员进行处理。于是C++11支持非静态成员变量在声明时进行初始化赋值,默认生成的构造函数会使用这些缺省值对成员进行初始化。 

    1. class Person
    2. {
    3. public:
    4. //...
    5. private:
    6. //非静态成员变量,可以在成员声明时给缺省值
    7. XM::string _name = "张三"; //姓名
    8. int _age = 20; //年龄
    9. static int _n; //静态成员变量不能给缺省值
    10. };

                     

    (2)补充

    ①给变量一个缺省值,这个缺省值是一个声明,只有初始化列表没有初始化就会用这个缺省值

                     

    ②类的成员函数此时没有空间,这里只是声明,没有空间

                            

    ③类编译出来都是指令,没有空间。什么时候有空间?  类定义出对象的时候有空间

                            

    ④Linux 写好的程序是一个文件,存在磁盘上面的 ; 编译器对我们写好的代码进行编译,没问题生成汇编,汇编再翻译成机器码,最后出来的就是二进制的指令,如何执行这些指令?

    • Linux对代码编译之后形成二进制文件 a.out , ./a.out 的时候是Linux下的一个终端生成的一个子进程,子进程进行程序替换(调用exec程序替换函数)

                             

    ⑤替换a.out中的代码和数据,子进程开始执行main函数,建立各种栈帧,编译器提前算好各种对象需要多少空间,s1需要多少空间,s2需要多少空间,提前就开辟好了

                    
    ⑥s1,s2在这个栈帧里面把空间分配好,但是main函数结束这个对象就销毁了,因为对象是在栈帧里面的,如果是全局变量为什么不会销毁呢?

    • 全局数据存在数据段,代码段,不随栈帧走 。

                                             

    ⑦编译器怎么知道对象有多大 ?

    • 语法有规则,经过内存对齐,这个Person有多大编译器肯定知道;编译器在编译的时候已经算好了要开多大的空间
    • 这个空间开辟是程序运行起来以后,建立栈帧开辟的空间 

                    

                                    

    3.强制生成默认函数的关键字default

    • C++11可以让我们更好的控制要使用的默认成员函数,假设在某些情况下我们需要使用某个默认成员函数,但是因为某些原因导致无法生成这个默认成员函数,这时可以使用default关键字强制生成某个默认成员函数。

                                     

    ①Person类中实现了拷贝构造函数

    1. class Person
    2. {
    3. public:
    4. //拷贝构造函数
    5. Person(const Person& p)
    6. :_name(p._name)
    7. , _age(p._age)
    8. {}
    9. private:
    10. XM::string _name; //姓名
    11. int _age; //年龄
    12. };

                                     

    如下代码就无法编译成功,因为Person类中编写了拷贝构造函数,导致无法生成默认的构造函数,因为默认构造函数生成的条件是没有编写任意类型的构造函数,包括拷贝构造函数。

    1. int main()
    2. {
    3. Person s; //没有合适的默认构造函数可用
    4. return 0;
    5. }

                                    

    ③我们就可以使用default关键字强制生成默认的构造函数

    • 默认成员函数都可以用default关键字强制生成,包括移动构造和移动赋值。
    1. class Person
    2. {
    3. public:
    4. Person() = default; //强制生成默认构造函数
    5. //拷贝构造函数
    6. Person(const Person& p)
    7. :_name(p._name)
    8. , _age(p._age)
    9. {}
    10. private:
    11. cl::string _name; //姓名
    12. int _age; //年龄
    13. };

                    

                                    

    4.禁止生成默认函数的关键字delete

     (1)想要限制某些默认函数生成时,可以通过如下两种方式:

    • 在C++98中,可以将该函数设置成私有,并且只用声明不用定义,这样当外部调用该函数时就会报错。
    • 在C++11中,可以在该函数声明后面加上=delete,表示让编译器不生成该函数的默认版本,我们将=delete修饰的函数称为删除函数。

                             

    (2)要让一个类不能被拷贝,可以用=delete修饰将该类的拷贝构造和拷贝赋值。

    • 被=delete修饰的函数可以设置为公有,也可以设置为私有,效果都一样。
    1. class CopyBan
    2. {
    3. public:
    4. CopyBan()
    5. {}
    6. private:
    7. CopyBan(const CopyBan&) = delete;
    8. CopyBan& operator=(const CopyBan&) = delete;
    9. };

                    

                    

    5.继承和多态中final与override关键字

    (1)final修饰类

    • 被final修饰的类叫做最终类,最终类无法被继承 
    1. class NonInherit final //被final修饰,该类不能再被继承
    2. {
    3. //...
    4. };

                     

     (2)final修饰虚函数

    • final修饰虚函数,表示该虚函数不能再被重写,如果子类继承后重写了该虚函数则编译报错
    1. //父类
    2. class Person
    3. {
    4. public:
    5. virtual void Print() final //被final修饰,该虚函数不能再被重写
    6. {
    7. cout << "hello Person" << endl;
    8. }
    9. };
    10. //子类
    11. class Student : public Person
    12. {
    13. public:
    14. virtual void Print() //重写,编译报错
    15. {
    16. cout << "hello Student" << endl;
    17. }
    18. };

                     

     (3)override修饰虚函数

    • override修饰子类的虚函数,检查子类是否重写了父类的某个虚函数,如果没有没有重写则编译报错
    1. //父类
    2. class Person
    3. {
    4. public:
    5. virtual void Print()
    6. {
    7. cout << "hello Person" << endl;
    8. }
    9. };
    10. //子类
    11. class Student : public Person
    12. {
    13. public:
    14. virtual void Print() override //检查子类是否重写了父类的某个虚函数
    15. {
    16. cout << "hello Student" << endl;
    17. }
    18. };

                    

                    

                    

    可变参数模板

                                            

    1.可变参数模板概念

    可变参数模板是C++11新增的最强大的特性之一,它对参数高度泛化,能够让我们创建可以接受可变参数的函数模板和类模板。

    • 在C++11之前,类模板和函数模板中只能包含固定数量的模板参数,可变模板参数无疑是一个巨大的改进,但由于可变参数模板比较抽象,因此使用起来需要一定的技巧。
    • 在C++11之前其实也有可变参数的概念,比如printf函数就能够接收任意多个参数,但这是函数参数的可变参数,并不是模板的可变参数。
    • 只了解函数的可变参数模板

     

                    

    2.可变参数模板的定义方式

    (1)定义方式

    1. template<class …Args>
    2. 返回类型 函数名(Args… args)
    3. {
    4.   //函数体
    5. }
    1. template<class ...Args>
    2. void ShowList(Args... args)
    3. {}
    • 模板参数Args前面有省略号,代表它是一个可变模板参数,我们把带省略号的参数称为参数包,参数包里面可以包含0到N ( N ≥ 0 ) 个模板参数,而args则是一个函数形参参数包。
    • 模板参数包Args和函数形参参数包args的名字可以任意指定,并不是说必须叫做Args和args。

            

    (2)举例使用

    ①调用ShowList函数时就可以传入任意多个参数了,并且这些参数可以是不同类型的 

    1. int main()
    2. {
    3. ShowList();
    4. ShowList(1);
    5. ShowList(1, 'A');
    6. ShowList(1, 'A', string("MIUI"));
    7. return 0;
    8. }

                                             

     ②在函数模板中通过sizeof计算参数包中参数的个数

    1. template<class ...Args>
    2. void ShowList(Args... args)
    3. {
    4. cout << sizeof...(Args) << endl; //获取参数包中参数的个数
    5. cout << sizeof...(args) << endl << endl;
    6. }

            

                    

    3.参数包的展开方式

    • 我们无法直接获取参数包中的每个参数,只能通过展开参数包的方式来获取,这是使用可变参数模板的一个主要特点,也是最大的难点。

            

    (1)错误的展开方式

    • 语法并不支持使用args[ i ]的方式来获取参数包中的参数
    1. template<class ...Args>
    2. void ShowList(Args... args)
    3. {
    4. //错误示例:
    5. for (int i = 0; i < sizeof...(args); i++)
    6. {
    7. cout << args[i] << " "; //打印参数包中的每个参数
    8. }
    9. cout << endl;
    10. }

                    

    (2)递归展开参数包

    ①递归展开参数包的方式如下:

    • 给函数模板增加一个模板参数,这样就可以从接收到的参数包中分离出一个参数出来。
    • 在函数模板中递归调用该函数模板,调用时传入剩下的参数包。
    • 如此递归下去,每次分离出参数包中的一个参数,直到参数包中的所有参数都被取出来。

                             

     ②打印调用函数时传入的各个参数,这样编写函数模板

    1. //展开函数
    2. //解析并打印参数包中的每个参数类型及值
    3. template<class T, class ...Args>
    4. void ShowList(T val, Args... args)
    5. {
    6. cout << typeid(val).name() << ":" << val << endl; //打印分离出的第一个参数
    7. ShowList(args...); //递归调用,将参数包继续向下传
    8. }

                             

     ③现在面临的问题是,如何终止函数的递归调用

    • 我们可以在刚才的基础上,编写只有一个带参的递归终止函数,该函数的函数名与展开函数的函数名相同,构成函数重载
    • 当递归调用ShowList函数模板时,如果传入的参数包中参数的个数为1,那么就会匹配到这一个参数递归终止函数,这样就结束了递归。
    • 但是需要注意,这里的递归调用函数需要写成函数模板,因为我们并不知道最后一个参数是什么类型的。
    1. //递归终止函数
    2. //可以认为是重载版本,当参数包里面只有一个参数的时候就会调用这个重载函数
    3. template<class T>
    4. void ShowList(const T& val)
    5. {
    6. cout << val << endl << endl;
    7. }
    8. //展开函数
    9. template<class T, class ...Args>
    10. void ShowList(T value, Args... args)
    11. {
    12. cout << typeid(val).name() << ":" << val << endl; //打印分离出的第一个参数
    13. ShowList(args...); //递归调用,将参数包继续向下传
    14. }

                                    

    ④修改代码,只能调用可变参数模板的修改

    • 但如果外部调用ShowList函数时就没有传入参数,那么就会直接匹配到无参的递归终止函数。
    • 而我们本意是想让外部调用ShowList函数时匹配的都是函数模板,并不是让外部调用时直接匹配到这个递归终止函数
    1. //递归终止函数
    2. template<class T>
    3. void ShowList(const T& val)
    4. {
    5. cout << val << endl << endl;
    6. }
    7. //展开函数
    8. template<class T, class ...Args>
    9. void ShowList(T value, Args... args)
    10. {
    11. cout << typeid(val).name() << ":" << val << endl;
    12. ShowList(args...);
    13. }
    14. //供外部调用的函数
    15. template<class ...Args>
    16. void ShowListArg(Args... args)
    17. {
    18. ShowList(args...);
    19. }

                     

     ⑤关于递归终止函数也可以这样写

    • 当递归调用ShowList函数模板时,如果传入的参数包中参数的个数为0,那么就会匹配到这个无参的递归终止函数,这样就结束了递归。
    1. //递归终止函数
    2. void ShowList()
    3. {
    4. cout << endl;
    5. }
    6. //展开函数
    7. template<class T, class ...Args>
    8. void ShowList(T value, Args... args)
    9. {
    10. cout << typeid(val).name() << ":" << val << endl;
    11. ShowList(args...);
    12. }

                    

    ⑥传入参数测试

                    

     (3)判断参数包中参数的个数 , 终止递归调用(×)

    ①既然我们可以通过sizeof计算出参数包中参数的个数,那我们能不能在ShowList函数中设置一个判断,当参数包中参数个数为0时就终止递归呢

    1. //错误示例
    2. template<class T, class ...Args>
    3. void ShowList(T value, Args... args)
    4. {
    5. cout << value << " "; //打印传入的若干参数中的第一个参数
    6. if (sizeof...(args) == 0)
    7. {
    8. return;
    9. }
    10. ShowList(args...); //将剩下参数继续向下传
    11. }

                            

    ②这种方式是不可行的

    • 函数模板并不能调用,函数模板需要在编译时根据传入的实参类型进行推演,生成对应的函数,这个生成的函数才能够被调用。
    • 而这个推演过程是在编译时进行的,当推演到参数包args中参数个数为0时,还需要将当前函数推演完毕,这时就会继续推演出来传入0个参数时的ShowList函数,此时就会产生报错,因为ShowList函数要求至少传入一个参数。
    • 这里编写的if判断是在代码编译结束后,运行代码时才会所走的逻辑,也就是运行时逻辑,而函数模板的推演是一个编译时逻辑,即在程序运行之前就会产生编译的报错。
       

                     

    (4)逗号表达式展开参数包

    ①通过列表获取参数包中的参数 

    • 1.数组可以通过列表进行初始化
    int a[] = {1,2,3,4};

                             

    • 2.如果参数包中各个参数的类型都是整型,那么也可以把这个参数包放到列表当中初始化这个整型数组,此时参数包中参数就放到数组中了
    1. //展开函数
    2. template<class ...Args>
    3. void ShowList(Args... args)
    4. {
    5. int arr[] = { args... }; //列表初始化
    6. //打印参数包中的各个参数
    7. for (auto e : arr)
    8. {
    9. cout << e << " ";
    10. }
    11. cout << endl;
    12. }

                             

    • 3.调用ShowList函数时就可以传入多个整型参数了
    1. int main()
    2. {
    3. ShowList(1);
    4. ShowList(1, 2);
    5. ShowList(1, 2, 3);
    6. return 0;
    7. }

            

    • 4.C++并不像Python这样的语言,C++规定一个容器中存储的数据类型必须是相同的,因此如果这样写的话,那么调用ShowList函数时传入的参数只能是整型的,并且还不能传入0个参数,因为数组的大小不能为0,因此我们还需要在此基础上借助逗号表达式来展开参数包。

                    

    ②通过逗号表达式展开参数包

    • 逗号表达式会从左到右依次计算各个表达式,并且将最后一个表达式的值作为返回值进行返回。
    • 将逗号表达式的最后一个表达式设置为一个整型值,确保逗号表达式返回的是一个整型值。
    • 将处理参数包中参数的动作封装成一个函数,将该函数的调用作为逗号表达式的第一个表达式。

                    

    1.在执行逗号表达式时就会先调用处理函数处理对应的参数,然后再将逗号表达式中的最后一个整型值作为返回值来初始化整型数组。

    • 我们这里要做的就是打印参数包中的各个参数,因此处理函数当中要做的就是将传入的参数进行打印即可。
    • 可变参数的省略号需要加在逗号表达式外面,表示需要将逗号表达式展开,如果将省略号加在args的后面,那么参数包将会被展开后全部传入PrintArg函数,代码中的{ (PrintArg(args), 0)... }将会展开成{ (PrintArg(arg1), 0),  (PrintArg(arg2), 0),  (PrintArg(arg3), 0),  etc... }。
    1. //处理参数包中的每个参数
    2. template<class T>
    3. void PrintArg(const T& val)
    4. {
    5. cout << typeid(T).name() << ":" << val << endl;
    6. }
    7. //展开函数
    8. template<class ...Args>
    9. void ShowList(Args... args)
    10. {
    11. int arr[] = { (PrintArg(args), 0)... }; //列表初始化+逗号表达式
    12. cout << endl;
    13. }

                    

    2.这时调用ShowList函数时就可以传入多个不同类型的参数了,但调用时仍然不能传入0个参数,因为数组的大小不能为0,如果想要支持传入0个参数,也可以写一个无参的ShowList函数。

    1. //支持无参调用
    2. void ShowList()
    3. {
    4. cout << endl;
    5. }
    6. //处理函数
    7. template<class T>
    8. void PrintArg(const T& val)
    9. {
    10. cout << typeid(T).name() << ":" << val << endl;
    11. }
    12. //展开函数
    13. template<class ...Args>
    14. void ShowList(Args... args)
    15. {
    16. int arr[] = { (PrintArg(args), 0)... }; //列表初始化+逗号表达式
    17. cout << endl;
    18. }

                            

    3.实际上我们也可以不用逗号表达式,因为这里的问题就是初始化整型数组时必须用整数,那我们可以将处理函数的返回值设置为整型,然后用这个返回值去初始化整型数组也是可以的 

    1. //支持无参调用
    2. void ShowList()
    3. {
    4. cout << endl;
    5. }
    6. //处理函数
    7. template<class T>
    8. int PrintArg(const T& val)
    9. {
    10. cout << typeid(T).name() << ":" << val << endl;
    11. return 0;
    12. }
    13. //展开函数
    14. template<class ...Args>
    15. void ShowList(Args... args)
    16. {
    17. int arr[] = { PrintArg(args)... }; //列表初始化+逗号表达式
    18. cout << endl;
    19. }

                    

                            

                            

     STL容器中的emplace相关接口函数

                    

    1.emplace版本的插入接口

    • C++11标准给STL中的容器增加emplace版本的插入接口,这些emplace版本的插入接口支持模板的可变参数
    • emplace系列接口的可变模板参数类型都带有“&&”,这个表示的是万能引用,而不是右值引用。

                    

     2.emplace系列接口的使用方式

    •  emplace系列接口的使用方式与容器原有的插入接口的使用方式类似,但又有一些不同之处

                             

    以list容器的emplace_back和push_back为例

    • 调用push_back函数插入元素时,可以传入左值对象或者右值对象,也可以使用列表进行初始化。
    • 调用emplace_back函数插入元素时,也可以传入左值对象或者右值对象,但不可以使用列表进行初始化。
    • 除此之外,emplace系列接口最大的特点就是,插入元素时可以传入用于构造元素的参数包。
    1. int main()
    2. {
    3. listint, string>> mylist;
    4. pair<int, string> kv(1, "A");
    5. mylist.push_back(kv); //传左值
    6. mylist.push_back(pair<int, string>(1, "A")); //传右值
    7. mylist.push_back({ 1, "A" }); //列表初始化
    8. mylist.emplace_back(kv); //传左值
    9. mylist.emplace_back(pair<int, string>(1, "A")); //传右值
    10. mylist.emplace_back(1, "A"); //传参数包
    11. return 0;
    12. }

                    

            

     3.emplace系列接口的工作流程

    • 先通过空间配置器为新结点获取一块内存空间,注意这里只会开辟空间,不会自动调用构造函数对这块空间进行初始化。
    • 然后调用allocator_traits::construct函数对这块空间进行初始化,调用该函数时会传入这块空间的地址和用户传入的参数(需要经过完美转发)。
    • 在allocator_traits::construct函数中会使用定位new表达式,显示调用构造函数对这块空间进行初始化,调用构造函数时会传入用户传入的参数(需要经过完美转发)。
    • 将初始化好的新结点插入到对应的数据结构当中,比如list容器就是将新结点插入到底层的双链表中。

                     

    4.emplace系列接口的意义

    (1)由于emplace系列接口的可变模板参数的类型都是万能引用,因此既可以接收左值对象,也可以接收右值对象,还可以接收参数包。

    • 如果调用emplace系列接口时传入的是左值对象,那么首先需要先在此之前调用构造函数实例化出一个左值对象,最终在使用定位new表达式调用构造函数对空间进行初始化时,会匹配到拷贝构造函数。
    • 如果调用emplace系列接口时传入的是右值对象,那么就需要在此之前调用构造函数实例化出一个右值对象,最终在使用定位new表达式调用构造函数对空间进行初始化时,就会匹配到移动构造函数。
    • 如果调用emplace系列接口时传入的是参数包,那就可以直接调用函数进行插入,并且最终在使用定位new表达式调用构造函数对空间进行初始化时,匹配到的是构造函数。

                     

    (2)小结

    • 传入左值对象,需要调用构造函数+拷贝构造函数。
    • 传入右值对象,需要调用构造函数+移动构造函数。
    • 传入参数包,只需要调用构造函数。

                             

    (3)emplace接口的意义

    • emplace系列接口最大的特点就是支持传入参数包,用这些参数包直接构造出对象,这样就能减少一次拷贝,这就是为什么有人说emplace系列接口更高效的原因。
    • 但emplace系列接口并不是在所有场景下都比原有的插入接口高效,如果传入的是左值对象或右值对象,那么emplace系列接口的效率其实和原有的插入接口的效率是一样的。
    • emplace系列接口真正高效的情况是传入参数包的时候,直接通过参数包构造出对象,避免了中途的一次拷贝。

                            

                     

    5.测试代码验证

    (1)验证上述对emplace系列接口的说法,需要借助一个深拷贝的类,模拟简化版的string类,类当中只编写了我们需要用到的成员函数。 

    • 通过在string的构造函数、拷贝构造函数和移动构造函数中的提示语句,因此我们可以通过控制台输出来判断这些函数是否被调用。
    1. namespace XM
    2. {
    3. class string
    4. {
    5. public:
    6. //构造函数
    7. string(const char* str = "")
    8. {
    9. cout << "string(const char* str) -- 构造函数" << endl;
    10. _size = strlen(str); //初始时,字符串大小设置为字符串长度
    11. _capacity = _size; //初始时,字符串容量设置为字符串长度
    12. _str = new char[_capacity + 1]; //为存储字符串开辟空间(多开一个用于存放'\0')
    13. strcpy(_str, str); //将C字符串拷贝到已开好的空间
    14. }
    15. //交换两个对象的数据
    16. void swap(string& s)
    17. {
    18. //调用库里的swap
    19. ::swap(_str, s._str); //交换两个对象的C字符串
    20. ::swap(_size, s._size); //交换两个对象的大小
    21. ::swap(_capacity, s._capacity); //交换两个对象的容量
    22. }
    23. //拷贝构造函数(现代写法)
    24. string(const string& s)
    25. :_str(nullptr)
    26. , _size(0)
    27. , _capacity(0)
    28. {
    29. cout << "string(const string& s) -- 拷贝构造" << endl;
    30. string tmp(s._str); //调用构造函数,构造出一个C字符串为s._str的对象
    31. swap(tmp); //交换这两个对象
    32. }
    33. //移动构造
    34. string(string&& s)
    35. :_str(nullptr)
    36. , _size(0)
    37. , _capacity(0)
    38. {
    39. cout << "string(string&& s) -- 移动构造" << endl;
    40. swap(s);
    41. }
    42. //拷贝赋值函数(现代写法)
    43. string& operator=(const string& s)
    44. {
    45. cout << "string& operator=(const string& s) -- 深拷贝" << endl;
    46. string tmp(s); //用s拷贝构造出对象tmp
    47. swap(tmp); //交换这两个对象
    48. return *this; //返回左值(支持连续赋值)
    49. }
    50. //移动赋值
    51. string& operator=(string&& s)
    52. {
    53. cout << "string& operator=(string&& s) -- 移动赋值" << endl;
    54. swap(s);
    55. return *this;
    56. }
    57. //析构函数
    58. ~string()
    59. {
    60. //delete[] _str; //释放_str指向的空间
    61. _str = nullptr; //及时置空,防止非法访问
    62. _size = 0; //大小置0
    63. _capacity = 0; //容量置0
    64. }
    65. private:
    66. char* _str;
    67. size_t _size;
    68. size_t _capacity;
    69. };
    70. }

                     

    (2)用一个容器来存储模拟实现的string,并以不同的传参形式调用emplace系列函数

    1. int main()
    2. {
    3. listint, XM::string>> mylist;
    4. pair<int, XM::string> kv(1, "A");
    5. mylist.emplace_back(kv); //传左值
    6. cout << endl;
    7. mylist.emplace_back(pair<int, XM::string>(1, "A")); //传右值
    8. cout << endl;
    9. mylist.emplace_back(1, "A"); //传参数包
    10. return 0;
    11. }

                             

    • 模拟实现string的拷贝构造函数时复用了构造函数,因此在调用string拷贝构造的后面会紧跟着调用一次构造函数。
    • 为了更好的体现出参数包的概念,因此这里list容器中存储的元素类型是pair,我们是通过观察string对象的处理过程来判断pair的处理过程的。

                     

    (3)以不同的传参方式调用push_back函数,顺便验证一下容器原有的插入函数的执行逻辑

    1. int main()
    2. {
    3. listint, XM::string>> mylist;
    4. pair<int, XM::string> kv(1, "A");
    5. mylist.push_back(kv); //传左值
    6. cout << endl;
    7. mylist.push_back(pair<int, XM::string>(1, "A")); //传右值
    8. cout << endl;
    9. mylist.push_back({ 1, "A" }); //列表初始化 (构造匿名对象)
    10. return 0;
    11. }

                            

  • 相关阅读:
    语义分割笔记(二):DeepLab V3对图像进行分割(自定义数据集从零到一进行训练、验证和测试)
    pyspark.sql.dataframe.DataFrame 怎么转pandas DataFrame
    当创建一个ingress后,kubernetes会发什么?
    实战经验分享FastAPI 是什么
    画图带你彻底弄懂三级缓存和循环依赖的问题
    我的创作纪念日的温柔与七夕的浪漫交织了在一起
    axios 源码简析
    微服务全链路灰度新能力
    【MATLAB教程案例41】语音信号的语谱图matlab仿真与应用分析
    【Axure教程】中继器联动——二级下拉列表案例
  • 原文地址:https://blog.csdn.net/m0_52169086/article/details/126924715