• C++面试八股文:什么是左值,什么是右值?


    某日二师兄参加XXX科技公司的C++工程师开发岗位第16面:

    面试官:什么是左值,什么是右值?

    二师兄:简单来说,左值就是可以使用&符号取地址的值,而右值一般不可以使用&符号取地址。

    int a = 42;	//a是左值,可以&a
    int* p = &a;
    int* p = &42;	//42是右值,无法取地址
    

    二师兄:一般左值存在内存中,而右值存在寄存器中。

    int a = 42, b = 1024;
    decltype(a+b);	//类型为右值,a+b返回的值存在寄存器中
    decltype(a+=b);	//类型为左值,a+=b返回的值存储在内存中
    

    二师兄:严格意义上分,右值分为纯右值(pvalue)和将亡值(xvalue)。C++中,除了右值剩余的就是左值。

    42;				//纯右值
    int a = 1024;
    std::move(a);	//将亡值
    

    面试官:C++98/03中已经有了左值,为什么还要增加右值的概念?

    二师兄:主要是为了效率。特别是STL中的容器,当需要把容器当作参数传入函数时:

    void function(std::vector<int> vi2)
    {
        vi2.push_back(6);
        for(auto& i: vi2) { std:: cout < i << " " ;}
        std::cout << std::endl;
    }
    int main(int argc, char* argv[])
    {
        std::vector<int> vi1{1,2,3,4,5};
        function(vi1);
        return 0;
    }
    

    二师兄:当我们要把vi1传入函数时,在C++98/03时只能通过拷贝构造函数,把vi1中所有的元素全部拷贝一份给vi2,拷贝完成之后,当function函数返回时,vi2被析构,然后vi1被析构。

    二师兄:在C++11及之后,我们可以通过std::move()vi1强制转为右值,此时在初始化vi2时执行的不是拷贝构造而是移动构造:

    void function(std::vector<int>&& vi2)
    {
        vi2.push_back(6);
        for(auto& i: vi2) { std:: cout < i << " " ;}
        std::cout << std::endl;
    }
    int main(int argc, char* argv[])
    {
        std::vector<int> vi1{1,2,3,4,5};
        function(std::move(vi1));
        return 0;
    }
    

    二师兄:这里只进行了一次构造。一次移动(当元素特别多时,移动的成本相对于拷贝基本可以忽略不记),一次析构。效率得到很大的提升。

    二师兄:当然,移动过后的变量已经不能再使用(身体被掏空),在std::move(vi1)之后使用vi1是未定义行为。

    面试官:好的。那你知道移动构造是如何实现的吗?

    二师兄:移动构造是通过移动构造函数实现的,当类有资源需要管理时,拷贝构造会把资源复制一份,而移动构造偷走了原对象的资源。

    struct Foo
    {
        int* data_;
        
        //copy construct
        Foo(const Foo& oth)
        {
            data_ = new int(*oth.data_);
        }
        //move construct
        Foo(Foo&& oth) noexcept
        {
            data_ = oth.data_;		//steal
            oth.data_ = nullptr;	//set to null
        }
    }
    

    面试官:好的。你觉得移动构造函数的noexcept关键字能省略吗?为什么?

    二师兄:应该不能吧,具体不清楚。

    面试官:那你知道std::move是如何实现的吗?

    二师兄:好像是static_cast实现的吧。

    面试官:那你知道什么叫万能引用吗?

    二师兄:万能引用主要用在模板中,模板参数是T,形参是T&&,此时可以传入任何类型的参数,所以称之为万能引用。

    template
    void function(T&& t) { ...}
    

    面试官:那你知道万能引用是如何实现的吗?

    二师兄:不太清楚。。

    面试官:完美转发知道吗?

    二师兄:std::forward 吗,了解过一些,不太熟悉。

    面试官:好的,回去等消息吧。

    让我们来回顾以下二师兄今天的表现:

    移动构造函数的noexcept关键字能省略吗?为什么?

    这里尽量不要省略。如果省略,编译器会推断是否会抛出异常。如果移动构造函数可能会抛出异常,则编译器不会将其标记为noexcept。当编译器不标记为noexcept时,为了保证程序的正确性,编译器可能会采用拷贝构造的方式实现移动构造,从而导致效率降低。

    需要注意的是,如果标记了noexcept但在移动时抛出了异常,则程序会调用std::terminate()函数来终止运行。

    知道std::move是如何实现的吗?

    这里的确是通过static_cast实现的,讲左值强行转换成右值,用来匹配移动语义而非拷贝。

    template
    typename std::remove_reference::type&& move(T&& t) { return static_caststd::remove_reference::type&&>(t);}
    

    万能引用是如何实现的?

    万能引用主要使用了引用折叠技术,

    template
    void function(T&& t) { ...}
    

    当T类型为左值时,&& & 被折叠为&, 当T类型为右值时,&& &&被折叠称为&&。以下是折叠规则:

    & &    -> &
    & &&   -> &
    && &   -> &
    && &&  -> &&
    

    完美转发知道吗?

    当我们需要在function中传递t参数时,如何保证它的左值或右值语义呢?这时候完美转发就登场了:

    template
    void function2(T&& t2) {}
    template
    void function(T&& t) 
    {
        function2(t);
    }
    

    当传入的参数t的类型时右值时,由于引用折叠还是右值,此时的t虽然时一个右值引用,但t本身却是一个左值!这里非常的不好理解。如果我们把t直接传入到function2,那么function2中的t2会被推导成左值,达不到我们的目标。如果在调用function2时传入std::move(t),当t是右值时没有问题,但当t是左值时,把t移动到t2t在外部不在能用。这也不符合我们的预期。此时std::forward闪亮登场!

    template
    void function2(T&& t2) {}
    template
    void function(T&& t) 
    {
        function2(std::forward(t));
    }
    

    std::forward使用了编译时多态(SFINAE)技术,使得当参数t是左值是和右值是匹配不同的实现,完成返回不同类型引用的目的。以下是标准库的实现:

    template 
    constexpr _Tp && forward(typename std::remove_reference<_Tp>::type &&__t) noexcept
    {
        return static_cast<_Tp &&>(__t);
    }
    
    template 
    constexpr typename std::remove_reference<_Tp>::type && move(_Tp &&__t) noexcept
    {
        return static_caststd::remove_reference<_Tp>::type &&>(__t);
    }
    

    好了,今日份面试到这里就结束了。二师兄的表现如何呢?预知后事如何,且听下回分解。

    关注我,带你21天“精通”C++!(狗头)

  • 相关阅读:
    一文2000字从0到1讲述持续集成下接口自动化测试实践
    程序员:如何做0-1的突破
    一建证书有什么用?拿到一级建造师证书能干什么?
    力扣C++学习笔记——C++ assign全面解析
    m基于MATLAB的通信系统仿真,包括信号源,载波信号,放大器,带宽滤波器,接收端包括放大器,带宽滤波器,载波解调,低通滤波器等
    SAP PI PO 接口常见问题处理:应用程序使用内容计划
    深入解析:探索Nginx与Feign交锋的背后故事 - 如何优雅解决微服务通信中的`301 Moved Permanently`之谜!
    ucos2源码分析---内核部分(三)
    【数据结构】顺序表和链表
    基于Springboot外卖系统20:前端菜品展示+菜品数量查询
  • 原文地址:https://www.cnblogs.com/binarch/p/17486732.html