面试真题 | 工商银行C++
1. vector的insert()和emplace()有什么区别?
在 C++ 中,vector
的 insert()
和 emplace()
虽然都用于插入元素,但在实现机制、性能优化和应用场景上有显著区别。以下是具体分析:
1. 参数传递与构造方式
• insert()
需要显式构造对象或传递已有对象。对于复杂类型(如自定义类),需先创建临时对象再进行拷贝或移动操作。例如:
std::vector<Foo> v;
v.insert(v.begin(), Foo(42, 3.14)); // 需要构造临时 Foo 对象
这会触发一次构造函数(临时对象)和一次移动/拷贝构造函数(插入操作)。
• emplace()
直接通过参数列表在容器内部构造对象,无需创建临时对象。例如:
v.emplace(v.begin(), 42, 3.14); // 直接在 vector 内存中构造 Foo(42, 3.14)
仅调用一次构造函数,减少拷贝/移动开销。
2. 性能差异
• 复杂对象场景
对于构造函数代价高的对象(如包含动态内存分配的类),emplace()
显著更高效。例如:
std::vector<std::string> vec;
vec.emplace(vec.begin(), "hello"); // 直接构造 std::string
vec.insert(vec.begin(), std::string("world")); // 构造临时对象 + 移动操作
emplace()
减少一次临时对象的构造和移动操作。
• 简单类型场景
对于 int
、double
等基础类型,两者性能差异可忽略,但 insert()
语法更直观。
3. 语法与功能
• insert()
的灵活性
insert()
支持多种插入模式:
• 插入单个元素:v.insert(pos, value)
• 插入多个相同元素:v.insert(pos, n, value)
• 插入范围:v.insert(pos, first, last)
• 插入初始化列表:v.insert(pos, {a, b, c})
。
• emplace()
的限制
emplace()
每次只能插入一个元素,语法形式为:
iterator emplace(const_iterator pos, Args&&... args);
例如:
v.emplace(v.end(), 1, 2.0); // 构造 Foo(1, 2.0) 并插入末尾
4. 显式构造函数处理
• emplace()
的优势
当类的构造函数为 explicit
时,emplace()
可以直接传递参数,而 insert()
需显式构造临时对象:
struct Bar {
explicit Bar(int a, double b) {}
};
std::vector<Bar> bv;
bv.emplace_back(1, 2.0); // 合法
// bv.push_back({1, 2.0}); // 非法,需显式构造:bv.push_back(Bar(1, 2.0))
5. 应用场景建议
• 优先使用 emplace()
的情况:
• 插入复杂对象(如自定义类、std::string
)。
• 构造函数参数较多或包含 explicit
声明。
• 需要极致性能优化的场景。
• 优先使用 insert()
的情况:
• 需要批量插入多个元素或范围。
• 插入基础类型且代码可读性更重要。
• 兼容 C++11 之前的代码。
总结
特性insert()
emplace()
参数类型 | 已构造对象或值 | 构造函数参数列表 |
临时对象 | 可能产生 | 避免产生 |
性能 | 适合简单类型或批量插入 | 适合复杂对象或单次插入 |
语法灵活性 | 支持多元素、范围、初始化列表插入 | 仅支持单元素插入 |
显式构造函数 | 需显式构造临时对象 | 直接传递参数,支持 explicit 构造函数 |
通过合理选择 insert()
和 emplace()
,可以平衡代码效率与可维护性。
2. C++中的智能指针有哪些?各自的特点是什么?
在 C++ 中,智能指针通过 RAII(资源获取即初始化)机制实现自动内存管理,避免内存泄漏和悬垂指针等问题。以下是标准库中主要智能指针的分类及其核心特点:
1. std::unique_ptr
(独占所有权)
• 特点:
• 独占性:同一时间只能有一个 unique_ptr
指向特定内存资源,禁止拷贝构造和赋值操作(但支持移动语义转移所有权)。
• 轻量高效:不引入额外内存开销,性能接近裸指针。
• 支持自定义删除器:可指定释放资源的特殊方式(如 delete[]
或文件句柄关闭)。
• 适用场景:
• 动态分配对象的独占管理(如工厂模式返回的资源)。
• 替代已弃用的 auto_ptr
,用于需要明确所有权的场景。
• 示例:
std::unique_ptr<int> ptr = std::make_unique<int>(42); // C++14 推荐创建方式
std::unique_ptr<FILE, decltype(&fclose)> file(fopen("data.txt", "r"), fclose);
2. std::shared_ptr
(共享所有权)
• 特点:
• 引用计数:多个 shared_ptr
共享同一对象,引用计数为 0 时自动释放资源。
• 线程安全:引用计数的增减操作是原子的。
• 循环引用风险:若两个 shared_ptr
互相引用,会导致内存泄漏,需配合 weak_ptr
解决。
• 适用场景:
• 多个模块或线程需要共享同一资源(如缓存数据)。
• 需要延迟释放资源的场景(如观察者模式)。
• 示例:
auto obj = std::make_shared<MyClass>(); // 引用计数初始化为 1
std::shared_ptr<MyClass> copy = obj; // 引用计数增至 2
3. std::weak_ptr
(弱引用观察者)
• 特点:
• 不增加引用计数:观察 shared_ptr
管理的资源,但不会阻止其释放。
• 解决循环引用:通过 weak_ptr
打破 shared_ptr
的相互依赖。
• 需通过 lock()
提升:访问资源前需转换为 shared_ptr
,若资源已释放则返回空指针。
• 适用场景:
• 缓存系统中观察资源是否有效。
• 避免双向关联对象(如树形结构父子节点)的循环引用。
• 示例:
std::weak_ptr<MyClass> observer = obj;
if (auto sptr = observer.lock()) { // 提升为 shared_ptr
sptr->doSomething();
}
4. 已弃用的 std::auto_ptr
• 特点:
• 隐式所有权转移:赋值操作会转移所有权,导致原指针悬空。
• 易引发未定义行为:如通过悬空指针访问已释放资源。
• 替代方案:C++11 起用 unique_ptr
替代,因其更安全且支持移动语义。
对比总结
特性unique_ptr
shared_ptr
weak_ptr
所有权模型 | 独占 | 共享 | 无所有权(观察者) |
内存开销 | 无额外开销 | 含控制块(引用计数) | 依赖 shared_ptr 控制块 |
线程安全性 | 非线程安全 | 引用计数原子操作 | 依赖 shared_ptr |
典型应用场景 | 独占资源管理 | 共享资源 | 解决循环引用/缓存观察 |
选择建议
- 优先使用
unique_ptr
:默认情况下选择独占所有权,减少引用计数开销和复杂性。 - 谨慎使用
shared_ptr
:仅在明确需要共享时使用,避免滥用导致性能损耗或循环引用。 weak_ptr
与shared_ptr
配合:用于需要观察资源生命周期的场景。
通过合理选择智能指针类型,可显著提升代码的健壮性和可维护性,同时规避常见的内存管理错误。
3. 解释一下C++中的RAII原则。
C++ 中的 RAII 原则解析
RAII(Resource Acquisition Is Initialization,资源获取即初始化)是 C++ 的核心编程范式,通过对象的生命周期自动管理资源,确保资源的安全获取与释放。以下从多个维度深入解析其原理与实践:
1. 核心机制:资源与对象生命周期的绑定
• 资源获取在构造函数:对象构造时完成资源的分配或获取(如内存、文件句柄、锁等)。例如,std::fstream
在构造时打开文件,std::lock_guard
在构造时加锁。
• 资源释放在析构函数:对象销毁时自动释放资源,无论程序正常执行完毕还是因异常提前退出。
class FileHandler {
public:
FileHandler(const char* filename) { file = fopen(filename, "r"); } // 构造时获取资源
~FileHandler() { if (file) fclose(file); } // 析构时释放资源
private:
FILE* file;
};
2. 核心优势:安全性与简洁性
• 异常安全:即使函数中途抛出异常,栈展开(Stack Unwinding)也会触发析构函数,确保资源释放。
void process() {
FileHandler fh("data.txt"); // 构造时打开文件
throw std::runtime_error("Oops!"); // 即使抛出异常,析构函数仍会关闭文件
} // 析构函数自动调用
• 代码简洁:避免手动管理资源(如 delete
、fclose
),减少因遗漏释放导致的资源泄漏。
• 原子性操作:资源获取与释放成对出现,避免“部分成功”导致资源泄漏(如构造函数中多个资源分配失败时的回滚)。
3. 典型应用场景
• 智能指针:
• std::unique_ptr
:独占资源所有权,析构时自动释放内存。
• std::shared_ptr
:通过引用计数共享资源,计数归零时释放。
• 锁管理:
std::mutex mtx;
void safe_write() {
std::lock_guard<std::mutex> lock(mtx); // 构造时加锁,析构时解锁
// 临界区操作
} // 自动解锁,避免死锁
• 文件与网络连接:如 std::fstream
、自定义数据库连接类。
4. RAII 与现代 C++ 实践
• 优先使用智能指针:替代裸指针和手动 new/delete
,避免内存泄漏。
auto ptr = std::make_unique<int>(42); // C++14 推荐方式
• 结合异常机制:异常触发时,RAII 保证资源释放,而 try-catch
块仅处理逻辑错误。
• 避免循环引用:shared_ptr
需配合 weak_ptr
打破循环引用,防止内存泄漏。
5. 局限性及注意事项
• 拷贝语义处理:需禁用拷贝构造函数或实现深拷贝,避免资源重复释放。
class NonCopyable {
public:
NonCopyable(const NonCopyable&) = delete; // 禁用拷贝
NonCopyable& operator=(const NonCopyable&) = delete;
};
• 不适用于所有资源:某些复杂资源(如需要批量处理的资源)可能难以用单一对象封装。
• 性能开销:引用计数(如 shared_ptr
)会引入额外开销,需权衡使用场景。
总结
RAII 是 C++ 资源管理的基石,通过对象生命周期自动化管理资源,兼顾安全性与简洁性。其核心理念是:
- 资源获取即初始化:构造函数中完成资源绑定。
- 析构即释放:对象销毁时保证资源回收。
- 异常安全:任何执行路径下资源均不会泄漏。
现代 C++ 中,RAII 已深度融入标准库(如容器、智能指针、锁),成为编写高效、安全代码的核心准则。合理使用 RAII,可显著提升代码健壮性,降低维护成本。
4. 默认构造函数、拷贝构造函数和移动构造函数的区别是什么?
在 C++ 中,默认构造函数、拷贝构造函数和移动构造函数分别承担不同的对象构造职责,其核心区别体现在对象初始化方式、资源管理策略和性能优化上。以下是三者对比的深度解析:
1. 核心功能区别
构造函数类型核心作用典型场景默认构造函数 | 无参数初始化对象,为成员变量赋予默认值 | 创建空对象、数组初始化 |
拷贝构造函数 | 通过已有对象复制构造新对象,可能引发深拷贝/浅拷贝问题 | 对象传参、返回值优化(RVO) |
移动构造函数 | 通过转移资源所有权构造新对象,避免冗余拷贝,提升性能(C++11 引入) | 临时对象处理、大对象资源转移 |
2. 参数与调用时机对比
默认构造函数
• 参数:无参数,或所有参数有默认值
• 调用时机:
MyClass obj; // 显式调用
std::vector<MyClass> vec(5); // 隐式调用 5 次默认构造
拷贝构造函数
• 参数:常量左值引用(const T&
)
• 调用时机:
MyClass obj2 = obj1; // 显式拷贝构造
void func(MyClass param) {} // 传参时隐式拷贝构造
移动构造函数
• 参数:右值引用(T&&
),通常配合 std::move
使用
• 调用时机:
MyClass obj2 = std::move(obj1); // 显式移动构造
return temporary_obj; // 函数返回临时对象时隐式调用
3. 资源管理方式
默认构造函数
• 资源分配:仅初始化成员变量,不涉及动态资源(除非成员本身持有资源)
class FileHandler {
public:
FileHandler() : file(nullptr) {} // 默认构造初始化空指针
};
拷贝构造函数
• 深拷贝:复制所有成员值,包括动态分配的资源(需手动实现)
// 深拷贝示例
MyClass(const MyClass& other) {
data = new int(*other.data); // 复制堆内存内容
}
• :默认生成的拷贝构造函数仅复制
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
【C/C++面试必考必会】专栏,直击面试核心,精选C/C++及相关技术栈中面试官最爱的必考点!从基础语法到高级特性,从内存管理到多线程编程,再到算法与数据结构深度剖析,一网打尽。助你快速构建知识体系,轻松应对技术挑战。希望专栏能让你在面试中脱颖而出,成为技术岗的抢手人才。