C++实战学习:输出类的抽象和实现详解

最近写了一些博客复习C++的知识,但理论终究是理论,那多态、继承等C++特性到底该在什么情况下使用?如何模块化地完成一个程序呢?还有没有什么C++语法方面值得学习的知识呢?本节就来分析一个实际项目中的例子,来理解这些知识和用法。

我们写程序的过程中难免会用到输出,比如输出到控制台,输出到串口,或者输出一些调试信息到文件系统。那么我们应该怎样设计一个类可以支持输出到不同的地方呢?这节课就来简单地分析一个用于输出的类应该如何实现。本节例子的源码摘自NXP官方用于加密镜像的开源C++项目elftosb

  • 参考:elftosb源码

文章目录

  • 1 代码分析
    • 1.1 main
    • 1.2 输出类分析:Log、Logger和StdoutLogger类
    • 1.3 smart_array_ptr类
  • 2 总结
    • 2.1 输出类实现总结
    • 2.2 C++知识总结

1 代码分析

1.1 main

一个程序从main开始运行,首先看一下main函数:

int main(int argc, char *argv[], char *envp[])
{
    try
    {
        return elftosbTool(argc, argv).run();
    }
    catch (...)//...表示捕获所有异常
    {
        Log::log(Logger::ERROR, "error: unexpected exception\n");
        return 1;
    }
    return 0;
}

可以看到程序就是通过接收命令行参数构造一个elftosbTool类,然后调用类方法run()来运行。同时这里使用了异常机制来捕获所有异常。下面先来分析一下出现的Log类。

1.2 输出类分析:Log、Logger和StdoutLogger类

Log类中有调用Logger类,所以一起来看看这两个**类声明**:

class Logger
{
public:
    enum log_level_t //log等级
    {
        URGENT = 0, //!< The lowest level, for messages that must always be logged.
        ERROR,      //!< For fatal error messages.
        WARNING,    //!< For non-fatal warning messages.
        INFO,       //!< The normal log level, for status messages.
        INFO2,      //!< For verbose status messages.
        DEBUG,      //!< For internal reporting.
        DEBUG2      //!< Highest log level; verbose debug logging.
    };

public:

    Logger(): m_filter(INFO), m_level(INFO){}//构造函数

    virtual ~Logger() {}//析构函数
    inline void setFilterLevel(log_level_t level) { m_filter = level; }//设置过滤等级
    inline log_level_t getFilterLevel() const { return m_filter; }//获得过滤等级
    inline void setOutputLevel(log_level_t level) { m_level = level; }//设置输出等级
    inline log_level_t getOutputLevel() const { return m_level; }//获得输出等级
	/* 几种不同格式的输出,虚函数 */
    virtual void log(const char *fmt, ...);
    virtual void log(const std::string &msg) { log(msg.c_str()); }
    virtual void log(log_level_t level, const char *fmt, ...);
    virtual void log(log_level_t level, const std::string &msg) { log(level, msg.c_str()); }
    virtual void log(const char *fmt, va_list args);
    virtual void log(log_level_t level, const char *fmt, va_list args);

protected:
    log_level_t m_filter; //过滤等级
    log_level_t m_level;  //输出等级

protected:
    virtual void _log(const char *msg) = 0;
};

class Log
{
public:
    static inline Logger *getLogger() { return s_logger; } //返回Logger对象
    static inline void setLogger(Logger *logger) { s_logger = logger; } //设置Logger对象
	/* Log类继承了Logger中其中四种输出虚函数 */
    static void log(const char *fmt, ...);
    static void log(const std::string &msg);
    static void log(Logger::log_level_t level, const char *fmt, ...);
    static void log(Logger::log_level_t level, const std::string &msg);

protected:
    static Logger *s_logger; //全局logger

public:
	/* SetOutputLevel类 */
    class SetOutputLevel
    {
    public:
		/* 设置输出等级的两个重载函数:用全局的s_logger输出 */
        SetOutputLevel(Logger::log_level_t level): m_logger(Log::getLogger()),m_saved(Logger::INFO)
        {
            assert(m_logger);
            m_saved = m_logger->getOutputLevel();
            m_logger->setOutputLevel(level);
        }
		/* 传入一个创建的logger类输出 */
        SetOutputLevel(Logger *logger, Logger::log_level_t level): m_logger(logger), m_saved(logger->getOutputLevel())
        {
            assert(m_logger);
            m_logger->setOutputLevel(level);
        }
		/* 恢复当前的Logger输出类为全局的s_logger,输出等级是之前设置的 */
        ~SetOutputLevel() { m_logger->setOutputLevel(m_saved); }
    protected:
        Logger *m_logger;            //指向当前类的Logger输出类
        Logger::log_level_t m_saved; //这里还保存了原来的输出等级
    };
};

上面类的声明很好理解,我们可以从中学到一些细节:

(1)基类析构函数加virtual

不加的话,当基类销毁时不会调用派生类的析构函数;反之会先调用派生类的析构函数再调用基类的析构函数

(2)类函数中的inline

其实在类内实现的函数默认就为内联函数,这里加上inline是确保其为内联函数,或确保在不同编译器都为内联(可能有的编译器不会默认内联?)。

(3)类函数中的static

所有声明该类的变量共用此方法。

(4)类中声明类

上面的Log中的public下还声明了一个SetOutputLevel类,实际上在Log类各个方法的实现中并没有任何方法调用到这个类。这样做可以让代码的逻辑更清晰,也完成了特定的需求功能:首先整个Log类,顾名思义作用就是输出,所以方法里应该要有输出函数和设置输出等级,对于输出等级来说,这里单独封装了一个SetOutputLevel类用于设置logger的输出等级。

注意这里的析构函数~SetOutputLevel()恢复了之前的输出等级,也就是说加入默认的输出等级是INFO,在某个方法中的输出等级需要为URGENT,我们可以声明一个SetOutputLevel局部变量改变仅此方法输出的输出等级为URGENT,在此方法调用完后,析构函数将恢复为之前的输出等级INFO,而不需要用户自行恢复。

Logger类的实现

Log就是通过s_logger调用方法,实际上也是调用Logger类,这里就不贴Log类的代码了。在Logger中,所有的log重载函数经过格式转换后最后调用的都是Logger::log(log_level_t level, const char *fmt, va_list args)

void Logger::log(log_level_t level, const char *fmt, va_list args)
{
    smart_array_ptr buffer = new char[1024];
    vsprintf(buffer, fmt, args);
    if (level <= m_filter)
    {
        _log(buffer);
    }
}

可以看到,最终都是调用_log函数对接输出接口进行输出。

用户自定义输出类

因为Logger中的virtual void _log(const char *msg)为虚函数,所以用户只需要声明一个输出接口类,并继承Logger类,然后重写_log实现自己的输出方法即可。如这里代码中实现了std标准输出类:

class StdoutLogger : public Logger
{
protected:
    virtual void _log(const char *msg);
};

void StdoutLogger::_log(const char *msg)
{
    printf("%s", msg);
}

同理,假设想输出到串口,用户只需定义一个串口输出类继承Logger即可。

实际使用流程

现在回到main函数,在catch中直接使用Log::log()进行输出异常提示信息,说明已经有地方调用Log类的类方法setLogger设置了s_logger,并且这个s_logger也实现了_log虚函数。

再来看,在try的代码块中执行了return elftosbTool(argc, argv).run(),所以我们来看一下elftosbTool类及其的构造函数(此类为整个程序的主类,有很多变量和方法实现,这里仅展示与输出类有关的部分):

class elftosbTool
{
protected:
    int m_argc;              //!< Number of command line arguments.
    char **m_argv;           //!< String value for each command line argument.
    StdoutLogger *m_logger;  //!< Singleton logger instance.

public:
    elftosbTool(int argc, char *argv[])
        : m_argc(argc)
        , m_argv(argv)
        , m_logger(0)
    {
        m_logger = new StdoutLogger();
        m_logger->setFilterLevel(Logger::INFO);
        Log::setLogger(m_logger);
    }
}

可以看出elftosbTool使用标准输出,所以声明了StdoutLogger *m_logger变量。在构造函数中分配了一个StdoutLogger对象并设置默认输出等级为INFO。然后设置Log中的默认全局输出变量static Logger *s_loggerm_logger。这样用户只需调用Log类进行输出即可,同时可以更改输出的等级,假设有一个printInfo函数:

void printInfo()
{
    Log::SetOutputLevel leveler(Logger::DEBUG);
    Log::log("positional args:\n");
}

1.3 smart_array_ptr类

我们注意到前面的Logger::log类实现中有一行smart_array_ptr buffer = new char[1024],还用到了一个smart_array_ptr类。这个类是一个智能指针,它封装了对动态数组的内存管理,并且提供了一些方便的运算符重载。下面来看一下这个类的声明:

template 
class smart_array_ptr
{
public:
    typedef T data_type;
    typedef T *ptr_type;
    typedef const T *const_ptr_type;
    typedef T &ref_type;
    typedef const T &const_ref_type;

    smart_array_ptr()
        : _p(0)
    {
    }
    smart_array_ptr(ptr_type p)
        : _p(p)
    {
    }

    virtual ~smart_array_ptr() { safe_delete(); }
    ptr_type get() { return _p; }
    const_ptr_type get() const { return _p; }
	/* 设置数组指针,并释放前一个指针的内存 */
    void set(ptr_type p)
    {
        if (_p && p != _p)
        {
            safe_delete();
        }
        _p = p;
    }

    void reset() { _p = 0; }
    void clear() { safe_delete(); }
	/* 释放分配的内存 */
    virtual void safe_delete()
    {
        if (_p)
        {
            delete[] _p;
            _p = 0;
        }
    }

    operator ptr_type() { return _p; }
    operator const_ptr_type() const { return _p; }
    operator ref_type() { return *_p; }
    operator const_ref_type() const { return *_p; }
    operator bool() const { return _p != 0; }
    smart_array_ptr &operator=(const_ptr_type p)
    {
        set(const_cast(p));
        return *this;
    }

    ptr_type operator->() { return _p; }
    const_ptr_type operator->() const { return _p; }
    ref_type operator[](unsigned index) { return _p[index]; }
    const_ref_type operator[](unsigned index) const { return _p[index]; }

protected:
    ptr_type _p;
};

使用:
smart_array_ptr buffer = new char[1024]

整体来说代码还是很好理解的,但有一些C++语法还是值得推敲的:

1、两个构造函数调用的情况

(1)调用smart_array_ptr(ptr_type p): _p(p)

smart_array_ptr buffer = new char[1024];

上面的代码由于是在声明时的赋值,在C++中等价于smart_array_ptr buffer(new char[1024])

(2)调用smart_array_ptr():_p(0)

smart_array_ptr buffer;
buffer = new char[1024];

先调用无参构造函数,然后再调用运算符=的重载函数smart_array_ptr &operator=(const_ptr_type p)

2、重载函数分析

(1)operator 数据类型(){}:该类的对象可以直接与特定数据类型的变量进行比较或赋值

typedef T data_type;
typedef T *ptr_type;
typedef const T *const_ptr_type;
typedef T &ref_type;
typedef const T &const_ref_type;
    
operator ptr_type() { return _p; }
operator const_ptr_type() const { return _p; }
operator ref_type() { return *_p; }
operator const_ref_type() const { return *_p; }
operator bool() const { return _p != 0; }

调用例子:

smart_array_ptr buffer1 = new char[16];
const smart_array_ptr buffer2 = new char[16];

char *tmp1 = buffer1;         //调用operator ptr_type()
const char * tmp2 = buffer2;  //调用operator const_ptr_type() const
char tmp3 = buffer1;          //调用operator ref_type()
const char tmp4 = buffer2;    //调用operator const_ref_type() const
const bool tmp5 = buffer2;    //调用operator bool() const

(2)返回值 operator重载运算符(参数){}:重载类的运算符

ptr_type operator->() { return _p; }
const_ptr_type operator->() const { return _p; }
ref_type operator[](unsigned index) { return _p[index]; }
const_ref_type operator[](unsigned index) const { return _p[index]; }

调用例子:

class test{
public:
    void test1(){printf("test\r\n");}
    void test2()const{printf("test\r\n");}
};

smart_array_ptr str1 = new test[16];
const smart_array_ptr str2 = new test[16];
smart_array_ptr buffer1 = new char[16];
const smart_array_ptr buffer2 = new char[16];

str1.operator->()->test1();  //调用ptr_type operator->()
str2.operator->()->test2();  //调用const_ptr_type operator->() const
buffer1[0] = 1;              //调用ref_type operator[]
const int a = buffer2[0];    //const_ref_type operator[](unsigned index) const
  • 实际上重载->运算符并不常用,这里仅仅是举一个例子

2 总结

2.1 输出类实现总结

本文介绍了elftosb工程中的输出类LogLoggerStdoutLogger的实现,我们来总结一下其中的原理:

  • Logger类:输出类中的底层,最终将调用这个类方法进行输出,它封装了一些格式化输出的函数,同时留出一个虚函数接口_log,即我们的输出方式有很多种,如标准输出、串口输出和网络输出等。
  • StdoutLogger类:继承Logger类,实现具体的_log输出方法,这里实现的是标准输出
  • Log类:给用户实际调用输出的类,其中声明了一个static Logger *s_logger变量,用户可以使用类方法setLogger进行设置这个Logger ,然后调用Log类中的log输出函数,实际上就是调用s_logger的类方法。
    • 同时这个类还提供了更改输出等级的类:SetOutputLevel,用于更改某个Logger 的输出等级

大家在设计类的时候可以学习一下这种模块化的设计。

2.2 C++知识总结

(1)基类析构函数加virtual:会先调用派生类的析构函数再调用基类的析构函数

(2)类函数中的inline:在类中实现的方法默认内联,加上是为了以防万一,同时显式地让程序员知道这是内联函数

(3)类中的static:对于类中变量,后续创建该类的实例的时候,不会重复创建这些变量的空间,因为这是所有对象共享的变量;对于类中的方法,不需要类的对象就可以被调用,它只能访问静态成员变量或其他静态类方法。

(4)类中声明类:在本例中,巧妙地运用了这个类中类的声明和析构函数的特性,让用户可以在任意函数中设置一次输出等级,然后在函数退出后,调用析构函数恢复之前的输出等级

(5)运算符重载

  • operator 数据类型(){}:该类的对象可以直接与特定数据类型的变量进行比较或赋值
  • 返回值 operator重载运算符(参数){}:重载类的运算符

你可能感兴趣的:(C++,c++,学习,驱动开发)