C++面试之线程池、智能指针、设计模式

智能指针引用链接:
https://blog.csdn.net/weixin_41937380/article/details/129151613
https://blog.csdn.net/zhizhengguan/article/details/112302192

一、线程池
1、线程池实现步骤
这里就讲讲正常的一个线程池的实现步骤。

1.1 定义任务类:首先需要定义一个任务类,用于封装需要在线程池中执行的任务。任务类至少应该包含一个执行任务的方法,可以是一个函数指针或者是一个函数对象。

class Task {
public:
    virtual void execute() = 0;
};

1.2 定义线程池类:接下来定义线程池类,其中包含了线程池的管理逻辑,如线程的创建、销毁、任务的添加等。线程池类需要包含一个线程池容器,用于存放线程对象。

#include <vector>
#include <thread>
#include <queue>
#include <mutex>
#include <condition_variable>

class ThreadPool {
public:
    ThreadPool(size_t numThreads);
    ~ThreadPool();

    void addTask(Task* task);

private:
    std::vector<std::thread> workers;  // 线程池中的线程
    std::queue<Task*> tasks;            // 任务队列
    std::mutex queueMutex;              // 保护任务队列的互斥量
    std::condition_variable condition;  // 用于线程间通信的条件变量
    bool stop;                          // 标志线程池是否停止的标志位
};

1.3 实现线程池类的构造函数和析构函数:在构造函数中创建指定数量的线程,并启动这些线程;在析构函数中停止线程池中的所有线程。

ThreadPool::ThreadPool(size_t numThreads) : stop(false) {
    for (size_t i = 0; i < numThreads; ++i) {
        workers.emplace_back([this] {
            while (true) {
                Task* task = nullptr;
                {
                    std::unique_lock<std::mutex> lock(queueMutex);
                    condition.wait(lock, [this] { return stop || !tasks.empty(); });
                    if (stop && tasks.empty()) return;
                    task = tasks.front();
                    tasks.pop();
                }
                task->execute();
                delete task;
            }
        });
    }
}

ThreadPool::~ThreadPool() {
    {
        std::unique_lock<std::mutex> lock(queueMutex);
        stop = true;
    }
    condition.notify_all();
    for (std::thread& worker : workers) {
        worker.join();
    }
}

1.4 实现添加任务的方法:在线程池类中添加一个方法用于向任务队列中添加任务。

void ThreadPool::addTask(Task* task) {
    {
        std::unique_lock<std::mutex> lock(queueMutex);
        tasks.push(task);
    }
    // notify_one():唤醒等待队列中的第一个线程,不存在锁争用,可立即获得锁,其他线程继续等待;
    // notify_all():唤醒所有等待队列中阻塞的线程,存在锁争用,只有一个线程能够获得锁。其余锁会继续尝试获得锁(类似于轮询),而不会再次阻塞。
    condition.notify_one();
}

1.5 使用线程池:最后,在主程序中使用定义好的线程池类来执行任务。

int main() {
     ThreadPool pool(4);  // 创建一个包含4个线程的线程池

     // 添加任务到线程池
     for (int i = 0; i < 8; ++i) {
         pool.addTask(new YourTask());  // YourTask 是需要执行的任务类
     }

     // ...

     return 0;
 }

2、存放线程执行任务的结构体或者类型是什么?(std::function)
在一个线程池中,通常需要一个结构体或者类型来表示线程执行的任务。这个结构体或者类型需要包含执行任务的信息,比如任务的具体内容、状态等。在C++中,可以使用函数指针、std::function 或者自定义的函数对象来表示任务。
举例:

#include <functional>

// 使用 std::function 来表示任务
struct Task {
    std::function<void()> function;

    // 构造函数
    Task(const std::function<void()>& f) : function(f) {}

    // 执行任务的方法
    void execute() {
        if (function) {
            function();
        }
    }
};

3、线程 A 如何向线程 B 发起异步请求并获取到处理结果、接口是什么?
在 C++ 中,线程 A 可以向线程 B 发起异步请求并获取处理结果的一种常见方式是使用 std::future 和 std::promise。这种方法允许线程 A 发起异步任务,并在需要时等待线程 B 完成任务并获取结果。

使用 std::future 和 std::promise 实现线程 A 向线程 B 发起异步请求并获取处理结果的简单示例:

#include <iostream>
#include <future>
#include <thread>

void asyncTask(std::promise<int>& promiseObj) {
    // 模拟一个耗时的异步任务
    std::this_thread::sleep_for(std::chrono::seconds(2));

    // 设置 promise 的值,表示任务完成
    promiseObj.set_value(42);
}

int main() {
    // 创建一个 promise 对象和一个 future 对象
    std::promise<int> promiseObj;
    std::future<int> futureObj = promiseObj.get_future();

    // 在另一个线程中执行异步任务
    // std::ref 它的作用是将一个对象转换成一个引用包装器(reference wrapper),以便在函数模板中使用
    std::thread worker(asyncTask, std::ref(promiseObj));
    worker.detach();  // 让 worker 线程在后台运行

    // 在主线程中等待异步任务的结果
    std::cout << "Waiting for result..." << std::endl;
    int result = futureObj.get();  // 阻塞等待任务完成并获取结果
    std::cout << "Result: " << result << std::endl;

    return 0;
}

示例中,线程 A(主线程)创建了一个 std::promise 对象 promiseObj 和一个与之关联的 std::future 对象 futureObj。然后,线程 A 启动了一个新的线程(线程 B),并将 promiseObj 作为参数传递给异步任务函数 asyncTask。异步任务函数中通过 promiseObj.set_value() 设置了异步任务的结果。

在主线程中,通过 futureObj.get() 方法阻塞等待异步任务的完成,并获取到任务的结果。这样,线程 A 就能够向线程 B 发起异步请求并获取处理结果了。

二:介绍一下智能指针及特性
2.1 unique_ptr
1)unique_ptr 用于管理独占所有权的对象,即同一时间只能有一个 unique_ptr 指向一个对象。
2)当 unique_ptr 被销毁时,它所指向的对象也会被销毁,这样可以确保资源的正确释放
3)unique_ptr 不支持拷贝和赋值操作,但可以通过 std::move 来转移所有权。
4)适合用于管理局部对象或者作为容器元素的指针。

2.2 shared_ptr
1)shared_ptr 用于管理共享所有权的对象,即多个 shared_ptr 可以指向同一个对象
2)内部通过引用计数来管理资源的生命周期,当最后一个指向对象的 shared_ptr 被销毁时,对象会被释放。
3)支持拷贝和赋值操作,内部使用引用计数来追踪对象的引用情况。
4)适合用于多个对象共享同一资源的情况,比如多个对象共享同一个动态分配的对象。

2.3 std::weak_ptr
1)weak_ptr 是 shared_ptr 的一种辅助工具,用于解决 shared_ptr 的循环引用问题。
2)weak_ptr 本身不增加引用计数,它只是观察 shared_ptr 的引用计数,并提供了一种机制来检测对象是否已经被释放。
3)可以通过 weak_ptr 的 lock 方法获取一个指向对象的 shared_ptr,如果对象已经被释放,则返回一个空的 shared_ptr。
4)适合用于解决 shared_ptr 循环引用导致的内存泄漏问题。
5)如何判断指针是否被销毁?
expired:
判断强引用计数是否为0,若为0(true),则被观测的指针已被销毁。

2.4 unique_ptr和shared_ptr的区别
(1)
unique_ptr代表的是专属所有权,不支持复制和赋值。但是可以移动
shared_ptr 代表的是共享所有权,shared_ptr 是支持复制的和赋值以及移动的
(2)资源消耗上
unique_ptr 在默认情况下和裸指针的大小是一样的;所以 内存上没有任何的额外消耗,性能是最优的;
shared_ptr 的内存占用是裸指针的两倍。因为除了要管理一个裸指针外,还要维护一个引用计数,因此内存占用更高。
2.5 unique_ptr和shared_ptr如何选择
1)尽可能使用unique_ptr
2)shared_ptr应该仅在绝对需要共享时使用
3)避免使用任何其他指针,weak_ptr / raw 等。
原因:
unique_ptr具有与C++原始指针‘几乎’相同的效率,包括大小分配、引用/取消引用;
unique_ptr明确了对象的所有权和生存时间;
shared_ptr的大小是原始指针的两倍,堆栈更大;
最重要的是,shared_ptr需要维护引用计数,并且该操作必须是原子的(即线程安全的),这与unique_ptr相比增加了恒定的开销。
2.6、shared_ptr是不是线程安全
不是
引用计数的增减是原子操作没问题,但是shared_pytr的读写本身不只包括引用计数操作,还包括资源所有权的操作(shared_ptr支持复制和赋值(读和写)操作),这两个操作合起来不是原子的,如果要求线程安全必须加锁。增加了恒定的开销。
详细解释:
(1) 同一个shared_ptr被多个线程“读”是安全的.
   这个应该很容易理解吧,因为是进行读操作,没有修改,肯定是线程安全的了。
(2)同一个shared_ptr被多个线程“写”是不安全的
   假如说有两个线程同时访问一个shared_ptr对象,一个进行释放(reset),另一个读取裸指针的值,那么最后的结果就不确定了,很可能会因为访问野指针的问题导致crash。
(3)共享引用计数的不同的shared_ptr被多个线程”写“ 是安全的;
   c++ 11官方文档有说,share_ptr的计数操作具有原子性。也就是说多个线程通过多个shaped_ptr(虽然这多个shared_ptr指向的是同一个对象)进行操作,是线程安全的。

三、了解哪些设计模式(单例、工厂、建造者)
3.1 单例模式
主要用于确保一个类只有一个实例,并提供一个全局访问点来访问该实例。
1、饿汉式单例模式(线程不安全):

1)在类的静态成员变量中直接创建实例,并在类的静态方法中返回该实例。
2)这种方式在程序启动时就会创建单例对象,无论是否需要使用,可能会导致资源浪费。
3)不适合在多线程环境下使用,因为没有进行线程安全的处理。

class Singleton {
public:
    static Singleton& getInstance() {
        static Singleton instance;
        return instance;
    }
    // 防止拷贝构造和赋值操作
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
private:
    Singleton() {}  // 私有化构造函数,禁止外部创建实例
};

2、懒汉式单例模式(线程安全):
1)使用加锁的方式保证在多线程环境下也能正常工作,但会影响性能。
2)在 getInstance 方法中加锁,避免了多个线程同时创建实例的问题。

#include <mutex>
class Singleton {
public:
    static Singleton& getInstance() {
        std::lock_guard<std::mutex> lock(mutex);
        static Singleton instance;
        return instance;
    }
    // 防止拷贝构造和赋值操作
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
private:
    Singleton() {}  // 私有化构造函数,禁止外部创建实例
    static std::mutex mutex;
};
std::mutex Singleton::mutex;

3.2 工厂模式
主要用于封装对象的创建过程。它通过定义一个工厂类来负责创建产品对象,从而将客户端代码与具体产品的实现进行解耦。

1、简单工厂模式(Simple Factory Pattern):
1)简单工厂模式通过一个工厂类来创建产品对象,客户端只需要与工厂类交互,而不需要直接与具体产品类交互。
2)客户端通过调用工厂类的静态方法来创建产品对象,工厂类根据参数的不同来创建不同的产品对象。

// 产品基类
class Product {
public:
    virtual void operation() = 0;
    virtual ~Product() {}
};
// 具体产品类
class ConcreteProduct : public Product {
public:
    void operation() override {
        // 具体产品的操作
    }
};
// 简单工厂类
class SimpleFactory {
public:
    static Product* createProduct() {
        return new ConcreteProduct();
    }
};

2、工厂方法模式(Factory Method Pattern):

1)工厂方法模式通过定义一个创建产品的接口,每个具体产品都有对应的工厂类负责创建。
2)客户端通过调用具体工厂类的方法来创建产品对象,不同的工厂类创建不同的产品对象。

// 产品基类
class Product {
public:
    virtual void operation() = 0;
    virtual ~Product() {}
};
// 具体产品类
class ConcreteProduct : public Product {
public:
    void operation() override {
        // 具体产品的操作
    }
};
// 工厂接口
class Factory {
public:
    virtual Product* createProduct() = 0;
    virtual ~Factory() {}
};
// 具体工厂类
class ConcreteFactory : public Factory {
public:
    Product* createProduct() override {
        return new ConcreteProduct();
    }
};

3、抽象工厂模式(Abstract Factory Pattern):

抽象工厂模式通过定义一组工厂接口来创建一组相关或依赖对象的产品族。
客户端通过选择具体的工厂来获取相应的产品族,不同的工厂可以创建不同的产品族。

// 抽象产品A
class AbstractProductA {
public:
    virtual void operationA() = 0;
    virtual ~AbstractProductA() {}
};

// 具体产品A1
class ConcreteProductA1 : public AbstractProductA {
public:
    void operationA() override {
        // 具体产品A1的操作
    }
};

// 抽象产品B
class AbstractProductB {
public:
    virtual void operationB() = 0;
    virtual ~AbstractProductB() {}
};

// 具体产品B1
class ConcreteProductB1 : public AbstractProductB {
public:
    void operationB() override {
        // 具体产品B1的操作
    }
};

// 抽象工厂
class AbstractFactory {
public:
    virtual AbstractProductA* createProductA() = 0;
    virtual AbstractProductB* createProductB() = 0;
    virtual ~AbstractFactory() {}
};

// 具体工厂1
class ConcreteFactory1 : public AbstractFactory {
public:
    AbstractProductA* createProductA() override {
        return new ConcreteProductA1();
    }

    AbstractProductB* createProductB() override {
        return new ConcreteProductB1();
    }
};

4、建造者模式
建造者模式是一种创建型设计模式,它的主要目的是将一个复杂对象的构建与其表示分离,使得同样的构建过程可以创建不同的表示。

建造者模式通常涉及以下几个角色:

1)Director(指挥者):负责使用建造者对象构建产品对象的算法。
2)Builder(建造者):定义创建产品各个部件的接口,以及构建产品的方法。
3)ConcreteBuilder(具体建造者):实现 Builder 接口,负责具体的产品构建工作。
4)Product(产品):表示被构建的复杂对象。
给个代码示例来加深理解:

#include <iostream>
#include <string>

// 产品(Pizza)
class Pizza {
public:
    void setDough(const std::string& dough) {
        dough_ = dough;
    }

    void setSauce(const std::string& sauce) {
        sauce_ = sauce;
    }

    void setTopping(const std::string& topping) {
        topping_ = topping;
    }

    void showPizza() {
        std::cout << "Pizza with " << dough_ << " dough, " << sauce_ << " sauce and " << topping_ << " topping." << std::endl;
    }

private:
    std::string dough_;   // 面团
    std::string sauce_;   // 酱料
    std::string topping_; // 配料
};

// 建造者(Builder)接口
class PizzaBuilder {
public:
    virtual void buildDough() = 0;     // 建造面团
    virtual void buildSauce() = 0;     // 建造酱料
    virtual void buildTopping() = 0;   // 建造配料
    virtual Pizza* getPizza() = 0;     // 获取Pizza对象
    virtual ~PizzaBuilder() {}
};

// 具体建造者(ConcreteBuilder)
class HawaiianPizzaBuilder : public PizzaBuilder {
public:
    void buildDough() override {
        pizza_->setDough("cross");      // 设置交叉面团
    }

    void buildSauce() override {
        pizza_->setSauce("mild");       // 设置温和酱料
    }

    void buildTopping() override {
        pizza_->setTopping("ham+pineapple"); // 设置火腿+菠萝配料
    }

    Pizza* getPizza() override {
        return pizza_;  // 返回构建好的Pizza对象
    }

    HawaiianPizzaBuilder() {
        pizza_ = new Pizza(); // 在构造函数中创建Pizza对象
    }

    ~HawaiianPizzaBuilder() {
        delete pizza_;  // 析构函数中释放内存
    }

private:
    Pizza* pizza_;  // Pizza对象指针
};

// 指挥者(Director)
class Waiter {
public:
    void setPizzaBuilder(PizzaBuilder* builder) {
        pizzaBuilder_ = builder;    // 设置建造者
    }

    Pizza* getPizza() {
        return pizzaBuilder_->getPizza();   // 获取建造好的Pizza对象
    }

    void constructPizza() {
        pizzaBuilder_->buildDough();       // 建造面团
        pizzaBuilder_->buildSauce();       // 建造酱料
        pizzaBuilder_->buildTopping();     // 建造配料
    }

private:
    PizzaBuilder* pizzaBuilder_;    // 建造者对象指针
};

int main() {
    Waiter waiter;  // 创建指挥者对象
    HawaiianPizzaBuilder hawaiianPizzaBuilder; // 创建具体建造者对象

    waiter.setPizzaBuilder(&hawaiianPizzaBuilder); // 设置具体建造者对象
    waiter.constructPizza();    // 指挥者构建Pizza对象

    Pizza* pizza = waiter.getPizza(); // 获取建造好的Pizza对象
    pizza->showPizza(); // 展示Pizza对象信息

    delete pizza;   // 释放Pizza对象内存

    return 0;
}

5、观察者模式
观察者模式是一种行为设计模式,用于定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖它的对象都会得到通知并自动更新。

观察者模式通常包含以下几个角色:

1)Subject(目标):目标是被观察的对象,它包含了一组观察者对象,并提供了添加、删除和通知观察者的方法。
2)Observer(观察者):观察者是依赖于目标的对象,当目标状态发生改变时,观察者会得到通知并进行相应的更新操作。
3)ConcreteSubject(具体目标):具体目标是实现了目标接口的具体对象,它维护了一组观察者对象,并在状态发生改变时通知观察者。
4)ConcreteObserver(具体观察者):具体观察者是实现了观察者接口的具体对象,它注册到具体目标中,并在目标状态发生改变时接收到通知并进行更新操作。
再来个例子:

#include <iostream>
#include <vector>

// 观察者接口
class Observer {
public:
    virtual void update() = 0;
    virtual ~Observer() {}
};

// 目标接口
class Subject {
public:
    virtual void attach(Observer* observer) = 0;
    virtual void detach(Observer* observer) = 0;
    virtual void notify() = 0;
    virtual ~Subject() {}
};

// 具体观察者
class ConcreteObserver : public Observer {
public:
    void update() override {
        std::cout << "ConcreteObserver: Received update from subject." << std::endl;
    }
};

// 具体目标
class ConcreteSubject : public Subject {
public:
    void attach(Observer* observer) override {
        observers_.push_back(observer);
    }

    void detach(Observer* observer) override {
        // 在实际应用中可能需要实现查找并删除的逻辑
        observers_.erase(std::remove(observers_.begin(), observers_.end(), observer), observers_.end());
    }

    void notify() override {
        for (auto observer : observers_) {
            observer->update();
        }
    }

    void setState(int state) {
        state_ = state;
        notify();
    }

private:
    std::vector<Observer*> observers_;
    int state_;
};

int main() {
    ConcreteSubject subject;
    ConcreteObserver observer1, observer2;

    // 将观察者注册到目标中
    subject.attach(&observer1);
    subject.attach(&observer2);

    // 改变目标的状态,并通知观察者
    subject.setState(1);

    // 将观察者从目标中移除
    subject.detach(&observer2);

    // 再次改变目标的状态,并通知观察者
    subject.setState(2);

    return 0;
}
在引用中,给出了一个程序的代码片段,这段代码涉及到了内存方面的问题。在这段代码中,函数`div()`用于进行两个整数的除法运算,如果除数为0,则会抛出`invalid_argument`异常。在`Func()`函数中,首先通过`new`关键字分别创建了两个`int`类型的指针变量`p1`和`p2`,然后调用了`div()`函数进行除法运算,并打印结果。最后,通过`delete`关键字释放了指针变量所指向的内存空间。 现在我们来分析一下这段程序中可能存在的内存方面的问题: 1. 如果在创建`p1`的语句后发生异常,那么`p1`指向的内存空间将无法被正确释放,造成内存泄漏。 2. 如果在创建`p2`的语句后发生异常,同样会导致`p2`指向的内存空间无法被正确释放,造成内存泄漏。 3. 如果在调用`div()`函数时发生异常,那么由于`p1`和`p2`指向的内存空间没有被正确释放,同样会造成内存泄漏。 为了解决这些内存问题,可以使用智能指针来管理动态分配的内存。智能指针可以自动管理内存的释放,避免内存泄漏的问题。在C++中,可以使用`std::shared_ptr`来管理动态分配的内存。它会在没有引用指向这块内存时自动释放它。 现在我们来回答相关问题: 相关问题: 1. 如果在创建`p1`的语句后发生异常,该如何处理? 在创建`p1`的语句后,可以将其包装在一个`std::shared_ptr`中,这样即使发生异常,智能指针也会自动释放其所指向的内存空间。 2. 如果在创建`p2`的语句后发生异常,该如何处理? 同样地,可以将`p2`包装在一个`std::shared_ptr`中,这样即使发生异常,智能指针也会自动释放其所指向的内存空间。 3. 如果在调用`div()`函数时发生异常,该如何处理? 可以使用`std::shared_ptr`来管理`p1`和`p2`,这样即使在调用`div()`函数时发生异常,智能指针也会自动释放`p1`和`p2`所指向的内存空间。 4. 如果多个线程同时进行拷贝操作,会出现什么问题? 在引用中,如果多个线程同时进行拷贝操作,可能会出现数据不一致的问题。这是因为在多线程环境下,共享指针的引用计数可能会出现竞争条件,导致引用计数不正确,从而无法正确地管理内存。 5. 如何使用智能指针来管理使用`new[]`创建的对象? 在引用中,如果使用`new[]`创建了对象,可以使用`std::shared_ptr`来管理这些对象。需要注意的是,`std::shared_ptr`默认使用`delete`来释放内存,而不是`delete[]`。所以,需要自定义删除器来使用`delete[]`来释放数组对象。 以上是关于C++智能指针面试题的回答。如果您还有其他
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ghx3110

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值