左值与右值

左值与右值

一、左值
左值表示一个占据内存中可识别位置的一个对象,更进一步地,可以对左值取地址

int a = 10;
int *p = &a;
int **q = &p;

a,p,q都是很经典的左值,可以通过标识符a,p,q,取出内存地址中对应的对象

int a;// ①
a = 4;// ②

①如果在函数中执行该语句的话,变量a会在栈帧中开辟一个4字节的内存空间其值未定义。所以a为左值,能够取其地址
②赋值语句中左操作数必须是一个左值,赋值操作本质上是对内存进行更新,所以我们必须要找到内存地址才能更新
二、右值
判断右值的一个简单方法就是能不能对变量或者表达式取地址,如果不能,他就是右值

int foo{return 10;};
x+1;

函数返回的对象(非引用,非指针)是一种典型的右值,这些对象在函数返回之后会被立刻销毁,也就不存在说取地址这样的操作了。如foo() = 20是一个典型的错误
加减乘除等表达式也是右值,因为x+1的结果保存在临时寄存器中,并不会输出到内存,因为没有办法对这个结果取地址,因为它们也是左值

a+1 = 4;
foo() = 10;

a+1以及foo()返回的值均为右值,它们都是一个临时的值,在表达式结束时,生命周期结束

三、左值和右值的转换
1.左值和右值在不同的表达式中有不同的定义

int a = 10,b = 20;
int c = a + b;

在第一行中,a,b都是左值,但在第二行中,由于要执行加法,所以会将左值隐式地转换成右值,其结果也是一个右值,保存在临时寄存器中,而后写入c的内存中
2.解引用操作符*(作用于右值,返回左值)
解引用操作符作用于指针,取出指针指向的内存内容,该结果可以作为左值使用

int *p = &a;
*(p+1) = 20;

p+1的结果是一个右值,但是*(p+1)的结果是左值
3.取地址操作符&(作用于左值,返回右值)
取地址操作符& 一定是作用在左值上,这也是前面定义左值使用的方式

int a[] = {1,2,3};
int *p = &a[2];
int *q = &(a+1);//error 

四丶左值引用
引用类型又称之为”左值引用“,顾名思义,引用只能引用一个左值,而不能是右值

string& s = string("hello");//error,这里引用了右值

但常量引用可以引用一个右值

const string& s = string("hello");
void foo(string& s){};
foo("hello"); //error 一个右值无法转换成左值引用
void foo1(const string& s){};
foo1("hello"); //一个右值可以转换成常量引用
int a = 10;
const int b = 20;

const int& c = a;//引用非常量右值
const int& d = b;//引用常量左值
const int& e = 10+20 //引用右值(正常来说,10+20这一右值会在表示结束时被销毁,但是常量引用演唱了其生命周期)

五、右值引用
右值引用是C++11的新特性,主要解决”移动语义“,以及”完美转发“,右值引用的标志是&&,顾名思义,右值引用专门为右值而生,可以指向右值,不能指向左值

int &&a = 5;
int b = 5;
int &&aa = b;//error,右值引用不可以指向左值

a = 6;//右值引用的用途,可以修改右值

右值引用本身是一个左值,这句话听起来有点难理解,但是右值引用是一个变量,而变量一般都是左值

int && s = 1;
int && q = s; //error 此时s为左值

右值引用由于引用的是右值,而右值又是临时的,随时可能被销毁的对象。因此在使用右值引用的地方,我们可以随意接管所引用对象的资源,而不用担心内存泄漏,数据被更改等情况

右值引用可实现移动语义和完美转发这两特性,它的主要目的有两方面:
1.消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率
2.能够更简洁明确的定义泛型函数

六丶生命周期和表达式类型
一个变量的生命周期在超出作用域时结束,如果一个变量代表一个对象,当然这个对象的生命周期也会在那时结束。那临时对象(pvalue)呢?在这里,C++的规则是:一个临时对象会在包含这个临时对象的完整表达式估值完成后,按照生成顺序的逆序被销毁,除非有生命周期延长发生,我们先看一个没有生命周期延长的情况

process_shape(circle(), triangle());

我们插入一些实际的代码,可以演示这一行为

class shape {
public:
    virtual ~shape() {}
};

class circle : public shape {
public:
    circle() { puts("circle()"); }
    ~circle() { puts("~circle()"); }
};

class triangle : public shape {
public:
    triangle() { puts("triangle()"); }
    ~triangle() { puts("~triangle()"); }
};

class result {
public:
    result() { puts("result()"); }
    ~result() { puts("~result()"); }
};

result
process_shape(const shape& shape1,
    const shape& shape2)
{
    puts("process_shape()");
    return result();
}

void main()
{
    puts("main()");
    process_shape(circle(), triangle());
    puts("something else");
}

输出结果是:
main()

circle()

triangle()

process_shape()

result()

~result()

~triangle()

~circle()

something else

可以看到结果的临时对象最后生成,最先析构,为了方便对临时对象的使用,C++对临时对象有特殊的生命周期延长规则,这条规则是:
如果一个prvalue被绑定到一个引用上,它的生命周期则会延长到跟这个引用变量一样长
我们对上面的代码只要改一行就能演示这个效果,把process_shape那行改成

result&& r = process_shape(
  circle(), triangle());

结果就变成了
main()

circle()

triangle()

process_shape()

result()

~triangle()

~circle()

something else

~result()
现在result的生成还在原来位置,但析构被延到了main最后

七丶移动的意义
对于smart_ptr,我们使用右值引用的目的是实现移动,而移动的意义是减少运动开销–在引用计数指针的场景下,这个开销并不大,移动构造和拷贝构造的差异仅在于
1.少一次other.shared_count_->add_count()的调用
2.被移动的指针被清空,因而析构的时候也少一次shared_count_->reduce_count()的调用

还有一点是,C++里的对象缺省的都是值语义,在下面这样的代码里:

class A{
 B b_;
 C c_;
 };

从实际内存布局的角度,很多语言如Java和Python会在A对象里放B和C的指针。而C++会直接把B和C对象放在A的内存空间里。这种行为即是优点也是缺点。说它是优点,因为它保证了内存访问的局域性,,而局限性在现在处理器架构上是绝对具有性能优势的。说它是缺点,因为复制对象的开销大大增加:在Java类语言里复制的是指针,在C++里是完整的对象,这就是为什么C++需要移动语义这一优化,而Java类语言里则根本不需要这一概念

移动语义使得C++里大对象(如容器)的函数和运算符成为现实,因而可以提高代码的简洁性和可读性,提高程序员的生产率。所有的现代的C++标准容器都针对移动进行了优化

如何实现移动
要让你设计的对象支持移动的话,通常需要下面几步
1.你的对象应该有分开的拷贝构造函数和移动构造函数(除非你只打算支持移动,不支持拷贝–如unique_ptr)
2.你的对象应该有swap成员函数,支持和另一个对象快速交换成员
3.在你的对象的名空间下,应当有一个全局的swap函数,调用成员函数swap来实现交换。支持这种用法会方便别人在其它对象里包含你的对象,并快速实现他们的swap函数
4.实现通用的operator=
5.上面各个函数如果不抛异常的化,应当标为noexcept。这对移动构造函数尤为重要

引用折叠
1.是不是看到T&,就一定是个左值引用? 是
2.是不是看到T&&,就一定是个右值引用? 否

关键在于,在有模板的代码里,对于类型参数的推到结果可能是引用,我们可以略过一些复杂的语法规则,要点是:
1.对于template foo(T&&)这样的代码,如果传递过去的参数是左值,T的推到结果是左值引用,如果传递过去的参数是右值,T的推导结果是参数的类型本身
2.如果T是左值引用,那T&&的结果依然是左值引用,即type& &&折叠成了 type&
3.如果T是一个实际类型,那T&&的结果自然是一个右值引用

完美转发
完美转发是指在函数模板中,完全按照模板的参数的类型,将参数传递给函数模板中调用另一个函数,即传入转发函数是左值对象,目标函数就能获得左值对象,转发函数是右值对象,目标函数就能获得右值对象,而不产生额外的开销

因此转发函数和目标函数一般采用引用类型,从而避免拷贝的开销。其次,由于目标函数可能需要能够既接受左值引用,又接受右值引用,所以考虑转发也需要兼容这两种类型

完美转发模板:

template<typename T>
void perfectForward(T&& t) {
    func(std:forward<T>(t));
}

1.当传入的为一个const T类型实参时,如果不适用右值引用,则需要声明一个const T的函数版本,因为cosnt T&无法转换成T&,由于使用的是右值引用,我们就可以完全不用声明带有const版本的转发函数
2.使用std::forward是为了保证函数可以调用正确形参形式的函数,std:forward的作用就是强制转换为实际类型的值作为形参

std::move()函数原型

template<typename T> struct remove_reference {
    typedef T type;  //定义T的类型别名为type
};

template<class T> struct remove_reference<T&> //左值引用
{
    typedef T type;
};
template<class T>struct remove_reference<T&&>//右值引用
{
    typedef T type;
};

int i;
remove_refrence<decltype(42)>::type a;             //使用原版本,
remove_refrence<decltype(i)>::type  b;             //左值引用特例版本
remove_refrence<decltype(std::move(i))>::type  b; //右值引用特例版本

举例转换过程:
1.std::move(var) => std::move(int&& &) =>折叠后std::move(int &)
2.此时:T的类型为int &,typename remove_reference::type为int,这里使用remove_reference的左值引用的特例化版本
3.通过static_cast将int&强制转化为int&&

int var = 10;
string&& move(int& t){
return static_cast<int&&>(t);

总结:
std::move()实现原理:
1.利用引用折叠原理将右值经过T&&传递类型保持不变还是右值,而左值经过T&&变为普通的左值引用,以保证模板可以传递任意实参,且保持类型不变
2.然后通过remove_refrence移除引用,得到具体的类型T
3.最后通过static_cast<>进行强制类型转换,返回T&& 右值引用

你可能感兴趣的:(C/C++,c++)