一、动态内存管理:new/delete与底层原理
核心问题1:new/delete vs malloc/free
区别对比:
特性 | new/delete | malloc/free |
---|---|---|
类型安全 | 自动推导类型,无需转型 | 返回void*,需强制转型 |
生命周期 | 自动调用构造/析构函数 | 需手动初始化/清理 |
错误处理 | 抛bad_alloc异常 | 返回NULL,检查errno |
数组支持 | new[]/delete[]自动管理元素 | 需手动处理数组元素 |
底层实现 | 封装operator new/delete(调malloc) | 直接调用系统函数 |
面试点睛:自定义类型必须用new/delete,因C语言的malloc无法管理对象生命周期,如String
类的构造函数会分配内存,析构函数释放内存,而malloc仅分配原始空间,易导致资源泄漏。
核心问题2:定位new的使用场景
定义:在已分配的内存上显式调用构造函数,语法为new(地址) 类型(参数)
。
场景:
- 内存池:预分配内存(如
char* buf = new char[sizeof(String)]
),用定位new初始化new(buf) String("hello")
。 - 缓冲区解析:网络接收的二进制数据解析为对象,复用已有内存。
- STL底层:如vector扩容时,对旧空间元素调用析构,新空间用定位new构造。
注意:需手动调用析构函数(s->~String()
)再释放内存,避免资源泄漏。
二、内存分布:栈、堆、数据段的本质区别
五大内存区域
- 栈区:自动分配释放,存局部变量、函数参数,生长方向高地址→低地址,效率高但空间有限(几MB)。
- 堆区:手动分配(new/malloc),存动态对象,生长方向低地址→高地址,大小灵活。
- 数据段:静态存储,分初始化(全局变量
int a=1
)和未初始化(BSS段,int b;
默认0)。 - 代码段:存可执行代码和只读常量(如
"hello"
),只读属性,共享性(多进程共享)。 - 内存映射段:动态链接库、共享内存,用于高效I/O和进程间通信。
典型变量位置
int global = 1; // 数据段(初始化)
static int stat = 2; // 数据段(静态全局)
void func() { int localVar = 3; // 栈区 static int statLoc = 4; // 数据段(静态局部) int* heapPtr = new int(5); // heapPtr在栈区,指向堆区
}
面试高频问:new
和malloc
分配的内存位于堆区,但new会调用构造函数,而malloc仅返回原始指针。
三、面向对象:封装本质与C++改进
核心问题1:封装的实现与作用
定义:通过类将数据(private成员)和方法(public接口)结合,控制访问权限。
示例:
class Stack {
public: void Push(int x); // 公开接口
private: int* _array; // 私有数据,外部不可直接访问
};
作用:
- 隐藏细节:用户无需知道栈如何扩容,只需调用Push。
- 数据保护:禁止外部直接修改
_top
,确保栈逻辑正确(如C语言中误用st.array[st.top]
会导致错误)。
核心问题2:构造/析构函数解决C语言痛点
C语言缺陷:手动调用StackInit
/StackDestroy
,易遗漏导致资源泄漏。
C++改进:
Stack st; // 自动调用构造函数初始化
st.Push(10);
// 离开作用域自动调用析构函数释放内存
本质:自动管理对象生命周期,避免人为错误,如文件句柄、网络连接等资源必在析构函数中释放。
四、STL:组件协作与容器选择
核心组件
- 容器:分顺序(vector/list/deque)、关联(set/map,红黑树实现)、适配器(stack/queue,封装底层容器)。
- 算法:通过迭代器操作容器,如
sort(vec.begin(), vec.end())
,与容器解耦。 - 迭代器:衔接容器与算法,分随机访问(vector)、双向(list)等,提供统一接口(
++
/*
)。
容器选择策略
- 随机访问优先:vector(下标O(1),连续存储,适合缓存友好)。
- 频繁头尾操作:deque(双端高效,如push_front/pop_back)。
- 有序唯一集合:set(自动排序,去重);键值对快速查找:unordered_map(哈希表,平均O(1)查找)。
面试陷阱:stack默认基于deque实现,而非vector,因deque头尾操作均为O(1),而vector尾插O(1),头插O(n)。
五、模板:泛型编程与实例化原理
核心问题1:函数模板vs类模板
特性 | 函数模板 | 类模板 |
---|---|---|
实例化 | 隐式(编译器推导) | 显式(必须写<T> ) |
类型参数 | 可省略 | 不可省略 |
生成产物 | 具体函数 | 具体类 |
作用范围 | 单个函数通用化 | 整个类通用化 |
示例:
template<typename T> void Swap(T& x, T& y); // 函数模板
Swap(1, 2); // 隐式实例化,T=int template<typename T> class Stack; // 类模板
Stack<int> st; // 显式实例化,必须指定T=int
核心问题2:模板为何导致代码膨胀?
原因:每个不同类型的实例化(如vector<int>
和vector<double>
)生成独立代码,无运行时类型擦除(对比Java泛型)。
优化:避免过度嵌套模板(如vector<vector<vector<int>>>
),利用编译器链接时优化(LTO)合并重复代码。
六、sizeof vs strlen:必考点对比
核心区别表
区别点 | sizeof(运算符) | strlen(库函数) |
---|---|---|
本质 | 编译时计算内存大小 | 运行时遍历\0 算长度 |
包含\0 | 是(如char arr[5]占5字节) | 否("abc"返回3) |
适用类型 | 所有类型 | 仅C风格字符串(\0 结尾) |
对指针处理 | 算指针本身大小(4/8字节) | 算指向字符串的长度 |
示例:
char str[] = "abc"; // sizeof(str)=4(含`\0`),strlen(str)=3
char* ptr = str; // sizeof(ptr)=8(64位指针),strlen(ptr)=3
易错点:对未以\0
结尾的字符数组用strlen,会越界访问,导致未定义行为。
七、string类:QT开发必知必会
核心问题1:小字符串优化(SBO)
机制:短字符串(如≤15字节,GCC实现)直接存在栈上,避免堆分配,提升性能。
string s = "hello"; // "hello"(5字节)存于string对象内部,无堆分配
s += "world"; // 超过SBO阈值,转为堆分配,capacity动态扩展(通常1.5倍)
核心问题2:与QT交互的编码处理
string转QString(UTF-8):
QString qstr = QString::fromUtf8(str.c_str(), str.size());
QString转string:
string str = qstr.toUtf8().data(); // 确保编码一致,避免中文乱码
多线程安全:
- 只读:线程安全,无需加锁;
- 写入:需用
QMutex
保护,如多个线程同时push_back
日志数据时。
面试准备建议
- 对比记忆:用表格梳理new/malloc、栈/堆、函数模板/类模板的区别,清晰直观。
- 原理深挖:理解new的底层步骤(operator new调malloc+构造函数),STL迭代器如何解耦容器与算法。
- 项目结合:如QT上位机开发中,用string处理UTF-8日志,通过reserve预分配提升性能,避免频繁扩容。
- 边界测试:准备越界访问(string::at()抛异常 vs operator[]断言)、内存泄漏(未配对delete[])等问题的解决方案。
通过系统梳理这些核心知识点,结合实际项目场景,可有效应对C++面试中的高频问题,展现扎实的基础与工程实践能力。