目录
[一、C/C++ 内存管理](#一、C/C++ 内存管理)
[1.1 C/C++ 内存分布](#1.1 C/C++ 内存分布)
[1.1.1 内存区域划分(从高地址到低地址)](#1.1.1 内存区域划分(从高地址到低地址))
[1.1.2 经典真题(选择 + 填空)](#1.1.2 经典真题(选择 + 填空))
[1.2 C 语言动态内存管理(malloc/calloc/realloc/free)](#1.2 C 语言动态内存管理(malloc/calloc/realloc/free))
[1.2.1 函数用法与核心区别](#1.2.1 函数用法与核心区别)
[1.2.2 常见问题:realloc 后是否需要 free 原指针(p2)?](#1.2.2 常见问题:realloc 后是否需要 free 原指针(p2)?)
[1.3. C++ 动态内存管理(new/delete)](#1.3. C++ 动态内存管理(new/delete))
[1.3.1 操作内置类型(申请 + 初始化 + 释放)](#1.3.1 操作内置类型(申请 + 初始化 + 释放))
[1.3.2 操作自定义类型(自动调用构造 / 析构函数)](#1.3.2 操作自定义类型(自动调用构造 / 析构函数))
[1.3.3 核心注意点:new [] 与 delete [] 必须匹配](#1.3.3 核心注意点:new [] 与 delete [] 必须匹配)
[1.4 operator new 与 operator delete 函数](#1.4 operator new 与 operator delete 函数)
[1.4.1 核心本质:全局函数(new/delete 的底层依赖)](#1.4.1 核心本质:全局函数(new/delete 的底层依赖))
[1.4.2 底层实现与失败处理](#1.4.2 底层实现与失败处理)
[1.4.3 关键注意点](#1.4.3 关键注意点)
[1.5 new 和 delete 的实现原理](#1.5 new 和 delete 的实现原理)
[1.5.1 内置类型](#1.5.1 内置类型)
[1.5.2 自定义类型](#1.5.2 自定义类型)
[new 的原理(3 步)](#new 的原理(3 步))
[delete 的原理(3 步)](#delete 的原理(3 步))
[new [] 的原理(4 步)](#new [] 的原理(4 步))
[delete [] 的原理(4 步)](#delete [] 的原理(4 步))
[1.6 定位 new 表达式(placement-new)](#1.6 定位 new 表达式(placement-new))
[1.6.1 通俗概念](#1.6.1 通俗概念)
[1.6.2 用法格式与代码示例](#1.6.2 用法格式与代码示例)
[1.7 高频面试题(内存管理核心)](#1.7 高频面试题(内存管理核心))
[1.7.1 malloc/free 与 new/delete 的 7 大区别(面试必问)](#1.7.1 malloc/free 与 new/delete 的 7 大区别(面试必问))
[1.7.2 内存泄漏](#1.7.2 内存泄漏)
[(3)分类(核心关注 2 类)](#(3)分类(核心关注 2 类))
[2.1 泛型编程(模板的核心目标)](#2.1 泛型编程(模板的核心目标))
[2.1.1 问题引入:函数重载的 2 大缺点](#2.1.1 问题引入:函数重载的 2 大缺点)
[2.1.2 泛型编程定义](#2.1.2 泛型编程定义)
[2.2 函数模板](#2.2 函数模板)
[2.2.1 概念与格式](#2.2.1 概念与格式)
[2.2.2 实现原理](#2.2.2 实现原理)
[2.2.3 实例化(生成具体类型函数)](#2.2.3 实例化(生成具体类型函数))
[(1)隐式实例化:编译器根据实参自动推演 T](#(1)隐式实例化:编译器根据实参自动推演 T)
[(2)显式实例化:手动指定 T 的类型](#(2)显式实例化:手动指定 T 的类型)
[2.2.4 模板参数匹配原则(3 大规则)](#2.2.4 模板参数匹配原则(3 大规则))
[规则 1:非模板函数与同名函数模板可共存](#规则 1:非模板函数与同名函数模板可共存)
[规则 2:优先调用非模板函数,模板可生成更匹配版本时选模板](#规则 2:优先调用非模板函数,模板可生成更匹配版本时选模板)
[规则 3:模板函数不支持自动类型转换,普通函数支持](#规则 3:模板函数不支持自动类型转换,普通函数支持)
[2.3 类模板](#2.3 类模板)
[2.3.1 定义格式(以动态顺序表 Vector 为例)](#2.3.1 定义格式(以动态顺序表 Vector 为例))
[2.3.2 类模板的实例化(必须显式指定类型)](#2.3.2 类模板的实例化(必须显式指定类型))
[2.3.3 关键注意点](#2.3.3 关键注意点)
一、C/C++ 内存管理
1.1 C/C++ 内存分布
1.1.1 内存区域划分(从高地址到低地址)
|----------|------------------------|------------------|
| 内存区域 | 存储内容 | 核心特点 |
| 内核空间 | 操作系统内核代码 / 数据 | 用户代码不可读写 |
| 栈(向下增长) | 非静态局部变量、函数参数、返回值 | 空间小(几 MB),自动分配释放 |
| 内存映射段 | 共享动态库、共享内存 | 高效 I/O 映射,进程间通信 |
| 堆(向上增长) | 动态分配的内存(malloc/new 申请) | 空间大,需手动分配释放 |
| 数据段(静态区) | 全局变量、静态变量(static) | 程序运行期间一直存在 |
| 代码段(常量区) | 可执行代码、只读常量(如字符串常量) | 只读,不可修改 |
1.1.2 经典真题(选择 + 填空)
cpp
int globalVar = 1; // 全局变量
static int staticGlobalVar = 1; // 静态全局变量
void Test() {
static int staticVar = 1; // 静态局部变量
int localVar = 1; // 局部变量
int num1[10] = {1,2,3,4};// 局部数组
char char2[] = "abcd"; // 局部数组(存储字符串拷贝)
const char* pChar3 = "abcd";// 指针(指向字符串常量)
int* ptr1 = (int*)malloc(sizeof(int)*4); // 动态内存指针
int* ptr2 = (int*)calloc(4, sizeof(int)); // 动态内存指针
int* ptr3 = (int*)realloc(ptr2, sizeof(int)*4); // 扩容指针
free(ptr1);
free(ptr3);
}

cpp
// 基于上面的Test函数
cout << sizeof(num1) << endl; // 答案:40(10个int,每个4字节)
cout << sizeof(char2) << endl; // 答案:5(含字符串结束符'\0')
cout << strlen(char2) << endl; // 答案:4(仅统计有效字符,不含'\0')
cout << sizeof(pChar3) << endl; // 答案:8(64位平台指针大小,32位为4)
cout << strlen(pChar3) << endl; // 答案:4(字符串常量"abcd"长度)
cout << sizeof(ptr1) << endl; // 答案:8(64位平台指针大小)
1.2 C 语言动态内存管理(malloc/calloc/realloc/free)
1.2.1 函数用法与核心区别
cpp
#include <stdlib.h>
#include <stdio.h>
void Test() {
// 1. malloc:申请空间,不初始化
int* p1 = (int*)malloc(sizeof(int)*4); // 申请16字节,内容随机
free(p1);
p1 = NULL; // 避免野指针
// 2. calloc:申请空间并初始化为0
int* p2 = (int*)calloc(4, sizeof(int)); // 申请16字节,每个字节为0
// 3. realloc:扩容/缩容已申请的空间
// 扩容后:若原空间后有足够空间,直接扩展;否则重新分配新空间,拷贝原数据并释放旧空间
int* p3 = (int*)realloc(p2, sizeof(int)*8); // 扩容到32字节
free(p3);
p3 = NULL;
}
三大函数核心区别:
|---------|----------------|---------------------|
| malloc | 不初始化(随机值) | 单个参数(总字节数) |
| calloc | 初始化为 0 | 两个参数(元素个数、单个元素 字节数) |
| realloc | 保留原数据(扩容 / 缩容) | 两个参数(原指针、新总字节数) |
1.2.2 常见问题:realloc 后是否需要 free 原指针(p2)?
答:不需要!原因:
- 若 realloc 扩容成功:
- 情况 1:原空间后有足够空间,直接扩展,返回原指针(p2 和 p3 指向同一地址),free (p3) 即释放原空间;
- 情况 2:原空间后无空间,重新分配新空间,拷贝原数据并自动释放旧空间,p2 变为野指针,只需 free (p3)。
- 若 realloc 失败:返回 NULL,原空间(p2)未释放,需手动 free (p2)。
1.3. C++ 动态内存管理(new/delete)
1.3.1 操作内置类型(申请 + 初始化 + 释放)
cpp
#include <iostream>
using namespace std;
void Test() {
// 1. 申请单个int空间(不初始化)
int* p1 = new int;
// 2. 申请单个int空间并初始化为10
int* p2 = new int(10);
// 3. 申请10个int空间(不初始化,C++11支持初始化:new int[10]{1,2,3})
int* p3 = new int[10];
// 释放空间(必须匹配使用)
delete p1; // 释放单个元素
delete p2; // 释放单个元素(带初始化)
delete[] p3; // 释放连续空间(数组)
p1 = p2 = p3 = NULL; // 避免野指针
}
int main() {
Test();
return 0;
}
1.3.2 操作自定义类型(自动调用构造 / 析构函数)
cpp
#include <iostream>
using namespace std;
class A {
public:
A(int a = 0) : _a(a) {
cout << "A():" << this << endl; // 构造函数
}
~A() {
cout << "~A():" << this << endl; // 析构函数
}
private:
int _a;
};
int main() {
// C语言malloc:仅开辟空间,不调用构造函数
A* p1 = (A*)malloc(sizeof(A));
free(p1); // 仅释放空间,不调用析构函数
p1 = NULL;
// C++ new:开辟空间 + 调用构造函数
A* p2 = new A(10);
delete p2; // 调用析构函数 + 释放空间
p2 = NULL;
// 数组:new[]调用N次构造,delete[]调用N次析构
A* p3 = new A[3]; // 3次构造
delete[] p3; // 3次析构
p3 = NULL;
return 0;
}
cpp
A():(0x7ffee3b558a0)
~A():(0x7ffee3b558a0)
A():(0x7ffee3b55890)
A():(0x7ffee3b55888)
A():(0x7ffee3b55880)
~A():(0x7ffee3b55880)
~A():(0x7ffee3b55888)
~A():(0x7ffee3b55890)
1.3.3 核心注意点:new [] 与 delete [] 必须匹配
- 若用
new[]申请数组,必须用delete[]释放:否则自定义类型会漏调用部分析构函数,导致资源泄漏; - 内置类型匹配错误可能不报错,但不推荐(行为未定义);
- 错误示例:
A* p = new A[3]; delete p;(仅调用 1 次析构,泄漏 2 个对象资源)。
1.4 operator new 与 operator delete 函数
1.4.1 核心本质:全局函数(new/delete 的底层依赖)
new 和 delete 是操作符 ,底层实际调用全局函数operator new和operator delete申请 / 释放空间。
1.4.2 底层实现与失败处理
cpp
// operator new:底层调用malloc,失败抛异常(而非返回NULL)
void* operator new(size_t size) {
void* p = malloc(size);
while (p == NULL) {
// 尝试执行用户自定义的空间不足应对措施
if (_callnewh(size) == 0) {
throw std::bad_alloc(); // 抛异常
}
p = malloc(size);
}
return p;
}
// operator delete:底层调用free
void operator delete(void* p) {
if (p != NULL) {
free(p);
}
}
1.4.3 关键注意点
operator new/operator delete可被用户重载(类内或全局),默认全局版本调用 malloc/free;- 与 malloc 的区别:malloc 失败返回 NULL,
operator new失败抛bad_alloc异常; operator new[]/operator delete[]:数组版本,底层调用operator new/operator delete,额外处理数组长度信息。
1.5 new 和 delete 的实现原理
1.5.1 内置类型
- new ≈ operator new + 空间分配(无初始化,除非显式指定);
- delete ≈ operator delete + 空间释放;
- new [] ≈ operator new [] + 分配 N 个元素空间;
- delete [] ≈ operator delete [] + 释放 N 个元素空间;
- 核心区别:new 失败抛异常,malloc 返回 NULL。
1.5.2 自定义类型
new 的原理(3 步)
- 调用
operator new函数申请空间; - 在申请的空间上自动调用构造函数,完成对象初始化;
- 返回对象地址。
delete 的原理(3 步)
- 调用对象的析构函数,完成资源清理;
- 调用
operator delete函数释放对象空间; - 指针置空(用户手动操作)。
new [] 的原理(4 步)
- 调用
operator new[]函数,底层调用operator new申请 N 个对象的总空间(含数组长度信息); - 在空间上调用 N 次构造函数,初始化每个对象;
- 返回数组首地址。
delete [] 的原理(4 步)
- 从数组首地址获取数组长度 N;
- 调用N 次析构函数,清理每个对象资源;
- 调用
operator delete[]函数,底层调用operator delete释放空间; - 指针置空(用户手动操作)。
1.6 定位 new 表达式(placement-new)
1.6.1 通俗概念
在已分配的原始内存中显式调用构造函数初始化对象(内存池场景常用,内存池分配的内存未初始化)。
1.6.2 用法格式与代码示例
cpp
#include <iostream>
using namespace std;
class A {
public:
A(int a = 0) : _a(a) {
cout << "A():" << this << endl;
}
~A() {
cout << "~A():" << this << endl;
}
private:
int _a;
};
int main() {
// 1. 用malloc申请原始内存(未调用构造函数,不是完整对象)
A* p1 = (A*)malloc(sizeof(A));
// 定位new:在p1指向的内存中调用构造函数(无参数)
new(p1)A;
// 定位new:带参数初始化
// new(p1)A(10);
// 2. 用operator new申请原始内存
A* p2 = (A*)operator new(sizeof(A));
new(p2)A(20); // 带参数初始化
// 3. 手动调用析构函数(必须!定位new不会自动调用)
p1->~A();
p2->~A();
// 4. 释放原始内存
free(p1);
operator delete(p2);
p1 = p2 = NULL;
return 0;
}
cpp
A():(0x7ffee3b558a0)
A():(0x7ffee3b55890)
~A():(0x7ffee3b558a0)
~A():(0x7ffee3b55890)
适用场景
- 内存池:内存池分配的内存是原始内存(无构造),需用定位 new 初始化自定义类型对象;
- 避免频繁申请释放内存:提前分配大块内存,用定位 new 重复初始化对象。
1.7 高频面试题(内存管理核心)
1.7.1 malloc/free 与 new/delete 的 7 大区别(面试必问)
|-------|--------------------------|--------------------------|
| 特性 | malloc/free | new/delete |
| 本质 | 库函数 | 操作符 |
| 初始化 | 不初始化(随机值) | 可初始化(如new int(10)) |
| 空间计算 | 需手动计算字节数(sizeof(int)*4) | 自动计算(new int[4]) |
| 返回值 | void*,需强转 | 对应类型指针,无需强转 |
| 失败处理 | 返回 NULL,需手动判空 | 抛bad_alloc异常,需捕获 |
| 自定义类型 | 仅开辟 / 释放空间,不调用构造 / 析构 | 开辟空间 + 构造,析构 + 释放空间 |
| 数组支持 | 需手动计算总字节数 | 直接用new[]/delete[],简洁 |
1.7.2 内存泄漏
(1)定义
程序未能释放不再使用的内存,导致内存浪费(内存未消失,而是失去控制)。
(2)危害
- 短期运行程序:影响小;
- 长期运行程序(操作系统、后台服务):内存逐渐耗尽,响应变慢,最终卡死。
(3)分类(核心关注 2 类)
- 堆内存泄漏(最常见):malloc/calloc/realloc/new 申请的内存未用 free/delete 释放;
- 系统资源泄漏:未释放系统资源(套接字、文件描述符、管道等)。
(4)检测方法
- VS 环境:使用
_CrtDumpMemoryLeaks()函数(简单检测,仅报泄漏字节数);
cpp
#include <crtdbg.h>
int main() {
int* p = new int[10];
_CrtDumpMemoryLeaks(); // 程序退出时检测内存泄漏
return 0;
}
- Linux 环境:valgrind 工具(
valgrind --leak-check=full ./a.out); - Windows 第三方工具:VLD(Visual Leak Detector,精准定位泄漏位置)。
(5)避免方法(事前预防为主)
- 养成良好编码习惯:申请内存后及时释放,匹配使用 malloc/free、new/delete;
- 采用 RAII 思想:用智能指针(shared_ptr/unique_ptr)管理动态内存;
- 使用内存管理库:公司内部私有库,自带泄漏检测;
- 借助工具:工程较大时用第三方工具检测。
二、模板初阶(泛型编程)
2.1 泛型编程(模板的核心目标)
2.1.1 问题引入:函数重载的 2 大缺点
cpp
// 函数重载实现不同类型的Swap,缺点明显
void Swap(int& left, int& right) { int temp = left; left = right; right = temp; }
void Swap(double& left, double& right) { double temp = left; left = right; right = temp; }
void Swap(char& left, char& right) { char temp = left; left = right; right = temp; }
- 缺点 1:代码复用率低,新增类型需手动添加重载函数;
- 缺点 2:可维护性差,一个函数出错,所有重载版本均受影响。
2.1.2 泛型编程定义
编写与类型无关的通用代码,通过模板让编译器根据不同类型生成具体代码,是代码复用的核心手段。
2.2 函数模板
2.2.1 概念与格式
- 概念:代表一个函数家族,与类型无关,使用时参数化,生成具体类型函数;
- 格式:
template<typename T1, typename T2,...>+ 函数定义; - 关键字:
typename可替换为class(不能用 struct)。
cpp
#include <iostream>
using namespace std;
// 函数模板:通用Swap
template<typename T> // T为模板参数(类型占位符)
void Swap(T& left, T& right) {
T temp = left;
left = right;
right = temp;
}
int main() {
int a = 10, b = 20;
Swap(a, b); // 编译器推演T为int,生成int版本Swap
cout << "a=" << a << ", b=" << b << endl; // 输出:a=20, b=10
double c = 1.1, d = 2.2;
Swap(c, d); // 编译器推演T为double,生成double版本Swap
cout << "c=" << c << ", d=" << d << endl; // 输出:c=2.2, d=1.1
char e = 'A', f = 'B';
Swap(e, f); // 编译器推演T为char,生成char版本Swap
cout << "e=" << e << ", f=" << f << endl; // 输出:e=B, f=A
return 0;
}
2.2.2 实现原理
函数模板本身不是函数,是编译器生成具体类型函数的 "模具":
- 编译阶段:编译器根据实参类型推演模板参数 T,生成对应类型的函数;
- 例如:
Swap(a,b)(int 类型)→ 生成void Swap(int&, int&);Swap(c,d)(double 类型)→ 生成void Swap(double&, double&)。
2.2.3 实例化(生成具体类型函数)
实例化分为隐式实例化 和显式实例化。
(1)隐式实例化:编译器根据实参自动推演 T
cpp
template<class T>
T Add(const T& left, const T& right) {
return left + right;
}
int main() {
int a1 = 10, a2 = 20;
Add(a1, a2); // 隐式推演T为int
double d1 = 10.0, d2 = 20.0;
Add(d1, d2); // 隐式推演T为double
// 错误:编译器无法推演T(a1是int,d1是double,T需统一)
// Add(a1, d1);
// 解决方式1:强制类型转换
Add(a1, (int)d1); // T推演为int
// 解决方式2:显式实例化
Add<double>(a1, d1); // 强制T为double
return 0;
}
(2)显式实例化:手动指定 T 的类型
cpp
int main() {
int a = 10;
double b = 20.0;
// 显式实例化:<>中指定T为int,编译器尝试隐式类型转换
Add<int>(a, b);
// 显式实例化:<>中指定T为double
Add<double>(a, b);
return 0;
}
2.2.4 模板参数匹配原则(3 大规则)
规则 1:非模板函数与同名函数模板可共存
cpp
// 非模板函数(专门处理int)
int Add(int left, int right) {
cout << "非模板函数 Add(int, int)" << endl;
return left + right;
}
// 函数模板(通用版本)
template<class T>
T Add(T left, T right) {
cout << "模板函数 Add(T, T)" << endl;
return left + right;
}
void Test() {
Add(1, 2); // 优先调用非模板函数(完全匹配)
Add<int>(1, 2); // 显式实例化,调用模板生成的int版本
}
cpp
非模板函数 Add(int, int)
模板函数 Add(T, T)
规则 2:优先调用非模板函数,模板可生成更匹配版本时选模板
cpp
// 非模板函数(int类型)
int Add(int left, int right) {
cout << "非模板函数 Add(int, int)" << endl;
return left + right;
}
// 函数模板(支持不同类型参数)
template<class T1, class T2>
T1 Add(T1 left, T2 right) {
cout << "模板函数 Add(T1, T2)" << endl;
return left + right;
}
void Test() {
Add(1, 2); // 非模板函数完全匹配,调用非模板
Add(1, 2.0); // 模板生成更匹配版本(T1=int, T2=double),调用模板
}
cpp
非模板函数 Add(int, int)
模板函数 Add(T1, T2)
规则 3:模板函数不支持自动类型转换,普通函数支持
cpp
void Test() {
Add(1, 2.0); // 模板函数:T1=int, T2=double(显式指定或推演,不自动转换)
Add(1, (int)2.0); // 普通函数:自动转换为int,调用非模板
}
2.3 类模板
2.3.1 定义格式(以动态顺序表 Vector 为例)
cpp
#include <iostream>
#include <cassert>
using namespace std;
// 类模板:Vector不是具体类,是生成具体类的模具
template<class T>
class Vector {
public:
// 构造函数(初始化列表)
Vector(size_t capacity = 10)
: _pData(new T[capacity])
, _size(0)
, _capacity(capacity)
{}
// 析构函数:类内声明,类外定义
~Vector();
// 成员函数
void PushBack(const T& data) {
// 扩容逻辑(简化版)
if (_size >= _capacity) {
T* temp = new T[_capacity * 2];
for (size_t i = 0; i < _size; ++i) {
temp[i] = _pData[i];
}
delete[] _pData;
_pData = temp;
_capacity *= 2;
}
_pData[_size++] = data;
}
void PopBack() {
if (_size > 0) {
--_size;
}
}
size_t Size() const { return _size; }
// 运算符重载:[]访问元素
T& operator[](size_t pos) {
assert(pos < _size);
return _pData[pos];
}
private:
T* _pData; // 动态数组
size_t _size; // 有效元素个数
size_t _capacity; // 容量
};
// 类模板的类外成员函数定义:必须加模板参数列表
template<class T>
Vector<T>::~Vector() {
if (_pData) {
delete[] _pData;
_pData = NULL;
_size = _capacity = 0;
}
}
2.3.2 类模板的实例化(必须显式指定类型)
类模板实例化与函数模板不同,必须显式指定模板参数,实例化后的结果才是具体类。
cpp
int main() {
// Vector<int>是具体类,s1是该类的对象
Vector<int> s1;
s1.PushBack(1);
s1.PushBack(2);
s1.PushBack(3);
cout << "s1.Size()=" << s1.Size() << endl; // 输出:3
cout << "s1[1]=" << s1[1] << endl; // 输出:2
// Vector<double>是另一个具体类,与Vector<int>完全独立
Vector<double> s2;
s2.PushBack(1.1);
s2.PushBack(2.2);
cout << "s2[0]=" << s2[0] << endl; // 输出:1.1
return 0;
}
2.3.3 关键注意点
- 类模板名(如 Vector)不是具体类,实例化后的
Vector<int>、Vector<double>才是具体类; - 不同实例化类型是完全独立的类,占用不同内存空间;
- 类外定义成员函数时,必须加模板参数列表(
template<class T>),且类名需带模板参数(Vector<T>::~Vector())。
三、学习总结与建议
内存管理部分
- 核心逻辑:掌握 "申请 - 初始化 - 使用 - 释放" 的完整流程,重点区分 malloc/new 的底层差异和适用场景;
- 重点突破:内存区域分布(真题必考)、new/delete 原理(自定义类型的构造 / 析构)、内存泄漏(面试高频);
- 避坑清单:new [] 与 delete [] 必须匹配、定位 new 需手动调用析构、new 失败抛异常需捕获。
模板部分
- 核心逻辑:模板是泛型编程的基础,本质是 "编译器生成代码的模具",解决代码复用问题;
- 重点突破:函数模板的实例化(隐式 + 显式)、匹配原则、类模板的类外成员函数定义与实例化;
- 避坑清单:模板参数关键字用 typename/class(不用 struct)、类模板必须显式实例化、模板函数不自动类型转换。