目录
在程序中我们常常要打印出当前函数、代码的执行情况或者错误码,以便调试。若只是简单地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的。