【项目收获】自己动手实现一个宏log功能

目录

一些零碎知识点

1.fwrite

2. __FILE__, __LINE__

3.ostringstream

4.c语言打开文件的方式

一个简单版本的log

有点疑惑

webserver程序里的log

临时变量的析构


在程序中我们常常要打印出当前函数、代码的执行情况或者错误码,以便调试。若只是简单地cout+重定向其实也能实现,但是总觉得不好用,而且不规范。一般在程序里都是在需要的地方打一条日志,比如这样:

LOG << "timer add fail";
LOG << "New connection from " << inet_ntoa(client_addr.sin_addr) << ":" << ntohs(client_addr.sin_port);

因此,好好研究一下底层的实现还是很有必要的。

一些零碎知识点

1.fwrite

函数原型:

size_t fwrite ( const void * ptr, size_t size, size_t count, FILE * stream );

功能:Write block of data to stream

详细介绍:Writes an array of count elements, each one with a size of size bytes, from the block of memory pointed by ptr to the current position in the stream.

参数:

ptr
    Pointer to the array of elements to be written, converted to a const void*.
size
    Size in bytes of each element to be written.
    size_t is an unsigned integral type.
count
    Number of elements, each one with a size of size bytes.
    size_t is an unsigned integral type.
stream
    Pointer to a FILE object that specifies an output stream.

使用实例:

/* fwrite example : write buffer */
#include <stdio.h>

int main ()
{
  FILE * pFile;
  char buffer[] = { 'x' , 'y' , 'z' };    //buffer
  pFile = fopen ("myfile.bin", "wb");    //二进制写方式打开
  fwrite (buffer , sizeof(char), sizeof(buffer), pFile);
  fclose (pFile);
  return 0;
}

这个函数用来实现log最底层的功能,往文件里面写。上层将会对此进行一系列的封装。

注:

为了快速,实际程序中使用unlocked(无锁)的fwrite函数。平时我们使用的C语言IO函数,都是线程安全的,
为了做到线程安全,会在函数的内部加锁,这会拖慢速度.而对于这个类,可以保证从
始到终只有一个线程能访问,所以无需进行加锁操作。

fwrite_unlocked(logline, 1, len, fp_);

 

2. __FILE__, __LINE__

打日志的时候,往往需要加上当前函数的名称、进程名、时间等信息,

__FILE__,__LINE__,__DATA__,__TIME__ ———— 编译器
 
    C / C++编译器会内置几个宏,这些宏定义可以帮助我们完成跨平台的源码编写,也可以输出有用的调试信息。
 
ANSI C标准中有几个标准预定义宏(也是常用的):
__DATE__:在源文件中插入当前的编译日期
__TIME__:在源文件中插入当前编译时间;
__FILE__:在源文件中插入当前源文件路径及文件名;
__LINE__:在源代码中插入当前源代码行号;
__STDC__:当要求程序严格遵循ANSI C标准时该标识被赋值为1;
__FUNCTION__:函数名;
__cplusplus:当编写C++程序时该标识符被定义。
 
C里就已经有这些宏,可以用于记录log,测试如下:
 
cout << "log in "<< __FILE__ << " : " << __FUNCTION__ <<" line: " << __LINE__ << endl;
// 输出: log in d:\tmp\test\test_sizeof.cpp : test line: 10

 

3.ostringstream

ostringstream是C++的一个字符集操作模板类,定义在sstream.h头文件中。ostringstream类通常用于执行C风格的串流的输出操作,格式化字符串,避免申请大量的缓冲区,替代sprintf。

派生关系图:

 è¿éåå¾çæè¿°

其使用见另一篇博客 用于任意类型转为string

注意点:

std::ostringstream::str()返回的是临时对象,不能对其直接操作。

例如会有如下误用:

const char * pBuffer = oss.str().c_str(); 
注意pBuffer指向的内存已被析构!!

实例:

    // 输出随机内存值,危险
    const char* buf = ostr2.str().c_str();  
    cout << buf << endl;

    // 正确输出_df
    string ss = ostr2.str();
    const char *buffer = ss.c_str();
    cout << buffer << endl;

4.c语言打开文件的方式

文件使用方式    含义    如果指定的文件不存在
r(只读)    读取一个已经存在的文本文件    出错
w(只写)    打开一个文本文件,输出数据,若文件存在则文件长度清为0,即该文件内容会消失    建立新文件
a (追加)    向文本文件末尾添加数据,原来文件中的数据保留,新的数据添加到文件为,原文件EOF保留    建立新文件
rb(只读)    读取一个二进制文件    出错
wb(只写)    打开一个二进制文件,输出数据,若文件存在则文件长度清为0,即该文件内容会消失    建立新文件
ab (追加)    向二进制文件尾添加数据    建立新文件
r+ (读写)    对一个文本文件进行读写操作    出错
w+ (读写)    对一个文本文件进行读写操作,若文件存在则文件长度清为0,即该文件内容会消失    建立新文件
a+(读写)    向文本文件末尾添加数据,原来文件中的数据保留,新的数据添加到文件尾,原文件EOF不保留    建立新文件
rb+ (读写)    读写一个二进制文件    出错
wb+ (读写)    对一个二进制文件进行读写操作,若文件存在则文件长度清为0,即该文件内容会消失    建立新文件
a+(读写)    向二进制文件末尾添加数据,原来文件中的数据保留,新的数据添加到文件尾    建立新文件
 

 

 

一个简单版本的log

先写一个非常简单的:

#include <iostream>
#include <string>
#include <sstream>
#include <fcntl.h>
#include <stdio.h>
#include <sys/stat.h>
#include <unistd.h>

using namespace std;
class logger;

#define LOG logger(__FILE__, __LINE__).addinfo()

class logger{
public:
	logger(const char* filename, int line)
	:filename(filename),
	 line(line)
	 {
		 fp_ = fopen("./simplelog.log","a+");
	 }
	
     	
	 ostream& addinfo(){
		 log << filename << ' ' << line << ' ';
		 return log;
	 }
	 
	 ~logger(){
		 string tmp = log.str();
		 fwrite(tmp.c_str(), 1, tmp.size(), fp_);
		 fclose(fp_);
                 cout<<"call ~logger()"<<endl;
	 }
	 
	string filename;
	int line;
	ostringstream log;
	FILE* fp_;
};

int main(){
		
	cout << "begin to log" << endl;
	
	LOG << "my simple log"<<endl;
	
	cout << "finish log" << endl;
	return 0;
}

在程序里使用直接LOG << "需要写入日志的内容"<<endl; 即可

实现原理:每次使用LOG都会创建一个临时对象logger,在其析构函数中往日志文件里写

虽然简单,但大体上思想就是这样的。

实际上这样实现有问题,因为同一个进程的线程是共享打开的文件的,不应该在每个线程都去打开一遍文件。

可以先在全局区

FILE *fp_ = NULL;
fp_ = fopen("./simplelog.log","a+");

在每次析构 LOG对象的时候往fp_里面写就行

 

 

有点疑惑

析构函数是在啥时候调用的?是在LOG语句执行完之后还是LOG语句所在函数块结束?

测试结果:

可以看到,在LOG语句结束之后马上就调用了析构函数了,在log文件中追加

 * 这是无名对象,当使用LOG_* << "***"时,
 * 1.构造logger类型的临时对象,返回ostream类型变量 log(ostringstream log)
 * 2.将调用语句的<<后的内容追加到log变量中
 * 3.当前语句结束,logger临时对象析构,调用logger析构函数,将log对象中的数据输出(写入到文件中)

形如LOG_*的调用实际上是宏定义,当使用LOG_*时,在编译期会被宏定义后面的语句替换。而实际上是创建了logger的临时对象,创建后调用addinfo()函数,addinfo()函数返回的对象是ostream类型的,可以将后面的字符串追加到输出流对象中。对于临时对象,所在语句结束后就被析构了,所以对于日志信息的输出,肯定都交给logger对象的析构函数处理了。

 

 

webserver程序里的log

前后端日志系统,保证性能。

全过程分析见另一篇博客 LOG全过程

 

临时变量的析构

接上上面提到的一点疑惑,临时变量的析构究竟是怎么一回事?

摘抄自https://blog.csdn.net/stpeace/article/details/46461167,大佬博客

先说结论:

              临时对象是在遇到其后的第一个分号(语句结束处)析构的。

见例子:

#include <iostream>
#include <string>
using namespace std;
 
class A
{
public:
	A()
	{
		cout << "A constructor" << endl;
	}
	~A()
	{
		cout << "A destructor" << endl;
	}
};
 
int main()
{
	A(); // 临时对象
 
	printf("end xxx\n");
	printf("end yyy\n");
 
	return 0;
}

稍微懂一点C++的人会说, 结果是:

A constructor
end xxx
end yyy
A destructor

      其实, 上述结果是错误的, 真正的结果是:

A constructor
A destructor
end xxx
end yyy

看来, 在执行完第一个语句后, 临时对象A()就析构了, 我们来看看汇编, 验证一下吧:

 我们看到, 临时对象确实是在printf之前析构的。

好, 我们接着看:

#include <iostream>
#include <string>
using namespace std;
 
class A
{
public:
	A()
	{
		cout << "A constructor" << endl;
	}
 
	~A()
	{
		cout << "A destructor" << endl;
	}
};
 
int main()
{
	A(), // 注意, 是逗号运算符
	printf("end xxx\n");
	printf("end yyy\n");
 
	return 0;
}

      运行结果为:

A constructor
end xxx
A destructor
end yyy
      不要惊讶, 查看汇编代码就知道, 临时对象是在 printf("end xxx\n");后析构的。

      继续看代码:

#include <iostream>
#include <string>
using namespace std;
 
class A
{
public:
	A()
	{
		cout << "A constructor" << endl;
	}
 
	~A()
	{
		cout << "A destructor" << endl;
	}
};
 
int main()
{
	A(), // 注意, 是逗号运算符
	printf("end xxx\n"), // 注意, 是逗号运算符
	printf("end yyy\n");
 
	return 0;
}

  运行结果为:

A constructor
end xxx
end yyy
A destructor

       不要惊讶, 查看汇编代码就知道, 临时对象是在 printf("end xxx\n");后析构的。

 

这里,作者提出了一个很容易出错的点,也是他写这篇博客的原因:

#include <iostream>
#include <string>
using namespace std;
 
int main()
{
	const char *p = string("abc").c_str(); // 临时对象在执行完该句后析构了
	cout << p << endl; // 此时p指向垃圾值
 
	return 0;
}

这种情况下是无法正确输出abc的。

 

 

 

 

 

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值