一、内存管理
1.1 C++ 内存布局
程序运行时的内存分布(从高地址到低地址):
1
2
3
4
5
6
7
8
9
10
11
12
13
| ┌─────────────────┐ 高地址
│ 栈(Stack) │ ← 局部变量、函数参数(编译器自动管理)
├─────────────────┤
│ 堆(Heap) │ ← new/malloc 分配(程序员手动管理)
├─────────────────┤
│ 全局/静态存储区 │ ← 全局变量、static 变量
│ .data(已初始化) │
│ .bss(未初始化) │
├─────────────────┤
│ 常量存储区 .rodata│ ← 字符串常量、const 全局变量
├─────────────────┤
│ 代码区 .text │ ← 可执行机器指令
└─────────────────┘ 低地址
|
各区域特点:
| 区域 | 管理方式 | 大小 | 生命周期 |
|---|
| 栈 | 编译器自动分配释放 | 小(默认 ~1MB) | 函数结束时 |
| 堆 | 程序员手动管理 | 大(虚拟内存) | 直到 delete/free |
| 全局/静态区 | 程序启动分配,结束释放 | 取决于变量 | 整个程序运行期 |
| 常量区 | 只读,不可修改 | 取决于常量 | 整个程序运行期 |
| 代码区 | 只读 | 取决于代码 | 整个程序运行期 |
堆和栈的区别:
| 栈 | 堆 |
|---|
| 生长方向 | 向下(地址减小) | 向上(地址增加) |
| 分配方式 | 静态分配(编译器)+ 动态分配(alloca) | 仅动态分配 |
| 分配效率 | 高(有专门的寄存器指令) | 低(库函数搜索空闲内存) |
| 碎片 | 无碎片 | 可能产生内存碎片 |
| 典型问题 | 栈溢出(递归过深) | 内存泄漏、野指针 |
1.2 内存泄漏
定义:申请的内存空间使用完毕后没有释放,导致该内存无法再被访问和回收。
类型:
- 堆内存泄漏:
new/malloc 后没有 delete/free - 系统资源泄漏:
socket、handle、Bitmap 等系统资源未释放
检测工具:
- Valgrind(Linux)
- Visual Studio 诊断工具(Windows)
- Dr.Memory
如何避免:
- 使用智能指针(
unique_ptr、shared_ptr) - RAII 机制(资源获取即初始化)
- 规范配对使用
new/delete、malloc/free
1.3 new/delete 和 malloc/free 的区别
| new/delete | malloc/free |
|---|
| 本质 | 操作符(可重载) | 库函数 |
| 内存计算 | 编译器自动计算 | 需显式指定字节数 |
| 返回值 | 类型安全的指针 | void* |
| 失败处理 | 抛出 std::bad_alloc 异常 | 返回 nullptr |
| 构造函数/析构函数 | 调用 | 不调用 |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| // malloc/free:仅分配和释放内存
int* p = (int*)malloc(sizeof(int) * 10);
free(p);
// new/delete:分配内存 + 构造/析构对象
int* p = new int(42); // 分配 + 初始化
delete p; // 析构 + 释放
Person* arr = new Person[10]; // 数组
delete[] arr; // 注意:数组必须用 delete[]
// 有了 malloc/free 为什么还需要 new/delete?
// 因为对于非内部数据类型,malloc/free 无法调用构造/析构函数
class Foo {
public:
Foo() { cout << "构造"; }
~Foo() { cout << "析构"; }
};
Foo* p1 = (Foo*)malloc(sizeof(Foo)); // 不会调用构造函数
Foo* p2 = new Foo(); // 会调用构造函数
delete p2; // 会调用析构函数
|
1.4 野指针和空指针
野指针(Wild Pointer):未被初始化的指针。指向不确定的内存地址。
空指针(Dangling Pointer):原指向的内存已被释放,但指针未被置空。
1
2
3
4
5
6
7
| int* p; // 野指针:未初始化
// *p = 10; // 危险!
int* q = new int(5);
delete q;
// *q = 10; // 空指针:内存已释放
q = nullptr; // 安全:释放后置空
|
如何避免:
- 定义指针后立即初始化
- 释放内存后立即将指针置为
nullptr - 优先使用智能指针
二、智能指针
2.1 为什么需要智能指针
智能指针是一个类,当超出作用域时自动调用析构函数释放资源,避免手动 delete 导致的内存泄漏。
核心机制:RAII(Resource Acquisition Is Initialization,资源获取即初始化)。
2.2 四种智能指针
(1)auto_ptr(C++11 已废弃):采用所有权模式,存在潜在的内存崩溃问题。
1
2
3
| auto_ptr<string> p1(new string("hello"));
auto_ptr<string> p2;
p2 = p1; // p1 失去所有权,再次访问 p1 会崩溃
|
(2)unique_ptr(独占所有权):同一时刻只能有一个 unique_ptr 指向对象,禁止拷贝但支持移动。
1
2
3
4
5
6
| unique_ptr<string> p1(new string("hello"));
// unique_ptr<string> p2 = p1; // 编译错误!禁止拷贝
unique_ptr<string> p2 = move(p1); // 转移所有权,p1 变为空
// 推荐:使用 make_unique(C++14)
auto p = make_unique<string>("hello");
|
(3)shared_ptr(共享所有权):多个 shared_ptr 可指向同一对象,通过引用计数管理生命周期。
1
2
3
4
5
6
7
8
9
10
11
| shared_ptr<int> p1 = make_shared<int>(42);
{
shared_ptr<int> p2 = p1; // 引用计数 +1
cout << p1.use_count(); // 输出 2
} // p2 析构,引用计数 -1
// p1 析构时引用计数归零,释放内存
// 引用计数存储在控制块(Control Block)中:
// - 强引用计数:shared_ptr 数量
// - 弱引用计数:weak_ptr 数量
// - 删除器、分配器
|
shared_ptr 的线程安全:
- 引用计数的增减是原子操作(线程安全)
- 对同一
shared_ptr 实例的写操作(如 reset())不是线程安全的 - 对管理对象的直接访问需要外部同步
(4)weak_ptr(弱引用):不控制对象生命周期,配合 shared_ptr 解决循环引用问题。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| struct B;
struct A {
shared_ptr<B> b_ptr;
~A() { cout << "A 析构"; }
};
struct B {
// shared_ptr<A> a_ptr; // 循环引用,导致内存泄漏
weak_ptr<A> a_ptr; // 使用 weak_ptr 打破循环
~B() { cout << "B 析构"; }
};
auto a = make_shared<A>();
auto b = make_shared<B>();
a->b_ptr = b;
b->a_ptr = a; // weak_ptr 不会增加引用计数
// a 和 b 都能正常析构
// weak_ptr 通过 lock() 获取 shared_ptr
if (auto shared = weak.lock()) {
// 安全访问
}
|
2.3 智能指针的缺点
| 缺点 | 说明 | 解决方案 |
|---|
| 性能开销 | 引用计数需要原子操作 | 优先用 unique_ptr |
| 循环引用 | shared_ptr 相互引用 | 使用 weak_ptr 打破 |
| 线程安全代价 | 原子操作有性能损耗 | 无跨线程共享时用局部 shared_ptr |
| 与裸指针混用 | 可能导致悬垂指针 | 避免暴露裸指针 |
三、指针和引用
3.1 指针和引用的区别
| 指针(Pointer) | 引用(Reference) |
|---|
| 本质 | 实体,存储地址值 | 别名,不占用独立存储 |
| 初始化 | 可以不初始化(野指针危险) | 定义时必须初始化 |
| 可修改 | 可改变指向 | 绑定后不可改变 |
| 空值 | 可为 nullptr | 不能为空 |
| const | 有 const 和非 const 区别 | 无此概念 |
| sizeof | 指针本身的大小(64位=8字节) | 所引用对象的大小 |
| 运算 | 支持算术运算(++、–) | 不支持 |
| 使用 | 需要解引用 *p | 直接使用 |
1
2
3
4
5
6
7
8
9
10
11
12
13
| int a = 10;
int* p = &a; // 指针:存储 a 的地址
int& r = a; // 引用:a 的别名
*p = 20; // 通过指针修改
r = 30; // 通过引用修改(无需解引用)
// 引用参数传递:无需拷贝,直接操作原对象
void swap(int& a, int& b) {
int t = a;
a = b;
b = t;
}
|
3.2 函数参数传递方式
| 方式 | 特点 | 对实参的影响 |
|---|
| 值传递 | 拷贝实参 | 不影响 |
| 指针传递 | 传地址值,解引用可修改 | 可通过解引用修改 |
| 引用传递 | 传别名,直接操作 | 直接修改 |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| void byValue(int x) { x = 10; } // 不影响实参
void byPointer(int* p) { *p = 10; } // 修改实参
void byReference(int& r) { r = 10; } // 修改实参
int a = 5;
byValue(a); // a = 5
byPointer(&a); // a = 10
byReference(a); // a = 10
// 指针传参时,改变指针本身不影响外部
void changePtr(int* p) {
p = new int(20); // 只改了形参的指向,外部无影响
}
// 需要用指针的引用才能修改指针本身
void changePtrRef(int*& p) {
p = new int(20); // 外部指针也会改变
}
|
四、面向对象三大特性
4.1 封装
将数据和操作数据的方法绑定在一起,通过访问修饰符控制外部访问。
1
2
3
4
5
6
7
8
9
10
| class BankAccount {
private:
double balance; // 私有,外部不可直接访问
public:
void deposit(double amount) {
if (amount > 0) balance += amount;
}
double getBalance() const { return balance; }
};
|
4.2 继承
派生类继承基类的属性和方法,实现代码复用。
继承方式:
| 继承方式 | 基类 public | 基类 protected | 基类 private |
|---|
| public 继承 | public | protected | 不可见 |
| protected 继承 | protected | protected | 不可见 |
| private 继承 | private | private | 不可见 |
构造/析构顺序:
1
2
| 构造函数顺序:基类 → 成员对象 → 派生类自身
析构函数顺序:派生类自身 → 成员对象 → 基类
|
4.3 多态
三要素:继承、方法重写(virtual)、父类引用指向子类对象。
静态多态(编译时):函数重载、模板。编译时期决定调用的函数。
动态多态(运行时):通过虚函数实现。运行时期根据对象实际类型决定调用的函数。
4.4 虚函数原理(虚函数表)
虚函数表(vtable):当类中包含虚函数时,编译器为每个类生成一个虚函数表,保存该类中所有虚函数的地址。
虚函数指针(vptr):每个包含虚函数的类的对象有一个隐藏的虚函数指针,指向该类的虚函数表。vptr 在构造函数中初始化。
1
2
3
4
5
6
7
8
9
10
11
| class Base {
public:
virtual void func1() { cout << "Base::func1"; }
virtual void func2() { cout << "Base::func2"; }
};
class Derived : public Base {
public:
virtual void func1() override { cout << "Derived::func1"; }
virtual void func3() { cout << "Derived::func3"; }
};
|
内存布局:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| Base 对象:
┌─────────────┐
│ vptr → Base │ ← 指向 Base 的虚函数表
│ vtable │
│ func1 ────┼──→ Base::func1
│ func2 ────┼──→ Base::func2
└─────────────┘
Derived 对象:
┌─────────────┐
│ vptr →Derived│ ← 指向 Derived 的虚函数表
│ vtable │
│ func1 ─────┼──→ Derived::func1(重写,覆盖基类地址)
│ func2 ─────┼──→ Base::func2(未重写,保留基类地址)
│ func3 ─────┼──→ Derived::func3(新增)
└─────────────┘
|
编译器处理虚函数表的步骤:
- 拷贝基类的虚函数表(多继承则拷贝多个)
- 如果派生类重写基类虚函数,替换为已重写的函数地址
- 如果派生类有自身虚函数,追加到自身的虚函数表中
4.5 析构函数为什么一般定义为虚函数
1
2
| Base* p = new Derived();
delete p; // 如果析构函数不是虚函数,只会调用 Base 的析构,Derived 的资源泄漏
|
原因:基类指针指向派生类对象时,如果析构函数不是虚函数,编译器根据指针类型(Base)调用析构函数,派生类的资源无法释放,造成内存泄漏。定义为虚函数后,根据实际对象类型调用,先执行派生类析构,再执行基类析构。
4.6 构造函数为什么一般不定义为虚函数
- 虚函数调用需要知道对象的具体类型,但构造函数调用时对象还未创建
- 虚函数通过
vptr 调用,但 vptr 是在构造函数中才初始化的 - 从设计角度,构造函数是为了创建对象,不应该被重写
4.7 纯虚函数和抽象类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| // 纯虚函数:在基类中声明但不实现,派生类必须重写
class Shape {
public:
virtual double area() const = 0; // 纯虚函数
virtual void draw() const = 0;
};
// 含有纯虚函数的类称为抽象类,不能实例化
// Shape s; // 编译错误!
class Circle : public Shape {
public:
double area() const override { return 3.14 * r * r; }
void draw() const override { cout << "画圆形"; }
private:
double r;
};
|
纯虚函数的目的:让派生类只继承函数的接口(interface),派生类必须提供实现。
五、重载、重写、隐藏
5.1 重载(Overload)
同一作用域内,同名函数但参数列表不同(个数/类型/顺序),不关心返回值。
1
2
3
4
5
6
7
| class Print {
public:
void show(int i) { cout << i; }
void show(double d) { cout << d; }
void show(string s) { cout << s; }
};
// 编译时根据参数确定调用哪个版本
|
5.2 重写(Override)
派生类重新定义基类的虚函数,函数名、参数列表、返回值必须完全相同。
1
2
3
4
5
6
7
8
| class Base {
public:
virtual void func() { cout << "Base"; }
};
class Derived : public Base {
public:
void func() override { cout << "Derived"; } // 重写
};
|
override(C++11):显式标记重写,编译器会检查是否真的重写了基类虚函数,避免因签名不一致导致的隐藏。
final(C++11):禁止类被继承,或禁止虚函数被重写。
1
2
3
4
5
6
| class Base final { }; // 禁止继承
// class Derived : Base { }; // 编译错误
class Base2 {
virtual void func() final; // 禁止重写
};
|
5.3 隐藏(Hide)
派生类的函数屏蔽了基类的同名函数,只要函数名相同就会隐藏(不管参数是否相同)。
1
2
3
4
5
6
7
8
9
10
11
| class Base {
public:
void func(int x) { }
};
class Derived : public Base {
public:
void func(string s) { } // 隐藏了基类的 func(int)
};
// Derived d;
// d.func(10); // 编译错误!因为 func(int) 被隐藏了
// d.Base::func(10); // 需通过基类作用域调用
|
六、const 和 static
6.1 const 关键字
修饰基本数据类型:变量成为常量,不可修改。
1
2
| const int MAX = 100; // 常量
int const MIN = 0; // 等价
|
修饰指针:
1
2
3
4
| const int* p = &a; // 指向常量的指针(指针指向的内容不可变)
int* const p = &a; // 常量指针(指针本身不可变,不能改指向)
const int* const p = &a; // 指向常量的常量指针
// 左定值(const 在 * 左),右定向(const 在 * 右)
|
修饰函数参数:保护原对象,常用于指针或引用参数。
1
| void print(const string& s); // 不修改 s
|
修饰成员函数:承诺不修改成员变量,常量对象只能调用 const 成员函数。
1
2
3
4
5
6
7
8
| class Person {
int age;
public:
int getAge() const { return age; } // const 成员函数
// 若需要修改某个成员,用 mutable
mutable int cache;
int getCache() const { return cache++; } // mutable 可在 const 函数中修改
};
|
6.2 static 关键字
修饰局部变量:生命周期延长至程序结束,但作用域不变(仍限语句块内)。
1
2
3
4
5
| void func() {
static int count = 0; // 只初始化一次
count++;
cout << count; // 多次调用时,count 会累加
}
|
修饰全局变量/函数:作用域从整个工程缩小到本文件。
1
2
3
| // file1.cpp
static int x = 10; // 只在 file1.cpp 中可见
static void helper() { } // 只在 file1.cpp 中可调用
|
修饰类成员:
1
2
3
4
5
6
7
8
9
10
11
12
| class MyClass {
public:
static int value; // 静态成员变量(所有对象共享一份)
static void func() { } // 静态成员函数(无 this 指针)
};
int MyClass::value = 0; // 必须在类外初始化
// 静态成员函数的特点:
// 1. 无 this 指针
// 2. 只能访问静态成员变量
// 3. 不能被 virtual 修饰
// 4. 通过 类名:: 调用
|
七、拷贝控制
7.1 深拷贝和浅拷贝
浅拷贝:默认拷贝构造函数,逐字节复制成员。如果成员有指针,两个对象指向同一块内存,析构时会重复释放。
深拷贝:显式定义拷贝构造函数,为指针成员在堆上分配新内存。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| class String {
private:
char* data;
public:
// 浅拷贝(默认):两个 String 的 data 指向同一块内存
// String(const String& other) : data(other.data) { }
// 深拷贝:分配新内存
String(const String& other) {
data = new char[strlen(other.data) + 1];
strcpy(data, other.data);
}
~String() {
delete[] data;
}
// 赋值运算符也需要深拷贝
String& operator=(const String& other) {
if (this == &other) return *this; // 自赋值检查
delete[] data;
data = new char[strlen(other.data) + 1];
strcpy(data, other.data);
return *this;
}
};
|
何时必须用深拷贝:类中包含指针成员、动态分配的资源、文件句柄等。
7.2 拷贝构造函数的调用时机
1
2
3
4
5
6
7
| void funcByValue(String s) { } // 值传递:拷贝构造
String funcReturn() {
String s("hello");
return s; // 返回值:拷贝构造
}
String s1 = s2; // 初始化:拷贝构造
String s3(s2); // 直接构造:拷贝构造
|
7.3 为什么拷贝构造函数必须是引用传递
1
2
3
4
| // String(String other) // 值传递 → 又需要拷贝构造来创建 other
// → 无限递归调用!
String(const String& other) // 引用传递,避免递归
|
如果拷贝构造函数是值传递,为了传递实参又要调用拷贝构造函数,导致无限递归。
八、C++11 新特性
8.1 Lambda 表达式
Lambda 表达式提供匿名函数,可以在需要函数对象的地方直接定义。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| // 基本语法
auto lambda = [] { cout << "Hello"; };
lambda();
// 带参数
auto add = [](int a, int b) -> int { return a + b; };
// 捕获外部变量
int x = 10;
auto byValue = [x] { return x; }; // 值捕获
auto byRef = [&x] { return ++x; }; // 引用捕获
auto allValue = [=] { /* 值捕获所有 */ };
auto allRef = [&] { /* 引用捕获所有 */ };
auto mix = [=, &x] { /* 其余值捕获,x 引用捕获 */ };
// STL 算法中的 Lambda
vector<int> v = {1, 8, 5, 3, 6, 10};
int val = 3;
int count = count_if(v.begin(), v.end(), [val](int x) { return x > val; });
// mutable:允值捕获的变量在函数体内修改
auto mutableLambda = [x]() mutable { x++; return x; };
|
Lambda 的原理:编译器生成一个匿名类(闭包类型),重载 operator(),捕获的变量成为类的成员。
8.2 右值引用和移动语义
右值引用(&&):绑定到即将销毁的临时对象,允许"偷取"资源而不必深拷贝。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| int&& rref = 42; // 右值引用绑定到临时值
class Person {
char* data;
public:
// 移动构造函数:从右值中"偷"资源
Person(Person&& other) noexcept {
data = other.data; // 偷取指针
other.data = nullptr; // 原对象置空,防止双重释放
}
// 移动赋值运算符
Person& operator=(Person&& other) noexcept {
if (this != &other) {
delete[] data;
data = other.data;
other.data = nullptr;
}
return *this;
}
};
// std::move:将左值转为右值引用,触发移动语义
vector<int> v1 = {1, 2, 3};
vector<int> v2 = move(v1); // v1 的资源被移动到 v2,v1 变为空
|
8.3 auto 和 decltype 类型推导
1
2
3
4
5
6
7
8
9
10
| auto a = 10; // a 是 int
auto b = 3.14; // b 是 double
auto c = "hello"; // c 是 const char*
vector<int> v = {1, 2, 3};
for (auto it = v.begin(); it != v.end(); ++it) { } // 自动推导迭代器类型
decltype(a) d = 20; // d 是 int
const int& ref = a;
decltype(ref) e = a; // e 是 const int&
|
8.4 nullptr
1
2
3
4
5
6
7
| // C++03:NULL 是宏,可能被定义为 0 或 (void*)0
void func(int); // #1
void func(char*); // #2
func(NULL); // 调用 #1(意外!)
// C++11:nullptr 是真正的空指针类型,安全
func(nullptr); // 调用 #2(符合预期)
|
九、STL 容器
9.1 vector 扩容机制
触发条件:插入元素时 size 超过 capacity。
扩容策略:
- GCC:新容量 = 旧容量 × 2
- MSVC:新容量 = 旧容量 × 1.5
扩容步骤:
- 分配更大的连续内存块
- 将旧元素移动/拷贝到新内存
- 释放旧内存
- 更新内部指针
时间复杂度:分摊 O(1)(每次插入的平均成本)。
迭代器失效:扩容后所有迭代器、指针、引用均失效。
1
2
3
4
5
6
| vector<int> v;
v.reserve(100); // 预分配容量,避免多次扩容
// 扩容过程示意图:
// capacity=1 → push_back → capacity=2(分配新内存,移动元素)
// → push_back → capacity=4 → push_back → capacity=8 ...
|
9.2 list 和 vector 的区别
| vector | list |
|---|
| 内存 | 连续 | 不连续(双向链表) |
| 随机访问 | O(1) | O(n) |
| 插入/删除 | O(n)(需移动元素) | O(1)(已知迭代器时) |
| 迭代器失效 | 扩容时全部失效 | 仅被删除的元素失效 |
| 适用场景 | 频繁随机访问 | 频繁插入删除 |
9.3 红黑树(RB-Tree)
定义:一种自平衡的二叉查找树,确保没有一条路径比其他路径长出两倍。
性质:
- 每个节点是红色或黑色
- 根节点是黑色
- 叶节点(NIL)是黑色
- 红色节点的子节点必须是黑色
- 任一节点到每个叶子的路径包含相同数量的黑节点
应用:C++ STL 中的 std::map、std::set 底层基于红黑树。
和 AVL 树的对比:红黑树是弱平衡,旋转次数少,适合插入/删除频繁的场景。
十、四种类型转换(cast)
10.1 static_cast
编译时检测的相关类型转换。用于整型和浮点数之间、相关类之间的指针/引用转换。
1
2
3
4
| int i = 10;
float f = static_cast<float>(i); // 数值类型转换
Base* b = new Derived();
Derived* d = static_cast<Derived*>(b); // 向下转换(不安全,不检查)
|
10.2 dynamic_cast
运行时检测的向下转换,需要 RTTI(运行时类型信息)。安全但有一定性能代价。
1
2
3
4
5
6
| Base* b = new Derived();
Derived* d = dynamic_cast<Derived*>(b); // 成功,返回有效指针
if (d) { /* 安全操作 */ }
Base* base = new Base();
Derived* d2 = dynamic_cast<Derived*>(base); // 失败,返回 nullptr
|
10.3 const_cast
移除或添加 const / volatile 属性。
1
2
3
| const int ci = 10;
int& modifiable = const_cast<int&>(ci); // 移除 const 属性
modifiable = 20; // 如果原对象本质是 const,这是未定义行为
|
10.4 reinterpret_cast
最危险的 cast,在几乎任意两种类型之间转换(如整型和指针)。
1
2
| int* p = new int(65);
char* ch = reinterpret_cast<char*>(p); // 指针类型强制转换
|
使用建议:
- 优先使用
static_cast - 需要多态时用
dynamic_cast - 仅处理 const 时用
const_cast - 尽量避免
reinterpret_cast
十一、内存池
定义:预先分配一块大内存,将其划分为多个固定大小的块,程序从池中分配和释放,减少频繁 new/delete 的开销和内存碎片。
优点:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| class MemoryPool {
private:
struct Block {
Block* next;
};
Block* freeList = nullptr;
public:
MemoryPool(size_t blockSize, size_t count) {
for (size_t i = 0; i < count; i++) {
Block* block = reinterpret_cast<Block*>(new char[blockSize]);
block->next = freeList;
freeList = block;
}
}
void* allocate() {
if (!freeList) throw bad_alloc();
Block* block = freeList;
freeList = freeList->next;
return block;
}
void deallocate(void* ptr) {
Block* block = reinterpret_cast<Block*>(ptr);
block->next = freeList;
freeList = block;
}
};
|
十二、链接与编译
12.1 从源码到可执行文件的四个阶段
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| .cpp 文件
↓ 预处理(Preprocessing)
展开 #define、处理 #include、删除注释
↓
.i 文件
↓ 编译(Compilation)
词法分析 → 语法分析 → 语义分析 → 生成汇编代码
↓
.s 文件
↓ 汇编(Assembly)
汇编代码 → 机器指令
↓
.o 文件(目标文件)
↓ 链接(Linking)
符号解析 + 重定向 → 合并目标文件和库
↓
可执行文件(.exe / a.out)
|
12.2 静态链接 vs 动态链接
| 静态链接(.a / .lib) | 动态链接(.so / .dll) |
|---|
| 代码位置 | 嵌入可执行文件 | 运行时加载 |
| 可执行文件大小 | 大 | 小 |
| 依赖 | 独立运行 | 依赖外部 DLL |
| 更新 | 需重新编译 | 替换库文件即可 |
| 内存占用 | 每个进程一份 | 多个进程共享一份 |
| 加载速度 | 快 | 稍慢(需加载库) |
十三、this 指针
定义:this 是隐含于每一个非静态成员函数中的特殊指针,指向正在被该成员函数操作的对象。
本质:
this 的类型是 ClassName* const(不能修改指向)- 在 const 成员函数中,类型为
const ClassName* const(指向的内容也不能修改) this 是一个右值,不能取地址(不能 &this)
使用场景:
1
2
3
4
5
6
7
8
9
10
11
12
13
| class MyClass {
int value;
public:
MyClass& setValue(int value) {
this->value = value; // 区分成员变量和参数
return *this; // 链式调用
}
bool operator==(const MyClass& other) {
return this == &other; // 检查是否同一对象
}
};
// 链式调用:obj.setValue(10).setValue(20);
|
十四、面试高频问答
14.1 程序是怎样跑起来的
- 编译阶段:预处理 → 编译 → 汇编 → 链接,生成可执行文件
- 进程创建:操作系统分配虚拟地址空间,加载代码段和数据段
- 初始化:设置堆栈,初始化全局变量,调用全局对象的构造函数
- 入口点:跳转到
_start,最终调用 main() - 执行:CPU 逐条执行指令,通过 MMU 将虚拟地址映射为物理地址
- 系统调用:需要操作系统服务时(如 IO),进入内核态执行
- 结束:程序执行完,操作系统回收资源
14.2 构造函数的类型
| 类型 | 说明 | 示例 |
|---|
| 默认构造函数 | 无参数或所有参数有默认值 | Person() { } |
| 拷贝构造函数 | 接受同类型常量引用 | Person(const Person&) |
| 转换构造函数 | 单参数构造函数 | explicit Damage(int) |
| 委托构造函数(C++11) | 一个构造调用另一个构造 | Person() : Person(0) { } |
| 继承构造函数(C++11) | using 继承基类构造 | using Base::Base; |
14.3 智能指针的核心组成
- 原始指针封装:内部持有
T* - 所有权模型:
unique_ptr 独占、shared_ptr 共享、weak_ptr 弱引用 - 引用计数控制块:强引用计数 + 弱引用计数 + 删除器
- 删除器:支持自定义释放逻辑
- 运算符重载:
operator*、operator-> - RAII:析构时自动释放
本文涵盖 C++ 从基础到进阶的核心知识点,适合面试复习和系统性查阅。