C++11 中新增的另一种类型就是原始字符串raw,即原始字符串中的字符表示的就是字符本身,不存在转义等的其他问题,因此,经常用来表示某些复杂的字符串
R"(…)"
由 R 开头,双引号内包含着 (…) ,实际的字符序列是小括号内的内容,小括号是字节序列的定界符
R"(C:\Program Files (x86)\Google\Chrome\Application)"
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;
}
因此关于左值与右值的区分并不是很好区分,一般认为:
int main()
{
// 普通类型引用只能引用左值,不能引用右值
int a = 10;
int& ra1 = a; // ra为a的别名
//int& ra2 = 10; // 编译失败,因为10是右值
const int& ra3 = 10;
const int& ra4 = a;
return 0;
}
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;
}
在上述代码中:
此时,来分析一下以值形式返回的过程:
借此,C++11 中提出了移动语义的概念,即:将一个对象中资源移动到另一个对象中的方式,可以有效缓解该问题
对于上述:strRet 拷贝构造临时对象成功后,strRet 就被销毁了,该过程中经历了:刚申请一段空间,又释放相同大小的一段空间。引入移动语义后进行优化:将strRet中资源转移到临时对象中
对上述String类增加移动构造如下:
String(String&& s)
: _str(s._str)
{
s._str = nullptr;
}
因为 strRet 对象的生命周期在创建好临时对象后就结束了,即将亡值,C++11 认为其是右值,在用 strRet 构造临时对象时,就会调用移动构造函数,即:将 strRet 中资源转移到临时对象中。而临时对象也是右值,因此在用临时对象构造 s3 时,也采用移动构造,将临时对象中资源转移到 s3 中,整个过程,只需要创建一块堆内存即可,既省了空间,有大大提高程序运行的效率
【注意】:
按照语法,右值引用只能引用右值,但在某些场景下,可能真的需要用右值去引用左值实现移动语义。当需要用右值引用引用一个左值时,可以通过 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;
}
【注意】:
完美转发是指在函数模板中,完全按照模板的参数的类型,将参数传递给函数模板中调用的另外一个函数
完美转发是目标函数总希望将参数按照传递给转发函数的实际类型转给目标函数,而不产生额外的开销,就好像转发者不存在一样
所谓完美:函数模板在向其他函数传递自身形参时,如果相应实参是左值,它就应该被转发为左值;如果相应实参是右值,它就应该被转发为右值。这样做是为了保留在其他函数针对转发而来的参数的左右值属性进行不同处理(比如参数为左值时实施拷贝语义;参数为右值时实施移动语义)
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表达式是C++11中引入的最重要和最常用的特性之一。利用lambda表达式,可以方便的定义和创建匿名函数。
lambda 表达式书写格式:
[ capture-list ] ( parameters ) 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;
}
函数对象,又称为仿函数,即可以像函数一样使用的对象,就是在类中重载了 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()