C++面向对象(OOP)编程-引用(左引用、右引用、万能引用、完美转发)

发布时间:2023年12月20日

本文旨在通过通俗易懂的语言全面介绍引用,包括引用的本质、左右引用、以及万能引用、循环引用和完美转发等内容。

目录

1 引用的本质

2 引用的性质

3 引用的分类? ? ? ?

3.1 普通引用

3.2 常引用

3.3 左右引用

3.3.1 左值

3.3.2 右值

3.3.3 左右值总结

3.3.4?左值引用

3.3.5?右值引用

3.5 万能引用

3.5.1 万能引用与右值引用的区别? ? ? ??

3.6 移动语义

3.7 完美转发

3.8 一些特殊的引用

4 引用和指针的区别


1 引用的本质

????????引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间它和它引用的变量共用同一块内存空间

? ? ? ? 引用是给变量取一个别名,不会开辟新的空间,它和变量共用一块内存空间。

????????引用的本质是一个指针常量,引用分为普通引用常引用,常引用一般会对其权限进行改变,为缩小,常引用就是定义了一个指向常量的指针常量 const int * const p。常引用不能对其值经过引用来修改。

2 引用的性质

? 引用的例子:


int a = 10;

int & a1 = a; // a1 = 10

(1)&在上面的代码中不是求地址运算,而是起到了标识作用。

(2)引用类型必须是和目标变量是同一类型的。

(3)引用在声名时必须初始化。

(4)一个变量可以有多个引用

(5)引用一旦引用了一个实体就不能再引用其他实体(从一而终)。

(6 ? 不能建立数组的引用,因为数组是一个由若干个元素所组成的元素集合,所以无法建立一个数组的别名。

? ? ? ? 错误的例子:

int a[3] = {0};

int & a1 = a; // 这是错误的代码,引用不可以引用数组,不可以建立数组的别名

3 引用的分类? ? ? ?

3.1 普通引用


int a = 10;

int & a1 = a; // a1 = 10

3.2 常引用

? ? ? ? 使用const 修饰的引用,不能通过引用来修改目标值,一般会对其权限缩小。常引用变量既可以是左值也可以是右值

int b = 78;

const int & b1 = b;

//b1 = 10;// 这是错误的

b = 10;//这是正确的

const int & b2 = 30; // 常引用可以加右值

std::cout << b1 << std::endl; // 10

std::cout << b2 << std::endl; // 30

3.3 左右引用

3.3.1 左值

????????左值和右值是表达式的属性。C++中的表达式,不是左值就是右值。左值可以位于赋值语句的左侧,而右值则不能;

? ? ? ? (1)当一个对象被用作右值的时候,用的是对象的值;

? ? ? (2)当对象被用作左值的时候,用的是对象的身份(在内存中的位置)。需要右值的地方可以用左值代替,但不能把右值当成左值使用。

? ? ? (3)变量都是左值,

????????左值(Lvalue)是指可以出现在赋值语句左边的表达式。左值可以是变量、数组元素、函数返回值等。左值必须满足以下条件之一:

  1. 是一个变量或数组元素;
  2. 是一个函数调用,且返回类型为左值引用;
  3. 是一个对象的成员,且成员的类型为左值引用。

一些例子:

int a = 1; // a是左值,因为它是一个变量
int b[5] = {1, 2, 3, 4, 5}; // b是左值,因为它是一个数组
int (*func())() = &foo; // func是左值,因为它是一个函数指针
struct S { int x; };
S s; // s是左值,因为它是一个对象
int &r = s.x; // r是左值,因为它是一个对象的成员

3.3.2 右值

??????C++中的右值(Rvalue)是指不能出现在赋值语句左边的表达式。右值可以是临时对象、函数返回值、字面量等。右值必须满足以下条件之一:

  1. 是一个临时对象,如局部变量、动态内存分配等;
  2. 是一个函数调用,且返回类型为右值引用;
  3. 是一个字面量,如整数、浮点数、字符等。

右值的主要特点有:

  1. 临时性:右值是临时创建的对象,当离开其作用域时会被销毁;
  2. 移动语义:右值支持移动语义,可以将资源从一个对象转移到另一个对象,而不需要复制数据;
  3. 可以作为左值使用:右值可以作为左值进行赋值操作,但只能用于移动语义的操作。

????????左值拥有持久的状态,而右值要么是字面常量,要么是在表达式求职过程中创建的临时对象。

????????右值引用的对象将要被销毁。不能将一个右值引用绑定到一个右值引用类型的变量上,因为变量都是左值。

3.3.3 左右值总结

????????左值 (lvalue, left value),顾名思义就是赋值符号左边的值。准确来说, 左值是表达式(不一定是赋值表达式)后依然存在的持久对象。

????????右值 (rvalue, right value),右边的值,是指表达式结束后就不再存在的临时对象。

而 C++11 中为了引入强大的右值引用,将右值的概念进行了进一步的划分,分为:纯右值、将亡值。

????????纯右值 (prvalue, pure rvalue),纯粹的右值,要么是纯粹的字面量,例如 10, true; 要么是求值结果相当于字面量或匿名临时对象,例如 1+2。非引用返回的临时变量、运算表达式产生的临时变量、 原始字面量、Lambda 表达式都属于纯右值。

????????需要注意的是,字面量除了字符串字面量以外,均为纯右值。而字符串字面量是一个左值,类型为 const char 数组。例如:

#include <type_traits>
int main() {
    // 正确,"01234" 类型为 const char [6],因此是左值
    const char (&left)[6] = "01234";
    // 断言正确,确实是 const char [6] 类型,注意 decltype(expr) 在 expr 是左值
    // 且非无括号包裹的 id 表达式与类成员表达式时,会返回左值引用
    static_assert(std::is_same<decltype("01234"), const char(&)[6]>::value, "");
    // 错误,"01234" 是左值,不可被右值引用
    // const char (&&right)[6] = "01234";
}

但是注意,数组可以被隐式转换成相对应的指针类型,而转换表达式的结果(如果不是左值引用)则一定是个右值(右值引用为将亡值,否则为纯右值)。例如:

const char*   p   = "01234";  // 正确,"01234" 被隐式转换为 const char*
const char*&& pr  = "01234";  // 正确,"01234" 被隐式转换为 const char*,该转换的结果是纯右值
// const char*& pl = "01234"; // 错误,此处不存在 const char* 类型的左值
// 将亡值 (xvalue, expiring value),是 C++11 为了引入右值引用而提出的概念
// 因此在传统 C++ 中, 纯右值和右值是同一个概念,也就是即将被销毁、却能够被移动的值。
// 将亡值可能稍有些难以理解,我们来看这样的代码:
std::vector<int> foo() {
    std::vector<int> temp = {1, 2, 3, 4};
    return temp;
}
std::vector<int> v = foo();

????????在这样的代码中,就传统的理解而言,函数 foo 的返回值 temp 在内部创建然后被赋值给 v, 然而 v 获得这个对象时,会将整个 temp 拷贝一份,然后把 temp 销毁,如果这个 temp 非常大, 这将造成大量额外的开销(这也就是传统 C++ 一直被诟病的问题)。在最后一行中,v 是左值、 foo() 返回的值就是右值(也是纯右值)。但是,v 可以被别的变量捕获到, 而 foo() 产生的那个返回值作为一个临时值,一旦被 v 复制后,将立即被销毁,无法获取、也不能修改。 而将亡值就定义了这样一种行为:临时的值能够被识别、同时又能够被移动。

????????在 C++11 之后,编译器为我们做了一些工作,此处的左值 temp 会被进行此隐式右值转换, 等价于 static_cast<std::vector<int> &&>(temp),进而此处的 v 会将 foo 局部返回的值进行移动。 也就是后面我们将会提到的移动语义。

3.3.4?左值引用

????????左值引用是常规的引用:变量都是左值。

????????左值拥有持久的状态。引用都可以绑定到左值上。引用都是变量。

一个正确的例子:

int c = 10;
int & c1 = c;
int && c2 = 11;
int & c3 = c1;
int & c4 = c2;

3.3.5?右值引用

????????右值引用是必须绑定到右值的引用,他有着与左值引用完全相反的绑定特性,我们通过 && 来获得右值引用。右值有一个重要的性质——只能绑定到一个将要销毁的对象上。

? ? ?右值要么是字面常量,要么是在表达式求职过程中创建的临时对象。

????????右值引用的对象将要被销毁。不能将一个右值引用绑定到一个右值引用类型的变量上,因为变量都是左值。

错误的例子:

int c = 10;
int & c1 = c;
int && c2 = 11;
int & c3 = c1;
int & c4 = c2;
//int && c5 = c2; //错误
//int && c6 = c3; //错误

3.5 万能引用

????????万能引用(Universal Reference)是指模板参数使用 && 时,能够接受任何类型的引用,包括左值引用和右值引用。右值引用是指绑定到右值的引用,可以实现移动语义和完美转发。

3.5.1 万能引用与右值引用的区别? ? ? ??

? ? ? ? (1)万能引用是模板参数的一种表达方式,而右值引用是一种变量类型。

? ? ? ? (2)万能引用可以接受任何类型的引用,包括左值引用和右值引用,而右值引用只能接受右值引用。

? ? ? ? (3)万能引用在模板函数中用于实现完美转发而右值引用主要用于实现移动语义

? ? ? ? (4)万能引用的声明方式为 T&&,而右值引用的声明方式为 X&&,其中 T 和 X 都表示类型。

一个例子:

#include <iostream>
#include "common.h"

using namespace std;

namespace QUOTE2_DAY19
{

    template<typename T, typename U = int> // 仅仅是默认使用int类型,也可以更改
    void f(T && t1, U && t2)
    {
        cout << t1+t2 << endl;
    }
};

int main(int argc, char *argv[])
{
    {
        __LOG__("万能引用");
        using namespace QUOTE2_DAY19;
        int per1 = 23;
        int per2 = 34;
        cout << "左左: ";
        f(per1,per2);
        cout << "右右: ";
        f(23,34);
        cout << "左右: ";
        f(per1,34);
    }

    return 0;
}

? ? ? ? 运行结果:

? ? ? ? 万能引用可以作为函数返回值的类型,这样返回值既可以接收左值又可以接收右值。同时万能引用在完美转发中也会用到。

3.6 移动语义

? ? ? ? 右值引用可以实现移动语义。可以将左值转换成对应的右值引用类型,通过std::move()来实现。

一个简单例子:

int && a1 = 10;
int a2 = 23;
cout << "移动语义前a1: " << a1 << endl;// 10
a1 = std::move(a2);
cout << "移动语义前后a1: " << a1 << endl;// 23

? ? ? ? 分析结果得到右值引用通过std::move(a2) 传入了左值,实现了移动语义。

3.7 完美转发

????????完美转发的目的是使用函数函数模板调用另一个函数时,希望既可以接收左值也可以接收右值。要调用的函数的参数不是万能引用,而是既可以接收左值又可以接收右值。因此需要对传入的参数使用std::forward<>做完美转发,以满足左值引用和右值引用。

????????通过函数模板调用另外一个函数,模板的万能引用既可以接收左值也可以接收右值,但是对于函数内部来说,不管接收的是左值还是右值,模板函数内部对于形参都是左值(T && t1 ,t1本身是左值)。

????????如果函数的第一个参数需要右值,必须这样调用f(std::move(t1),t2),但是模板是通用的,我们不能直接将std::move()来写死,这样就不能调用接收左值的函数了,因此我们使用完美转发std::forward<>来实现。

一个例子:

#include <iostream>
#include "common.h"

using namespace std;


template<typename F ,typename T, typename U>
void testFun(F f, T && t1, U && t2)
{
    f(std::forward<T>(t1),std::forward<U>(t2));
}

void gu_y(int && t1, int & t2) // 接收左值和右值
{
    cout << t1+t2 << endl;
}

int main(int argc, char *argv[])
{
    {
        __LOG__("完美转发");
        int per1 = 23;
        int per2 = 34;

        cout << "左右: ";
        testFun(gu_y,23,per2);// 传入右值和左值
    }

    return 0;
}

? ? ? ? 运行结果:

? ? ? ? 对于T && t1 ,t1始终是变量是左值,因此在传入函数gu_y()时,需要将一个左值转换成右值,又由于这是一个模板函数,因此不可以使用移动语义将其直接固定为右值,所以提出了完美转发。

3.8 一些特殊的引用

? ? ? ? 前置自增为左值,后置自增为右值。

int & ko1 = ++lp;// 前置自增为左值
int && ko2 = lp++; // 后置自增为右值

? ? ? ? 可以用万能引用,来推导类型

int lo = 5;
auto && v1 = lo;//auto 为 int ,v1的类型为int &
auto && v2 = 6;// auto 为int,v2的类型为int &&
auto v3 = lo; // v3的类型为int

4 引用和指针的区别

  1. 定义方式不同:指针是一个变量,存储另一个变量的地址;引用是一个别名,与另一个变量共享同一块内存空间。

  2. 操作方式不同:指针可以进行加减运算,表示指向内存中的其他位置;引用只是一个别名,不能进行加减运算。

  3. 空值处理不同:指针可以为空,表示不指向任何内存位置;引用必须连接到一个实际的对象,不能为空。

  4. sizeof运算符不同:sizeof(指针)返回的是指针本身所占用的内存大小;sizeof(引用)返回的是引用所绑定的对象所占用的内存大小。

  5. const修饰不同:const指针可以指向常量对象,也可以指向非常量对象;const引用必须连接到一个常量对象。

文章来源:https://blog.csdn.net/m0_47324800/article/details/135093238
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。