C++八股题整理
- 内存布局
- C++中的内存分配情况
- 堆和栈的内存有什么区别?
- 堆
- 堆内存分配慢如何优化?内存池
- 内存溢出和内存泄漏是什么?如何避免?
- 内存碎片是什么?怎么解决?
- 栈
- 为什么栈的访问效率比堆高?
- 函数调用时栈的变化?
- 函数的参数列表为什么从右往左入栈?
内存布局
C++中的内存分配情况
区域 | 存储内容 | 分配方式 | 生命周期 |
---|---|---|---|
栈 (Stack) | 局部变量、局部常量、函数的参数和返回地址 | 自动分配和释放,由编译器管理。连续的内存块,分配释放速度快 | 函数调用时入栈,返回时释放 |
堆 (Heap) | 动态分配的对象和数组 | 由new 和delete ,或malloc 和free 动态分配和释放。不连续的内存块,易出现碎片,速度慢 | 程序员手动分配和释放 |
全局/静态存储区 | 全局变量、静态变量 | 程序启动时分配,程序结束时释放 | 与程序的整个生命周期一致 |
常量区 | 字符串字面量、const 修饰的全局和静态变量、虚函数表 | 程序启动时分配 | 与程序的整个生命周期一致 |
代码区 | 程序的二进制代码 | 由操作系统在程序加载时分配 | 与程序的整个生命周期一致 |
#include <iostream>
int globalVar = 10; // 全局变量,存储在全局/静态存储区
static int staticGlobalVar = 20; // 静态全局变量,存储在全局/静态存储区
class MyClass {
public:
int memberVar; // 成员变量,存储在对象实例所分配的内存中(栈或堆)
static int staticMemberVar; // 静态成员变量,存储在全局/静态存储区
// 构造函数,存储在代码区
MyClass(int val) : memberVar(val) {}
// 成员函数,存储在代码区
void show() {
std::cout << "Member Var: " << memberVar << std::endl;
}
};
const int constGlobalVar = 40; // 全局常量,存储在常量区
int main() {
int localVar = 50; // 局部变量,存储在栈中
const int constLocalVar = 60; // 局部常量,存储在栈中(通常编译器优化后会放入寄存器)
// 静态创建对象
MyClass obj(localVar); // 对象本身和成员变量存储在栈中
// 动态创建对象
MyClass* heapObj = new MyClass(localVar); // 指针heapObj在栈上,对象本身和成员变量存储在堆中
// 释放动态分配的对象
delete heapObj; // 释放堆中的内存
return 0;
}
堆和栈的内存有什么区别?
特性 | 栈内存 | 堆内存 |
---|---|---|
管理方式 | 自动管理,系统分配和释放 | 手动管理,程序员分配和释放 |
内存布局 | 连续的内存块,不会产生内存碎片 | 非连续的内存块,会产生内存碎片 |
分配地址 | 由高向低 | 由低向高 |
分配速度 | 快 | 慢,因为要寻找合适大小的内存块 |
空间大小 | 较小,通常几MB到几十MB | 较大,通常可达GB级别 |
生命周期 | 随函数调用开始和结束 | 程序员控制,直到显式释放 |
存储内容 | 局部变量、函数调用信息 | 动态分配的对象和数据结构 |
堆
堆内存分配慢如何优化?内存池
内存池通过一次性预先分配一大块内存,并将其划分为多个固定大小的小块,当需要分配内存时,从这些小块中快速分配,而不是每次调用 malloc;释放内存时,直接将小块返回内存池,而不调用 free。这样大幅减少了频繁的堆内存分配和释放操作的系统开销,降低了内存碎片的风险,显著提升了在高频率、小内存块分配场景下的性能。
#include <iostream>
class MemoryAllocator {
private:
int poolsize; // 内存池的总大小
int blocksize; // 每个块的大小
int numblocks; // 内存池中块的数量
char* pool; // 指向内存池的指针
bool* used; // 用于标记每个块是否被使用的布尔数组
public:
// 构造函数:初始化内存池
MemoryAllocator(int ps, int bs) : poolsize(ps), blocksize(bs), numblocks(ps / bs) {
pool = new char[poolsize]; // 分配内存池
used = new bool[numblocks]; // 分配标记数组
for (int i = 0; i < numblocks; i++) {
used[i] = false; // 初始化标记数组,所有块都是未使用的
}
}
// 析构函数:释放分配的内存
~MemoryAllocator() {
delete[] pool; // 释放内存池
delete[] used; // 释放标记数组
}
// 分配内存块
void* Alloc() {
for (int i = 0; i < numblocks; i++) {
if (!used[i]) { // 查找第一个未使用的块
used[i] = true; // 标记为已使用
return &pool[blocksize * i]; // 返回块的指针
}
}
return nullptr; // 如果没有可用块,返回nullptr
}
// 释放内存块
void free(void* ptr) {
// 检查指针是否在内存池的有效范围内
if (ptr >= pool && ptr < pool + poolsize) {
// 计算块的索引
int index = (static_cast<char*>(ptr) - pool) / blocksize;
if (used[index]) used[index] = false; // 标记为未使用
}
}
};
int main() {
MemoryAllocator pool(100, 4); // 创建一个内存池,总大小100字节,每块4字节
// 分配两个内存块
void* ptr1 = pool.Alloc();
std::cout << "Allocated at " << ptr1 << std::endl;
void* ptr2 = pool.Alloc();
std::cout << "Allocated at " << ptr2 << std::endl;
// 释放第二个内存块
pool.free(ptr2);
std::cout << "Freed at " << ptr2 << std::endl;
return 0;
}
内存溢出和内存泄漏是什么?如何避免?
- 内存泄漏:程序在堆上动态分配内存后,未能正确释放不再需要的内存,导致这部分内存无法被重用,从而使得系统中的可用内存逐渐减少。内存泄漏最终会导致内存溢出。
- 内存溢出(out of memory,OOM):程序试图分配的内存超过了系统或应用程序所能提供的最大可用内存,导致程序运行失败或异常。内存溢出可能发生在堆或栈上。
- 堆溢出:通常是因为内存泄漏
int main() { while (true) { int* ptr = new int[1000000]; } // 不断分配内存,最终会导致堆溢出 return 0; }
- 栈溢出:通常是因为过深的递归调用
void recursiveFunction() { int largeArray[10000]; // 大量消耗栈空间 recursiveFunction(); // 无限递归调用,最终会导致栈溢出 } int main() { recursiveFunction(); return 0; }
- 堆溢出:通常是因为内存泄漏
可以使用智能指针来自动管理内存,避免手动管理的复杂性。
内存碎片是什么?怎么解决?
- 内存碎片:在动态内存管理中,由于频繁的分配和释放内存,导致内存中出现许多大小不一的、无法被利用的空闲块的现象
- 内部碎片:当分配的内存块比实际需要的内存要大时,未使用的部分称为内部碎片。内部碎片主要出现在固定大小的内存分配策略中
- 外部碎片:内存中存在足够多的空闲空间总量,但由于它们不连续,无法为大块的内存请求提供服务
- 解决方法
- 内存紧缩:通过移动分散的内存块来将合并外部碎片,形成连续的内存块。紧缩通常伴随暂停程序运行,因此在实时系统中不太适用
- 优化分配策略:最佳适应、最先适应、最后适应、首次适应等
- 内存池:因为块的大小固定且地址连续,因此能避免外部碎片
栈
为什么栈的访问效率比堆高?
- 缓存局部性:栈内存分配是线性增长的,符合CPU缓存的局部性原则,访问效率高;而堆内存分配则可能在内存中是离散的,导致缓存命中率低,访问速度相对较慢
- 线程安全性:每个线程都有自己独立的栈,所以栈上的操作通常是线程安全的,不需要同步机制;堆是全局共享的资源,多个线程访问时可能需要同步机制(如锁),这会进一步降低堆内存分配和访问的效率
函数调用时栈的变化?
参考资料 参考资料
比如main函数调用add(int a , int b)函数:
- 从右往左压入add的参数值
- 保存call指令的下一条指针的地址
- 压入EBP(此时EBP指向main的栈底),EBP=ESP(让EBP指向add的栈底),保存main的栈
- 开辟add函数的栈空间
- add函数中的指令
- add函数保存计算值
- ESP=EBP,add函数开始退栈
- EBP出栈,就是还原main函数的栈底指针
- 清除add函数栈空间,继续执行main
这一过程中开辟出的空间称为栈帧,它包括以下内容:
- 被调函数的参数:通常从右向左顺序入栈(根据调用约定),即最后一个参数最先入栈。
- 被调函数的返回地址:用于在被调函数执行完毕后,返回到调用函数的下一条指令。
- 调用者函数的栈帧指针(ebp):保存调用者的栈底地址,以便在被调函数执行完毕后恢复调用者的栈帧。
- 被调函数的局部变量:在栈上为被调函数的局部变量分配的空间。
函数的参数列表为什么从右往左入栈?
主要是为了支持函数的变长参数 示例
但这只是一种约定,有些语言和平台并不这样