列表初始化
用法
C++11中,扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型
和用户自定义的类型,使用初始化列表时,可添加等号(=),也可不添加
// 内置类型变量
int a{ 2 };
int b = { 3 };
int c = { a + b };
// 动态数组 C++98不支持
int* arr = new int[5]{ 1,2,3,4,5 };
// 容器使用{}进行初始化
// vector<int> v = { 1,2,3 };
vector<int> v{ 1,2,3 };// 等号可以省略不写
map<int, int> m{ {1,1},{2,2},{3,3} };
initializer_list
initializer_list一般是作为构造函数的参数,C++11对STL中的不少容器就增加initializer_list作为参数的构造函数,这样初始化容器对象就更方便了。也可以作为operator=的参数,这样就可以用大括号赋值对应多个对象的列表初始化,必须支持一个带有initializer_list类型参数的构造函数。
vector<int> v = { 1,2,3,4 };
list<int> lt = { 1,2 };
简单模拟让我们的vector也支持这样的初始化
template<class T>
class Vector
{
public:
Vector(initializer_list<T> l)
:_size(0)
,_capacity(l.size())
{
_a = new T[_capacity];
for (auto e : l)
{
_a[_size++] = e;
}
}
private:
T* _a;
size_t _size;
size_t _capacity;
};
int main()
{
Vector<int> v1 = { 1,2,3 };
return 0;
}
类型推导
auto类型推导这里不做介绍
decltype类型推导
decltype是根据表达式的实际类型推演出定义变量时所用的类型。且还可以使用推导出来的类型进行变量声明。
int main()
{
int a = 10;
int b = 20;
// 用decltype自动推演a+b的实际类型
decltype(a + b) c = 10;
}
右值引用和移动语义
概念
左值引用是&,右值引用是&&
专有名词 | 概念 |
---|---|
左值 | 一个表示数据的表达式,可以取地址和赋值,且左值可以出现在赋值符号的左边,也可以出现在赋值符号的右边,例如:普通变量、指针等,const修饰后的左值不可以赋值,但是可以取地址,所以还是左值 |
左值引用 | 给左值的引用,给左值取别名 ,例如:int& ra = a; |
右值 | 一个表示数据的表达式,右值不能取地址,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边如:字面常量、表达式返回值,函数返回值(这个不能是左值引用返回)等等 |
右值引用 | 给右值的引用,给右值取别名,例如:int&& ra = Add(a,b) |
int Add(int x, int y)
{
return x + y;
}
int main()
{
int a = 10;// 左值,可以取地址
int& ra = a;// 左值引用
int&& ret = Add(3, 4);// 函数的返回值是一个临时变量,是一个右值
return 0;
}
右值分为:
- 纯右值:基本类型的常量或临时对象,如:a+b,字面常量
- 将亡值:自定义类型的临时对象用完自动完成析构,如:函数以值的方式返回一个对象
左值引用与右值引用比较
左值引用总结:
-
左值引用只能引用左值,不能引用右值。
-
但是const左值引用既可引用左值,也可引用右值
int main()
{
// 左值引用只能引用左值,不能引用右值。
int a = 10;
int& ra1 = a; // ra为a的别名
//int& ra2 = 10; // 编译失败,因为10是右值
// const左值引用既可引用左值,也可引用右值。
const int& ra3 = 10;
const int& ra4 = a;
return 0; }
右值引用总结:
- 右值引用只能右值,不能引用左值。
- 但是右值引用可以move以后的左值。
int main()
{
// 右值引用只能右值,不能引用左值。
int&& r1 = 10;
// error C2440: “初始化”: 无法从“int”转换为“int &&”
// message : 无法将左值绑定到右值引用
int a = 10;
int&& r2 = a;
// 右值引用可以引用move以后的左值
int&& r3 = std::move(a);
return 0;
}
移动语义
**移动语义:**将一个对象中资源移动到另一个对象中的方式,可以有效减少拷贝,减少资源浪费,提供效率。
先看看我们之前的代码,以下会执行几次拷贝构造?
class String
{
public:
String(const char* str = "")
:_str(new char[strlen(str) + 1])
,_size(strlen(str)+1)
{
strcpy(_str, str);
_str[_size] = '\0';
}
String(const String& s)
:_str(new char[strlen(s._str) + 1])
, _size(s._size)
{
cout << "深拷贝" << endl;
strcpy(_str, s._str);
}
String& operator=(String& s)
{
if (this != &s)
{
cout << "深拷贝" << endl;
delete _str;
_str = new char[strlen(s._str) + 1];
strcpy(_str, s._str);
_size = s._size;
_str[_size] = '\0';
}
return *this;
}
~String()
{
delete _str;
}
private:
char* _str;
size_t _size;
};
String func(String& str)
{
String tmp(str);
return tmp;
}
int main()
{
String s1("123");//
String s2(s1);
String s3(func(s1));
return 0;
}
解答:深拷贝 深拷贝 深拷贝
第一次深拷贝是因为s1拷贝构造s2,这里都不难理解。主要看后两次,s1传参过程不发生深拷贝,因为这里是传引用,接着就是str拷贝构造tmp,这里会发生一次深拷贝,紧接着就是返回tmp,tmp会先拷贝构造一个临时对象(这里会发生一次深拷贝),然后临时对象拷贝构造给s3(这里会发生一次深拷贝),连续两次拷贝构造会被编译器优化成一次,所以执行两次深拷贝。
问题:
在上面的代码中,可以发现,func中的tmp、返回是构造的临时对象和s3都有一个独立的空间,且内容是相同的,这里相当于创建了3个内容完全相同的对象,这是一种极大的浪费,且效率也会降低。
解决方法:
使用移动语义即可解决
String(String&& s)
:_str(s._str)
{
// 对于将亡值,内部做移动拷贝
cout << "移动拷贝" << endl;
s._str = nullptr;
}
运行结果为:深拷贝 深拷贝 移动拷贝
因为返回的临时对象是一个右值,所以会调用上面的移动构造的代码对返回的临时对象进行构造,本质是资源进行转移,此时tmp指向的是一块空的资源。最后返回的临时对象拷贝构造给s3时还是调用了移动构造,两次移动构造被编译器优化为一个。可以看出的是这里解决的是减少接受函数返回对象时带来的拷贝,极大地提高了效率。
我们也可以自己编写一个移动赋值,代码如下:
String& operator=(String&& s)
{
cout << "移动赋值" << endl;
_str = s._str;
_size = s._size;
s._str = nullptr;
return *this;
}
运行结果为:深拷贝 移动拷贝 移动赋值
func返回的临时对象是通过移动赋值给s2的,也是一次资源的转义
注意:
- 移动构造和移动赋值函数的参数千万不能设置成const类型的右值引用,因为资源无法转移而导致移动语义失效。
- 在C++11中,编译器会为类默认生成一个移动构造和移动赋值,该移动构造和移动赋值为浅拷贝,因此当类中涉及到资源管理时,用户必须显式定义自己的移动构造和移动赋值。
右值引用和左值引用减少拷贝的场景:
作参数时: 左值引用减少传参过程中的拷贝。右值引用解决的是传参后,函数内部的拷贝构造
作返回值时: 如果出了函数作用域,对象还存在,那么可以使用左值引用减少拷贝。如果不存在,那么产生的临时对象可以通过右值引用提供的移动构造生成,然后通过移动赋值或移动构造的方式将临时对象的资源给接受返回值者
move
当需要用右值引用引用一个左值时,可以通过move来获得绑定到左值上的右值引⽤。C++11中,std::move()函数位于 头文件中,该函数名字具有迷惑性,它并不搬移任何东西,唯一的功能就是将一个左值强制转化为右值引用,然后实现移动语义。
例子:使用move
int main()
{
String s1("123");
String s2(move(s1));
return 0;
}
运行结果为:移动拷贝
如果我们查看的话会发现s1的资源被转移给了s2,s1没有资源了
完美转发和万能引用
- **完美转发:**是指在函数模板中,完全依照模板的参数的类型,将参数传递给函数模板中调用的另外一个函数。
- **万能引用:**模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。
问题: 右值引用的对象再作为实参传递时,属性会退化为左值,只能匹配左值引用
代码如下:
void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }
// std::forward<T>(t)在传参的过程中保持了t的原生类型属性。
template<typename T>
void PerfectForward(T&& t)
{
Fun(t);
}
int main()
{
PerfectForward(10); // 右值
int a;
PerfectForward(a); // 左值
PerfectForward(std::move(a)); // 右值
const int b = 8;
PerfectForward(b); // const 左值
PerfectForward(std::move(b)); // const 右值
return 0;
}
运行结果如下:
std::forward 完美转发在传参的过程中保留对象原生类型属性
Fun(std::forward<T>(t));
运行结果如下:
类的新功能
C98中有6个默认成员函数
- 构造函数
- 析构函数
- 拷贝构造函数
- 拷贝赋值重载
- 取地址重载
- const 取地址重载
在C++11中由新增了两个默认成员函数:
- 移动构造函数
- 移动赋值运算符重载
它们生成默认移动构造和移动复制有一些准则
- 如果没有实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个(意思是只要实现一个,就不能生成默认移动构造)。那么编译器会自动生成一个默认移动构造。
默认生成的移动构造函数,对于内置类型成员会按照字节序进行浅拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。 - 如果没有实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。
默认生成的移动构造函数,对于内置类型成员会按照字节序进行浅拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。
强制生成默认函数的关键字default
C++11可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原
因这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以
使用default关键字显示指定移动构造生成。
namespace wzh
{
class string
{
public:
string(const char* str = "")
:_size(strlen(str))
, _capacity(_size)
{
//cout << "string(char* str)" << endl;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
// s1.swap(s2)
void swap(string& s)
{
::swap(_str, s._str);
::swap(_size, s._size);
::swap(_capacity, s._capacity);
}
// 拷贝构造
string(const string& s)
:_str(nullptr)
{
cout << "string(const string& s) -- 深拷贝" << endl;
string tmp(s._str);
swap(tmp);
}
// 赋值重载
string& operator=(const string& s)
{
cout << "string& operator=(string s) -- 深拷贝" << endl;
string tmp(s);
swap(tmp);
return *this;
}
// 移动构造
string(string&& s)
:_str(nullptr)
, _size(0)
, _capacity(0)
{
cout << "string(string&& s) -- 移动构造" << endl;
swap(s);
}
// 移动赋值
string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 移动赋值" << endl;
swap(s);
return *this;
}
~string()
{
delete[] _str;
_str = nullptr;
}
private:
char* _str;
size_t _size;
size_t _capacity; // 不包含最后做标识的\0
};
}
class Person
{
public:
Person(const char* name = "", int age = 0)
:_name(name)
, _age(age)
{}
Person(const Person& p)
:_name(p._name)
,_age(p._age)
{}
Person(Person&& p) = default;
private:
//如果把这个注释掉,只会深拷贝,但如果写这个则会生成默认移动构造,然后会调用string里面的移动构造
wzh::string _name;
int _age;
};
int main()
{
Person s1;
Person s2 = s1;
Person s3 = std::move(s1);
return 0;
}
禁止生成默认函数的关键字delete
如果能想要限制某些默认函数的生成,在C++98中,是该函数设置成private,并且只声明补丁
已,这样只要其他人想要调用就会报错。在C++11中更简单,只需在该函数声明加上=delete即
可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。
class Person
{
public:
Person(const char* name = "", int age = 0)
:_name(name)
, _age(age)
{}
Person(const Person& p) = delete;
private:
wzh::string _name;
int _age;
};
int main()
{
Person s1;
Person s2 = s1;
Person s3 = std::move(s1);
return 0;
}
这段代码会报错,因为不会默认生成拷贝构造,而我们这里又用了拷贝构造
可变参数模版
您创建可以接受可变参数的函数模板和类模板,相比C++98/03,类模版和函数模版中只能含固定数量的模版参数,可变模版参数无疑是一个巨大的改进
实际在底层编译器对于lambda表达式的处理方式,完全就是按照函数对象的方式处理的,即:如
果定义了一个lambda表达式,编译器会自动生成一个类,在该类中重载了operator()。
语法如下:
template <class ...Args>
void fun(Args ...args)
{}
递归函数方式展开参数包
// 递归终止函数
void ShowList()
{
cout << endl;
}
// 展开函数
template <class T, class ...Args>
void ShowList(T value, Args ...args)
{
cout << value << " ";
ShowList(args...);
}
int main()
{
ShowList(1);
ShowList(1, 'A');
ShowList(1, 'A', std::string("sort"));
return 0;
}
逗号表达式展开参数包
template<class T>
void PrintArg(T value)
{
cout << value << " ";
}
template<class ...Args>
void ShowList(Args ...args)
{
int arr[] = { (PrintArg(args), 0)... };
cout << endl;
}
lambda表达式——是匿名函数对象
lambda表达式书写格式:[capture-list] (parameters) mutable -> return-type { statement}
- [capture-list] : 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来
判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda
函数使用。(必须写) - (parameters):参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以
连同()一起省略(不是必须写的) - mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量
性。使用该修饰符时,参数列表不可省略(即使参数为空)。(一般不需要) - ->returntype:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回
值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推
导。(不是必须写) - {statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获
到的变量。(必须写)
例子:
struct Goods
{
public:
Goods(string name,double price)
:_name(name)
,_price(price)
{}
string _name;
double _price;
};
struct ComparePriceless
{
bool operator()(const Goods& g1,const Goods& g2)
{
return g1._price<g2._price;
}
};
int main()
{
vector<Goods> v={{"香蕉",3},{"苹果",5},{"苹果",1},{"香蕉",10}};
// sort(v.begin(),v.end(),ComparePriceless());
sort(v.begin(),v.end(),[](const Goods& g1,const Goods& g2){return g1._price<g2._price;});
for(auto e : v)
{
cout << e._name << e._price << endl;
}
}
之前我们都是使用注释的方法写的,但是我们现在可以使用lambda表达式去写
lambda还有各种用法
例子1:
int main()
{
//必须使用auto来接受!
//->bool可以加也可以不加
//[](int x,int y)->bool{return x<y;}是一个匿名对象,赋值给compare(对象)
auto compare=[](int x,int y)->bool{return x<y;};
cout << compare(1,2) << endl;
}
运行结果为1
例子:
int main()
{
int a=1,b=2;
//可以在捕捉外面的对象,捕捉列表里面可以写多个
auto add=[b](int x){return x+b;};
cout << add(a);
}
运行结果为3
例子:
int main()
{
int a=1,b=2;
//默认具有常性,使用mutable可以取消常性
auto swap1=[a,b]()mutable
{
int tmp=a;
a=b;
b=tmp;
};
swap1();
cout << a << " " << b << endl;
}
这样输出的话并不会交换a和b,想要正确交换就必须写成
auto swap1=[&a,&b]()mutable
{
int tmp=a;
a=b;
b=tmp;
};
例子:
int main()
{
int a=1,b=2;
//混合捕捉
//向上的父作用域都可以获得,但是对b进行引用获取
auto swap1=[=,&b]()
{
b=20;
};
}