一、统一的列表初始化
C++98的处理方式
在原本的C++98中,已经有了对数组或者结构体的{}初始化方式,他们的使用格式见下方代码:
#include <iostream>
using namespace std;
struct test {
int _x;
int _y;
};
int main() {
int arry[] = { 0,1,2 };
int arry[6] = { 1,2,3 };
test t = { 1,2 };
return 0;
}
其中,对结构体的初始化,是将1和2分别赋值给结构体中的成员,_x和_y。
注意:如果是对class类的对象进行初始化,要求初始化的成员的访问权限为公有。
C++11的处理方式
到了C++11,{}初始化的范围扩大到了所有的内置类型和用户的自定义类型
#include <iostream>
using namespace std;
struct test {
int _x;
int _y;
};
int main() {
//内置类型
int x1 = 1;
int x2{ 1 };
//数组
int arry[] = { 1,2,3 };
int arry[5]{};
//自定义类型
test t{ 5,6 };
return 0;
}
注意:{}进行初始化加不加=都可以,例如上面自定义类型test的初始化还可以写成test t={5,6};
在创建自定义类型对象的时候,也可以通过{}调用有参构造函数,对对象进行初始化
#include <iostream>
using namespace std;
struct Date {
Date(int year,int month,int day)
:_year(year)
,_month(month)
,_day(day)
{
cout << "Date(int year,int month,int day) " << endl;
}
int _year;
int _month;
int _day;
};
int main() {
Date date{ 2024,7,27 };
return 0;
}
运行程序会发现屏幕上打印了"Date(int year,int month,int day) ",说明使用{}进行构造调用了有参构造
二、auto和decltype关键字
auto和decltype用法
auto关键字的作用使用来定义一个变量,编译器能够自动确定变量的类型,当然前提是在定义的时候就进行了初始化,编译器根据初始化所使用量的类型来确定定义变量的类型。
decltype关键字则是将变量的类型声明为制定表达式的类型
#include <iostream>
using namespace std;
int main() {
auto x = 1;
cout << typeid(x).name() << endl;
decltype(1.1 * 5.6) ret;
cout << typeid(ret).name() << endl;
}
最后的结果就是,x的类型显示为为其进行初始化的常量1的类型int,ret的类型显示为表达式(1.1*5.6)运算后的类型double。
使用auto,判断{}的类型
有了auto自动获取类型,再配合typeid()函数,我们就可以知道{}的类型,进而了解一下{}进行初始化的底层原理
#include <iostream>
using namespace std;
int main() {
auto il={ 1,2,3 };
cout << typeid(il).name() << endl;
}
运行代码,结果如下图所示
由此我们可知,{}的类型是一个名叫initializer_list的模板类,使用它进行初始化,其实就是使用它进行构造
三、nullptr
由于c++中的NULL被定义成字面量0,这样就带来了一些问题,因为0既能表示指针常量,又能表示指针常量,所以在C++11中新增了nullptr用来表示空指针。
四、右值引用和移动构造以及移动赋值
什么是右值引用
相信左值引用大家已经很熟悉了,在了解右值引用前,先要知道什么是右值,判断左右值有一个最权威的可以看作定义的方法就是,可以获取它的地址的就是左值,反之则是右值。除此之外左值可以出现在=的左边,而右值不可以。但是要知道左值定义的时候如果加了const也是不可以赋值,但是可以对他进行区地址。有了左右值,对左值的引用就是左值引用,对右值的引用就是右值引用。左右值及其引用的常见例子见下
#include <iostream>
using namespace std;
//double fmin(double x,double y) {
// return x < y ? x : y;
//}
int main() {
//以下的p,b,c,*p都是左值
int* p = new int(0);
int b = 1;
const int c = 2;
//以下几个是对上面左值的左值引用
int*& rp = p;
int& rb = b;
const int& rc = c;
int& pvalue = *p;
double x = 1.1, y = 2.2;
//以下几个都是常见的右值
10;
x + y;
fmin(x,y);
//以下几个都是对右值的右值引用
int&& rr1 = 10;
double&& rr2 = x + y;
double&& rrs=fmin(x, y);
}
注意:1.const左值引用既可以引用左值也可以引用右值
2.右值引用可以引用move后的左值
右值引用的使用场景和意义
在研究右值引用的作用之前,先想一下左值引用的作用,也就是在函数传参和函数传返回值的时候使用来减少拷贝。那么再思考左值引用有没有彻底解决这两个问题?答案是没有,在函数传参部分的问题是解决了,但是传返回值的时候,当返回值如果是函数中的局部变量这种情况没有解决。所以右值引用的价值之一,就是补齐了最后这个模块——传值返回的拷贝问题。
首先先来看不使用引用,函数内返回局部变量的情况,我们以自己模拟实现的string类进行实验,调用to_string函数,内部就是生成了一个局部的string对象,并返回。
由图可知,编译器优化之前,进行了两次拷贝,第一次是将局部string对象,传入寄存器,方便使用,第二次拷贝是函数外,使用寄存器中的string对象,对新创建的string对象ret进行拷贝构造。但是编译器会对两次拷贝构造进行优化,直接使用函数内的局部string对象,对ret进行拷贝构造。
看完之前的做法之后,我们再来看有了右值引用后的做法,首先,我们在string类内,实现一个移动构造函数,什么是移动构造函数呢?移动构造函数就是将构造的参数设置为右值引用,当通过右值或者将亡值(将要被释放的局部变量),进行string的拷贝构造的时候,就会调用该拷贝构造函数,该函数的内部,将局部变量和string的资源直接进行交换,比如类成员是一个指针,指向一段区域,则直接将双方指针指向区域进行交换,新创建的string对象就会掌管将要释放的对象的所管理的区域,进而免去了耗时的拷贝操作。
//移动构造
string(string&& s){
cout<<"string(string&& s)"<<endl;
swap(s);
}
有了以上铺垫,我们来考虑,有了右值引用和移动构造以后,函数内返回局部变量的情况
由上图我们可以看出,编译器优化之前,第一步仍然是将局部对象拷贝构造给寄存器,第二步,由于寄存器里的值为将亡值,所以不再调用普通的构造函数,而是根据重载的特性调用移动构造。经过编译器优化之后,就只需要一次移动构造就完成了。大大减少了拷贝成本。
除了上面说的一种情景外,还有好多移动拷贝应用的例子。比如我们去看C++官网中,list的push_back接口可以发现,C++11中新增了传入参数为右值引用的版本
这一改动,其实就是底层调用了传入参数的移动构造,减少了参数的拷贝。因为要想讲解清楚涉及到要自己模拟实现list,以及模拟实现一个使用拷贝构造的参数,还涉及到万能引用,以及forward保持参数原属性。这些部分讲起来比较复杂,如果有需要可以在评论区留言,再另外单独写一篇文章进行讲解。
移动构造和移动赋值的默认生成
移动构造和移动赋值是C++11新增的,除了自己手动实现外,如果没有自己实现移动构造,并且没有实现析构函数、拷贝构造、拷贝赋值重载中任意的一个。编译器就会自动生成一个默认的移动构造函数,默认生成的移动构造函数,对内置类型成员会按成员进行逐字节拷贝,自定义类型的成员,如果实现了移动构造则调用其移动构造,没有实现则调用其拷贝构造。
此外,就算不满足上述条件也可以通过使用default关键字,强制编译器生成默认函数,以及使用delete关键字,禁止生成默认函数
Person(Person&& p)=default;
Person(Person& p)=delete;
五、lambda表达式
lambda表达式有什么用?
在回答这个问题之前,我们先来看一个C++98,对自定义类型进行排序的例子。
#include <iostream>
#include<vector>
#include<algorithm>
using namespace std;
struct Goods {
string _names;//名字
double _price;//价格
int _evaluate;//评价
Goods(const char* str, double price, int evaluate)
:_names(str)
, _price(price)
, _evaluate(evaluate)
{}
};
struct ComparePriceLess
{
bool operator()(const Goods&gl,const Goods&gr) {
return gl._price < gr._price;
}
};
struct ComparePriceGreater
{
bool operator()(const Goods& gl, const Goods& gr) {
return gl._price > gr._price;
}
};
int main() {
vector<Goods> v = { {"苹果",2.1,5},{"香蕉",3,4},{"橙子",2.2,3},{"菠萝",1.5,4} };
sort(v.begin(), v.end(), ComparePriceLess());//升序
sort(v.begin(), v.end(), ComparePriceGreater());//降序
return 0;
}
代码中对自定义的Goods类,使用algorithm库中的sort函数,对v进行排序,排序需要传入用于比较的仿函数,但是这种写法太复杂了,每次为了实现一个algorithm算法,都要重新实现去写一个类,如果每次比较的逻辑不一样,还要去实现多个类,特别是相同类的命名,因此在C++11中出现了lamdba表达式。
lambda表达式的语法
lambda表达式的书写格式为:[capture-list](parameters)mutable -> return-type {statement}
[capture-list]:捕捉列表,总是放在开始的位置,编译器就是通过他来判断接下来的代码是否是lambda函数,它能够捕捉上下文中的变量,以供lambda使用。
(parameters):参数列表。和普通函数的参数列表一样。如果不需要参数,可以连同()一起省略。
mutable:lambda函数默认具有const属性。加了mutable可以取消其常量性。注意使用该修饰符时,参数列表不可省略(即使参数为空)。
-> return-type:返回值类型。没有返回值类型时可以省略。返回值类型明确时也可以省略,由编译器自动推倒。
{statement}:函数体。可以使用参数和捕捉到的变量
lambda表达式使用的例子
#include <iostream>
using namespace std;
int main() {
int a = 1, b = 2;
auto swap1 = [a, b]()mutable{
int temp=a;
a = b;
b = temp;
};
swap1();
cout << "a=" << a << " b=" << b << endl;
return 0;
}
上述代码就是用lambda表示试,实现的一个交换a,b值的函数,通过捕捉列表将a,b的值进行传入。添加mutable关键字,取消常量性,使得a,b可以修改。可是运行函数函数的结果是a=1 b=2。未完成交换,原因是上述捕捉方式是传值捕捉,要想完成交换需要使用引用捕捉。下面对代码进行修改。
#include <iostream>
using namespace std;
int main() {
int a = 1, b = 2;
auto swap1 = [&a, &b](){
int temp=a;
a = b;
b = temp;
};
swap1();
cout << "a=" << a << " b=" << b << endl;
return 0;
}
我们在a,b之前加上&,就是引用捕捉,捕捉了a,b本身,实现了a,b的交换。除此之外,可以捕捉=,表示普通捕捉lambda实现上方的所有变量,可以捕捉&,表示引用捕捉lambda实现上方的所有变量。甚至还可以混合捕捉,下面再看几个例子。
#include <iostream>
using namespace std;
int main() {
int x=0, y=1;
auto func1 = [x, &y] {};
auto func2 = [=, &y] {
//cout<<m<<endl;//m捕捉不到
cout << x << endl;
};
int m;
return 0;
}
上述两个lambda表达式,都是混合捕捉。其中func1表示,对x进行普通捕捉,对y进行引用捕捉。
func2表示,对所有变量普通捕捉,对y引用捕捉。