1. 引用

1.1 左值、纯右值与将亡值

类别

全称

中文

特点

底层存储位置(典型情况)

lvalue

left-value

左值

有身份(identity),不可移动(通常)

栈(局部变量)、堆(动态分配)、全局/静态区(全局或 static 变量) —— 有确定、持久的内存地址

rvalue

right-value

右值

可移动(movable)

(右值是 prvalue 和 xvalue 的统称)

└─prvalue

pure rvalue

纯右值

无身份(如字面量、临时对象)

寄存器(小类型如 int/double)调用者栈帧中的临时对象区域(经 RVO/NRVO 优化后);生命周期极短,可能根本不在内存中

└─xvalue

expiring value

将亡值

有身份 + 可移动

与原始对象相同的位置(如栈、堆、全局区)—— 它不是新对象,只是对已有对象的“可移动视图”

1.2 & 用作函数参数

普通按值传参使得函数调用值的拷贝,而按引用传参可直接访问该变量。

所谓“按引用传参”,底层本质是传递地址的值,然后通过该地址间接访问原始内存。

例:

void foo(int &x) {
    x = 100;
}
int main() {
    int a = 5;
    foo(a);
    // a == 100
}
  • 编译器实际上把 int &x 编译成一个常量指针(int * const x)。
  • 调用时自动传 &a,函数内使用 x 时自动解引用。
  • 所以,引用在底层仍然是通过地址访问,但由编译器自动处理,避免显式 * 和 &。

1.3 & 用作函数返回值

返回引用不可以返回临时变量(作用域在函数内的),可以将一个形参作为返回值,或使用new开辟一段内存空间。

函数的返回值一般为右值(临时变量,位于临时内存单元,执行下一条语句可能就会消失,不可取地址),而返回引用为左值(除了右值引用&&),如果函数声明为:

int& Function (int a, int b);

使得在编写下面语句时不会报错:

Function(a,b)= 2;

故需要使函数返回值为 const &(为不可更改的左值,不可被赋值),避免上述错误,正确编写:

const int& Function(int& a, int& b);

返回的引用必须是函数中作为参数的引用,否则便是返回局部变量。

即:

int& identity(int& x) {
    return x; // ✅ 安全!x 是调用者传进来的对象的引用
}

int main() {
    int a = 10;
    int& r = identity(a); // r 就是 a 的别名
    r = 20;               // 修改 a
}

例:

int global = 100;

int& getRef() {
    return global; // 返回一个已有变量的引用
}

int main() {
    getRef() = 200; // 合法!因为返回的是左值
    int* p = &getRef(); // 合法!可以取地址
}

1.4 & 用作复制构造函数

复制构造函数的形参必须是引用,否则会无限递归,如:

Myclass(const Myclass);

这里形参不是引用,在传实参 myClass1 时,按值传递会进行复制,myClass1 被复制时又会调用拷贝构造函数,再次传参从而复制从而调用拷贝构造函数,造成没有终止条件的无穷递归。

1.5 右值引用

1.5.1 std::move

int a = 10;
int& ra = a;   // 左值引用 引用 左值
int&& rr1 = 10; // 右值引用 引用 右值,延长了其生命周期(直到 rr1 作用域结束)
int&& rr2 = std::move(rr1); // 资源所有权转移,rr1是左值,std::move将其转化为将亡值
  • 右值引用是个类型,右值引用类型的变量才分左右值,这里rr1是个右值引用类型的变量,为左值。
  • std::move 的本质是将一个对象转换为右值引用,允许资源被转移而不是拷贝,提高性能。
  • std::move 不移动任何东西,它只是“类型转换”;真正的移动发生在移动构造函数或移动赋值运算符中。
  • 无论是左值引用还是右值引用,本质上都是在给这一块内存起别名,当右值引用引用右值时,则是对该内存的所有权进行转移,原来右值的资源(如vector和类所持有的内存块的指针,动态分配的内存)等处于一个未定义的状态(通常是nullptr,但也可以是其他值,具体取决于实现)。如:
std::string a = "hello world";  // a 拥有堆上的字符数据
std::string b = std::move(a); 
// 即:
std::string b(std::string&& other);  // 移动构造函数被调用
// 移动构造函数的实现:
std::string::string(std::string&& other) {
    ptr = other.ptr;        // 1. 把 other 的指针“拿过来”
    size = other.size;
    capacity = other.capacity;

    other.ptr = nullptr;    // 2. 把 other 置为空,不再拥有资源
    other.size = 0;
    other.capacity = 0;
}

原来的对象仍然在内存中存在, 只是其所拥有的指针和值等被置空.

对const对象无效,移动会变为拷贝:

const std::string str = "Hello";
std::string newStr = std::move(str); // 实际上调用了拷贝构造函数

常用于容器中的元素移动:

#include <iostream>
#include <vector>
#include <string>
#include <utility>
 
int main() {
    std::vector<std::string> vec;
    std::string str = "Hello, World!";
    
    vec.push_back(std::move(str)); // 将 str 的内容移动到 vec 中
 
    std::cout << "vec[0]: " << vec[0] << "\n"; // 输出 "Hello, World!"
    std::cout << "str: " << str << "\n";       // 输出空字符串(资源已被移动)
    return 0;
}

在使用std::move前,应确原对象不会再次被使用。

1.5.2 移动构造函数

#include <iostream>
#include <utility> // For std::move
 
class Resource {
private:
    int* data; // 指向动态分配的资源
public:
    // 构造函数
    Resource(int size) : data(new int[size]) { };
 
    // 移动构造函数
    Resource(Resource&& other) noexcept : data(other.data) {
        other.data = nullptr; 
    }
 
    // 析构函数
    ~Resource() {
        delete[] data; // 释放资源
    }
 
    // 禁止复制构造函数和复制赋值运算符 
    Resource(const Resource&) = delete;
    Resource& operator=(const Resource&) = delete;
 
    // 移动赋值运算符
    Resource& operator=(Resource&& other) noexcept {
        delete[] data; // 释放当前资源
        data = other.data;
        other.data = nullptr; // 将资源指针设置为nullptr
        return *this;
    }
};
int main() {
    Resource r1(10); // 创建Resource对象r1
    Resource r2 = std::move(r1); // 将r1移动到r2
 
    // 此时,r1处于未定义状态,r2接管了r1的资源
    // 下面的代码是危险的,因为它试图访问r1的资源,而r1的资源已经被移动
    // std::cout << *r1.data << std::endl; // 未定义行为
 
    // 正确做法是不再使用r1,因为它已经没有资源了
    // r2将继续存在,直到其生命周期结束,此时将释放资源
 
    return 0;
}

这里的未定义状态是在移动构造函数或移动赋值运算符中实现的。

1.5.3 &&用作函数返回值

语义:返回一个 将亡值(xvalue)

std::string global = "hello";

std::string&& getRvalueRef() {
    return std::move(global); // 返回 global 的右值引用
}
std::string s = getRvalueRef(); // 调用移动构造函数(高效)

1.6 通用引用

如果 T&& 出现在模板中,并且 T 是通过类型推导确定的,则 T&& 是通用引用 。

template <typename T>
void foo(T&& param);  // 通用引用
 
foo(42);  		// T 推导为 int,为通用引用
foo<int>(42);   // 显式指定 T,此时 T&& 是普通右值引用

用于实现完美转发:

  • 如果传入的是右值,T&& 表示右值引用。
  1. 如果传入的是左值,T&& 表示左值引用。
  • 常与std::forward一起使用,如std::forward<int>(x)。

2. 指针

2.1 指针的风险

2.1.1 声明而不开辟

int* a;
*a = 1;
  1. 执行 int* a; 创建指针时,内存中只开辟了a的内存块,而a的值未知即 a 这个内存块的值未知,未开辟 a 指向的内存块,执行 *a = 1 时,如果 a 碰巧=100,则地址为100的内存块中就会存储为1,即使该内存块正在被其他地方使用,这可能导致bug。
  2. 故在声明指针而不赋值时,应使用new开辟指针所指向的内存块,即又有 a 的内存块,又有 a 指向的地址的内存块,使用完后使用delete释放内存,但忘记使用delete或出现异常未执行delete会导致内存泄露。
  3. 但如果只对a操作,而不操作内存块,则无风险。

2.1.2 返回局部变量的地址

局部变量存储在栈上,函数返回时,局部变量的生命周期结束,这个地址指向的局部变量(栈上的内存)可能已经不再属于原来的变量。

2.2 数组名

数组名作为形参传递给函数时,会退化为指针,此时使用sizeof(数组名)(sizeof 是编译期运算符,不是函数)会得到指针大小而不是数组大小。

2.3 成员函数名

静态成员函数名即为地址,如:

静态成员函数名即为地址,如:

MyClass::StaticMemberFunction

非静态成员函数名包含隐式的this指针,若获取指针,使用&,如:

&MyClass::MemberFunction

2.4 智能指针

2.4.1 unique_ptr

        std::unique_ptr 是基于 RAII(资源获取即初始化) 和 移动语义 实现的独占式智能指针。它内部封装了一个原始指针和一个可配置的删除器(Deleter),默认使用 delete 或 delete[](数组特化)。它通过删除拷贝构造函数和拷贝赋值运算符,禁止了共享行为,确保同一时刻只有一个 unique_ptr 拥有资源。资源的释放发生在其析构函数中,自动调用删除器,实现异常安全的资源管理。所有操作在编译期确定,无运行时开销,性能几乎与裸指针一致。支持自定义删除器(如 fclose、CloseHandle),可用于管理非内存资源。

不允许被赋值,除非源unique_ptr是个临时的右值:

std::unique_ptr<int> Function(int);
std::unique_ptr<int> a;
a = Function(1);

可以移动,ptr1 的所有权被移动到了 ptr2,ptr1 变成了一个空的智能指针,不会释放任何资源:

std::unique_ptr<int> ptr1(new int);
std::unique_ptr<int> ptr2 = std::move(ptr1); 

2.2.2 shared_ptr

        std::shared_ptr 实现共享所有权核心机制是引用计数。它内部包含两个指针:一个指向管理的对象,另一个指向控制块。

控制块中包含:

  1. 强引用计数(shared_ptr 的数量)
  2. 弱引用计数(weak_ptr 的数量)
  3. 自定义删除器
  4. 分配器
  5. 可能的对象指针(make_shared 优化时)

        每次 shared_ptr 被拷贝,强引用计数原子递增;析构时递减。当强引用计数归零时,调用删除器释放对象。控制块本身在最后一个 shared_ptr 和 weak_ptr 都释放后才被销毁。引用计数的操作是 原子的,因此多个线程同时拷贝或析构不同的 shared_ptr 实例是线程安全的(但操作同一个实例仍需加锁)。

        make_shared 合并对象与控制块的内存分配,减少开销、提升性能和异常安全性。

        多个 std::shared_ptr 可以共享同一个对象的所有权,当所有 std::shared_ptr 都不再引用该对象时,对象会被自动释放。shared_ptr 的循环引用会导致内存泄漏,两个对象互相持有对方的 shared_ptr,且main函数中又有两个对象的shared_ptr,它们的引用计数都为 2(一个来自 main,一个来自对方),退出作用域后仍为 1,无法归零。

2.4.3 weak_ptr

        std::weak_ptr 是 shared_ptr 的观察者,用于解决循环引用问题。它不增加强引用计数,只增加控制块中的弱引用计数。weak_ptr 不能直接访问所指对象,必须通过 lock() 方法尝试提升为 shared_ptr。lock() 会检查强引用计数:

  1. 如果 > 0,返回一个有效的 shared_ptr(强引用 +1)
  2. 如果为 0(对象已销毁),返回空 shared_ptr

这种机制允许安全地检查对象是否还存在,而不会延长其生命周期。

控制块的销毁时机:当强引用计数为 0 且弱引用计数也为 0 时,控制块本身被释放。这意味着即使对象已销毁,只要还有 weak_ptr 存在,控制块仍保留,以便 lock() 能正确判断状态。

例:两个对象互相持有 shared_ptr,导致内存泄漏(循环引用)

#include <iostream>
#include <memory>

class B; // 前向声明

class A {
public:
    std::shared_ptr<B> b_ptr;
    ~A() { std::cout << "A destroyed\n"; }
};

class B {
public:
    std::shared_ptr<A> a_ptr; // 循环引用
    ~B() { std:: std::cout << "B destroyed\n"; }
};

int main() {
    auto a = std::make_shared<A>();
    auto b = std::make_shared<B>();

    a->b_ptr = b; // A 持有 B
    b->a_ptr = a; // B 持有 A → 循环!

    std::cout << "a.use_count(): " << a.use_count() << "\n"; // 输出 2
    std::cout << "b.use_count(): " << b.use_count() << "\n"; // 输出 2

    return 0;
    // 程序结束,但 A 和 B 都不会被销毁!因为彼此的 shared_ptr 使引用计数 ≥1
}

输出:

a.use_count(): 2
b.use_count(): 2
// 没有 "A destroyed" 或 "B destroyed"!内存泄漏!

解决方案:将其中一个指针改为 weak_ptr

通常在“观察者”或“从属”关系中使用 weak_ptr。比如 B 是 A 的附属,那么 B 不应拥有 A。

#include <iostream>
#include <memory>

class B;

class A {
public:
    std::shared_ptr<B> b_ptr;
    ~A() { std::cout << "A destroyed\n"; }
};

class B {
public:
    std::weak_ptr<A> a_ptr; // 改为 weak_ptr!不再增加强引用计数
    ~B() { std::cout << "B destroyed\n"; }

    void doSomething() {
        // 安全地访问 A(如果还存在)
        if (auto locked = a_ptr.lock()) {
            std::cout << "A is still alive!\n";
        } else {
            std::cout << "A has been destroyed.\n";
        }
    }
};

int main() {
    auto a = std::make_shared<A>();
    auto b = std::make_shared<B>();

    a->b_ptr = b;
    b->a_ptr = a; // weak_ptr 赋值,不增加 a 的强引用计数

    std::cout << "a.use_count(): " << a.use_count() << "\n"; // 1(只有 main 中的 a)
    std::cout << "b.use_count(): " << b.use_count() << "\n"; // 2(main + a->b_ptr)

    b->doSomething(); // 输出: A is still alive!

    return 0;
    // 函数结束:
    //   - a 被销毁 → A 对象析构
    //   - b 被销毁 → B 对象析构
}
  • shared_ptr 和 weak_ptr 共享同一个控制块(包含强引用计数、弱引用计数、删除器等)。
  • 对象销毁时机强引用计数 == 0 → 调用析构函数,释放对象内存。
  • 控制块销毁时机强引用计数 == 0 且 弱引用计数 == 0 → 释放控制块本身。
  • 因此,只要还有 weak_ptr 存在,控制块就保留,使得 lock() 能正确返回空 shared_ptr。
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐