C++11:声明 & 初始化
- 初始化
- { }初始化
- initializer_list
- 声明
- auto
- decltype
- nullptr
初始化
{ }初始化
在C++98中,允许使用花括号{ }
对数组或者结构体元素进行统一的列表初始化。
用{ }
初始化数组:
int arr[] = { 1, 2, 3, 4, 5 };
用{ }
初始化结构体:
struct stu
{char name[20];int age;
};int main()
{stu s1 = { "Jones", 18 };return 0;
}
C++11扩大了
{ }
的适用范围,其可以用于所有的内置类型和自定义类型的初始化
C++11希望通过这个语法,使得所有变量可以以一种统一的方式进行初始化
比如对于内置类型:
int i = { 1 };
double d = { 3.14 };
char c = { 'x' };int* pf = { nullptr };
int& ri = { i };
我们可以通过大括号初始化整型,浮点型,指针,引用等等。在使用{ }
进行初始化时,可以省略掉=
,所以以上代码也可以写为:
int i{ 1 };
double d{ 3.14 };
char c{ 'x' };int* pf{ nullptr };
int& ri{ i };
但是这个语法其实也比较鸡肋,因为这个写法完全没有直接int i = 1;
这样来的直接。
接着就是自定义类型的初始化:
struct stu
{char name[20];int age;
};int main()
{stu s{ "jack", 18 };string str{ "hello world" };int arr[5]{ 1, 2, 3, 4, 5 };return 0;
}
以上代码中,通过直接在变量后面加一对大括号来实现初始化,数组和结构体初始化时的=
也可以省略掉。
在此,我要额外辨析一下现有的类的初始化方式:
现在有如下日期类:
class Date
{
public:Date(int year, int month, int day): _year(year),_month(month),_day(day){}private:int _year;int _month;int _day;
};
其含义三个变量,表示年月日,一个三参数的构造函数,初始化这个Date
。
我们有如下方式对其初始化:
Date d1(2024, 4, 3);
Date d2 = { 2024, 4, 3 };
Date d3{ 2024, 4, 3 };
请问这三个方式,分别是如何初始化一个类的?
- 对于
Date d1(2024, 4, 3);
,其就是最基础的构造函数调用语法,也就是直接构造 - 对于
Date d2 = { 2024, 4, 3 };
,很多人看到这个写法,再想到我们刚才讲的{ }
初始化,以为这个是C++11的新语法,其实并不是的。这个写法是多参数构造函数的类型转化,也就是说这个写法{ 2024, 4, 3 };
就是把三个int
类型转换为Date
类型。如果你用explicit
关键字修饰这个构造函数,那么类型转换功能就会被禁止,这个写法就会报错 - 对于
Date d3{ 2024, 4, 3 };
,这个写法即使用了{ }
,而且还省略了=
,这就是C++11提供的新语法了,当用explicit
修饰这个构造函数,这个写法依然有效。因为这个写法也是直接调用构造函数,而不是进行类型转换
其实整体上来说,C++11提供{ }
的意图在于提供统一的方式来初始化所有类型,但是奈何大部分程序员已经习惯了之前的写法,{ }
既没有带来效率的提高,也没有更加人性化的语法设计(甚至我感觉int i = 1
比int i(1);
更符合人类的习惯),因此这个语法并没有被广泛接受。
initializer_list
initializer_list
是一个新的C++类型,我先为大家创建一个initializer_list
类型:
auto li = { 1, 2, 3, 4 };
此时,li
的类型就是initializer_list
,这个时候有的人就疑惑了,{ 1, 2, 3, 4 }
分明是一个整型数组,怎么改了个名字就变成新类型了?initializer_list
翻译为中文就是初始化列表
,也就是说,这是一个用于初始化的工具。
假设现在你有以下数组:
int arr[5] = { 1, 2, 3, 4 };
你要如何用这个数组来初始化一个vector
,初始化一个list
,初始化一个set
呢?
我们好像只能粗暴的遍历数组,然后一个一个插入数据:
vector<int> v;
for (int i = 0; i < 5; i++)
{v.push_back(arr[i]);
}
这着实有点麻烦了,但是C++11后,STL的所有容器都增加了新的构造函数,可以通过initializer_list
来初始化容器:
vector<int> v({ 1, 2, 3, 4, 5 });
以上代码中,{ 1, 2, 3, 4, 5 }
整体就是一个initializer_list
,作为参数传给v,调用vector
的构造函数。
当然,我们也可以这样写:
vector<int> v = { 1, 2, 3, 4, 5 };
这个写法,则是单参数的类型转化,因为{ 1, 2, 3, 4, 5 }
整体就是一个initializer_list
类型的参数。
相同的办法,我们还可以初始化map
:
map<string, string> m = { {"apple","苹果"}, {"strawberry","草莓"}, {"watermelon", "西瓜"} };
以上代码中,最外层的{ }
括起来的就是一个initializer_list
,内部的三个{ }
则是三个不同的pair<const char*, const char*>
,不过const char*
可以转为string
,因此最后pair<const char*, const char*>
会变成pair<string, string>
。
最外层的initializer_list
内部的三个pair
,会依次插入进map
中,也就是一次拿多个值初始化map
的多个节点。
至此,你应该理解了,initializer_list
就是在类构造时,如果我们想要一次性初始化多个节点,就把这些节点放进一个initializer_list
内部,这样就能在构造函数中直接构造好。
initializer_list
本质上也是一个容器,一个类模板:
因此{ 1, 2, 3, 4, 5 }
的准确类型应该是:initializer_list<int>
。
而initializer_list
的底层也非常简单,我们看看其仅有的四个接口:
一个构造函数constructor
,一个描述长度的接口size
,以及迭代器begin
,end
。也就是说initializer_list
本质上是一个通过迭代器访问数组的容器。当其它容器通过initializer_list
构造自己,其实就是通过迭代器遍历那个存储了节点的数组,然后把数组元素一个一个插入。
也就是说,以下两种情况,本质是一样的:
initializer_list<int> lt = { 1, 2, 3, 4 };list<int> l1({ 1, 2, 3, 4 });
list<int> l2(lt.begin(), lt.end());
第一个list
通过initializer_list
初始化自己,第二个list
则通过迭代器初始化自己。不过前者更加方便,是C++11提供的,而后者是C++98提供的。
声明
auto
在C++11中,新增了关键字auto
,其可以自动推导类型:
auto i = 1;//整型
auto d = 3.14;//浮点型
auto p = &i;
此时i
就会被自动识别为int
,d
就自动识别为double
,p
自动识别为int*
。
auto
的主要作用在于对于有一些类型,它的长度太长了,我们就可以用auto
一笔带过。
比如完整地定义一个迭代器:
vector<int> v;
vector<int>::iterator it = v.begin();
但是我们可以用auto
直接自动识别:
vector<int> v;
auto it = v.begin();
在定义迭代器的时候,auto
的使用还是比较常见的。
decltype
在C++11以前,有一个关键字typeid
,其可以识别一个类型,并且可以通过name
成员函数来输出类型名。
比如这样:
int i = 0;
int* pi = &i;cout << typeid(i).name() << endl;
cout << typeid(pi).name() << endl;
输出结果为:
int
int * __ptr64
也就是说,我们可以通过typeid
来检测甚至输出变量类型。
而decltype
也是用于识别类型的,但是decltype
与typeid
应用方向不同。
decltype
可以检测一个变量的类型,并且拿这个类型去声明新的类型
比如这样:
int i = 0;
decltype(i) x = 5;
decltype(i)
检测出i
的类型为int
,于是decltype(i)
整体就变成int
,从而定义出一个新的变量x
。
nullptr
在C++11后,推出了新的空指针nullptr
,明明已经有NULL
了,为啥还需要nullptr
?
NULL
在C语言中,表示的是((void*)0)
,也就是被强制转为void*
类型的0。但是在C++中,NULL
就是整数0
比如可以用刚才学的typeid
验证一下:
cout << typeid(NULL).name() << endl;
输出结果为:int
,这下就石锤了NULL
在C++中就是int
。
这会导致不少问题,比如这样:
void func(int x)
{cout << "参数为整型" << endl;
}void func(void* x)
{cout << "参数为指针" << endl;
}int main()
{func(NULL);return 0;
}
以上代码中,func
函数有两个重载,一个是参数为指针,一个是参数为整型。我现在就是想传一个空指针去调用指针版本的func
。但是最后还是会调用int
类型的。
而nullptr
不一样,nullptr
不仅不是整型,而且其也不是void*
。C++给了nullptr
一个专属类型nullptr_t
。这个类型有一个非常非常大的优势,该类型只能转化为其它指针类型,不能转化为指针以外的类型。
比如以下代码:
int x1 = NULL;//正确
int x2 = nullptr;//错误
因为NULL
本质是0,其可以转化为很多非指针类型,比如int
,double
,char
。但是nullptr
是nullptr_t
,它只能转化为其他指针。上述代码中,我们把nullptr
转化为一个int
,此时编译器会直接报错,绝对禁止这个行为。
但是这样是可以的:
void* p1 = nullptr;
int* p2 = nullptr;
char* p3 = nullptr;
double* p4 = nullptr;
可以看到,nullptr
保证了指针类型的稳定,空指针不会被传递到指针以外的类型。因此nullptr
在各方面都有足够的优势,以更加安全的形式给用户提供空指针。