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; //全局loggerpublic:/* 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<char> 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<char> buffer = new char[1024],还用到了一个smart_array_ptr类。这个类是一个智能指针,它封装了对动态数组的内存管理,并且提供了一些方便的运算符重载。下面来看一下这个类的声明:

template <typename T>
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<T> &operator=(const_ptr_type p){set(const_cast<ptr_type>(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<char> buffer = new char[1024]

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

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

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

smart_array_ptr<char> buffer = new char[1024];

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

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

smart_array_ptr<char> buffer;
buffer = new char[1024];

先调用无参构造函数,然后再调用运算符=的重载函数smart_array_ptr<T> &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<char> buffer1 = new char[16];
const smart_array_ptr<char> 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<test> str1 = new test[16];
const smart_array_ptr<test> str2 = new test[16];
smart_array_ptr<char> buffer1 = new char[16];
const smart_array_ptr<char> 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重载运算符(参数){}:重载类的运算符

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

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

相关文章

多测师肖sir_高级金牌讲师_jenkins搭建

jenkins操作手册 一、jenkins介绍 1、持续集成&#xff08;CI&#xff09; Continuous integration 持续集成 团队开发成员每天都有集成他们的工作&#xff0c;通过每个成员每天至少集成一次&#xff0c;也就意味着一天有可 能多次集成。在工作中我们引入持续集成&#xff0c;通…

大模型时代,开发者成长指南 | 新程序员

【编者按】GPT 系列的面世影响了全世界、各个行业&#xff0c;对于开发者们的感受则最为深切。以 ChatGPT、Github Copilot 为首&#xff0c;各类 AI 编程助手层出不穷。编程范式正在发生前所未有的变化&#xff0c;从汇编到 Java 等高级语言&#xff0c;再到今天以自然语言为特…

Python高级语法----Python多线程与多进程

文章目录 多线程多进程注意事项多线程与多进程是提高程序性能的两种常见方法。在深入代码之前,让我们先用一个简单的比喻来理解它们。 想象你在一家餐厅里工作。如果你是一个服务员,同时负责多个桌子的顾客,这就类似于“多线程”——同一个人(程序)同时进行多项任务(线程…

HttpUtils工具类

作为Java开发程序员&#xff0c;需要我们经常写一些工具类来简化开发过程&#xff0c;我们自己肯定写过或者用过HttpUtils用来发送http请求&#xff0c;但是每次手写太繁琐了&#xff0c;于是就按照标准写了一个Http工具类&#xff0c;现在分享出来。 1.HTTP请求简介 HTTP(Hy…

SSM 线上知识竞赛系统-计算机毕设 附源码 27170

SSM线上知识竞赛系统 摘 要 科技进步的飞速发展引起人们日常生活的巨大变化&#xff0c;电子信息技术的飞速发展使得电子信息技术的各个领域的应用水平得到普及和应用。信息时代的到来已成为不可阻挡的时尚潮流&#xff0c;人类发展的历史正进入一个新时代。在现实运用中&#…

虚幻引擎:如何使用 独立进程模式进行模拟

第一步:先更改配置 第二步,在启动的两个玩家里面,一个设为服务器,一个链接进去地图就可以了 1.设置服务器 2.另一个玩家链接

企业级低代码开发,科技赋能让企业具备“驾驭软件的能力”

科技作为第一生产力&#xff0c;其强大的影响力在各个领域中都有所体现。数字技术&#xff0c;作为科技领域中的一股重要力量&#xff0c;正在对传统的商业模式进行深度的变革&#xff0c;为各行业注入新的生命力。随着数字技术的不断发展和应用&#xff0c;企业数字化转型的趋…

远程运维用什么软件?可以保障更安全?

远程运维顾名思义就是通过远程的方式IT设备等运行、维护。远程运维适用场景包含因疫情居家办公&#xff0c;包含放假期间出现运维故障远程解决&#xff0c;包含项目太远需要远程操作等等。但远程运维过程存在一定风险&#xff0c;安全性无法保障&#xff0c;所以一定要选择靠谱…

RocketMQ如何安全的批量发送消息❓

优点&#xff1a; 批量发送消息可以提高rocketmq的生产者性能和吞吐量。 使用场景: 发送大量小型消息时&#xff1b;需要降低消息发送延迟时&#xff1b;需要提高生产者性能时&#xff1b; 注意事项&#xff1a; 消息列表的大小不能超过broker设置的最大消息大小;消息列表…

如何快速教你看自己电脑cpu是几核几线程

目录 一、我们日常中说的电脑多少核多少线程&#xff0c;很多人具体不知道什么意思&#xff0c;下面举例4核和4线程什么意思。二、那么4线程又是怎么回事呢&#xff1f;三、那么知道了上面的介绍后怎么看一台电脑是几核&#xff0c;几线程呢&#xff1f; 一、我们日常中说的电脑…

JSONP 跨域访问(2), JSONP劫持

JSONP 跨域访问(2), JSONP劫持 一, 利用 XSS 漏洞执行jsonp 1. 利用过程 发现有jsonp的请求: <script type"text/javascript" src"http://192.168.112.200/security/jsonp.php?callbackjsonpCallback"></script>向xss漏洞的位置注入代码…

​软考-高级-信息系统项目管理师教程 第四版【第24章-法律法规与标准规范-思维导图】​

软考-高级-信息系统项目管理师教程 第四版【第24章-法律法规与标准规范-思维导图】 课本里章节里所有蓝色字体的思维导图

springboot 项目升级 2.7.16 踩坑

记录一下项目更新版本依赖踩坑 这个是项目最早的版本依赖 这里最初是最初是升级到 2.5.7 偷了个懒 这个版本的兼容性比较强 就选了这版本 也不用去修改就手动的去换了一下RabbitMQ的依赖 因为这边项目有AMQP 风险预警 1.spring-amqp版本低于2.4.17的用户应升级到2.4.17 2.spri…

时序预测 | MATLAB实现WOA-CNN-BiLSTM-Attention时间序列预测(SE注意力机制)

时序预测 | MATLAB实现WOA-CNN-BiLSTM-Attention时间序列预测&#xff08;SE注意力机制&#xff09; 目录 时序预测 | MATLAB实现WOA-CNN-BiLSTM-Attention时间序列预测&#xff08;SE注意力机制&#xff09;预测效果基本描述模型描述程序设计参考资料 预测效果 基本描述 1.MAT…

蓝桥杯官网练习题(移动距离)

题目描述 X 星球居民小区的楼房全是一样的&#xff0c;并且按矩阵样式排列。其楼房的编号为 1,2,3, 当排满一行时&#xff0c;从下一行相邻的楼往反方向排号。 比如&#xff1a;当小区排号宽度为 6 时&#xff0c;开始情形如下&#xff1a; 1 2 3 4 5 6 12 …

python模块之redisbloom redis布隆过滤器

一、简介 RedisBloom 是一个 Redis 模块&#xff0c;提供了布隆过滤器&#xff08;Bloom Filter&#xff09;、计数器&#xff08;Count-Min Sketch&#xff09;、Top-K&#xff08;Top-K&#xff09;、Top-K with expiry&#xff08;Top-K with Expiration&#xff09;和多样…

开发知识点-golang

golang语言学习 环境搭建win10配置go环境 ubuntu20.04安装golang介绍下载 Go 压缩包调整环境变量验证 Go 安装过程 环境搭建 win10配置go环境 中文网进行下载 https://studygolang.com/dl 配置环境变量 增加GOROOT: 新建 -->变量名为: GOROOT(必须大写) 变量值: 你安装…

2311rust无畏并发.

原文 Rust无畏并发 Rust是为了解决两个麻烦问题: 1,如何安全系统编程 2,如何无畏并发 最初,这些问题似乎是无关的,但令惊讶的是,方法竟然是相同的:使Rust安全的相同工具也可帮助解决并发问题. 内存安全和并发错误,一般认为是代码在不应访问数据时访问数据.Rust依靠所有权为…

删除word最后一页之后的空白页

最近编辑word比较多&#xff0c;有时最后一页&#xff08;最后一页内容还有可能是表格&#xff09;之后&#xff0c;还有一页空白页&#xff0c;单独按下backspace、del都删不掉&#xff0c;很让人着急。 经过查询有几种方法&#xff1a; &#xff08;1&#xff09;点击选中空…

12、填写NGINX配置部署前端;运行jar部署后端

后端可以部署的方式&#xff0c;首先直接运行jar是肯定可以的。此外&#xff0c;可以单独开docker容器运行在容器中。 但是这里运行在容器中必要性&#xff0c;其实并不大。 当前我们直接运行jar来运行后端。后面推出集成docker。 直接运行jar包的方式&#xff0c;首先需要打…