json其实不难,只需了解一点,就能轻松玩转它

工作过程中,经常需要使用json这种轻量级的数据交换格式,例如,通过它可以组织数据保存到配置文件,客户端和服务端通过json格式来进行通信等,但是,针对特定的需求场景,需要设计怎样的json格式比较合适呢,json到底可以支持多少种格式呢,有没有一种简单的概括,就能让使用者轻松使用它呢!

一般知识都有基本的理论结构,所以,本文首先将说明json的基本知识点,然后基于开源软件jsoncpp来说明如何构建/读写json,   再分享个人的使用心得,最后再总结json的关键要点,理解了这一点,玩转json不是问题。

一、Json简介

Json是轻量级的数据交换格式,它便于阅读和编写,它是完全独立于程序语言的文本格式。

二、Json结构

Json有两个结构, 分别是“名称/值”对的集合和值的有序列表。“名称/值”对的集合可以简单理解为对象集合,而值的有序列表可以理解为数组。

这里举一个“名称/值”对的集合的例子,它是以左大括号开始,以右大括号结束,中间是由多个“名称/值”对组成,各个“名称/值”对之间用逗号隔开。

{
    "cpu_name" : "special",
    "cpu_temp" : 40
}

举一个“值的有序列表”的例子,它是以左中括号开始,以右中括号结束,中间是由多个值组成,各个值之间用逗号隔开。

["apple", "pear", "banana "]

三、Json形式

Json主要由三种形式,分别为对象(object),  数组(array),  值(value)。

对象(object)是“名称/值”对集合,名称于值之间通过冒号隔开,另外对象是以左大括号开始,以右大括号结束。

数组(array)是值的有序集合,它是以左中括号开始,以右中括号结束。

值(value)可以是字符串(string)、数值(number)、对象(object)、数组(array)、true、false、null。这里我们会发现对象(object)里面有值(value),  数组(array)里面也有值(value),  而值(value)又包含有对象和数组,所以它们是可以嵌套的。

Json就是由上面简单的元素来组建复杂的信息元素。

四、Json例子

jsoncpp是C++语言编写的开源json库,通过该库,我们可以很容易的构建、读写json。接下来就基于jsoncpp来解释几个构建、读取json的例子。通过例子可以对json有更深的理解。jsoncpp最基本的对象就是Json::Value。

构建一个最简单的对象,然后输出整个json信息,最后读取json值,先调用isMember判断名称是否为root成员,如果是的话,那么就读取输出。

Json::Value root;
root["result"] = "true";

Json::StyledWriter styled_writer;
std::string str_json = styled_writer.write(root);
LOG(INFO) << "str_json: " << str_json.c_str();

if (root.isMember("result"))
{
    LOG(INFO) << "root[\"result\"] = " << root["result"];
}

输出的日志信息如下所示,大括号包含了一个“名称/值”对。

2020-05-02 17:59:32,670 INFO  [default] str_json: {
   "result" : "true"
}

2020-05-02 17:59:32,670 INFO  [default] root["result"] = "true"

构建嵌套对象,第一个根“名称/值”对中的“值”又是一个对象。

Json::Value root;
Json::Value value;
value["cpu_name"] = "arm";
root["cpu_info"] = value;

Json::StyledWriter styled_writer;
std::string str_json = styled_writer.write(root);
LOG(INFO) << "str_json: " << str_json.c_str();

if (root["cpu_info"].isMember("cpu_name"))
{
    LOG(INFO) << "root[\"cpu_info\"][\"cpu_name\"] = " << root["cpu_info"]["cpu_name"];
}

输出的日志信息如下所示

2020-05-02 17:59:32,670 INFO  [default] str_json: {
   "cpu_info" : {
      "cpu_name" : "arm"
   }
}

2020-05-02 17:59:32,670 INFO  [default] root["cpu_info"]["cpu_name"] = "arm"

构建三层嵌套对象,第一个根“名称/值”对中的“值”是一个对象,而该对象的“值”又是一个对象。依次类推,可以构建更多层的嵌套对象。

Json::Value root;
Json::Value value_01;
Json::Value value_02;

value_02["cell_number"]  = 255;
value_01["eye"] = value_02;
root["body"] = value_01;

Json::StyledWriter styled_writer;
std::string str_json = styled_writer.write(root);
LOG(INFO) << "str_json: " << str_json.c_str();

if (root["body"]["eye"].isMember("cell_number"))
{
    LOG(INFO) << "root[\"body\"][\"eye\"][\"cell_number\"]  = " << root["body"]["eye"]["cell_number"];
}

输出的日志信息如下所示

2020-05-02 17:59:32,670 INFO  [default] str_json: {
   "body" : {
      "eye" : {
         "cell_number" : 255
      }
   }
}

2020-05-02 17:59:32,670 INFO  [default] root["body"]["eye"]["cell_number"]  = 255

构建简单的数组,jsoncpp中构建数组是通过append的接口来创建的。读取数组之前,先调用isArray来判断对象是否为数组,如果是的话,再读取输出。这里需要注意数组的个数。从防御式编程的角度看,读取数组值之前,需要判断数组索引是否在有效范围内。

Json::Value array;
array.append("one");
array.append("two");

Json::StyledWriter styled_writer;
std::string str_json = styled_writer.write(array);
LOG(INFO) << "str_json: " << str_json.c_str();

if (array.isArray())
{
    LOG(INFO) << "array.size(): " << array.size();
    LOG(INFO) << "array[0]: " << array[0];
    LOG(INFO) << "array[1]: " << array[1];
}

输出的日志信息如下所示,从这里我们也可以确定数组是可以单独作为独立json串出现的。之前一直都有一个误区,就是认为json一定要用大括号包括起来。

2020-05-02 17:59:32,670 INFO  [default] str_json: [ "one", "two" ]

2020-05-02 17:59:32,671 INFO  [default] array.size(): 2
2020-05-02 17:59:32,671 INFO  [default] array[0]: "one"
2020-05-02 17:59:32,671 INFO  [default] array[1]: "two"

构建对象和数组组成的json。首先创建一个数组,然后将其作为对象的值。

Json::Value array;
array.append("one");
array.append("two");
array.append("three");

Json::Value root;
root["number"] = array;

Json::StyledWriter styled_writer;
std::string str_json = styled_writer.write(root);
LOG(INFO) << "str_json: " << str_json.c_str();

if (root["number"].isArray())
{
    LOG(INFO) << "root[\"number\"].size(): " << root["number"].size();
    LOG(INFO) << "root[\"number\"][0]: " << root["number"][0];
    LOG(INFO) << "root[\"number\"][1]: " << root["number"][1];
    LOG(INFO) << "root[\"number\"][2]: " << root["number"][2];
}

输出的日志信息如下所示

2020-05-02 17:59:32,671 INFO  [default] str_json: {
   "number" : [ "one", "two", "three" ]
}

2020-05-02 17:59:32,671 INFO  [default] root["number"].size(): 3
2020-05-02 17:59:32,671 INFO  [default] root["number"][0]: "one"
2020-05-02 17:59:32,671 INFO  [default] root["number"][1]: "two"
2020-05-02 17:59:32,671 INFO  [default] root["number"][2]: "three"

最后再构建稍微复杂一点的json串,它是由对象、数组、对象来组成的,即对象的值是一个数组,而数组内部的值是由对象组成。

Json::Value root;
Json::Value array;
Json::Value value_01;
Json::Value value_02;

value_01["peripheral"] = 1;
value_01["patient"] = 2;

value_02["image"] = 3;
value_02["auto"] = 4;

array.append(value_01);
array.append(value_02);

root["department"] = array;

Json::StyledWriter styled_writer;
std::string str_json = styled_writer.write(root);
LOG(INFO) << "str_json: " << str_json.c_str();

if (root["department"].isArray())
{
    LOG(INFO) << "root[\"department\"].size(): " << root["department"].size();
    LOG(INFO) << "root[\"department\"][0][\"patient\"]: " << root["department"][0]["patient"];
    LOG(INFO) << "root[\"department\"][1][\"auto\"]: " << root["department"][1]["auto"];
}

输出的日志信息如下所示

2020-05-02 17:59:32,671 INFO  [default] str_json: {
   "department" : [
      {
         "patient" : 2,
         "peripheral" : 1
      },
      {
         "auto" : 4,
         "image" : 3
      }
   ]
}

2020-05-02 17:59:32,671 INFO  [default] root["department"].size(): 2
2020-05-02 17:59:32,671 INFO  [default] root["department"][0]["patient"]: 2
2020-05-02 17:59:32,671 INFO  [default] root["department"][1]["auto"]: 4

五、使用心得

  1. 读取json值之前,先判断其有效性,可以结合断言机制,调用isMember或者isArray来进行判断。
  2. 使用数组的时候,需要特别注意数组下标。

六、总结

json主要是由对象或数组创建而成,而它们的嵌套使用就可以创建复杂的json串,根据特定场景的需求来创建适用的json格式。

基于面向对象的思想来使用结构体,将会有意想不到的效果

程序开发过程中,很多人都会接触到客户服务端模型,通常客户服务端模型是基于socket的网络通信,而网络通信是需要定义通信协议,通信协议结构一般是用结构体的方式来表示,而数据内容有的可能会使用json格式,对于嵌入式设备,数据内容更多的还是采用结构体的方式来表示。

本文首先会基于Qt提供的socket接口来实现一个简单的客户服务端模型,主要是为后面数据内容采用结构体通信的说明提供基础。接着定义通信协议结构体,然后再说明C语言方式使用结构体的方法,再介绍基于面向对象的思想来使用结构体,从而体会两者方式之间的区别,最后再介绍如何采用模版方式来更好的获取结构格式不定的数据内容。

一、客户服务端模式

客户服务端模式的机制是,服务端启动监听端口来等待客户端的连接,客户端创建socket启动连接,服务端成功接收到连接之后,等待客户端发送数据,客户端开始发送数据,服务端接收到数据,并进行解析处理。

下面会基于QT提供的socket接口来实现简单的客户服务端模型,实现之前需要在pro文件中添加network库的支持。

QT       += core gui network

1、定义实现简单的服务端类JTcpServer, 首先构造函数创建QTcpServer对象用来启动监听等待新的连接,当有新的连接请求的时候,则通过QTcpServer提供的接口nextPendingConnection来返回连接成功的socket, 然后等待客户端发送数据,如果有可读数据,那么读取数据进行处理。

// 定义服务端类
class JTcpServer : public QObject
{
    Q_OBJECT
public:
    JTcpServer();
    ~JTcpServer();

    void Start();

public slots:
    void AcceptConnection();
    void ReadClient();

private:
    QTcpServer* m_pTcpServer;
    QTcpSocket *m_pClientConnection;
};

// 实现服务端类
JTcpServer::JTcpServer()
    : QObject(nullptr)
{
    LOG(INFO) << " contructor";
    m_pTcpServer = new QTcpServer(this);
}

JTcpServer::~JTcpServer()
{
    LOG(INFO) << " decontructor";
    m_pTcpServer->close();
}

void JTcpServer::AcceptConnection()
{
    LOG(INFO) << "receive new connection";
    m_pClientConnection = m_pTcpServer->nextPendingConnection();
    if (m_pClientConnection->waitForReadyRead())
    {
        ReadClient();
    }
}

void JTcpServer::ReadClient()
{
    QString str = m_pClientConnection->readAll();
    LOG(INFO) << "str: " << str.toStdString().c_str();
}

void JTcpServer::Start()
{
    LOG(INFO) << "start tcp server";

    m_pTcpServer->listen(QHostAddress::Any, 9999);
    if (m_pTcpServer->waitForNewConnection(500000))
    {
        AcceptConnection();
    }

    LOG(INFO) << "end tcp server";
}

2、定义实现简单的客户端类,构造函数创建QTcpSocket用来连接服务端,并且发送数据。

// 定义客户端类
class JTcpClient : public QObject
{
    Q_OBJECT
public:
    JTcpClient();
    ~JTcpClient();

    void Start();

private:
    QTcpSocket* m_pclientSocket;
};

// 实现服务端类
JTcpClient::JTcpClient()
    : QObject(nullptr)
{
    LOG(INFO) << " contructor";
    m_pclientSocket = new QTcpSocket(this);
}

JTcpClient::~JTcpClient()
{
    LOG(INFO) << " decontructor";
    m_pclientSocket->close();
}

void JTcpClient::Start()
{
    LOG(INFO) << "start tcp client";

    m_pclientSocket->connectToHost(QHostAddress("127.0.0.1"), 9999);
    char ac_data[512] = {0};
    std::memcpy(ac_data, "hello everyone!", sizeof("hello everyone!"));
    m_pclientSocket->write(ac_data);
    m_pclientSocket->waitForBytesWritten();

    LOG(INFO) << "end tcp client";
}

3、完成客户端和服务端的实现,启动两个分离线程来分别执行客户端和服务端代码

// 启动服务端
std::thread thread_server( [&]{
    JTcpServer *p_tcp_server = new JTcpServer();
    p_tcp_server->Start();
} ) ;

std::this_thread::sleep_for(std::chrono::seconds(1));

// 启动客户端
std::thread thread_client( [&]{
    JTcpClient *p_tcp_client = new JTcpClient();
    p_tcp_client->Start();
} ) ;

if (thread_server.joinable())
{
    thread_server.detach();
}

if (thread_client.joinable())
{
    thread_client.detach();
}

4、启动运行之后,可以看到服务端成功打印了客户端发送的数据,这说明客户端和服务端之间是能够通信的。

[void JTcpServer::ReadClient():60] str: hello everyone!

二、通信协议

定义客户端和服务端的通信协议,它包括帧号,该帧号具有唯一性;帧类型,根据具体业务场景进行定义,比如命令帧、结果帧等;帧的来源表示帧的发送者; 帧的目的表示帧的接受者;数据帧长度则存储数据内容的具体长度; 数据则存放不定长度的数据内容。

三、基于C语言方式的结构体

基于C语言方式定义通信协议的结构体

typedef struct CFrame
{
    int iId;
    int iType;
    int iFrom;
    int iTo;
    int iDataLen;
    char data[0];
}CFRAME;

再定义数据内容的结构体

typedef struct CParam
{
    int iParam;
    char acInfo[32];
}CPARAM;

客户端构建数据,并发送。首先malloc申请内存,然后填充帧头和数据内容,最后发送数据,再free释放内存。

CFRAME *p_frame = nullptr;
p_frame = (CFRAME *)malloc(sizeof(CFRAME) + sizeof(CPARAM));
memset(p_frame, 0x00, sizeof(CFRAME) + sizeof(CPARAM));
p_frame->iId = 111;
p_frame->iType = 3;
p_frame->iFrom = 1;
p_frame->iTo = 2;
p_frame->iDataLen = sizeof(CPARAM);
CPARAM param;
param.iParam = 400;
memcpy(param.acInfo, "happy.", sizeof("happy."));
memcpy(p_frame->data,  &param, sizeof(CPARAM));

int i_write_len = m_pclientSocket->write((char *)p_frame, sizeof(CFRAME) + sizeof(CPARAM));
LOG(INFO) << "i_write_len: " << i_write_len;
m_pclientSocket->waitForBytesWritten();

free(p_frame);

服务端接收数据,并解析。服务端接收全部数据,然后解析并打印出来。由于TCP是流的方式,一般来说,先解析帧头,再解析帧数据,但是这里暂时不需要关注,所以没有考虑。

QByteArray array = m_pClientConnection->readAll();
LOG(INFO) << "array.size(): " << array.size();

CFRAME *p_frame = reinterpret_cast<CFRAME *>(array.data());
if (!p_frame)
{
    LOG(INFO) << "p_frame nullptr";
}

LOG(INFO) << "p_frame->iId: " << p_frame->iId;
LOG(INFO) << "p_frame->iType: " << p_frame->iType;
LOG(INFO) << "p_frame->iFrom: " << p_frame->iFrom;
LOG(INFO) << "p_frame->iTo: " << p_frame->iTo;
LOG(INFO) << "p_frame->iDataLen: " << p_frame->iDataLen;

CPARAM *p_param = reinterpret_cast<CPARAM *>(p_frame->data);
if (p_param)
{
    LOG(INFO) << "p_param->iParam: " << p_param->iParam;
    LOG(INFO) << "p_param->acInfo: " << p_param->acInfo;
}

四、基于面向对象的结构体

上面的方式是基于基于C语言的方式来使用结构体,接下来就来说明如何基于面向对象的方式来使用结构体。

基于面向对象方式定义通信协议的结构体

struct CXXFrame
{
    int iId;
    int iType;
    int iFrom;
    int iTo;
    int iDataLen;
    char data[0];
    CXXFrame()
        : iId(1)
        , iType(0)
        , iFrom(1)
        , iTo(2)
        , iDataLen(0)
    {}
};

基于面向对象方式定义数据内容结构体

struct JParam
{
    int iParam;
    char acInfo[32];
    JParam()
        : iParam(1)
    {
        memset(acInfo, 0x00, sizeof(acInfo));
    }
};

构建客户端数据,并且发送数据。为了避免申请内存而忘记释放,这里使用std::vector来定义数组来存储发送的数据,这样就避免忘记释放内存。

CXXFrame *p_frame = nullptr;
int i_frame_len = sizeof(CXXFrame) + sizeof(JParam);
std::vector<char> vec_frame(i_frame_len, 0);
p_frame =  reinterpret_cast<CXXFrame *>(vec_frame.data());
p_frame->iId = 111;
p_frame->iType = 3;
p_frame->iFrom = 1;
p_frame->iTo = 2;
p_frame->iDataLen = sizeof(JParam);
JParam *p_param = reinterpret_cast<JParam *>(p_frame->data);
p_param->iParam = 400;
memcpy(p_param->acInfo, "happy!", sizeof("happy!"));
m_pclientSocket->write(vec_frame.data(), vec_frame.size());
m_pclientSocket->waitForBytesWritten();

服务端接收数据,并且解析数据。

QByteArray array = m_pClientConnection->readAll();
LOG(INFO) << "array.size(): " << array.size();

CXXFrame *p_frame = reinterpret_cast<CXXFrame *>(array.data());
if (!p_frame)
{
    LOG(INFO) << "p_frame nullptr";
}

LOG(INFO) << "p_frame->iId: " << p_frame->iId;
LOG(INFO) << "p_frame->iType: " << p_frame->iType;
LOG(INFO) << "p_frame->iFrom: " << p_frame->iFrom;
LOG(INFO) << "p_frame->iTo: " << p_frame->iTo;
LOG(INFO) << "p_frame->iDataLen: " << p_frame->iDataLen;

JParam *p_param = reinterpret_cast<JParam *>(p_frame->data);
if (p_param)
{
    LOG(INFO) << "p_param->iParam: " << p_param->iParam;
    LOG(INFO) << "p_param->acInfo: " << p_param->acInfo;
}

五、模版方式获取数据内容

服务器接收数据的内容的长度是不确定,不同的应用场景对应不同的结构体。为了统一、并且简化代码量。可以实现模版方法来获取数据内容。

template<typename T>
T* GetFrameParam(CXXFrame *pFrame)
{
    return reinterpret_cast<T*>(pFrame->data);
}

那么服务端接收数据内容,就可以调用模版方法。

QByteArray array = m_pClientConnection->readAll();
LOG(INFO) << "array.size(): " << array.size();

CXXFrame *p_frame = reinterpret_cast<CXXFrame *>(array.data());
if (!p_frame)
{
    LOG(INFO) << "p_frame nullptr";
}

LOG(INFO) << "p_frame->iId: " << p_frame->iId;
LOG(INFO) << "p_frame->iType: " << p_frame->iType;
LOG(INFO) << "p_frame->iFrom: " << p_frame->iFrom;
LOG(INFO) << "p_frame->iTo: " << p_frame->iTo;
LOG(INFO) << "p_frame->iDataLen: " << p_frame->iDataLen;

//JParam *p_param = reinterpret_cast<JParam *>(p_frame->data);
JParam *p_param = GetFrameParam<JParam>(p_frame);
if (p_param)
{
    LOG(INFO) << "p_param->iParam: " << p_param->iParam;
    LOG(INFO) << "p_param->acInfo: " << p_param->acInfo;
}

六、总结

本文通过实现一个简单的客户服务端模型来说明C语言定义的结构体和面向对象定义的结构体的区别,通过面向对象定义的结构体可以在构造函数中初始化数据成员,这样就不必每次定义对象,还需要使用memset来初始化结构体,同时也避免了忘记初始化结构体的可能性。另外一个,我觉得可以很好简化代码量,并且让代码统一的一个点就是,使用模版方法来解析帧的数据内容。除了解析帧的数据内容之外,还可以通过模版方法来构建帧的数据内容,这样不同结构体,只需要一个模版方法就可以解决。所以,使用面向对象的设计语言的时候,尽量用面向对象的设计思想来开发,很多时候可以简化代码,甚至简化代码的逻辑。

还在为频繁变动的需求而苦恼吗?学会这个原则,让你从容应对

工作过程中,开发人员根据需求文档完成程序的开发任务,但是,程序投入测试使用的时候,经常因为各种各样的原因,比如,用户体验不好、操作不方便等,需要变动需求,甚至添加新功能,这可能就会导致开发人员原来设计的方案不能满足新变动的需求。

那么,如何应对频繁变动的需求呢,那么就需要本文将要介绍的原则登场了,即开闭原则。

介绍开闭原则之前,首先会结合例子来讲解C++提供的std::sort排序函数的用法,主要是为辅助后续要说明的开闭原则的示例,然后介绍开闭原则的两个特性,接着再叙述开闭原则常用应用场景,最后会详细介绍一个应用开闭原则的经典例子,该例子可以细细推敲,相信对加深开闭原则的理解。

一、排序函数的用法

1、std::sort的一般用法

首先创建测试存储整数类型向量,然后写入乱序的整数数据。

std::vector<int> JDebugSort::Build()
{
    std::vector<int> vec_data;
    vec_data.push_back(2);
    vec_data.push_back(1);
    vec_data.push_back(3);
    vec_data.push_back(4);
    return vec_data;
}

调用std::sort对上面创建的向量变量进行排序。

std::vector<int> vec_data_01 = Build();
std::sort(vec_data_01.begin(), vec_data_01.end());
Print(vec_data_01);

运行程序,依次输出向量存储的数据如下图所示,可以看出std::sort默认按照升序进行排列。

如果想要降序排列,怎么办呢,首先需要自定义比较函数,具体实现如下所示

bool compare(int a, int b)
{
    return (a > b);
}

同样调用std::sort对上面的向量进行排序,但是std::sort的第三个参数为上面定义实现的比较函数compare。

std::vector<int> vec_data_02 = Build();
std::sort(vec_data_02.begin(), vec_data_02.end(), compare);
Print(vec_data_02);

再次运行程序,其输出的信息如下,可以看出std::sort已经按照降序进行排列。注意如果将compare内部实现使用的大于号(>)修改为小于号(<), 那么就会变成升序排序。

除了自定义比较函数来决定std::sort的排序顺序之外,如果排序的的类型是普通数据类型, 比如整数类型,那么可以直接使用标准库使用提供的函数std::less或者std::greater来决定是升序还是降序排列。

从输出的打印信息看,std::less是升序排列,std::greater是降序排列。

2、类内部重载

上面讲述的是std::sort的一般用法,接下来将讲解类内部重载operator<来控制升降序。假设需要对部门的ID进行排序,定义实现如下所示的部门类,该类主要存储部门id和部门的名称,并且重载了operator<运算符。

class Department
{
public:
    explicit Department(int id, const std::string &name)
        : m_id(id)
        , m_name(name)
    {}
    Department(){}
    ~Department(){}

    bool operator<(const Department& deparment) const
    {
        //return m_id < deparment.m_id; // 升序
        return m_id > deparment.m_id;  // 降序
    }

    int GetId() const { return m_id;}
    std::string GetName() const {return m_name;}

private:
    int m_id;
    std::string m_name;
};

为了验证效果,首先定义存储Department类型的向量,同样存入部门id号为乱序的部门信息

std::vector<Department> JDebugSort::BuildDeparment()
{
    std::vector<Department> vec_data;
    vec_data.push_back(Department(3,"li"));
    vec_data.push_back(Department(2,"zheng"));
    vec_data.push_back(Department(5,"xxx"));

    return vec_data;
}

调用std::sort对上面的Department类型的向量进行排序

std::vector<Department> vec_data_05 = BuildDeparment();
std::sort(vec_data_05.begin(), vec_data_05.end());
PrintDepartment(vec_data_05);

运行打印结果如下所示,输出的部门信息按照部门id进行降序排列。

3、自定义比较类

除了自定义函数来确定升降序之外,还可以自定义类来确定升降序,而自定义类需要重载operator()运算符。这里还是采用上面的部门类来进行说明。operator()运算符传入两个表示部门信息的参数,函数内部还是通过部门id号来确定升降序。

class JLess
{
public:
    bool operator()(const Department& d1, const Department& d2)
    {
        return d1.GetId() < d2.GetId(); //升序排列
    }
};

调用方法如下所示,从实际的测试结果看,std::sort会优先调用operator()中定义排序顺序,不管自定义类中是否重载了operator<运算符。

std::vector<Department> vec_data_06 = BuildDeparment();
std::sort(vec_data_06.begin(), vec_data_06.end(), JLess());
PrintDepartment(vec_data_06);

还可以定义如下所示的自定义比较类,还是内部直接通过部门对象进行比较,实际上最终是调用部门类重载的operator<运算符来确定排序顺序的。

class JLess2
{
public:
    bool operator()(const Department& d1, const Department& d2)
    {
        return d1 < d2;
    }
};

二、开闭原则的特性

开闭原则意思就是可以扩展,但是又不能修改。体现在代码上就是添加新的代码,但是不需要改变已经运行的代码。概况来说,它的两个基本特性是:1)、对于扩展是开放的,2)、对于更改是封闭的。

那么如何在不改变模块原有的代码的情况下,添加新的功能点呢?

三、开闭原则的应用

关键是抽象,即有一个抽象的基类,而可能变动的行为则由派生类来实现。

客户端与服务端的通信。client类使用的是抽象类client interface,  而实际功能由server去实现,当使用的时候,创建具体的server对象,然后将其传递给client对象,如果希望client类使用不同的server类,那么只要新的server类是从client interface类派生出来,那么新的server对象就可以传递给client对象,而且client类不需要进行任何修改。

上面的例子是遵循开闭原则,而另一个比较常见并且遵循开闭原则的是模版方法,简单来说就是,基类实现基本通用的逻辑,并且该逻辑过程包含虚函数或纯虚函数,而虚函数或者纯虚函数的具体功能则由派生子类来实现。例如,下图的模版方法,TemplateMethod是实现通用的逻辑,primitive1和primitive2则是虚函数或纯虚函数,需要子类SubClass1和SubClass2来实现。

四、经典示例

现在需要制作一个绘制正方形和圆形的应用程序,并且按照指定顺序进行绘制。那么如何实现才能遵循开闭原则呢。

根据前面介绍的std::sort用法,如果需要按照指定顺序来绘制图形,那么可以利用std::sort函数,并且自定义比较类模版,重载operator<运算符。

自定义比较类模版如下,该模版类重载operator()运算符。

template <typename T>
class Less
{
public:
    bool operator()(const T t1, const T t2)
    {
        return (*t1) < (*t2);
    }
};

定义形状基类,Draw是纯虚函数,需要子类实现,重载运算符operator<主要是为了控制绘制形状的顺序。静态成员变量m_OrderTable存放绘制形状顺序的名称。

class Shape
{
public:
    Shape();
    virtual ~Shape();
    virtual void Draw() = 0;

    bool Precedes(const Shape&) const;
    bool operator<(const Shape&) const;
private:
    static const char* m_OrderTable[];
};

实现形状基类,operator<运算符内部调用Precedes,Precedes实现升序绘制形状。m_OrderTable的赋值需要实现具体子类之后才能给出。

Shape::Shape(){}

Shape::~Shape(){}


bool Shape::Precedes(const Shape& s) const
{
    const char * this_type = typeid(*this).name();
    const char * arg_type = typeid(s).name();
    int i_thisord = -1;
    int i_argord = -1;

    int i_size = sizeof(m_OrderTable)/sizeof(m_OrderTable[0]);
    for(int i = 0; i < i_size; i++)
    {
        const char* p_table_entry = m_OrderTable[i];
        if (p_table_entry != nullptr)
        {
            if (strcmp(p_table_entry, this_type) == 0)
            {
                i_thisord = i;
            }
            if (strcmp(p_table_entry, arg_type) == 0)
            {
                i_argord = i;
            }
            if (i_thisord >= 0 && i_argord >= 0)
            {
                break;
            }
        }
    }
    return i_thisord < i_argord;
}

bool Shape::operator<(const Shape& s) const
{
    return Precedes(s);
}

定义实现正方形

/// 定义
class Square : public Shape
{
public :
    Square();
    virtual ~Square() override;
    virtual void Draw() override;
};

/// 实现
Square::Square(){}

Square::~Square(){}

void Square::Draw()
{
    LOG(INFO) << "draw Square";
}

定义实现圆形

/// 定义
class Circle : public Shape
{
public :
    Circle();
    virtual ~Circle() override;
    virtual void Draw() override;
};

/// 实现
Circle::Circle(){}

Circle::~Circle(){}

void Circle::Draw()
{
    LOG(INFO) << "draw Circle";
}

实现完成正方形和圆形之后,就可以给m_OrderTable赋值,其先后顺序就确定了对应形状的绘制顺序。

const char* Shape::m_OrderTable [] =
{
    typeid(Circle).name(),
    typeid (Square).name()
};

实现绘制所有形状的逻辑,函数DrawAllShape接受存储类型为Shape*的向量,内部实现如下所示,调用std::sort对向量进行排序,然后再循环调用向量中的每一个对象的Draw进行绘制。

void JDebugOCP::DrawAllShape(std::vector<Shape*> &allShape)
{
    std::vector<Shape*> order_all_shape = allShape;
    std::sort(order_all_shape.begin()
             ,order_all_shape.end()
             ,Less<Shape*>());

     std::vector<Shape*>::const_iterator iter_const;
     for(iter_const = order_all_shape.begin(); iter_const != order_all_shape.end(); iter_const++)
     {
         (*iter_const)->Draw();
     }
}

最后使用的方式如下,创建存储各个形状的的向量,并且创建的形状不需要按照顺序。再将其向量传入上面定义实现的函数DrawAllShape。

std::vector<Shape*> vec_shape;
vec_shape.push_back(new Square());
vec_shape.push_back(new Circle());

DrawAllShape(vec_shape);

运行的结果如下,程序按照m_OrderTable赋值的顺序绘制图形。并且后续添加新的形状,并且指定输出顺序,那么也只要调整驱动表m_OrderTable即可,其他代码都不需要改变,这也满足了开闭原则。

[virtual void Circle::Draw():66] draw Circle
[virtual void Square::Draw():57] draw Square

五、总结

std::sort默认按照升序进行排列,如果重载使用大于号,那么按照降序排列,如果使用小于号,那么按照升序排列。开发过程中,遵循开闭原则能够有效解决预防频繁变动的需求,开闭原则的特性就是:对于扩展是开放的,对于更改是封闭的。开闭原则的关键就是抽象,抽象体现在C++就是虚函数或者纯虚函数。

没想到bind的功能这么强大,赶紧来看看

std::bind是C++11中一个函数模版,就像函数适配器,接受一个可调用对象(callable object),生成一个新的可调用对象。通过它,我们可以实现类似传统的函数指针,函数回调等功能,并且能够降低代码的复杂度。

本文首先详细说明std::bind的基本用法以及解释使用过程中疑问点,然后再介绍如何利用传统函数指针搭建基础结构,再说明如何用std::bind来代替函数指针,最后介绍如何用std::bind来实现函数回调的功能。

一、std::bind的基本用法

首先看下std::function, 它就是std::bind返回的新的可调用对象。如下图,定义实现了普通加法函数Add,  然后将该函数指针赋值给std::function类型的变量,这里可以注意到,使用了Add和&Add进行赋值。两者是等效的,这是因为使用Add的时候,会隐式转换成函数指针。

static int Add(int a, int b)
{
    return (a+b);
}

std::function<int (int, int)> fun = Add;
std::function<int (int, int)> fun2 = &Add;
LOG(INFO) << "fun(1, 1):"<< fun(1, 1);
LOG(INFO) << "fun2(1, 2):"<< fun2(1, 2);

运行程序之后的输出信息,可以看出std::function类型的变量的使用与普通函数的使用是一样的。

[2020-01-05 17:43:05,243189] [void JDebugBind::StartDebug():184] fun(1, 1):2
[2020-01-05 17:43:05,243206] [void JDebugBind::StartDebug():185] fun2(1, 2):3

我们不直接采用普通函数对std::function进行赋值,而是采用stb::bind,首先看下简单的实例,其中std::placeholders::_1和std::placeholders::_2是占位符,代表函数的入参。如果调用的时候,需要传递具体实参,那么就需要使用placeholders来占位。这里需要注意std::placeholders::_1并不是代表函数的第一个入参数,至于为什么,请继续往下阅读,下面将会通过实例进行阐述。

std::function<int (int, int)> fun3 = std::bind(Add, std::placeholders::_1, std::placeholders::_2);
std::function<int (int, int)> fun4 = std::bind(&Add, std::placeholders::_1, std::placeholders::_2);

LOG(INFO) << "fun3(1, 3):"<< fun3(1, 3);
LOG(INFO) << "fun4(1, 4):"<< fun4(1, 4);

如果函数的第二个入参是一个固定值,那么第一个入参就需要使用占位符std::placeholders::_1,如下所示,函数第二个参数固定位数值5,那么使用std::function类型变量的时候,也只需要传递一个参数,该参数代表Add函数的第一个参数。

std::function<int (int)> fun5 = std::bind(Add, std::placeholders::_1, 5);
LOG(INFO) << "fun5(1):"<< fun5(1);

如果Add函数的第一个入参是一个固定值,那么第二个入参就需要使用占位符std::placeholders::_1(注意不是std::placeholders::_2),如下所示,函数第一个参数固定位数值6,那么使用std::function类型变量的时候,也只需要传递一个参数,该参数代表Add函数的第二个参数。

std::function<int (int)> fun6 = std::bind(Add, 6, std::placeholders::_1);
LOG(INFO) << "fun6(1):"<< fun6(1);

当然,如果函数Add的两个参数都是固定值,那么使用std::function类型变量的时候,就不需要参数了。

std::function<int()> fun7 = std::bind(Add, 3, 7);
LOG(INFO) << "fun7():"<< fun7();

这里有个小技巧,如果不想要书写std::function那么繁琐的信息表示,那么可以采用auto代替,但是注意不要滥用auto.

auto fun8 = std::bind(Add, std::placeholders::_1, std::placeholders::_2);
LOG(INFO) << "fun8(1, 8):"<< fun8(1,8);

二、std::bind的扩展

上面说明的是stb::bind使用普通函数的方法,那么如果是类的成员函数呢?应该如何使用呢?首先s td::bind的第一个参数是类成员函数指针,第二个参数为类对象的指针,其他的用法与使用普通函数的用法是一样的。

class JBindClass
{
public:
    int Multi(int a, int b)
    {
        return (a * b);
    }
};


JBindClass bind_class;
auto fun9 = std::bind(&JBindClass::Multi, &bind_class, std::placeholders::_1, std::placeholders::_2);
LOG(INFO) << "fun9(1, 9):"<< fun9(1,9);

std::bind参数值是默认按照值传递的,首先实现函数Print, 该函数的入参是一个引用,函数内部将参数自增1,然后输出打印信息。接着再通过输出std::bind使用前后日志信息来确认是否是按照值传递。

static void Print(int &value)
{
    value++;
    LOG(INFO) << value;
}

int i_value = 10;
LOG(INFO) << "before i_value:" << i_value;
std::function<void()> fun10 = std::bind(Print,i_value);
fun10();
LOG(INFO) << "after i_value:" << i_value;

从输入的打印信息看,std::bind使用前后的信息没有发生变化,说明std::bind是默认按照值传递的。

[void JDebugBind::StartDebug():207] before i_value:10
[void Print(int &):24] 11
[void JDebugBind::StartDebug():210] after i_value:10

如果想要按照引用来传递变量,应该如何操作呢,那么就是std::ref登场的时候,std::ref是用于包装引用传递的值。

LOG(INFO) << "before i_value:" << i_value;
std::function<void()> fun11 = std::bind(Print,std::ref(i_value));
fun11();
LOG(INFO) << "after i_value:" << i_value;

从输出打印信息看,采用std::ref传递变量之后,std::bind使用前后的信息发生变化了。

[void JDebugBind::StartDebug():213] before i_value:10
[void Print(int &):24] 11
[void JDebugBind::StartDebug():216] after i_value:11

另外补充一点,std::cref用于包装const引用传递的值。

static void Printc(const int &value)
{
    LOG(INFO) << value;
}

int i_value_c = 12;
std::function<void()> fun12 = std::bind(Printc,std::cref(i_value_c));
fun12();

三、传统函数指针

函数指针变量用于存储函数指针,以便后续的调用。有时候可以利用它实现多个消息对象的处理,并且一定程度满足开闭原则。

首先实现抽象基类JAbstractBaseTest,接着再实现继承JAbstractBaseTest的两个子类JObjA和JObjB

/// 基类
class JAbstractBaseTest
{
public:
    JAbstractBaseTest(){}
    virtual ~JAbstractBaseTest(){}

    virtual void run() = 0;

};

/// 子类JObjA
class JObjA: public JAbstractBaseTest
{
public:
    void run(){LOG(INFO) << "JObjA Run";}

    static JAbstractBaseTest* create_instance()
    {
        return new JObjA();
    }

};

/// 子类JObjB
class JObjB: public JAbstractBaseTest
{
public:
    void run(){LOG(INFO) << "JObjB Run";}

    static JAbstractBaseTest* create_instance()
    {
        return new JObjB();
    }
};

完成上面的测试类,接着实现基础的框架,定义函数指针CreateObj,该函数指针用于动态创建对象,然后再分别实现初始化创建对象的函数指针映射表以及通过id从映射表中获取函数对象的两个函数。

class JDebugMain
{
public:
    JDebugMain()
    {
        InitObj();
    }

    // 定义函数指针
    typedef JAbstractBaseTest* (*CreateObj)();
    
    enum E_OBJ_ID
    {
        E_OBJ_A,
        E_OBJ_B,
    };

    // 初始化创建对象的函数指针映射表
    void InitObj()
    {
         m_mapRegisterClass[E_OBJ_A] = &JObjA::create_instance;
         m_mapRegisterClass[E_OBJ_B] = &JObjB::create_instance;
    }

    // 通过id从映射表中获取函数对象
    JAbstractBaseTest* GetObj(E_OBJ_ID eObjId)
    {
        std::map<E_OBJ_ID,CreateObj>::iterator iter;
        iter = m_mapRegisterClass.find(eObjId);
        if (iter != m_mapRegisterClass.end())
        {
            return m_mapRegisterClass[eObjId]();
        }
        return nullptr;
    }

private:
    std::map<E_OBJ_ID, CreateObj> m_mapRegisterClass;
};

使用调用方式如下,通过id获取对象指针,然后执行对象的run函数。通过这样的方式,可以做到主体循环不变,如果需要添加新的对象处理,那么只要实现新的类,然后添加到映射表中即可。

JDebugMain debug_main;
JAbstractBaseTest* p_obj = debug_main.GetObj(JDebugMain::E_OBJ_A);
if (p_obj)
{
    p_obj->run();
    delete p_obj;
    p_obj = nullptr;
}

四、std::bind代替函数指针

std::bind和std::function的结合,可以实现函数指针的功能。通过using Funtor = std::function<void (void)>来实现类似函数指针的声明。其中Funtor表示std::function<void (void)>的别名。然后在初始化表函数InitTab中,通过使用std::bind将类的函数成员一一映射到map中。

/// 类定义
class JDebugBind
{
public:
    using Funtor = std::function<void (void)>;
    enum
    {
        E_TEST_FUN_01,
        E_TEST_FUN_02,
    };

    JDebugBind();
    /// 根据测试id来执行对应的测试函数
    void RunTest(int iType);

protected:
    void Test01();
    void Test02();

private:
    void InitTab();

private:
    std::map<int, Funtor> m_mapTab;
};

/// 类实现
JDebugBind::JDebugBind()
{
    InitTab();
}

void JDebugBind::InitTab()
{
    m_mapTab.clear();
    m_mapTab[E_TEST_FUN_01] = std::bind(&JDebugBind::Test01, this);
    m_mapTab[E_TEST_FUN_02] = std::bind(&JDebugBind::Test02, this);
}

void JDebugBind::RunTest(int iType)
{
    std::map<int, Funtor>::iterator iter;
    for(iter = m_mapTab.begin(); iter != m_mapTab.end(); iter++)
    {
        if (iType == iter->first)
        {
            iter->second();
        }
    }
}

void JDebugBind::Test01()
{
    LOG(INFO) << "Test01";
}

void JDebugBind::Test02()
{
    LOG(INFO) << "Test02";
}

调用JDebugBind的方式如下,只需要传递函数的id给函数RunTest,即可执行到对应的函数。同样的,后续如果想要添加新的功能,那么只要实现新的函数,并且将其添加到map中即可。

JDebugBind debug_bind;
debug_bind.RunTest(JDebugBind::E_TEST_FUN_01);

五、std::bind实现函数回调

函数回调在编程实现是一个特别重要的特性,它经常会在一些架构中使用到。而std::bind是可以实现函数回调的特性的。下图实现的类JDebugCallback中,构造函数接受一个类型为std::function的参数之后,将其赋值给类的成员函数m_callback,后续调用函数Start的时候,Start函数内部再调用m_callback,从而实现函数回调。这里只是一个简单的例子说明,可能还不能充分看到函数回调的强大。希望这里作为一个引入,后续在实际工作中,再慢慢的体会。

class JDebugCallback
{
public:
    JDebugCallback(std::function<void()> callback)
        : m_callback(callback)
    { }

    void Start()
    {
        m_callback();
    }

private:
    std::function<void()> m_callback;
};

最后看下怎么使用JDebugCallback类,实现类两个函数CallBack01和CallBack02,然后通过std::bind传递给JDebugCallback,接着JDebugCallback对象调用Start来执行传递进来的函数。

static void CallBack01()
{
    LOG(INFO) << "CallBack01";
}

static void CallBack02()
{
    LOG(INFO) << "CallBack02";
}

JDebugCallback debug_cb_01(std::bind(CallBack01));
debug_cb_01.Start();

JDebugCallback debug_cb_02(std::bind(CallBack02));
debug_cb_02.Start();

五、总结

至此,C++11提供的std::bind的用法和扩展已经介绍完毕,虽然工作中有各种各样的需求场景,但是只要掌握了知识的基本原理,就能够以不变应万变。本文介绍了std::bind的各种基本应用场景,并结合了例子进行说明,相信应该已经说明白了。

解决内存管理问题的最佳利器-valgrind

Valgrind是一个动态分析工具,能够自动检测许多内存管理问题、线程bug,  并且能够分析程序的状况。它内部支持多个工具集,包括内存错误检测器,线程错误检测器,缓存分析器、堆分析器等,默认使用的是内存检测器(memcheck),  它是使用最多的一个内存检测工具。当然,你也可以基于Valgrind自己建立新的工具。

Valgrind支持的平台有:x86/Linux、AMD/64Linux、PPC32/Linux、PPC64LE/Linux、S390X/Linux、ARM/Linux(supported since ARMv7)、ARM64/Linux、MIPS32/Linux、MIPS64/Linux、X86/Solaris、 AMD64/Solaris、 X86/illumos、 AMD64/illumos、X86/Darwin (10.10, 10.11)、 AMD64/Darwin (10.10, 10.11)、ARM/Android、ARM64/Android、 MIPS32/Android、X86/Android

Valgrind是开源免费的软件,基于GNU General Public License, version 2.

一、快速入门

Valgrind工具集中最受欢迎的是memcheck,  它满足大部分的场景。memcheck能够检测内存相关的错误,并且是采用C/C++编译的程序,程序运行过程中奔溃或者不可预料的行为都可以使用Valgrind中的memcheck来进行检测。

使用Valgrind前,采用-g选项编译程序,这样memcheck才能够提取到具体的行号信息,同时可以使用-O0优化选项,但是如果使用-O1选项,那么显示的行号信息可能就不准确;不推荐使用-O2选项,如果使用的话,memcheck偶尔上报不是真的存在的未初始化的错误信息

命令行一般的使用格式如下所示,–leak-check=yes是打开内存泄露的检测器,

valgrind --leak-check=yes myprog arg1 arg2

下面提供一个C++例子,该例子有内存泄露和访问不存在地址的两个错误

#include <string>

void f(void)
{
    int* x = new int[10](); 
    x[10] = 0; // 访问不存在地址       
} // 内存泄露,没有释放内存                   

int main(void)
{
    f();
    return 0;
}

错误信息描述如下,表示访问不存在地址,第一行“Invalid write of size 4”表明什么类型错误,写数据到内存中,而该内存是不应该访问的。1066表示进程id号。如果错误的堆栈信息显示不够显示,那么可以加上选项–num-callers,再加上层级数量,比如–num-callers=20。

==1066== Invalid write of size 4
==1066==    at 0x100000F55: f() (example_02.cpp:6)
==1066==    by 0x100000F83: main (example_02.cpp:11)
==1066==  Address 0x100dea808 is 0 bytes after a block of size 40 alloc'd
==1066==    at 0x1000AC086: malloc (in /usr/local/Cellar/valgrind/3.15.0/lib/valgrind/vgpreload_memcheck-amd64-darwin.so)
==1066==    by 0x100179627: operator new(unsigned long) (in /usr/lib/libc++abi.dylib)
==1066==    by 0x100000F33: f() (example_02.cpp:5)
==1066==    by 0x100000F83: main (example_02.cpp:11)

内存泄露的错误信息提示描述如下, 它会告诉你内存分配的位置,但是它不能告诉你内存为什么泄露。

==1122== 40 bytes in 1 blocks are definitely lost in loss record 14 of 42
==1122==    at 0x1000AC086: malloc (in /usr/local/Cellar/valgrind/3.15.0/lib/valgrind/vgpreload_memcheck-amd64-darwin.so)
==1122==    by 0x100179627: operator new(unsigned long) (in /usr/lib/libc++abi.dylib)
==1122==    by 0x100000F33: f() (example_02.cpp:5)
==1122==    by 0x100000F83: main (example_02.cpp:11)

一般有几种内存泄露的类型,比较重要的两种是definitely lost和possibly lost,definitely lost是确定内存泄露,需要修复它,possibly lost可能存在内存泄露,需要仔细确认。

==1122== LEAK SUMMARY:
==1122==    definitely lost: 40 bytes in 1 blocks
==1122==    indirectly lost: 0 bytes in 0 blocks
==1122==      possibly lost: 72 bytes in 3 blocks
==1122==    still reachable: 200 bytes in 6 blocks
==1122==         suppressed: 18,127 bytes in 153 blocks
==1122== Reachable blocks (those to which a pointer was found) are not shown.
==1122== To see them, rerun with: --leak-check=full --show-leak-kinds=all

另外memcheck比较经常会上报没有初始化值的信息,但是要定位到错误信息的根本原因是比较困难的,对此,可以添加参数–track-origins=yes来获取更多的信息,但是,这样会使得memcheck运行的更慢。

Conditional jump or move depends on uninitialised value(s)

二、memcheck的错误信息

memcheck是内存错误的检测器,他可以检测C/C++常见的下列错误问题

  1. 访问不应该访问的内存,例如堆溢出、栈溢出、访问已经释放的内存
  2. 使用没有定义的值,例如值没有初始化
  3. 不正确的释放堆内存,例如重复释放内存,malloc/new/new[] 和 free/delete/delete[]没有一一对应使用
  4. 使用memcpy函数,源地址和目的地址重叠
  5. 向内存分配函数中,传递一个不正确的参数,例如负数
  6. 内存泄露
  • 非法读写错误,例如读取已经释放内存的地址,为了获取更多的信息,可以加上–read-var-info=yes的选项
==1178== Invalid read of size 16
==1178==    at 0x101321A50: qstricmp(char const*, char const*) (in /Users/lizijun/Qt5.13.0/5.13.0/clang_64/lib/QtCore.framework/Versions/5/QtCore)
==1178==    by 0x101539A81: QTimerInfoList::activateTimers() (in /Users/lizijun/Qt5.13.0/5.13.0/clang_64/lib/QtCore.framework/Versions/5/QtCore)
  • 使用没有定义的值,例如定义了变量,但是没有初始化,如果信息不够详细,可以添加参数–track-origins=yes来获取更多的信息
#include <string>
#include <iostream>

int main(void)
{
    int i_number;
    std::cout << i_number << std::endl;
    return 0;
}
==1189== Conditional jump or move depends on uninitialised value(s)
==1189==    at 0x1003D83C5: __vfprintf (in /usr/lib/system/libsystem_c.dylib)
==1189==    by 0x1003FF058: __v2printf (in /usr/lib/system/libsystem_c.dylib)
==1189==    by 0x1003E434A: _vsnprintf (in /usr/lib/system/libsystem_c.dylib)
==1189==    by 0x1003E43A7: vsnprintf_l (in /usr/lib/system/libsystem_c.dylib)
==1189==    by 0x1003D53B2: snprintf_l (in /usr/lib/system/libsystem_c.dylib)
==1189==    by 0x1000D4D22: std::__1::num_put<char, std::__1::ostreambuf_iterator<char, std::__1::char_traits<char> > >::do_put(std::__1::ostreambuf_iterator<char, std::__1::char_traits<char> >, std::__1::ios_base&, char, long) const (in /usr/lib/libc++.1.dylib)
==1189==    by 0x1000C8F27: std::__1::basic_ostream<char, std::__1::char_traits<char> >::operator<<(int) (in /usr/lib/libc++.1.dylib)
==1189==    by 0x100000D0D: main (example_03.cpp:7)
  • 非法释放地址,例如重复释放内存
#include <string>
#include <iostream>

int main(void)
{
    char *p_data = new char[64]();
    delete []p_data;
    delete []p_data;
    return 0;
}
==1212== Invalid free() / delete / delete[] / realloc()
==1212==    at 0x1000AC463: free (in /usr/local/Cellar/valgrind/3.15.0/lib/valgrind/vgpreload_memcheck-amd64-darwin.so)
==1212==    by 0x100000F7D: main (example_04.cpp:8)
==1212==  Address 0x100dea7e0 is 0 bytes inside a block of size 64 free'd
==1212==    at 0x1000AC463: free (in /usr/local/Cellar/valgrind/3.15.0/lib/valgrind/vgpreload_memcheck-amd64-darwin.so)
==1212==    by 0x100000F62: main (example_04.cpp:7)
==1212==  Block was alloc'd at
==1212==    at 0x1000AC086: malloc (in /usr/local/Cellar/valgrind/3.15.0/lib/valgrind/vgpreload_memcheck-amd64-darwin.so)
==1212==    by 0x100179627: operator new(unsigned long) (in /usr/lib/libc++abi.dylib)
==1212==    by 0x100000F2A: main (example_04.cpp:6)
  • 调用申请和释放内存的方法不匹配,例如malloc申请内存,但是使用delete来释放,对某些系统来说是不允许的,因此,为了保证程序健壮,使用malloc,那么对应使用free; 使用new,那么对应使用delete; 使用new [], 那么对应使用delete []。
Mismatched free() / delete / delete []

三、Valgrind调用QtCreator程序

mac系统通过QtCreator创建程序之后,也可以采用Valgrind在终端上检测QtCreator生成的程序。

首先进入QtCreator编译生成的文件目录

接着选择build开头的目录,右键弹出的列表选择“服务”->”新建位于文件夹位置的终端窗口”来启动终端,  终端输入如下所示的命令来使用Valgrind测试QtCreator编译生成的程序JQtTestStudy.app

四、局限性

  1. Memcheck并不完美,它也会出现误报,但是它有99%的准确性,对于它提示的信息我们应该警惕。
  2. memcheck不能检测每一种内存错误,比如它不能检测到对静态分配或堆栈上的数组的超出范围的读写,但是它还是能够检测出使得你程序奔溃的错误,例如段错误segmentation fault

    五、总结

程序开发过程中,可能会遇到崩溃的问题,如果代码量很多的时候,我们可能会使用gdb来查看coredump信息,但是有时候gdb的信息比较简单,没有更加详细的堆栈信息,那么就可以考虑使用Valgrind进行分析。最近,工作中遇到一个问题,程序运行过程中,会偶发崩溃问题,使用gdb查看coredump信息,显示是重复释放内存,但是堆栈信息很少,一直找不到位置,后来使用Valgrind来查看程序,仔细查看从Valgrind提供的堆栈信息,很快找到问题的位置,原因确实是重复释放内存。

温馨提示:Valgrind经常上报了很多错误提示信息,这个可能是同样一个地方调用了多次,所以,如果解决了一个地方的问题,错误提示信息就会全部消失,需要耐心仔细。

析构函数的妙用, 让你明白流方式输出日志的实现原理

学习面向对象(如C++编程语言),那么肯定了解析构函数,它在对象销毁的时候被调用,通常我们在构造函数中申请资源,在析构函数中释放资源。那么析构函数在实现以流方式输出日志中有什么妙用呢?接下来请让我一步步为你揭开这层迷雾。

C/C++语言日志输出模式一般有两种,一种类似printf的方式,另一种类似std::cout的方式,这里说的流方式输出日志指的就是类似std::cout的方式,并且自定义日志输出的格式,同时既可以将日志输出到终端,也可以将日志输出到文件。

printf("%s  %d \n", "this is my log", 1);
std::cout << "this is my log " << 1 << std::endl;

一、格式化字符串的输出流

C++语言提供了ostringstream模版,它支持格式化字符串输出流。

  • 首先让我们看看ostringstream的简单使用,定义ostringstream变量oss,然后将当前的线程id以十六进制的方式写入ostringstream变量,  再调用ostringstream的函数str(),将其转换为std::string字符串之后,打印输出到终端。
#include <sstream>

std::ostringstream oss;
oss << std::hex << std::this_thread::get_id();
LOG(INFO) << oss.str();

输出的信息如下所示,当前的线程id是以十六进制的格式输出。

[2019-11-30 22:03:50,124554] [bool JDebugCPPAttr::TestOstringstream():277] 0x7fff9e22c380
  • 上面是ostringstream的简单使用方法,那么下面将说明如何构造输出函数名称和行号的字符串。通过利用系统提供的宏定义__func__和__LINE__来构造所需字符串信息。
std::ostringstream oss2;
oss2 << "[" << __func__ << ":" << __LINE__ << "]";
std::cout << oss2.str() << std::endl;

从输出的格式内容看,ostringstream按照预期的效果输出了正确的字符串格式。

[TestOstringstream:281]

二、资源获取即初始化

RAII全称是“Resource Acquisition is Initialization”,资源获取即初始化”,简单来说,就是说在构造函数中申请分配资源,在析构函数中释放资源。经常使用的方式是:构造函数中通过new申请内存,析构函数中通过delete释放内存。

  • 基于RAII的思想,我们实现资源管理的管理类,管理类ResourceManager构造函数接受std::function类型的变量, 将其赋值给类的私有成员变量exit_handle,析构函数内调用exit_handle,  那么如果想要实现满足RAII, 那么只要构建释放资源的std::function类型的变量,然后传递给 ResourceManager。
class ResourceManager
{
public:
    explicit ResourceManager(std::function<void()> fun):exit_handle(fun)
    {
        std::cout << "call constructor" << std::endl;
    }
    
    ~ResourceManager()
    {
        std::cout << "call destructor " << std::endl;
        exit_handle();
    }
    
private:
    std::function<void()> exit_handle;
};

申请创建内存,然后再创建ResourceManager对象,构造函数的入参是一个匿名函数,函数的功能是释放创建的内存。

{
    int *p_data = new int();
    ResourceManager( [&]()
                   {
                       std::cout << "delete p_data" << std::endl;
                       delete p_data;
                   });
}

运行程序之后,输出打印信息

call constructor
call destructor 
delete p_data
  • 同样的方式,我们可以创建文件之后,再创建ResourceManager对象,构造函数的参数功能是释放文件句柄。
{
    std::ofstream ofs("test.txt");
    ResourceManager( [&]
                   {
                       std::cout << "close ofs" << std::endl;
                       ofs.close();
                   });
}

运行程序之后,输出打印信息

call constructor
call destructor 
close ofs
  • 从上面的两个例子中,可以看出都是利用对象在销毁时会调用析构函数的原理来实现,简单来说,申请资源之后,紧接着设置释放资源,等到申请的资源使用完成之后,资源管理对象在退出作用域之后,就会调用析构函数来释放资源,这样做的好处是,我们不必关注资源什么时候进行释放的问题,同时一定程度上也防止忘记释放资源。

三、利用析构函数来实现日志输出

结合std::ostringstream可以格式化输出流的功能和对象销毁时调用析构函数的原理,我们就可以实现自定义格式,并以流方式输出日志的功能。

  • 实现JWriter类来格式化日志信息并输出,这里我们只是简单输出到终端,当然,你也可以将自定义格式的日志信息写入文件或者写入队列,再由线程将队列中的日志信息写入文件。
  • JWriter类的构造函数接受三个参数:日志等级、函数名称、行号;并且重载了operator<<运算符
///类定义
class JWriter
{
public:
    explicit JWriter(const std::string &strLevel, const std::string &strFun, int iLine);
    ~JWriter();

    // 重载operator<<运算符
    template <typename T>
    inline JWriter& operator<<(const T& log) {
        m_log << log;
        return *this;
    }

private:
    std::string GetSysTimeToMs();

private:
    std::ostringstream m_log;
};


///类实现
#include <iostream>
#include <thread>
#include <chrono>
#include <sys/timeb.h>

JWriter::JWriter(const std::string &strLevel, const std::string &strFun, int iLine)
{
    m_log <<"["<< GetSysTimeToMs() << "]" << "[" << strFun << ":" << iLine << "]" << "[" << strLevel << "] ";
}


JWriter::~JWriter()
{
    m_log << std::endl;
    /// 这里可以实现将日志输出到终端或者写入文件
    std::cout << m_log.str();
}

std::string JWriter::GetSysTimeToMs()
{
    time_t timep;
    struct timeb tb;

    time (&timep);
    char tmp[128] ={0};
    strftime(tmp, sizeof(tmp), "%Y-%m-%d %H:%M:%S",localtime(&timep) );

    char tmp2[128] ={0};
    ftime(&tb);
    snprintf(tmp2,sizeof(tmp2),"%d",tb.millitm);

    std::ostringstream buffer;
    buffer << tmp << "." << tmp2 ;

    return buffer.str();
}

  • 那么如何来使用JWriter类,使用效果又是怎样呢?其实很简单,定义如下所示的宏,该宏只接受日志等级的字符串参数。
#define MyLogJ(LEVEL) JWriter(LEVEL, __func__, __LINE__)
  • 调用方式如下所示,它跟我们熟悉使用的std::cout的方式是一样一样的,只是std::cout换成了我们实现的MyLogJ()宏,因此,不存在需要花费时间来学习它的使用的问题。
MyLogJ("INFO") << "hello " << 123;
MyLogJ("INFO") << "hello " << " world";
  • 如下所示输出的效果,它首先输出日期时间,然后是函数名和对应行号以及日志等级,最后才输出用户输入的日志信息。这样的格式,通常是比较美观,并且利于问题的定位,当然,你也可以根据个人的喜好来修改JWriter的构造函数来自定义自己的日志格式。
[2019-12-01 10:26:00.657][TestMyLog:266][INFO] hello 123
[2019-12-01 10:26:00.657][TestMyLog:267][INFO] hello  world

四、总结

自定义日志格式并以流方式输出的功能已经介绍结束,它是利用了std::ostringstream可以格式化输出流的功能,并且在构造函数格式日志信息,析构函数最后处理日志信息,同时重载了operator<<运算符。

析构函数不只是用于释放资源,我们可以利用它的特性来做其他的运用,就如本文介绍的一样,利用了析构函数实现了流方式的日志功能,如果没有,单纯利用构造函数很难实现流方式的日志功能。当然,析构函数可能还有其他妙用,这需要我们不断去发掘。

 

结合生产消费者模式实现异步日志功能

软件程序开发过程中,日志是诊断bug必不可少的功能,日志功能通常是将每条日志信息按照一定的格式写入指定的文件,但是,实时将日志信息写入文件,必定耗费时间,对于性能要求比较高的机器来说,可能是无法接受的,并且由于时间差问题可能会带来无法预料的问题。

基于上面的原因,解决方案是将日志信息临时存储内存,然后启动线程来将内存中的日志写入文件,因此,本文将结合生产消费者模式来实现异步写入日志的功能。

生产者消费者模式,顾名思义,就是生产者生成数据,消费者处理数据。首先,将通过例子来说明生产者消费者的模式,然后再介绍异步写入日志的功能,其功能代码虽然简单,但是对于日志功能要求场景不多的人来说,却是相当实用的。

一、生产者消费者模式

1、实现简单的生产消费者管理类,生产者即函数Product, 它首先加锁,然后将数据写入队列,最后通过条件变量来唤醒消费者来处理数据;消费者即函数Consume,  它首先加锁,调用条件变量的wait等待接受信号,如果接受到信号,那么从队列中取出数据然后处理,这里需要注意的是取出数据之后,可以提前解锁,以便生产者能够尽快处理数据,另外wait函数添加的匿名函数,它判断队列是否为空,如果不为空,才继续往下处理数据,如果为空,那么继续等待,   这样做的原因是wait返回有可能不是因为接受到生产者发送的信号。

#include <iostream>
#include <mutex>
#include <condition_variable>
#include <thread>
#include <queue>

class JDataManager
{
public:
    //生产者
    void Product()
    {
        for(int i = 0; i < 5; i++)
        {
            std::unique_lock<std::mutex> lock(m_mutex);
            m_queue.push(string("hello"));
            m_condition.notify_one();
            std::cout << "Product  i =  " << i << std::endl;
        }
    }
    
    //消费者
    void Consume()
    {
        int i = 0;
        do
        {
            std::unique_lock<std::mutex> lock(m_mutex);
            m_condition.wait(lock, [&]{ return !m_queue.empty(); } );
            std::string str = m_queue.front();
            m_queue.pop();
            std::cout << "Consume i = " << i << std::endl;
            i++;
        }while(true);
    }
private:
    std::mutex m_mutex;
    std::queue<std::string> m_queue;
    std::condition_variable m_condition;
};

2、上面实现了生产消费者管理类,接下来测试下其运行效果,首先启动线程用于执行消费者函数, 休眠两秒,再启动第二个线程用于执行生产者函数

JDataManager data_manager;

std::thread consumer(&JDataManager::Consume, &data_manager);

std::this_thread::sleep_for(std::chrono::seconds(2));

std::thread productor(&JDataManager::Product, &data_manager);


if(productor.joinable())
{
    productor.join();
}
if(consumer.joinable())
{
    consumer.join();
}

3、最后运行打印的信息如下图所示,生产者生成的数据,消费者对应的提取出来。这先启动消费者,再启动生产者后,运行的效果是合理的。

4、上面是先启动消费者,再启动生产者,如果反过来呢,即先启动生产者,再启动消费者

std::thread productor(&JDataManager::Product, &data_manager);

std::this_thread::sleep_for(std::chrono::seconds(2));

std::thread consumer(&JDataManager::Consume, &data_manager);

5、最后运行打印的信息如下图所示,先启动生产者再启动消费者,消费者也能够正常处理生产者生成的数据。

二、异步日志功能

1、日志是程序中每个模块都会使用到的功能,所以,考虑采用单例模式来实现日志的基本框架。

/// 类定义
class JMyLog
{
public:
    ~JMyLog();
    static JMyLog* Instance(void);

private:
    JMyLog();
);

private:
    static JMyLog* m_pMyLog;
};


/// 类实现
JMyLog* JMyLog::m_pMyLog = nullptr;

JMyLog::JMyLog()
{
}


JMyLog::~JMyLog()
{
    if (m_pMyLog)
    {
        delete m_pMyLog;
        m_pMyLog = nullptr;
    }
}

JMyLog* JMyLog::Instance(void)
{
    if (m_pMyLog == nullptr)
    {
        m_pMyLog = new JMyLog();
    }
    return m_pMyLog;
}

2、实现将每条日志信息写入队列的函数接口,这个相当于生产者, 它负责将写入的每条日志写入队列,再通过条件变量通知消费者处理数据。

/// 类定义
class JMyLog
{
public:
    ~JMyLog();
    static JMyLog* Instance(void);
    // 每条日志信息写入队列
    void WriteLog(int iLogLevel, const std::string &strFileName, int iLineNum
                  , const std::string &strFunName, const char *pFmt, ...);
private:
    JMyLog();

    std::string GetSysTimeToMs();
    std::string GetFirstLog();
    std::string GetLevelInfo(int iLevel);
    std::string GetThreadId();

private:
    static JMyLog* m_pMyLog;
    std::mutex  m_mutex;
    std::deque<std::string> m_deque;
    int m_iLogLevel;
    std::condition_variable m_condVariable;
};


/// 函数实现
static const std::string LOG_DEBUG = "DEBUG";

void JMyLog::WriteLog(int iLogLevel, const std::string &strFileName, int iLineNum
              , const std::string &strFunName, const char *pFmt, ...)
{

    std::unique_lock<std::mutex> lock(m_mutex);
    va_list vaa;
    va_start(vaa, pFmt);
    char ac_logbuf[1024];
    std::memset(ac_logbuf, 0x00, sizeof(ac_logbuf));

    snprintf(ac_logbuf, sizeof (ac_logbuf) - 2, "[%s][%s:%d:%s][%s][%s]"
             , GetSysTimeToMs().c_str()
             , strFileName.c_str()
             , iLineNum
             , strFunName.c_str()
             , GetThreadId().c_str()
             , GetLevelInfo(iLogLevel).c_str());

    size_t ilog_len = strlen(ac_logbuf);
    vsnprintf(ac_logbuf + ilog_len, sizeof(ac_logbuf) - ilog_len -2, pFmt, vaa);
    ilog_len = strlen(ac_logbuf);
    ac_logbuf[ilog_len] = '\n';
    m_deque.push_back(ac_logbuf);
    va_end(vaa);
    m_condVariable.notify_one();
    lock.unlock();
}

std::string JMyLog::GetSysTimeToMs()
{
    time_t timep;
    struct timeb tb;

    time (&timep);
    char tmp[128] ={0};
    strftime(tmp, sizeof(tmp), "%Y-%m-%d %H:%M:%S",localtime(&timep) );

    char tmp2[128] ={0};
    ftime(&tb);
    snprintf(tmp2,sizeof(tmp2),"%d",tb.millitm);

    std::ostringstream buffer;
    buffer << tmp << "." << tmp2 ;

    return buffer.str();
}

std::string JMyLog::GetLevelInfo(int iLevel)
{
    std::string str_level_info;
    switch (iLevel)
    {
        case E_LOG_DEBUG:
        {
            str_level_info = LOG_DEBUG;
            break;
        }
        default:
        {
            str_level_info = LOG_DEBUG;
            break;
        }
    }
    return str_level_info;
}

std::string JMyLog::GetThreadId()
{
    std::ostringstream thread_id;
    thread_id << std::hex << std::this_thread::get_id();
    return thread_id.str();
}

std::string JMyLog::GetFirstLog()
{
    std::string str = m_deque.front();
    m_deque.pop_front();
    return str;
}

3、上面实现了生产者的日志生成功能后,接下来就是实现消费者的日志处理功能,由于考虑的是异步的模式,所以,消费者需要在线程中运行。下面实现的消费者是从队列中取出数据,然后将日志信息打印到终端,后面将添加日志写入文件的功能。

/// 类定义
class JMyLog
{
public:
    ~JMyLog();
    static JMyLog* Instance(void);
    // 每条日志信息写入队列
    void WriteLog(int iLogLevel, const std::string &strFileName, int iLineNum
                  , const std::string &strFunName, const char *pFmt, ...);
private:
    JMyLog();

    std::string GetSysTimeToMs();
    std::string GetFirstLog();
    std::string GetLevelInfo(int iLevel);
    std::string GetThreadId();

    // 启动线程
    void StartThread();
    // 线程执行函数
    void ThreadExce();

private:
    static JMyLog* m_pMyLog;
    std::mutex  m_mutex;
    std::deque<std::string> m_deque;
    int m_iLogLevel;
    std::condition_variable m_condVariable;
};

/// 函数实现
void JMyLog::StartThread()
{
    std::thread thread_obj(&JMyLog::ThreadExce, this);
    thread_obj.detach();
}

void JMyLog::ThreadExce()
{
    while(true)
    {
        std::unique_lock<std::mutex> lock_log(m_mutex);
        m_condVariable.wait(lock_log, [&] { return !m_deque.empty();});
        std::string str = GetFirstLog();
        lock_log.unlock();
        if (!str.empty())
        {
            std::cout << str;
        }
        str.clear();
    }
}

4、为了用户更加方便的调用,我们定义了如下所示的宏

enum E_LOG_LEVEL
{
    E_LOG_DEBUG = 1,
};

#define MyLogD(pFmt, ...) \
    JMyLog::Instance()->WriteLog(E_LOG_DEBUG, __FILE__, __LINE__, __func__, pFmt, ##__VA_ARGS__);

5、测试代码如下所示,调用者按照类似printf的格式使用定义好的宏MyLogD

MyLogD("%s", "this is mylog1.");
MyLogD("%s", "this is mylog2.");

6、日志输出的格式如下图所示

[2019-11-24 16:27:28.950][../JQtTestStudy/debbugtest/jdebugcppattr.cpp:259:TestMyLog][0x7fffa4c16380][DEBUG]this is mylog1.
[2019-11-24 16:27:28.951][../JQtTestStudy/debbugtest/jdebugcppattr.cpp:260:TestMyLog][0x7fffa4c16380][DEBUG]this is mylog2.

7、 为了将日志信息写入文件,封装日志文件写入类,该写入类主要实现打开文件、关闭文件,日志写入文件以及刷新文件的四个函数。

#include <string>
#include <fstream>

/// 类定义
class JLogFileHandler
{
public:
    JLogFileHandler(const std::string &strFilePath);
    ~JLogFileHandler();

    void Open();
    void Close();
    void Write(const std::string &strInfo);
    void Flush();

private:
    void InitFilePath();

private:
    std::string m_strFilePath;
    std::ofstream m_outfstream;
};

/// 类实现
JLogFileHandler::JLogFileHandler(const std::string &strFilePath)
    : m_strFilePath(strFilePath)
{
    InitFilePath();
}


JLogFileHandler::~JLogFileHandler()
{

}

void JLogFileHandler::InitFilePath()
{

}

void JLogFileHandler::Open()
{
    if (!m_outfstream.is_open())
    {
        m_outfstream.open(m_strFilePath);
    }
}

void JLogFileHandler::Close()
{
    if (m_outfstream.is_open())
    {
        m_outfstream.close();
    }
}

void JLogFileHandler::Write(const std::string &strInfo)
{
    m_outfstream << strInfo;
}

void JLogFileHandler::Flush()
{
    m_outfstream.flush();
}

8、JMyLog类添加文件处理者对象,然后线程执行函数中将日志信息写入文件,并且执行刷新功能

/// 类定义
class JMyLog
{
public:
    ~JMyLog();
    static JMyLog* Instance(void);
    // 每条日志信息写入队列
    void WriteLog(int iLogLevel, const std::string &strFileName, int iLineNum
                  , const std::string &strFunName, const char *pFmt, ...);
private:
    JMyLog();

    std::string GetSysTimeToMs();
    std::string GetFirstLog();
    std::string GetLevelInfo(int iLevel);
    std::string GetThreadId();

    // 启动线程
    void StartThread();
    // 线程执行函数
    void ThreadExce();

    // 初始化文件
    void InitFile();

private:
    static JMyLog* m_pMyLog;
    std::mutex  m_mutex;
    std::deque<std::string> m_deque;
    int m_iLogLevel;
    std::condition_variable m_condVariable;
    std::shared_ptr<JLogFileHandler> m_FileHandler; // 文件处理者

};

/// 函数实现
JMyLog::JMyLog()
{
    InitFile();
    StartThread();
}

void JMyLog::ThreadExce()
{
    while(true)
    {
        std::unique_lock<std::mutex> lock_log(m_mutex);
        m_condVariable.wait(lock_log, [&] { return !m_deque.empty();});
        std::string str = GetFirstLog();
        lock_log.unlock();
        if (!str.empty())
        {
            std::cout << str;

            m_FileHandler->Write(str);
            m_FileHandler->Flush();
        }
        str.clear();
    }
}

void JMyLog::InitFile()
{
    std::string str_file = "../../../log/test.log";
    m_FileHandler = std::make_shared<JLogFileHandler>(str_file);
    m_FileHandler->Open();
}

9、再次运行测试代码,查看日志目录下生成了日志文件test.log,并且日志信息也成功写入到文件

三、总结

最后再来总结异步日志功能的实现步骤,首先采用单例模式实现日志的基本框架,接着实现日志生产者,即提供写入日志信息接口,然后再实现日志消费者,日志消费者运行在线程中,并且收到信号才开始处理数据,最后实现文件处理者,将日志信息写入文件。至此,结合生产者消费者模式的异步日志功能完成了。

基于future和promise实现的异步收发数据模版类

std::future和std::promise两者结合可以实现异步的功能场景,本文将介绍的异步收发数据模版类是在实践中结合std::future和std::promise而摸索出来的。

工作过程中,我们可能会经常遇到这样的场景,需要从线程中获取运行的结果。现在我们有两种方式可以实现这样的效果。

  • 第一种方式,属于通用用法,通过使用指针在线程间共享数据。传递指针给新建的线程,主线程使用条件变量等待被唤醒;当线程设置完成数据到传递过来的指针之后,发送条件变量信号,主线程被唤醒之后,从指针中提取数据。这种方式采用条件变量、锁、指针结合才实现了异步功能,比较复杂。
  • 第二种方式,采用std::future和std::promise对象,也就是本文接下来要详细说明的一种异步实现方式。
  • std::future是一个类模版,内部存储一个将来用于分配的值,它提供了get()成员函数来访问该值的机制。如果关联值可用之前,调用了get函数,那么get函数将阻塞直到关联值不可用。
  • std::promise也是一个类模版,它用来设置上面的关联值,每一个stb::promise和一个std::future对象关联,一旦stb::promise设置值之后,std::future对象的get()函数就会获取到值,然后返回。std::promise与它关联的std::future共享数据。

一、阻塞等待获取数据

1、实现线程执行函数,入参是一个std::promise指针,函数内调用std::promise指针设置值

void thread_function(std::promise<std::string>* pPromiseObj)
{
    if(nullptr == pPromiseObj)
    {
        return;
    }
    
    pPromiseObj->set_value("this is my name.");
}

2、定义std::promise对象,从该对象获取关联的std::future对象,启动线程并且传入std::promise对象的指针,调用std::future对象的get()函数阻塞等待,如果返回,那么打印输出返回的字符串信息。

// 定义std::promise对象,从该对象获取关联的std::future
std::promise<std::string> promise_obj;
std::future<std::string> future_obj = promise_obj.get_future();

// 启动线程
std::thread thread_obj(&thread_function, &promise_obj);

// 阻塞等待
std::string str = future_obj.get();
std::cout << "std = " << str << std::endl;

// 等待线程退出
thread_obj.join();

3、运行程序,输出的信息如下所示,从这里可以看出,std::promise在线程中设置值之后,std::future对象的get()函数成功获取并返回。

二、通知线程退出

基于std::promise和std::future的机制,我们可以利用std::promise的set_value来通知运行的线程退出。具体如何做呢,我们接下来给出例子进行说明。

1、实现线程的执行函数,入参为与std::promise关联的std::future对象,执行函数内部调用std::future的wait_for循环超时等待,如果std::future的wait_for在超时时间内没有收到std::promise调用set_value发送的信号,那么继续循环等待,如果在超时时间内收到std::promise调用set_value发送的信号,那么退出循环,同时线程页退出了。

void JThreadFunction(std::future<void> FutureObj)
{
    // 调用std::future的wait_for循环超时等待
    while(FutureObj.wait_for(std::chrono::milliseconds(1))
          == std::future_status::timeout)
    {
        std::cout << "do something" << std::endl;
        std::this_thread::sleep_for(std::chrono::milliseconds(1000));
    }
}

2、创建std::promise对象,从std::promise对象提取关联的future对象,启动线程,并且将上面的future对象传递给线程,主线程休眠一段时间之后,调用std::promise对象的set_value函数来发送信号,通知线程退出。

//创建promise对象
std::promise<void> exit_signal;

//提取future对象
std::future<void> future_obj = exit_signal.get_future();

//启动线程
std::thread thread_obj(JThreadFunction, std::move(future_obj));

//休眠
std::this_thread::sleep_for(std::chrono::seconds(3));

//发送信号
std::cout << "send signal" << std::endl;
exit_signal.set_value();

//等待线程退出
thread_obj.join();
std::cout << "exit function" << std::endl;

3、从输出的结果信息看,线程一直在运行,当收到std::promise对象发送信号的信号之后就退出。

三、异步收发数据

经过上面两个例子的讲解,相信大家对std::future和std::promise已经有了一个大概的了解。下面就给出异步收发数据的模版类。

1、类模版JAsyncSender实现两个函数,一个是Send用于发送数据,它可以在线程中执行,另一个是Wait等待接收数据,如果第三个参数没有输入,那么默认一直等待,否则在指定时间内,没有收到信息,那么返回失败。

#ifndef JASYNCSENDER_H
#define JASYNCSENDER_H

#include <future>
#include <chrono>
#include <thread>
#include "log/easylogging++.h"

///
/// 模版类声明
///
template <class RealT>
class JAsyncSender
{
public:
    JAsyncSender();
    ~JAsyncSender();

    // 发送数据
    bool Send(const RealT &data);
    // 等待接收数据,需要先运行
    bool Wait(std::promise<RealT> promiseObj, RealT &data, unsigned int uiTimeMills = 0);
private:
    std::promise<RealT> m_promiseObj;
};


///
/// 类模版实现
///
template  <typename RealT>
JAsyncSender<RealT>::JAsyncSender()
{

}

template  <typename RealT>
JAsyncSender<RealT>::~JAsyncSender()
{

}

template  <typename RealT>
bool JAsyncSender<RealT>::Send(const RealT &data)
{
    try
    {
        m_promiseObj.set_value(data);
    } catch (const std::exception &e)
    {
        LOG(INFO) << "exception: " << e.what();
    }
    return true;
}


template  <typename RealT>
bool JAsyncSender<RealT>::Wait(std::promise<RealT> promiseObj, RealT &data, unsigned int uiTimeMills)
{
    std::future<RealT> future_obj = promiseObj.get_future();
    m_promiseObj = std::move(promiseObj);
    if (uiTimeMills > 0)
    {
        while(future_obj.wait_for(std::chrono::milliseconds(uiTimeMills))
              == std::future_status::timeout)
        {
            return false;

        }
    }
    data = future_obj.get();
    return true;
}

#endif // JASYNCSENDER_H

2、接下来说明类模版JAsyncSender的使用方法

  • 定义成员变量m_AsyncSendInt,它由主线程和子线程共享。JAsyncSender的type为整型,也可以定义为字符串,甚至是自定义对象,根据具体需求场景具体定义。
    JAsyncSender<int> m_AsyncSendInt;
  • 通过lambda方式创建线程,当然你也可以使用其他方式,线程内部先休眠一段时间,然后发送数据。
// 通过lambda方式创建线程
std::thread thread_obj( [&]{
     LOG(INFO) <<  ": lambda thread executing";
     std::this_thread::sleep_for(std::chrono::seconds(3));
     m_AsyncSendInt.Send(20);
 } ) ;


 std::promise<int> promise_obj;
 int i_data = -1;
 // 等待线程返回数据
 m_AsyncSendInt.Wait(std::move(promise_obj), i_data);
 LOG(INFO) <<  "i_data: " << i_data;
 if (thread_obj.joinable())
 {
     thread_obj.join();
 }
  • 从运行结果看,基于future和promise实现的异步收发数据模版类的功能是正常的。
[2019-11-17 19:29:01,539829] [auto JDebugCPPAttr::TestAsyncSender()::(anonymous class)::operator()() const:235] : lambda thread executing
[2019-11-17 19:29:04,542497] [bool JDebugCPPAttr::TestAsyncSender():244] i_data: 20

四、总结

std::promise与std::future的结合使用,可以更加容易处理异步消息事件,另外C++11标准中提供的 std::asych和std::packaged_task也是结合std::future来处理异步的事件流程。std::promise与std::future虽然功能强大,但是std::promise与std::future是一一对应的,目前没有办法处理一对多的问题,比如一个std::promise对应多个std::future。std::promise如果设置过一次,再次设置会报错,如果需要重新使用,需要再创建std::promise对象。

C++容器中实用的查找功能

C++标准中std提供了几种容器,它们包括顺序容器,比如vector, list, deque, queue, stack等,关联容器 ,比如map, set等,其中使用频率比较高的容器是vecotor向量容器、map键值对容器,我们经常会使用这两个容器来存储数据,然后根据不同的场景来查找获取容器内的值。而本文接下来将说明从这两类容器中快速查找获取数据的方法。

一、vector容器查找功能

vector容器自身没有提供查找函数,这里借助标准模版库algorithm提供的find,  使用的时候需要包含该头文件。

1、首先定义vector容器变量,然后存入数据,接着遍历打印容器内的所有数据,最后调用algorithm提供的函数find从vector向量中查找数据,algorithm提供的函数find需要输入三个入参数,第一个参数是容器开始查找的迭代器变量,第二个变量是容器结束查找的迭代器变量,第三个参数是需要查找的数据。

#include <vector>
#include <algorithm>

// 定义vecotor,然后存入数据
std::vector<std::string> vec_str;
vec_str.push_back("abc");
vec_str.push_back("def");
vec_str.push_back("fhj");
vec_str.push_back("123");
vec_str.push_back("456");

// 遍历打印vector容器内的数据
std::vector<std::string>::iterator iter = vec_str.begin();
for(iter = vec_str.begin(); iter != vec_str.end(); iter++)
{
    LOG(INFO) << *iter;
}
LOG(INFO) << "======";

// 调用find函数查找,内容为“def”的信息
iter = find(vec_str.begin(),vec_str.end(), "def");
if (iter != vec_str.end())
{
    LOG(INFO) << "find info: " << *iter;
}
LOG(INFO) << "======";

2、运行程序,输出的内容如下图所示,容器内存在需要查找的数据,返回迭代器变量,我们根据迭代器变量输出数据内容

二、map容器查找功能

map容器自身提供了查找功能,同时它也支持使用标准模版库algorithm提供的find函数。

1、首先定义map容器变量,写入数据,再遍历输出容器内的数据,接着调用map容器自身提供的find函数来查找key为2的数据,返回迭代器变量,然后根据这个迭代器变量输出键值,接下来调用algorithm提供的find函数,需要注意的是第三个参数输入的是迭代器的取值,最后也是返回迭代器变量。

#include <map>
#include <algorithm> 

// 首先定义map容器变量,写入数据
std::map<int, std::string> map_str;
map_str[1] = "aa";
map_str[2] = "bb";
map_str[3] = "cc";

// 遍历输出容器内的所有内容
std::map<int, std::string>::iterator iter_map;
for(iter_map = map_str.begin(); iter_map != map_str.end(); iter_map++)
{
    LOG(INFO) << "key: " << iter_map->first <<" value "<< iter_map->second;
}
LOG(INFO) << "======";

// 调用map容器自身提供的find函数,查找key为2的数据
iter_map = map_str.find(2);
if (iter_map != map_str.end())
{
    LOG(INFO) << "key: " << iter_map->first <<" value: "<< iter_map->second;
}
LOG(INFO) << "======";

// 调用algorithm提供的find函数来查找数据,注意find的第三个参数输入的是迭代器的取值
std::map<int, std::string>::iterator iter_map_ret;
iter_map_ret = find(map_str.begin(), map_str.end(), *iter_map);
if (iter_map_ret != map_str.end())
{
    LOG(INFO) << "result, key: " << iter_map_ret->first <<" value: "<< iter_map_ret->second;
}

2、运行程序,输出的内容如下图所示

三、键自定义的map容器查找功能

map容器使用过程中,有时候为了程序的可维护性以及降低代码的复杂度,需要自定义类作为map的键,在这种场景下,上面的查找方法是否也能够生效呢?接下来让我们进行验证。

1、首先自定义类JKeyPair来作为map的key,  如果自定义对象要作为map的键,那么需要重载operator<运算符,而如果要使用algorithm中的find,需要重载operator==运算符

#include <iostream>

/// 类的定义
class JKeyPair
{
public:
    JKeyPair(const std::string &strName, int iIndex);
    ~JKeyPair();

    std::string GetName(void);
    int GetIndex(void);

    // 自定义map中的key,需要重载operator<运算符(一定要记得加上const, 否则调用出错)
    bool operator<(const JKeyPair &rhs) const;

    // 自定义map中的key,使用algorithm中的find,需要重载operator==运算符(一定要记得加上const, 否则调用出错)
    bool operator==(const JKeyPair& rhs) const;
private:
    std::string m_strName;
    int m_iIndex;
};


/// 类的实现
JKeyPair::JKeyPair(const std::string &strName, int iIndex)
    :m_strName(strName),m_iIndex(iIndex)
{}

JKeyPair::~JKeyPair()
{}

std::string JKeyPair::GetName(void)
{
    return m_strName;
}

int JKeyPair::GetIndex(void)
{
    return m_iIndex;
}

bool JKeyPair::operator<(const JKeyPair &rhs) const
{
    if (m_iIndex < rhs.m_iIndex)
    {
        return true;
    }
    else if ((m_iIndex ==  rhs.m_iIndex)
             && (m_strName < rhs.m_strName))
    {
        return true;
    }
    return false;
}


bool JKeyPair::operator==(const JKeyPair& rhs) const
{
    if ((m_iIndex ==  rhs.m_iIndex)
            && (m_strName == rhs.m_strName))
    {
        return true;
    }
    return false;
}

2、验证调用map自身提供的find函数,其测试代码如下图所示

// 定义map容器变量,key是自定义类型,然后写入数据
std::map<JKeyPair, std::string> map_info;
JKeyPair key_pair_1("one", 1);
map_info[key_pair_1] = "value_one";
JKeyPair key_pair_2("two", 2);
map_info[key_pair_2] = "value_two";
JKeyPair key_pair_3("three", 2);
map_info[key_pair_3] = "value_three";

// 循环遍历容器内数据
std::map<JKeyPair, std::string>::iterator iter;
for(iter = map_info.begin(); iter != map_info.end(); iter++)
{
    JKeyPair key_pair = iter->first;
    LOG(INFO) << "name : " << key_pair.GetName();
    LOG(INFO) << "index : " << key_pair.GetIndex();
    LOG(INFO) << "value : " << iter->second;
}
LOG(INFO) << "======";

// 通过调用map自身提供的函数find,来查找键为key_pair_2的数据
iter = map_info.find(key_pair_2);
if (iter != map_info.end())
{
    LOG(INFO) << "find, value : " << iter->second;
}

3、从运行的结果看,使用自定义对象作为key值,map容器提供的find函数能够正确运行,并且从实际操作过程中,可以得出,map容器提供的find函数,自定义对象不需要重载operator==运算符。

4、接着再验证algorithm中的find,从实际操作过程中,自定义对象需要重载operator==运算符,测试代码段如下图所示,其中find中第三个参数是上一步骤中调用map自身的find返回的迭代器变量

// 使用alogrithm提供的find来查找map容器的数据
std::map<JKeyPair, std::string>::iterator iter_map_key;
iter_map_key = std::find(map_info.begin(), map_info.end(), *iter);
if (iter_map_key != map_info.end())
{
    JKeyPair key_pair = iter_map_key->first;
    LOG(INFO) << "result, self key: " << key_pair.GetName()  << "/" << key_pair.GetIndex()
              <<" value: "<< iter_map_key->second;
}

5、运行后打印的结果信息看,能够正确调用algorithm中的find来查找map中的数据

四、总结

到这里,我们已经将容器vector,map的查找功能介绍完成。接下来梳理总结。vector容器自身没有提供查找函数,因此,需要调用algorithm中的find来快速查找数据。map容器自身既提供了查找函数,也支持使用algorithm中的find来快速查找数据。而自定义对象作为map容器的键的情况下,如果需要支持上面的场景,那么自定义类需要重载operator<和operator==运算符。

快速追踪内存使用情况的方法

函数mtrace是linux中用于开启内存使用记录的函数接口,而函数muntrace是关闭内存使用记录的函数接口,另外环境变量MALLOC_TRACE则是决定内存使用记录是否记录到文件中。

#include <mcheck.h>
void mtrace(void);
void muntrace(void);

一、入门例子

1、首先给出简单的例子,初步了解函数的使用方法。

#include <mcheck.h>
#include <stdlib.h>

int main(int argc, char** argv)
{
    // 设置MALLOC_TRACE环境变量
    setenv("MALLOC_TRACE", "trace.log", 1);
    // 开启内存使用情况记录
    mtrace();
    return 0;
}

2、编译运行上面创建的cpp文件

3、运行成功之后,执行ls -l查看当前目录情况,可以看到生成了trace.log文件,其内容如下图所示, 文件以“= Start”开始。

二、没有内存泄露的情况

1、原来的例子中增加调用malloc申请内存,再调用free释放内存,最后再调用muntrace关闭内存使用情况记录

#include <mcheck.h>
#include <stdlib.h>

int main(int argc, char** argv)
{
    // 设置MALLOC_TRACE环境变量
    setenv("MALLOC_TRACE", "trace.log", 1);
    // 开启内存使用情况记录
    mtrace();

    char *p = (char *)malloc(10000);
    free(p);

    // 关闭内存使用情况记录
    muntrace();
    return 0;
}

2、编译上面的cpp文件,然后运行生成的可执行文件,再查看生成的trace.log文件,其内容如下所示,以“= Start”开始, 以“= End”结束, 其中+号代表申请内存,-号代表释放内存。从这个文件,我们可以发现,cpp文件最后调用muntrace函数之后,生成的tracel.log文件中的最后会以“= End”结束。

三、内存泄露的情况

1、这次在上面cpp文件的基础上,再次调用malloc申请内存,但是不释放内存

#include <mcheck.h>
#include <stdlib.h>

int main(int argc, char** argv)
{
    // 设置MALLOC_TRACE环境变量
    setenv("MALLOC_TRACE", "trace.log", 1);
    // 开启内存使用情况记录
    mtrace();

    char *p = (char *)malloc(10000);
    free(p);

    char *p2 = (char *)malloc(20000);

    // 关闭内存使用情况记录
    muntrace();
    return 0;
}

2、同样的编译cpp文件,然后运行可执行文件,查看生成的trace.log文件,从内容看,文件倒数第二行中申请了内存,但是没有释放内存。

四、转换为可理解的信息

从上面三个例子看,生成的trace.log的内容的可读性比较差,为了提高可读性,我们可以使用mtrace命令工具来解析trace.log文件,mtrace命令是glibc-utils的工具, 如果linux上没有mtrace命令,那么需要先下载安装glibc-utils。下面首先介绍基于fedora系统下下载安装glibc-utils的方法,最后再来介绍将trace.log转换为可读性的信息命令。

1、下载安装glibc-utils

首先终端执行命令yum install glib-utils,  然后会提示“Is this ok [y/N]:”,直接输入y,然后按下回车键。

如果安装成功的话,那么最后会出现“Complete!”的提示信息

2、将trace.log转换为可读性的信息,其格式为:mtrace 可执行文件  生成的记录内存文件, 如下图所示,终端执行mtrace  a.out trace.log之后,界面直接显示“Memory not freed:”表示内存没有释放,并且还指出文件的具体位置。

五、总结

mtrace()是linux上开启记录内存的函数接口,muntrace()是关闭记录内存的函数接口,MALLOC_TRACE是决定是否将记录内存情况写入文件的环境变量,mtrace命令是glibc-utils的工具,可以将记录内存情况的文件转换为可理解的信息。