日志系统第二弹:设计模式介绍,C和C++不定参函数的介绍

日志系统第二弹:设计模式介绍

设计模式就是一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总结,是很多大佬在编程过程中总结下来的经验,类似于"武林秘籍"

我们的项目当中需要用到几个常见的设计模式:

  1. 单例模式
  2. 工厂模式
  3. 建造者模式
  4. 代理模式

其中,单例模式广为人知,我们就不赘述了

我们这篇博客要介绍工厂模式、建造者模式和代理模式

但是,在介绍设计模式之前,我们要先介绍程序设计的六大原则

一、六大原则

1、单一职责原则

定义:一个类应该只负责一项职责

目的:使责任划分清晰明了,便于责任归咎和整改

实现:当发现一个类承担了多项职责时,应该考虑将其分为多个类,每个类只负责一项职责

2.开闭原则

定义:软件实体(类、模块、函数等)对扩展开放,对修改关闭

即:软件实体在扩展新功能时不应该修改原有代码

目的:提高代码的灵活性和可维护性,降低因修改代码而引入的风险

实现:通过多态,实现在不修改原有代码的基础上扩展新的类

3.里氏替换原则

定义:子类对象能够安全地替换父类对象被调用,且不会影响程序的正确性

目的:要求子类继承父类时,遵循父类的规定,提高代码的稳定性和可维护性

4.依赖倒置原则

定义:高层模块不应该依赖底层模块,两者都应该依赖其抽象;抽象不应该依赖具体,而具体应该依赖抽象

实现:每个类都应该派生于抽象类,而不应该派生于具体类

5.接口隔离原则

定义:类之间的依赖的关系应该建立在最少的接口上,不提供没有必要的接口

应用:在定义接口时,应该根据客户端具体需求来定义接口,不提供没有必要的接口

6.迪米特法则(最少知道原则)

定义:一个对象应该对其他对象保持最少程度了解,即尽量减少对象之间的交互和依赖关系

目的:尽量减少对象和对象之间的交互,从而减少类之间的耦合

二、工厂模式

1.介绍

工厂模式是一种创建型设计模式,提供一种创建对象的最佳方式。

在工厂模式当中,我们创建对象时,不会对上层暴露创建逻辑,而是通过一个共同结构来指向新创建的对象,以此实现创建和使用的分离

2.为何要有工厂模式

很多时候,我们要用一个对象,不过我们需要先构造该对象。

这么做没有什么毛病,但是一旦对象修改了导致我们构造的地方也需要变动,那就需要该很多地方,非常不优雅

因此,大佬们决定在中间加一层,就形成了简单工厂模式:

这样的话具体细节被工厂类屏蔽了,改动代码时只需要改动工厂类即可

3.简单工厂模式

1.代码

Talk is cheap,Show me the code

cpp 复制代码
class Fruit
{
public:
    // 父类指针指向new出来的子类对象,父类的析构函数必须要加virtual
    // 否则delete时就会调用父类的析构函数,极大可能会导致 内存泄漏或其他潜在风险
    virtual ~Fruit() {}
    virtual void name() = 0;
};

class Apple : public Fruit
{
public:
    virtual void name()
    {
        cout << "我是苹果\n";
    }
};

class Banana : public Fruit
{
public:
    virtual void name()
    {
        cout << "我是香蕉\n";
    }
};

enum FruitType
{
    APPLE,
    BANANA
};

namespace ns_easy
{
    class FruitFactory
    {
    public:
        static shared_ptr<Fruit> create(FruitType type)
        {
            if (type == APPLE)
                return make_shared<Apple>();
            else if (type == BANANA)
                return make_shared<Banana>();
            else
                return shared_ptr<Fruit>(); // 空的智能指针
        }
    };
}

int main()
{
    auto sp = ns_easy::FruitFactory::create(APPLE);
    sp->name();
    sp = ns_easy::FruitFactory::create(BANANA);
    sp->name();
    return 0;
}

有了简单工厂模式之后,构造苹果和香蕉就方便多了,但是一旦再次引入新水果(比如橙子),那就需要修改工厂类当中的create函数了

2.优缺点

特点:通过参数控制,便可以生产任何产品

优点:简单粗暴,直观易懂,使用一个工厂便可以生产一类产品

缺点:

  1. 违背了开闭原则,想要新增产品就需要修改工厂源代码

因此为了遵循开闭原则,大佬们搞出了工厂设计模式

4.工厂设计模式

1.介绍

既然不能修改原工厂,那么我们就仿照水果和(苹果,香蕉。。)之间的关系,将原工厂改为抽象类,一个类型的水果就对应一个工厂

2.代码

Talk is cheap,Show me the code

cpp 复制代码
class Fruit
{
public:
    // 父类指针指向new出来的子类对象,父类的析构函数必须要加virtual
    // 否则delete时就会调用父类的析构函数,极大可能会导致 内存泄漏或其他潜在风险
    virtual ~Fruit() {}
    virtual void name() = 0;
};

class Apple : public Fruit
{
public:
    virtual void name()
    {
        cout << "我是苹果\n";
    }
};

class Banana : public Fruit
{
public:
    virtual void name()
    {
        cout << "我是香蕉\n";
    }
};

class Orange : public Fruit
{
public:
    virtual void name()
    {
        cout << "我是橙子\n";
    }
};

namespace ns_method
{
    class FruitFactory
    {
    public:
    	virtual ~FruitFactory(){}
        virtual shared_ptr<Fruit> create() = 0;
    };

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

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

    class OrangeFactory : public FruitFactory
    {
    public:
        virtual shared_ptr<Fruit> create()
        {
            return make_shared<Orange>();
        }
    };
}

int main()
{
    std::shared_ptr<ns_method::FruitFactory> factory = std::make_shared<ns_method::AppleFactory>();
    auto fruit = factory->create();
    fruit->name();

    factory = std::make_shared<ns_method::BananaFactory>();
    fruit = factory->create();
    fruit->name();

    factory = std::make_shared<ns_method::OrangeFactory>();
    fruit = factory->create();
    fruit->name();
    return 0;
}

3.优缺点

特点:一个产品,一个工厂

优点:遵循了开闭原则

缺点:当产品特别多的时候,会造成代码臃肿

因此为了应对产品特别多的时候,大佬们发明了抽象工厂模式:

5.抽象工厂模式

1.介绍

依然是一个产品抽象类一个工厂,只不过这种方式更适用于多产品模式

2.代码

cpp 复制代码
class Animal
{
public:
    virtual ~Animal() {}
    virtual void name() = 0;
};

class Dog : public Animal
{
public:
    virtual void name()
    {
        cout << "我是狗\n";
    }
};

class Cat : public Animal
{
public:
    virtual void name()
    {
        cout << "我是猫\n";
    }
};

enum AnimalType
{
    DOG,
    CAT
};

namespace ns_abstract
{
    class Factory
    {
    public:
        virtual shared_ptr<Fruit> createFruit(FruitType type) = 0;
        virtual shared_ptr<Animal> createAnimal(AnimalType type) = 0;
    };

    class FruitFactory : public Factory
    {
    public:
        virtual shared_ptr<Fruit> createFruit(FruitType type)
        {
            if (type == APPLE)
                return make_shared<Apple>();
            if (type == BANANA)
                return make_shared<Banana>();
            if (type == ORANGE)
                return make_shared<Orange>();
            return shared_ptr<Fruit>();
        }
        virtual shared_ptr<Animal> createAnimal(AnimalType type)
        {
            return shared_ptr<Animal>();
        }
    };

    class AnimalFactory : public Factory
    {
    public:
        virtual shared_ptr<Fruit> createFruit(FruitType type)
        {
            return shared_ptr<Fruit>();
        }
        virtual shared_ptr<Animal> createAnimal(AnimalType type)
        {
            if (type == DOG)
                return make_shared<Dog>();
            if (type == CAT)
                return make_shared<Cat>();
            return shared_ptr<Animal>();
        }
    };
}

int main()
{
    std::shared_ptr<ns_abstract::Factory> factory = make_shared<ns_abstract::FruitFactory>();

    auto fruit = factory->createFruit(APPLE);
    fruit->name();
    fruit = factory->createFruit(BANANA);
    fruit->name();
    fruit = factory->createFruit(ORANGE);
    fruit->name();

    factory = make_shared<ns_abstract::AnimalFactory>();
    auto animal = factory->createAnimal(DOG);
    animal->name();
    animal = factory->createAnimal(CAT);
    animal->name();
    return 0;
}

3.优缺点

优点:适用于多产品,代码冗余度相对较小

缺点:违背开闭原则,增加新的产品类需要修改代码

三、建造者模式

1.介绍

建造者模式是一种创建型设计模式,主要用于解决对象的构建过于复杂的问题

使用一个个简单的对象一步一步构建成一个复杂的对象

2.为何要有建造者模式

在项目开发当中,有些类是由一堆零部件组装而成的,因此构造对象时过于复杂,不够优雅

比如:电脑的组装:

建筑者分为:装Windows电脑的,装Linux,装Mac OS电脑的人

他们都继承于同一个抽象建筑者类

然后搞一个指示者,负责按照正确的顺序调用建筑者的相应接口来完成特定任务

最后电脑就装好了

(如果零部件之间没有特定顺序要求的话,那么实际上是不需要这个指示者的,直接让建筑者提供一个总的build接口即可)

cpp 复制代码
因此建造者模式需要:
1. 抽象产品类
2. 具体产品类
3. 抽象Builder类:创建所有零部件的抽象接口
4. 具体Builder类:实现抽象接口,构建对应负责的零部件
5. 指挥者Director类:统一组件过程,通过指挥者来构造产品

3.代码

cpp 复制代码
// 每个电脑都必须要有一个OS,因此我们把OS定为纯虚函数,强制子类重写
class Computer
{
public:
    virtual ~Computer() {}
    void setBoard(const string &board)
    {
        _board = board;
    }
    void setDisplay(const string &display)
    {
        _display = display;
    }
    virtual void setOS() = 0;

    void show()
    {
        cout << "board:\t" << _board << "\n";
        cout << "display:\t" << _display << "\n";
        cout << "os:\t" << _os << "\n";
    }

protected:
    string _board;
    string _display;
    string _os;
};

class Winbook : public Computer
{
public:
    virtual void setOS()
    {
        _os = "Win 11";
    }
};

class Builder
{
public:
    virtual ~Builder() {}
    virtual void setBoard(const string &board) = 0;
    virtual void setDisplay(const string &display) = 0;
    virtual void setOS() = 0;
    virtual shared_ptr<Computer> get() = 0;
};

class WinbookBuilder : public Builder
{
public:
    WinbookBuilder() : _book(std::make_shared<Winbook>()) {}
    virtual void setBoard(const string &board)
    {
        _book->setBoard(board);
    }
    virtual void setDisplay(const string &display)
    {
        _book->setDisplay(display);
    }
    virtual void setOS()
    {
        _book->setOS();
    }
    virtual shared_ptr<Computer> get()
    {
        return _book;
    }

private:
    shared_ptr<Winbook> _book;
};

class Director
{
public:
    Director(const shared_ptr<Builder> &builder)
        : _builder(builder) {}
    void build(const string &board, const string &display)
    {
        _builder->setBoard(board);
        _builder->setDisplay(display);
        _builder->setOS();
    }

    shared_ptr<Builder> get()
    {
        return _builder;
    }

private:
    shared_ptr<Builder> _builder;
};

int main()
{
    Director director(make_shared<WinbookBuilder>());
    shared_ptr<Builder> builder = director.get();
    director.build("XXX主板", "XXX显示器");
    auto sp = builder->get();
    sp->show();
    return 0;
}

四、代理模式

1.介绍

代理模式是一种结构型设计模式,它为其他对象提供一种代理,以控制对该对象的访问

代理模式的主要目的是在客户端和目标对象之间增加一个中间层,起到增加功能、控制访问、减少系统间的耦合度的作用

cpp 复制代码
代理模式的结构包括:

一个目标对象和一个代理对象,目标对象和代理对象实现同一个接口
先访问代理对象,在通过代理对象来访问目标对象

代理模式分为静态代理和动态代理:

静态代理:在编译时就已经确定了代理类和被代理类之间的关系

动态代理:在运行时才能确定代理类和被代理类之间的关系

2.代码

以租房为例,房东在租房的时候需要发布招租启示,带人看房,负责维修等等,而房东为了图方便,可以把自己的房子委托给中介进行租赁

cpp 复制代码
class RentHouse
{
public:
    virtual void rent()=0;
};

class Landlord:public RentHouse
{
public:
    virtual void rent()
    {
        cout<<"把房子租出去\n";
    }
};

class Intermediary:public RentHouse
{
public:
    virtual void rent()
    {
        cout<<"发布招租启示\n";
        cout<<"带人开房\n";
        _landlord.rent();
        cout<<"负责维修\n";
    }
private:
    Landlord _landlord;
};

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

五、C不定参宏函数的介绍

我们的项目不仅仅会用到那四个设计模式,还会用到C和C++的不定参函数与C的不定参宏函数

因此,我们把他们介绍一下:

在学习C的时候,我们见过printf的函数原型:int printf(const char *format, ...);

...是可变参数列表,支持传入任意多个参数,首先我们先介绍它在C的宏函数当中的使用

我们就实现一个printf的宏函数吧

cpp 复制代码
__VA_ARGS__是一个宏,用来展开...当中的所有参数

因此,我们就可以实现一个printf

cpp 复制代码
#define PRINT(format,...) printf(format,__VA_ARGS__)
int main()
{
    PRINT("hello %s - %d\n","world",1);
    return 0;
}
wzs@iZ2ze5xfmy1filkylv86zbZ:~/blog/practice$ ./macro_args 
hello world - 1

打印日志时,我们通常把打印该日志的地方标记到日志当中,通常是以文件名:行号的方式来进行记录

需要用到__FILE__和__LINE__这两个宏,而C当中字符串是可以默认拼接的:

cpp 复制代码
#define PRINT(format, ...) printf("%s:%d " format "\n", __FILE__, __LINE__, __VA_ARGS__)

int main()
{
    PRINT("hello %s - %d", "world", 1);
    return 0;
}
wzs@iZ2ze5xfmy1filkylv86zbZ:~/blog/practice$ ./macro_args 
macro_args.c:7 hello world - 1

如果用户调用时,没有给...传入任何参数,那么宏替换之后,就会多出一个逗号,函数调用就会有问题

我们来看一下,宏替换之后会变成什么样子

cpp 复制代码
gcc -E  macro_args.c > macro_args.i

针对这种情况,编译器提供了##__VA_ARGS__,这个##的作用就是当...为空时,他会删除前面的那个逗号

因此,正确的版本是这样的:

cpp 复制代码
#define PRINT(format, ...) printf("%s:%d " format "\n", __FILE__, __LINE__, ##__VA_ARGS__)

然后我们重新gcc -E一下

六、C不定参函数的介绍

C的不定参函数的函数签名是这样的:

cpp 复制代码
至少需要一个固定参数,最后用...来接受任意数量的参数
函数内部需要根据固定参数来解析可变参数列表当中的所有参数

例如:
void func(int num,...);
void printf(const char* format,...);

1.C可变参数列表的介绍

在<stdarg.h>当中,定义了几个宏用来解决该问题

cpp 复制代码
va_list:这是一个类型,用于声明一个变量,该变量将用于访问可变参数列表

va_start(ap, last):宏,用于初始化 va_list 类型的变量 ap,以访问 last 参数之后的参数。
last 是可变参数列表之前的最后一个固定参数。

va_arg(ap, type):宏,用于返回参数列表中下一个参数的值,并将其类型转换为 type。
每次调用 va_arg 都会使 ap 指向下一个参数。

va_end(ap):宏,用于清理 va_list 变量 ap。
这是使用可变参数列表后应该进行的清理工作。

2.vsprintf系列函数的介绍

cpp 复制代码
int vsprintf(char *str, const char *format, va_list ap);
int vsnprintf(char *str, size_t size, const char *format, va_list ap);
int vasprintf(char **strp, const char *fmt, va_list ap);

根据format格式化字符串和ap可变参数列表,将对应参数提取出来存放到str当中

vsprintf和vsnprintf的区别是:

vsnprint允许指定最大字符数目,以防止缓冲区溢出

而vsprintf则没有这样的限制,容易发生安全问题

vasprintf是允许我们传入一级指针的地址,他在函数内部会给我们malloc一段空间,将数据放到其中

需要我们自行free掉这个strp,是这三个当中最好用的

若失败,则返回-1

3.使用可变参数列表

先给大家演示一下可变参数列表的使用:

cpp 复制代码
void func(int num, ...)
{
    va_list ap;
    va_start(ap, num);
    for (int i = 0; i < num; i++)
    {
    	// 指针偏移
        int val = va_arg(ap, int);
        printf("%d ",val);
    }
    printf("\n");
    va_end(ap);
}

int main()
{
    func(3,1,2,3);
    func(0);
    func(1,1);
    return 0;
}

wzs@iZ2ze5xfmy1filkylv86zbZ:~/blog/practice$ ./macro_args 
1 2 3 

1 

4.实现myprintf

想要使用vasprintf函数,需要 #define _GNU_SOURCE

cpp 复制代码
void myprintf(const char *format, ...)
{
    va_list ap;
    va_start(ap, format);
    char* buf=NULL;
    int ret=vasprintf(&buf,format,ap);
    if(ret!=-1)
    {
        printf("%s\n",buf);
        free(buf);
    }
    va_end(ap);
}

int main()
{
    myprintf("hello %s - %d","world",1);
    myprintf("hello %s","world");
    myprintf("hello");
    return 0;
}
cpp 复制代码
wzs@iZ2ze5xfmy1filkylv86zbZ:~/blog/practice$ ./macro_args 
hello world - 1
hello world
hello

七、C++不定参函数的介绍

C++不定参函数是通过可变模板参数包来实现的,是通过递归的方法来进行解包的

注意:因为可变模板参数包需要在编译时完成递归,而if语句在编译时,两条路都要走

因此如果args模板包为空,递归调用自己的路还是要编译的

而myprintf不支持传入0个参数,因此我们需要给myprintf加一个接受0个参数的函数重载,来解决编译时报错的问题

cpp 复制代码
void myprintf()
{
    cout << "\n";
}

template <class T, class... Args>
void myprintf(const T &val, Args &&...args)
{
    // 先打印val
    cout << val;
    // 如果args模板包当中参数个数大于0,则递归分用
    if ((sizeof...(args)) > 0)
    {
        myprintf(forward<Args>(args)...);
    }
    else
    {
        myprintf();
    }
}

int main()
{
    myprintf();
    myprintf("hello");
    myprintf("hello ", "world");
    myprintf("hello ", "world-", 11);
    return 0;
}

否则就会:

cpp 复制代码
cpp_args.cc: In function 'void myprintf(const T&, Args&& ...)':
cpp_args.cc:21:18: error: no matching function for call to 'myprintf()'
   21 |         myprintf();
      |                  ^
cpp_args.cc:10:6: note: candidate: 'template<class T, class ... Args> void myprintf(const T&, Args&& ...)'
   10 | void myprintf(const T &val, Args &&...args)
      |      ^~~~~~~~
cpp_args.cc:10:6: note:   template argument deduction/substitution failed:
cpp_args.cc:21:18: note:   candidate expects at least 1 argument, 0 provided
   21 |         myprintf();

candidate expects at least 1 argument, 0 provided:

我期待至少一个参数,结果你给我0个...

八、项目工具类

我们的项目还需要几个小工具类:

1.获取系统当前时间

cpp 复制代码
class DateHelper
{
public:
    static time_t now()
    {
        return time(nullptr);
    }
};

2.判断文件是否存在

cpp 复制代码
class FileHelper
    {
    public:
        static bool exists(const std::string &pathname)
        {
            struct stat st;
            // stat遵循POSIX标准(Portable Operating System Interface for UNIX 可移植操作系统接口)
            // 具有良好的跨平台移植性
            return stat(pathname.c_str(), &st) >= 0; // 第二个参数不能给nullptr,因为它是一个输出型参数
        }

3.获取文件父级路径

find_last_of是从后往前找第一个符合条件的字符,只要该字符位于传入的字符集合即可

cpp 复制代码
static std::string getPath(const std::string &pathname)
{
    // ./a/b/c/d
    // 从后往前找第一个'/' 然后返回./a/b/c
    // 如果没有,则返回.
    // find_last_of
    size_t pos = pathname.find_last_of(R"(/\)"); // 支持(/)Linux、Mac OS和Win(\)
    if (pos == std::string::npos)
    {
        return ".";
    }
    return pathname.substr(0, pos);
}

因为\是转义字符,想要表示单纯的\的话,需要二次转义\\

不够优雅,所以我们用了一下C++11的 字符串字面量R"(/\)"

4.创建目录

./a/b/c

从前往后依次创建 ./a ./a/b ./a/b/c即可,就是简单的string的find和substr还有mkdir的使用而已

cpp 复制代码
static bool createDir(const std::string &pathname)
{
    // ./a/b/c
    // 从前往后依次创建 ./a  ./a/b  ./a/b/c
    size_t pos = pathname.find_first_of(R"(/\)");
    while (pos != std::string::npos)
    {
        umask(0);
        std::string substr = pathname.substr(0, pos);
        int ret = mkdir(substr.c_str(), 0775);
        if (ret == -1 && errno != EEXIST)
        {
            std::cout << "目录创建失,总目录:" << pathname << ",当前目录:" << substr << "\n";
            return false;
        }
        pos = pathname.find_first_of(R"(/\)", pos + 1);
    }
    // 创建整个目录
    return (mkdir(pathname.c_str(), 0775) != -1 || errno == EEXIST);
}

以上就是日志系统第二弹:设计模式介绍,C和C++不定参函数的介绍的全部内容

相关推荐
呆萌很5 分钟前
C++ 集合 list 使用
c++
诚丞成1 小时前
计算世界之安生:C++继承的文水和智慧(上)
开发语言·c++
东风吹柳2 小时前
观察者模式(sigslot in C++)
c++·观察者模式·信号槽·sigslot
A懿轩A2 小时前
C/C++ 数据结构与算法【栈和队列】 栈+队列详细解析【日常学习,考研必备】带图+详细代码
c语言·数据结构·c++·学习·考研·算法·栈和队列
思忖小下2 小时前
梳理你的思路(从OOP到架构设计)_简介设计模式
设计模式·架构·eit
大胆飞猪3 小时前
C++9--前置++和后置++重载,const,日期类的实现(对前几篇知识点的应用)
c++
1 9 J3 小时前
数据结构 C/C++(实验五:图)
c语言·数据结构·c++·学习·算法
夕泠爱吃糖3 小时前
C++中如何实现序列化和反序列化?
服务器·数据库·c++
长潇若雪3 小时前
《类和对象:基础原理全解析(上篇)》
开发语言·c++·经验分享·类和对象
liyinuo20174 小时前
嵌入式(单片机方向)面试题总结
嵌入式硬件·设计模式·面试·设计规范