目录
①在C++11之前,一个类中有如下六个默认成员函数:
②其中前四个成员函数最重要,后面两个成员函数一般不会用到,这里“默认”的意思就是你不写编译器会自动生成。在C++11标准中又增加了两个默认成员函数,分别是移动构造和移动赋值重载函数。
①C++11中新增的移动构造函数和移动赋值函数的生成条件如下:
②移动构造和移动赋值的生成条件与之前六个默认成员函数不同,并不是单纯的没有实现移动构造和移动赋值编译器就会默认生成。
③ 如果我们自己实现了移动构造或者移动赋值,就算没有实现拷贝构造和拷贝赋值,编译器也不会生成默认的拷贝构造和拷贝赋值。
①模拟实现一个简化版的string类
- namespace XM
- {
- class string
- {
- public:
- //构造函数
- string(const char* str = "")
- {
- _size = strlen(str); //初始时,字符串大小设置为字符串长度
- _capacity = _size; //初始时,字符串容量设置为字符串长度
- _str = new char[_capacity + 1]; //为存储字符串开辟空间(多开一个用于存放'\0')
- strcpy(_str, str); //将C字符串拷贝到已开好的空间
- }
-
- //交换两个对象的数据
- void swap(string& s)
- {
- //调用库里的swap
- ::swap(_str, s._str); //交换两个对象的C字符串
- ::swap(_size, s._size); //交换两个对象的大小
- ::swap(_capacity, s._capacity); //交换两个对象的容量
- }
-
- //拷贝构造函数(现代写法)
- string(const string& s)
- :_str(nullptr)
- , _size(0)
- , _capacity(0)
- {
- cout << "string(const string& s) -- 深拷贝" << endl;
-
- string tmp(s._str); //调用构造函数,构造出一个C字符串为s._str的对象
- swap(tmp); //交换这两个对象
- }
-
- //移动构造
- string(string&& s)
- :_str(nullptr)
- , _size(0)
- , _capacity(0)
- {
- cout << "string(string&& s) -- 移动构造" << endl;
- swap(s);
- }
-
- //拷贝赋值函数(现代写法)
- string& operator=(const string& s)
- {
- cout << "string& operator=(const string& s) -- 深拷贝" << endl;
-
- string tmp(s); //用s拷贝构造出对象tmp
- swap(tmp); //交换这两个对象
- return *this; //返回左值(支持连续赋值)
- }
-
- //移动赋值
- string& operator=(string&& s)
- {
- cout << "string& operator=(string&& s) -- 移动赋值" << endl;
- swap(s);
- return *this;
- }
-
- //析构函数
- ~string()
- {
- //delete[] _str; //释放_str指向的空间
- _str = nullptr; //及时置空,防止非法访问
- _size = 0; //大小置0
- _capacity = 0; //容量置0
- }
- private:
- char* _str;
- size_t _size;
- size_t _capacity;
- };
- }
②简单的Person类,Person类中的成员name的类型就是我们模拟实现的string类。
- class Person
- {
- public:
- //构造函数
- Person(const char* name = "", int age = 0)
- :_name(name)
- , _age(age)
- {}
-
- //拷贝构造函数
- Person(const Person& p)
- :_name(p._name)
- , _age(p._age)
- {}
-
- //拷贝赋值函数
- Person& operator=(const Person& p)
- {
- if (this != &p)
- {
- _name = p._name;
- _age = p._age;
- }
- return *this;
- }
-
- //析构函数
- ~Person()
- {}
- private:
- XM::string _name; //姓名
- int _age; //年龄
- };
③Person类当中没有实现移动构造和移动赋值,但拷贝构造、拷贝赋值和析构函数Person类都实现了,因此Person类中不会生成默认的移动构造和移动赋值
- int main()
- {
- Person s1("张三", 100);
- Person s2 = std::move(s1); //想要调用Person默认生成的移动构造
-
- return 0;
- }
④ 验证Person类中默认生成的移动赋值函数
- int main()
- {
- Person s1("张三", 100);
- Person s2;
- s2 = std::move(s1); //想要调用Person默认生成的移动赋值
-
- return 0;
- }
⑤补充
(1)默认生成的构造函数,对于自定义类型的成员会调用其构造函数进行初始化,但并不会对内置类型的成员进行处理。于是C++11支持非静态成员变量在声明时进行初始化赋值,默认生成的构造函数会使用这些缺省值对成员进行初始化。
- class Person
- {
- public:
- //...
- private:
- //非静态成员变量,可以在成员声明时给缺省值
- XM::string _name = "张三"; //姓名
- int _age = 20; //年龄
- static int _n; //静态成员变量不能给缺省值
- };
(2)补充
①给变量一个缺省值,这个缺省值是一个声明,只有初始化列表没有初始化就会用这个缺省值
②类的成员函数此时没有空间,这里只是声明,没有空间
③类编译出来都是指令,没有空间。什么时候有空间? 类定义出对象的时候有空间
④Linux 写好的程序是一个文件,存在磁盘上面的 ; 编译器对我们写好的代码进行编译,没问题生成汇编,汇编再翻译成机器码,最后出来的就是二进制的指令,如何执行这些指令?
⑤替换a.out中的代码和数据,子进程开始执行main函数,建立各种栈帧,编译器提前算好各种对象需要多少空间,s1需要多少空间,s2需要多少空间,提前就开辟好了
⑥s1,s2在这个栈帧里面把空间分配好,但是main函数结束这个对象就销毁了,因为对象是在栈帧里面的,如果是全局变量为什么不会销毁呢?
⑦编译器怎么知道对象有多大 ?
①Person类中实现了拷贝构造函数
- class Person
- {
- public:
- //拷贝构造函数
- Person(const Person& p)
- :_name(p._name)
- , _age(p._age)
- {}
- private:
- XM::string _name; //姓名
- int _age; //年龄
- };
②如下代码就无法编译成功,因为Person类中编写了拷贝构造函数,导致无法生成默认的构造函数,因为默认构造函数生成的条件是没有编写任意类型的构造函数,包括拷贝构造函数。
- int main()
- {
- Person s; //没有合适的默认构造函数可用
-
- return 0;
- }
③我们就可以使用default关键字强制生成默认的构造函数
- class Person
- {
- public:
- Person() = default; //强制生成默认构造函数
-
- //拷贝构造函数
- Person(const Person& p)
- :_name(p._name)
- , _age(p._age)
- {}
- private:
- cl::string _name; //姓名
- int _age; //年龄
- };
(1)想要限制某些默认函数生成时,可以通过如下两种方式:
(2)要让一个类不能被拷贝,可以用=delete修饰将该类的拷贝构造和拷贝赋值。
- class CopyBan
- {
- public:
- CopyBan()
- {}
- private:
- CopyBan(const CopyBan&) = delete;
- CopyBan& operator=(const CopyBan&) = delete;
- };
(1)final修饰类
- class NonInherit final //被final修饰,该类不能再被继承
- {
- //...
- };
(2)final修饰虚函数
- //父类
- class Person
- {
- public:
- virtual void Print() final //被final修饰,该虚函数不能再被重写
- {
- cout << "hello Person" << endl;
- }
- };
-
- //子类
- class Student : public Person
- {
- public:
- virtual void Print() //重写,编译报错
- {
- cout << "hello Student" << endl;
- }
- };
(3)override修饰虚函数
- //父类
- class Person
- {
- public:
- virtual void Print()
- {
- cout << "hello Person" << endl;
- }
- };
-
- //子类
- class Student : public Person
- {
- public:
- virtual void Print() override //检查子类是否重写了父类的某个虚函数
- {
- cout << "hello Student" << endl;
- }
- };
可变参数模板是C++11新增的最强大的特性之一,它对参数高度泛化,能够让我们创建可以接受可变参数的函数模板和类模板。
- template<class …Args>
- 返回类型 函数名(Args… args)
- {
- //函数体
- }
- template<class ...Args>
- void ShowList(Args... args)
- {}
①调用ShowList函数时就可以传入任意多个参数了,并且这些参数可以是不同类型的
- int main()
- {
- ShowList();
- ShowList(1);
- ShowList(1, 'A');
- ShowList(1, 'A', string("MIUI"));
- return 0;
- }
②在函数模板中通过sizeof计算参数包中参数的个数
- template<class ...Args>
- void ShowList(Args... args)
- {
- cout << sizeof...(Args) << endl; //获取参数包中参数的个数
- cout << sizeof...(args) << endl << endl;
- }
我们无法直接获取参数包中的每个参数,只能通过展开参数包的方式来获取,这是使用可变参数模板的一个主要特点,也是最大的难点。
- template<class ...Args>
- void ShowList(Args... args)
- {
- //错误示例:
- for (int i = 0; i < sizeof...(args); i++)
- {
- cout << args[i] << " "; //打印参数包中的每个参数
- }
- cout << endl;
- }
①递归展开参数包的方式如下:
②打印调用函数时传入的各个参数,这样编写函数模板
- //展开函数
- //解析并打印参数包中的每个参数类型及值
- template<class T, class ...Args>
- void ShowList(T val, Args... args)
- {
- cout << typeid(val).name() << ":" << val << endl; //打印分离出的第一个参数
- ShowList(args...); //递归调用,将参数包继续向下传
- }
③现在面临的问题是,如何终止函数的递归调用
- //递归终止函数
- //可以认为是重载版本,当参数包里面只有一个参数的时候就会调用这个重载函数
- template<class T>
- void ShowList(const T& val)
- {
- cout << val << endl << endl;
- }
-
- //展开函数
- template<class T, class ...Args>
- void ShowList(T value, Args... args)
- {
- cout << typeid(val).name() << ":" << val << endl; //打印分离出的第一个参数
- ShowList(args...); //递归调用,将参数包继续向下传
- }
④修改代码,只能调用可变参数模板的修改
- //递归终止函数
- template<class T>
- void ShowList(const T& val)
- {
- cout << val << endl << endl;
- }
-
- //展开函数
- template<class T, class ...Args>
- void ShowList(T value, Args... args)
- {
- cout << typeid(val).name() << ":" << val << endl;
- ShowList(args...);
- }
-
- //供外部调用的函数
- template<class ...Args>
- void ShowListArg(Args... args)
- {
- ShowList(args...);
- }
⑤关于递归终止函数也可以这样写
- //递归终止函数
- void ShowList()
- {
- cout << endl;
- }
-
- //展开函数
- template<class T, class ...Args>
- void ShowList(T value, Args... args)
- {
- cout << typeid(val).name() << ":" << val << endl;
- ShowList(args...);
- }
⑥传入参数测试
①既然我们可以通过sizeof计算出参数包中参数的个数,那我们能不能在ShowList函数中设置一个判断,当参数包中参数个数为0时就终止递归呢
- //错误示例
- template<class T, class ...Args>
- void ShowList(T value, Args... args)
- {
- cout << value << " "; //打印传入的若干参数中的第一个参数
- if (sizeof...(args) == 0)
- {
- return;
- }
-
- ShowList(args...); //将剩下参数继续向下传
- }
②这种方式是不可行的
①通过列表获取参数包中的参数
int a[] = {1,2,3,4};
- //展开函数
- template<class ...Args>
- void ShowList(Args... args)
- {
- int arr[] = { args... }; //列表初始化
-
- //打印参数包中的各个参数
- for (auto e : arr)
- {
- cout << e << " ";
- }
- cout << endl;
- }
- int main()
- {
- ShowList(1);
- ShowList(1, 2);
- ShowList(1, 2, 3);
- return 0;
- }
②通过逗号表达式展开参数包
1.在执行逗号表达式时就会先调用处理函数处理对应的参数,然后再将逗号表达式中的最后一个整型值作为返回值来初始化整型数组。
- //处理参数包中的每个参数
- template<class T>
- void PrintArg(const T& val)
- {
- cout << typeid(T).name() << ":" << val << endl;
- }
-
- //展开函数
- template<class ...Args>
- void ShowList(Args... args)
- {
- int arr[] = { (PrintArg(args), 0)... }; //列表初始化+逗号表达式
- cout << endl;
- }
2.这时调用ShowList函数时就可以传入多个不同类型的参数了,但调用时仍然不能传入0个参数,因为数组的大小不能为0,如果想要支持传入0个参数,也可以写一个无参的ShowList函数。
- //支持无参调用
- void ShowList()
- {
- cout << endl;
- }
-
- //处理函数
- template<class T>
- void PrintArg(const T& val)
- {
- cout << typeid(T).name() << ":" << val << endl;
- }
-
- //展开函数
- template<class ...Args>
- void ShowList(Args... args)
- {
- int arr[] = { (PrintArg(args), 0)... }; //列表初始化+逗号表达式
- cout << endl;
- }
3.实际上我们也可以不用逗号表达式,因为这里的问题就是初始化整型数组时必须用整数,那我们可以将处理函数的返回值设置为整型,然后用这个返回值去初始化整型数组也是可以的
- //支持无参调用
- void ShowList()
- {
- cout << endl;
- }
-
- //处理函数
- template<class T>
- int PrintArg(const T& val)
- {
- cout << typeid(T).name() << ":" << val << endl;
-
- return 0;
- }
-
- //展开函数
- template<class ...Args>
- void ShowList(Args... args)
- {
- int arr[] = { PrintArg(args)... }; //列表初始化+逗号表达式
- cout << endl;
- }
以list容器的emplace_back和push_back为例
- int main()
- {
- list
int, string>> mylist; - pair<int, string> kv(1, "A");
- mylist.push_back(kv); //传左值
- mylist.push_back(pair<int, string>(1, "A")); //传右值
- mylist.push_back({ 1, "A" }); //列表初始化
-
- mylist.emplace_back(kv); //传左值
- mylist.emplace_back(pair<int, string>(1, "A")); //传右值
- mylist.emplace_back(1, "A"); //传参数包
- return 0;
- }
(1)由于emplace系列接口的可变模板参数的类型都是万能引用,因此既可以接收左值对象,也可以接收右值对象,还可以接收参数包。
(2)小结
(3)emplace接口的意义
(1)验证上述对emplace系列接口的说法,需要借助一个深拷贝的类,模拟简化版的string类,类当中只编写了我们需要用到的成员函数。
- namespace XM
- {
- class string
- {
- public:
- //构造函数
- string(const char* str = "")
- {
- cout << "string(const char* str) -- 构造函数" << endl;
-
- _size = strlen(str); //初始时,字符串大小设置为字符串长度
- _capacity = _size; //初始时,字符串容量设置为字符串长度
- _str = new char[_capacity + 1]; //为存储字符串开辟空间(多开一个用于存放'\0')
- strcpy(_str, str); //将C字符串拷贝到已开好的空间
- }
-
- //交换两个对象的数据
- void swap(string& s)
- {
- //调用库里的swap
- ::swap(_str, s._str); //交换两个对象的C字符串
- ::swap(_size, s._size); //交换两个对象的大小
- ::swap(_capacity, s._capacity); //交换两个对象的容量
- }
-
- //拷贝构造函数(现代写法)
- string(const string& s)
- :_str(nullptr)
- , _size(0)
- , _capacity(0)
- {
- cout << "string(const string& s) -- 拷贝构造" << endl;
-
- string tmp(s._str); //调用构造函数,构造出一个C字符串为s._str的对象
- swap(tmp); //交换这两个对象
- }
-
- //移动构造
- string(string&& s)
- :_str(nullptr)
- , _size(0)
- , _capacity(0)
- {
- cout << "string(string&& s) -- 移动构造" << endl;
- swap(s);
- }
-
- //拷贝赋值函数(现代写法)
- string& operator=(const string& s)
- {
- cout << "string& operator=(const string& s) -- 深拷贝" << endl;
-
- string tmp(s); //用s拷贝构造出对象tmp
- swap(tmp); //交换这两个对象
- return *this; //返回左值(支持连续赋值)
- }
-
- //移动赋值
- string& operator=(string&& s)
- {
- cout << "string& operator=(string&& s) -- 移动赋值" << endl;
- swap(s);
- return *this;
- }
-
- //析构函数
- ~string()
- {
- //delete[] _str; //释放_str指向的空间
- _str = nullptr; //及时置空,防止非法访问
- _size = 0; //大小置0
- _capacity = 0; //容量置0
- }
- private:
- char* _str;
- size_t _size;
- size_t _capacity;
- };
- }
(2)用一个容器来存储模拟实现的string,并以不同的传参形式调用emplace系列函数
- int main()
- {
- list
int, XM::string>> mylist; -
- pair<int, XM::string> kv(1, "A");
- mylist.emplace_back(kv); //传左值
- cout << endl;
-
- mylist.emplace_back(pair<int, XM::string>(1, "A")); //传右值
- cout << endl;
-
- mylist.emplace_back(1, "A"); //传参数包
- return 0;
- }
(3)以不同的传参方式调用push_back函数,顺便验证一下容器原有的插入函数的执行逻辑
- int main()
- {
- list
int, XM::string>> mylist; -
- pair<int, XM::string> kv(1, "A");
- mylist.push_back(kv); //传左值
- cout << endl;
-
- mylist.push_back(pair<int, XM::string>(1, "A")); //传右值
- cout << endl;
-
- mylist.push_back({ 1, "A" }); //列表初始化 (构造匿名对象)
- return 0;
- }