C++11新特性

关键字delete和default

我们现在要定义一个类,要求A的对象不能进行拷贝和赋值。我们先看一下C++98的做法。

cpp 复制代码
class A
{
//要求A的对象不能拷贝和赋值
public:
	A(const A& aa);
	A& operator=(const A& aa);
	//C++98的做法
	//只声明 无定义 链接失败 缺点:别人可以在类外面定义
	//为了解决这个缺陷 我们用private修饰一下
private:
	int _a = 10;
};
cpp 复制代码
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将变量的类型声明为表达式指定的类型,以下为代码示例:

cpp 复制代码
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 引出右值引用(主要给右值取别名)

不管左值引用,还是右值引用都是给对象取别名

什么是左值?什么是右值?

等号左边就是左值吗?等号右边就算右值吗?

(类似于左移,右移,是向高位或者低位移动 而不是看 左右)

这里的左右不是方向。左边的值不一定是左值,右边的值不一定是右值 可以修改就可以认为是左值,左值通常是变量

右值通常是常量,表达式或者函数返回值(临时对象)

cpp 复制代码
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)

cpp 复制代码
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...)都可以调用右值引用的移动拷贝和移动赋值。
  • 现实中不可避免存在传值返回的场景,返回的是拷贝返回对象的临时对象,右值可以一定程度上提高函数的值返回的效率。
  • 结论:右值引用是为了解决左值引用解决不了的问题的,实现了移动构造和移动赋值,面对接受函数的传值返回对象(右值)等场景,可以提高效率
cpp 复制代码
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;
}

总结:左值引用主要用于减少传参和返回值过程中的拷贝,而右值引用则弥补了左值引用的不足,特别是在处理临时对象和移动构造时。右值引用通过利用移动构造和移动赋值,有效减少了拷贝操作,尤其是在函数参数和返回值的处理中。两者相辅相成,共同实现了代码性能的优化。

完美转发

首先我们先介绍一下什么是完美转发?即在传值过程中保持右值的属性不被丢失。我们先来解释一下为什么在传值的过程中右值的属性会丢失,我们先来看下面这段代码。

cpp 复制代码
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来保持右值属性。

cpp 复制代码
template<typename T>
void PerfectForward(T&& t)
{
Fun(std::forward<T>(t));
}

总结:STL的是std::move其实是"去引用"的实现,为了将传入参数转换为右值引用,实现引用拷贝。但有时我们也需要保留传入参数的原本类型不变(左值、右值或引用),进行原样转发,std::forward就是为了实现此功能而定义的。

可变参数模板

可变参数的函数模板

cpp 复制代码
template <class ...Args>
void ShowList(Args... args)
{}

上面的参数args前面有省略号,它就是一个可变模版参数,我们把带省略号的参数称为"参数

包",它里面包含了0到N(N>=0)个模版参数。我们无法直接获取参数包args中的每个参数的,

只能通过展开参数包的方式来获取参数包中的每个参数。

cpp 复制代码
// 递归终止函数
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表达式的格式:[捕捉列表](参数)-> 返回值类型{函数体} 例如:

cpp 复制代码
auto add = [](int c,int d)->int {return d+c; };
cout << add(a,b);

捕捉列表就是捕捉跟我一个作用域的对象

传值捕捉 [a] [a,b] [=]捕捉同一作用域的所有对象

传引用捕捉 [&a] [&a,&b] [&]捕捉同一作用域的所有对象

cpp 复制代码
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表达式的语法能显著提升代码的可读性和编写效率。

cpp 复制代码
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()运算符的

类对象。

cpp 复制代码
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()成员函数。

相关推荐
Allen Bright7 分钟前
【Java基础-46.3】Java泛型通配符详解:解锁类型安全的灵活编程
java·开发语言
JANGHIGH13 分钟前
c++ std::list使用笔记
c++·笔记·list
画个逗号给明天"20 分钟前
C++STL容器之list
开发语言·c++
hrrrrb1 小时前
【Java】Java 常用核心类篇 —— 时间-日期API(上)
java·开发语言
小突突突1 小时前
模拟实现Java中的计时器
java·开发语言·后端·java-ee
七禾页话1 小时前
垃圾回收知识点
java·开发语言·jvm
web137656076431 小时前
Scala的宝藏库:探索常用的第三方库及其应用
开发语言·后端·scala
煤炭里de黑猫1 小时前
Lua C API :使用 lua_tonumber 函数从 Lua 栈中提取数值
开发语言·lua
闲猫1 小时前
go 反射 interface{} 判断类型 获取值 设置值 指针才可以设置值
开发语言·后端·golang·反射
Lqingyyyy2 小时前
P2865 [USACO06NOV] Roadblocks G 与最短路的路径可重复的严格次短路
开发语言·c++·算法