一. 浅拷贝与深拷贝

struct Node{
    int size;
    int *p;

    Node() = default;
    Node(int x){
        size = x;
        p = new int(size);
    }
};

1. 浅拷贝

按位拷贝对象,如果拷贝对象中存在指针,那么两指针指向的地址相同(但是两指针的地址不一定相同)

Node shllowCopy(Node &v){
        this -> size = v.size;
        this -> p = v.p;
}

2. 深拷贝

拷贝对象的所有属性,如果拷贝对象中存在指针,那么会在堆上重新开辟一片相同大小的空间,并让指针指向该空间

Node deepCopy(Node &v){
        this -> size = v.size;
        this -> p = new int[size];
        for(int i = 0; i < size; i++){
            this -> p[i] = v.p[i];
        }
}

二. 左值与右值

  • 左值(lvalue):表达式结束后仍然存在的持久对象
  • 右值(rvalue):表达式结束后就被销毁的临时对象

一般来说,有变量名的都是右值,没有变量名的是左值

特殊:字符字面量(如'c','a')这种是右值;但是字符串字面量,如"abcd"这种,由于它存在于静态存储区,所以字符串字面量均为左值

三. 左值引用与右值引用

1. 左值引用(&)

左值引用本质上是给左值变量起了一个别名

  • 改变任意一方,双方都改变
  • 只能绑定在左值上
  • 特殊:const int &x = 10,为常量左值引用,可以绑定右值,但是不可更改

2. 右值引用(&&)

绑定在右值上,可以延长临时对象的生命周期,并允许更改

应该注意的是,右值引用类型的变量,它本身是个左值

比如说:int &&x = 10,x的类型是右值引用,但它本身是个左值

如果你要把x再次当成右值传为另一个函数或者赋值给另一个数,那么直接传是不行的,这个时候就要用到std::move()

四. 强转右值std::move()

std::move()可以将左值强制转化为右值,类型上变成右值引用类型

作用是把原对象指向堆内存空间的指针地址转交给新对象,然后将原对象的该指针

置为nullptr

但是std::move()对基本类型,如int,double,char,指针无效,因为他们的值就存在于栈上,不指向任何外部资源

比如说:

int res = 1;
int ans = std::move(res);
std::cout << res << "\n";

此时你会发现输出的仍是1

如果不是的话,那么原对象会被“掏空”,比如说:

std::string s = "abc";
std::string t = std::move(s);
std::cout << s << "\n";

输出的就是空

  • 注意,只有在std::move()用于创建新对象时,才触发移动构造

五. 纯右值与将亡值

1. 纯右值

没有身份,没有固定内存地址,不可被移动,用完即销毁的值/表达式

特点:

  • 没有标识符(没有名字)
  • 不能取地址
  • 一般用于初始化和计算

2.将亡值

有身份,可被移动的值/表达式

特点:

  • 通常对应一个右值引用

  • 在内存中有地址

  • 通过std::move()强转后的就是将亡值

六. 函数参数传递

问:这六次函数调用,分别各触发了几次拷贝,几次移动构造?

void f1(Node v){
    return;
}

void f2(Node &v){
    return;
}

void f3(Node &&v){
    return;
}
int main(){

    Node a;
    Node &b = a;
    Node c;
    Node d;

    f1(a);//1次拷贝,0次移动构造
    f1(b);//1次拷贝,0次移动构造
    f1(std::move(c));//0次拷贝,1次移动构造

    f2(a);//0次拷贝,0次移动构造
    f2(b);//0次拷贝,0次移动构造

    f3(std::move(c));//0次拷贝,0次移动构造
    return 0;
}

七. 引用折叠

规则:只要有左值引用,结果就是左值引用;只有当两个都是右值引用时,结果才是右值引用

原始类型(T) 声明的引用类型 折叠后结果
T& & T&
T& && T&
T&& & T&
T&& && T&&

八. 万能引用

万能引用的好处在于既能绑定左值,又能绑定右值

万能引用形式上为T&&,但要成为万能引用,必须满足两个条件:

  • 必须存在类型推导,如模板,auto
  • 形式只能是T&&,如果用const修饰,就会变回普通的右值引用

易错区分:

void func(std::vector<T> &&v){
    return;
}

这不是万能引用!

因为它被固定为了 std::vector 的右值引用。它只能接收 std::vector 的右值,不能接收 std::vector 的左值,更不能接收其他的类型(如 int

万能引用的形式只能是T&&

  • 万能引用是贪婪的,这里模板推导的“精确匹配”优先级高于普通函数的“隐式转换”,也就是说可能会引发不必要的引用转化

九.完美转发std::forward<T>

用处是当一个函数把参数传递为另一个函数时,保留参数的原生属性

例:

void target(int&& x) {
    return;
}

template<typename T>
void wrapper(T&& param) { 
    target(param); //报错
}

int main() {
    wrapper(10); 
}

在这个例子中,10确实是右值,但是在wrapper函数中,param是左值,无法进行&&的右值绑定,于是会报错

为了解决这个问题,我们引入了std::forward<T>,它能根据T推导的结果,决定把param转化为左值或者是右值,于是就可以:

template<typename T>
void wrapper(T&& param) {
    target(std::forward<T>(param)); 
}
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐