乐趣区

关于c++: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<typename T>
void function(T&& t) {...}

面试官:那你晓得万能援用是如何实现的吗?

二师兄:不太分明。。

面试官:完满转发晓得吗?

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

面试官:好的,回去等音讯吧。

让咱们来回顾以下二师兄明天的体现:

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

这里尽量不要省略。如果省略,编译器会推断是否会抛出异样。如果挪动构造函数可能会抛出异样,则编译器不会将其标记为 noexcept。当编译器不标记为noexcept 时,为了保障程序的正确性,编译器可能会采纳拷贝结构的形式实现挪动结构,从而导致效率升高。

须要留神的是,如果标记了 noexcept 但在挪动时抛出了异样,则程序会调用 std::terminate() 函数来终止运行。

晓得 std::move 是如何实现的吗?

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

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

万能援用是如何实现的?

万能援用次要应用了援用折叠技术,

template<typename T>
void function(T&& t) {...}

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

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

完满转发晓得吗?

当咱们须要在 function 中传递 t 参数时,如何保障它的 左值或右值 语义呢?这时候完满转发就退场了:

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

当传入的参数 t 的类型时右值时,因为援用折叠还是右值,此时的 t 尽管时一个右值援用,但 t 自身却是一个 左值 !这里十分的不好了解。如果咱们把t 间接传入到 function2,那么function2 中的 t2 会被推导成左值,达不到咱们的指标。如果在调用 function2 时传入 std::move(t),当t 是右值时没有问题,但当 t 是左值时,把 t 挪动到 t2t 在内部不在能用。这也不合乎咱们的预期。此时 std::forward 闪亮退场!

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

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

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

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

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

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

退出移动版