目录
一、统一的列表初始化
1.1{}初始化
在C++98中,标准允许使用花括号{}对数组或者结构体元素及进行统一的列表初始化设定
C++11扩大了用大括号扩起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自定义类型使用初始化列表时,可添加等号(=),也可以不添加
一切都可以用列表初始化
int x = 0;
int y{ 1 };
int z = { 2 };
int a[]{ 1,2,3 };
int a1[5]{ 0 };
struct add
{
add(int x, int y)
:_x(x)
,_y(y)
{
}
int _x;
int _y;
};
add p{ 1,2 };
add p1(3, 4);
add p2={ 3,4 };
const add& pp2 = { 3,4 };
add* p3 = new add[2]{ p,p1 };
add* p4 = new add[2]{ {4,5},{6,7} };
add p1(3, 4);
add p2={ 3,4 };
这两个可不一样,上面那个是直接构造,而下面那个类似构造实例化出一个对象,再把这个对象拷贝构造给p2,但是连续的构造和拷贝构造就会被编译器直接优化成构造
vector<int> v = { 1,2,3,4,5,6 };
list<int> l = { 1,2,3,4,5,6 };
那这种列表初始化有不同于上面写的那个,上面那个是固定的参数数量,这个初始化的数量是考验改变的,原因就是C++11引入了一个叫initializer_list(可变参数列表)
1.2initializer_list
其实就是开好一段空间,把你列表里面的值存进去,让start指向你的开头,finish指向你的结尾
auto l1 = { 1,2,3,4,5,6 };
cout << typeid(l1).name() << endl;
那initializer_list是怎么去初始化vector或者list的呢,可以一个一个的插入,也可以迭代器区间去初始化
二、声明
C++11提供了多种简化声明的方式,尤其是在使用模板的时候
2.1auto
在C++98中去偷是一个存储类型的说明符,表明变量是局部存储类型,但是局部域中定义局部的变量默认就是自动存储类型,所以auto就没什么价值了,但是C++11中auto原来的用法,将其用于实现自动类型推断,这样要求必须进行显示初始化,让编译器将定义对象的类型设置为初始化值的类型
int main()
{
int i = 10;
auto p = &i;
vector<pair<string, string>> dict = { {"apple","苹果"},{"banana","香蕉"} };
auto it = dict.begin();
cout << typeid(p).name() << endl;
cout << typeid(it).name() << endl;
}
看识别到这么长的类型还是非常实用的
2.2decltype
typeid只能把类型以字符串的方式打印出来,但是不可以让它作为类型去定义变量
int main()
{
int i = 1;
double j = 1.1;
cout << typeid(i).name() << endl;
cout << typeid(j).name() << endl;
auto ret = i * j;
vector<decltype(ret)> v;
cout << typeid(v).name() << endl;
return 0;
}
如果我们需要用ret的类型去实例化vector
decltype可以推导对象的类型。这个类型是可以用来模板实参,或者再定义对象的
2.3 nullptr
由于C++NuLL被定义成字面量0,这样就可能会带来一些问题,因为0既能指针常量,又能表示整型变量,所以处于清晰和安全的角度考虑,C++11新增了nullptr,用于表示空指针
2.4范围for循环
这个在前面STL文章已经充分讲解了,这里不做太多赘述
2.5智能指针
这个也是一个大模块,所以会针对这个专门写一篇文章
2.6STL的新容器
map和set的哈希表上一篇文章我们已经做了非常细致的讲解
array就是静态数组,至于这个东西有什么用呢,大家都知道[]会对越界访问进行检查,就是有了一个更严格的规范相当于
int main()
{
array<int, 10> arr;
arr[10];
return 0;
}
像这里我们运行一下直接就报错了
但是这个还是很鸡肋的,不如用vector
forward_list就是单链表,只支持头插头删的可以用它
三、右值引用和移动语义
3.1左值引用和右值引用
传统的C++语法中就有引用的语法,而C++11新增了的右值引用语法特性,之前我们了解的引用就叫左值引用。无论是左值引用和右值引用,都是给对象取别名
那什么是左值引用呢
左值是一个表示数据的表达式,我们可以获取它的地址,一般可以对它赋值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。定义const修饰符后的左值,不能给他赋值,但是可以取它 的地址。左值引用就是给左值的引用,给左值取别名
int main()
{
int i = 0;
int j = i;
int* p = &i;
int* d = new int(0);
return 0;
}
能被取地址的就叫左值,左值可以出现在左边也可以出现在右边
什么是右值引用呢
右值也是一个表示数据的表达式,如:表达式返回值,函数返回值等等,右值可以出现在赋值符号的右边,但是不能出现在赋值符号的左边,右值不能取地址,右值引用就是对右值的引用,给右值取别名
10;
i + j;
min(i, j);
像这些都是右值,它们都是临时对象
那左值能否给右值取别名呢
int& r = 10;
const int& r1 = 10;
直接的是不行的,临时常量是不能被修改的,你转为普通对象是权限的放大所以要加const
右值引用可以给右值取别名
int&& r1 = 10;
int&& r2 = i + j;
T&&,这个&&可以看作右值引用符号
右值引用不能给左值取别名,但是右值引用可以给move(左值)取别名
int&& r3 = move(i);
那么为什么要有左值引用和右值引用呢,引用的本质问题都是减少拷贝
string& add(string x, string y)
{
return x+y;
}
int main()
{
string ret ;
ret = add("1", "2");
return 0;
}
像这种情况就不可以用左值引用返回,你x+y是临时变量,临时变量出了作用域就销毁了,自然就不能取别名
但是没有引用以后你x+y要走一个拷贝构造一个临时对象复制一下这个x+y的资源,再把这个临时对象赋值给ret,这期间要用两次深拷贝
有人说用右值引用可不可以,右值引用改变不了这个对象的生命周期,改变不了它出了作用域就销毁的事实,这就有了移动构造和移动赋值
3.2移动构造和移动赋值
C++把右值分为纯右值和将亡值
纯右值 内置类型右值
将亡值 自定义的右值
这个成本是很大的
我们移动构造就是实现一个右值引用的构造,构造一个临时对象指向和你x+y一样的资源,然后我们同样的实现一个右值引用的赋值,让ret也指向这个资源,把ret不要的资源换给临时对象让它一并带走
所以我们之前说的move本质就是把左值变成右值的属性,调用移动赋值减少深拷贝
String(String&& s)
: _data(s._data)
, _size(s._size)
{
s._data= nullptr;
s._size= 0;
}
String& operator=(String&& s) {
if (this != &other)
{
delete[] _data;
_data= s._data;
_size= s._size;
s._data= nullptr;
s._size= 0;
}
return *this;
}
int main()
{
string ret ;
ret = add("1", "2");
string arr=move(ret);
return 0;
}
C++11里面的swap也支持了移动构造和移动赋值
STL的容器在C++11都支持了移动构造和移动赋值
插入也支持了移动构造和移动赋值
注意:右值被右值引用引用以后的属性是左值
int main()
{
int&& r = 10;
int p = r;
cout << p << endl;
}
这是编译器的强制修改
3.3函数模板:万能引用
void Func(int&& x)
{
cout << "右值引用" << endl;
}
void Func(int& x)
{
cout << "左值引用" << endl;
}
template <class T>
void Print(T&& x)
{
Func(x);
}
int main()
{
int a = 0;
Print(a);
Print(std::move(a));
}
void Print(T&& x)
{
Func(x);
}
但是这里的func因为是右值被右值引用所以x变成了左值,如果我们move改一下又全部变成了右值
void Print(T&& x)
{
Func(move(x));
}
void Print(T&& x)
{
Func(forward<T>(x));
}
所以库里面就有了forward这个完美转化,保持实参的属性
如果它不是模板是普通函数就不能左值是左值引用右值是右值引用了
不是模板就只是右值引用了
而且C++类中的默认成员函数也加入了移动构造函数和移动赋值运算符重载
针对移动构造函数和移动赋值运算符重载有一些需要注意的点如下:
如果你没有自己实现移动构造函数,且没有实现析构函数,拷贝构造,拷贝赋值重载(都没写)的任意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造
移动赋值也是跟上面一样
如果你提供了移动构造和移动赋值,编译器不会自动提供拷贝构造和拷贝赋值
看起来很严格实际上也是很合理的,因为你如果自己实现了析构函数,就代表你要完成深拷贝,那默认提供给你的移动构造也是完不成什么东西的
3.4default关键字
class People
{
public:
People(const char* name = "", int age = 0)
:_name(name)
,_age(age)
{
cout << "构造" << endl;
}
People(const People&p)
:_name(p._name)
, _age(p._age)
{
cout << "拷贝构造" << endl;
}
//People& operator=(const People& p)
//{
// if (this != &p)
// {
// _name = p._name;
// _age = p._age;
// }
// cout << "拷贝赋值" << endl;
// return *this;
//
//}
People& operator=(const People& p) = default;
private:
string _name;
int _age;
};
int main()
{
People p("人");
People pp;
pp = p;
return 0;
}
这个default就是让编译器强制生成比如说这个拷贝赋值
3.5delete关键字
People& operator=(const People& p) = delete;
相反的这个delete就是不让它生成
四、可变参数模板
就是可以传入任意多个参数
Args是一个模板参数包,args是一个函数形参参数包
声明一个参数包Args...args,这个参数包可以包含0到任意个模板参数
template<class ...Args>
void List(Args... args)
{
cout << sizeof...(args) << endl;
}
int main()
{
List(1);
List(1,1.1);
List(1,1.1,'x');
}
cout << sizeof...(args) << endl;
这个...是规定
可以计算你的参数包有几个参数
那我们想要解析这个参数包该怎么办呢
void _List()
{
cout << endl;
}
template<class T,class ...Args>
void _List(const T& val, Args... args)
{
cout << val << " ";
_List(args...);
}
template<class ...Args>
void List(Args... args)
{
_List(args...);
}
int main()
{
List(1);
List(1,1.1);
List(1,1.1,'x');
}
这算是一个编译时的递归推演
假设一开始我们有三个参数包,传给list,然后_list会把它分成一个参数包和两个参数包,打印第一个,再分解剩下两个参数包,又会分成一个和另一个,到0个就不会继续往下推了,然后调用换行就结束了
第一个模板参数一次解析后去参数值
template<class T>
int Print(T t)
{
cout << t << " ";
return 0;
}
template<class ...Args>
void List(Args... args)
{
int arr[] = { Print(args)... };
cout << endl;
}
int main()
{
List(1);
List(1,1.1);
List(1,1.1,'x');
}
也就相当于这里你要初始化arr,知道它的大小,你就必须让编译器强行解析参数包,参数包有几个参数,Print就依次推演生成几个
五、lamda表达式
struct Books
{
string _name; // 名字
double _price; // 价格
int _evaluate; // 评价
//...
Books(const char* str, double price, int evaluate)
:_name(str)
, _price(price)
, _evaluate(evaluate)
{
}
};
struct ComparePriceLess
{
bool operator()(const Books& gl, const Books& gr)
{
return gl._price < gr._price;
}
};
struct ComparePriceGreater
{
bool operator()(const Books& gl, const Books& gr)
{
return gl._price > gr._price;
}
};
int main()
{
vector<Books> v = { { "哈利波特", 22.1, 5 }, { "西游记", 30, 4 }, { "红楼梦", 32.2, 3 }, { "三国演义", 41.5, 4 } };
sort(v.begin(), v.end(), ComparePriceLess());
sort(v.begin(), v.end(), ComparePriceGreater());
}
像这种类进行比较的时候我们都要专门写一个仿函数来支持它,但是如果参数过多或者仿函数的名字写的比较令人混淆就会引出很多的问题所以引入了lamda表达式
5.1lambda表达式书写格式:
[capture-list] (parameters) mutable-> return-type{statement}
[capture-list]:捕捉列表。该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。
(parameters):参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略。
mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)
->return-type:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可以省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
{statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。
打×的可以不写,打勾×的可写可不写
auto f=[](int x)->int {cout << x << endl; return 0; };
f(50);
auto f1 = [](int x)
{
cout << x << endl; return 0;
};
f1(20);
这是一个函数对象,我们可以用auto来自动推导它的类型,返回值也可以不写,lamda也会自动推导出它的返回值
因为它整体而言是一个语句所以也要加;
sort(v.begin(), v.end(), [](const Books& b1, const Books& b2)->bool {return b1._price < b2._price; });
我们上面的比较函数就不用写仿函数了,这样也清晰明了,是按价格的升序比较
这里就不用加;上面那个因为他是一条语句的结束,但是这里它是一个对象
auto f=[](int x)->int {cout << x << endl; return 0; };
f(50);
cout << typeid(f).name() << endl;
auto f1 = [](int x){cout << x << endl; return 0;};
cout << typeid(f1).name() << endl;
这个类的名字 <lamda_uuid> uuid是根据算法来的
但是f不能赋值给f1因为它们两个是不同的类型
5.2捕捉列表说明
捕捉列表描述了上下文中那些数据可以被lamda使用,以及使用的方式传值还是传引用
[var]:表示值传递方式捕捉变量var。
[=]:表示值传递方式捕获所有父作用域中的变量(成员函数包括this指针)。
[&var]:表示引用传递捕捉变量var。
[&]:表示引用传递捕捉所有父作用域中的变量(成员函数包括this指针)。
[this]:表示值传递方式捕捉当前的this指针。
注意:
父作用域指的是包含lambda函数的语句块。
语法上捕捉列表可由多个捕捉项组成,并以逗号分割。比如[=, &a, &b]。
捕捉列表不允许变量重复传递,否则会导致编译错误。比如[=, a]重复传递了变量a。
在块作用域以外的lambda函数捕捉列表必须为空,即全局lambda函数的捕捉列表必须为空。
在块作用域中的lambda函数仅能捕捉父作用域中的局部变量,捕捉任何非此作用域或者非局部变量都会导致编译报错。
lambda表达式之间不能相互赋值,即使看起来类型相同。
int x = 1, y = 2;
auto f = [](int& i, int& j)
{
int tmp = i;
i = j;
j = tmp;
};
f(x, y);
我们可以传参过去使用
auto f1 = [x, y]()
{
int tmp = x;
x = y;
y = tmp;
};
f1();
我们捕捉过来的就变成了这个类的成员变量但是成功成员变量是const修饰的const修饰的就不能修改所以我们就要加一个mutable让它变成可变的
x = 0, y = 1;
auto f1 = [x, y]()mutable
{
int tmp = x;
x = y;
y = tmp;
};
f1();
cout << x << " " << y << " " << endl << endl;
但是我们运行以后会发现它们两个根本就没有交换
x = 0, y = 1;
cout << &x << " " << &y << endl;
auto f1 = [x, y]()mutable
{
cout << &x << " " << &y << endl;
int tmp = x;
x = y;
y = tmp;
};
f1();
cout << x << " " << y << " " << endl << endl;
相当于一种传值传参,
x = 0, y = 1;
cout << &x << " " << &y << endl;
auto f1 = [&x, &y]()mutable
{
cout << &x << " " << &y << endl;
int tmp = x;
x = y;
y = tmp;
};
f1();
cout << x << " " << y << " " << endl << endl;
加一个以引用捕捉就好了,拿外面的x,y来初始化自己
这里指出捕捉的变量是自己所处的父作用域
int x = 1, y = 2, z = 3;
auto f2 = [=, &z]()
{
z++;
cout << x << endl;
cout << y << endl;
cout << z<< endl;
};
f2();
=就等于是全部传值捕捉
class A
{
public:
void func()
{
auto f2 = [=]()
{
cout << a1 << endl;
cout << a2 << endl;
};
f2();
}
private:
int a1=1;
int a2=1;
};
这个就是捕捉this指向的成员变量
lamda的底层原理其实是仿函数
六、function包装器和bind
6.1funtion包装器
包装器包装的其实是可调用对象分别是:
函数指针
仿函数
lambda
函数指针的缺点是太复杂,看不懂
仿函数的缺点是太重了写一个比较还要去专门实现一个类
lamda的缺点是无法搞类型,类型是匿名的,不同的时间点不同的编译器类型都是不一样的
//类模板原型如下
template<class T>function;
template<class Ret,class...Args>
class function<Ret(Args...)>;
Ret:被调用函数的返回类型
Args...被调用函数的形参
void swap_func(int& i, int& j)
{
int tmp = i;
i = j;
j = tmp;
}
struct Swap
{
void operator()(int& i, int& j)
{
int tmp = i;
i = j;
j = tmp;
}
};
int main()
{
int x = 1, y = 2;
auto swaplamda = [](int& i, int& j)
{
int tmp = i;
i = j;
j = tmp;
};
function<void(int&, int&)> f1 = swap_func;
cout << x << " " << y << endl << endl;
function<void(int&, int&)> f2 = Swap();
cout << x << " " << y << endl << endl;
function<void(int&, int&)> f3 = swaplamda;
cout << x << " " << y << endl << endl;
}
我们可以对任何可调用对象进行包装
funtion把上面三种比较方法包装成一种统一的类型,现在我们来看一种实用例子
map<string, function<void(int&, int&)>> cmdop = {
{"仿函数",Swap()},
{"函数指针",swap_func},
{"lamda",swaplamda}
};
cmdop["函数指针"](x, y);
cout << x << " " << y << endl << endl;
cmdop["仿函数"](x, y);
cout << x << " " << y << endl << endl;
cmdop["lamda"](x, y);
cout << x << " " << y << endl << endl;
6.2包装成员函数
成员函数取地址比较特殊,要加一个类域和取地址
class Func
{
public:
static int plusi(int a, int b)
{
return a + b;
}
double plusd(double a, double b)
{
return a + b;
}
};
int main()
{
function<int(int, int)> f1 = &Func::plusi;
cout << f1(1, 2) << endl;
function<double(Func*,double, double)> f2 = &Func::plusd;
Func fu;
cout<<f2(&fu, 1.1, 2.2)<<endl;
function<double(Func, double, double)> f3 = &Func::plusd;
cout << f3(Func(), 1.1, 2.2) << endl;
return 0;
}
不是静态的成员函数参数里面都会包含一个隐藏的this指针所以我们要加进去
最后一个是一个是指针去调用里面的成员函数,最后一个是对象去调用里面的成员函数
但是成员函数我们每次都要传第一个成员参数,但是我们能不能不传啊,所以就有了bind
6.3bind
它也在function头文件里面
可以调整顺序也可以调整个数
bind是一个函数模板,它就像一个函数包装器(适配器),接受一个可调用对象,生成一个新的可调用对象来适应原对象的参数列表
调整参数顺序
int Sub(int a, int b)
{
return a - b;
}
int main()
{
function<int(int, int)> f1 = Sub;
cout << f1(5, 2) << endl;
function<int(int, int)> f2 = bind(Sub,placeholders::_2,placeholders::_1);
cout << f2(5, 2) << endl;
return 0;
}
placeholder相当于一个标识符
调整参数个数,有些参数可以bind时写死
function<int(int)> f3 = bind(Sub, 20, placeholders::_1);
cout << f3(5) << endl;
这就能解决我们上面老是要传成员参数的问题了
function<double(double, double)> f4 = bind(&Func::plusd,Func(),placeholders::_1,placeholders::_2);
cout << f4(1.1, 2.2) << endl;
void Print(int a, int b, int c)
{
cout << a << endl;
cout << b << endl;
cout << c << endl;
}
int main()
{
function<void(int, int)> f1 = bind(Print, placeholders::_1, 10, placeholders::_2);
f1(1, 2);
}
也可以bind中间那个参数,这里传的placeholde_1/2r代表的是第一个实参还是第二个实参
希望对大家有所帮助