引用可以理解为 “给已存在的变量起别名”,通过引用可以直接操作原变量。C++ 中引用主要分为左值引用和右值引用。

一、左值引用(Type&

左值引用是对左值(可被取地址的变量、对象等持久化数据)的别名,声明形式为 Type& 引用名 = 原变量;

特性

1.必须初始化,且初始化后不能更改指向(从一而终)。

2.对引用的操作等价于对原变量的操作。

int a = 1;
int& b = a;  // b 是 a 的别名
b = 1001;    // 等价于 a = 1001
cout << a;   // 输出 1001

用途

(1)作为函数参数

1.直接修改实参:函数内部对引用的修改会直接影响外部实参。类似指针。

2.避免拷贝开销:对于大型对象,传递引用可减少内存拷贝,提高效率。

void modify(int& x) {  // x 是实参的别名
    x = 999;          // 直接修改实参
}

int main() {
    int a = 1;
    modify(a);        // 传入 a 的引用
    cout << a;        // 输出 999(a 被修改)
}
(2)作为函数返回值

1.允许返回值作为左值:若返回的是全局 / 静态变量或类成员的引用,可直接赋值。

注意禁止返回局部变量的引用(局部变量生命周期结束后会被销毁,引用会成为 “悬垂引用”,访问时 undefined behavior)。

int a = 1;  // 全局/静态变量(生命周期长)

int& getValue() {
    return a;  // 返回 a 的引用(合法)
}

int main() {
    getValue() = 10;  // 合法:相当于 a = 10
    cout << a;        // 输出 10
}
(3) const 限定的左值引用(常引用)

1.用于引用常量或临时值(右值),避免被修改。

const int& xx = 13;  // 合法:给常量 13 起别名(常引用)

经典错误

class Vector {
private:
    int& x;  // 引用成员
    int& y;  // 引用成员
public:
    Vector(int& a, int& b) : x(a), y(b) {}
    // 默认拷贝构造函数等价于:
    // Vector(const Vector& other) : x(other.x), y(other.y) {}  // 错误!
};

int main() {
    int a = 1, b = 2;
    Vector v1(a, b);
    Vector v2 = v1;  // 错误:无法复制引用成员
}

引用的本质是对象的别名,一旦初始化就不能更改其指向的对象。因此,当类中包含引用成员时,默认的拷贝构造函数会尝试重新初始化引用,但这是非法的。

举个简单示例理解一下这里的错误。

假设我们强行允许引用成员的拷贝:

int a = 1, b = 2;
Vector v1(a, b);     // x 引用 a,y 引用 b

int c = 3, d = 4;
Vector v2(c, d);     // x 引用 c,y 引用 d

v2 = v1;  // 假设允许拷贝,会发生什么?

1.若 x 和 y 重新绑定到 v1.x 和 v1.y 引用的对象(即 a 和 b),则违反引用不可重新绑定的规则。2.若 x 和 y 保持对 c 和 d 的引用,则拷贝操作没有意义。

解决办法:

class Vector {
private:
    int& x;
    int& y;
public:
    Vector(int& a, int& b) : x(a), y(b) {}
    Vector(const Vector&) = delete;             // 禁用拷贝构造函数
    Vector& operator=(const Vector&) = delete;  // 禁用赋值运算符
};

二、右值引用

右值引用是 C++11 引入的重要特性,用于实现移动语义和完美转发。

右值

只能出现在赋值号右边的临时表达式,即没有持久内存地址的对象。

特点:没有名字、不可取地址、生命周期短(当前表达式结束后销毁)。

纯右值(prvalue):基本类型的字面量、临时对象。

42;             // 整数字面量是右值
x + 5;          // 表达式结果是右值
std::string("tmp");  // 临时对象是右值

将亡值:通过 std::move 或右值引用生成的对象。

std::move(x);   // 将左值转为右值
int&& rr = 100; // rr 是右值引用,但 100 是右值

特性

右值引用是一种只能绑定到临时对象(右值)的引用,语法为 T&&

用途

1.延长临时对象的生命周期。(就是说,这一块内存已经要释放了,这里的资源我暂存一会,延长其生命。)

2.区分左值和右值参数,实现移动语义。

右值引用的核心作用是在对象所有权转移时避免深拷贝,直接复用临时对象的资源。

为什么要有右值引用呢?

在传统的情况里,我们会遇到这种情况:即你有一个类class TEMP,你实现了TEMP A,然后你现在需要将其复制给TEMP B,平常会调用传统的拷贝构造函数。

这当然没什么问题,但是如果这个A已经没什么用了,你准备释放了,只是想把值给B,那么这时候,调用拷贝构造函数就会进行深拷贝(创建新对象时,为所有资源(如动态内存、文件句柄)分配新空间,并复制数据。),需执行耗时的内存分配和数据复制。

但是如果你使用右值引用,那么这时候,你就可以将值直接给B,通过右值引用识别临时对象,直接转移资源所有权(如指针),避免深拷贝。

移动拷贝的过程

  • 临时对象(右值) 的资源(如动态分配的内存)被直接转移到新对象。
  • 原对象被置空(如指针设为 nullptr),确保析构时不会释放已转移的资源。

接下来举个例子:

#include <iostream>
#include <utility>

class MyString {
private:
    char* data;  // 指向动态分配的内存
    size_t length;

public:
    // 构造函数
    MyString(const char* str = "") {
        length = strlen(str);
        data = new char[length + 1];
        std::strcpy(data, str);
        std::cout << "构造: " << data << std::endl;
    }

    // 拷贝构造函数(深拷贝)
    MyString(const MyString& other) {
        length = other.length;
        data = new char[length + 1];
        std::strcpy(data, other.data);
        std::cout << "拷贝构造: " << data << std::endl;
    }

    // 移动构造函数(资源转移)
    MyString(MyString&& other) noexcept {
        data = other.data;       // 直接接管资源
        length = other.length;
        other.data = nullptr;    // 清空原对象
        other.length = 0;
        std::cout << "移动构造: " << data << std::endl;
    }

    // 析构函数
    ~MyString() {
        delete[] data;
    }
};

int main() {
    // 1. 创建临时对象(右值)
    MyString temp = MyString("temporary");  // 临时对象
    
    // 2. 通过右值引用转移资源
    MyString str = std::move(temp);         // 调用移动构造函数
    
    // 此时 temp 的 data 已被清空(nullptr)
    return 0;
}

Logo

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

更多推荐