最近写了一些博客复习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 exceptionn");
return 1;
}
return 0;
}
可以看到程序就是通过接收命令行参数构造一个elftosbTool
类,然后调用类方法run()
来运行。同时这里使用了异常机制来捕获所有异常。下面先来分析一下出现的Log
类。
1.2 输出类分析:Log、Logger和StdoutLogger类
Log
类中有调用Logger
类,所以一起来看看这两个**类声明**:
class Logger
{
public:
enum log_level_t //log等级
{
URGENT = 0, //!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
可以看到,最终都是调用_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; //!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");
}
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("testrn");}
void test2()const{printf("testrn");}
};
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 输出类实现总结
本文介绍了elfto服务器托管网sb
工程中的输出类Log
、Logger
和StdoutLogger
的实现,我们来总结一下其中的原理:
-
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重载运算符(参数){}
:重载类的运算符
服务器托管,北京服务器托管,服务器租用 http://www.fwqtg.net
答开源创业 15 问:选协议、维权、公关、找钱 本文分享自华为云社区《GaussDB数据库SQL系列-数据去重》,作者: Gauss松鼠会小助手2 。 一、前言 数据去重在数据库中是比较常见的操作。复杂的业务场景、多业务线的数据来源等等,都会带来重复数据的存储…