根据C++提供的构造函数,析构函数我们可以实现在对象创建的时候和对象销毁的时候根据我们的需要进行一些输出操作.我们可以在调用的函数中开始的时候声明这样一个对象,用来跟踪函数执行的生命周期.
设计一个跟踪类
class Trace{
public:
Trace() { std::cout << "Hello\n"; }
~Trace() { std::cout << "Bye\n"; }
};
然后在需要跟踪的函数中使用这个类:
void foo {
Trace t;
// do something here
}
这样会在函数开始执行的时候打印Hello
在函数结束的时候打印:Bye
.
这个简单的类在多个函数中使用的时候会出现不能区分是哪些函数的执行过程的问题,所以我们对他进行一些修改.
class Trace{
public:
Trace(const char* p, const char* q): bye(q) { std::cout << p << "\n"; }
~Trace() { std::cout << bye << "\n"; }
private:
const char* bye;
};
在使用时就可以根据初始化的参数的不同进行区分:
void foo() {
Trace t("begin foo", "end foo");
// do something here
}
在使用这个类时间长了以后,我们会发现这个类还有三个地方可以进行优化:
- 打印的内容一般都是以"begin"开始,“end”结尾.
- 如果可以通过开关控制是否要打印输出会给我们带来好处.
- 如果可以选择输出到哪里,就可以跟业务逻辑的输出区分开存储了.
我们需要仔细区分下2和3.一般操作系统都提供一个输出到/dev/null的类似功能,输出到这里的输出将全部被丢弃,也就是说:禁止输出其实是输出重定向的一个特例.
下面我们来想一想,这个输出重定向的流应该放在哪里.
- 使用全局指针的方式.Trace使用全局的输出流指针进行输出.这样会给用户带来使用的负担,有些需要打开输出,有些需要关闭输出这样的控制需要用户很谨慎的操作.
- 将
ostream*
放在Trace
类中,当作成员变量.这样在Trace
对象创建的时候我们总要给一个ostream
的指针才行. - 使用
Trace
类进行跟踪的场景总是聚集在一起的.所以我们几乎总是需要所有的Trace对象同时输出到某个文件中.所以我们考虑新建一个类,用来管理一组Trace对象输出到哪里.
class Channel {
friend class Trace;
ostream* trace_file;
public:
Channel(ostream* o = &cout): trace_file(o) {}
void reset(ostream* o) {trace_file = o}
};
class Trace {
public:
Trace(const char* s, Channel* c):name(s), cp(c) {
if (cp->trace_file) {
*(cp->trace_file) << "begin " << name << endl;
}
}
~Trace() {
if (cp->trace_file) {
*(cp->trace_file) << "end " << name << endl;
}
}
private:
Channel* cp;
const char* name;
};
这样做Trace就可以在绑定了相应的Channel对象后将内容打印到Channel中的输出流中.并且可以通过Channel的reset方法进行调整.
创建死代码
我们的Trace类对象每次创建的时候都会消耗资源,为了减少这些消耗,我们需要在代码中添加一些死代码,以让编译器识别出来进而降低函数调用的开销.
static const bool debug = 0;
class Trace {
public:
Trace(const char* s, Channel* c): {
if (debug) {
name = s;
cp =c;
if (cp->trace_file) {
*(cp->trace_file) << "begin " << name << endl;
}
}
}
~Trace() {
if (debug) {
if (cp->trace_file) {
*(cp->trace_file) << "end " << name << endl;
}
}
}
private:
Channel* cp;
const char* name;
};
注意,如果debug是false的话,构造函数中根本不会初始化cp
,name
这两个成员.
生成对象审计跟踪
上面只是讨论了函数的跟踪,我们也可以将Trace作为一个类的成员变量使用.进而跟踪类的创建和销毁是否匹配,有没有存在的内存泄漏.
我们将跟踪对象的Trace类实现为Obj_trace并且直接输出到cout
.
class Obj_trace {
public:
Obj_trace():ct(++count) {
std::cout << "Object " << ct << "constructed" << std::endl;
}
~Obj_trace() {
std::cout << "Object " << ct << "destroyed" << std::endl;
}
Obj_trace(const Obj_trace& ):ct(++count) {
std::cout << "Object " << ct << "copy constructed" << std::endl;
}
Obj_trace& operator=(const Obj_trace&) {
return *this;
}
private:
static int count;
int ct;
};
int Obj_trace::count = 0;
比较特殊的是赋值运算符的实现,因为没有新的对象产生,所以我们没有赋值ct
.将ct
当作每个对象唯一的编号看待.