列表初始化、声明、范围for、array容器
-
- 一、统一的列表初始化
-
- [1.1 使用{ }初始化](#1.1 使用{ }初始化)
- [1.2 initializer_list容器](#1.2 initializer_list容器)
- 二、声明
-
- [2.1 auto关键字](#2.1 auto关键字)
- [2.2 decltype关键字](#2.2 decltype关键字)
- [2.3 nullptr关键字](#2.3 nullptr关键字)
- 三、范围for
- 四、array容器和forward_list容器
一、统一的列表初始化
1.1 使用{ }初始化
在C++98中,标准允许使用花括号{}对数组或者结构体元素进行统一的列表初始值设定。比如:
cpp
struct Person
{
string _name
int _age;
};
int main()
{
int arr1[] = { 1, 2, 3, 4, 5 };
int arr2[5] = { 0 };
Person p = { "张三", 21};
return 0;
}
C++11扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自定义的类型,使用初始化列表时,可添加等号(=),也可不添加。
cpp
struct Person
{
string _name
int _age;
};
int main()
{
//单个元素
int a = 1;
int b = { 2 };
int c{ 2 };
//数组
int arr1[]{ 1, 2, 3, 4, 5 };
int arr2[5]{ 0 };
//自定义结构体
Person p = { "张三", 21};
//调用new时,对每个对象初始化
int* p1 = new int(1);
int* p2 = new int[3]{ 1, 3, 4 };
return 0;
}
创建对象时也可以使用列表初始化方式调用构造函数初始化。
cpp
class Date
{
public:
Date(int year, int month, int day)
:_year(year)
, _month(month)
, _day(day)
{
cout << "Date(int year, int month, int day)" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2024, 12, 17); // old style
// C++11支持的列表初始化,这里会调用构造函数初始化
Date d2 = { 2024, 12, 17 };//类似隐式类型转换 + 优化
Date d3{ 2024, 12, 17 };
Date* p1 = new Date(2024, 12, 17);
Date* p2 = new Date[3]{ { 2024, 12, 17}, { 2024, 12, 18}, { 2024, 12, 19} };
return 0;
}
1.2 initializer_list容器
c++11里添加了initializer_list容器,介绍文档链接:initializer_list的介绍
此容器提供的成员函数只有三个,还有一个构造函数:
cpp
int main()
{
initializer_list<double> lt = { 24, 17, 21 };
initializer_list<double>::iterator it = lt.begin();
while (it != lt.end())
{
cout << *it << " ";//24 17 21
it++;
}
cout << endl;
for (auto e : lt)
{
cout << e << " ";//24 17 21
}
return 0;
}
该类型用于访问C++初始化列表中的值,该列表是 类型的元素列表。这种类型的对象由编译器从初始化列表声明自动构造,初始化列表声明是用大括号括起来的逗号分隔元素的列表:const T。
常量的花括号列表会被编译器识别成initializer_list
cpp
int main()
{
// the type of il is an initializer_list
auto il = { 10, 20, 30 };
cout << typeid(il).name() << endl;//class std::initializer_list<int>
return 0;
}
initializer_list的使用场景:
initializer_list一般是作为构造函数的参数,C++11对STL中的不少容器增加initializer_list作为参数的构造函数,这样初始化容器对象时就变得方便了,也可以作为operator =的参数,这样就可以用大括号赋值:
cpp
class Date
{
public:
Date(int year, int month, int day)
:_year(year)
, _month(month)
, _day(day)
{
cout << "Date(int year, int month, int day)" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
//vector
vector<int> v1 = { 1, 2 ,3 ,4, 5 };
// 使用大括号对容器赋值,{}调用构造函数构造一个vector对象,再赋值
v1 = {10, 20, 30};
vector<int> v2{ 1, 2, 3, 4, 5 };
vector<Date> v3 = { { 2024, 12, 17}, { 2024, 12, 18}, { 2024, 12, 19} };
//list
list<int> lt1{ 1, 2, 3 };
//set
set<int> s1{ 3, 4, 5, 6, 3 };
//map
map<string, string> dict = { {"apple", "苹果" }, {"english", "英语" } };
return 0;
}
initializer_list的使用示例:
对我们以前实现过的vector类型中的构造函数进行改动,可以使构造函数还能实现的再简单点,直接在初始化列表里把三个成员变量初始化,在遍历ls,复用push_back依次插入到vector即可:
cpp
vector(initializer_list<T> il)
:_start(nullptr)
, _finish(nullptr)
, _endofstoage(nullptr)
{
for (auto e : il)
{
push_back(e);
}
}
注意:
- 最好增加一个以initializer_list作为参数的赋值运算符重载函数,以支持直接用列表对容器对象进行赋值,但实际也可以不增加。
如下:
cpp
vector<int> v1 = { 1, 2 ,3 ,4, 5 };
// 使用大括号对容器赋值,{}调用构造函数构造一个vector对象,再赋值
v1 = {10, 20, 30};
对于第二行的赋值操作,涉及到了隐式类型转换,先使用{}调用构造函数构造一个vector对象,再赋值。
二、声明
c++11提供了多种简化声明的方式,尤其是在使用模板时。
2.1 auto关键字
在C++98中auto是一个存储类型的说明符,表明变量是局部自动存储类型,但是局部域中定义局部的变量默认就是自动存储类型,所以auto就没什么价值了。C++11中废弃auto原来的用法,将其用于实现自动类型腿断。这样要求必须进行显示初始化,让编译器将定义对象的类型设置为初始化值的类型。
cpp
int main()
{
int i = 10;
//int*
auto p = &i;
map<string, string> dict = { {"sort", "排序"}, {"insert", "插入"} };
//map<string, string>::iterator
auto it = dict.begin();
return 0;
}
2.2 decltype关键字
decltype
是 C++11 引入的一个关键字,用于在编译时推导表达式的类型。它根据给定表达式的类型来确定一个新类型,可以在模板编程和类型推导中非常有用
cpp
// decltype的一些使用使用场景
template<class T1, class T2>
void F(T1 t1, T2 t2)
{
decltype(t1 * t2) ret;
cout << typeid(ret).name() << endl;//double
}
int main()
{
const int x = 1;
double y = 2.2;
decltype(x * y) ret; // ret的类型是double
decltype(&x) p; // p的类型是int const *
cout << typeid(ret).name() << endl;//double
cout << typeid(p).name() << endl;//int const *
F(1, 'a');
return 0;
}
下面来区分下typeid和decltype:
- typeid(变量名).name():专门用来输出一个变量的类型,返回的是一个字符串。帮助我们观察此字符串的类型,不能用其去定义变量。
- decltype:将变量的类型声明为表达式指定的类型,可以用其去定义变量。
2.3 nullptr关键字
由于C++中NULL被定义成字面量0,这样就可能回带来一些问题,因为0既能指针常量,又能表示整形常量。所以出于清晰和安全的角度考虑,C++11中新增了nullptr,用于表示空指针。
cpp
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endi
如果没有定义宏,如果在cplusplus里,NULL被定义成0。可以看到,NULL可能被定义为字面常量0,或者被定义为无类型指针(void*)的常量。不论采取何种定义,在使用空值的指针时,都不可避免的会遇到一些麻烦,比如:
cpp
void print(int* a){
cout << "int*" << endl;
}
void print(int a){
cout << "int" << endl;
}
int main(){
print(NULL); //int
return 0;
}
程序本意是想通过print(NULL)调用指针版本的print(int *)函数,但是由于NULL被定义成0,因此与程序的初衷相悖。在C++98中,字面常量0既可以是一个整形数字,也可以是无类型的指针(void)常量,但是编译器默认情况下将其看成是一个整形常量,如果要将其按照指针方式来使用,必须对其进行强转(void *)0。
注意:
- 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的。
- 在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。
- 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。
三、范围for
范围for的底层就是被替换成了迭代器。
范围for的语法
若是在C++98中我们要遍历一个数组,可以按照以下方式:
cpp
int main()
{
int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
//将数组元素值全部乘以2
for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
{
arr[i] *= 2;
}
//打印数组中的所有元素
for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
{
cout << arr[i] << " ";
}
cout << endl;
return 0;
}
以上方式也是C语言中所用的遍历数组的方式,但对于一个有范围的集合而言,循环是多余的,有时还容易犯错。
C++11中引入了基于范围的for循环,for循环后的括号由冒号分为两部分,第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。比如
cpp
int main()
{
int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
//将数组元素值全部乘以2
for (auto& e : arr)
{
e *= 2;
}
//打印数组中的所有元素
for (const auto e : arr)
{
cout << e << " "; // 2 4 5 8 10 12 14 16 18 20
}
cout << endl;
return 0;
}
注意: 与普通循环类似,可用continue来结束本次循环,也可以用break来跳出整个循环。
范围for的使用条件
1. for循环迭代的范围必须是确定的
- 对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围。
2. 迭代的对象要支持++和==操作
- 范围for本质上是由迭代器支持的,在代码编译的时候,编译器会自动将范围for替换为迭代器的形式。而由于在使用迭代器遍历时需要对对象进行++和操作,因此使用范围for的对象也需要支持++和操作。
四、array容器和forward_list容器
1、array容器
array就是一个静态数组,其有两个模板参数,第一个模板参数代表的是存储的数据类型,第二个是非类型模板参数,代表的是存储元素的个数:
cpp
int main()
{
array<int, 10> a1;
array<double, 15> a2;
return 0;
}
array和普通数组最大的区别在于对于越界访问的检查:
cpp
int main()
{
int a[10];
cout << a[10] << endl;//越界不一定能检查出来
array<int, 10> b;
cout << b[10] << endl;//只要越界,一定能检查出来
return 0;
}
总结:
- array容器的对象是建立在栈区的,不适合定义大数组
- array容器的设计可能是为了代替静态数组,因为array容器更安全,能够检查除越界的错误,而静态数组并不一定能够检查出来。
2、forward_list容器
forward_list容器本质就是一个单链表,相比list的区别在于forward_list节省了空间,实际使用上使用forward_list的比率还是比较低的,还是使用list来的方便。
容器中的一些新方法
如果我们再细细去看会发现基本每个容器中都增加了一些C++11的方法,但是其实很多都是用得比较少的。
比如提供了cbegin和cend方法返回const迭代器等等,但是实际意义不大,因为begin和end也是可以返回const迭代器的,这些都是属于锦上添花的操作。