引用的知识

常量引用可以绑定非常量左值、常量左值、右值,在绑定右值的时候可以将右值的生命周期延长,缺点是只能读不能改  示例: const int &a = 10; // 这里其实是编译器创建了一个临时变量,再将这个临时变量给a  const int temp = 10; const int &b = temp;

左值引用只能引用左值吗? - const左值引用能指向右值,局限是不能修改这个值。
右值引用只能引用右值吗? - 右值引用通过std::move()可以指向左值,实现资源转移

声明出来的左值引用和右值引用都是左值。 int &&j = 100; int &k = i; // j,k都是左值。

引用: 是一个别名、声明时必须要初始化、通过引用修改变量值

左值引用和右值引用的区别:
    左值引用避免对象拷贝,主要用在函数传参和函数返回值
    右值引用: 实现移动语义、实现完美转发

    什么是左值:
        特征: 可以在等号的左边、能够取地址、具名
        变量名(int a)、返回左值引用的函数调用、前置自增(++i = 10)、前置自减、赋值运算/复合赋值运算((i=9)=100; (i+=10) = 100;)、解引用(*a = 10)...
    什么是右值: 分为纯右值和将亡值
        特征: 只能放在等号右边、不能取地址、不具名
        纯右值: 字面值(具体的数字)、返回非引用类型的函数调用、后置自增/自减、算术表达式、逻辑表达式、比较表达式...
        将亡值: C++11新引入的与右值引用(移动语义)相关的值类型。
            将亡值可以用来触发移动构造或者移动赋值构造,并进行资源转移,之后将调用析构函数。


移动语义: 使用场景:STL中应用广泛、智能指针、std::function
    在c++11 之后,当函数试图返回一个对象的时候首先会尝试的调用移动构造函数,移动构造函数找不到才会调用拷贝构造函数,这样的话可以减少拷贝对象带来的性能开销
    使用场景: 对象赋值时,避免资源重新分配。
    实现: 通过移动构造以及移动赋值构造实现
    移动构造函数语法: 类名 (类名 &&源对象) {...}
    移动赋值函数语法: 类名 &operator=(类名 &&源对象) {...}

    各种构造函数示例:
    class A {
        public:
            A(int *dt):m_data(new int[sizeof(dt)]) { // 参数列表申请内存
                *m_data = *dt; // 赋值
            }
            A () {} // 默认构造函数
            A(const A &a):m_data(nullptr) { // 拷贝构造
                A temp(a.m_data);
                std::swap(m_data, temp.m_data); // 库里面的交换函数
            }
            A &operator=(A &a) { // 赋值构造
                if (this != &a) { // 避免自我赋值
                    A temp(a.m_data);
                    std::swap(m_data, temp.m_data);
                }
                return *this;
            }
            A(A &&a):m_data(nullptr) { // 移动构造
                std::swap(m_data, a.m_data);
            }
            A &operator=(A &&a) { // 移动赋值构造
                if (this != &a) { // 避免自我赋值
                    std::swap(m_data, temp.m_data);
                }
                return *this;
            }
            ~AA()
            {
                delete m_data;
                m_data = nullptr;
            }
        public:
            int *m_data;
    };
移动构造函数使用的注意点:
    生成条件: 自己实现了移动构造函数,编译器不会再提供拷贝构造。如果没有实现移动构造,并且析构、拷贝构造、拷贝赋值都没有实现的话,编译器会默认提供一个移动构造函数。
    执行方式: 默认生成的移动构造函数,对于内置成员会执行逐成员按字节拷贝,存在自定义类型成员,如果内部实现了移动构造就调用自身的移动构造,否则就调用拷贝构造。
    如果通过 A(A &&a) = default; 语句让编译器强制生成移动构造函数,那析构、拷贝构造、移动赋值这些函数就不会被编译器自动生成了。

完美转发: 函数模板可以将自己的参数完美地转发给内部调用的其他函数。完美是指不仅能准确地转发参数的值,还能保证被转发的参数的左右值属性不变。

完美转发的问题:
void func(int &n)
{
    cout << "lvalue=" << n << endl;
}

void func(int &&n)
{
    cout << "rvalue=" << n << endl;
}

template <typename T>
void revoke(T t)
{
    func(t);
}
c++11 之前的写法,t是左值,内部函数调用时只能调用到func(int &n)左值引用的接口。 revoke(8); 或者 int a = 13; revoke(a); // 两次调用都会调到func(int &n)接口
template <typename T>
void revoke(T &&t)
{
    func(forword<T>(t));
}
加入右值引用之后,调用revoke()接口时,如果传入参数是右值,将会调用到func(int &&n)右值引用的接口。例: revoke(8); 或者 int a = 13; revoke(std::move(a));
说明: 如果模板中(包括类模板和函数模板)函数的参数书写成为T&&,那么,函数既可以接受左值引用,又可以接受右值引用。

完美转发示例:
    完美转发的一个常用场景是在构造函数中转发参数到另一个构造函数,或者在泛型编程中将参数赚翻给其他函数,同时保持参数的原始类型和值类别(左值或者右值)。这对于设计通用库、容器或工具函数尤其有用,因为它们需要能够处理各种类型的参数。
    下面是一个使用完美转发来实现一个简单的工厂函数的实例。这个工厂函数可以创建任意类型的对象,并且能够接受任意数量和类型的构造函数参数。
    #include <iostream>
    #include <memory>
    #include <utility> // 包含std::forward
    
    class MyClass {
    public:
        MyClass(int x, double y) {
            cout << "MyClass constructed with x = " << x << ", y = " << y << endl;
        }
    };
    
    // 通用工厂函数模板
    template <typename T, typename... Args>
    std::unique_ptr<T> make_unique(Args&& ...args) {
        return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
    }
    
    int main()
    {
        auto myObj = make_unique<MyClass>(42, 13.14);
        
        return 0;
    }
    上述代码解析:
        ① make_unique 是自定义的一个工厂函数,模仿的是C++14标准库的std::make_unique函数,但这里我们展示了如何手动实现它来更好的理解完美转发
        ② 它使用了变参模板(<typename Args... args>)来接受任意数量和类型的参数
        ③ 使用std::forward<Args>(args)... 来完美转发这些参数到T类型的构造函数。这确保了每个参数的左值或者右值属性被保留
        ④ 返回一个指向新创建对象的std::unique_ptr<T>,提供了智能指针的内存管理优势。
        
    完美转发的实现原理: 涉及右值引用、转发引用、std::forward 和 引用折叠等概念。
        右值引用: 通过 && 符号声明的引用类型,用于绑定到临时对象(右值)。右值引用通常与完美转发一起使用,以支持将右值传递给其他函数。
        转发引用: 在模板函数中使用的引用,同时具有右值引用和左值引用的特性,也称为转发引用。转发引用使用模板参数推导机制来捕获传递给模板函数的参数的值类别,从而实现完美转发。
        std::forward: 是c++11 标准库中的一个工具函数,用于在模板函数中进行完美转发。它接受一个转发引用,并根据该引用的值类别(左值还是右值)选择性的转发参数。
        引用折叠: 是指模板函数在处理引用类型时发生的规则,用于确定最终的引用类型。当两个引用相遇时,编译器会根据一组规则将它们合并成一个引用类型。


常量引用的常见问题:
    常量引用的使用场景一般是用来修饰函数的形参,防止被误操作修改
        错误示例:
        函数定义: void func(const int &t) { t += 10; cout << t;}
        函数调用: int a = 1; func(1);
        说明: 定义中 t += 10; 语句会报错。原因是t被const修饰,不可被修改,报错: 表达式t必须是可修改的左值
    用引用接收一个函数的返回值(非引用值)时,需要加上const
        示例:
            int func(void) {int n = 10; return n;}
            int &a = func(); // 错误: 非常量引用的初始值必须是左值,而func()是右值
            说明: func()返回的是右值,要用引用接收该值时应该用常量引用或者右值引用接收。
            修改: int &&a = func(); 或者 const int &a = func();
    使用引用时类型不一样的场景
        示例:
            int i = 10; double &b = i; // 报错: 无法用"int"类型的值初始化 "double &"类型的引用(非常量限定)
            修改: const double &b = i;
            说明: 这里并不是类型不匹配的问题,而是编译器创建了一个临时变量来接收int类型i转化为double类型的值,而这个临时变量具有常量性,所以要加上const,告诉编译器我们用的是常引用。
 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值