C++的一些特性(2)

左值和右值

概念

这两个概念其实就涉及到我们的赋值和引用。左值直观的理解就是在等号左边的值,右值就是出现在等号右边的值。左值一定是占据了内存中一个可识别的位置的,而右值则不一定有。

比较容易混淆的两个例子可能是

1
2
x++;
++x;

其中,x++ 是右值,++x 是左值。

也很好理解,我们在重载前置 ++ 的时候,是先加再返回,这个时候我完全可以给它加上之后返回一个原对象。但是后置 ++ 则不行,我需要先给它保存原对象,然后原对象++,返回刚刚保存的对象。

函数返回值一般情况下为右值,少数情况下,比如在定义返回值的时候类型加了引用(&),那么得到的值可能是左值。判断是否为左值很简单,在该值之前加一个 & 看看能否取出一个地址来。

左值引用和右值引用

左值引用

写法:

1
2
int a=1;
int &b=a;

引用其实前面介绍过了,就是绑定一个对象,让我对引用对象操作与实际被绑定的对象操作没有任何区别,也就是说在执行完 int &b=a 之后,a 和 b 变量没有区别的,都是操作同一块内存。

左值引用在执行的时候它的对象必须是一个左值,可以理解为指针,并且接下来我对指针不能赋值,我对指针的所有操作都变成对它内容的操作。

左值引用可以绑定一个右值,我们只需要定义左值引用的时候加一个 const 即可变为常量左值引用,就可以绑定。

右值引用

写法:

1
int &&b=1;

右值引用可以是一个左值,但是我们要对它强制类型转换为右值,像下面这样

1
2
int i=0;
int &&b=static_cast<int &&>(i);

右值引用同样不能直接绑定左值,如果要绑定需要强转为右值之后再绑定,使用如下语法

1
2
int x=1;
int&& y=static_cast<int&&>x;

万能引用

当我们的类型为模板类的时候,编译器会进行类型推导,此乃万能引用,但是万能引用带来的一个问题是调用其它函数的时候不能合理地分辨参数为左值还是右值,它是不完美的。

完美转发

右值引用可以实现完美转发,何为“完美”,举个例子

1
2
3
4
template<typename T>
void function(T t) {
otherdef(t);
}

如上所示,function() 函数模板中调用了 otherdef() 函数。在此基础上,完美转发指的是:如果 function() 函数接收到的参数 t 为左值,那么该函数传递给 otherdef() 的参数 t 也是左值;反之如果 function() 函数接收到的参数 t 为右值,那么传递给 otherdef() 函数的参数 t 也必须为右值。

显然,function() 函数模板并没有实现完美转发。一方面,参数 t 为非引用类型,这意味着在调用 function() 函数时,实参将值传递给形参的过程就需要额外进行一次拷贝操作;另一方面,无论调用 function() 函数模板时传递给参数 t 的是左值还是右值,对于函数内部的参数 t 来说,它有自己的名称,也可以获取它的存储地址,因此它永远都是左值,也就是说,传递给 otherdef() 函数的参数 t 永远都是左值。总之,无论从那个角度看,function() 函数的定义都不“完美”。

我们通过右值引用加上转发模板函数 forward<>() 实现完美转发。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include<iostream>
using namespace std;
template<typename T>
void show_type(T &a) {
std::cout << "rvalue" << std::endl;
//std::cout << typeid(a).name() << std::endl;
}

template<typename T>
void show_type(const T& a) {
std::cout << "lvalue" << std::endl;
//std::cout << typeid(a).name() << std::endl;
}

template <typename T>
void function(T&& t) {
show_type(forward<T>(t));
}

int main() {
int a = 1;
function(5);
function(a);
}
//lvalue
//rvalue

主要是因为右值引用可以接收左值作为参数,并且它也能完美转发,此乃 C++ 特性也,记住即可。

C++定义函数的其他方法

在C++中,还可以使用如下的方式声明函数

1
2
auto func = []()->int{};
func();

在这里,定义了一个函数指针,[] 为捕获列表,表示我要捕获当前作用域的哪些变量,变量拿来可以直接用,等会在调用的时候捕获的变量不需要放到参数列表中,() 为参数列表,-> 为后置返回值类型,{} 为代码块。

捕获列表具体可以为以下的一些情况

  • 空。没有使用任何函数对象参数。
  • =。函数体内可以使用Lambda所在作用范围内所有可见的局部变量(包括Lambda所在类的this),并且是值传递方式(相当于编译器自动为我们按值传递了所有局部变量)。
  • &。函数体内可以使用Lambda所在作用范围内所有可见的局部变量(包括Lambda所在类的this),并且是引用传递方式(相当于编译器自动为我们按引用传递了所有局部变量)。
  • this。函数体内可以使用Lambda所在类中的成员变量。
  • a。将a按值进行传递。按值进行传递时,函数体内不能修改传递进来的a的拷贝,因为默认情况下函数是const的。要修改传递进来的a的拷贝,可以添加mutable修饰符。
  • &a。将a按引用进行传递。
  • a, &b。将a按值进行传递,b按引用进行传递。
  • =,&a, &b。除a和b按引用进行传递外,其他参数都按值进行传递。
  • &, a, b。除a和b按值进行传递外,其他参数都按引用进行传递。

C++异常

C++ 已经实现了完整的异常处理机制,我们在可能会出现异常的块用 try 关键字包裹,然后在后面可以使用多个 catch 去捕获异常,异常可以是基本类型,也可以是一个类,我们通过 throw 关键字可以把异常抛出,然后会被 catch 捕获。

1
2
3
4
5
6
try{
throw 1;
}
catch (int e){
printf("Int:%d\n",e);
}

除此之外,我们可以用 ... 表示所有异常,所有在之前没被捕捉到的异常在这一块一定会被捕捉,而且它只能处理抛出的异常,不能处理它指令执行的异常,比如 double free,NULLptr 错误等等,如果要捕获这些异常可以用 windows 自带的 SEH。

if-switch 初始化

C++ 17 开始支持,如果需要用需要把 VS 的编译器中语言功能里面切换到这个版本,打开之后我们可以在 if 和 switch 语句中初始化变量。

1
2
3
if(int b=1;b){
cout<<b<<endl;
}

C++断言

运行时断言

我们使用 assert 函数可以去判断我们的表达式是否为真,如果为真则这句话没其它作用,为假则会报运行时错误,它会在代码中实时检测你的表达式是否为真,相比于 if 来说可以让我们写的代码缩进更少。

1
2
char *s=malloc(0x10);
assert(s);

若分配内存失败,则这里会报运行时错误,如果转为普通的判断我们可能需要这么写:

1
2
3
4
5
char *s=malloc(0x10);
if(s==NULL){
perror("error:");
exit(-1);
}

编译时断言

使用 static_assert 关键字去判断,顾名思义,不满足条件会在编译时组织编译。