0.上篇文章
1.新的类功能
默认成员函数
原来C++类中,有6个默认成员函数:
1. 构造函数
2. 析构函数
3. 拷贝构造函数
4. 拷贝赋值重载
5. 取地址重载
6. const 取地址重载
最后重要的是前4个(属于深拷贝,后两个用处不大。默认成员函数就是我们不写编译器会生成一个默认的。
C++11 新增了两个:移动构造函数和移动赋值运算符重载。(因为有了右值引用,针对的是深拷贝的自定义类型对象:如string,vector,list等等,属于移动拷贝)
针对移动构造函数和移动赋值运算符重载有一些需要注意的点如下:
- 如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
- 如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)。
- 如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。、、
class Person { public: Person(const char* name = "张三", int age = 18) :_name(name) , _age(age) {} private: mystring::string _name; int _age; }; int main() { Person s1; Person s2 = s1; // 默认拷贝构造 Person s3 = std::move(s1); // 默认移动构造 return 0; }
像s3,该类并没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。对于自定义类型s3,编译器会优先调用该成员的移动构造。
(如果写了其中任何一个,编译器都不会生成默认的移动构造,而是调用拷贝构造,这是语法规定)
s1的值被拿走了确实是调用了移动构造。
当然也会有默认的移动赋值
int main() { Person s1; Person s2 = s1; // 默认拷贝构造 Person s3 = std::move(s1); // 默认移动构造 Person s4; s4 = std::move(s2); // 默认移动赋值 return 0; }
2.强制生成默认函数的关键字default
C++11可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原因这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用default关键字显示指定移动构造生成。
以下四个有一个需要强制生成,就都强制生成,否则互相会牵扯
class Person { public: Person(const char* name = "张三", int age = 18) :_name(name) , _age(age) {} // 强制生成 Person(const Person& p) = default; Person& operator=(const Person & p) = default; Person(Person&& p) = default; Person& operator=(Person&& p) = default; ~Person() {} private: mystring::string _name; int _age; }; int main() { Person s1; Person s2 = s1; // 默认拷贝构造 Person s3 = std::move(s1); // 默认移动构造 Person s4; s4 = std::move(s2); // 默认移动赋值 return 0; }
3.禁止生成默认函数的关键字delete
如果能想要限制某些默认函数的生成,在C++98中,是该函数设置成private,并且只声明补丁已,这样只要其他人想要调用就会报错。在C++11中更简单,只需在该函数声明加上=delete即可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。
eg:只允许在堆上申请空间,不能在栈上。此时我们c++11的方法就可以直接将拷贝构造给delete掉,此时该类就不能使用默认的拷贝构造了。只能用类的规则去堆申请空间,而不能使用编译器默认生成的拷贝构造了。
class HeapOnly { public: static HeapOnly* CreateObj() { return new HeapOnly; } // C++11 HeapOnly(const HeapOnly&) = delete; // C++98 私有+只声明不实现 private: //HeapOnly(const HeapOnly&); HeapOnly() {} int _a = 1; }; int main() { HeapOnly* p1= HeapOnly::CreateObj(); HeapOnly p2(*p1);//不能使用拷贝构造了 return 0; }
4.可变参数模板
C++11的新特性可变参数模板能够让您创建可以接受可变参数的函数模板和类模板,相比C++98/03,类模版和函数模版中只能含固定数量的模版参数,可变模版参数无疑是一个巨大的改进。然而由于可变模版参数比较抽象,使用起来需要一定的技巧,所以这块还是比较晦涩的。我们掌握一些基础的可变参数模板特性就够我们用了。
下面就是一个基本可变参数的函数模板:
// Args是一个模板参数包,args是一个函数形参参数包 // 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。 template <class ...Args> void ShowList(Args... args) {}
上面的参数args前面有省略号,所以它就是一个可变模版参数,我们把带省略号的参数称为“参数包”,它里面包含了0到N(N>=0)个模版参数。我们无法直接获取参数包args中的每个参数的,只能通过展开参数包的方式来获取参数包中的每个参数,这是使用可变模版参数的一个主要特点,也是最大的难点,即如何展开可变模版参数。由于语法不支持使用args[i]这样方式获取可变参数,所以我们的用一些奇招来一一获取参数包的值
递归函数方式展开参数包:
使用递归的方式调用,当参数大于1时调用的是有参数的函数,当参数为0时调用的则是重载的没有参数的函数,意味着递归终止。
//递归终止函数 void _Cpp_Printf() { cout << endl; } // 编译时,参数推到递归 template <class T, class ...Args> void _Cpp_Printf(const T& val, Args... args) { cout << val << endl; _Cpp_Printf(args...); } int main() { _Cpp_Printf(1.1); _Cpp_Printf(1.1, 'x'); _Cpp_Printf(1, 'A', std::string("sort")); return 0; }
逗号表达式展开参数包
这种展开参数包的方式,不需要通过递归终止函数,是直接在expand函数体中展开的,printarg不是一个递归终止函数,只是一个处理参数包中每一个参数的函数。这种就地展开参数包的方式实现的关键是逗号表达式。我们知道逗号表达式会按顺序执行逗号前面的表达式。
expand函数中的逗号表达式:(printarg(args), 0),也是按照这个执行顺序,先执行printarg(args),再得到逗号表达式的结果0。同时还用到了C++11的另外一个特性——初始化列表,通过初始化列表来初始化一个变长数组, {(printarg(args), 0)...}将会展开成((printarg(arg1),0),(printarg(arg2),0), (printarg(arg3),0), etc... ),最终会创建一个元素值都为0的数组int arr[sizeof...(Args)]。由于是逗号表达式,在创建数组的过程中会先执行逗号表达式前面的部分printarg(args)打印出参数,也就是说在构造int数组的过程中就将参数包展开了,这个数组的目的纯粹是为了在数组构造的过程展开参数包template <class T> int PrintArg(T t) { cout << t << " "; return 0; } template <class ...Args> void Cpp_Printf(Args... args) { // 编译时推导,args...参数有几个值,PrintArg就调用几次,就有几个返回值,arr就开多大 int arr[] = { PrintArg(args)... }; cout << endl; } int main() { Cpp_Printf(1, 'A', std::string("sort")); return 0; }
STL容器中的empalce相关接口函数:
template <class... Args> void emplace_back (Args&&... args);
首先我们看到的emplace系列的接口,支持模板的可变参数,并且万能引用。那么相对insert和emplace系列接口的优势到底在哪里呢?
emplace_back支持可变参数,拿到构建pair对象的参数后自己去创建对象
那么在这里我们可以看到除了用法上,和push_back没什么太大的区别int main() { std::list< std::pair<int, char> > mylist; mylist.emplace_back(10, 'a'); mylist.emplace_back(20, 'b'); mylist.emplace_back(make_pair(30, 'c')); mylist.push_back(make_pair(40, 'd')); mylist.push_back({ 50, 'e' }); for (auto e : mylist) cout << e.first << ":" << e.second << endl; return 0; }
下面我们试一下带有拷贝构造和移动构造的bit::string:
我们发现直接传pair,无论是emplace_back还是push_back没什么区别,都是构造加移动构造,但是emplace_back是支持,直接传构造pair的参数包的,底层则是直接构造,移动构造消耗也不大,两者相差不大。但对于浅拷贝类型来说差别就大了,因为浅拷贝类型只有拷贝构造,没有移动构造,这时候差距就出来了
int main() { // 没区别 list<pair<mystring::string, int>> lt; pair<mystring::string, int> kv1("xxxxx", 1); lt.push_back(move(kv1)); cout << endl; // 直接传pair的对象效果跟push_back系列是一样的 pair<mystring::string, int> kv2("xxxxx", 1); lt.emplace_back(move(kv2)); cout << endl; // 直接传构造pair的参数包,参数包一直往下传,底层直接构造 lt.emplace_back("xxxxx", 1); return 0; }