面试真题 | 工商银行C++

1. vector的insert()和emplace()有什么区别?

在 C++ 中,vectorinsert()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() 减少一次临时对象的构造和移动操作。

简单类型场景
对于 intdouble 等基础类型,两者性能差异可忽略,但 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_ptrshared_ptrweak_ptr
所有权模型 独占 共享 无所有权(观察者)
内存开销 无额外开销 含控制块(引用计数) 依赖 shared_ptr 控制块
线程安全性 非线程安全 引用计数原子操作 依赖 shared_ptr
典型应用场景 独占资源管理 共享资源 解决循环引用/缓存观察

选择建议

  1. 优先使用 unique_ptr:默认情况下选择独占所有权,减少引用计数开销和复杂性。
  2. 谨慎使用 shared_ptr:仅在明确需要共享时使用,避免滥用导致性能损耗或循环引用。
  3. weak_ptrshared_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!");  // 即使抛出异常,析构函数仍会关闭文件
}  // 析构函数自动调用

代码简洁:避免手动管理资源(如 deletefclose),减少因遗漏释放导致的资源泄漏。
原子性操作:资源获取与释放成对出现,避免“部分成功”导致资源泄漏(如构造函数中多个资源分配失败时的回滚)。

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++ 资源管理的基石,通过对象生命周期自动化管理资源,兼顾安全性与简洁性。其核心理念是:

  1. 资源获取即初始化:构造函数中完成资源绑定。
  2. 析构即释放:对象销毁时保证资源回收。
  3. 异常安全:任何执行路径下资源均不会泄漏。

现代 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++面试必考必会】专栏,直击面试核心,精选C/C++及相关技术栈中面试官最爱的必考点!从基础语法到高级特性,从内存管理到多线程编程,再到算法与数据结构深度剖析,一网打尽。助你快速构建知识体系,轻松应对技术挑战。希望专栏能让你在面试中脱颖而出,成为技术岗的抢手人才。

全部评论

相关推荐

评论
3
11
分享

创作者周榜

更多
牛客网
牛客企业服务