主要基于 C++ 贺同学 。
一、 C++ 语言基础知识
1. 智能指针
在传统的 C++ 开发中,手动管理内存(new/delete)极易引发三个致命问题:
- 内存泄漏:申请了空间但忘记释放。
- 野指针/悬空指针:指针指向的内存已被释放,但指针还在被使用。
- 重复释放:对同一块内存调用了两次
delete,导致程序崩溃。
智能指针的核心思想是 RAII(Resource Acquisition Is Initialization,资源获取即初始化)。
- 封装成类:它将原始指针封装在一个类中。
- 自动析构:利用 C++ 的局部对象生命周期管理特性。当智能指针对象超出其作用域(Scope)时,编译器会自动调用它的析构函数。
- 释放资源:我们在析构函数内部编写了释放内存的逻辑,从而确保资源一定会被回收,无需程序员手动干预。
常用接口:
T* get():获取封装在内部的原生指针(裸指针)。operator*/operator->:重载了指针操作符,让智能指针用起来和普通指针一模一样。T* release():交出控制权。将内部指针置为nullptr,并返回原来的裸指针。注意:它不会释放裸指针指向的内存!(需要手动delete返回的指针)。void reset(T* ptr = nullptr):重置/替换。先释放当前管理的内存(如果有),然后接管传入的新指针ptr。
1.1. auto_ptr (C++98 引入,C++11 已被明确废弃)
- 机制:采用所有权剥夺模式。
- 致命缺点:存在隐式的内存崩溃风险。
auto_ptr<string> p1(new string("hello"));
auto_ptr<string> p2 = p1; // 编译通过,不报错。
// 此时 p1 的控制权被 p2 剥夺,p1 变成了 nullptr。
// 如果之后代码继续访问 p1->size(),程序会直接崩溃 (Crash)!
1.2. unique_ptr (C++11 引入,用来替代 auto_ptr)
- 机制:独占式拥有。保证同一时间内只有一个智能指针可以指向该对象。
- 安全性:直接在编译阶段扼杀隐患。
- 优点:零额外开销(和裸指针一样快),极其安全,避免资源泄漏的首选。
unique_ptr<string> p3(new string("auto"));
unique_ptr<string> p4 = p3; // ❌ 编译报错!禁止直接拷贝赋值。
// 如果真的想转移所有权,必须显式使用 std::move
unique_ptr<string> p5 = std::move(p3); // ✅ 成功,p3 置空,p5 接管
1.3. shared_ptr (共享型,强引用)
- 机制:共享式拥有。利用引用计数 (Reference Counting) 机制,允许多个指针指向同一个对象。
- 生命周期:每多一个
shared_ptr指向该资源,计数 +1;每销毁一个,计数 -1。当最后一个引用被销毁(计数为 0)时,自动释放资源。 - 关键函数:
use_count()可以查看当前有多少个指针共享该资源。
1.4. weak_ptr (弱引用)
- 机制:不控制对象生命周期的观察者。它依附于
shared_ptr存在。 - 特点:它的构造和析构不会引起引用计数的增加或减少。它只提供对对象的访问手段,没有重载
*和->。 - 用法:需要访问资源时,调用
lock()函数。如果资源还没被释放,lock()会返回一个临时的shared_ptr供你使用;如果资源已释放,返回空的shared_ptr。 - 核心作用:解决循环引用(死锁)问题。
- 循环引用场景:对象 A 内部有一个
shared_ptr指向对象 B;对象 B 内部也有一个shared_ptr指向对象 A。两者的引用计数永远降不到 0(最低是 1),导致内存永远泄漏。 - 解决方案:将 A 或 B 其中一方内部的智能指针换成
weak_ptr即可解决。
- 循环引用场景:对象 A 内部有一个
- 注意:
lock()是一个原子操作(Atomic Operation)。它把“检查对象是否存活”和“增加引用计数防止被销毁”这两个动作绑定在了一起。要么一起成功,要么直接返回空指针。因此,永远使用lock()去访问weak_ptr,而不是先查expired()。
2. 内存分配

2.1. 栈区 (Stack)
- 特性:由编译器自动分配和释放,操作方式类似于数据结构中的栈(先进后出 LIFO)。分配效率极高。
- 存储内容:局部变量、函数的参数值、函数返回地址等。
- 生长方向:通常是向下生长(从高地址向低地址扩展)。
- 致命风险(Stack Overflow):栈的空间非常有限(在 Linux 系统下默认通常是 8MB)。如果你开了一个超大的局部数组(比如
int arr[1000000];),或者递归调用层数太深(没有退出条件),就会把栈撑爆,导致栈溢出崩溃。
2.2. 堆区 (Heap)
- 特性:动态内存分配区域,必须由程序员手动申请(
new/malloc)和释放(delete/free)。空间非常大,几乎受限于机器的物理内存。 - 生长方向:通常是向上生长(从低地址向高地址扩展)。
- 致命风险:
- 内存泄漏 (Memory Leak):
new了之后忘记delete。 - 内存碎片 (Memory Fragmentation):频繁地分配和释放不同大小的小块内存,会导致内存空间变得七零八碎,虽然总空间够,但申请不出一整块连续的内存。
- 内存泄漏 (Memory Leak):
2.3. 全局/静态存储区 (Global/Static Segment)
这里存放的是生命周期贯穿整个程序运行期的变量(全局变量和 static 静态变量)。在底层,它被细分为两个相邻的区域:
.data区(已初始化数据区):存放已经明确初始化且不为 0 的全局变量和静态变量。.bss区(未初始化数据区):存放未初始化或初始化为 0 的全局变量和静态变量。操作系统在加载程序时,会自动把这块区域的数据清零。
2.4. 常量存储区 (Constant / Read-Only Segment)
- 特性:也被称为
.rodata(Read-Only Data) 区。这里的内存受到操作系统的严格保护,绝对只读。 - 存储内容:字符串字面量(比如
char* p = "Hello World";里的"Hello World")以及部分const修饰的变量。 - 致命风险:如果你尝试强行修改这块区域的数据(比如
p[0] = 'h';),操作系统会立刻抛出 段错误 (Segmentation Fault) 并终止程序。
2.5. 代码区 (Code / Text Segment)
- 特性:存放程序的机器指令(也就是你写的代码编译后的二进制文件)。
- 特点:
- 只读:防止程序在运行中意外修改自己的指令。
- 共享:如果你同时打开了 5 个记事本程序,内存中其实只有一份记事本的代码区,它们共享这部分内存以节约资源。
3. 指针参数传递和引用参数传递
3.1. 指针传递:本质是“值传递”(传的是地址的副本)
当你把一个指针传给函数时,其实是发生了一次“拷贝”。
- 栈区开辟:编译器会在被调用函数的栈帧里,开辟一块新的内存,专门用来存放传进来的地址值。这个新指针(形参)和外面的旧指针(实参)是两个独立的变量,只是它们碰巧装了同一个地址。
- 改指针的指向(形参变,实参不变):如果你在函数里写
p = nullptr;,这只是把形参那个临时变量清空了,外面的实参指针完全不受影响。 - 改指针指向的内容(形参变,实参也变):如果你写
*p = 10;,因为形参和实参拿着同一把钥匙(地址),这会切实地改变外部数据。
- 改指针的指向(形参变,实参不变):如果你在函数里写
3.2. 引用传递:底层是“间接寻址”(安全的高级马甲)
引用传递在底层实现上,其实也占用了栈空间的内存(通常被编译器实现为一个常量指针 Type* const)。
- 操作透明化:最大的区别在于,编译器负责了所有的“解引用(
*)”工作。当你在函数里操作引用变量时,编译器自动将其翻译为间接寻址,直接去操作主调函数里的那个本体。 - 修改本体:你在函数内对引用做的任何赋值,都会 100% 作用于外部的实参。
3.3. 为什么想改外部指针的指向,必须用 ** 或 *&?
既然指针传递是拷贝了指针本身,如果想在函数里改变外部指针自身的指向(比如让外部指针指向一块新 new 出来的内存):
- 传“指针的指针”(二级指针
int** p)。 - 或者传“指针的引用”(
int* &p,推荐写法,更直观)。
3.4. 编译器视角:符号表 (The Symbol Table)
- **符号表(Symbol Table)**是编译器在编译期间维护的一张表,记录了变量名和对应的内存地址。
- 指针的符号表:记录的是【指针变量名 —> 指针自己的内存地址】。指针自己的地址是固定的,但指针里面存的值(指向哪里)随时可以改。
- 引用的符号表:记录的是【引用变量名 —> 本体对象的内存地址】。
- 符号表一旦生成,变量名和地址的映射关系就彻底定死了。所以指针可以随意改变其保存的地址值,而引用一旦绑定了某个对象,终生不能换绑。
4. static 和 const 关键字
4.1. static 关键字
static 的核心逻辑在于改变变量的生命周期(使其贯穿整个程序)或限制标识符的链接属性(使其仅在特定范围内可见)。
4.1.1. 修饰局部变量(静态局部变量)
- 存储位置:由栈区改为静态数据区。
- 生命周期:从程序运行到结束,不会随函数退出而销毁。
- 作用域:保持不变,仍限制在定义它的语句块内。
- 特性:只在第一次执行时初始化一次,后续调用保留上次运行的值。
4.1.2. 修饰全局变量(静态全局变量)
- 可见性变化:由“整个工程可见”变为**“仅本源文件可见”**(内部链接)。
- 用途:有效防止多个文件中出现同名变量导致的链接冲突。
4.1.3. 修饰函数(静态函数)
- 作用:与修饰全局变量类似,限制函数仅在声明它的**模块(文件)**内可见,隐藏了函数接口。
4.1.4. 修饰类成员(静态成员变量/函数)
静态成员变量:
- 归属:属于类而非特定对象,所有对象共享同一个副本。
- 初始化:必须在类外进行定义和初始化(因为它们独立于对象存在)。
静态成员函数:
- 无
this指针:因此不能访问非静态成员,只能访问静态成员。 - 调用方式:可以通过类名直接调用
Class::Func(),也可以通过对象调用。 - 禁止 virtual:虚函数依赖
vptr指针,而vptr存放在对象内存中并通过this访问,static函数无this,故不能为虚函数。虚函数的调⽤关系:this -> vptr -> ctable -> virtual function。
4.2. const 关键字
const 的核心逻辑是只读保护,防止数据被意外修改。
4.2.1. 修饰基本数据类型
const int a = 10;与int const a = 10;等价。- 变量值被锁定,尝试修改会导致编译错误。
4.2.2. 修饰指针与引用(关键区分)
- 常量指针(Pointer to Constant):
const int* p或int const* p。const在*左侧,修饰指向的内容。内容不可变,指针指向可变。 - 指针常量(Constant Pointer):
int* const p。const在*右侧,修饰指针本身。内容可变,指针指向不可变。 - 双重限定:
const int* const p。指向的内容和指针本身都不可变。
4.2.3. 函数中的 const 应用
- 做参数:保护输入数据不被函数内部修改,常用于
const T&(引用传递)以提高效率并确保安全。 - 做返回值:防止返回值被作为左值修改(如返回
const ref)。
4.2.4. 类中的 const 用法
- const 成员变量:
- 初始化:只能在构造函数的初始化列表中进行。
- 特性:在对象生命周期内是常量,不同对象的该常量值可以不同。
- const 成员函数:
- 目的:承诺不修改对象的任何非静态成员。
- 互斥性:不能与
static同时修饰同一个成员函数(因为static无this指针,而const修饰的是隐式的this)。 - 例外:被
mutable修饰的变量可以在const函数中被修改。
- 常量对象:
- 限制:一旦定义为
const ClassObj,该对象只能调用 const 成员函数。
- 限制:一旦定义为
4.2.5. 常量成员函数深度解析
成员函数在调用时会隐式传递 this 指针:
- 普通成员函数:隐式形参为
T* const this(指针地址不可变,指向内容可变)。 - 常量成员函数:隐式形参变为
const T* const this(指向的内容也不可变)。
| 对象类型 | 可调用函数类型 | 说明 |
| 非常量对象 | const 和 非const 成员函数 | 权限可以缩小(从可读写到只读) |
| 常量对象 | 仅 const 成员函数 | 权限不能放大(必须保证只读) |