面试真题 | 飞科 C++[20241008]

@[toc]

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

回答

std::vector 是 C++ 标准库中的一个序列容器,它能够在尾部动态地增加或减少元素。vector 提供了多种方法来插入元素,其中 insert()emplace() 是两种常用的插入方法,但它们有一些关键的区别。

  1. insert() 方法:

    • insert() 方法用于在指定位置插入一个或多个元素。
    • 它接受一个迭代器(或位置索引)以及要插入的值(或值的范围)。
    • insert() 首先将新元素复制(或移动)到容器中的适当位置,如果插入的是一个对象,那么会调用该对象的复制构造函数(或移动构造函数)。
    • 示例代码:
      std::vector<int> vec = {1, 2, 3};
      vec.insert(vec.begin() + 1, 4); // vec 变为 {1, 4, 2, 3}
      
  2. emplace() 方法:

    • emplace() 方法也用于在指定位置插入元素,但它更高效,因为它直接构造元素在容器的内存位置。
    • 它接受一个迭代器(或位置索引)以及构造元素所需的参数。
    • 使用 emplace() 可以避免不必要的复制或移动操作,因为它直接在容器的内存中构造对象,调用的是构造函数而不是复制或移动构造函数。
    • 示例代码:
      std::vector<std::pair<int, std::string>> vec;
      vec.emplace(vec.end(), 1, "one"); // 直接在容器的末尾构造 pair 对象
      
  3. 性能差异:

    • 对于简单类型(如 int),insert()emplace() 的性能差异可能不大,因为复制构造的开销很小。
    • 但是,对于复杂类型或包含大量数据的对象,emplace() 可以显著减少复制或移动的开销,从而提高性能。
  4. 使用场景:

    • 使用 insert() 时,你已经有一个要插入的对象实例。
    • 使用 emplace() 时,你希望直接在容器的内存中构造对象,通常是因为构造对象所需的参数比已经存在的对象实例更容易获取或更直观。

面试官追问

  1. 如果 vector 的容量不足以容纳新元素,insert()emplace() 在内存分配和元素移动方面的行为有何异同?

    • 回答:在容量不足的情况下,两者都会触发容器的重新分配(通常是将容量加倍),并移动现有元素到新的内存位置。在这方面,它们的行为是相同的。不过,由于 emplace() 避免了不必要的临时对象创建,所以在重新分配和移动元素时,它可能在总体上稍微高效一些(尽管这个差异通常很小)。
  2. emplace_back()push_back() 的区别是什么?它们与 insert()emplace() 有什么联系?

    • 回答:emplace_back()push_back() 都是专门用于在 vector 的末尾插入元素的方法。emplace_back() 直接在末尾构造元素,而 push_back() 则接受一个已经构造的对象并将其复制(或移动)到末尾。这与 insert()emplace() 的区别类似,但 emplace_back()push_back() 只适用于在末尾插入的情况。
  3. 在什么情况下 emplace() 可能会抛出异常,而 insert() 不会?

    • 回答:emplace()insert() 都可能抛出异常,具体取决于元素的构造函数和复制/移动构造函数的异常安全性。然而,如果 emplace() 直接在容器中构造元素时使用了某些可能抛出异常的操作(如动态内存分配),而 insert() 只是复制或移动一个已经构造好的对象,那么在某些特定情况下(如内存不足),emplace() 可能会因为直接构造而抛出异常,而 insert() 可能因为已经有一个构造好的对象而避免了这种情况(尽管复制或移动构造本身也可能抛出异常)。通常,这种差异在实践中不太常见,因为大多数构造函数都设计为异常安全的。

2. C++中的智能指针有哪些?各自的特点是什么?

在嵌入式C++面试中,关于智能指针的问题非常常见,因为这涉及到内存管理的关键方面。

C++中的智能指针及其特点

C++中的智能指针主要包括以下几种:

  1. unique_ptr

    • 特点:unique_ptr是C++11引入的一种智能指针,它实现了独占所有权语义。这意味着一个unique_ptr实例在其生命周期内始终是其所管理对象的唯一所有者。unique_ptr不能被复制,但可以被移动(使用std::move),从而转移所有权。当unique_ptr被销毁时,它所管理的对象也会被自动删除,从而避免了内存泄漏。
    • 用途:适用于需要确保对象在同一时间内只被一个指针所拥有的场景。
  2. shared_ptr

    • 特点:shared_ptr实现了共享所有权语义。多个shared_ptr实例可以共享同一个对象,当最后一个shared_ptr被销毁时,它所管理的对象才会被删除。shared_ptr内部使用引用计数来跟踪有多少个shared_ptr实例共享同一个对象。
    • 用途:适用于对象需要在多个地方被共享,且需要自动管理其生命周期的场景。
    • 注意:使用shared_ptr时需要注意循环引用的问题,这可能导致内存无法被正确释放。为了解决这个问题,可以使用weak_ptr。
  3. weak_ptr

    • 特点:weak_ptr是一种不拥有所管理对象的智能指针。它可以从shared_ptr或另一个weak_ptr构造而来,但不会增加所管理对象的引用计数。当所管理的对象被销毁时,weak_ptr会自动变为空指针。
    • 用途:主要用于解决shared_ptr之间的循环引用问题,以及在不增加引用计数的情况下观察对象的生命周期。
  4. auto_ptr已废弃):

    • 特点:auto_ptr是C++98引入的一种智能指针,但由于其设计上的缺陷(如所有权语义不明确、易导致资源泄露等),在C++11中被废弃。
    • 注意:在现代C++编程中,应避免使用auto_ptr。

面试官可能追问的深入问题

  1. unique_ptr和shared_ptr在内存管理上的主要区别是什么?

    • 回答可以强调unique_ptr的独占所有权和shared_ptr的共享所有权,以及它们对对象生命周期管理的影响。
  2. 如何在shared_ptr之间解决循环引用的问题?

    • 回答可以指出循环引用会导致内存无法被正确释放,并说明weak_ptr是如何用来解决这个问题的。
  3. weak_ptr与shared_ptr之间有哪些主要的区别和联系?

    • 回答可以强调weak_ptr不拥有所管理对象,不会增加引用计数,而shared_ptr则拥有对象并会增加引用计数。同时,可以说明weak_ptr可以从shared_ptr构造而来,并用于观察对象的生命周期。
  4. 在实际开发中,你如何选择合适的智能指针?

    • 回答可以根据项目的具体需求和对象的使用场景来选择合适的智能指针。例如,在需要独占所有权时选择unique_ptr,在需要共享所有权时选择shared_ptr,在需要解决循环引用时选择weak_ptr等。

3. 解释一下C++中的RAII原则。

回答

RAII(Resource Acquisition Is Initialization)是C++中的一种资源管理技术,它基于构造函数和析构函数的特性来管理资源,确保资源在对象的生命周期内被正确获取和释放。RAII的核心思想是将资源的获取(如动态内存分配、文件句柄打开、互斥锁锁定等)放在对象的构造函数中,而将资源的释放(如内存释放、文件关闭、互斥锁解锁等)放在对象的析构函数中。

  1. 资源获取与初始化:当对象被创建时,构造函数自动执行,这时可以在构造函数中完成资源的分配和初始化。这样,一旦对象被成功构造,资源就已经被安全地获取。

  2. 资源释放与析构:当对象的作用域结束或对象被显式销毁时,析构函数自动执行,这时可以在析构函数中完成资源的释放。由于C++保证对象的析构函数在其生命周期结束时一定会被调用,因此可以确保资源被正确释放,避免资源泄露。

  3. 异常安全性:RAII还提供了异常安全性。如果在资源使用的过程中抛出异常,只要对象的作用域结束或对象被销毁,析构函数仍然会被调用,从而释放资源。这避免了在异常处理中手动释放资源的复杂性和潜在的错误。

  4. 简化资源管理:使用RAII可以简化资源管理代码,使代码更加清晰和易于维护。通过封装资源的获取和释放逻辑,可以减少资源泄露的风险,并提高代码的可读性和可靠性。

面试官追问

  1. 你能给出一个RAII的实际应用例子吗?

    • 示例:可以使用智能指针(如std::unique_ptrstd::shared_ptr)作为RAII的一个实际应用。智能指针在构造时分配内存,在析构时自动释放内存,从而确保内存不会被泄露。
  2. RAII在处理文件操作时是如何工作的?

    • 回答:在RAII模式下,可以定义一个文件操作类,其构造函数中打开文件,析构函数中关闭文件。这样,当对象被创建时文件被打开,当对象被销毁时文件被关闭,从而确保文件资源得到正确管理。
  3. 如果对象的构造函数抛出异常,析构函数还会被调用吗?(针对已经构造的部分对象)

    • 回答:如果对象的构造函数抛出异常,那么该对象的构造失败,析构函数不会被调用。但是,对于该对象构造过程中已经成功构造的成员对象(如果是通过成员初始化列表构造的),它们的析构函数会被调用。此外,如果在构造函数体内动态分配了资源,并且这些资源没有在构造函数的异常处理中正确释放,那么可能会导致资源泄露。因此,在构造函数中处理资源时,需要特别小心,确保在异常发生时也能正确释放资源。
  4. RAII与手动资源管理相比有哪些优势?

    • 回答:RAII通过自动管理资源,减少了手动管理资源的复杂性和错误风险。它确保资源在对象的生命周期结束时自动释放,避免了资源泄露。此外,RAII还提供了异常安全性,即使在异常发生的情况下也能保证资源被正确释放。这使得代码更加健壮和易于维护。

4. 默认构造函数、拷贝构造函数和移动构造函数的区别是什么?

回答

在C++中,默认构造函数、拷贝构造函数和移动构造函数是三种特殊的构造函数,它们在对象创建和初始化时扮演着不同的角色。

  1. 默认构造函数(Default Constructor)

    • 定义:没有参数或所有参数都有默认值的构造函数。
    • 用途:用于创建对象时不需要提供任何初始化值的情况。
    • 示例
      class MyClass {
      public:
          MyClass() {
              // 默认构造函数体
          }
      };
      
    • 调用时机
      • 使用MyClass obj;创建对象时。
      • 动态分配对象时,如MyClass* obj = new MyClass();
      • 作为类成员时,如果外部没有提供初始化值。
  2. 拷贝构造函数(Copy Constructor)

    • 定义:接受一个同类型对象作为参数的构造函数。
    • 用途:用于通过另一个同类型对象来初始化新对象,实现对象的深拷贝或浅拷贝。
    • 示例
      class MyClass {
      public:
          MyClass(const MyClass& other) {
              // 拷贝构造函数体
          }
      };
      
    • 调用时机
      • 使用MyClass obj2 = obj1;进行对象赋值时。
      • 将对象作为参数传递给函数时(如果函数参数是按值传递)。
      • 从函数返回对象时(如果返回类型是按值返回)。
  3. 移动构造函数(Move Constructor)

    • 定义:接受一个右值引用(rvalue reference)作为参数的构造函数。
    • 用途:用于从临时对象或即将被销毁的对象中“窃取”资源,避免不必要的拷贝,提高性能。
    • 示例
      class MyClass {
      public:
          MyClass(MyClass&& other) noexcept {
              // 移动构造函数体
          }
      };
      
    • 调用时机
      • 使用MyClass obj2 = std::move(obj1);进行对象赋值时。
      • 标准库容器(如std::vector)在重新分配内存和移动元素时。

面试官追问

  1. 追问1:在拷贝构造函数和移动构造函数中,如何区分传入的参数是左值还是右值?

    • 回答:通过参数类型来区分。拷贝构造函数的参数是左值引用(const MyClass&),而移动构造函数的参数是右值引用(MyClass&&)。
  2. 追问2:在移动构造函数中,为什么要使用noexcept关键字?

    • 回答:关键字表示该构造函数不会抛出异常。这对于标准库容器和某些算法来说很重要,因为它们可以利用这个保证来优化性能,例如,在移动

剩余60%内容,订阅专栏后可继续查看/也可单篇购买

C/C++面试必考必会 文章被收录于专栏

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

全部评论

相关推荐

评论
点赞
8
分享
牛客网
牛客企业服务