关键字delete和default
我们现在要定义一个类,要求A的对象不能进行拷贝和赋值。我们先看一下C++98的做法。
class A
{
//要求A的对象不能拷贝和赋值
public:
A(const A& aa);
A& operator=(const A& aa);
//C++98的做法
//只声明 无定义 链接失败 缺点:别人可以在类外面定义
//为了解决这个缺陷 我们用private修饰一下
private:
int _a = 10;
};
class A
{
//要求A的对象不能拷贝和赋值
public:
A() = default; //指定显式生成默认构造函数
//C++11 可以用delete 不生成默认构造函数
A(const A& aa)=delete;
A& operator=(const A& aa)=delete;
A(const int& a)
:_a(a)
{}
//这里写了构造函数,就不会自动生成默认构造函数了
private:
int _a = 10;
};
C++11引进了新的语法来解决这个问题,delete不生成该默认构造函数和default指定显式生成默认构造函数。
关键字decltype
关键字decltype将变量的类型声明为表达式指定的类型,以下为代码示例:
template<class T1, class T2>
void F(T1 t1, T2 t2)
{
decltype(t1 * t2) ret;
cout << typeid(ret).name() << endl;
}
int main()
{
const int x = 1;
double y = 2.2;
decltype(x * y) ret; // ret的类型是double
decltype(&x) p; // p的类型是int*
cout << typeid(ret).name() << endl;
cout << typeid(p).name() << endl;
F(1, 'a');
return 0;
}
运行结果:
右值引用 和移动语义(重点)
C++98就提出了引用的概念,引用就给一个对象取别名
C++98 引出左值引用(主要给左值取别名) C++11 引出右值引用(主要给右值取别名)
不管左值引用,还是右值引用都是给对象取别名
什么是左值?什么是右值?
等号左边就是左值吗?等号右边就算右值吗?
(类似于左移,右移,是向高位或者低位移动 而不是看 左右)
这里的左右不是方向。左边的值不一定是左值,右边的值不一定是右值 可以修改就可以认为是左值,左值通常是变量
右值通常是常量,表达式或者函数返回值(临时对象)
int x1=10;
int x2=x1;
int x3=x1+x2;
//这里x1是左值,10是右值
//x2是左值,x1+x2表达式返回值就是右值
int main()
{
//左值引用
int a = 0;
int& b = a;
int x = 1, y = 2;
//左值引用不能引用右值,const左值引用可以
int& r=19;
int& ret = x + y;
const int& r = 19;
const int& ret = x + y;
//int&& r = 19;
//右值引用不能引用左值,但是可以引用move后的左值
//int&& m = a;
int&& m = move(a);
int&& c = 10;
int&& d = x+y;
return 0;
}
因为const& 左值引用可以引用右值,所有下面的代码如果没有void f(const T&& a)函数,左值右值都会调用void f(const T& a)
template<class T>
void f(const T& a)
{
cout << "void f(const T& a)" << endl;
}
template<class T>
void f(const T&& a)
{
cout << "void f(const T&& a)" << endl;
}
int main()
{
int x = 10;
f(x);//这里会匹配左值引用的参数f
f(10);//这里会匹配右值引用的参数f
return 0;
}
提出右值引用的作用是什么呢?(可以减少深拷贝、提高效率)
C++11又将右值分为纯右值和将亡值。
- 纯右值:基本类型的常量或者是临时对象
- 将亡值:自定义类型的临时对象
- 所有深拷贝类(vector/list/map/set...)都可以调用右值引用的移动拷贝和移动赋值。
- 现实中不可避免存在传值返回的场景,返回的是拷贝返回对象的临时对象,右值可以一定程度上提高函数的值返回的效率。
- 结论:右值引用是为了解决左值引用解决不了的问题的,实现了移动构造和移动赋值,面对接受函数的传值返回对象(右值)等场景,可以提高效率
class String
{
public:
String(const char* str="")
{
_str = new char[strlen(str) + 1];
strcpy(_str, str);
}
//s2(s1)
String(const String& s)
{
cout << "String(const String& s)-深拷贝" << "效率低" << endl;
_str = new char[strlen(s._str) + 1];
strcpy(_str, s._str);
}
String(String&& s)
:_str(nullptr)
{
//传过来一个将亡值
//我的目的是跟你有一样的空间一样的值,不如把将亡值的控制权给我,这样效率高一点
cout << "移动拷贝" <<"效率高"<<endl;
swap(_str, s._str);
}
//s3=s4
//深拷贝赋值
String& operator=(const String& s)
{
cout << "String& operator=(const String& s)-深拷贝赋值" << "效率低" << endl;
if (this != &s)
{
char* newstr = new char[strlen(s._str) + 1];
strcpy(newstr, s._str);
delete[] _str;
_str = newstr;
}
return *this;
}
//s3=s4
//移动赋值
String& operator=(String&& s)
{
cout << "String& operator=(String&& s)-移动赋值" <<"效率高" << endl;
swap(_str, s._str);
return *this;
}
~String()
{
delete[] _str;
}
private:
char* _str;
};
String f(const char* str)
{
String tmp(str);
return tmp;//这里返回实际是拷贝tmp的临时对象
}
int main()
{
String s1("左值");
String s2(s1); //参数是左值
String s3(f("右值"));
// //参数是右值-将亡值(传递之后就发生析构)
String s5(move(s1)); //参数是左值
s3 = s2;
s5 = move(s2);
return 0;
}
总结:左值引用主要用于减少传参和返回值过程中的拷贝,而右值引用则弥补了左值引用的不足,特别是在处理临时对象和移动构造时。右值引用通过利用移动构造和移动赋值,有效减少了拷贝操作,尤其是在函数参数和返回值的处理中。两者相辅相成,共同实现了代码性能的优化。
完美转发
首先我们先介绍一下什么是完美转发?即在传值过程中保持右值的属性不被丢失。我们先来解释一下为什么在传值的过程中右值的属性会丢失,我们先来看下面这段代码。
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; }
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<T>(t)在传参的过程中保持了t的原生类型属性。所有在一些特定的情景可以用forward来保持右值属性。
template<typename T>
void PerfectForward(T&& t)
{
Fun(std::forward<T>(t));
}
总结:STL的是std::move其实是“去引用”的实现,为了将传入参数转换为右值引用,实现引用拷贝。但有时我们也需要保留传入参数的原本类型不变(左值、右值或引用),进行原样转发,std::forward就是为了实现此功能而定义的。
可变参数模板
可变参数的函数模板
template <class ...Args>
void ShowList(Args... args)
{}
上面的参数args前面有省略号,它就是一个可变模版参数,我们把带省略号的参数称为“参数
包”,它里面包含了0到N(N>=0)个模版参数。我们无法直接获取参数包args中的每个参数的,
只能通过展开参数包的方式来获取参数包中的每个参数。
// 递归终止函数
template <class T>
void ShowList(const T& t)
{
cout << t << 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;
}
lambda表达式
lambda表达式:定义在函数内部的匿名函数
lamber表达式的格式:[捕捉列表](参数)-> 返回值类型{函数体} 例如:
auto add = [](int c,int d)->int {return d+c; };
cout << add(a,b);
捕捉列表就是捕捉跟我一个作用域的对象
传值捕捉 [a] [a,b] [=]捕捉同一作用域的所有对象
传引用捕捉 [&a] [&a,&b] [&]捕捉同一作用域的所有对象
auto swap1 = [](int& n1, int& n2)->void{int c = n1; n1 = n2; n2 = c; };
auto swap3 = [a,b]()mutable->void {int c = a; a = b; b = c; };
auto swap2 = [&a,&b]()->void {int c = a; a = b; b = c; };
传值捕捉到对象无法修改(加上mutable就可以),但是还是无法完成交换,因为传值是拷贝出来的临时对象而不是外面的a,b,熟悉lambda表达式的语法能显著提升代码的可读性和编写效率。
vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2,
3 }, { "菠萝", 1.5, 4 } };
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2){
return g1._price < g2._price; });
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2){
return g1._price > g2._price; });
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2){
return g1._evaluate < g2._evaluate; });
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2){
return g1._evaluate > g2._evaluate; });
lambda表达式可以提高代码的可读性和灵活性,避免了使用仿函数时对命名规范的过度依赖,从而使得代码更加直观和易于理解。
仿函数与lambda表达式的对比
函数对象,又称为仿函数,即可以像函数一样使用的对象,就是在类中重载了operator()运算符的
类对象。
class Rate
{
public:
Rate(double rate): _rate(rate)
{}
double operator()(double money, int year)
{ return money * _rate * year;}
private:
double _rate;
};
int main()
{
//仿函数
double rate = 0.49;
Rate r1(rate);
r1(10000, 2);
// lamber
auto r2 = [=](double monty, int year)->double{return monty*rate*year;
};
r2(10000, 2);
return 0;
}
lambda表达式实际上是通过编译器的替换机制(参考范围for和迭代器),将lambda表达式转换为一个具有唯一UUID(通用唯一识别码)的仿函数对象。UUID的使用确保了即使有多个lambda表达式,它们的类名也能保持唯一性,避免命名冲突。通过这种方式,lambda表达式得以像对象一样被调用和使用,其内部实现则依赖于仿函数的Operate()成员函数。