日志项目1

news2024/11/25 3:06:41

文章目录

  • 【C++ - 基于多设计模式下的同步&异步日志系统】
    • 项目介绍 + 前置技术
      • 1. 项目介绍
      • 2. 开发环境
      • 3. 核心技术
      • 4. 环境搭建
      • 5. 日志系统介绍
        • 5.1 为什么需要日志系统
        • 5.2 日志系统技术实现
          • 5.2.1 同步写日志
          • 5.2.2 异步写日志
      • 6. 前置技术补充
        • 6.1 不定参函数
          • 6.1.1 不定参宏函数
          • 6.1.2 C风格不定参函数
          • 6.1.3 C++风格不定参函数
        • 6.2 设计模式
          • 6.2.1 六大原则
          • 6.2.2 单例模式
          • 6.2.3 工厂模式
          • 6.2.4 建造者模式
          • 6.2.5 代理模式

【C++ - 基于多设计模式下的同步&异步日志系统】

项目介绍 + 前置技术

1. 项目介绍

本项目主要实现一个日志系统, 其主要支持以下功能:

  • 支持多级别日志消息
  • 支持同步日志和异步日志
  • 支持可靠写入日志到控制台、文件以及滚动文件中
  • 支持多线程程序并发写日志
  • 支持扩展不同的日志落地目标地

2. 开发环境

  • CentOS 7
  • vscode/vim
  • g++/gdb
  • Makefile

3. 核心技术

  • 类层次设计(继承和多态的应用)
  • C++11(多线程、auto、智能指针、右值引用等)
  • 双缓冲区
  • 生产消费模型
  • 多线程
  • 设计模式(单例、工厂、代理、模板等)

4. 环境搭建

本项目不依赖其他任何第三方库, 只需要安装好CentOS/Ubuntu + vscode/vim环境即可开发。

5. 日志系统介绍

5.1 为什么需要日志系统
  • 生产环境的产品为了保证其稳定性及安全性是不允许开发人员附加调试器去排查问题, 可以借助日志系统来打印一些日志帮助开发人员解决问题

  • 上线客户端的产品出现bug无法复现并解决, 可以借助日志系统打印日志并上传到服务端帮助开发人员进行分析

  • 对于一些高频操作(如定时器、心跳包)在少量调试次数下可能无法触发我们想要的行为,通过断点的暂停方式,我们不得不重复操作几十次、上百次甚至更多,导致排查问题效率是非常低下, 可以借助打印日志的方式查问题

  • 在分布式、多线程/多进程代码中, 出现bug比较难以定位, 可以借助日志系统打印log帮助定位bug

  • 帮助首次接触项目代码的新开发人员理解代码的运行流程

5.2 日志系统技术实现

日志系统的技术实现主要包括三种类型:

  • 利用printf、std::cout等输出函数将日志信息打印到控制台

  • 对于大型商业化项目, 为了方便排查问题,我们一般会将日志输出到文件或者是数据库系统方便查询和分析日志, 主要分为同步日志和异步日志方式

    • 同步写日志
    • 异步写日志
5.2.1 同步写日志

同步日志是指当输出日志时,必须等待日志输出语句执行完毕后,才能执行后面的业务逻辑语句,日志输出语句与程序的业务逻辑语句将在同一个线程运行。每次调用一次打印日志API就对应一次系统调用write写日志文件

在这里插入图片描述

在高并发场景下,随着日志数量不断增加,同步日志系统容易产生系统瓶颈:

  • 一方面,大量的日志打印陷入等量的write系统调用,有一定系统开销
  • 另一方面,使得打印日志的进程附带了大量同步的磁盘IO,影响程序性能
5.2.2 异步写日志

异步日志是指在进行日志输出时,日志输出语句与业务逻辑语句并不是在同一个线程中运行,而是有专门的线程用于进行日志输出操作。业务线程只需要将日志放到一个内存缓冲区中不用等待即可继续执行后续业务逻辑(作为日志的生产者),而日志的落地操作交给单独的日志线程去完成(作为日志的消费者), 这是一个典型的生产-消费模型

在这里插入图片描述

这样做的好处是即使日志没有真的地完成输出也不会影响程序的主业务,可以提高程序的性能:

  • 主线程调用日志打印接口成为非阻塞操作
  • 同步的磁盘IO从主线程中剥离出来交给单独的线程完成

6. 前置技术补充

6.1 不定参函数

在初学C语言的时候,我们都用过printf函数进行打印。其中printf函数就是一个不定参函数,在函数内部可以根据格式化字符串中格式化字符分别获取不同的参数进行数据的格式化。

而这种不定参函数在实际的使用中也非常多见,在这里简单做一介绍:

6.1.1 不定参宏函数
#include<cstdio>

// __FILE__: 文件名  __LINE__: 行号
// ##: 一旦不定参数为空,取消前面的 ,
#define LOG(fmt,...) printf("[%s:%d] " fmt "\n", __FILE__, __LINE__, ##__VA_ARGS__) 

int main()
{
    LOG("%s-%s","hello","比特");
    LOG("比特");
}

在这里插入图片描述

6.1.2 C风格不定参函数
#include<cstdio>
#include<cstdarg>
#include<cstdlib>

// C语言中不定参函数的使用, 不定参数据的访问
void PrintNum(int count,...)
{
    va_list ap;
    va_start(ap,count); // 获取指定参数的起始地址, 这里是让ap指向count参数之后的第一个参数的起始地址
    for(int i=0;i<count;++i)
    {
        int num=va_arg(ap,int);
        printf("param[%d]:%d\n",i,num);
    }
    va_end(ap);  // 将ap指针置空
}

int main()
{
    PrintNum(3,1,2,4);
    PrintNum(4,1,2,3,88);
}

在这里插入图片描述

模拟实现printf

#include<cstdio>
#include<cstdarg>
#include<cstdlib>

void myprintf(const char*fmt,...)   // 模拟实现printf
{
    // int vasprintf(char **strp, const char *fmt, va_list ap);

    va_list ap;
    va_start(ap,fmt);
    char*res;
    int len=vasprintf(&res,fmt,ap);    // 通过ap指针根据fmt组织好数据后放入res中
    if(len!=-1)
    {
        printf(res);
        free(res);
    }
    va_end(ap);
}
int main()
{
    myprintf("%s-%d\n","比特", 666);
    myprintf("%s-%f\n","比特", 6.0);
    return 0;
}

在这里插入图片描述

6.1.3 C++风格不定参函数
// C++风格的不定参数使用

void xprintf()
{
    std::cout<<std::endl;
}
template<class T, class ...Args>
void xprintf(const T&val, Args &&...args)  // 万能引用 + 完美转发
{
    std::cout<<val<<" ";
    if((sizeof...(args)) > 0)
        xprintf(std::forward<Args>(args)...);
    else
        xprintf();
}

int main()
{
    xprintf();
    xprintf("比特");
    xprintf("比特", 666);
    return 0;
}

在这里插入图片描述

6.2 设计模式

设计模式是前辈们对代码开发经验的总结,是解决特定问题的一系列套路。它不是语法规定,而是一套用来提高代码可复用性、可维护性、可读性、稳健性以及安全性的解决方案。

6.2.1 六大原则
  • 单一职责原则(Single Responsibility Principle);

    • 类的职责应该单一,一个方法只做一件事。职责划分清晰了,每次改动到最小单位的方法或类。
    • 使用建议:两个完全不一样的功能不应该放一个类中,一个类中应该是一组相关性很高的函数、数据的封装
    • 用例:网络聊天:网络通信 & 聊天,应该分割成为网络通信类 & 聊天类
  • 开闭原则(Open Closed Principle);

    • 对扩展开放,对修改封闭
    • 使用建议:对软件实体的改动,最好用扩展而非修改的方式。
    • 用例:超时卖货:商品价格—不是修改商品的原来价格,而是新增促销价格。
  • 里氏替换原则(Liskov Substitution Principle);

    • 通俗点讲,就是只要父类能出现的地方,子类就可以出现,而且替换为子类也不会产生任何错误或异常。
    • 在继承类时,务必重写父类中所有的方法,尤其需要注意父类的protected方法,子类尽量不要暴露自己的public方法供外界调用。
    • 使用建议:子类必须完全实现父类的方法,孩子类可以有自己的个性。覆盖或实现父类的方法时,输入参数可以被放大,输出可以缩小
    • 用例:跑步运动员类-会跑步,子类长跑运动员-会跑步且擅长长跑, 子类短跑运动员-会跑步且擅长短跑
  • 依赖倒置原则(Dependence Inversion Principle)。

    • 高层模块不应该依赖低层模块,两者都应该依赖其抽象. 不可分割的原子逻辑就是低层模式,原子逻辑组装成的就是高层模块。
    • 模块间依赖通过抽象(接口)发生,具体类之间不直接依赖
    • 使用建议:每个类都尽量有抽象类,任何类都不应该从具体类派生。尽量不要重写基类的方法。结合里氏替换原则使用。
    • 用例:奔驰车司机类–只能开奔驰; 司机类 – 给什么车,就开什么车; 开车的人:司机–依赖于抽象
  • 迪米特法则(Law of Demeter),又叫“最少知道法则”;

    • 尽量减少对象之间的交互,从而减小类之间的耦合。一个对象应该对其他对象有最少的了解。对类的低耦合提出了明确的要求:
      • 只和直接的朋友交流, 朋友之间也是有距离的。自己的就是自己的(如果一个方法放在本类中,既不增加类间关系,也对本类不产生负面影响,那就放置在本类中)。
    • 用例:老师让班长点名–老师给班长一个名单,班长完成点名勾选,返回结果,而不是班长点名,老师勾选
  • 接口隔离原则(Interface Segregation Principle);

    • 客户端不应该依赖它不需要的接口,类间的依赖关系应该建立在最小的接口上
    • 使用建议:接口设计尽量精简单一,但是不要对外暴露没有实际意义的接口。
    • 用例:修改密码,不应该提供修改用户信息接口,而就是单一的最小修改密码接口,更不要暴露数据库操作

从整体上来理解六大设计原则,可以简要的概括为一句话,用抽象构建框架,用实现扩展细节,具体到每一条设计原则,则对应一条注意事项:

  • 单一职责原则告诉我们实现类要职责单一;
  • 里氏替换原则告诉我们不要破坏继承体系;
  • 依赖倒置原则告诉我们要面向接口编程;
  • 接口隔离原则告诉我们在设计接口的时候要精简单一;
  • 迪米特法则告诉我们要降低耦合;
  • 开闭原则是总纲,告诉我们要对扩展开放,对修改关闭
6.2.2 单例模式

一个类只能创建一个对象,即单例模式,该设计模式可以保证系统中该类只有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息,这种方式简化了在复杂环境下的配置管理。

单例模式有两种实现模式:饿汉模式和懒汉模式

  • 饿汉模式: 程序启动时就会创建一个唯一的实例对象。 因为单例对象已经确定, 所以比较适用于多线程环境中, 多线程获取单例对象不需要加锁, 可以有效的避免资源竞争, 提高性能。
// 饿汉模式: 一上来就实例化对象
// 版本1:
class Singleton
{
public:
    static Singleton& getInsance()
    {
        return _ins;
    }
private:
    Singleton(const Singleton&)=delete;

    Singleton& operator=(const Singleton&)=delete;

    Singleton()
    {
        std::cout<<"单例对象构造完成"<<std::endl;
    }

    ~Singleton()
    {}

    static Singleton _ins;
};
Singleton Singleton::_ins;

使用饿汉模式时,无论是否使用单例对象都会创建实例对象,即程序启动就会创建一个唯一实例对象。此时如果单例对象过大,程序启动速度会变慢

在这里插入图片描述

懒汉模式: 第一次使用要使用单例对象的时候创建实例对象。如果单例对象构造特别耗时或者耗费济源(加载插件、加载网络资源等), 可以选择懒汉模式, 在第一次使用的时候才创建对象。

  • 这里介绍的是《Effective C++》一书作者 Scott Meyers 提出的一种更加优雅简便的单例模式Meyers’ Singleton in C++。
  • C++11 Static local variables 特性以确保C++11起,静态变量将能够在满足 thread-safe 的前提下唯一地被构造和析构
// 懒汉模式: 懒加载---延迟加载的思想---一个对象在用的时候再进行实例化

class Singleton
{
public:
    static Singleton& getInstance()
    {
        static Singleton ins;    // C++11, 静态变量是线程安全的
        return ins;
    }
private:
    int _data;
public:
    int getData()
    {
        return _data;
    }
private:
    Singleton()
    {
        std::cout<<"单例对象构造完成"<<std::endl;
    }

    ~Singleton(){}

    Singleton(const Singleton&)=delete;
    Singleton& operator=(const Singleton&)=delete;
};

C++11之前,懒汉模式实现方法:双检查加锁

class Singleton
{
public:
    static Singleton* getInstance()
    {
        if( _ins == nullptr)      // 提高效率, 不需要每次获取单例都加锁解锁
        {
            _mtx.lock();
            if(_ins==nullptr)    // 保证线程安全和只new一次
            {
                _ins=new Singleton;
            }
            _mtx.unlock();
        }
        return _ins;
    }
private:
    Singleton()
    {
        std::cout<<"单例对象构造完成"<<std::endl;
    }

    ~Singleton(){}

    Singleton(const Singleton&)=delete;
    Singleton& operator=(const Singleton&)=delete;
private:
    static Singleton* _ins;
    static std::mutex _mtx;
};
Singleton* Singleton::_ins=nullptr;
std::mutex Singleton::_mtx;
6.2.3 工厂模式

工厂模式是一种创建型设计模式, 它提供了一种创建对象的最佳方式。在工厂模式中,我们创建对象时不会对上层暴露创建逻辑,而是通过使用一个共同结构来指向新创建的对象,以此实现创建-使用的分离

工厂模式可以分为: 简单工厂模式工厂方法模式抽象工厂模式

简单工厂模式: 简单工厂模式实现由一个工厂对象通过类型决定创建出来指定产品类的实例。假设有个工厂能生产出水果,当客户需要产品的时候明确告知工厂生产哪类水果,工厂需要接收用户提供的类别信息,当新增产品的时候,工厂内部去添加新产品的生产方式。

class Fruit
{
public:
    Fruit(){}
    virtual void name()=0;
};

class Apple:public Fruit
{
public:
    Apple(){}

    virtual void name()
    {
        std::cout<<"我是一个苹果"<<std::endl;
    }
};

class Banana:public Fruit
{
public:
    Banana(){}

    virtual void name()
    {
        std::cout<<"我是一个香蕉"<<std::endl;
    }
};
class FruitFactory1
{
public:
    static std::shared_ptr<Fruit> create(const std::string name)
    {
        if(name=="苹果")
            return std::make_shared<Apple>();
        else if(name=="香蕉")
            return std::make_shared<Banana>();
    }
};
int main()
{
    std::shared_ptr<Fruit> fruit=FruitFactory1::create("苹果");
    fruit->name();
    fruit=FruitFactory1::create("香蕉");
    fruit->name();
}

简单工厂模式: 通过参数控制可以生产任何产品

优点: 简单粗暴,直观易懂。使用同一个工厂生产同一等级结构下的任意产品

缺点:

  1. 所有东西生产在一起, 产品太多会导致代码量庞大
  2. 开闭原则遵循(开放扩展, 关闭修改)的不是很好, 要新增产品就必须修改工厂方法

工厂方法模式: 在简单工厂模式下新增多个工厂,多个产品,每个产品对应一个工厂。假设现在有A、B 两种产品,则开两个工厂,工厂 A 负责生产产品 A,工厂 B 负责生产产品 B,用户只知道产品的工厂名,而不知道具体的产品信息,工厂不需要再接收客户的产品类别,而只负责生产产品。

class Fruit
{
public:
    Fruit(){}
    virtual void name()=0;
};

class Apple:public Fruit
{
public:
    Apple(){}

    virtual void name()
    {
        std::cout<<"我是一个苹果"<<std::endl;
    }
};

class Banana:public Fruit
{
public:
    Banana(){}

    virtual void name()
    {
        std::cout<<"我是一个香蕉"<<std::endl;
    }
};

class FruitFactory2
{
public:
    virtual std::shared_ptr<Fruit> create()=0;
};

class AppleFactory:public FruitFactory2
{
public:
    virtual std::shared_ptr<Fruit> create() override
    {
        return std::make_shared<Apple>();
    }
};

class BananaFactory:public FruitFactory2
{
public:
    virtual std::shared_ptr<Fruit> create() override
    {
        return std::make_shared<Banana>();
    }
};

int main()
{
   std::shared_ptr<FruitFactory2> ff(new AppleFactory());
   std::shared_ptr<Fruit> fruit=ff->create();
   fruit->name();
   ff.reset(new BananaFactory());
   fruit=ff->create();
   fruit->name();

   return 0;
}

工厂方法: 定义一个创建对象的接口, 但是由子类来决定创建哪种对象, 使用多个工厂分别生产指定的固定产品

优点:

  1. 减轻了工厂类的负担, 将某类产品的生产交给指定的工厂来进行
  2. 开闭原则遵循较好, 添加新产品只需要新增产品的工厂即可, 不需要修改原先的工厂类

缺点: 对于某种可以形成一组产品族的情况处理较为复杂, 需要创建大量的工厂类

抽象工厂模式: 工厂方法模式通过引入工厂等级结构,解决了简单工厂模式中工厂类职责太重的问题,但由于工厂方法模式中的每个工厂只生产一类产品,可能会导致系统中存在大量的工厂类,势必会增加系统的开销。此时,我们可以考虑将一些相关的产品组成一个产品族(位于不同产品等级结构中功能相关联的产品组成的家族),由同一个工厂来统一生产,这就是抽象工厂模式的基本思想。

class Animal
{
public:
    Animal(){}

    virtual void name()=0;
};

class Cat:public Animal
{
public:

    virtual void name() 
    {
        std::cout<<"我是一只小猫"<<std::endl;
    }
};

class Dog:public Animal
{
public:
    virtual void name()
    {
        std::cout<<"我是一只小狗"<<std::endl;
    }
};

class Factory
{
public:
    virtual std::shared_ptr<Fruit> getFruit(const std::string&name)=0;
    virtual std::shared_ptr<Animal> getAnimal(const std::string&name)=0;
};

class FruitFactory:public Factory
{
public:
    virtual std::shared_ptr<Animal> getAnimal(const std::string&name) override
    {
        return std::shared_ptr<Animal>();
    }

    virtual std::shared_ptr<Fruit> getFruit(const std::string&name)  override
    {
        if(name=="苹果")
            return std::make_shared<Apple>();
        else if(name=="香蕉")
            return std::make_shared<Banana>();
    }
};

class AnimalFactory:public Factory
{
public:
    virtual std::shared_ptr<Fruit> getFruit(const std::string&name)  override
    {
        return std::shared_ptr<Fruit>();
    }

    virtual std::shared_ptr<Animal> getAnimal(const std::string&name) override
    {
        if(name=="小猫")
            return std::make_shared<Cat>();
        else if(name=="小狗")
            return std::make_shared<Dog>();
    }
};

class FactoryProducer
{
public:
    static std::shared_ptr<Factory> create(const std::string&name)
    {
        if(name=="水果")
            return std::make_shared<FruitFactory>();
        else if(name=="动物")
            return std::make_shared<AnimalFactory>();
    }
};
int main()
{
    std::shared_ptr<Factory> ff=FactoryProducer::create("水果");
    std::shared_ptr<Fruit> fruit=ff->getFruit("苹果");
    fruit->name();
    fruit=ff->getFruit("香蕉");
    fruit->name();

    std::shared_ptr<Factory> fa=FactoryProducer::create("动物");
    std::shared_ptr<Animal> animal=fa->getAnimal("小猫");
    animal->name();
    animal=fa->getAnimal("小狗");
    animal->name();
}

抽象工厂:围绕一个超级工厂创建其他工厂。每个生成的工厂按照工厂模式提供对象。
思想:将工厂抽象成两层,抽象工厂 & 具体简单工厂子类, 在工厂子类种生产不同类型的子产品

抽象工厂模式适用于生产多个工厂系列产品衍生的设计模式,增加新的产品等级结构复杂,需要对原有系统进行较大的修改,甚至需要修改抽象层代码,违背了“开闭原则”。

6.2.4 建造者模式

建造者模式是一种创建型设计模式, 使用多个简单的对象一步一步构建成一个复杂的对象,能够将一个复杂的对象的构建与它的表示分离,提供一种创建对象的最佳方式。主要用于解决对象的构建过于复杂的问题。

建造者模式主要基于四个核心类实现:

  • 抽象产品类:
  • 具体产品类:一个具体的产品对象类
  • 抽象Builder类:创建一个产品对象所需的各个部件的抽象接口
  • 具体产品的Builder类:实现抽象接口,构建各个部件
  • 指挥者Director类:统一组建过程,提供给调用者使用,通过指挥者来构造产品
#include<iostream>
#include<string>
#include<memory>

// 抽象电脑类
class Computer
{
public:
    Computer(){}

    void SetBoard(const std::string & board)
    {
        _board=board;
    }

    void SetDisplay(const std::string & display)
    {
        _display=display;
    }

    virtual void SetOS()=0;

    void ShowParmaters()
    {
        std::string param="Computer Paramaters:\n";
        param+="\tBoard: " + _board + "\n";
        param+="\tDisplay: " + _display + "\n";
        param+="\tOs: " + _os + "\n";
        std::cout<<param<<std::endl;
    }
protected:
    std::string _board;
    std::string _display;
    std::string _os;
};

// 具体产品类
class MacBook:public Computer
{
public:
    MacBook(){}

    virtual void SetOS()
    {
        _os="Mac Os X12";
    }
};

// 抽象建造者类
class Builder
{
public:
    virtual void buildBoard(const std::string & board)=0;
    virtual void buildDisplay(const std::string & display)=0;
    virtual void buildOS()=0;
    virtual std::shared_ptr<Computer> build()=0;
};

// 具体产品的建造者类: 实现抽象接口, 构建和组装各个部件
class MacBookBuilder: public Builder
{
public:
    MacBookBuilder()
        :_computer(new MacBook())
    {

    }

    virtual void buildBoard(const std::string & board)
    {
        _computer->SetBoard(board);
    }

    virtual void buildDisplay(const std::string & display)
    {
        _computer->SetDisplay(display);
    }

    virtual void buildOS()
    {
        _computer->SetOS();
    }

    std::shared_ptr<Computer> build()
    {
        return _computer;
    }
private:
    std::shared_ptr<Computer> _computer;
};

// 指挥者类,提供给调用者使用,通过指挥者来构造复杂产品
class Director
{
public:
    Director(Builder*builder)
        :_builder(builder)
    {

    }

    void construct(const std::string& board, const std::string& display)
    {
        _builder->buildBoard(board);
        _builder->buildDisplay(display);
        _builder->buildOS();
    }
private:
    std::shared_ptr<Builder> _builder;
};
int main()
{
    Builder*builder=new MacBookBuilder();
    std::unique_ptr<Director> director(new Director(builder));
    director->construct("华硕主板", "三星显示器");
    std::shared_ptr<Computer> computer=builder->build();
    computer->ShowParmaters();
    return 0;
}

在这里插入图片描述

6.2.5 代理模式

代理模式指代理控制对其他对象的访问, 也就是代理对象控制对原对象的引用。在某些情况下,一个对象不适合或者不能直接被引用访问,而代理对象可以在客户端和目标对象之间起到中介的作用。

代理模式的结构包括一个是真正的你要访问的对象(目标类)、一个是代理对象。目标对象与代理对象实现同一个接口,先访问代理类再通过代理类访问目标对象。代理模式分为静态代理、动态代理:

  • 静态代理指的是,在编译时就已经确定好了代理类和被代理类的关系。也就是说,在编译时就已经确定了代理类要代理的是哪个被代理类。
  • 动态代理指的是,在运行时才动态生成代理类,并将其与被代理类绑定。这意味着,在运行时才能确定代理类要代理的是哪个被代理类。

以租房为例,房东将房子租出去,但是要租房子出去,需要发布招租启示, 带人看房,负责维修,这些工作中有些操作并非房东能完成,因此房东为了图省事,将房子委托给中介进行租赁。 代理模式实现:

#include<iostream>

// 房东要把一个房子通过中介租出去理解代理模式
class RentHouse
{
public:
    virtual void rentHouse()=0;
};

// 房东类:将房子租出去
class Landlord: public RentHouse
{
public:
    virtual void rentHouse()
    {
        std::cout<<"将房子租出去"<<std::endl;
    }
};

// 中介代理类:对租房子进行功能加强,实现租房以外的其他功能
class Intermediary: public RentHouse
{
public:
    virtual void rentHouse()
    {
        std::cout<<"发布招租启示"<<std::endl;
        std::cout<<"带人看房"<<std::endl;
        _landlord.rentHouse();
        std::cout<<"负责租后维修"<<std::endl;
    }
private:
    Landlord _landlord;
};

int main()
{
    Intermediary intermediary;
    intermediary.rentHouse();
    return 0;
}

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1393879.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

7.6 MySQL基本函数的使用(❤❤❤)

7.6 MySQL基本函数的使用 1. 提要2. 数字函数3. 字符函数3.1 替换字符3.2 左填充字符及截取字符串 4. 日期函数4.1 日期函数4.2 表达式占位符4.3 日期偏移计算4.4 日期间隔 5. 条件函数5.1 IF语句5.2 case...when语句 1. 提要 2. 数字函数 3. 字符函数 3.1 替换字符 -- INSERT…

FPGA引脚选择(Select IO)--认知1

主要考虑功能角度&#xff08;速度&#xff0c;电平匹配&#xff0c;内部程序编写&#xff09;去找研究芯片内部资源 1. 关键字 HP I/O Banks, High performance The HP I/O banks are deisgned to meet the performance requirements of high-speed memory and other chip-to-…

软件测试|使用Python轻松裁剪视频

简介 裁剪视频是在视频编辑和处理中常见的任务之一&#xff0c;Python提供了多种库和工具&#xff0c;可以用来裁剪视频。在本文中&#xff0c;我们将详细讨论如何使用Python来裁剪视频&#xff0c;并提供示例代码。 步骤1&#xff1a;环境准备 首先&#xff0c;我们要安装必…

如何使用CureIAM自动清理GCP基础设施中的IAM账号权限

关于CureIAM CureIAM是一款针对GCP基础设施的账号权限安全检查与管理工具&#xff0c;该工具易于使用&#xff0c;是一个功能强大且易于使用的可靠高性能引擎。在该工具的帮助下&#xff0c;广大研究人员能够以自动化的形式在GCP云基础设施上实践最低权限原则。 CureIAM可以允…

如何在Linux运行RStudio Server并实现Web浏览器远程访问

&#x1f308;个人主页: Aileen_0v0 &#x1f525;热门专栏: 华为鸿蒙系统学习|计算机网络|数据结构与算法 ​&#x1f4ab;个人格言:“没有罗马,那就自己创造罗马~” 文章目录 前言1. 安装RStudio Server2. 本地访问3. Linux 安装cpolar4. 配置RStudio server公网访问地址5. …

自建服务器如何备案?

随着互联网的普及和发展&#xff0c;越来越多的人开始考虑自建服务器。然而&#xff0c;在中国大陆地区&#xff0c;自建服务器需要进行备案。本文将介绍自建服务器备案的流程、所需材料以及注意事项。 一、备案流程 确定备案地区 根据《中华人民共和国计算机信息网络国际联网…

khbc靶场小记(upload 666靶场)

尝试上传正常的png jpg gif php的格式的文件发现老是提示烦人的消息&#xff08;上传不成功&#xff09;&#xff1b; 通过抓包对MIME进行爆破没爆出来&#xff0c;当时可能用成小字典了&#xff1b; 猜测可能是把后缀名和MIME绑定检测了&#xff1b; 反正也没思路&#xff0c;…

归并排序(C语言)

目录 1.归并排序图解 2.归并排序&#xff08;递归版&#xff09; 3.归并排序&#xff08;非递归版&#xff09; 1.归并排序图解 归并排序的核心思想是让左右两边有序的部分进行合并比较排序&#xff0c;具体什么意思呢&#xff1f;分两点&#xff1a; 1.分&#xff1a;左右两边…

SwitchyOmega插件管理海外動態IP代理設置教程

SwitchyOmega插件很好解決了管理多個代理並在它們之間切換的問題&#xff0c;通過本文來全面瞭解SwitchyOmega&#xff0c;比如SwitchyOmega插件的用途、它的主要功能和應用、怎麼下載和使用&#xff0c;如何管理海外動態IP代理。 SwitchyOmega插件有什麼用途&#xff1f; Swit…

扫描电子显微镜在材料失效分析中的主要作用

扫描电子显微镜&#xff08;Scanning Electron Microscope&#xff0c;简称SEM&#xff09;是一种高分辨率的显微镜&#xff0c;能够在纳米级别上观察样品的表面形貌和微观结构。在材料失效分析中&#xff0c;SEM起着至关重要的作用&#xff0c;为深入理解材料的性质和失效机制…

excel统计分析——Scheffe法多重比较

参考资料&#xff1a;生物统计学 Scheffe法&#xff08;雪费法&#xff09;多重比较和LSD法一致&#xff0c;但通过k-1作为F分布的第一自由度对临界值进行调整&#xff1a; 其中&#xff0c;k为处理水平数&#xff0c;df为误差自由度&#xff1b;为 excel操作步骤如下&#xf…

ADSelfService Plus 推出离线多因素身份验证以提升远程工作安全性

采用先进验证方法&#xff0c;确保在任何时间、地点或连接问题下对业务数据的合法访问即使远程用户未连接到身份验证服务器或互联网&#xff0c;也可通过MFA安全认证。 MFA 得克萨斯州德尔瓦雷 — 2023年5月3日 — Zoho Corporation 旗下的企业IT管理部门ManageEngine今日宣布…

rust让你的python飞起来!

Note: 本文作为入门教程&#xff0c;抛砖引玉&#xff0c;帮你初步了解如何使用rust为python写扩展模块&#xff0c;涉及从头到尾的详细步骤&#xff0c;基于此&#xff0c;剩下的只有深入rust&#xff0c;才能做得更好。 众所周知&#xff0c;python性能比较差&#xff0c;尤其…

自动驾驶轨迹规划之碰撞检测(三)

欢迎大家关注我的B站&#xff1a; 偷吃薯片的Zheng同学的个人空间-偷吃薯片的Zheng同学个人主页-哔哩哔哩视频 (bilibili.com) 目录 1.基于圆覆盖 2.BVH 3.MATLAB自动驾驶工具箱 4 ROS内置的模型 自动驾驶轨迹规划之碰撞检测&#xff08;一&#xff09;-CSDN博客 自动驾…

性价比高的宠物空气净化器有哪些?五款猫用空气净化器测评推荐!

作为一位经验丰富的铲屎官&#xff0c;我深切理解养猫后家里到处都是猫毛和异味的困扰。养猫后&#xff0c;家里的空气质量往往变得不佳&#xff0c;猫毛和皮屑漫天飞舞。而如今&#xff0c;室内空气质量普遍较差&#xff0c;受到雾霾、螨虫和甲醛等污染。长期处于低质量的室内…

postman测试文件上传接口设置说明

Postman介绍及下载链接地址 Download Postman | Get Started for Free 打开postman 选择POST方法&#xff0c;然后设置goform 设置Header参数 设置Body参数&#xff0c;选择数据form-data 添加文件&#xff0c; 选择为文件属性 添加需要上传的文件

【备战蓝桥杯】吃奶酪问题 / 超硬核,文附template拓展知识!

蓝桥杯备赛 | 洛谷做题打卡day9 文章目录 蓝桥杯备赛 | 洛谷做题打卡day9再来了解一下状压dp**简介(Introduction)****描述(Description)** - 吃奶酪题目描述输入格式输出格式样例 #1样例输入 #1样例输出 #1 提示数据规模与约定提示 * template拓展知识我的一些话 【引入】今天…

sftp配置互信:sftp免密登录

一、安装ssh 使用yum命令安装或者去ssh官网下载进行安装 yum install openssh.x86_64二、设置免密 在asd(192.168.47.130)通过ssh或者sftp登录demo(192.168.47.140) shh免密登录有俩种方式 1、生成ssh公钥以及私钥 俩台服务器都不要生成 ssh-keygen在asd执行 在demo执行 …

探索直流电源模块的应用领域

探索直流电源模块的应用领域 直流电源模块广泛应用于许多领域&#xff0c;包括电子设备、通信、工业自动化、航空航天等。以下是一些常见的应用领域&#xff1a; 1. 电子设备&#xff1a;直流电源模块用于给各种电子设备供电&#xff0c;如计算机、手机、平板电脑、摄像机等。…

从零实现一套低代码(保姆级教程)【后端服务】 --- 【18】实现页面接口对应的前端

摘要 在上一篇中&#xff0c;我们已经把和页面相关的接口完成的差不多了。从创建页面&#xff0c;更新页面等等&#xff1a; 有了接口之后&#xff0c;我们就可以构建前端页面了。那这部分前端内容我们应该写在哪里呢&#xff1f; 有两种方式&#xff1a; 直接写在我们的Xin…