读c++ primer有感 -- 左值和右值

一、变量和内存
变量实际对应栈地址,例1:
    int i = 5;
    int j = i;
直接g++不优化,主要的汇编代码对应三行:
    movl    $5, 24(%esp) //栈顶地址+24字节的栈空间被赋值为25L,也就是i的地址
    movl    24(%esp), %eax //i的地址对应内容(也就是5),拷贝到寄存器ax中
    movl    %eax, 28(%esp) //ax的再拷贝到栈顶地址+28字节的栈空间,也就是j的地址
可见i,j变量所指向的值都是有实实在在的内存对应的。

g++ -O优化后:
    andl    $-16, %esp //栈顶对齐16B(大概吧)
    subl    $16, %esp //栈顶减16,即当前栈16B
    movl    $5, 4(%esp) //当前栈的倒数第二个四个字节被赋值为5
    movl    $_ZSt4cout, (%esp) //std::cout标签被赋值到栈顶部四个字节
    call    _ZNSolsEi //调用重载后的<<操作符
可见,现在i已经没有对应内存了,j变成了立即数以代码形式存在。

例2:
int getNum(){
    int tempI = 99; //tempI作为函数内的局部变量
    return tempI; //赋值操作
}
int main(){
    int i = getNum();
    std::cout<<i<<std::endl;
}
不优化汇编,首先看getNum内部:
    subl    $16, %esp
    movl    $99, -4(%ebp) //99L立即数赋值给栈底4字节,就是局部变量temp的对应内存
    movl    -4(%ebp), %eax //temp赋值给ax寄存器作为返回值(99)
再看main函数内部:
    call    _Z6getNumv
    movl    %eax, 28(%esp) //把getNum存在ax的返回值存到栈底(栈顶+28)
这里有一个“临时变量”,getNum()的返回值是存到了寄存器里面。

例三:
class MyInt{
    public:
        int i = 10;
};

MyInt getMi(){
    MyInt miTemp;
    return miTemp;
}
int main(){
    MyInt mi = getMi();
    std::cout<<mi.i<<std::endl;
}
不优化汇编,先看main函数相关部分:

    subl    $36, %esp //分配36B栈空间
    leal    -12(%ebp), %eax //离栈底12B处栈位置给ax
    movl    %eax, (%esp) //通过ax把12B这个位置给栈顶
    call    _Z5getMiv //调用getMi,接下来会看到,实际是把12B对应的字节赋值为了10
    subl    $4, %esp //栈扩大4B
    movl    -12(%ebp), %eax //12B位置再给ax
    movl    %eax, 4(%esp) //再通过ax把12B位置传给栈顶4B处(即栈底36B)
    movl    $_ZSt4cout, (%esp) //std::cout标签位置传给栈顶
    call    _ZNSolsEi //调用重载的ostream &operator<<(ostream &,int)
再看下getMi内部
    movl    8(%ebp), %eax //从getMi栈底再向前8B,因为call命令会把程序计数器ip入栈,getMi一开始又把main函数的bp(栈底)入栈,所以向前8B之后恰好是main函数中栈顶位置,从前可知这里存着main函数12B的位置
    movl    $10, (%eax) //把立即数10赋值给main的栈顶存储的位置(main函数12B处)
    nop
    movl    8(%ebp), %eax //把12B的值给ax(10)
    popl    %ebp //main栈底出栈
这里,调用的getMi实际上形如MyInt getMi(MyInt &),main函数中的局部变量mi在getMi中被赋值,当然“临时变量”还是通过寄存器ax返回。

总结,举了三个例子,目的是试图找出一些变量和对应的内存的关系。我的结论是:变量是给程序员看的,是逻辑概念,具体到机器那一层可以,变量可以完全被优化掉,也可以在立即数,在寄存器,在本函数栈,或者在其他函数的栈。那>么,变量和内存是没有必然联系的。我们下面讨论的左值、右值应该是纯抽象的。

二、“临时变量”和右值
先探讨下临时变量,因为按照c++之父的说法,右值是能够被移动的值,临时变量如果是临时的,用后就可以舍弃了,绝对能被移动。这个词听说过无数次了,到底是嘛玩意?查了资料(stackoverflow,百度,这个c++ primer实在是没有),其实没有查到定义,那还是通过几个例子试图说明一些片段。
首先比较明确的是用户自定义的“临时变量”,也就是常规意义上,大家常常讨论的,比如:

//为了更像那么回事,这里还应该输出“请输入您的名字”,并从cin中读进,但还是省了吧
string tempName = "liu";
std::cout<<"hello,"<<tempName<<std::endl;
//接下来的五千行函数内代码不会用到tempName,所以其实我们内心已经把它定义为了“临时”,正像名字提示的一样
//但是编译器不会分析名字,它会一直保存tempName占用的资源,包括tempName本身的栈空间和内部可能用到的堆空间
//其实还可以这样tempName.~string();不过貌似string对析构函数没有兼容两次两次delete的情况,重新修改下,作演示用,异常处理省略

class MyString{
    public:
        MyString(const char *pcc){
            _pc = (char *)malloc(strlen(pcc) + 1);
            strcpy(_pc,pcc);
        }
        ~MyString(){
            //加一个验证防止重复free
            if(_pc != NULL){
                free(_pc);
                _pc = NULL;
            }
        }
        MyString(MyString &ms){
            if(ms._moveable || ms._pc == NULL){
                _pc = ms._pc;
                ms._pc = NULL;

            }else{
                MyString(ms._pc);
            }
        }
        void makeMoveable(){
            _moveable = true;
        }
        const char *getPc() const{
            return _pc;
        }
    private:
        bool _moveable = false;
        char *_pc;
};

void printStr(const MyString &ms){
    if(ms.getPc() != NULL)
        std::cout<<ms.getPc()<<std::endl;
    else
        std::cout<<"empty"<<std::endl;
}

int main(){
    MyString tempName("liu3");
    tempName.~MyString();
    MyString tempName2("liu3");
    tempName2.makeMoveable();
    MyString constName(tempName2);
    printStr(tempName);
    printStr(tempName2);
    printStr(constName);
    std::cout<<"done"<<std::endl;
}
结果是
empty
empty
liu3
done
对“用户级临时变量”,我们可以用用户级的手段来回收和移动资源。像手动调析构函数~MyString,以及通过makeMoveable来“模拟”std::move操作达到移动构造函数的目的。类似地再看看“编译器临时变量”:
//又侧的两个string是不折不扣的临时变量,短暂的生命之后将被抛弃,好像那朝露一样,去日苦多
string usefulStr = string("hello ") + string("liu3");
//加一行说明调用析构函数的时机
std::cout<<"done"<<std::endl;
stl的代码对我来说比较难懂,我这儿不贴汇编代码了。hello、liu3、usefulStr都是位于栈内,hello的位置是12,liu3是16,usefulStr是20。函数执行顺序是:
hello构造函数;liu3构造函数;hello和liu3执行+重载操作,以及对usefulStr的赋值构造,这两步其实合并了,所以内部到底调的是拷贝构造还是移动构造不清楚;liu3的析构;hello的析构;打印“done”;usefulStr的析构。
总结,尽管usefulStr是否是移动构造不清楚,但是hello和liu3两个临时变量在本行执行完后立即就被析构了是肯定的,就像上面我们自己的例子一样。
类相关的临时变量比较清晰,再看看不是类的:
    int j = 200;
    short i = 100;

    //类型不能能够避免被直接优化掉,即使没有开-O
    long long k = i + j;
i+j的临时的结果被放到了ax里面,再赋值给k,其实也就当垃圾扔了,下次ax再被使用就不复存在了。在看一个:
string retStr(){
    return string("hello") + string(" liu3");
}
int main(){
    auto size = retStr().size();
    std::cout<<"done"<<std::endl;
}
retStr()的返回值放到了离栈底16的位置,在调用完size后被析构。
最后来一个,看看右值引用能不能保住临时变量。
    //本来要销毁的临时变量,被rrs来了个“刀下留人!!!!”
    string &&rrs = retStr();
一直到main结束前才执行析构。
需要稍微注明下,赋值给左值引用
    //编译不过,临时变量语句后就要销毁,rs引用销毁后的对象报错
    string &rs = retStr();
ps:还应该加一个例子

总结:我之前有点纳闷为啥没查到“临时变量”的明确定义,也许是我查得不够多,但是我觉得这种感性的概念:“一个东西,觉得可能没用,可以把它销毁或者移动。编译器的临时变量编译器负责销毁,用户临时变量用户自己负责。”的确不>太好书面说出来。那么右值,很大程度上由临时变量构成(还有一部分是什么子对象,先不管了)。临时变量既然是临时的,那么它的资源一定是可释放的,可释放就是可以析构,可以析构说明占用的资源没用,没用就可以移动,所以临时>变量是右值。用户逻辑上的“临时变量”可以通过std::move告知编译器。

三、左值和右值
关于这两个概念,看了stackoverflow引用的一篇c++之父的解释,也算是有了自己的理解。
先总结一下前两节的结论:
1.左值和右值和最终的机器代码实现关系不是那么大,很大程度上是概念性质的;
2.临时变量getStr().size()中的getStr()本行执行完之后立即析构,这是编译器能识别的右值,该变量的确是“临时的”;而string &&rrs = getStr()之后没有被立即析构,因为编译器也看出来了该变量还有“活下去”的理由。问题来了,rrs还是可移动的(可移动就是它的资源还是可以被随便夺走的),就是说这是一个“不临时的右值”???
3.左值是用户能够通过variable reference(就是int i中的i,void hello(int xixi)中的xixi,void (*pf)(int) = [](int i){return i + 1;}中的pf,int *pint = new int[100]数组中第一百个元素)直接或间接感知到的值,这是c++团
队定义的;
4.c++开发者自己觉得的“用户型右值”比如string tempMyName="liu3",但这没话说就是个左值。我们可以自己用自己的野路子回收tempMyName的资源,从而完全忽略编译器的感受。也可以用std::move()告诉编译器,我希望在函数重载时让tempMyName用移动构造函数或者移动赋值操作。就是说这个左值我们希望它有可移动的属性,好吧”可移动的左值“。

新的c++11在引入移动操作的同时,拓展了左右值的概念。有只有引用没有move的lvalue,只有move没有引用的prvalue,有引用又能move的xvalue,lvalue和xvalue同城glvalue,xvalue和prvalue统称rvalue。关键点在于”可移动“和”被引用
“(引用就是用户有一个”句柄“,有句柄说明用户在乎,不然取名字干嘛?用户在乎,那么一般就是不能随便被处理的)。

网上找了个说法
###########################
std::move执行一个无条件的转化到右值。它本身并不移动任何东西。
###########################
嗯,不过要让我说,我会写“无条件提供一个右值的解释”,值本身是值,没有左右,关键是用什么样的引用看它。
class MyString{
    public:
        MyString(){
            std::cout<<"in"<<std::endl;
        }
        MyString(const MyString &ms){

            std::cout<<"in &"<<std::endl;
        }
        MyString(MyString &&ms){
            std::cout<<"in &&"<<std::endl;
        }
};
int main(){
    MyString ms;
    MyString &&rrms = std::move(ms);
    MyString temp3(std::move(rrms));
    MyString temp(ms);
    MyString temp2(rrms);
    std::cout<<"done"<<std::endl;
}
得到:
in
in &&
in &
in &
done
那一坨叫做“值”的东西,用std::move去看它,它就是右的,用ms去看它,它就是左的。好吧,rrms的所作所为我实在编不下去了,只能用“rrms是到值的引用,但它初始化为了右值引用说明值可以移动,这时从rrms去看值其实是从xvalue看>的,编译器强制把xvalue绑定到lvalue”来聊以自慰。
加一行MyString &rms = std::move(ms);编译失败。
说明的编译器的左值是一个lvalue,不能有一星半点的可移动属性。
至于第二句,“它本身不移动任何东西”,绝对的,移动是一个人为的定义,我们甚至可以从左值抢资源赋值给右值,并把这种行为定义为“移动”。

最后再总结一下我的想法:
1.左右值是概念,不能用“机器怎么去实现这个值”来理解;
2.编译器看来的临时变量,没有引用,可以销毁(附带着可移动属性)。把临时变量销毁不是因为临时变量是纯右值prvalue,纯右值只是说资源可以移动,临时变量被销毁是因为占用资源没有用了,实际上,可移动和去移动和怎么去移动完
全是两码事。临时变量被销毁可以看是编译器对右值移动的一种实现(就是说编译器如果弱爆了把纯右值再复制个十分八分右值还是右值,只是该右值的移动操作是复制);
3.值按且仅按是否有引用和是否可移动来分成lvalue,rvalue,prvalue,xvalue,glvalue。在c++里面,引用名这个看值的窗口决定了值是左是右,左值引用和普通变量名肯定是引用并且没有可移动信息所以是lvalue;右值引用也是引用但
是说明值可以移动是xvalue但实际和lvalue一样绝对重载规则,作用可以把只有纯右值解释的变量(编译器看来的临时变量)赋值给它从而使得该右值存活下来;std::move(xxx)返回的没有引用是个prvalue;
4.c++的XXX &&右值引用其实有两个成分,带不带名称,直接的std::move返回一个 prvalue,赋值后的是个xvalue;
最后再举例子:
MyString getMStr(){
    return MyString();
}
//函数变成getMStr(MyString &test),执行默认构造函数
MyString test = getMStr();
//从rvalue赋值,调用移动构造函数
MyString test = std::move(getMStr());
//prvalue,编译器自动执行析构函数作为该纯右值的move实现;
getMStr();
//编译错误,MyString &是一个lvalue,lvalue不能有移动属性
MyString &rms = getMStr();
//rrms是术语的右值引用,实际是xvalue,编译器不再因为其是可移动的而执行销毁的移动操作。
MyString &&rrms = getMStr();
//xvalue在重载函数时体现为lvalue
MyString temp(rrms);
//move后就重载到了&&版本
MyString temp2(std::move(rrms));

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值