目录
[1. this关键字](#1. this关键字)
[1.1 this 的本质](#1.1 this 的本质)
[1.2 const 成员函数中的 this](#1.2 const 成员函数中的 this)
[2. 对象生命周期(栈作用域)](#2. 对象生命周期(栈作用域))
[2.1 基本概念](#2.1 基本概念)
[2.2 作用域指针](#2.2 作用域指针)
[3. 智能指针](#3. 智能指针)
[3.1 为什么需要智能指针?](#3.1 为什么需要智能指针?)
[3.2 std::unique_ptr(独占所有权)](#3.2 std::unique_ptr(独占所有权))
[3.3 std::shared_ptr(共享所有权)](#3.3 std::shared_ptr(共享所有权))
[3.4 std::weak_ptr(弱引用)](#3.4 std::weak_ptr(弱引用))
[3.5 注意事项](#3.5 注意事项)
[3.6 使用总结](#3.6 使用总结)
[4. 复制与拷贝构造函数](#4. 复制与拷贝构造函数)
[4.1 基本概念](#4.1 基本概念)
[4.2 浅拷贝 vs 深拷贝](#4.2 浅拷贝 vs 深拷贝)
[4.3 避免隐式拷贝](#4.3 避免隐式拷贝)
1. this关键字
1.1 this 的本质
-
this是一个指向当前对象 的指针(ClassName* const this),在非静态成员函数内部可用。 -
它由编译器隐式传递给成员函数(作为第一个隐藏参数),指向调用该成员函数的对象实例。
class Entity {
private:
int m_X;
public:
void SetX(int x) {
// 编译器自动将 this 传入,this 指向调用该函数的对象
this->m_X = x; // 等价于 m_X = x;
}
};
1.2 const 成员函数中的 this
-
在 const 成员函数 中,
this的类型是const ClassName* const(指向常量的常量指针)。 -
这意味着不能通过
this修改任何非 mutable 成员变量。
cpp
class Entity {
int m_X;
public:
int GetX() const {
// this 的类型是 const Entity*
// this->m_X = 10; 错误:不能修改
return this->m_X; // 允许读取
}
};
2. 对象生命周期(栈作用域)
2.1 基本概念
-
对象生命周期:从对象创建(构造函数执行)到销毁(析构函数执行)的时间段。
-
栈作用域 :对象在进入作用域时创建,离开作用域时自动销毁(自动存储期)。这是 C++ 最安全、最高效的内存管理方式。
cpp
void function() {
Entity e; // 进入作用域,构造
// ... 使用 e
} // 离开作用域,自动析构
2.2 作用域指针
cpp
class ScopedPtr {
private:
Object* m_Ptr;
public:
ScopedPtr(Object* ptr) : m_Ptr(ptr) {}
~ScopedPtr() { delete m_Ptr; }
};
int main() {
{
ScopedPtr e = new Object(5); // 隐式转换:new Object(5) 传给构造函数
} // 离开作用域,~ScopedPtr 自动 delete m_Ptr
std::cin.get();
}
- 利用栈对象的自动析构,确保资源(内存、文件句柄、锁等)不会泄漏。
3. 智能指针
3.1 为什么需要智能指针?
-
原始指针(
Object*)需要手动delete,容易忘记导致内存泄漏,或过早delete导致悬空指针。 -
智能指针利用栈对象的析构自动释放堆内存,遵循 RAII 原则。
-
C++11 标准库提供了三种智能指针:
std::unique_ptr、std::shared_ptr、std::weak_ptr。
3.2 std::unique_ptr(独占所有权)
-
独占 :同一时刻只能有一个
unique_ptr指向一块内存。 -
不可拷贝,只能移动 :拷贝构造和拷贝赋值运算符重载被删除,但支持移动语义(
std::move)。 -
开销小 :与原始指针大小相同,析构时自动
delete内部指针。
cpp
#include <memory>
// 推荐:使用 std::make_unique (C++14)
auto ptr = std::make_unique<Object>(5);
// C++11 需使用原始 new
std::unique_ptr<Object> ptr(new Object(5));
std::unique_ptr<Object> a = std::make_unique<Object>();
// std::unique_ptr<Object> b = a; // 错误!不能拷贝
std::unique_ptr<Object> b = std::move(a); // 正确,a 转移所有权给 b,a 变为空
3.3 std::shared_ptr(共享所有权)
-
多个指针共享同一对象 :内部使用引用计数 (reference count)记录有多少个
shared_ptr指向同一块内存。 -
当最后一个
shared_ptr被销毁时(引用计数降为 0),自动释放对象内存。 -
引用计数存储在控制块(control block)中,与对象内存分离。
cpp
// 推荐:std::make_shared(一次分配,同时创建对象和控制块,效率更高)
auto ptr = std::make_shared<Object>(5);
// 也可以使用原始 new
std::shared_ptr<Object> ptr(new Object(5));
std::shared_ptr<Object> sp1 = std::make_shared<Object>(); // 引用计数 = 1
{
std::shared_ptr<Object> sp2 = sp1; // 引用计数 = 2
// sp2 离开作用域,引用计数变为 1
}
// 此时引用计数 = 1,对象仍未释放
sp1.reset(); // 引用计数 = 0,对象释放
3.4 std::weak_ptr(弱引用)
-
不增加引用计数 :弱引用指向
shared_ptr管理的对象,但不拥有所有权。 -
可能悬空 :对象可能已被其他
shared_ptr释放。 -
必须转换为
shared_ptr才能访问对象 (通过lock()方法)。
cpp
std::shared_ptr<Object> sp = std::make_shared<Object>();
std::weak_ptr<Object> wp = sp; // wp 观察 sp,引用计数不变
if (auto locked = wp.lock()) { // 尝试提升为 shared_ptr
locked->DoSomething(); // 安全访问
} else {
// 对象已被释放
}
3.5 注意事项
- 循环引用 :两个
shared_ptr互相引用对方(例如双向链表),导致引用计数永远不为 0,内存泄漏。解决办法是使用weak_ptr打破循环。
cpp
#include <iostream>
#include <memory>
class Node {
public:
std::shared_ptr<Node> next; // 指向下一个节点
std::shared_ptr<Node> prev; // 指向前一个节点
int value;
Node(int val) : value(val) {
std::cout << "Node(" << value << ") constructed\n";
}
~Node() {
std::cout << "Node(" << value << ") destructed\n";
}
};
int main() {
{
auto node1 = std::make_shared<Node>(1);
auto node2 = std::make_shared<Node>(2);
node1->next = node2; // node1 的 next 指向 node2
node2->prev = node1; // node2 的 prev 指向 node1
// 此时引用计数:
// node1: 自身 (1) + node2->prev (1) = 2
// node2: 自身 (1) + node1->next (1) = 2
} // 离开作用域,node1 和 node2 的 shared_ptr 局部变量销毁
// 但 node1 和 node2 的引用计数各减 1 后变为 1(因为互相仍有引用)
// 内存永远不会释放,析构函数不会调用
std::cout << "End of main (memory leaked)\n";
return 0;
}
解决方案:使用 weak_ptr 打破循环
cpp
#include <iostream>
#include <memory>
class Node {
public:
std::shared_ptr<Node> next; // 下一个节点:强引用
std::weak_ptr<Node> prev; // 前一个节点:弱引用(不增加引用计数)
int value;
Node(int val) : value(val) {
std::cout << "Node(" << value << ") constructed\n";
}
~Node() {
std::cout << "Node(" << value << ") destructed\n";
}
};
int main() {
{
auto node1 = std::make_shared<Node>(1);
auto node2 = std::make_shared<Node>(2);
node1->next = node2; // node2 引用计数 +1 → 2
node2->prev = node1; // 弱引用,node1 引用计数不变(仍为 1)
// 此时引用计数:
// node1: 自身 (1) (因为 prev 是 weak_ptr,不增加)
// node2: 自身 (1) + node1->next (1) = 2
} // 离开作用域:
// node1 销毁 → 引用计数 1→0,释放 node1(调用析构)
// node2 销毁 → 引用计数 2→1(因为 node1->next 已随 node1 销毁而销毁),
// 然后 node2 引用计数变为 0,释放 node2
std::cout << "End of main (no leak)\n";
return 0;
}
- 性能开销 :比
unique_ptr多一份控制块内存和原子操作(线程安全的引用计数),适合真正需要共享所有权的场景。
3.6 使用总结
cpp
{
std::shared_ptr<Object> obj;
{
std::shared_ptr<Object> sharedObj = std::make_shared<Object>();
// make_shared 一次分配:对象内存 + 控制块,引用计数初始为 1
obj = sharedObj; // 拷贝赋值,引用计数变为 2
std::weak_ptr<Object> weakObj = obj; // 弱引用,不增加计数
std::unique_ptr<Object> object = std::make_unique<Object>();
// std::unique_ptr<Object> obj = object; // 错误!unique_ptr 不可拷贝
object->Print();
}
// 离开内层作用域:
// - sharedObj 销毁,引用计数从 2 减为 1(因为 obj 仍持有)
// - object(unique_ptr)销毁,自动 delete 其管理的 Object
// - weakObj 销毁,不影响引用计数
}
// 离开外层作用域:
// - obj 销毁,引用计数从 1 减为 0,delete 管理的内存
-
make_shared优于直接new:一次分配(对象+控制块),异常安全,效率高。 -
shared_ptr支持拷贝,引用计数随之增减。 -
weak_ptr不增加计数,用于观察和打破循环。 -
unique_ptr不可拷贝,只能移动。
4. 复制与拷贝构造函数
4.1 基本概念
-
复制:用一个已存在的对象创建另一个新对象,新对象与原对象内容相同(但通常应是独立的内存副本)。
-
拷贝构造函数 :一种特殊的构造函数,参数是当前类的 const 引用,用于定义"如何从另一个对象构造当前对象"。
-
默认拷贝行为 :编译器会生成一个默认的拷贝构造函数,执行浅拷贝(逐成员复制,对于指针只复制地址,不复制指向的数据)。
4.2 浅拷贝 vs 深拷贝
浅拷贝(默认拷贝构造器)
cpp
String(const String& other) : m_Buffer(other.m_Buffer), m_Size(other.m_Size) {}
两个对象的 m_Buffer 指向同一块堆内存 。当一个对象析构 delete[] 后,另一个对象的指针变成悬空指针;当第二个对象析构时,对同一块内存再次 delete 导致双重释放,程序崩溃。
深拷贝(可自定义拷贝构造)
cpp
String(const String& other) : m_Size(other.m_Size) {
m_Buffer = new char[m_Size + 1];
memcpy(m_Buffer, other.m_Buffer, m_Size + 1);
}
为新对象分配独立的内存,完整复制数据内容。两个对象的指针指向不同内存,完全独立,互不影响,析构时各自释放自己的内存。
4.3 函数传递参数的发式
值传递: 形参是实参的拷⻉,函数内部对形参的操作并不会影响到外部的实参。
指针传递: 也是值传递的⼀种⽅式,形参是指向实参地址的指针,当对形参的指向操作时,就相当于对实参本身进⾏操作。
引用传递: 实际上就是把引⽤对象的地址放在了开辟的栈空间中,函数内部对形参的任何操作可以直接映射到外部的实参上⾯。
4.4 避免隐式拷贝
在函数传参时,按值传递会调用拷贝构造函数,对于大型对象或管理资源的类,这种隐式拷贝开销很大,且可能引入不必要的深拷贝。使用引用可以避免拷贝,提升性能(尤其对于大对象)const修饰引用 保证函数不会修改原对象,语义清晰。
cpp
void PrintString(String s) { // 按值传递,拷贝构造
std::cout << s << std::endl;
}
String name = "Cherno";
PrintString(name); // 触发 String 的深拷贝,即使只读
void PrintString(const String& s) { // const 引用,不拷贝
std::cout << s << std::endl;
}
String name = "Cherno";
PrintString(name); // 无拷贝,直接使用原对象
何时需要按值传递
-
需要在函数内修改副本且不影响原对象(例如对参数进行排序、变换)。
-
对于小且简单 的类型(如
int、double、指针),按值传递通常比引用更快(引用也有开销)。