七 在创建对象时注意区分()和{}
C++11中多了一种初始化的方式,就是通过{}来进行初始化,例如初始化一个int类型
int x(0);
int y = 0;
int z{
0};
int z = {
0};
使用大括号初始化容器非常方便:
vector<int> vec{
1,2,3};
大括号同样可以用来初始化类内非静态成员变量,当然也可以用=初始化,但不可以用()
class A
{
private:
int x{
0};
int y = 10;
//int z(2); //不可以这样初始化
};
大括号是通用的,可以用在很多场合,所以尽量使用大括号进行初始化。
大括号的特性之一是禁止进行内建型别之间的向下类型转换,如果大括号内部的表达式无法保证能采用进行初始化的对象来表达,则代码不能通过编译。但是使用用()是可以初始化的。
double x = 1.0, y = 1.0, z = 1.0;
int sum1{x + y + z};
int sum2(x + y + z); //double被向下转换为int
VS2017下无法通过编译:
1>error C2397: 从“double”转换到“int”需要收缩转换
小括号进行类对象的初始化时,如果没有参数传入是不能写括号的(写括号就变成声明一个函数了),但是此时可以写大括号进行初始化。
A a1(3);
A a2();
A a3{};
cout << typeid(a1).name() << endl; //class A
cout << typeid(a2).name() << endl; //class A __cdecl(void)
cout << typeid(a3).name() << endl; //class A
输出已用注释方式写出。很明显这里用大括号进行初始化是正确无误的。
大括号如果初始化auto对象的话,auto的类型会被推导为std::initializer_list
。
std::initializer_list
提供的操作如下:
initializer_list<T> lst;
//默认初始化;T类型元素的空列表
initializer_list<T> lst{a,b,c...};//initializer_list对象中的元素是常量值,无法被更改 。
lst2(lst)
lst2=lst
//拷贝或赋值一个initializer_list对象不会拷贝列表中的元素;拷贝后,原始列表和副本元素共享
lst.size() //列表中的元素数量
lst.begin() //返回指向lst中首元素的指针
lst.end() //返回指向lst中尾元素下一位置的指针
//也可以用迭代器来访问lst
类的构造函数可以声明具备initializer_list<T>
型别的形参,那么采用大括号初始化语法的调用语句会优先调用这个重载版本。编译器只要有任何可能把一个采用了大括号初始化语法的调用语句解读为带有initializer_list<T>
型别形参的构造函数,则编译器就是采用这种解释。
class A
{
public:
A(int _x, bool _y) :x(_x), y(_y) { cout << "int and bool" << endl; }
A(int _x, double _z) :x(_x), z(_z) { cout << "int and double" << endl; }
//A(initializer_list<long double> ld) { cout << "initializer_list" << endl; }
private:
int x{
0};
bool y = false;
double z = 1.0;
};
int main()
{
A a1(3, true);
A a2{ 3, true };
A a3(3, 1.5);
A a4{ 3, 1.5 };
return 0;
}
在没有initializer_list<T>
型别形参的构造函数之前,输出如下:
int and bool
int and bool
int and double
int and double
将注释去掉,输出就变成了:
int and bool
initializer_list //发生了向上的类型转换
int and double
initializer_list //发生了向上的类型转换
只有找不到任何办法把大括号初始化物中的实参转换成initializer_list<T>
模板中的型别的时候,编译器才会退而去检查普通的重载决议。
class A
{
public:
A(int _x, bool _y) :x(_x), y(_y) { cout << "int and bool" << endl; }
A(int _x, double _z) :x(_x), z(_z) { cout << "int and double" << endl; }
//现在参数是string了
A(initializer_list<string> ld) { cout << "initializer_list" << endl; }
private:
int x{
0};
bool y = false;
double z = 1.0;
};
int main()
{
A a1(3, true);
A a2{ 3, true };
A a3(3, 1.5);
A a4{ 3, 1.5 };
return 0;
}
这时int,bool都无法转换为string类型,所以最终输出为:
int and bool
int and bool
int and double
int and double
空的大括号代表的意思是没有实参,而不是空的initializer_list
,所以当一个类同时拥有默认构造函数和initializer_list<T>
构造函数的时候,传入空的大括号A a{};
调用的是默认构造函数。(不能写成A a();
,这样是声明函数)。如果这时候想调用initializer_list<T>
构造函数并且传入空的initializer_list
的时候,则可以再大括号外面套一层小括号或者大括号:
A a({}); //调用initializer_list<T>构造函数并且传入空的initializer_list
A a{
{}}; //同上
补充一个vector的对象定义,使用大括号和小括号定义会有区别:
vector<int> v(10, 20); //创建一个包含10个20的vector
vector<int> v{
10, 20}; //创建一个包含2个变量,分别为10,20的vector
如果想用任意数量的实参来创建一个任意型别的对象,那么最好使用可变参数模板。
可变参数模板
变长参数的模板声明:
template<typename... E> class tuple;
标识符E前面的省略号表示了该参数是变长的,C++11中,E被称作是一个模板参数包,有了这个参数包,就能接受任意多个参数作为模板参数。
同时模板参数包也可以不是模板类型:
template<int... A>
class As{
};
//定义对象
As<1, 0, 2> as;
模板参数包再模板推导的时候会被认为是模板的单个参数(虽然实际上是任意个数量的实参的集合),为了使用模板参数包要对其进行解包,C++11中通常使用一个名为包扩展的表达式完成。
包扩展就是把把类型后面加一个省略号,例子如下:
template<typename T1, typename T2>
class B
{
public:
B(int _x, int _y) : x(_x), y(_y) { cout << x << ' ' << y << endl; }
virtual ~B() {}
private:
int x;
int y;
};
template<typename... T>
class A : private B<T...> //<T...>是包扩展
{
public:
A(int a = 0, int b = 1):B<T...>(a, b){ }
};
int main()
{
A<int, int> a;
return 0;
}
这里A将模板参数包解包并传递给私有基类B。
但是这里如果A后面跟着多个类型,就无法通过编译,因为这里的B仅仅要求两个类型,如何实现任意参数类型的模板参数包呢?可以使用递归+特化版本的方式。
template<typename... T>
class Mytuple{ };
template<typename H, typename... T>
class Mytuple<H, T...> : private Mytuple<T...>
{
public:
Mytuple() { cout << typeid(head).name() << endl; }
private:
H head;
};
template<>
class Mytuple<> {}; //特化版本,边界条件
int main()
{
Mytuple<int, double, char, float> mt;
return 0;
}
上述代码中,先是定义了一个只有一个模板参数包的模板类Mytuple,紧接着定义了偏特化版本,包含一个模板参数和一个模板参数包的模板类,这个类以template<typename... T> class Mytuple
为私有基类。这样,当我们定义Mytuple<int, double, char, float> mt;
时,将会引起递归构造,由于特化版本和最优匹配版本具有最高优先权,所以首先会匹配template<typename H, typename... T> class Mytuple
,在这里会递归构造基类并传入类型包Mytuple<T...>
,这里的类型包已经不包括int类型,因为int类型已经给了head变量,然后是一样的偏特化匹配,递归直到最后匹配template<> class Mytuple<>
,再逐渐弹栈进行构造。所以构造的顺序是Mytuple<>
->Mytuple<float>
->Mytuple<char, float>
->Mytuple<double, char, float>
->Mytuple<int, double, char, float>
。所以最终输出为:
float
char
double
int
同样的,可以使用可变模板参数进行递归计算,这样可以把运行期的计算转移到编译期来。(这里就是非类型的模板参数包)
#include <array> //用来检测是否为编译期的常量值
template<long long... nums> //声明这个类是一个可变参数模板类
struct Multiply;
template <long first, long... last> //偏特化版本
struct Multiply<first, last...>
{
static const long long val = first * Multiply<last...>::val; //递归求乘数值
};
template <> //特化版本
struct Multiply<>
{
static const long long val = 1; //初始值为1
};
int main()
{
cout << Multiply<2, 3, 4, 5>::val << endl; //编译期就能确定的常量值
array<int, Multiply<2, 3, 4, 5>::val> a; //std::array传入的长度值必须是编译期确定的常量
return 0;
}
补充一个使用逗号表达式展开变长参数模板的例子(我对其不是非常理解)
#include <iostream>
using namespace std;
template<typename F, typename ...Args>
void expand(const F &f, Args&&... args)
{
//这里采用了逗号表达式+大括号初始化列表,完美转发一个变长参数模板,展开的同时,通过逗号表达式将0赋值到initializer_list中
initializer_list<int>{ (f(forward<Args>(args)), 0)... };
}
int main()
{
expand([](auto i) { cout << i << endl; }, 1,2,3,"test");
return 0;
}
除了变长模板类,C++11还可以声明变长模板函数,C++11额外的要求是模板函数的模板参数包必须唯一,并且是函数的最后一个参数。
变长模板函数的两个例子:
变长模板函数实现printf打印:
#include <exception> #include <string> void myprint(const char *s) { while (*s) {