初始化列表
如何使用
在Cpp11中允许使用初始化列表初始化任何类型,不论是内置类型还是自定义类型都可以使用初始化列表进行初始化,而在Cpp98的版本中是不能初始化自定义类型的。
#include <iostream>
#include <vector>
class Test
{
public:
Test(int a, int b)
:_a(a)
,_b(b)
{
}
void Print()
{
std::cout << _a << " " << _b << std::endl;
}
private:
int _a;
int _b;
};
int main()
{
//内置类型的初始化列表初始化
int a = {10};
int b = {3 + 4};
std::cout << a << " " << b << std::endl;
int arr[] = {1, 2, 3, 4};
for(int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
{
std::cout << arr[i] << " ";
}
std::cout << std::endl;
//自定义类型初始化列表初始化
std::vector<int> arr2 = {4, 3, 2, 1};
for(auto e : arr2)
{
std::cout << e << " ";
}
std::cout << std::endl;
Test test = {7, 8};
test.Print();
}
10 7
1 2 3 4
4 3 2 1
7 8
initializer_list
但是自定义类型想要支持像vector这样的初始化列表并不是天然就支持的,而是在Cpp11中新增了一个容器叫initializer_list,初始化列表,借助这个容器我们可以实现vector这样的初始化。
#include <iostream>
#include <vector>
class Test
{
public:
Test(int a, int b)
:_a(a)
,_b(b)
{
}
void Print()
{
std::cout << _a << " " << _b << std::endl;
}
private:
int _a;
int _b;
};
template<class T>
class Vector
{
public:
Vector(size_t n = 0)
:_start(new T[n])
,_finish(_start + n)
,_endOfStorge(_start + n)
{
}
//我们想要使用初始化列表初始化vector多亏下面这样的构造函数
Vector(const std::initializer_list<T>& list)//初始化列表容器
:_start(new T[list.size()])
,_finish(_start + list.size())
,_endOfStorge(_start + list.size())
{
//std::initializer_list容器有三个公有接口,start(), end()提供遍历,size()提供大小
int index = 0;
for(auto e : list)
{
_start[index] = e;
index++;
}
}
void Print()
{
T* start = _start;
while(start != _finish)
{
std::cout << *start << " ";
start++;
}
std::cout << std::endl;
}
private:
T* _start;
T* _finish;
T* _endOfStorge;
};
int main()
{
Vector<int> vec = {1, 2, 3, 4, 5};
vec.Print();
}
1 2 3 4 5
因此如果我们在自己今后写自定义类型时,想要在初始化时利用初始化列表进行不定长参数的初始化时,就可以借助initializer_list来实现。
如果我们想让一个自定义类型不再支持初始化列表进行初始化我们也可以通过加explicit关键字来禁用。
变量类型推导
变量类型推导其中的典型代表就是我们一直在使用的auto关键字,它可以帮助我们简化代码书写,一些很复杂的类型一个auto即可代替,但是auto是编译时类型识别,除此之外还有一个关键字这里要提一下,即decltype,这个关键字我们之前并没有使用过,不过这个关键字是RTTI的,它可以在运行时进行类型识别。
auto和decltype之间最显著的差别就是,auto在编译时就将变量类型确定下来,因此如果我们声明一个变量而不去定义它那么编译器此时就无法识别它的变量类型,因此这是不合法的书写方式,但是decltype却有办法去识别类型。
#include <iostream>
int fun()
{
return 10;
}
int main()
{
//auto a;//这是不合法的,因为编译器此时无法判断它的类型
decltype(3 + 1) b;//decltype可以通过推导括号中表达式的类型来定义变量
std::cout << typeid(b).name() << std::endl;//typeid可以识别变量类型,它也以RTTI的思想来实现的
decltype(fun()) c;
std::cout << typeid(c).name() << std::endl;//typeid可以识别变量类型,它也以RTTI的思想来实现的
}
i
i
委派构造函数
委派构造函数即在一个类的构造函数中可以调用这个类的其它构造函数。Cpp11之前是不允许这样的语法的,但是在Cpp11中加入了这样的新特性,但是却也带来了问题,就像是下面这样的情况。
#include <iostream>
class A
{
public:
A()
:A(10, 20)//无参构造中调用带参构造
{
}
A(int a, int b)
:A()//带参构造中调用无参构造
{
_a = a;
_b = b;
}
private:
int _a;
int _b;
};
int main()
{
A a;
std::cout << "finish" << std::endl;//这里调用就会出现死递归调用,从而崩掉
}
**崩溃**
默认函数控制
默认函数控制可以帮助我们很好的管理一个类中的默认成员函数,控制其是否应该自动生成。我们之前想要禁用拷贝构造,赋值运算符重载往往是将他们的声明放在private中,但是在Cpp11中我们可以这样写。
#include <iostream>
class A
{
public:
A(int a)
:_a(a)
{
}
A(const A& a) = delete;
A& operator=(const A& a) = delete;
private:
int _a;
};
int main()
{
A a(1);
//A b(a);//禁用拷贝
}
同时我们也可也让编译器自动帮我们生成默认构造函数。
#include <iostream>
class A
{
public:
A(int a)
:_a(a)
{
}
A() = default;//生成默认构造
A(const A& a) = delete;
A& operator=(const A& a) = delete;
private:
int _a;
};
int main()
{
A a(1);
A b;//合法
//A b(a);//禁用拷贝
}
右值引用
左值和右值
什么是右值呢?在C语言中就有左值和右值的概念,这里简单总结下可以理解为:
1、左值就是可以出现在复制运算符左右两边的值,左值往往是可以取地址的。
2、右值就是只可以出现在赋值运算符右边的值,右值往往不可以取地址。
常见的右值有常量,临时变量和将亡值(即将销毁的值)。
左值引用和右值引用
我们之前所使用的引用都是左值引用,左值引用既可以引用左值,也可也引用右值,因为我们可以使用指向常量的引用去引用常量,例如const int& ra = 10;这条语句是合法的。
而右值引用只可以引用右值。例如int&& rra = 10;,此时不需要加const就可以直接引用右值,这就是一条典型的右值引用。,当然右值引用也可引用临时变量和将亡值。
int fun(int a)
{
return a;
}
int main()
{
int a = 10;
fun(a);
const int & ra = fun(a);//这里fun(a)返回的是一个临时变量,我们不可以直接使用引用指向它,但是左值引用加上const就可以指向右值
int&& rra = fun(a);//但是我们如果使用右值引用就可以直接引用临时变量
}
移动语义
如果我想要右值引用去引用左值可以么?在Cpp中移动语义可以帮助我们完成将一个左值变为右值,从而可以让右值引用去引用它。我们可以通过调用std::move()完成移动语义。
#include <iostream>
int main()
{
int a = 10;
int && ra = std::move(a);//移动语义,将左值改为右值
const int& rra = std::move(a);
}
移动构造
既然左值引用既可以引用左值也可也引用右值,那还要右值引用有什么用呢?我们先看一下这个例子。
#include <iostream>
#include <string.h>
//实现一个简单的string类
class String
{
friend std::ostream& operator<<(std::ostream& out, const String& str);
public:
String(const char* str = "")
:_str(new char[strlen(str) + 1])
{
strcpy(_str, str);
std::cout << "String(char* str = )" << std::endl;
}
String(const String& s)
:_str(new char[strlen(s._str) + 1])
{
strcpy(_str, s._str);
std::cout << "String(const String& s)" << std::endl;
}
String& operator=(const String& s)
{
if (this != &s)
{
char* temp = _str;
_str = new char[strlen(s._str) + 1];
strcpy(_str, s._str);
delete[] temp;
}
return *this;
}
~String()
{
delete[] _str;
}
private:
char* _str;
};
std::ostream& operator<<(std::ostream& out, const String& str)
{
out << str._str;
return out;
}
String getString(const char* str)
{
String temp(str);
return temp;
}
void test()
{
String str = getString("lieng");
std::cout << str << std::endl;
}
int main()
{
test();
}
String(char* str = )
String(const String& s)
lieng
在这个例子中其实会生成三个对象。首先有参构造一个,然后由于我们的函数返回值返回对象,于是拷贝构造临时对象,然后再用临时对象拷贝构造最终我们需要的str对象,于是会发生依次构造,两次拷贝构造,但是由于编译器的优化会帮我们减少为一次构造一次拷贝构造(如果是更厉害的编译器会直接优化成一次构造,比如MinGW,为此为了演示我换上了vs),但是仍然有多余的损耗,因为我们为了构造str而构造了temp,temp构造后是一个将亡值,随后很快会进行释放,紧接着我们用它构造str又开辟了新的空间,为了构造str我们先释放了temp的空间又重新为str开辟了空间,尽管他们空间中的内容应该是一样的。所以这是多次一举的行为,十分低效。
那么有没有一种办法可以让我们的str直接利用temp开辟好的空间,而不再多此一举自己重新开辟空间呢?这样的做法明显是更加高效的,答案是有的。在Cpp11中由于右值引用的出现,于是出现了移动构造,即利用右值引用进行构造。这里右值引用的多是一个将亡值,即即将释放资源的值,既然他们的资源即将要释放,而我们构造新对象所需要的资源刚好和他们要释放的资源一样,那不如直接把他们的资源拿来用。
#include <iostream>
#include <string.h>
//实现一个简单的string类
class String
{
friend std::ostream& operator<<(std::ostream& out, const String& str);
public:
String(const char* str = "")
:_str(new char[strlen(str) + 1])
{
strcpy(_str, str);
std::cout << "String(char* str = )" << std::endl;
}
String(const String& s)
:_str(new char[strlen(s._str) + 1])
{
strcpy(_str, s._str);
std::cout << "String(const String& s)" << std::endl;
}
//移动构造
String(String&& s)
:_str(s._str)
{
s._str = nullptr;//这里一定要记着将将亡值的原本指针置空,否则会把我们拿来用的资源释放了
std::cout << "String(String&& s)" << std::endl;
}
String& operator=(const String& s)
{
if (this != &s)
{
char* temp = _str;
_str = new char[strlen(s._str) + 1];
strcpy(_str, s._str);
delete[] temp;
}
return *this;
}
~String()
{
delete[] _str;
}
private:
char* _str;
};
std::ostream& operator<<(std::ostream& out, const String& str)
{
out << str._str;
return out;
}
String getString(const char* str)
{
String temp(str);
return temp;
}
void test()
{
String str = getString("lieng");
std::cout << str << std::endl;
}
int main()
{
test();
}
String(char* str = )
String(String&& s)
lieng
这里编译器判断temp是一个右值,自动调用了移动构造,移动构造中所做的事情就是将即将释放的资源直接拿来给新创建的对象使用,于是省掉了一次先释放空间再申请空间的过程。移动构造可以提升代码效率,减少拷贝。
移动赋值
有移动构造就有移动赋值。
#include <iostream>
#include <string.h>
//实现一个简单的string类
class String
{
friend std::ostream& operator<<(std::ostream& out, const String& str);
public:
String(const char* str = "")
:_str(new char[strlen(str) + 1])
{
strcpy(_str, str);
std::cout << "String(char* str = )" << std::endl;
}
String(const String& s)
:_str(new char[strlen(s._str) + 1])
{
strcpy(_str, s._str);
std::cout << "String(const String& s)" << std::endl;
}
//移动构造
String(String&& s)
:_str(s._str)
{
s._str = nullptr;//这里一定要记着将将亡值的原本指针置空,否则会把我们拿来用的资源释放了
std::cout << "String(String&& s)" << std::endl;
}
String& operator=(const String& s)
{
if (this != &s)
{
char* temp = _str;
_str = new char[strlen(s._str) + 1];
strcpy(_str, s._str);
delete[] temp;
}
return *this;
}
//移动赋值
String& operator=(String&& s)
{
if (this != &s)
{
char* temp = _str;
_str = s._str;
s._str = temp;
std::cout << "String& operator=(String&&)" << std::endl;
}
return *this;
}
~String()
{
delete[] _str;
}
private:
char* _str;
};
std::ostream& operator<<(std::ostream& out, const String& str)
{
out << str._str;
return out;
}
String getString(const char* str)
{
String temp(str);
return temp;
}
void test()
{
String str = getString("lieng");
str = String("misaki");
std::cout << str << std::endl;
}
int main()
{
test();
}
String(char* str = )
String(String&& s)
String(char* str = )
String& operator=(String&&)
lieng
我们在写移动构造和移动赋值的时候要注意如果一个类中有自定义类型成员,我们也应该让他们进行移动构造或移动赋值,可以通过移动语义将内部成员从左值转换为右值从而调用他们的移动构造或移动赋值。