一.列表初始化
1.C++11中的{}
1.C++11以后想统一初始化方式,试图实现一切对象都可以用{}初始化,这也叫做列表初始化;
2.内置类型和自定义类型都支持,自定义类型本质是类型转换,中间会产生临时变量,最后优化了以后变成了直接构造;
3.用{}初始化的过程中=可以省略;
4.C++{}初始化的本意是实现一个同一的初始化方式,其次它在其他场景下也带来了不少遍历,如push/insert多参数时,用{}会很方便。
cpp
struct Point
{
Point(int x = 1,int y = 2)
:_x(x)
,_y(y)
{ }
int _x;
int _y;
};
int main()
{
//这两个的初始化叫做初始化列表,
//本质上是用{}中的参数构造了一个initializer_list(初始化列表),
//再将该初始化列表传参给int[]的构造
//库里面的所有容器基本都有参数为初始化列表的构造
int array1[] = { 1, 2, 3, 4, 5 };
int array2[5] = { 0 };
//而这个是列表初始化,{}内的参数与该类构造函数的参数一一对应
Point p = { 1, 2 };
//内置类型也可以
int a = { 1 };
char c = { 'b' };
//甚至可以省略赋值
int a1{ 2 };
Point p1{ 3,4 };
int array3[]{ 2,3,45,6 };//这个是初始化列表
//当将point的构造写成全缺省时
Point p2 = 4;//相当于只传一个参数
Point p3{ 4 };//相当于只传一个参数
return 0;
}
2.C++11中的initializer_list
1.上面的初始化已经很方便了,但是对象容器初始化还是不太方便,比如一个vector对象,我想用N个值去初始化构造它,那我们得实现很多个构造函数才能支持:参数个数为1、2、3...N;
2.C++11库中就提出了一个std::initializer_list的类,它的本质是底层开一个数组,将数据拷贝过来,std::initializer_list的底层有两个指针分别指向数组的开头和结尾;
3.STL中支持任意多个值构成的{x1,x2,x3...}进行初始化的容器,就是通过std::initializer_list的构造函数来实现的。
二. 右值引用和移动语义
C++98的语法中就有引用的语法,比如int&...,这就叫左值引用 ;而C++11新增了右值引用的语法特性。无论是左值还是右值引用都是给变量取别名。
1.左值与右值
1.左值是一个表示数据的表达式(如变量名),一般有持久状态,储存在内存中,我们可以获取它的地址,左值既可以出现在赋值的左边也可以是右边。定义时const修饰的左值不能修改,但可以取它地址;
2.右值也是一个表示数据的表达式,要么是字面值常量、要么是表达式求值过程中创建的临时变量等,右值可以出现在赋值的右边,但不能出现在左边,右值不能取地址;
3.左值和右值在英文中的缩写是lvalue、rvalue,传统认为它们是left/right value的缩写。在现代c++中,它们分别被解释为locator value(储存在内存中,有明确地址可以取的值) 、read value(可以提供数据值,但不可以寻址,如临时变量、字面量常量)的缩写,也就是说左右值的核心区别在于能否寻址。
左右值间最大的区别就是能否取地址。
cpp
int add(int a, int b)
{
return a + b;
}
int main()
{
//这些都是左值,因为它们都可以取地址
int a = 1;
int b = 2;
int* p = new int;
*p = 10;
string s("fodsai");
s[0] = 'k';
//这些都是右值,不能取它们的地址,它们的寿命只有一行
a + b;//临时变量
3;//常量
string("oashjdf");//匿名对象
add(a, b);//临时变量
}
2.左值引用和右值引用
1.左值引用就是给左值起别名,右值引用就是给右值起别名;
cpp
int a = 9;
int& b = a;//左值引用
int&& c = 2;//右值引用
2.左值不能直接引用右值,但const左值可以引用右值;
cpp
//int& a = 10;//不行
//int&& a = 10;//右值引用
const int& a = 10;
3.右值不能直接引用左值,但可以引用move(左值);
cpp
int a = 10;
//int&& b = a;//不行
int&& b = move(a);
4.move是一个函数模板,本质是内部进行强转,还涉及一些折叠知识;
5.需要注意的是,变量表达式都是左值属性,也就意味着一个右值被右值引用绑定后,该右值引用表达式是左值属性;
cpp
int&& a = 10;//a是右值引用表达式
int& b = a;//可以直接左值引用a
b = 2;//a和b都会变
6.语法层面上,左右值引用都是取变量别名,不会额外开空间;从底层来讲,它们都是取地址,没什么区别,但它们的语法规则就是不一样。
例子:
cpp
int add(int a, int b)
{
return a + b;
}
int main()
{
//这些都是左值,因为它们都可以取地址
int a = 1;
int b = 2;
int* p = new int;
*p = 10;
string s("fodsai");
s[0] = 'k';
//这些都是右值,不能取它们的地址,它们的寿命只有一行
a + b;//临时变量
3;//常量
string("oashjdf");//匿名对象
add(a, b);//临时变量
//对于左值,我们得用左值取引用,右值则右值引用
//左值引用
int& a1 = a;
int*& pp = p;
string& ps = s;
char& c = s[0];
//不能引用右值,会报错
//int& a2 = a + b;
//int& a3 = 3;
//右值引用
int&& aa1 = a + b;
int&& aa2 = 3;
string&& ps1 = string("oashjdf");
int&& aa3 = add(a, b);
//不能引用左值,会报错
//int&& aa4 = a;
//string&& ps2 = s;
//用左值引用也可以引用右值,前面要加const修饰
const int& bb1 = 3;
const int& bb2 = a + b;
//用右值引用也可以引用左值,但是要用上一个模板函数move
//本质上是把左值强转为右值了
int&& cc1 = move(a);
int&& cc2 = move(b);
string&& ss1 = move(s);
cc1 = 10;//cc1可以改变,且a会跟着改变
//但abs本身没有改变性质,仍然可以修改值
//在这里修改值后,它们的右值引用cc1、cc2等的值都会改变
a = 20;
b = 9;
s = "daol";
return 0;
}
3.引用延长生命周期
右值引用可用于临时对象延长生命周期,const的左值引用也可以延长临时对象的生命周期但它不可以修改。
cpp
int main()
{
std::string s1 = "Test";
//std::string&& r1 = s1;
const std::string& r2 = s1 + s1;//s1 + s1这个临时对象虽然被r2引用,但它不能修改
// r2 += "Test";
std::string&& r3 = s1 + s1;//s1 + s1这个临时对象没有被销毁,而是一值存在着被r3引用
r3 += "Test";
std::cout << r3 << '\n';
return 0;
}
4.左值和右值的参数匹配
1.C++98中我们实现以const左值引用为参数的函数,那么它左值和右值都可以接收;
2.C++11后重载左值引用、const左值引用、右值引用作为某函数的形参,那么调用时,实参为左值会匹配左值引用、const左值会匹配const左值引用、右值会匹配右值引用,但如果形参没有右值引用,则右值会匹配const左值引用;
3.右值引用的表达式为左值;
5.右值引用与移动语义的使用场景
1.左值引用场景回顾
主要场景是在函数中左值引用传参和左值引用返回减少拷贝,同时还可以修改实参和返回对象。 左值引用解决了大部分场景的拷贝效率问题,但有些场景是无法用左值引用返回的,如addStrings函数:
cpp
class Solution {
public:
string addStrings(string num1, string num2) {
string str;
int end1 = num1.size() - 1, end2 = num2.size() - 1;
int next = 0;
while (end1 >= 0 || end2 >= 0)
{
int val1 = end1 >= 0 ? num1[end1--] - '0' : 0;
int val2 = end2 >= 0 ? num2[end2--] - '0' : 0;
int ret = val1 + val2 + next;
next = ret / 10;
ret = ret % 10;
str += ('0' + ret);
}
if (next == 1)
str += '1';
reverse(str.begin(), str.end());
return str;
}
};
那它能用右值返回吗?也不行,因为一旦出了该函数str就被析构了。
2.移动构造与移动赋值
1.移动构造是一种构造函数,类似于拷贝构造,但它要求第一个参数为该类类型的右值引用,如果有额外的参数,那么这些参数必须要有缺省值;
2.移动赋值时赋值运算符的重载,类似拷贝赋值,但它要求第一个参数是该值的右值引用;
3.对于像vector\string这样要进行深拷贝的类移动构造与赋值才有意义,因为它们可以"窃取"引用的右值对象的资源,而不是像拷贝构造那样取拷贝资源,从而提高效率。
加上了右值引用模拟实现的string:
cpp
namespace my
{
class string
{
public:
typedef char* iterator;
typedef const char* const_iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
const_iterator begin() const
{
return _str;
}
const_iterator end() const
{
return _str + _size;
}
string(const char* str = "")
:_size(strlen(str))
, _capacity(_size)
{
cout << "string(char* str)-构造" << endl;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
// 拷贝构造
string(const string& s)
:_str(nullptr)
{
cout << "string(const string& s) -- 拷贝构造" << endl;
reserve(s._capacity);
for (auto ch : s)
{
push_back(ch);
}
}
void swap(string& ss)
{
::swap(_str, ss._str);
::swap(_size, ss._size);
::swap(_capacity, ss._capacity);
}
//移动构造
string(string&& s)
{
cout << "string(string&& s) -- 移动构造" << endl;
// 转移掠夺你的资源
swap(s);
}
string& operator=(const string& s)
{
cout << "string& operator=(const string& s) -- 拷贝赋值" <<
endl;
if (this != &s)
{
_str[0] = '\0';
_size = 0;
reserve(s._capacity);
for (auto ch : s)
{
push_back(ch);
}
}
return *this;
}
// 移动赋值
string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 移动赋值" << endl;
swap(s);
return *this;
}
~string()
{
//cout << "~string() -- 析构" << endl;
delete[] _str;
_str = nullptr;
}
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
void reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];
if (_str)
{
strcpy(tmp, _str);
delete[] _str;
}
_str = tmp;
_capacity = n;
}
}
void push_back(char ch)
{
if (_size >= _capacity)
{
size_t newcapacity = _capacity == 0 ? 4 : _capacity *
2;
reserve(newcapacity);
}
_str[_size] = ch;
++_size;
_str[_size] = '\0';
}
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
const char* c_str() const
{
return _str;
}
size_t size() const
{
return _size;
}
private:
char* _str = nullptr;
size_t _size = 0;
size_t _capacity = 0;
};
}
3.右值引用和移动语义解决传值返回问题
cpp
class Solution {
public:
// 传值返回需要拷贝
my::string addStrings(my::string num1, my::string num2) {
my::string str;
int end1 = num1.size() - 1, end2 = num2.size() - 1;
// 进位
int next = 0;
while (end1 >= 0 || end2 >= 0)
{
int val1 = end1 >= 0 ? num1[end1--] - '0' : 0;
int val2 = end2 >= 0 ? num2[end2--] - '0' : 0;
int ret = val1 + val2 + next;
next = ret / 10;
ret = ret % 10;
str += ('0' + ret);
}
if (next == 1)
str += '1';
reverse(str.begin(), str.end());
//cout << &str << endl;
return str;
}
};
int main()
{
my::string ret = Solution().addStrings("11111", "2222");
cout << ret.c_str() << endl;
return 0;
}
编译器的优化程度会因为编译器的不同而有所不同,以下展示的优化是比较早期的优化。
右值对象构造,只有拷贝构造没有移动构造

它会经历拷贝后,把原来储存了值的str释放掉;
在这种情况下,后期的编译器优化甚至不会创建临时变量str,而是直接让str当ret的引用用。
右值对象构造,有移动构造

它不会经历拷贝,而是用ret将返回值str的值掠夺过去,而str的值将变为ret原来的值(空);很明显比单纯的拷贝构高效。
移动赋值
移动赋值与拷贝赋值间的关系与移动构造和拷贝构造间的差不多,当实参为右值时,移动赋值是比拷贝赋值高效的多的。
总结
1.左值引用和右值引用都是为了减少拷贝,提高程序的运行效率而诞生;
2.其中左值引用就可以解决很多地方的拷贝问题,但是还有一些情况下是无法用左值引用的,早期这些情况的提效需要依靠编译器的优化,但是不同的编译器优化可能是不同的,C++没有统一规定。于是右值引用就出现了;
3.右值引用引入后,无论编译器的优化如何,在解决拷贝消耗这一块,它们的区别都不大。
对于vector、list等需要深拷贝的类型,实现移动构造/赋值的意义是非常大的;但对于int、char、pair<int,int>、Data(日期类)等只需要浅拷贝的类型,实现移动构造/赋值的意义就不是很大,因为它们的拷贝构造消耗并不大。
6.表达式分类
C++11以后进一步对表达式进行了划分:
1.表达式被分为两大类:泛左值和右值;
2.右值分为:纯右值、将亡值;
3.纯右值指的是字面常量或求值结果相当于字面常量的式子或一个不具名的临时变量 ,如4、true、nullptr、a+b(a,b为整形)、a++、str1+str2(函数传值返回)等。c++11划分的纯右值相当于c++98中的右值,当时还没有将亡值的概念;
4.将亡值指的是返回右值引用的函数调用表达式和转化为右值引用的转化函数的调用表达式 ,如:move(x)将x转化为右值引用返回、static_cast<X&&>(x)相当于强转;
5.泛左值分为:左值和将亡值;

7.引用折叠
1.C++中不能直接定义引用的引用,如:int&& & a = ...;这样做会报错,但是通过模板或者typedef中的类型操作,可以实现引用的引用 ;
2.通过模板或typedef实现引用的引用时,c++定义只有组合为右值引用的右值引用将折叠为右值引用,其他组合均折叠为左值引用 ;
3.typdef类型操作实现引用的引用(int为例):
cpp
int main()
{
typedef int& lref;//左引用
typedef int&& rref;//右引用
int a = 1;
//左引用 + 左/右引用 = 左引用
lref& b = a;
//lref& b = 1;报错
lref&& c = a;
//lref&& c = 1; 报错
rref& d = a;//右引用 + 左引用 = 左引用
//rref& d = 1; 报错
//右引用 + 右引用 = 右引用
//rref&& e = a;//报错
rref&& e = 1;//必须引用右值
return 0;
}
4.模板中的引用折叠(以函数模板为例,类模板一样的逻辑):
显式实例化:
当形参默认为左值引用时:
cpp
template<class T>
void Func(T& t)//这里传过来的T必将与左值引用进行折叠,则t只能接收左值引用类型
{
cout << "Func(T& t)" << endl;
}
int main()
{
int a = 0;
const int b = 0;
//显式实例化
//1.传int,不发生折叠,形参类型为 左值引用
//Func<int>(1);//实参为右值,报错
//Func<int>(b);//实参为const左值,报错
Func<int>(a);
//2.传const int,不发生折叠,形参类型为 const左值引用
Func<const int>(a);
Func<const int>(b);
Func<const int>(2);
//3.传int&,发生折叠,形参类型为 左值引用
//Func<int&>(1);//实参为右值,报错
//Func<int&>(b);//实参为const左值,报错
Func<int&>(a);
//4.传const int&,发生折叠,形参为 const左值引用
Func<const int&>(a);
Func<const int&>(b);
Func<const int&>(2);
//5.传int&&,发生折叠,形参类型为 左值引用
//Func<int&&>(1);//实参为右值,报错
//Func<int&&>(b);//实参为const左值,报错
Func<int&&>(a);
//6.传const int&&,发生折叠,形参类型为 const左值引用
Func<const int&&>(a);
Func<const int&&>(b);
Func<const int&&>(2);
return 0;
}
可见,这种情况下形参只能为左值引用。
const不会影响折叠,会对折叠后的类型加以修饰。
当形参默认为右值引用时:
cpp
template<class T>
void Func(T&& t)//这里传过来的T必将与右值引用进行折叠
{
cout << "Func(T& t)" << endl;
}
int main()
{
int a = 0;
const int b = 0;
//显式实例化
//1.传int,不发生折叠,形参类型为 右值引用
//Func<int>(b);//实参为const左值,报错
//Func<int>(a);//实参为左值,报错
Func<int>(1);
//2.传const int,不发生折叠,形参类型为 const右值引用
//Func<const int>(a);
//Func<const int>(b);
Func<const int>(2);
//3.传int&,发生折叠,形参类型为 左值引用
//Func<int&>(1);//实参为右值,报错
//Func<int&>(b);//实参为const左值,报错
Func<int&>(a);
//4.传const int&,发生折叠,形参为 const左值引用
Func<const int&>(a);
Func<const int&>(b);
Func<const int&>(2);
//5.传int&&,发生折叠,形参类型为 右值引用
Func<int&&>(1);
//Func<int&&>(b);//实参为const左值,报错
// Func<int&&>(a);//实参为左值,报错
//6.传const int&&,发生折叠,形参类型为 const右值引用
//Func<const int&&>(a);
//Func<const int&&>(b);
Func<const int&&>(2);
return 0
}
可见,当形参默认为右值引用时,无论是左值引用还是右值引用模板都能实例化出来,这样的模板的参数叫做 万能引用。
也许你会想就算模板参数不加引用同样能做到相同的效果,但是如果是隐式实例化呢?
隐式实例化:
cpp
template<class T>
void Func(T&& t)//这里传过来的T必将与右值引用进行折叠
{
cout << "Func(T& t)" << endl;
}
int main()
{
int a = 1;
const int b = 0;
Func(10);
//10为右值,推导出T为int类型,不折叠,形参类型为T&&;
Func(move(a));
//move(a)右值引用类型,但推导出T为int类型,不折叠,形参类型为T&&;
Func(a);
//a为左值,T推导为int&类型,折叠,形参类型为T&;
Func(b);
//b为const左值,T推导为const int&,折叠,形参类型为const T&;
return 0;
}
可见,在这种情况下 (万能引用 + 隐式实例化) ,传左值T会被推到为左值引用,传右值T会被推到为该数据类型本身。
8.完美转发
前面有提到过,变量表达式都是左值属性,也就是说,在这个函数中,t始终为左值,但向外传t给万能引用时,那个引用只能折叠为左值引用(参数属性丢失);
cpp
template<class T>
void Func(T&& t)
{
cout << "Func(T& t)" << endl;
}
这时候就要借助一个函数模板 :完美转发forward。如在这种情况下:
cpp
template<class T>
void Func1(T&& t)
{
Fun2(forward<T>(t));
}
template<class T>
void Func2(T&& t)
{
cout << "Func(T& t)" << endl;
}
如果Func1中模板参数类型被折叠为左值引用,用forward(t)向Func2传递的参数类型将为左值引用;如果被折叠为右值引用,那么用forward(t)向Func2传递的参数类型将为右值引用;如果不加forward,则向Func2中传递的参数类型只能为左值。
拿模拟实现的list来举例子:
拿push_back举例子,所以与其有关的我写成了万能引用的形式
cpp
namespace my
{
template<class T>
struct list_node
{
list_node() = default;
template<class X>
list_node(X&& val)
:_data(val)
, _next(nullptr)
, _prev(nullptr)
{}
//如果X被识别为左值引用,就无法用T()构造了,所以必须加一个编译器自动生成的无参构造。
T _data;
list_node* _next;
list_node* _prev;
};
template<class T,class Ref,class Ptr>
struct list_iterator
{
typedef list_node<T> Node;
typedef list_iterator<T,Ref,Ptr> Self;
typedef list_iterator<T, T&, T*> iterator;
typedef list_iterator<T, const T&, const T*> const_iterator;
Node* _node;
list_iterator(Node* node = nullptr)
:_node(node)
{}
Ref operator*() const
{
return _node->_data;
}
Ptr operator->() const
{
return &(_node->_data);
}
Self& operator++()
{
_node = _node->_next;
return *this;
}
Self& operator--()
{
Self ret(_node);
_node = _node->_prev;
return ret;
}
Self operator++(int)
{
Self ret(_node);
_node = _node->_next;
return ret;
}
Self operator--(int)
{
_node = _node->_prev;
return *this;
}
bool operator!=(const Self& it) const
{
return it._node != _node;
}
bool operator==(const Self& it) const
{
return it._node == _node;
}
};
template<class T>
class list
{
typedef list_node<T> Node;
public:
typedef list_iterator<T, T&, T*> iterator;
typedef list_iterator<T, const T&, const T*> const_iterator;
void Init_empty()//创建头节点
{
_head = new Node;
_head->_prev = _head;
_head->_next = _head;
_size = 0;
}
list()
{
Init_empty();
}
list(const initializer_list<T> il)
{
Init_empty();
for (auto& e : il)
{
push_back(e);
}
}
list(const list& lt)
{
Init_empty();
for (auto& e : lt)
{
push_back(e);
}
}
void clear()
{
auto it = begin();
while (it != end())
{
it = erase(it);
}
}
~list()
{
clear();
delete _head;
_head = nullptr;
_size = 0;
}
void swap(list& lt)
{
std::swap(_head, lt._head);
std::swap(_size, lt._size);
}
list& operator=(list lt)
{
clear();
swap(lt);
return *this;
}
template<class X>
void push_back(X&& val)
{
insert(end(), val);
}
//void push_back(const T& val)
//{
// insert(end(), val);
//}
void pop_back()
{
erase(--end());
}
void push_front(const T& val)
{
insert(begin(), val);
}
void pos_front()
{
erase(begin());
}
iterator begin()
{
return _head->_next;
}
iterator end()
{
return _head;
}
const_iterator begin() const
{
return _head->_next;
}
const_iterator end() const
{
return _head;
}
template<class X>
iterator insert(iterator pos, X&& val)
{
Node* newnode = new Node(val);
Node* prev = pos._node->_prev;
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = pos._node;
pos._node->_prev = newnode;
_size++;
return newnode;
}
//iterator insert(iterator pos, const T& val)
//{
// Node* newnode = new Node(val);
// Node* prev = pos._node->_prev;
// prev->_next = newnode;
// newnode->_prev = prev;
// newnode->_next = pos._node;
// pos._node->_prev = newnode;
// _size++;
// return newnode;
//}
iterator erase(iterator pos)
{
assert(pos != _head);
Node* next = pos._node->_next;
Node* prev = pos._node->_prev;
prev->_next = next;
next->_prev = prev;
delete pos._node;
_size--;
return next;
}
private:
Node* _head;
size_t _size;
};
template<class Container>
void Printf_Con(const Container& con)
{
for (auto& e : con)
{
//e++;
cout << e << ' ';
}
cout << endl;
}
}
在这里,想传一个右值调用移动构造,会先先将push_back的参数识别为右值引用,在该函数内调用insert就只会识别为左值引用了。所以我们必须在这些函数中调用其他函数时加上forward:
cpp
template<class X>
void push_back(X&& val)
{
insert(end(), forward<X>(val));
}
iterator insert(iterator pos, X&& val)
{
Node* newnode = new Node(forward<X>(val));
Node* prev = pos._node->_prev;
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = pos._node;
pos._node->_prev = newnode;
_size++;
return newnode;
}
这样就可以一步步调用到Node的右值引用构造。
三.可变参数模板
1.基本语法和原理
1.C++11支持可变参数模板,即支持可变数量参数的函数模板或类模板 ,可变数目的参数被称为参数包 ,存在两种参数包,模板参数包,表示零或多个模板参数;函数参数包,表示零或多个函数参数 ;
2.用法:
cpp
template<class... Arg>
void print(Arg&&... arg)
{
cout << sizeof...(arg) << endl;
//用来计算参数包arg有多少个参数
}
int main()
{
int a = 1;
print();//参数包内没有参数
print(a);//有一个
print(a,string("sah"));//有两个
print(a, string("sah"),3.4);//有三个
cout << endl;
return 0;
}
有一个print的可变参数模板就可以是实现多种参数情况的print函数;
3.我们用省略号来指出一个模板参数或函数参数表示一个包,在模板参数列表中,typedef/class...表示接下来的参数是表示零或多个类型的列表 ,如:template<class... Arg> 中的Arg(名字取其他的可以);在函数参数列表中,类型后接...指该参数表示的是零或多个参数列表 ;参数包可以用左值和右值引用,和普通的模板一样受引用折叠影响;
4.可变参数模板和普通模板类似,本质还是去实例化出对应参数个数和类型的函数/类 ;
就像该main函数内的print调用,如果不支持模板的话,我们只能自己写出对应的函数:
cpp
//函数重载
void print()
{
cout << endl;
}
void print(int& x)
{
cout << x<<' ';
}
void print(int& x,string&& y)
{
cout << x<<' '<<y<<' ';
}
void print(int& x,string&& y,double&& z)
{
cout << x<<' '<<y<<' '<<z<<' ';
}
如果不存在可变参数模板的话,那我们也只能手动写出对应参数个数的模板函数:
cpp
void print()
{
cout << endl;
}
template<class T1>
void print(T1&& x)
{
cout << x << ' ' << endl;
}
template<class T1,class T2>
void print(T1& x,T2&& y)
{
cout << x<<' '<<y<<' ' << endl;
}
template<class T1, class T2,class T3>
void print(T1& x,T2&& y,T3&& z)
{
cout << x<<' '<<y<<' '<<z<<' ' << endl;
}
如果支持可变参数模板的话就将这些繁琐的工作交给编译器即可:
cpp
void print(Arg&&... arg)
{
cout << sizeof...(arg) << endl;
}//具体怎么打印在下一小节
5.可以用运算符sizeof...算出参数包内含参个数;
6.原理:在语法上我们可以认为编译器根据我们调用模板所传的参数个数,生成对应参数个数的模板,在根据参数具体的类型,实例化出对应的函数/类;但实际上编译器是直接通过可变参数模板实例化出对应的函数/类的,因为可变参数模板{}中的逻辑普通模板没法实现,它必须要通过包扩展的方式实现;
7.和普通模板一样,可变参数模板的实例化也是在程序的编译阶段完成的。
2.包扩展
1.对于一个参数包,我们出了计算它有几个参数外,唯一 能做的就是扩展参数包 ,扩展一个包就是将它分解为构成它的元素 ,当我们扩展一个包时,我们还要提供用于每个扩展元素的模式 ,对每个元素应用模式,获得扩展后的列表,简单点来说就是参数包扩展后获得的是一个参数列表,我们必须对该列表进行操作 。我们通常在该模式/操作 整体的右边加上...来触发扩展操作 。
cpp
void Print()
{
cout << endl;
}
template<class T,class ...Arg>
void Print(T&& x, Arg&&... arg)
{
cout << x << ' ';
Print(forward<Arg>(arg)...);
}
template<class ...Arg>
void print(Arg&&... arg)
{
Print(forward<Arg>(arg)...);
}
int main()
{
int a = 1;
print(a, string("sah"),3.4);
return 0;
}
这里我们就是通过包扩展来实现参数包中参数的打印的,其中Print就是接收参数包中参数列表的一种模式,它的逻辑如下:

我们能否不实现没有参数的Print函数呢?就像这样:
cpp
template<class T,class ...Arg>
void Print(T&& x, Arg&&... arg)
{
if (sizeof...(arg) == 0)
return;
cout << x << ' ';
Print(forward<Arg>(arg)...);
}
通过sizeof...计算参数包中参数为0作为递归终止条件。这样是不行的!
前面说过,可变参数模板实例化是在编译阶段完成的,而if的判断是在运行程序时进行的 ,所以但参数包空了时,编译器不会进行if判断,而是会去找与Print(forward(arg)...); 匹配的参数,所以参数为空的Print是必须写的。
2.C++11还支持更复杂的包扩展:直接将参数包依次展开一次作为实参传给一个函数去处理。
cpp
template<class T>
int getarg(T&& a)
{
cout << a << ' ';
return 0;
}
template<class ...Arg>
void argument(Arg&& ...arg)
{ }
template<class ...Arg>
void show(Arg&&... arg)
{
argument(getarg(forward<Arg>(arg))...);
//getarg(forward<Arg>(arg))...
//就是一个参数列表,必须要有操作来接受它
}
int main()
{
int a = 1;
show(a, string("sah"),3.4);
return 0;
}
在getarg(forward(arg)) 这个整体后加上...就是为了让参数包内的参数依次调用getarg,结果就是获得一个参数列表,必须用argument函数来接收,即使它什么都不做。
3.emplace系列接口
1.C++11后STL容器新增了emplace接口,它们都是可变参数模板,功能上兼具push和insert系列,但也有其他新玩法;
那list的emplace举例:

2.emplace系列总体来说比insert和push系列高效,所以推荐用该系列;
下面代码使用了上文写的my::string:
cpp
int main()
{
list<my::string> lt;
my::string s1("hello");
my::string s2("C++");
//情况1
//传左值它们的结果没有区别,都是要调拷贝构造
cout << "--------------------------" << endl;
lt.push_back(s1);
cout << "--------------------------" << endl;
lt.emplace_back(s2);
cout << endl;
//情况2
//用将亡值结果也没区别,都是要调移动构造
cout << "--------------------------" << endl;
lt.push_back(move(s1));
cout << "--------------------------" << endl;
lt.emplace_back(move(s2));
cout << endl;
//情况3
//用纯右值传就有区别了
cout << "--------------------------" << endl;
lt.push_back("faisugf");
cout << "--------------------------" << endl;
lt.emplace_back("faisugf");
cout << endl;
return 0;
}

情况1、2两者的效率没什么区别,因为由emplace接收左值和将亡值时,其参数类型实例化出于push的一致;需要注意的是情况2中将move(s1/s2)传给插入后会调用移动构造,s1和s2的值将被掠夺,后续要谨慎使用;
情况3就有所不同了,"faisugf" 通过push_back传过去会调用my::string的构造以构造一个临时变量,如何该临时变量会在push_back的逻辑中被掠夺给list的节点;而当它传给emplace_back时,其可变参数会被识别为const char* 类型,它会作为参数包不断地传递下去,最后扩展后只需调用my::stirng地构造,这就比push_back高效了一个移动。
对于深拷贝类型来说,emplace和push系列差别不大,因为深拷贝有移动构造,消耗小;但对于浅拷贝来说,emplace就明显优于push一个拷贝构造 。
模拟实现含emplace的list(只含emplace_back):
cpp
namespace my
{
template<class T>
struct list_node
{
list_node() = default;
//template<class X>
template<class... Arg>
list_node(Arg&&... arg)
:_data(forward<Arg>(arg)...)
, _next(nullptr)
, _prev(nullptr)
{
}
T _data;
list_node* _next;
list_node* _prev;
};
template<class T, class Ref, class Ptr>
struct list_iterator
{
typedef list_node<T> Node;
typedef list_iterator<T, Ref, Ptr> Self;
typedef list_iterator<T, T&, T*> iterator;
typedef list_iterator<T, const T&, const T*> const_iterator;
Node* _node;
list_iterator(Node* node = nullptr)
:_node(node)
{
}
Ref operator*() const
{
return _node->_data;
}
Ptr operator->() const
{
return &(_node->_data);
}
Self& operator++()
{
_node = _node->_next;
return *this;
}
Self& operator--()
{
Self ret(_node);
_node = _node->_prev;
return ret;
}
Self operator++(int)
{
Self ret(_node);
_node = _node->_next;
return ret;
}
Self operator--(int)
{
_node = _node->_prev;
return *this;
}
bool operator!=(const Self& it) const
{
return it._node != _node;
}
bool operator==(const Self& it) const
{
return it._node == _node;
}
};
template<class T>
class list
{
typedef list_node<T> Node;
public:
typedef list_iterator<T, T&, T*> iterator;
typedef list_iterator<T, const T&, const T*> const_iterator;
void Init_empty()//创建头节点
{
_head = new Node;
_head->_prev = _head;
_head->_next = _head;
_size = 0;
}
list()
{
Init_empty();
}
list(const initializer_list<T> il)
{
Init_empty();
for (auto& e : il)
{
push_back(e);
}
}
list(const list& lt)
{
Init_empty();
for (auto& e : lt)
{
push_back(e);
}
}
void clear()
{
auto it = begin();
while (it != end())
{
it = erase(it);
}
}
~list()
{
clear();
delete _head;
_head = nullptr;
_size = 0;
}
void swap(list& lt)
{
std::swap(_head, lt._head);
std::swap(_size, lt._size);
}
list& operator=(list lt)
{
clear();
swap(lt);
return *this;
}
void push_back(const T& val)
{
insert(end(), val);
}
void push_back(T&& val)
{
insert(end(), forward<T>(val));
}
template<class... Arg>
void emplace_back(Arg&&... arg)
{
insert(end(), forward<Arg>(arg)...);
}
void pop_back()
{
erase(--end());
}
void push_front(const T& val)
{
insert(begin(), val);
}
void pos_front()
{
erase(begin());
}
iterator begin()
{
return _head->_next;
}
iterator end()
{
return _head;
}
const_iterator begin() const
{
return _head->_next;
}
const_iterator end() const
{
return _head;
}
template<class... Arg>
iterator insert(iterator pos, Arg&&... arg)
{
Node* newnode = new Node(forward<Arg>(arg)...);
Node* prev = pos._node->_prev;
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = pos._node;
pos._node->_prev = newnode;
_size++;
return newnode;
}
iterator insert(iterator pos, const T& val)
{
Node* newnode = new Node(val);
Node* prev = pos._node->_prev;
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = pos._node;
pos._node->_prev = newnode;
_size++;
return newnode;
}
iterator insert(iterator pos, T&& val)
{
Node* newnode = new Node(forward<T>(val));
Node* prev = pos._node->_prev;
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = pos._node;
pos._node->_prev = newnode;
_size++;
return newnode;
}
iterator erase(iterator pos)
{
assert(pos != _head);
Node* next = pos._node->_next;
Node* prev = pos._node->_prev;
prev->_next = next;
next->_prev = prev;
delete pos._node;
_size--;
return next;
}
private:
Node* _head;
size_t _size;
};
template<class Container>
void Printf_Con(const Container& con)
{
for (auto& e : con)
{
//e++;
cout << e << ' ';
}
cout << endl;
}
}
四. C++11后新的类功能
1.默认的移动构造和移动赋值
1.原来C++中有6个默认的成员函数:构造函数、拷贝构造函数、析构函数、赋值重载、取地址重载、const取地址重载,重要的为前4个;C++11后新增了两个重要的默认函数:移动构造、移动赋值重载;
2.如果你没有写移动构造,并且析构、拷贝构造、赋值重载都没有写,那么编译器就会默认生成移动构造 。默认生成的移动构造对于内置类型会进行逐字节进行拷贝,对于自定义类型,有移动构造就调用它的移动构造,没有就调拷贝构造;
3.如果你没有写移动赋值重载,并且析构、拷贝构造、赋值重载都没有写,那么编译器就会默认生成移动赋值重载 。默认生成的移动赋值重载于内置类型会进行逐字节进行拷贝,对于自定义类型,有移移动赋值重载就调用它的移动赋值重载,没有就调拷贝赋值重载;
4.因为自己提供了析构、拷贝构造、赋值重载这些函数的任意一个就说明该类需要进行深拷贝,而深拷贝需要移动语义提高效率,所以有它们就声明要自己实现深拷贝;
5.如果你提供了移动构造或赋值,则编译器不会默认生成拷贝构造和赋值。
cpp
class Person
{
public:
Person(const char* name = "", int age = 0)
:_name(name)
, _age(age)
{
}
//~Person()
//{}//写了析构就不会生成移动构造和赋值了
//写了拷贝构造和拷贝赋值也一样
private:
my::string _name;
int _age;
};
int main()
{
Person s1;
Person s2 = s1;
Person s3 = std::move(s1);
Person s4;
s4 = std::move(s2);
return 0;
}
没写析构

写了:

2.成员变量声明时给缺省值
cpp
class Person
{
public:
Person(const char* name = "", int age = 0)
:_name(name)
, _age(age)
{
}
private:
my::string _name = new char('/0);
int _age = 10;
};
成员变量初始化给的缺省值是给初始化列表用的,如果初始化列表没有显示初始化,那么该成员变量就会用该缺省值初始化;如果声明时和参数列表中都给了缺省值,那么会优先用参数列表中的。
3.default和delete
有了这两个关键字,你可以在C++中更好的控制默认函数。
1.假设你要使用某个默认函数,但它没有默认生成,你就可以用default强制其生成;如:我们提供了拷贝构造,他就不会默认生成移动构造了,那么我们可以用default强制其生成:
cpp
class Person
{
public:
Person(const char* name = "", int age = 0)
:_name(name)
, _age(age)
{
}
Person(Person&& p) = default;//强制生成移动构造
Person& operator=(Person&& p) = default;//强制生成移动赋值重载
~Person(){}
private:
my::string _name;
int _age = 10;
};
int main()
{
Person s1;
Person s2;
//Person s2 = s1;
Person s3 = std::move(s1);
Person s4;
s4 = std::move(s2);
return 0;
}
这里要注意,强制生成移动构造和移动复制后,拷贝构造和拷贝赋值不在默认生成,也可以用default强制生成。
2.如果想限制某些函数的生成,在C++98中就得将它声明在private中,并且只是声明;C++11后就只需要在该函数后加上=delete即可,该语法指示编译器不在生成该函数的默认版本,称被=delete修饰的函数为删除函数。
4.final和override
final:修饰类,则该类无法被继承;修饰虚函数,则该虚函数无法被重写;
override:检查虚函数是否成功被重写;
详细前面模板和继承的文章有写。
五.STL中的一些变化
1.C++11后STL新增容器array、forward_list、unordered_map、unordered_set,实际上最有用的是unordered_map和unordered_set;
2.新增容器接口不少,重要的有initialize_list版本的构造和emplace系列接口;
3.范围for遍历。
六.lambda
1.lambda的表达式语法
1.lambda表达式本质是一个匿名函数对象,跟普通函数不同的是它可以定义在函数内 ;它在表达式语法使用层而言没有类型,所以我们一般用auto和模板参数定于的对象接收 ;
2.lambda表达式的格式:
cpp
[capture-list] (parameters)-> return type
{ function boby }
3.capture-list :捕获列表,该列表总是出现在lambda开始的位置,编译器通过是否有\[\]来判断接下来是否是lambda ,捕捉列表可以捕捉该lambda函数前的变量供其使用,具体下面讲;
4.(parameters) :参数列表,如普通函数的参数列表类似,如果不需要传参,()可以省略;
5.return type :返回值类型,没有返回值可以省略;有也可以,编译器可以自己推;
6.{ function boby } :函数体,与普通函数的一样函数体内既可以用()内的参数,也可以用捕获列表中的数据。
cpp
int main()
{
int a = 1;
int b = 2;
auto add = [](int x, int y)->int{return x + y;};
//[]里为捕捉列表,()参数列表,->int为返回值类型,
//{}为函数体(不能为空)
auto tmp = [] {return 0; };
//捕捉列表为空也不能省略
//()参数列表为空可以省略
//返回值类型可以省略,可以自动推导
//函数体不可省略
cout << add(5, 6) << endl;
return 0;
}
2.捕获列表
1.lambda中默认只能用lambda函数体内和参数列表中的变量,如果要用外层作用域内的就要捕获;
2.第一种方式是捕获列表中显示的传值捕获和传引用捕获,捕获的多个变量用','分割;
cpp
int main()
{
int a = 1;
double b = 2.3;
string c = "dalkhn";
//传值捕获和传引用捕获
auto f1 = [a, &b, c]
//变量前面加&就相当于传引用捕获
{
//a++;//a是值捕获
b += 1;//b是引用捕获
//c += "oasji";//同a一样
};
f1();
cout << b << endl;
return 0;
}
b是引用捕获,它就相当于外面的b,lambda中的b改变,外面的也会改变;a和c都是值捕获,是外面a和c的拷贝,并被const修饰;
3.第二种方式是在捕捉列表中隐式捕获 ,我们在捕获列表\[\]中写一个=就表示隐式值捕获,&就表示隐式引用捕获,这样我们在lambda中用了哪些变量,他就会捕获那些变量;
cpp
int main()
{
int a = 1;
double b = 2.3;
string c = "dalkhn";
auto f2 = [=](double _b,int _a)
{
/* a++;
b += 1;
c += "daloki";*/
_a += a;
_b += b;
};
//隐式传引用捕获
auto f3 = [&]//就写一个&相当于引用捕获将该lambda前面的所有变量
{
a++;
b += 1;
c += "daloki";
};
r
return 0;
}
4.第三种方式是在捕获列表中混合使用值捕获和引用捕获 ;=,\&x表示其他变量值捕获,x引用捕获;\&,x,y表示其他变量引用捕获,x、y值捕获;当混合使用时要求捕获列表第一个必须为&或=,如果第一个为&,后面就不能引用捕获;为=后面就不能值捕获;
cpp
int main()
{
int a = 1;
double b = 2.3;
string c = "dalkhn";
auto f4 = [=, &a]
{
a++;
//b += 1;
//c += "daloki";
};
auto f5 = [&, a]
{
//a++;
b += 1;
c += "daloki";
};
return 0;
}
5.lambda如果在局部域中可以捕获在它之前的变量,但不能捕获静态局部变量和全局变量,因为它们不捕获lambda内也可以用,这也就意味着如果lambda函数声明在全局,那么其被捕获列表为空(局部变量捕获不到,全局变量不用捕获);
cpp
int A = 10;
int main()
{
int a = 1;
double b = 2.3;
string c = "dalkhn";
static int d = 2;
//auto f5 = [A, d]
// {
// int a = A;
// };
//lambda不能捕获全局变量和静态局部变量
auto f6 = []
{
int a = A;
int D = d;
};
return 0;
}
6.默认情况下,捕获列表中的值捕获会被const修饰,也就是说传值捕捉过来的对象不能修改,但mutable加在参数列表后面可以取消其常性 ,即使用它后,传值捕获过来的对象可以修改了,但修改它影响不到外面的那个对象,因为值捕获过来的只是一个拷贝。使用mutable后,参数列表不能省略(即使没参数)。
cpp
int main()
{
int a = 1;
double b = 2.3;
string c = "dalkhn";
auto f7 = [a, &b, c]() mutable
{
a++;
b += 1;
c += "oasji";
};
return 0;
}
3.捕获列表的应用
在学习lambda前,我们使用的可调用对象只有函数和仿函数,函数指针的类型定义起来比较麻烦,仿函数需要定义一个类,相比起来lambda去定义可调用对象较轻松便利。
cpp
struct goods
{
double price;
double evaluate;
};
int main()
{
vector<goods> v({ { 92,4.5 }, { 87,4.6 }, { 99,5 } });
sort(v.begin(), v.end(), [](goods&& g1, goods&& g2) { return g1.price > g2.price; });
sort(v.begin(), v.end(), [](goods&& g1, goods&& g2) { return g1.evaluate > g2.evaluate; });
//很明显的看出分别是用price和evaluate排序
return 0;
}
4.lambda的原理
1.lambda的原理和范围for类似,编译后从汇编角度看压根没有lambda和范围for这两个东西。范围for的底层是迭代器,lambda的底层是仿函数,也就是说我们写一个lambda后,编译器会自动生成一个对应的仿函数的类;
2.仿函数的类名是编译器按一定规则生成的,保证每个lambda类名生成的不一样,lambda的参数/返回值/函数体就是对应生成的类的operator()的参数/返回值/函数体,捕获列表相当于生成的类的成员变量 ,也就是说捕获列表中的变量都是lambda类构造函数的实参,当然隐式捕捉要看lambda中用了哪些变量。
七.包装器
1.function
1.std::function是类模板也是包装器 ,其实例化对象可以储存其他可调用对象,如函数指针、仿函数、lambda、bind表达式等 。储存的可调用对象被称为std::function的目标。若std::function不含目标,则称它为空,调用空的std::function会抛异常;
2.以下为std::function的原型,其包含于头文件functional中:

3.函数指针、仿函数、lambda中可调用对象的类型各不相同,function的优势就是统一类型,对它们都可以进行包装,这样在很多地方就方便声明可调用对象的类型。
用法:function<返回值类型(参数类型)> 变量名 = 可调用对象。
cpp
#include<functional>
int f(int a, int b)
{
return a + b;
}
struct Functor
{
public:
int operator() (int a, int b)
{
return a + b;
}
};
class Plus
{
public:
Plus(int n = 10)
:_n(n)
{
}
static int plusi(int a, int b)
{
return a + b;
}
double plusd(double a, double b)
{
return (a + b) * _n;
}
private:
int _n;
};
int main()
{
function<int(int, int)> func1 = f;//包装普通函数
cout << f(1, 2) << ' ' << func1(1, 2) << endl;
function<int(int, int)> func2 = Functor();//包装仿函数
cout << Functor()(1, 2) << ' ' << func2(1, 2) << endl;
//function<int(int, int)> func3 = Plus::plusi;
//包装静态成员函数,静态成员本质不属于类,没有this指针
function<int(int, int)> func3 = &Plus::plusi;//这样也可以
cout << Plus().plusi(1,2) << ' ' << func3(1, 2) << endl;
//function<int(int, int)> func4 = Plus::plusd;
//对于非静态成员函数,必须要加&符号,并且参数要多一个this指针
function<int(Plus*,int, int)> func4 = &Plus::plusd;
Plus pl;
cout << pl.plusd(1, 2) << ' ' << func4(&pl,1, 2) << endl;
function<int(Plus, int, int)> func5 = &Plus::plusd;//也可以直接是该类
cout << Plus().plusd(1, 2) << ' ' << func5(Plus(),1, 2) << endl;
function<int(Plus&, int, int)> func6 = &Plus::plusd;//也可以直接是该类左值引用
cout << Plus().plusd(1, 2) << ' ' << func6(pl, 1, 2) << endl;
function<int(Plus&&, int, int)> func7 = &Plus::plusd;//也可以直接是该类右值引用
cout << Plus().plusd(1, 2) << ' ' << func7(Plus(), 1, 2) << endl;
function<int(int, int)> func8 = [](int a, int b) {return a + b; };//包装lambda
cout << func8(1, 3) << endl;
return 0;
}
2.bind(绑定)
1.bind是一个函数模板,也是一个可调用对象的包装器 ,可以把它看作一个函数适配器,对接收的可调用对象进行处理后返回一个可调用对象。它可以用来调整参数顺序和个数。 它也在functional中。
2.调用bind的形式为auto newCallable = bind(callable,arg_list); newCallable 本身是一个可调用对象,arg_list是以逗号分隔的参数列表对应callable。当我们调用newCallable时,它就会调用callable,并把arg_list传给它。
3.arg_list中的可能有形如_n的参数,其中n为正整数,这些参数是占位符 ,表示newCallable中的参数,它们占据了传给newCallable的参数的位置,n表示可调用对象中参数的位置:_1为newCallable中第一个参数的位置,_2为第二个...以此类推,这些占位符放在了名为placeholder的命名空间中。
前提条件:
cpp
int Sub(int a, int b)
{
return (a - b) * 10;
}
int SubX(int a, int b, int c)
{
return (a - b - c) * 10;
}
class Plus
{
public:
Plus(int n = 10)
:_n(n)
{
}
static int plusi(int a, int b)
{
return a + b;
}
double plusd(double a, double b)
{
return (a + b) * _n;
}
private:
int _n;
};
普通用法和调整参数顺序:
cpp
int main()
{
//普通用法
auto f = bind(Functor(), _1, _2);
auto f1 = bind(Sub, _1, _2);
cout << f1(10, 5) << endl;
cout << f(10, 5) << endl;
//改变参数顺序
auto f2 = bind(Sub, _2, _1);
cout << f2(10, 5) << endl;
return 0;
}
_1接收第一个参数,_2第二个...接着会按bind内写的顺序传给Sub。
调整参数个数:
cpp
//改变参数个数
auto f3 = bind(SubX, 100, _1, _2);
cout << f3(10, 5) << endl;
auto f4 = bind(SubX, _1, 100, _2);
cout << f4(10, 5) << endl;
auto f5 = bind(SubX, _1, _2, 100);
cout << f5(10, 5) << endl;
应用:
cpp
//将成员函数进行绑死,其function包装就不需要每次调用都传该类对象了
//没用包装
function<double(Plus*,double, double)> func4 = &Plus::plusd;
Plus pl;
cout << func4(&pl,1.2, 2.3) << endl;
function<double(double, double)> f6 = bind(&Plus::plusd, Plus(), _1, _2);//对于非静态成员函数也要加&
cout << f6(3.4, 5.6) << endl;
//绑死⼀些参数,实现出支持不同年华利率,
//不同金额和不同年份计算出复利的结算利息
auto func = [](double rate, double money, int year)
{
double ret = money;
for (int i = 0; i < year; i++)
{
ret += ret * rate;
}
return ret - money;//返回利息
};
function<double(double)> func5_1_5 = bind(func, 0.015, _1, 5);//给出本金money,算出在利率为0.015的情况下,五年后的利息
function<double(double)> func10_1_5 = bind(func, 0.015, _1, 10);
function<double(double)> func5_2_5 = bind(func, 0.025, _1, 5);
function<double(double)> func10_2_5 = bind(func, 0.025, _1, 10);
cout << func5_1_5(100000) << ' ' << func10_1_5(100000) << ' '
<< func5_2_5(100000) << ' ' << func10_2_5(100000) << ' ';