如何正确使用C++多重继承

原创文章,转载请注明:转载自Soul Apogee
本文链接地址:如何正确使用C++多重继承

C++多重继承一直是一个让人搞不太清楚的一个问题,但是有时候为了实现多个接口,多重继承是基本不可避免,当然在Windows下我们有强大的COM来帮我们搞定这个事情,不过如果你想自己实现一套类似于COM的东西出来的时候,麻烦事情就来了。

在COM里面,有两个很基础的,而且我们都会用到的特性:
1. 纯虚接口:一般使用一个只有纯虚函数的类来作为接口
2. 引用计数:在C++中一般都使用引用计数来管理对象的生命周期

这两个功能在一般设计C++接口的时候也经常用到。其实说到底,上面这两个特性牵扯到的是多重继承的二个表现:
1. 多重继承中的数据存储
2. 多重继承中的虚函数

在COM中,纯虚接口是使用的interface来定义的,引用计数是通过IUnknown接口来实现的,所有的接口都是从IUnknown这种接口中派生出来的。当我们需要某一个类实现多个接口的时候,就经常会遇见这样的多重继承:
multi-inheritance-com:
multi-inheritance-com
哦?!是不是很眼熟,ios,istream,ostream,iostream。。各种C++书籍最喜欢用的一个示例。好吧,现在我们先自己实现一个吧,看看到底要怎么使用多重继承。

多重继承中对象的的数据存储

#include <stdio.h>

class IBase
{
public:
    IBase() : n(0) {}
    virtual ~IBase() {}
    void show() { printf("%dn", n); }
    int inc() { return ++n; }
    int dec() { return --n; }

protected:
    int n;
};

class IA : public IBase
{
public:
    virtual ~IA() {}
};

class IB : public IBase
{
public:
    virtual ~IB() {}
};

class CImpl : public IA, public IB
{
public:
    virtual ~CImpl() {}
};

int main(int argc, char* argv[])
{
    CImpl o;
    IA *pA = &o;
    IB *pB = &o;

    pA->inc();
    pA->show();

    pB->dec();
    pB->show();

    return 0;
}

编译,OK,成功了!好,运行试一试。
run-result-1:
run-result-1
为什么是1和-1呢?明明n只在继承的一个类IBase里面有,一次加1,一次减一,结果不是应该是1和0么?是不是很奇怪?

这便是使用多重继承的时候经常产生的第一个问题:多副本的数据存储。
当然这个问题很好解决,只需要使用虚继承即可解决。只需要在IA和IB的定义中,在public IBase前加入virtual关键字即可。

class IA : virtual public IBase
class IB : virtual public IBase

现在再让我们来看一看运行结果:
run-result-2:
run-result-2

结果已经正确了,为什么会发生这种情况呢?虚继承到底干了些什么呢?

我们先来看看在没有使用虚继承的情况下,CImpl的在内存中是怎么样的:
cimpl-memory-normal:
cimpl-memory-normal

对于普通的public继承(非虚继承),C++会将基类和派生类的内容放在一块,合起来组成一个完整的派生类,在内存上看,它们是连在一起的。按照这样的规则,在这里,IA和IB中就会各包含一个IBase,而IA,IB和CImpl中的部分内容又共同组成了CImpl。在将CImpl对象o的指针转型成IA的指针pA过程中,指针将被移动到IA所在的部分区域,同样在转型成IB的过程中,指针将被移动到IB所在的部分区域(也就是说,转型之后,指针的值都不一样)。在之后的操作中,pA操作的便是IA这个部分中的IBase,而pB操作的便是IB这个部分中的IBase,最后IA部分中的IBase变成了1,而IB部分中的IBase变成了-1。所以输出的结果也就变成了1和-1了。

之后我们修改成了虚继承,看看到底发生了什么?
cimpl-memory-virtual:
cimpl-memory-virtual
原来的IA和IB中的IBase部分变成了一个指向基类的指针,而基类也变成了一个单独的部分。这样一旦对基类做任何的修改,都会通过这个指针重定向到这个独立的基类上去,于是,就不存在多副本的数据存储了,这个诡异的问题也就解决了。但是当然从这个图上我们也可以看到,使用虚继承后,访问数据多了一次跳转,这多出的一次跳转将导致效率的下降一倍甚至更多,所以如果一个类使用的非常频繁,很明显应该尽量避免使用虚继承。

二义性

当然数据的存储只是使用多重继承中遇到的一个问题,现在我们来看另外一个问题,函数的二义性。
首先我们先把数据的存储抛开,单纯的来看一个只有函数的继承关系。

class IBase
{
public:
    virtual ~IBase() {}
    void foo() { }
};

class IA : public IBase
{
public:
    virtual ~IA() {}
};

class IB : public IBase
{
public:
    virtual ~IB() {}
};

class CImpl : public IA, public IB
{
public:
    virtual ~CImpl() {}
};

int main(int argc, char* argv[])
{
    CImpl o;
    o.foo();    // 直接调用CImpl的foo函数

    return 0;
}

编译一下,试试。
error C2385: ambiguous access of ‘foo’
could be the ‘foo’ in base ‘IBase’
or could be the ‘foo’ in base ‘IBase’
error C3861: ‘foo’: identifier not found

出错了!杯具。。为什么?错误还这么奇怪,神马叫做可以是IBase中的foo又可以是IBase中的foo呢?

这就是使用多重继承的时候经常产生的第二个问题:二义性。
在使用多重继承时,如果有两个被继承的类拥有共同的基类,那么就很容易出现这种情况。那什么是二义性呢?
我们先来看一个更简单的继承关系:

class A
{
public:
    void foo();
};

class B : public A
{
public:
    void foo();
};

class C : public B
{
public:
    void foo();
};

我们可以把继承关系中,两个类之间沿着基类方向的相隔的继承级数看成一个距离,那么C到A的距离是2,B到A的距离就是1。当然距离不能为负。
当我们对ABC中某个对象调用foo函数的时候,编译器会优先选择离当前指针类型的距离最短的一个函数实现去调用,也就是说,foo函数的查找路径是C->B->A,找到一个最近的去调用。
而对于我们当前这个继承关系来说,IA和IB还是各包含一份IBase的实例,虽然在内存里这里仅仅是包含一份数据,但是在编译的过程中,IA和IB中还包含了一份从IBase中继承下来的函数列表。所以有两个包含有foo函数类与CImpl类的距离是一样的,所以在对CImpl调用foo函数,就产生了所谓的二义性,除非我们指定使用IA::foo或者IB::foo,否则编译器将无法决定使用哪一个基类的foo函数。

o.IA::foo();    // 指定调用CImpl从IA部分继承过来的foo函数,这样就可以编译通过了。

当然如果我们这样写代码也是不行的:

IBase *pBase = &o;    // 指针转义时的二义性,不知道是使用IA中的IBase部分,还是IB中的IBase部分
o.inc();                    // 数据访问时的二义性,不知道是访问IA中IBase部分的n,还是IB中IBase部分的n

多重继承中的虚函数

既然直接使用多重继承会有如此多的问题,那么我们能不能通过虚函数来解决这个问题呢?

这里小小的提一下,刚刚二义性里面说到两个类的距离,对于编译器来说,一般是找离当前的类距离最近的函数实现来调用(或者数据来访问),而虚函数则是让编译器做相反的事情:找一个离当前类反向距离最远的函数实现来调用。

好,我们先把上面的程序做一点点小改变,把foo()函数变成一个虚函数,看看有什么变化。

class IBase
{
public:
    virtual ~IBase() {}
    virtual void foo() {}    // 变成虚函数了
};

编译,结果还是失败。
error C2385: ambiguous access of ‘foo’
could be the ‘foo’ in base ‘IBase’
or could be the ‘foo’ in base ‘IBase’
error C3861: ‘foo’: identifier not found

产生问题的原因依然是二义性。即便换成virtual函数,也不能改变二义性这个问题。为什么呢?
因为我们是用的.运算符来访问的,而不是用指针,所以这里虚函数和普通函数没有任何区别。=.=。。。
好,我们再来小小的修改一下,把他变成指针,让他通过虚表去访问,看看行不行。

CImpl *p = &o;
p->foo();

编译,结果。。。还是一样失败。。。
好吧,我们可以把调用foo()的几句话都去掉,来看看CImpl中生成的虚表到底是个什么样子。
debug-result-vptr-1:
debug-result-vptr-1

在这个实例中,IA和CImpl部分公用一个虚表,而IB则使用另外的一个虚表(两个虚表这个特性主要是在指针类型转换的时候有用,这里就不说了)。
在这IA的虚表中存在一个指向IBase::foo()的指针,在IB的虚表中也存在一个指向IBase::foo()的指针,所以在CImpl中,可以找到两个IBase::foo()函数的指针,这样,编译器就无法确定到底应该使用哪一个IBase::foo()函数作为他自己的foo()函数了。二义性也就产生了。

既然如此,那解决起来就没有什么别的办法了,只能把foo函数的最终在CImpl中实现一次了。

class CImpl : public IA, public IB
{
public:
    virtual ~CImpl() {}
    virtual void foo() { }
};

int main(int argc, char* argv[])
{
    CImpl o;
    o.foo();

    CImpl *p = &o;
    p->foo();

    return 0;
}

编译一下,通过了!对于o.foo()来说,这当然是意料之中,离CImpl距离最近的foo函数实现,就是CImpl自己嘛,当然没有问题。
对于后面这个p->foo()的调用,编译器现在也已经可以决定对于CImpl这个类来说,离他最远的foo函数调用是谁了——也是他自己。
所以这里就不会产生二义性的问题了。

在多重继承中编译器对this指针的修正
这里再让我们来看看这次编译出来的虚表,看看还有什么发现。
debug-result-vptr-2:
debug-result-vptr-2

0x004112a3 [thunk]:CImpl::foo`adjustor{4}’ (void) *
这个看上去很怪的函数是什么呢?我们反汇编一下他看看。
virtual-function-wrapper:
virtual-function-wrapper

这里可以看到有一句汇编指令:sub ecx, 4。这条指令的左右其实是在修正this指针。
因为从IB的虚表来的请求,this指针都是指向CImpl中IB的部分,而当调用CImpl中的foo函数时,如果还使用IB的this指针,那么程序就会出错,所以这里需要先将this指针修正为CImpl所在的地址,才能调用CImpl的foo函数。

在程序运行的时候,this指针一般被存储在ecx寄存器中,或者当前函数的第一个参数传递进去,不过不同的语言或者不同的编译器编译出来的代码可能会不一样。

我们这里的析构函数都是虚函数,所以我们还可以在截图中看到,编译器会对析构函数做同样的处理。

如何同时解决数据访问和二义性问题呢

貌似到现在都只提到最简单的一种多重继承的情况,但是实际上我们已经遇到了很多的问题了,既然多重继承中会有这么多问题,那我们有没有什么比较通用的方法能把他们一起解决了呢?
方法肯定是有的:
1. 使用虚继承
这算是一种确实可行的方法,只是说会带来额外的时间和空间的开销,访问任何一个数据,都需要通过虚继承表进行跳转,不过一般来说够用了。

2. 虚函数当接口,继承多个接口,统一实现
这个思想就类似于COM了,只是说COM用的是纯虚函数,对于那些会产生二义性的类,我们在最后都实现一边,这样就不会有问题了。这样带来的时间开销也仅仅是调用时查询一次虚表。但是麻烦的地方就是,有时候继承一下,你可能就要实现一下了,比如引用计数神马的,当然你也可以通过模版来简化你的代码。

class IBase
{
public:
    virtual ~IBase() {}
    virtual void show() = 0;
};

class IA : public IBase
{
public:
    virtual ~IA() {}
    virtual int inc() = 0;
};

class IB : public IBase
{
public:
    virtual ~IB() {}
    virtual int dec() = 0;
};

class CImpl : public IA, public IB
{
public:
    CImpl() : n(0) {}
    virtual ~CImpl() {}
    int inc() { return ++n; }
    int dec() { return --n; }
    void show() { printf("%dn", n); }

private:
    int n;
};

3. 通过纯虚函数实现模版方法,将函数转移
这种实现比较复杂,wtl中用的比较多,一般是用在引用计数上,好处很明显,就是可以继承,不用每个类都实现一个引用计数,而只用将新的基类的引用计数转移至原本存在的类上就可以了。

class IBase
{
public:
    virtual ~IBase() {}
    void foo() {}
};

class IA : public IBase
{
public:
    virtual ~IA() {}
};

class IShifter
{
public:
    virtual ~IShifter() {}
    void foo() { do_foo(); }

protected:
    virtual void do_foo() = 0;
};

class IB : public IShifter
{
public:
    virtual ~IB() {}
};

class CImpl : public IA, public IB
{
public:
    virtual ~CImpl() {}
    void foo() { IA::foo(); }

protected:
    virtual void do_foo() { IA::foo(); }
};
Posted in 03 Binary Life. Tags: , . 一条评论 »

Google Breakpad 完全解析(二) —— Windows前台实现篇

Table of contents for Google Breakpad 完全解析

  1. Google Breakpad 完全解析(一) —— Windows入门篇
  2. Google Breakpad 完全解析(二) —— Windows前台实现篇

原创文章,转载请标明出处:Soul Apogee (http://bigasp.com),谢谢。

好,看完了如何使用breakpad,我们现在看看breakpad在Windows下到底是如何实现的呢?

代码结构

在我们来看breakpad是如何实现其强大的功能之前,我们先来看一下他的代码结构吧。

Google breakpad的源代码都在src的目录下,他分为如下几个文件夹:
client:这下面包含了前台应用程序中捕捉dump的部分代码,里面按照平台分成各个子文件夹
common:前台后台都会用到的部分基础代码,字符串转换,内存读写,md5神马的
google_breakpad:breakpad中公共的头文件
processor:用于在后台处理崩溃的核心代码
testing:测试工程
third_party:第三方库
tools:一些小工具,用于处理dump文件和符号表

我们先来看Windows下前台实现的部分,也就是client文件夹下的代码。

breakpad的崩溃捕获机制

在Windows下捕获崩溃,大家很容易会想到那个捕获结构化异常的Api:SetUnhandledExceptionFilter

breakpad中也使用了这个Api来实现的崩溃捕获,另外,breakpad还捕获了另外两种C++运行库提供的崩溃,一种是使用_set_purecall_handler捕获纯虚函数调用产生的崩溃,还有一种是使用_set_invalid_parameter_handler捕获错误的参数调用产生的崩溃。

    if (handler_types & HANDLER_EXCEPTION)
      previous_filter_ = SetUnhandledExceptionFilter(HandleException);

#if _MSC_VER >= 1400  // MSVC 2005/8
    if (handler_types & HANDLER_INVALID_PARAMETER)
      previous_iph_ = _set_invalid_parameter_handler(HandleInvalidParameter);
#endif  // _MSC_VER >= 1400

    if (handler_types & HANDLER_PURECALL)
      previous_pch_ = _set_purecall_handler(HandlePureVirtualCall);

另外由于C++运行库提供的崩溃回调中,并不会提供当前的线程现场和崩溃信息,所以breakpad会自己生成好这些信息,然后请求生成dump。
这里值得一说的是,在非异常崩溃处理中,breakpad获取线程现场使用的函数是RtlCaptureContext而不是GetThreadContext。
RtlCaptureContext只能捕获当前线程的现场,而GetThreadContext可以捕获任意线程的现场,只要有这个线程的句柄即可。
但是GetThreadContext有两个不好的地方:不能获取当前线程的现场;获取现场前必须先用SuspendThread暂停目标线程。
而RtlCaptureContext虽然只能获取当前线程的现场,但是调用他时可以不用暂停线程的运行。
对于breakpad来说,崩溃发生后越早获取现场就越好,所以breakpad使用RtlCaptureContext函数作为他的线程获取函数。

breakpad中的C/S结构

由于breakpad是在进程外抓取dump,所以breakpad需要实现一个C/S结构来处理崩溃进程抓取dump的请求。

1. breakpad跨进程通信的实现
breakpad中使用了命名管道来实现IPC。

在客户端,初始化ExceptionHandler的时候,如果指定了PipeName,也就表示此时需要使用进程外的dump抓取,ExceptionHandler,会建立一个 CrashGenerationClient的对象,由这个对象连接服务端,将自己注册到服务端上去。
大家可以参看exception_handler.cc中的ExceptionHandler::Initialize函数。

在服务端,初始化CrashGenerationServer的时候,就会建立一个命名管道,并等待客户端来连接。一旦有客户端连接上来,服务端会为每一个客户端生成一个ClientInfo的对象,之后用这个对象来管理所有的客户端,一旦有崩溃发生,服务端都会从这个对象中取出dump所需要的信息。
大家可以参看crash_generation_server.cc中的CrashGenerationServer::HandleReadDoneState函数。

2. breakpad捕获崩溃生成dump的流程
breakpad进程外生成dump的流程大概如下:
google-breakpad-out-of-process-dump:
google-breakpad-out-of-process-dump
这段流程的代码就是crash_generation_client.cc和crash_generation_server.cc。

有两个简单的问题,这里说明一下,高手们就请直接忽略吧,咩哈哈:
在服务端如何为客户端生成事件句柄?
使用DuplicateHandle,即可把任意一个内核对象的句柄复制到其他进程,并且可以指定产生的句柄的权限。

如何异步的等待一个事件?
使用RegisterWaitForSingleObject,即可异步的等待一个事件,当事件发生的时候,就可以回调到一个指定的回调函数中,但是要注意的是,RegisterWaitForSingleObject会在一个新的线程中来等待这个事件,此处很容易产生多线程的调用,需要注意线程问题。

3. 服务端关键数据结构:ClientInfo
ClientInfo是服务端中最重要的数据结构,服务端通过它来管理所有的客户端。客户端注册时,会保存或生成里面所有的信息,在客户端请求生成dump的时候,服务端就会通过ClientInfo获取所有客户端的信息。ClientInfo中保存了如下信息:

  • 客户端进程pid和句柄
  • 生成Minidump的类型
  • 自定义的客户端信息
  • 客户端崩溃的线程ID
  • 客户端崩溃的信息
  • 客户端请求崩溃所使用的事件句柄

这里有一个问题:在客户端发生崩溃时,服务器如何通过ClientInfo获取到客户端的崩溃信息呢?

客户端中有几个用于保存崩溃信息的变量,在注册时,客户端会将这几个变量的地址发送至服务端,服务端将其保存在ClientInfo中,然后当崩溃发生的时候,服务端就可以通过ReadProcessMemory读取客户端中的信息,从而生成dump。这样做就避免了每次发生崩溃,都要通过Pipe将崩溃信息传递到服务端中去了。

这些变量分别是:崩溃的线程ID,EXCEPTION_POINTERS和MDRawAssertionInfo。
EXCEPTION_POINTERS和MDRawAssertionInfo的区别在于,异常崩溃的信息会被写入EXCEPTION_POINTERS,非异常崩溃(非法参数和纯虚函数调用)的信息会被写入MDRawAssertionInfo中。

dump文件的上传

在breakpad的工程中,有一个工程叫做:crash_report_sender,里面是一个上传崩溃文件的类,他的实现很简单,他使用Windows Internet Api来完成dump文件的上传。
在使用crash_report_sender时,可以为其指定一个checkpoint_file。

explicit CrashReportSender(const wstring &checkpoint_file);

这个文件只有一个作用,就是用来保存上次上传崩溃的时间和今天上传过的崩溃的次数。通过这个文件,我们就可以来设置每日上传的崩溃的最大数量。

CrashReportSender::CrashReportSender(const wstring &checkpoint_file)
    : checkpoint_file_(checkpoint_file),
      max_reports_per_day_(-1),
      last_sent_date_(-1),
      reports_sent_(0) {
  FILE *fd;
  if (OpenCheckpointFile(L"r", &fd) == 0) {
    ReadCheckpoint(fd);
    fclose(fd);
  }
}

ReportResult CrashReportSender::SendCrashReport(
    const wstring &url, const map<wstring, wstring> &parameters,
    const wstring &dump_file_name, wstring *report_code) {
  int today = GetCurrentDate();
  if (today == last_sent_date_ &&
      max_reports_per_day_ != -1 &&
      reports_sent_ >= max_reports_per_day_) {
    return RESULT_THROTTLED;
  }

  // 上传文件部分代码,省略
}

调整每日上传崩溃的最大数量的函数是set_max_reports_per_day。

需要注意的是:在上传dump文件的时候,crash_report_sender并不会对dump文件进行分析,而是直接上传整个dump文件,如果你需要上传的dump文件非常大的话,可以考虑把崩溃分析处理的逻辑放入前台,通过去重或者直接上传分析结果,减少上传的文件大小。

breakpad存在的问题

进程外生成dump有很多好处,其中最大的好处就是不会被崩溃进程影响,这样dump的过程就不容易出错,但是这样也有一定的弊端。

1. 部分崩溃无法抓取
在一些极端的崩溃,如堆栈溢出之类的崩溃,进程外抓取dump有时候会失败。

2. 无法抓取死锁或者其他原因导致的进程僵死
breakpad现在没有检测进程死锁的代码,也没有在服务端控制客户端请求dump的代码,所以现在breakpad无法抓取死锁等进程僵死的问题。不过因为breakpad的定位是处理崩溃,如果有这种需要的童鞋,可以自行修改breakpad的代码,添加这些功能。

3. 对服务端有依赖
如果指定了在使用进程外抓取dump,breakpad对服务端就有依赖。主要体现在抓取dump时,如果服务端不存在,客户端将无法正常抓取dump,甚至有时会出现阻塞。

当然对于这些问题,随着breakpad的发展肯定会越来越完善。如果,你遇到了了这些问题,而又绕过不了,那就改代码,并且提交给breakpad吧,开源项目就是这么发展的。

好,到此breakpad的Windows实现就已经说完了,如果有神马问题,还请多多指教。谢谢大家。

Google Breakpad 完全解析(一) —— Windows入门篇

Table of contents for Google Breakpad 完全解析

  1. Google Breakpad 完全解析(一) —— Windows入门篇
  2. Google Breakpad 完全解析(二) —— Windows前台实现篇

原创文章,转载请标明出处:Soul Apogee (http://bigasp.com),谢谢。

Google breakpad是一个非常实用的跨平台的崩溃转储和分析模块,他支持Windows,Linux和Mac和Solaris。由于他本身跨平台,所以很大的减少我们在平台移植时的工作,毕竟崩溃转储,每个平台下都不同,使用起来很难统一,而Google breakpad就帮我们做到了这一点,不管是哪个平台下的崩溃,都能够进行统一的分析。现在很多工程都在使用他:最著名的几个如Chrome,Firefox,Picasa和Google Earth。另外他的License是BSD的,也就是说,我们即便是在商业软件中使用,也是合法的,哈哈,这么好的东西,我们能放过么?现在就让我们来看看这个神奇的软件吧。

原理简介

breakpad抓取dump的方式和一般我们抓取dump的方式不一样。在breakpad的wiki上有一幅图可以很好的概括他的原理。

breakpad把应用程序分成三个部分,代码,breakpad客户端和调试信息。

1. 在build system中,通过symbol dumper用平台相关的调试信息生成平台无关的symbol文件。这样做的好处很明显,一旦平台无关了,所有平台的崩溃就可以做统一的分析了。
2. breakpad采取进程外转储和分析崩溃的方式,他使用C/S结构,客户端用来捕获当前进程中发生的崩溃,并通知服务端崩溃发生。服务端用来响应客户端,抓取dump文件。这样做的目的是为了减少崩溃进程对dump的影响。
3. Dump生成后转发到崩溃分析器中,这个部分可以在本地也可以在服务器上,他对Dump文件进行解析,生成可读的堆栈信息。

这就是breakpad处理dump大概的流程。

对于原理的介绍google写的已经相当好了。更多的详细信息,可以直接移步到breakpad的wiki

安装和编译

breakpad的编译比较曲折,所以在此记录一下。

编译breakpad,请确认你的机器上装有以下的软件:
1. python 2.4.3
请不要使用python3,会报错。另外python2中推荐这个版本,使用新的版本在编译其他google的工程时有时会报错

2. Windows SDK 7
如果没有这个,编译会报错。另外这个是在线安装,时间很久,最好并行做其他的事情。

3. VS2005的补丁
KB918559
KB926601
KB935225
KB943969
KB947315

已经安装了以上软件的童鞋,就可以开始进行下面的工作鸟

1. 使用svn把代码checkout下来

# Non-members may check out a read-only working copy anonymously over HTTP.
svn checkout http://google-breakpad.googlecode.com/svn/trunk/ google-breakpad-read-only

2. 设置Windows SDK 7
装过其他版本Windows SDK的童鞋,记得一定要进行这一步,SDK的安装程序,并不会帮你设置VS。
运行开始菜单->程序->Microsoft Windows SDK v7.0->Visual Studio Registration->Windows SDK Configuration Tool,选择v7.0,点击Make Current。

3. 为python设置环境变量
由于breakpad使用python来生成Windows下的工程文件,所以需要将python所在目录,设置到环境变量PATH中去。

4. 生成Windows工程文件

cd "源码目录/src/tools/gyp"

# 注意,此处不能使用全路径,不然会出错
gyp.bat "../../client/windows/breakpad_client.gyp"

此时,在src/client/windows下就可以看到生成好的breakpad_client.sln了。运行吧!

5. Hello World!
编译build all,现在一般是不会报错了,如果报错,请检查是不是漏了什么步骤,特别是补丁。
编译完成之后,运行crash_generation_app吧,这是他的测试程序,dump的默认位置保存在C:Dumps下,请注意先建立好目录,不然会无法使用。
启动测试程序之后,此时还不能抓取dump,因为这个是breakpad中的服务器端,需要再启动一个测试程序,在第二个测试程序中,我们就可以试验Client菜单中的各种崩溃了。这些崩溃都会被抓住转存到C:Dumps目录下。

如何使用breakpad

在Windows下使用breakpad的方法很简单,只需要创建一个ExceptionHandler的类即可,大家可以在crash_generation_app这个工程中找到示例代码,也可以直接移步Wiki,上面说的也很详细。

1.进程内抓取Dump文件

进程内抓取Dump文件是最简单的breakpad的用法。使用方法很简单:

const std::wstring s_strCrashDir = L"c:\dumps";

bool
InitBreakpad()
{
    google_breakpad::ExceptionHandler *pCrashHandler =
        new google_breakpad::ExceptionHandler(s_strCrashDir,
        onExceptionFilter,
        onMinidumpDumped,
        NULL,
        google_breakpad::ExceptionHandler::HANDLER_ALL,
        MiniDumpNormal,
        NULL,
        NULL);

    if(pCrashHandler == NULL) {
        return false;
    }

    return true;
}

2.进程外抓取Dump文件

使用进程外抓取Dump时,需要指定服务端和客户端,在服务端中需要创建CrashGenerationServer的实例,而在客户端中则只需要创建ExceptionHandler即可。此外,如果服务端自己需要抓进程内的Dump,请将pipe的参数置为NULL。

const wchar_t s_pPipeName[] = L"\\.\pipe\breakpad\crash_handler_server";
const std::wstring s_strCrashDir = L"c:\dumps";

bool
InitBreakpad()
{
    google_breakpad::CrashGenerationServer *pCrashServer =
        new google_breakpad::CrashGenerationServer(s_pPipeName,
        NULL,
        onClientConnected,
        NULL,
        onClientDumpRequest,
        NULL,
        onClientExited,
        NULL,
        true,
        &s_strCrashDir);

    if(pCrashServer == NULL) {
        return false;
    }

    // 如果已经服务端已经启动了,此处启动会失败
    if(!pCrashServer->Start()) {
        delete pCrashServer;
        pCrashServer = NULL;
    }

    google_breakpad::ExceptionHandler *pCrashHandler =
        new google_breakpad::ExceptionHandler(s_strCrashDir,
        onExceptionFilter,
        onMinidumpDumped,
        NULL,
        google_breakpad::ExceptionHandler::HANDLER_ALL,
        MiniDumpNormal,
        (pCrashServer == NULL) ? s_pPipeName : NULL, // 如果是服务端,则直接使用进程内dump
        NULL);

    if(pCrashHandler == NULL) {
        return false;
    }

    return true;
}

使用breakpad的时候,有两个地方需要注意:
1. 记得把breakpad的solution下的几个工程,包含到你开发的工程中,或者直接包含他们的lib。
common:基础功能,包含一个对GUID的封装和http上传的类。
exception_handler:用来捕获崩溃的类。
crash_generation_server:breakpad的服务端,用来在产生崩溃时抓取dump。
crash_generation_client:breakpad的客户端,用来捕获当前进程的崩溃。

2. 在初始化breakpad之前,记得先创建好dump文件的目录,不然breakpad服务端将不能正常的写dump,这会导致breakpad客户端在崩溃时无限等待服务端dump写完的消息,最后失去响应。

《重构》读书笔记 —— 如何让你的代码变得更好

原创文章,转载请标明出处:Soul Apogee (http://bigasp.com),谢谢。

好不容易读完了重构——2010年下半年唯一读了的一本书。只能说这半年以来很忙,来了北京之后连个喘息的机会都没有,半年都没有管过博客了,写写读后感,就当作是除除草吧。
以下只是我的一些读书笔记,不过虽然是笔记,但是里面并没有关于重构方法的细节记录,而只是粗略的记下了我理解的重构的大致思路。和《重构》这本书不同,这本书本身说的很细,作者把每一步重构的都拆成了很小的步骤,每一步都可以轻松的回滚,所以有兴趣的可以去看看这本书,肯定都会有一定收获的。
我现在只是一名小菜,过段时间之后,我自己回头看这篇文章,可能都会觉得自己很傻,所以如有不对,还请多多指教,如果实在看不顺眼,就请纯当笑话看吧。
好,下面进入正题。

注:因为是读书笔记,所以可能会有抄袭等等奇奇怪怪的问题,如果发现有版权问题,请联系我,我会尽快删除本文。

===================== 我是欢乐的分隔线 =====================

重构的目的

我们总希望自己的代码能写的更加好看,以至于我MM把我写代码比喻成为打扮自己。没有错,代码就是一个程序员的外表。而重构就是为了让你的代码更加好看。
但是仅仅是好看而已么?
不是,好看意味着简洁,好理解,好维护,好扩展。这就是我们真正要达到的目的。
那这些目的最终又是为了完成什么呢?我们最后再说。

设计模式和重构

设计模式想必大家肯定已经不陌生了,平时在开发的时候,大家也肯定用过各种各样的模式来解决遇到的问题。而当你现在手头的代码使用的模式不能很好的支持你继续开发的时候(可能是不够灵活,也可能是过于灵活),你就需要使用重构来修改它,将你的代码变得更加优雅。

重构与测试

重构与测试的关系为什么要写在前面?因为测试实在是太重要了。
我在最近的项目中备受重构的挫折,为什么?因为没有测试。和很多程序员一样,经常迫于项目的压力,没有时间去实现一些测试用例,或者有些测试用例不方便实现,等到需要重构的时候,问题就出现了。

“我不敢改这段代码,改了要是出错了怎么办?”
“但是这个版本一定要有这个功能啊。”
“那我尝试着改吧,出了错再调。”

如果连这段代码的作者都这么说,谁还敢改这段代码?而偏偏修改中又引入了其他的Bug,结果就是和这个功能相关的所有人都为此买单。大家的时间就这么浪费在了无意义的事情上。如何避免这个问题呢?测试!这是我自己受到的血淋淋的教训。

写测试代码的时候,经常会遇见两个问题:

Q:“刚开发完一个功能,紧接着就要开发另外一个,没有时间来写这些测试代码啊?”
A:在开发这个功能之前,请先些测试用例,这样不仅仅节省了写测试用例的时间,而且能帮助你更快的设计接口,避免走弯路,因为你已经在考虑如何使用这个接口了。

Q:“我还有开发任务,没有办法写全面的测试用例啊!”
A:一个全面的测试程序基本是不可能写出来的,功能一直在加,开发一直在进行,没有那么多时间给我们补全测试用例。但是我们不能因噎废食,测试对我们有好处,我们只要实现可能导致出错,或者实现原来出过错的测试用例即可。能找出大部分问题,总比什么问题都找不出,之后去补救的好。

重构的手段

不要把重构看成一件很大或者需要花费很多时间的工程,也许它只是添加或者删除一个函数的参数,也许是从一种设计模式切换到另外一种,当然也有需要大规模的修改代码的结构的时候,具体看你遇见的问题来进行操作。

但是请记住:不管过程如何,重构的目的和本质总是不变的。不要为了重构而重构。

如何让代码变得更加好看,答案其实很废话:封装。把不变的部分放在一块,把变化的部分提取出来封装在另外一块。
而何时封装和如何封装,正是整本书在讲的内容。我归纳了一下,大概如下:

1.让人迷惑的临时变量

临时变量容易让人产生迷惑,一个变量被来回的赋值,在某一段代码中他表达的是一个意思,在另外一段代码中,表达的又是另外一个意思,到最后就会让人产生疑惑,这个变量到底是什么?所以使用临时变量一定要小心。如果遇见代码中有很多临时变量,我们可以通过以下一些步骤来消除他们:
1)如果一个临时变量在代码中表示了多个意思,那么将代码分段,给每段的临时变量取上不同的名字
2)将能用函数查询代替的临时变量全变成函数查询,期间请先不要担心效率
3)将能提取的代码提取成让人更好理解的函数
4)如果还有顽固的让人不爽的临时变量,请修改你的算法

当然并不是所有临时变量都是不好的,我们的目的是让代码更加易懂,所以只要这个临时变量能帮助我们理解这段代码,那这个变量就是有意义的。比如用一个临时变量来代替一个复杂的表达式。

2.简化条件判断

条件判断是程序开发中不可避免的一部分,而当某些语句执行的条件比较复杂的时候,如果处理不好,各种各样的条件判断就会阻碍我们理解这段代码真正的目的。而以下就几个方法,能将复杂的条件判断变得清晰。
1)用易懂的临时变量或者查询来代替复杂的条件。比如isValid()函数,就比xxx & FLAG_XXX != 0 && xxxx != “” …,要清晰许多。
2)确定这段代码的真实目的,对于处理前的条件判断,尽量使用ASSERT和卫语句,发现错误就尽快退出处理,减少条件判断嵌套的层次。
3)如果是类型的判断,请引入子类+多态来解决这个问题。
4)对于NULL的处理,可以引入NULL对象。通过多态,定义在对象为NULL时的行为。

在写代码时,请不要在意如“一个函数,一个出口”,“使用breakcontinue是破坏代码的行为”之类的,一直被人灌输的思想。只要能让代码易懂,请尽情的使用这些关键字。

3.减少重复代码

重复代码是万恶之源(当然还有各种万恶之源,如调试器,我们都忽略吧),这点想必只要做过开发的人都经历过,改了这里忘了改那里。既然是相同的代码,那么就请放在一起来吧,尽量不要让重复的代码在你的代码中出现。避免重复代码的方法,有下面几种:
1)如果代码完全一样,可以直接提取公共函数
2)如果你使用的是一个库,无法修改他本身的代码,那么我们可以建立代理函数或者代理类来减少重复代码
3)对于相似的代码,可以使用模版方法,Trait技术来减少重复的代码

减少重复代码还有一个很重要的方法,那就是避免纯数据类。

纯数据类是一个很神奇的东西,OO开发带来的一个很大的优点就是可以把数据和数据的操作封装在一起,让代码看起来更加自然。所以在代码中应该尽量避免出现纯数据类,如果经常出现对一个纯数据类的操作,请将他直接封装在这个数据类中。
比如:文件路径,本身它是一个字符串,习惯Windows开发的童鞋喜欢直接调用Window Api来对路径进行操作,但是有些操作需要多个Api组合完成。这时候,我们无意中就会写出很多重复的代码,实际上,我们把他封装成一个文件路径的类,就可以很好的避免这些重复代码的产生。

4.封装变化

如何让代码看起来简单?有一个标准,看起来变化越少的代码,越易懂。如果一段只有几行的简单代码,那肯定是很容易懂的。既然这样,那么如果我们把变化的部分都封装起来,那么代码肯定看起来就简单了。
如何封装变化,大家可以猛烈的参考设计模式。如果没有看过,Gang of FourHead First的都不错。
对于经常用的方法,我这里总结了一下(以下不变化的意思是,不变或者变化较少):

1)数据变化不会导致行为发生变化
在这种情况下,你需要做的就仅仅是封装数据了。
l尽量避免纯数据类,因为行为是固定的,所以请把行为放入数据类中,让它变得更加有用。
l有一些基本类型实际上也是纯数据类,如上文提到的文件路径。
l如果这个变化的数据是几个固定的型别码,那么请用类将它封装起来,再提供几个静态变量表示各种类型,避免出现超出范围的型别码。

2)数据变化会导致行为发生变化
l一般这种情况都是因为数据本身表示类型,这种情况下,请引入子类+多态。解决这种问题设计模式有很多:状态模式,职责链和命令模式等等。

3)即使数据不变化,行为本身也可能发生变化
l如果行为本身会有变化,此时,我们就需要使用反封装的方法,将数据操作提取出来。此类有一个典型的设计模式:Strategy

在封装变化的时候,还有一个问题要注意:
如果出现了“一个新的变化,你要修改很多个类的方法”的问题,请将这些方法移入一个类中,或者提取出一个公有的类。避免这种问题。

5.整理你的代码

有一个很有趣的关于bug数量的规律:一个模块的逻辑代码行数在200-400行之间的时候,bug数量是最少的。根据这个规律,如果你某个模块的代码过多,那么你就应该开始拆分他了。
而对于实现来说,除了一些变态的Trick,一般越是短小的代码越容易理解,所以如果你写了一段惊世骇俗的冗长代码,请尽快缩短他,不然后来的人要理解他就比较痛苦了。

1)过长的函数
一段非常长的函数,常会让希望理解他的人望而却步。因为他没有办法很快的理解这段函数的目的,深陷在各种逻辑的泥沼中不能自拔。
l对于过长的函数,可以先检查里面是否有重复代码,或者可以分段,如果有,请提取成新的函数
l检查里面是否有临时变量,看能不能用上述方法简化
l如果还是过长,对于缩短代码,这里有一个杀手锏,把这个函数变成一个方法类。
如:有一个很复杂的函数:ComplexBehavior(xxx),其中的逻辑无法简化。那么可以把它转化成一个叫做ComplexBehavior的类,在其中提取方法,达到缩短过长函数的目的。之后把调用的形式变成ComplexBehavior(xxx).run()即可。

2)过长的参数列表
如果一个函数如果参数较多,可能会导致这个函数难以理解,不方便使用,而且这种函数经常还会遇见需要添加新的参数的情况,这种修改有时是很痛苦的。而在OO的程序中,参数列表一般要短很多。为什么呢?答案就是封装。如何封装?
l首先,看有没有能省去的参数,是否存在在对象成员变量中的参数,如果有,请删除它
l然后,自然而然想到的,就是整合参数,因为很可能这些参数之间是互相关联的,那么把这些互相关联的参数,提取成一个对象吧
l不要想着提高效率或者这个函数不需要某个对象的某些参数,而不把这个对象传入(除非是其他部分的接口)。这种效率不是我们应该考虑的事情,这种提高一般都是零头,而对于不需要的情况,你也不知道什么时候他就需要这些东西了,都传进去总是不会错的。
l如果还是不行,请继续用那个杀手锏,建立一个方法类,把所有的参数都转化成他的成员变量。

3)过长的类
在代码中,大家是不是经常遇见吓人的万能的工具类呢?如果一个类,他本身想做很多事情,那么这个类必然会变得过长。
l在设计一个类的时候,请首先确定好这个类的目的是什么
l对于已经过长的类,请先检查有没有重复代码,有请先消除
l确定好他到底想做那些事情,然后将各个部分分别提取成单独的类
l如果功能已经很单一了,只是实现本身复杂,请将他的实现分解成多个类
l如果这个类多个方法很类似,只是使用方式不同,请尝试提取出共有的接口,然后找出确定使用方式的型别码,然后利用多态来简化这个类

6.胶合层

在代码中,由于自顶向下设计和自下向上实现产生的冲突导致的,用于连接两部分的代码,就是胶合层。他一般出现在接口和代码真正实现的连接处。胶合层有时候是不可避免的,但是如果过厚,就会导致代码难以理解,找很久也发现不了代码的真实在做的事情,所以一般胶合层是越薄越好。如果发现你的代码中间出现了不必要的胶合层,请删除他们。
什么样是不必要的胶合层呢?如果他的存在并不能使你的代码看起来更加简单,而他又没有什么特殊的用途,那么他就是不必要的胶合层。比如一些可有可无的AdapterProxy

7.过多的注释

写注释是件好事,但是如果发现有一大段代码在解释一些写的非常烂的代码的时候,那么尝试重构这段代码吧。因为不管注释写的再多,我们总归是要去理解这段奇特的代码的,所以让代码本身变得可读性更高才是正确的选择。

需要注意的是,在实施重构的时候,请使用“建立新函数之后替换原函数”的方式来实现重构,而不是直接修改,这样才能保证每一步足够小,而且能回滚,举一个例子,删除函数参数。

重构与性能

程序员写代码的时候,最喜欢做的一件事情,那就是下意识的检查自己写过的代码。下面这些问题,不知道你是否问过自己。

“这里用for循环查找,万一数据量大一点,效率会有问题吧?”
“这里用map是不是比vector更好一些?”
“这里遍历询问好么?要不要让每个模块先注册一下他关注的内容?”
“这种实现方式会不会绕太多弯路了,速度会很慢吧”

除非你对程序这部分会承担的数据量或者他所使用组件的性能有非常清楚的了解,否则,请不要幻想着程序会慢在哪些地方,然后擅自的优化代码。因为事实可能并非你的想象,往往对于vectormap的优化,在时间上表现的都只是一个零头。而如果因为这个修改,使你这部分代码变得晦涩难懂,那就太得不偿失了。在优化性能之前,请先实现好整个原型,有了这个,你才能好好的雕琢它。写代码的时候,请谨记KISS原则,优化是之后的事情。

那到底是哪一部分代码导致我的程序性能不好呢?问性能分析工具吧。

Linux开发的童鞋请移步gprofhttp://www.cs.utah.edu/dept/old/texinfo/as/gprof_toc.html
Windows开发的童鞋请自行搜索:aqtime

重构的本质

开头说了很多重构的目的,提高可理解性,降低其修改成本等等。我们再深究一些,完成这些都是为了什么?所有这些重构的手法,为的是什么?为了让代码简单?如果只是为了让代码简单,书中就不会提及如产生变化会导致多个模块调整等等的问题。那重构到底为了什么?
正交性!
看过Unix编程艺术的人应该都知道,这就是重构的本质。如何让每一个动作,只改变一件事情,而不会影响其他。这就是重构想完成的事情。

结束

好了,乱七八糟的写了这么多,至此,就是我看《重构》这本书的一些心得。

===================== 我是欢乐的分隔线 =====================

如果你看到了这个地方,我只能说你强悍,都是海大空的文字,连个像样的图或者代码实例都没有,所以如果你没有开始开一篇博客,就必须看完的强迫症,真的多谢你的耐心。