Skip to content

百度C++开发二面面经

来源:https://www.nowcoder.com/discuss/396066339339743232

1、项目(略)

2、vector分配内存的方式

当我们使用 std::vector 时,它会动态地管理内存,以适应元素的增长。std::vector 内部使用动态数组来存储元素,这个动态数组的大小是动态调整的,因此我们可以在运行时向 std::vector 中添加或删除元素,而不需要担心数组大小的问题。

std::vector 会维护两个关键的属性:

  1. size:表示当前 std::vector 中的元素个数。
  2. capacity:表示 std::vector 内部分配的存储空间的容量。

当我们向 std::vector 中添加新元素时,如果 size 达到了 capacity,即当前存储空间已经满了,std::vector 就会触发内部的重新分配内存的过程,来扩充存储空间。这个过程通常包括以下几个步骤:

  1. 分配新的更大的内存空间:std::vector 会根据需要申请一块更大的内存空间,通常会将容量扩大为原来的两倍或更多,以减少频繁的内存分配操作。
  2. 将原有元素从旧的内存空间拷贝到新的内存空间:std::vector 会将原来的元素按顺序拷贝到新的内存空间中。
  3. 释放旧的内存空间:一旦所有元素都已成功拷贝到新的内存空间,std::vector 会释放原来的内存空间。

3、std::queue的数据结构

std::queue 是 C++ 标准库中的队列容器适配器,它是基于其他底层容器(比如 std::dequestd::list)实现的。std::queue 的特点是先进先出(FIFO)的数据结构,它只允许在队列的末尾(尾部)添加元素,并且只允许在队列的开头(头部)移除元素。

默认情况下,std::queue 使用 std::deque(双端队列)作为其默认底层容器。std::deque 是一种双端队列,它允许在两端高效地添加和删除元素,因此非常适合作为队列的底层数据结构。在 std::queue 中,元素的添加操作称为入队(enqueue),元素的移除操作称为出队(dequeue)。std::queue 提供了 push(入队)、pop(出队)、front(返回队头元素)、back(返回队尾元素)等操作,以及 empty(判断队列是否为空)、size(返回队列中元素的个数)等方法。

上个例子:

c++
#include <iostream>
#include <queue>
using namespace std;

int main() {
    // 创建一个 std::queue
    queue<int> q;

    // 向队列中添加元素
    q.push(1);
    q.push(2);
    q.push(3);

    // 输出队列中的元素
    cout << "队列中的元素:";
    while (!q.empty()) {
        cout << q.front() << " "; // 输出队头元素
        q.pop(); // 出队
    }
    cout << endl;

    return 0;
}

4、map的数据结构

在 C++ 标准库中,std::map 是一个关联容器,它提供了一种将键值对关联起来的方式。std::map 的数据结构通常是基于红黑树(Red-Black Tree)实现的。

红黑树是一种自平衡的二叉查找树,它具有以下特点:

  1. 每个节点要么是红色,要么是黑色。
  2. 根节点是黑色的。
  3. 每个叶子节点(NIL 节点,空节点)是黑色的。
  4. 如果一个节点是红色的,则它的两个子节点都是黑色的。
  5. 对于每个节点,从该节点到其所有后代叶子节点的简单路径上,均包含相同数目的黑色节点。

由于红黑树具有自平衡的特性,它可以保证在进行插入、删除等操作时,树的高度始终保持在 O(log n) 的水平,从而保证了 std::map 的操作效率。

std::map 中,每个元素都是一个键值对,键和值可以是任意类型。std::map 中的元素按照键的顺序进行排序,因此可以通过键来快速查找和访问对应的值。在 std::map 中,键是唯一的,如果插入具有相同键的元素,新元素会覆盖旧元素。

5、unordered_map的数据结构

在 C++ 标准库中,std::unordered_map 是一个关联容器,它提供了一种将键值对关联起来的方式,但与 std::map 不同的是,std::unordered_map 使用哈希表(hash table)作为其底层数据结构,而不是红黑树。

哈希表是一种通过哈希函数将键映射到索引的数据结构,它具有以下特点:

  1. 快速查找:通过哈希函数计算键对应的索引,可以在接近 O(1) 的时间复杂度内查找到对应的值。
  2. 无序性:哈希表中的元素是无序的,与元素插入的顺序无关。
  3. 冲突处理:由于哈希函数的映射可能存在冲突(即多个键映射到同一个索引),因此哈希表需要解决冲突的方法,常见的方法包括链地址法(Chaining)和开放地址法(Open Addressing)等。

std::unordered_map 中,每个元素都是一个键值对,键和值可以是任意类型。std::unordered_map 中的元素存储在哈希表中,通过键的哈希值来确定元素的存储位置,因此可以快速查找和访问对应的值。

需要注意的是,由于哈希表的特性,std::unordered_map 中的元素是无序的,因此在需要有序性的场景下,可能需要考虑使用 std::map。此外,由于哈希表的性质,std::unordered_map 中的元素插入和查找操作的平均时间复杂度为 O(1),但最坏情况下的时间复杂度可能会达到 O(n),其中 n 是元素的数量。

6、map和unordered_map的使用场景,大数据量的时候用哪一个?

std::mapstd::unordered_map 都是关联容器,它们提供了键值对的关联方式,但是它们在实现和使用上有一些不同,因此适用的场景也有所不同。

  • std::map
    • 基于红黑树实现,有序性好,元素按照键的顺序排列。
    • 插入、删除、查找等操作的平均时间复杂度为 O(log n),其中 n 是元素的数量。
    • 适用于需要元素有序存储,并且需要频繁地进行插入、删除、查找等操作的场景。
  • std::unordered_map
    • 基于哈希表实现,无序性好,元素存储在哈希表中,根据键的哈希值快速查找。
    • 插入、删除、查找等操作的平均时间复杂度为 O(1),但最坏情况下的时间复杂度可能为 O(n),其中 n 是元素的数量。
    • 适用于不需要元素有序存储,但需要快速进行插入、删除、查找等操作的场景。

当需要元素有序存储,并且需要频繁地进行插入、删除、查找等操作时,可以选择使用 std::map。而当不需要元素有序存储,但需要快速进行插入、删除、查找等操作时,可以选择使用 std::unordered_map

对于大数据量的情况,一般情况下 std::unordered_map 更适合,因为它的插入、删除、查找等操作的平均时间复杂度为 O(1),而 std::map 的这些操作的时间复杂度是 O(log n),在大数据量的情况下,std::unordered_map 的性能优势会更加明显。但是需要注意,std::unordered_map 在极端情况下(比如哈希冲突严重)的性能可能会下降,需要根据具体场景进行选择。

7、简单说一下用过的智能指针?

  1. std::shared_ptr

    • 原理:std::shared_ptr是基于引用计数的智能指针,用于管理动态分配的对象。它维护一个引用计数,当计数为零时,释放对象的内存。

    • 使用场景:适用于多个智能指针需要共享同一块内存的情况。例如,在多个对象之间共享某个资源或数据。

    • C++
      std::shared_ptr<int> sharedInt = std::make_shared<int>(42);
      std::shared_ptr<int> anotherSharedInt = sharedInt; // 共享同一块内存
  2. std::unique_ptr

    • 原理:std::unique_ptr是独占式智能指针,意味着它独占拥有所管理的对象,当其生命周期结束时,对象会被自动销毁。

    • 使用场景:适用于不需要多个指针共享同一块内存的情况,即单一所有权。通常用于资源管理,例如动态分配的对象或文件句柄。

    • C++
      std::unique_ptr<int> uniqueInt = std::make_unique<int>(42);
      // uniqueInt 的所有权是唯一的
  3. std::weak_ptr

    • 原理:std::weak_ptr是一种弱引用指针,它不增加引用计数。它通常用于协助std::shared_ptr,以避免循环引用问题。

    • 使用场景:适用于协助解决std::shared_ptr的循环引用问题,其中多个shared_ptr互相引用,导致内存泄漏。

    • C++
      std::shared_ptr<int> sharedInt = std::make_shared<int>(42);
      std::weak_ptr<int> weakInt = sharedInt;
  4. std::auto_ptr(已废弃):

    • 原理:std::auto_ptr是C++98标准引入的智能指针,用于独占地管理对象。但由于其存在潜在的问题,已在C++11中被废弃。

    • 使用场景:在C++98标准中,可用于独占性地管理动态分配的对象。不推荐在现代C++中使用。

    • C++
      std::auto_ptr<int> autoInt(new int(42)); // 已废弃

8、make_shared函数里面干了哪些事情?

std::make_shared 是 C++11 引入的函数模板,用于创建 std::shared_ptr 对象并初始化它所管理的对象。在调用 std::make_shared 时,它会完成以下几件事情:

  1. 分配内存:std::make_shared 会在一块连续的内存上分配所需的内存空间,用于存储对象的数据以及 std::shared_ptr 的控制块。
  2. 构造对象:在分配的内存空间中调用对象的构造函数,初始化对象的数据。
  3. 创建 std::shared_ptr:将分配的内存空间与一个控制块关联起来,并返回一个 std::shared_ptr 对象,该对象包含了指向分配的内存空间的指针以及一个引用计数。

由于 std::make_shared 在一次内存分配中完成了对象的构造和控制块的分配,因此它通常比直接使用 new 来创建 std::shared_ptr 更高效,因为减少了额外的内存分配和构造的开销。

9、什么时候不能用make_shared函数,只能用shared_ptr的构造函数?

  1. 需要控制对象的内存分配方式:std::make_shared 会在一次内存分配中分配对象和控制块的内存,但有时可能需要更精细的控制,比如指定自定义的内存分配器或者在构造对象时需要传递额外的参数。
  2. 需要避免一次性分配大量内存:由于 std::make_shared 会一次性分配内存来存储对象和控制块,当对象较大或者需要创建大量对象时,可能会导致内存浪费或者内存碎片化。
  3. 需要延迟对象的构造:std::make_shared 会立即构造对象,如果需要延迟对象的构造(比如在构造函数中可能会抛出异常),就不能使用 std::make_shared

在这些情况下,可以使用 std::shared_ptr 的构造函数来手动分配内存,并在需要时显式地调用对象的构造函数。

10、shared_ptr和unique_ptr如何自定义删除函数?

在 C++11 及更新的标准中,可以使用自定义的删除器(deleter)来管理 std::shared_ptrstd::unique_ptr 指向的资源。自定义删除器是一个函数对象或者函数指针,用于在释放资源时执行特定的清理操作。

对于 std::shared_ptr,可以通过在创建 std::shared_ptr 时指定删除器来自定义删除函数。例子:

c++
struct MyDeleter {
    void operator()(int* p) const {
        std::cout << "Deleting int pointer" << std::endl;
        delete p;
    }
};

int main() {
    std::shared_ptr<int> sp(new int(42), MyDeleter());
    // 使用 sp
    return 0;
}

对于 std::unique_ptr,可以在创建 std::unique_ptr 对象时指定删除器,也可以在运行时使用 std::unique_ptr::reset() 方法重新指定删除器。例如:

c++
struct MyDeleter {
    void operator()(int* p) const {
        std::cout << "Deleting int pointer" << std::endl;
        delete p;
    }
};

int main() {
    std::unique_ptr<int, MyDeleter> up(new int(42));
    // 使用 up
    up.reset(new int(10), MyDeleter()); // 重新指定删除器
    return 0;
}

例子中,MyDeleter 是一个自定义的删除器,它重载了函数调用运算符 operator(),在其中执行了特定的清理操作。在创建 std::shared_ptrstd::unique_ptr 对象时,将自定义的删除器作为第二个模板参数传递给它们,即可使用自定义的删除函数来管理资源的释放。

11、简单讲讲C++中的静态多态和动态多态,哪些是静态多态和动态多态,模板是静态多态还是动态多态?

  1. 静态多态(静态绑定):
    • 在编译时确定函数调用的具体实现,不会在运行时再去动态查找函数的地址。
    • 静态多态的实现方式主要有函数重载和模板。
  2. 动态多态(动态绑定):
    • 在运行时根据对象的实际类型确定函数调用的具体实现,需要通过虚函数(virtual function)来实现。
    • 动态多态的实现方式主要是通过继承和虚函数实现的。

模板是一种静态多态的机制,它通过在编译时生成不同类型的代码来实现多态性。在使用模板时,编译器会根据模板参数的具体类型生成对应的代码,因此模板实现了静态多态。

而虚函数机制则是 C++ 中实现动态多态的方式,通过基类的指针或引用调用虚函数时,会根据对象的实际类型来确定调用的函数实现。这种动态的函数调用是在运行时确定的,因此实现了动态多态。

12、引用存不存在空悬引用?

在 C++ 中,引用不存在空悬引用的问题,因为引用在定义时必须初始化,并且一旦引用与某个对象绑定后,它将一直引用该对象,无法重新绑定到其他对象。因此,正常情况下,C++ 中的引用不会出现空悬引用的情况。

13、引用底层实现是怎么实现的?

在 C++ 中,引用的底层实现通常是通过指针来实现的,但是在语法和使用上有所不同。编译器在编译时会对引用进行优化,使得它在使用上更类似于直接访问对象而不是通过指针。

具体地说,引用在内部通常会被实现为对其所引用对象的一个指针。这个指针在引用初始化时就会被指向引用的对象,并且在整个引用的生命周期内不会改变指向其他对象。由于引用在语法上更类似于对象本身,因此可以在代码中使用引用来代替指针,使得代码更加简洁和易读。

需要注意的是,由于引用在底层是通过指针实现的,因此在某些情况下,引用可能会受到指针的限制,比如无法指向空值(nullptr),也无法实现指针的一些特性,比如指针的算术运算。

14、既然引用底层是指针,那么引用中的指针会不会存在空悬的情况?

在 C++ 中,引用的底层实现通常是通过指针来实现的,但是引用在语义上和指针是有很大区别的。在语义上,引用代表了一个已经存在的对象的别名,它在定义时必须初始化,并且在其生命周期内一直指向同一个对象,无法改变指向其他对象。由于引用的这些特性,它不会出现空悬的情况。

空悬指针是指指针在指向的对象被销毁后仍然存在,指针的值未被清空,这时如果再去访问指针所指向的对象就会产生未定义行为。而引用在定义时必须初始化,并且在整个生命周期内都指向同一个对象,因此不存在空悬引用的情况。

15、空悬指针是在编译阶段出现的还是运行阶段出现的?

空悬指针通常是在运行阶段(即程序执行时)出现的。空悬指针是指指针在运行时指向了一个已经被释放或者超出了作用域的内存地址,导致在访问该地址时产生未定义行为。这种情况通常是由于程序员错误地使用了已经被释放或者超出了作用域的指针,或者在指针被释放后没有将其设置为 nullptr 等无效值导致的。

空悬指针的出现通常是由于程序中的逻辑错误或者使用了未初始化的指针等问题引起的,它可能导致程序崩溃、内存泄漏或者其他未定义行为。因此,在编写 C++ 程序时,需要特别注意对指针的管理,确保指针的有效性以避免空悬指针的出现。

16、左值和右值的区别?

  1. 左值:
    • 左值是指向内存位置的表达式,可以出现在赋值语句的左边或右边。
    • 左值具有持久性,可以取地址。
    • 可以修改左值所代表的对象的值。
  2. 右值:
    • 右值是指临时对象的表达式,通常出现在赋值语句的右边。
    • 右值通常是临时生成的,没有持久性,不能取地址。
    • 不能对右值进行赋值操作。

在 C++11 中,引入了右值引用的概念,它可以绑定到右值,例如 int&& 就是一个右值引用类型。右值引用可以用于移动语义和完美转发等高级特性。

17、如何快速判断一个值是左值还是右值?

  1. 左值:
    • 如果一个表达式有名称(变量、函数、对象等),那么它通常是一个左值。
    • 可以对左值进行取地址操作(&运算符)。
  2. 右值:
    • 如果一个表达式是临时生成的、没有名称的,通常是一个右值。
    • 不能对右值进行取地址操作。

例如:

c++
int x = 10; // x 是左值
int y = x;  // x 是左值,y 是左值
int z = x + y; // x + y 是右值
int* ptr = &x; // &x 是左值,ptr 是左值

18、介绍一下左值引用和右值引用?

  1. 左值引用:
    • 左值引用是对左值的引用,通常表示一个具名对象的引用。
    • 使用 & 符号声明,例如 int& ref = x;,其中 ref 是对 x 的左值引用。
    • 左值引用可以绑定到左值,但不能绑定到右值。
  2. 右值引用:
    • 右值引用是对右值的引用,通常表示一个临时对象的引用。
    • 使用 && 符号声明,例如 int&& rref = 10;,其中 rref 是对临时对象 10 的右值引用。
    • 右值引用可以绑定到右值,但不能绑定到左值。

右值引用最重要的应用之一是支持移动语义(Move Semantics),通过移动构造函数和移动赋值运算符,可以高效地将资源(比如动态分配的内存)从一个对象转移到另一个对象,避免了不必要的资源拷贝,提高了程序的性能。

19、函数的返回值是左值还是右值?

函数的返回值可以是左值也可以是右值,取决于返回类型和返回表达式的类型。

  1. 如果返回类型是非引用类型(如 intdouble 等基本类型或者类类型),并且返回的是一个具体的对象(非引用),那么返回值是一个右值。

    c++
    int getValue() {
        return 10; // 返回值是一个右值
    }
  2. 如果返回类型是引用类型,那么返回值是一个左值。

    c++
    int x = 10;
    int& getRef() {
        return x; // 返回值是一个左值引用
    }
  3. 如果返回类型是右值引用类型,那么返回值是一个右值。

    c++
    int&& getRvalueRef() {
        return 10; // 返回值是一个右值引用
    }

20、前置++和后置++的区别?返回值的类型是什么?

  1. 前置递增运算符 ++

    • 先对操作数进行递增操作,然后返回递增后的值。

    • 返回的是递增后的值的引用。

    c++
    int x = 10;
    int& ref = ++x; // x 先递增为 11,然后将递增后的值 11 赋给 ref
  2. 后置递增运算符 ++

    • 先返回操作数的原值,然后再对操作数进行递增操作。
    • 返回的是递增前的值的副本(而非引用)。
    c++
    int x = 10;
    int y = x++; // 先将 x 的值 10 赋给 y,然后 x 递增为 11

21、重载、重写、重载覆盖的区别?

  1. 重载(Overload):

    • 重载指的是在同一个作用域内,使用相同的函数名但参数列表不同的多个函数,以实现同一种功能的不同方式。

    • 重载函数的区分标准是函数的参数列表(包括参数的类型、个数、顺序等)。

    • 重载函数可以有不同的返回类型,但不能仅仅通过返回类型来区分。

    c++
    void print(int x) {
        // ...
    }
    
    void print(double x) {
        // ...
    }
  2. 重写(Override):

    • 重写是指在派生类中重新定义基类中的虚函数,以实现不同的行为。
    • 重写函数的函数名、参数列表和返回类型都必须与基类中的虚函数一致。
    • 重写函数可以通过 override 关键字进行标识,以增强代码的可读性和可维护性。
    c++
    class Base {
    public:
        virtual void print() {
            // ...
        }
    };
    
    class Derived : public Base {
    public:
        void print() override {
            // ...
        }
    };
  3. 覆盖:

    • 覆盖是一个更加宽泛的概念,它可以指代重写(Override),也可以指代其他形式的覆盖。
    • 在某些语境下,覆盖可以指代函数的重载(Overload)或者其他概念。
    • 在 C++ 中通常更倾向于使用“重写(Override)”来表示在派生类中重新定义基类中的虚函数的行为。

22、模板?

C++ 模板是一种通用编程工具,允许编写通用的类和函数,以支持类型无关的编程。C++ 模板包括函数模板和类模板两种类型,它们都可以实现通用性的编程。

  1. 函数模板(Function Templates):

    • 函数模板是一种定义通用函数的方法,可以用于生成特定类型的函数。
    • 函数模板的定义以关键字 template 开头,后面跟着模板参数列表和函数声明或定义。
    • 模板参数列表中可以包含类型参数、非类型参数和模板参数包等。
    • 调用函数模板时,编译器会根据实际参数类型推断模板参数类型,并生成相应的函数实例。
    c++
    template <typename T>
    T max(T a, T b) {
        return (a > b) ? a : b;
    }
    
    int main() {
        int result = max(10, 20); // 实例化 max 函数模板,T 推断为 int
        return 0;
    }
  2. 类模板(Class Templates):

    • 类模板是一种定义通用类的方法,可以用于生成特定类型的类。
    • 类模板的定义以关键字 template 开头,后面跟着模板参数列表和类的定义。
    • 类模板的模板参数列表和函数模板类似,可以包含类型参数、非类型参数和模板参数包等。
    • 使用类模板时,需要提供模板参数,用以实例化特定类型的类。
    c++
    template <typename T>
    class MyContainer {
    public:
        void add(T element);
        T get(int index);
        // ...
    };
    
    int main() {
        MyContainer<int> container; // 实例化 MyContainer 类模板,T 替换为 int
        container.add(10);
        int value = container.get(0);
        return 0;
    }

    C++ 模板的主要优势在于可以实现通用性的编程,减少了代码的重复性,同时提高了代码的可重用性和可维护性。模板还是实现泛型编程的重要工具,在 STL(标准模板库)等库中得到广泛应用。

23、堆和栈的区别?

  1. 栈:
    • 栈是一种线性的数据结构,它具有后进先出(LIFO)的特性。
    • 栈的内存分配由编译器自动完成,存储局部变量、函数参数、函数返回地址等信息。
    • 栈上的内存由系统自动分配和释放,变量的生命周期与函数的调用关系密切相关。
    • 栈的大小有限,通常是固定的,因此存储在栈上的数据大小和生命周期需要在编译时确定。
  2. 堆:
    • 堆是一种树形的数据结构,它的内存分配由程序员手动控制。
    • 堆上的内存由程序员手动申请和释放,存储动态分配的对象、数组等。
    • 堆上的内存分配和释放不受函数调用关系的影响,生命周期由程序员控制。
    • 堆上的内存大小一般比较大,并且可以动态增长,但需要程序员手动管理内存,避免内存泄漏和内存访问错误。

24、用过GDB调试没?

给大家直接看看简单的流程吧。

  1. 编译时添加调试信息:在编译程序时,需要使用-g选项,以便将调试信息嵌入可执行文件中。例如:
Bash
g++ -g -o my_program my_program.cpp
  1. 启动 GDB:在终端中执行以下命令:
Bash
gdb ./my_program

这将启动 GDB 并加载你的可执行文件。

  1. 设置断点:设置断点以在程序执行到指定位置时暂停。可以使用break命令:
Bash
break main

这将在main函数的开头设置一个断点。你也可以使用文件名和行号设置断点。

  1. 运行程序:在 GDB 中执行run命令启动程序:
Bash
run

当程序执行到断点时,会暂停执行。

  1. 查看变量的值:使用print或简写的p命令来查看变量的值:
Bash
print variable_name
  1. 单步执行:使用step命令来单步执行程序,进入函数内部:
Bash
step
  1. 下一步执行:使用next命令来执行下一行代码,不进入函数内部:
Bash
next
  1. 继续执行:使用continue或简写的c命令来继续执行程序直到下一个断点:
Bash
continue
  1. 查看堆栈:使用backtrace或简写的bt命令来查看函数调用堆栈:
Bash
backtrace
  1. 退出 GDB:使用quit或简写的q命令退出 GDB:
Bash
quit

25、设计模式用过吗?写一下单例模式?

前面好多章节都有讲过,今天说一下单例模式的两种形式。

  1. 懒汉式单例模式:

懒汉式单例模式在需要时才创建单例实例。如果实例尚未创建,就创建并返回,否则直接返回现有的实例。这种方式在首次使用时才创建实例,延迟了对象的创建时间。

C++
#include <iostream>

class LazySingleton {
private:
    // 私有构造函数,防止外部创建实例
    LazySingleton() {
        std::cout << "LazySingleton instance created." << std::endl;
    }

    // 静态成员变量,用于保存唯一实例
    static LazySingleton* instance;

public:
    // 获取单例实例的静态方法
    static LazySingleton* getInstance() {
        if (!instance) {
            instance = new LazySingleton();
        }
        return instance;
    }

    // 其他成员函数
    void doSomething() {
        std::cout << "LazySingleton is doing something." << std::endl;
    }
};

// 初始化静态成员变量
LazySingleton* LazySingleton::instance = nullptr;

int main() {
    // 获取单例实例
    LazySingleton* instance1 = LazySingleton::getInstance();
    LazySingleton* instance2 = LazySingleton::getInstance();

    // 执行一些操作
    instance1->doSomething();
    instance2->doSomething();

    // 输出结果应该表明 instance1 和 instance2 是同一个实例
    return 0;
}
  1. 饿汉式单例模式:

饿汉式单例模式在类加载时就创建了单例实例。这样可以确保实例在首次使用之前就已经存在。饿汉式的缺点是在程序启动时就创建实例,可能会导致不必要的资源浪费。

C++
#include <iostream>

class EagerSingleton {
private:
    // 私有构造函数,防止外部创建实例
    EagerSingleton() {
        std::cout << "EagerSingleton instance created." << std::endl;
    }

    // 静态成员变量,用于保存唯一实例
    static EagerSingleton* instance;

public:
    // 获取单例实例的静态方法
    static EagerSingleton* getInstance() {
        return instance;
    }

    // 其他成员函数
    void doSomething() {
        std::cout << "EagerSingleton is doing something." << std::endl;
    }
};

// 初始化静态成员变量
EagerSingleton* EagerSingleton::instance = new EagerSingleton();

int main() {
    // 获取单例实例
    EagerSingleton* instance1 = EagerSingleton::getInstance();
    EagerSingleton* instance2 = EagerSingleton::getInstance();

    // 执行一些操作
    instance1->doSomething();
    instance2->doSomething();

    // 输出结果应该表明 instance1 和 instance2 是同一个实例
    return 0;
}

这两种单例模式各有优缺点,懒汉式节省了资源,但在首次使用时会稍微延迟,而饿汉式则在程序启动时就创建了实例,确保了实例的立即可用性。

26、写个程序,删除一个vector<int>中所有值为奇数的元素?

  1. 定义一个 std::vector<int> 类型的变量 numbers,其中包含了一组整数。
  2. 使用 std::remove_if 算法和 Lambda 表达式来删除 numbers 中所有的奇数元素:
    • std::remove_if 算法接受三个参数:容器的起始迭代器、容器的结束迭代器和一个谓词函数(predicate function)。
    • 谓词函数是一个返回 bool 类型的函数,用于判断容器中的元素是否满足某个条件。在这个例子中,Lambda 表达式 [](int i) { return i % 2 != 0; } 是一个用于判断奇数的谓词函数,它会返回 true 如果一个数是奇数,这样 std::remove_if 就能找到所有奇数,并将它们移动到容器末尾。
  3. 使用 erase 方法删除 numbers 容器末尾的所有奇数元素:
    • erase 方法接受两个迭代器参数,表示要删除的范围。在这个例子中,我们使用 std::remove_if 的返回值作为 erase 方法的参数,它指向了容器中最后一个不满足条件的元素之后的位置。
    • erase 方法会删除从指定位置到容器末尾的所有元素,从而实现了删除所有奇数元素的操作。
  4. 最后,使用循环遍历 numbers 容器,并输出删除后的结果。

代码:

c++
#include <iostream>
#include <vector>
#include <algorithm>

int main() {
    std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9};
    
    // 使用 erase-remove 习惯用法删除奇数元素
    numbers.erase(std::remove_if(numbers.begin(), numbers.end(), [](int i) { return i % 2 != 0; }), numbers.end());

    // 打印删除后的结果
    for (int num : numbers) {
        std::cout << num << " ";
    }
    std::cout << std::endl;

    return 0;
}

27、手撕代码,反转链表?