指针基础
指针本身只是一个变量,只不过它的值正是另一个变量的内存地址。 但是其需要指定一个类型:去维护它的内存大小
C语言中两个问题的解决
-
野指针的解决:
何为野指针?就是指针未初始化时指向一个不确定的内存地址(没有实际意义)
C++中引入了空指针去解决这个问题,表示这个指针不指向任何对象,和C不同的是,C中使用的是NULL,而他也可以被解释成0,C++使用的nullptr 是一个明确的指针类型,可以避免这个问题。
-
悬空指针的解决:
何为悬空指针?其实是指向的内存已经被释放,但是指针本身没有被置空
int *p = new int(10); delete p; ***p = 100;** // 严重的未定义行为解决方式:delete之后就应该立刻将指针置为空,即
p = nullptr
与数组的基础结合
数组 = 一段连续空间,这是指针使用的绝佳方式。
-
举例:
int a[10]; int *p = a; // 等价于 int *p = &a;核心等价关系: a[i] 完全等价于
*(a+i) 和 *(p+i)。 -
这里的a和p有区别:
- 指针 p 是一个变量,可以进行 p++ 这样的自增操作。
- 数组名 a 是一个常量,不能进行 a++ 这样的操作,这会导致编译错误。
sizeof(a)返回整个数组的大小10个int,而sizeof(p)只返回指针自身的大小1个int。
多维数组和指针
C++中并不存在真正的多维数组,它本质上是“数组的数组”。一块连续的内存空间,被人为地划分为行和列。
- 指针的两种玩法:
- *指针指向元素 (int p): 这是最直接的玩法。
int *p = &a[0][0];或int *p = a[0];- 这里的a[0]是数组名,即指针常量
- p 指向数组的第一个整数。p++ 会移动到下一个整数。
- 访问 a[i][j] 就变成了 *(p + i * 列数 + j)。
- *指针指向数组 (int (q)[4]): 这种玩法更高级,也更容易混淆。
- int (*q)[4] = a;
- q 是一个指向“包含4个整数的数组”的指针。
- q++ 会跳过4个整数,直接移动到下一行。
- *指针指向元素 (int p): 这是最直接的玩法。
- 交错数组 (Ragged Array): 这不是C++的标准二维数组。它是一个指针数组,每个指针再分别指向一个一维数组。
int* arr[3];- 这种数组的每一行可以有不同的长度,内存不一定是连续的,因此更加灵活,但管理也更复杂。
顺时针螺旋法则
如上面所展示的那样,在指针这里,声明很重要,这会决定一个指针变量的含义以及其步长,但是它很难读,这里展示一种很好的阅读方案。
- 从变量名开始:找到你要解析的变量名。
- 向右看:从变量名开始,向右看,遇到 [] (数组) 或 () (函数),就先把它读出来。
- 向左看:当右边遇到 ) 或者到头了,再回到变量名,向左看,遇到 * (指针),就把它读出来。
- 跳出括号:如果整个声明被括号 () 包围,那就跳出这层括号,重复第2和第3步。
- 最后读类型:当所有 *, [], () 都解析完后,最后读出最左边的类型名 (如 int, char, void 等)。
-
实例解析 1:
int* arr[3];-
从变量名开始:找到变量名是 arr。
int* arr[3];
↑
-
向右看:从 arr 向右看,我们遇到了 [3]。
- [3] 的意思是“一个包含3个元素的数组”。
- 所以第一部分的解读是:“arr 是一个包含3个元素的数组...”
int* arr[3];
- ---->
-
向左看:右边到头了,回到 arr,向左看,我们遇到了 *。
- 的意思是“...的指针”。
- 所以第二部分的解读是:“...数组的每个元素都是一个指针...”
int* arr[3];
<--
-
跳出括号:这里没有括号包围,跳过。
-
最后读类型:解析完所有符号后,读最左边的类型 int。
- int 的意思是“...指向 int 类型”。
- 所以第三部分的解读是:“...这个指针指向的是 int 类型。”
组合起来的完整含义:
arr 是一个包含3个元素的数组,该数组的每一个元素都是一个指向 int 类型的指针。
-
维度的“降维打击”与“升维思考”
由于数组在内存中的连续性,我们可以巧妙地在函数传递中改变它的维度
-
维度退化 (Dimension Reduction):
- 将一个多维数组传递给一个期望一维数组的函数是完全可行的,这是一种“降维打击”。
int a[2][4]; maximum(a[0], 2*4); // 将其视为一个长度为8的一维数组 maximum(&a[0][0], 8); // 效果完全相同- 本质: 我们只是告诉函数:“这是一块连续内存的起始地址,以及它的总长度”,函数并不关心它原本是几行几列。
-
维度提升 (Dimension Augmentation):
- 反过来,我们也可以通过强制类型转换,让一个函数把一维数组当成多维数组来处理,这是“升维思考”。
int b[12]; show((int (*)[2])b, 6); // 将b看作一个6行2列的数组- “坑”: 这是一种非常危险但强大的操作。程序员必须自己保证逻辑上的维度和内存长度是匹配的,否则极易导致内存越界。
C风格的内存管理
在C++的new/delete和智能指针的出现之前,malloc和free是C语言操控堆内存的唯一手段。
- malloc是什么:
- malloc (memory allocation) 是一个函数,它向操作系统申请一块指定大小的原始、未初始化的堆内存。
- 它返回一个 void* 类型的指针,需要我们手动强制转换为所需的类型。
int *p = (int*)malloc(sizeof(int));
- malloc的原则: 为了效率,malloc的实现通常遵循以下原则,以避免频繁地、代价高昂地向操作系统申请内存(即系统调用 brk 或 mmap)。
- 减少系统调用: malloc会预先向操作系统申请一大块内存(内存池),然后自己在这块内存里进行管理和分配。
- 快速匹配: 内部通过不同的数据结构(如bins)来管理不同大小的空闲内存块,以便快速找到最合适的块进行分配。
- 减少锁竞争: 在多线程环境下,为每个线程维护独立的内存池(如ptmalloc, tcmalloc),避免线程之间因争夺全局内存锁而造成的性能瓶颈。
RAII & 智能指针
接上,C++中为了解决手动管理内存的种种问题,引入了RAII(资源获取即初始化)思想,智能指针就是其最典型的应用。
RAII —— 智能指针的灵魂
- 旧时代的“痛” (old_use):
- 在复杂的逻辑中,如果中间因为异常 (throw) 或者提前返回 (return),代码执行不到最后的 delete q;,那么这块内存就永远不会被释放,这就是内存泄漏 (Memory Leak)。
- 手动管理内存,既容易忘记释放,又难以保证异常安全。
- RAII的“优雅” (newer_use):
auto p = int_ptr(new Blob(a));// 1. 在构造函数中获取资源- 我们把裸指针 new Blob(a) 交给了一个栈上的对象 p 来管理。
- 无论函数是正常结束,还是因为异常、return等原因提前退出,栈展开 (stack unwinding) 机制都会保证栈上的对象 p 被正确地销毁。
- p 在其析构函数 (~int_ptr) 中自动释放资源 (delete ptr)。// 2. 在析构函数中释放资源
- 这就是RAII的魔力:将资源释放的责任从“人”转移给了“编译器一定会执行的规则”,从而实现了资源的自动管理,做到了谁申请,谁释放和异常安全。
auto_ptr 98
auto_ptr是C++对RAII思想的第一次尝试,但它有一个致命的缺陷,现在已经被废弃,我们了解它主要是为了避免在旧代码中踩坑。
-
反直觉的拷贝语义: 这是auto_ptr最大的“坑”。
auto_ptr<int> p1(new int(10)); auto_ptr<int> p2 = p1;- 这个“拷贝”操作,实际上执行的是所有权的转移。执行后,p2 获得了资源的所有权,而 p1 会自动变成一个空指针
- 此时访问 *p1 将导致程序崩溃。(悬空指针!!)
-
不适合STL容器: 这种诡异的拷贝行为,使得auto_ptr完全无法与STL容器(如std::vector)一起工作,因为容器在内部会进行大量的拷贝操作,会导致容器内的元素莫名其妙地“丢失”。
unique_ptr 11
-
专属所有权 (Exclusive Ownership): unique_ptr 保证在任何时刻,只有一个指针能指向一个给定的资源。
- 它从语法上禁止了拷贝。
unique_ptr<int> p1(new int(20)); unique_ptr<int> p2 = p1; // 编译错误!这样就从根本上杜绝了auto_ptr的错误。 -
明确的所有权转移: 如果你确实需要转移所有权,必须显式地使用 std::move。
unique_ptr<int> p2 = std::move(p1);- 这行代码清晰地表达了你的意图:“我要把p1的所有权转移给p2”,转移后p1变为空(nullptr)。
-
零开销抽象: unique_ptr 和裸指针的大小完全一样,性能开销也几乎为零,是C++“零开销抽象”理念的典范。
shared_ptr 11
当一个资源需要被多个所有者共同管理和拥有时,shared_ptr就派上了用场。
- 共享所有权 (Shared Ownership): shared_ptr通过**引用计数(
use_count())**来工作。- std::shared_ptr<int> p1{new int(10)}; // 此时引用计数为 1
- std::shared_ptr<int> p2 = p1; // 拷贝p1,引用计数变为 2
- std::shared_ptr<int> p3 = p2; // 拷贝p2,引用计数变为 3
- 自动释放: 每当一个shared_ptr被销毁(例如离开作用域),引用计数就会减一。当引用计数降为0时,代表没有任何人再需要这个资源了,资源会被自动释放。
- 实现原理: 一个shared_ptr对象内部通常包含两个指针:一个指向资源本身,另一个指向一个控制块。这个控制块中存储了引用计数等管理信息。所有指向同一资源的shared_ptr都共享同一个控制块。
weak_ptr 11
shared_ptr虽然强大,但它有一个致命的问题——循环引用 (Circular Reference)。
- 循环引用的“死锁”:
- 双向链表: 如果Node A的next指针(shared_ptr)指向Node B,而Node B的prev指针(shared_ptr)又指回Node A。即使没有任何外部指针指向这两个节点,它们各自的引用计数也永远是1,导致两个节点都无法被释放,造成内存泄漏。
- 观察者模式: 如果Teacher(发布者)持有Student(订阅者)的shared_ptr,而Student为了能回调老师,也持有了Teacher的shared_ptr,同样会形成循环引用。
- weak_ptr的解决方案: weak_ptr是一种不控制资源生命周期的智能指针。它像一个“观察者”,可以指向由shared_ptr管理的资源(仅仅观察),但是不会增加引用计数。
- 解决方式: 在上述场景中,将导致循环的那条引用关系改为weak_ptr。例如,双向链表中的prev指针,或者Student指回Teacher的指针。
- 安全访问: weak_ptr不能直接访问资源,因为它不确定资源是否还存在。必须通过调用lock()方法。
- lock()会检查资源是否存在。如果存在,它会返回一个指向该资源的有效的shared_ptr(使得在访问期间资源不会被释放);如果资源已被销毁,它会返回一个空的shared_ptr。这保证了访问的绝对安全。
shared_ptr 和 weak_ptr 的内存反映
-
shared_ptr 对象自身 (例如 sp1, sp2): 它的大小通常是裸指针的两倍。因为它内部包含了两个指针:
Data Addr和Ctrl Addr-
如图:

-
-
Ctrl Addr指向的内容又分为两块内容
- 上面是指向虚函数表
vtable - 下面则是很重要的部分
- 强引用计数 (use_count): 记录当前有多少个 shared_ptr 共同拥有这个数据对象。
- 弱引用计数 (weak_count):weak_count = (当前 weak_ptr 的数量) + (当前 shared_ptr 是否存在?)
- 上面是指向虚函数表
-
我们着重强调下面一部分的实现,举PPT中的例子说明一下:
auto sp1 = make_shared<MyObject>(0xFF); // 01 00 00 00 | 01 00 00 00 auto sp2 = sp1; // 02 00 00 00 | 01 00 00 00 weak_ptr<MyObject> wp = sp1; // 02 00 00 00 | 02 00 00 00 auto sp_locked = wp.lock(); // 03 00 00 00 | 02 00 00 00 sp_locked.reset(); // 02 00 00 00 | 02 00 00 00- 行一:
- sp1 被创建。它的 Data Addr 指向 MyObject,Ctrl Addr 指向控制块。
- 控制块初始化:
- use_count (强引用计数) 被设为 1 (因为 sp1 这一个 shared_ptr 正在拥有它)。
- weak_count (弱引用计数) 也被设为 1。这是因为即使只有一个 shared_ptr 存在,控制块也必须存在,所以弱引用计数从1开始,代表强引用自身对控制块的“引用”。
- 行二:
- sp2 的 Data Addr 和 Ctrl Addr 被设置为与 sp1 完全相同的值。现在 sp1 和 sp2 指向同一个数据和同一个控制块。
- 控制块更新:
- 由于多了一个所有者 (sp2),use_count 加 1,从 01 变为 02。
- weak_count 保持不变。
- 行三:
- wp 也指向了那个控制块,但它是一个“非拥有”的、弱的引用。
- 控制块更新:
- use_count 不变,因为它不增加所有权。
- weak_count 加 1,从 01 变为 02,因为多了一个观察者。
- 行四:
- 一个新的 shared_ptr sp_locked 被创建。
- 控制块更新:
- use_count 加 1,从 02 变为 03。这是为了保证在 sp_locked 的生命周期内,所指向的数据不会被释放。
- weak_count 保持不变。
- 行五:
- 控制块更新:
- use_count 减 1,从 03 变回 02。
- weak_count 保持不变。
- 控制块更新:
- 行一:
指针&函数
这一部分C++并没有什么创新点,所以我们不多做阐述
- 用于参数:
- 提高传输效率
- 使用函数的副作用去修改外部变量的值
- 常量指针
- 函数指针:
- 比如说计算一元函数在某区间上的定积分
-1
455

被折叠的 条评论
为什么被折叠?



