最近写了一些博客复习C++的知识,但理论终究是理论,那多态、继承等C++特性到底该在什么情况下使用?如何模块化地完成一个程序呢?还有没有什么C++语法方面值得学习的知识呢?本节就来分析一个实际项目中的例子,来理解这些知识和用法。
我们写程序的过程中难免会用到输出,比如输出到控制台,输出到串口,或者输出一些调试信息到文件系统。那么我们应该怎样设计一个类可以支持输出到不同的地方呢?这节课就来简单地分析一个用于输出的类应该如何实现。本节例子的源码摘自NXP官方用于加密镜像的开源C++项目elftosb
。
一个程序从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
类。
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_logger
为m_logger
。这样用户只需调用Log
类进行输出即可,同时可以更改输出的等级,假设有一个printInfo
函数:
void printInfo()
{
Log::SetOutputLevel leveler(Logger::DEBUG);
Log::log("positional args:\n");
}
我们注意到前面的Logger::log
类实现中有一行smart_array_ptr
,还用到了一个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
。
(2)调用smart_array_ptr():_p(0)
smart_array_ptr buffer;
buffer = new char[1024];
先调用无参构造函数,然后再调用运算符=
的重载函数smart_array_ptr
。
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
->
运算符并不常用,这里仅仅是举一个例子本文介绍了elftosb
工程中的输出类Log
、Logger
和StdoutLogger
的实现,我们来总结一下其中的原理:
Logger
类:输出类中的底层,最终将调用这个类方法进行输出,它封装了一些格式化输出的函数,同时留出一个虚函数接口_log
,即我们的输出方式有很多种,如标准输出、串口输出和网络输出等。StdoutLogger
类:继承Logger
类,实现具体的_log
输出方法,这里实现的是标准输出Log
类:给用户实际调用输出的类,其中声明了一个static Logger *s_logger
变量,用户可以使用类方法setLogger
进行设置这个Logger
,然后调用Log
类中的log
输出函数,实际上就是调用s_logger
的类方法。
SetOutputLevel
,用于更改某个Logger
的输出等级大家在设计类的时候可以学习一下这种模块化的设计。
(1)基类析构函数加virtual
:会先调用派生类的析构函数再调用基类的析构函数
(2)类函数中的inline
:在类中实现的方法默认内联,加上是为了以防万一,同时显式地让程序员知道这是内联函数
(3)类中的static
:对于类中变量,后续创建该类的实例的时候,不会重复创建这些变量的空间,因为这是所有对象共享的变量;对于类中的方法,不需要类的对象就可以被调用,它只能访问静态成员变量或其他静态类方法。
(4)类中声明类:在本例中,巧妙地运用了这个类中类的声明和析构函数的特性,让用户可以在任意函数中设置一次输出等级,然后在函数退出后,调用析构函数恢复之前的输出等级
(5)运算符重载
operator 数据类型(){}
:该类的对象可以直接与特定数据类型的变量进行比较或赋值返回值 operator重载运算符(参数){}
:重载类的运算符