C++11特性(二)

原始字符串 raw

简介

C++11 中新增的另一种类型就是原始字符串raw,即原始字符串中的字符表示的就是字符本身,不存在转义等的其他问题,因此,经常用来表示某些复杂的字符串

语法

R"(…)"

由 R 开头,双引号内包含着 (…) ,实际的字符序列是小括号内的内容,小括号是字节序列的定界符

示例
  • Windows 文件路径

R"(C:\Program Files (x86)\Google\Chrome\Application)"

  • Json 字符串

R"({“name”: “xx”, “age”: 10})"

委派构造函数

委派构造函数可以减少构造函数的书写量

class Info 
{
public:
	Info() 
		: type(1)
		, name('a') 
	{
		InitRest();
	}
	
	Info(int i) 
		: type(i)
		, name('a') 
	{
		InitRest();
	}
	
	Info(char e) 
		: type(1)
		, name(e) 
	{
		InitRest();
	}
private:
	void InitRest() 
	{ 
		//其他初始化 
	}
	int type;
	char name;
};

如上述代码中,每一个构造函数都需要初始化了列表来初始化成员type和name,且都调用了相同的函数InitRest(),存在重复
在C++11中,可以使用委派构造函数来简化代码,如:

class Info 
{
public:
	Info()  //称为目标构造函数(被调用)
	{ 
		InitRest(); 
	}
	Info(int i)  //委派构造函数(调用者)
		: Info() 
	{ 
		type = i; 
	}
	Info(char e)  //委派构造函数(调用者)
		: Info() 
	{ 
		name = e; 
	}
private:
	void InitRest() 
	{ 
		//其他初始化 
	}
	int type {1};
	char name {'a'};  
};

右值引用

右值引用是C++标准中引入的新特性,利用右值引用可以实现 移动语义 和 完美转发。它的主要目的为:

  • 消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率
  • 能更加简洁明确地定义泛型函数

左值和右值

左值与右值是C语言中的概念,但C标准并没有给出严格地区分方式,一般认为:可以放在等号左边的或者能够取地址的成为左值,只能放在等于号右边的或者不能被取地址的称为右值,但是也并不是绝对的

int g_a = 10;

// 函数的返回值结果为引用
int& GetG_A()
{
	return g_a;
}
int main()
{
	int a = 10;
	int b = 20;
	// a和b都是左值,b既可以在=的左侧,也可在右侧,
	// 说明:左值既可放在=的左侧,也可放在=的右侧
	a = b;
	b = a;
	const int c = 30;
	// 编译失败,c为const常量,只读不允许被修改
	//c = a;
	// 因为可以对c取地址,因此c严格来说不算是左值
	cout << &c << endl;
	// 编译失败:因为b+1的结果是一个临时变量,没有具体名称,也不能取地址,因此为右值
	//b + 1 = 20;
	GetG_A() = 100;
	return 0;
}

因此关于左值与右值的区分并不是很好区分,一般认为:

  • 普通类型的变量,因为有变量名,可以取地址,都认为是左值
  • const 修饰的常量,不可修改,只读类型的,理论应该按照右值对待,但是因为其可以取地址(如果只是 const 类型常量的定义,编译器不给其开辟空间,如果对该常量取地址时,编译器才为其开辟空间),C++11认为其是左值
  • 如果表达式的运行结果是一个临时变量或者对象,认为是右值
  • 如果表达式运行结果或单个变量是一个引用,则认为是左值
C++11 对右值进行了严格的区分:
  • C语言中的纯右值。如:a + b、100
  • 将亡值。如:表达式的中间结果、函数按照值的方式进行返回

引用与右值引用比较

  1. C++98 中,普通引用只能引用左值,不能引用右值,const引用既可以引用左值,也可以引用右值
int main()
	{
	// 普通类型引用只能引用左值,不能引用右值
	int a = 10;
	int& ra1 = a; // ra为a的别名
	//int& ra2 = 10; // 编译失败,因为10是右值
	const int& ra3 = 10;
	const int& ra4 = a;
	return 0;
}
  1. C++11 中,右值引用只能引用右值,一般情况下不能直接引用左值
int main()
{
	// 10纯右值,本来只是一个符号,没有具体的空间,
	// 右值引用变量r1在定义过程中,编译器产生了一个临时变量,r1实际引用的是临时变量
	int&& r1 = 10;
	r1 = 100;
	int a = 10;
	int&& r2 = a; // 编译失败:右值引用不能引用左值
	return 0;
}

以值的形式返回对象的缺陷

以STL中string容器的加号运算符重载函数为例,如下:

String
{
public:
	// ...
	String operator+(const String& s)
	{
		char* pTemp = new char[strlen(_str) + strlen(s._str) + 1];
		strcpy(pTemp, _str);
		strcpy(pTemp + strlen(_str), s._str);
		String strRet(pTemp);
		return strRet;
	}
	// ...
private:
	char* _str;
};

int main()
{
	String s1("hello");
	String s2("world");
	String s3(s1+s2);
	return 0;
}

在上述代码中:

  1. 创建出一个新的字符串指针
  2. 将当前对象中的内容添加至指针指向的空间中(调用这个重载函数的对象,即this)
  3. 将待加对象中的内容添加至指针指向空间的末尾(加号后面的对象,即传入的参数)
  4. 根据这个字符串指针的内容构造出一个string对象,以值的形式返回(不能以地址的形式返回,因为返回的对象是在栈上开辟的空间)

此时,来分析一下以值形式返回的过程:

  1. strRet 在按照值返回时,必须创建一个临时对象。临时对象创建好之后,strRet就被销毁了
  2. 使用临时对象拷贝构造s3。s3构造好之后,临时对象被销毁
    在上面的过程中,strRet、临时对象、s3 这三个对象,每个对象中都有自己独立的空间,但是空间中的内容都是相同的,相当于创建了三个完全相同的对象。这样对空间是一种浪费,并且程序的效率会降低,同时临时对象的作用确实也不是很大!!!

移动语义

借此,C++11 中提出了移动语义的概念,即:将一个对象中资源移动到另一个对象中的方式,可以有效缓解该问题
对于上述:strRet 拷贝构造临时对象成功后,strRet 就被销毁了,该过程中经历了:刚申请一段空间,又释放相同大小的一段空间。引入移动语义后进行优化:将strRet中资源转移到临时对象中
对上述String类增加移动构造如下:

String(String&& s)
	: _str(s._str)
{
	s._str = nullptr;
}

因为 strRet 对象的生命周期在创建好临时对象后就结束了,即将亡值,C++11 认为其是右值,在用 strRet 构造临时对象时,就会调用移动构造函数,即:将 strRet 中资源转移到临时对象中。而临时对象也是右值,因此在用临时对象构造 s3 时,也采用移动构造,将临时对象中资源转移到 s3 中,整个过程,只需要创建一块堆内存即可,既省了空间,有大大提高程序运行的效率
【注意】:

  • 移动构造函数的参数千万不能设置成 const 类型的右值引用,因为资源无法转移而导致移动语义失效
  • 在 C++11 中,编译器会为类默认生成一个移动构造函数,该移动构造函数为浅拷贝,因此当类中涉及到资源管理时,用户必须显式定义自己的移动构造

右值引用左值 std::move()

按照语法,右值引用只能引用右值,但在某些场景下,可能真的需要用右值去引用左值实现移动语义。当需要用右值引用引用一个左值时,可以通过 move() 函数将左值转化为右值。C++11 中,std::move() 函数位于头文件中,该函数名字具有迷惑性,它并不搬移任何东西,唯一的功能就是将一个左值强制转化为右值,然后实现移动语义
关于move有如下示例:

class Person
{
public:
	Person(char* name, char* sex, int age)
		: _name(name)
		, _sex(sex)
		, _age(age)
	{}
	
	Person(const Person& p)
		: _name(p._name)
		, _sex(p._sex)
		, _age(p._age)
	{}
	
#if 0
	Person(Person&& p)
		: _name(p._name)
		, _sex(p._sex)
		, _age(p._age)
	{}
	
#else
	Person(Person&& p)
		: _name(move(p._name))
		, _sex(move(p._sex))
		, _age(p._age)
	{}
	
#endif
private:
	String _name;
	String _sex;
	int _age;
};

Person GetTempPerson()
{
	Person p("prety", "male", 18);
	return p;
}

int main()
{
	Person p(GetTempPerson());
	return 0;
}

【注意】:

  • 被 std::move() 转化的左值,其生命周期并没有随着左值的转化而改变,即 std::move() 转化的左值变量不会被销毁
  • STL中也有一个move函数,就是将一个范围中的元素搬移到另一个位置

完美转发 std::forward()

完美转发是指在函数模板中,完全按照模板的参数的类型,将参数传递给函数模板中调用的另外一个函数
完美转发是目标函数总希望将参数按照传递给转发函数的实际类型转给目标函数,而不产生额外的开销,就好像转发者不存在一样
所谓完美:函数模板在向其他函数传递自身形参时,如果相应实参是左值,它就应该被转发为左值;如果相应实参是右值,它就应该被转发为右值。这样做是为了保留在其他函数针对转发而来的参数的左右值属性进行不同处理(比如参数为左值时实施拷贝语义;参数为右值时实施移动语义)

C++11 通过 std::forward() 函数来实现完美转发,如:

void Fun(int &x)
{
	cout << "lvalue ref" << endl;
}
void Fun(int &&x)
{
	cout << "rvalue ref" << endl;
}
void Fun(const int &x)
{
	cout << "const lvalue ref" << endl;
}
void Fun(const int &&x)
{
	cout << "const rvalue ref" << endl;
}

template<typename T>
void PerfectForward(T &&t)
{
	Fun(std::forward<T>(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;
}

lambda 表达式

lambda表达式是C++11中引入的最重要和最常用的特性之一。利用lambda表达式,可以方便的定义和创建匿名函数。

基本语法

lambda 表达式书写格式:

[ capture-list ] ( parameters ) mutable -> return-type { statement }

  • [ capture-list ]:捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据 [] 来判断接下来的代码是否为 lambda 表达式,捕捉列表能够捕捉上下文中的变量供 lambda 函数使用。捕捉列表可由多个捕捉项组成,并以逗号分隔;捕捉列表不允许变量重复传递,否则就会导致编译错误
    • [var]:表示值传递方式捕捉变量 var
    • [=]:表示值传递方式捕获所有父作用域中的变量,包括this
    • [&var]:表示引用传递方式捕捉变量var
    • [&]:表示引用传递方式捕捉所有父作用域中的变量,包括this
    • [this]:表示值传递方式捕捉当前的this指针
  • ( parameters ):参数列表,与普通函数的参数列表一致,如果不需要参数传递,则可以连同 () 一起省略
  • mutable:默认情况下,lambda 表达式总是一个const函数mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)
  • ->return-type:返回值类型,用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,有编译器对返回类型进行推导
  • { statement }:函数体,在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量

【注意】:在 lambda 函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为空
有如下示例:

int main()
{
	// 最简单的lambda表达式, 该lambda表达式没有任何意义
	[]{};
	// 省略参数列表和返回值类型,返回值类型由编译器推导为int
	int a = 3, b = 4;
	[=]{return a + 3; };
	// 省略了返回值类型,无返回值类型
	auto fun1 = [&](int c){b = a + c; };
	fun1(10)
	cout<<a<<" "<<b<<endl;
	// 各部分都很完善的lambda函数
	auto fun2 = [=, &b](int c)->int{return b += a+ c; };
	cout<<fun2(10)<<endl;
	// 复制捕捉x
	int x = 10;
	auto add_x = [x](int a) mutable { x *= 2; return a + x; };
	cout << add_x(10) << endl;
	return 0;
}

函数对象与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 表达式完全一样
函数对象将rate作为其成员变量,在定义对象时给出初始值即可,lambda表达式通过捕获列表可以直接将该变量捕获到
在这里插入图片描述

实际在编译器底层对于lambda表达式的处理方式,完全就是按照函数对象的方式处理的,即:如果定义了一个lambda表达式,编译器会自动生成一个类,在该类中重载operator()


THE END

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值