软件回归测试:什么情况下最需要回归测试?_百度知道7: class Graphics
virtual void drawLine(int x0, int y0, int x1, int y1);
virtual void drawLine(Point p0, Point p1);
virtual void drawRectangle(int x0, int y0, int x1, int y1);
virtual void drawRectangle(Point p0, Point p1);
virtual void drawArc(int x, int y, int r);
virtual void drawArc(Point p, int r);
这里略去了很多与本文主题无关细节,比如 Graphics 的构造与析构、draw*() 函数应该是 public、Graphics 应该不允许复制,还比如 Graphics 可能会用 pure virtual functions 等等,这些都不影响本文的讨论。
这个 Graphics 库的使用很简单,客户端看起来是这个样子。
Graphics* g = getGraphics();
g-&drawLine(0, 0, 100, 200);
releaseGraphics(g); g = NULL;
似乎一切都很好,阳光明媚,符合“面向对象的原则”,但是一旦考虑升级,前景立刻变得昏暗。
虚函数作为接口的弊端
以虚函数作为接口在二进制兼容性方面有本质困难:“一旦发布,不能修改”。
假如我需要给 Graphics 增加几个绘图函数,同时保持二进制兼容性。这几个新函数的坐标以浮点数表示,我理想中的新接口是:--- old/graphics.h
13:12:44. +0800
+++ new/graphics.h
13:13:30. +0800
@@ -7,11 +7,14 @@
class Graphics
virtual void drawLine(int x0, int y0, int x1, int y1);
virtual void drawLine(double x0, double y0, double x1, double y1);
virtual void drawLine(Point p0, Point p1);
virtual void drawRectangle(int x0, int y0, int x1, int y1);
virtual void drawRectangle(double x0, double y0, double x1, double y1);
virtual void drawRectangle(Point p0, Point p1);
virtual void drawArc(int x, int y, int r);
virtual void drawArc(double x, double y, double r);
virtual void drawArc(Point p, int r);
受 C++ 二进制兼容性方面的限制,我们不能这么做。其本质问题在于 C++ 以 vtable[offset] 方式实现虚函数调用,而 offset 又是根据虚函数声明的位置隐式确定的,这造成了脆弱性。我增加了 drawLine(double x0, double y0, double x1, double y1),造成 vtable 的排列发生了变化,现有的二进制可执行文件无法再用旧的 offset 调用到正确的函数。
怎么办呢?有一种危险且丑陋的做法:把新的虚函数放到 interface 的末尾,例如:--- old/graphics.h
13:12:44. +0800
+++ new/graphics.h
13:58:22. +0800
@@ -7,11 +7,15 @@
class Graphics
virtual void drawLine(int x0, int y0, int x1, int y1);
virtual void drawLine(Point p0, Point p1);
virtual void drawRectangle(int x0, int y0, int x1, int y1);
virtual void drawRectangle(Point p0, Point p1);
virtual void drawArc(int x, int y, int r);
virtual void drawArc(Point p, int r);
virtual void drawLine(double x0, double y0, double x1, double y1);
virtual void drawRectangle(double x0, double y0, double x1, double y1);
virtual void drawArc(double x, double y, double r);
这么做很丑陋,因为新的 drawLine(double x0, double y0, double x1, double y1) 函数没有和原来的 drawLine() 函数呆在一起,造成阅读上的不便。这么做同时很危险,因为 Graphics 如果被继承,那么新增虚函数会改变派生类中的 vtable offset 变化,同样不是二进制兼容的。
另外有两种似乎安全的做法,这也是 COM 采用的办法:
1. 通过链式继承来扩展现有 interface,例如从 Graphics 派生出 Graphics2。--- graphics.h
13:12:44. +0800
+++ graphics2.h
13:58:35. +0800
@@ -7,11 +7,19 @@
class Graphics
virtual void drawLine(int x0, int y0, int x1, int y1);
virtual void drawLine(Point p0, Point p1);
virtual void drawRectangle(int x0, int y0, int x1, int y1);
virtual void drawRectangle(Point p0, Point p1);
virtual void drawArc(int x, int y, int r);
virtual void drawArc(Point p, int r);
+class Graphics2 : public Graphics
using Graphics::drawL
using Graphics::drawR
using Graphics::drawA
// added in version 2
virtual void drawLine(double x0, double y0, double x1, double y1);
virtual void drawRectangle(double x0, double y0, double x1, double y1);
virtual void drawArc(double x, double y, double r);
将来如果继续增加功能,那么还会有 class Graphics3 : public Graphics2;以及 class Graphics4 : public Graphics3 等等。这么做和前面的做法一样丑陋,因为新的 drawLine(double x0, double y0, double x1, double y1) 函数位于派生 Graphics2 interace 中,没有和原来的 drawLine() 函数呆在一起,造成割裂。
2. 通过多重继承来扩展现有 interface,例如定义一个与 Graphics class 有同样成员的 Graphics2,再让实现同时继承这两个 interfaces。--- graphics.h
13:12:44. +0800
+++ graphics2.h
13:16:45. +0800
@@ -7,11 +7,32 @@
class Graphics
virtual void drawLine(int x0, int y0, int x1, int y1);
virtual void drawLine(Point p0, Point p1);
virtual void drawRectangle(int x0, int y0, int x1, int y1);
virtual void drawRectangle(Point p0, Point p1);
virtual void drawArc(int x, int y, int r);
virtual void drawArc(Point p, int r);
+class Graphics2
virtual void drawLine(int x0, int y0, int x1, int y1);
virtual void drawLine(double x0, double y0, double x1, double y1);
virtual void drawLine(Point p0, Point p1);
virtual void drawRectangle(int x0, int y0, int x1, int y1);
virtual void drawRectangle(double x0, double y0, double x1, double y1);
virtual void drawRectangle(Point p0, Point p1);
virtual void drawArc(int x, int y, int r);
virtual void drawArc(double x, double y, double r);
virtual void drawArc(Point p, int r);
+// 在实现中采用多重接口继承
+class GraphicsImpl : public Graphics,
// version 1
public Graphics2, // version 2
这种带版本的 interface 的做法在 COM 使用者的眼中看起来是很正常的,解决了二进制兼容性的问题,客户端源代码也不受影响。
在我看来带版本的 interface 实在是很丑陋,因为每次改动都引入了新的 interface class,会造成日后客户端代码难以管理。比如,如果代码使用了 Graphics3 的功能,要不要把现有的 Graphics2 都替换掉?
如果不替换,一个程序同时依赖多个版本的 Graphics,一直背着历史包袱。依赖的 Graphics 版本越积越多,将来如何管理得过来?
如果要替换,为什么不相干的代码(现有的运行得好好的使用 Graphics2 的代码)也会因为别处用到了 Graphics3 而被修改?
这种二难境地纯粹是“以虚函数为库的接口”造成的。如果我们能直接原地扩充 class Graphics,就不会有这些屁事,见本文“推荐做法”一节。
假如 Linux 系统调用以 COM 接口方式实现
或许上面这个 Graphics 的例子太简单,没有让“以虚函数为接口”的缺点充分暴露出来,让我们看一个真实的案例:Linux Kernel。
Linux kernel 从 0.10 的 发展到 2.6.37 的 ,kernel interface 一直在扩充,而且保持良好的兼容性,它保持兼容性的办法很土,就是给每个 system call 赋予一个终身不变的数字代号,等于把虚函数表的排列固定下来。点开本段开头的两个链接,你就能看到 fork() 在 Linux 0.10 和 Linux 2.6.37 里的代号都是 2。(系统调用的编号跟硬件平台有关,这里我们看的是 x86 32-bit 平台。)
试想假如 Linus 当初选择用 COM 接口的链式继承风格来描述,将会是怎样一种壮观的景象?为了避免扰乱视线,请移步观看。(先后关系与版本号不一定 100% 准确,我是用 git blame 去查的,现在列出的代码只从 0.01 到 2.5.31,相信已经足以展现 COM 接口方式的弊端。)
不要误认为“接口一旦发布就不能更改”是天经地义的,那不过是“以 C++ 虚函数为接口”的固有弊端,如果跳出这个框框去思考,其实 C++ 库的接口很容易做得更好。
为什么不能改?还不是因为用了C++ 虚函数作为接口。Java 的 interface 可以添加新函数,C 语言的库也可以添加新的全局函数,C++ class 也可以添加新 non-virtual 成员函数和 namespace 级别的 non-member 函数,这些都不需要继承出新 interface 就能扩充原有接口。偏偏 COM 的 interface 不能原地扩充,只能通过继承来 workaround,产生一堆带版本的 interfaces。有人说 COM 是二进制兼容性的正面例子,某深不以为然。COM 确实以一种最丑陋的方式做到了“二进制兼容”。脆弱与僵硬就是以 C++ 虚函数为接口的宿命。
相反,Linux 系统调用以编译期常数方式固定下来,万年不变,轻而易举地解决了这个问题。在其他面向对象语言(Java/C#)中,我也没有见过每改动一次就给 interface 递增版本号的诡异做法。
还是应了《》中的那句话,Explicit is better than implicit, Flat is better than nested.
动态库的接口的推荐做法
取决于动态库的使用范围,有两类做法。
如果,动态库的使用范围比较窄,比如本团队内部的两三个程序在用,用户都是受控的,要发布新版本也比较容易协调,那么不用太费事,只要做好发布的版本管理就行了。再在可执行文件中使用 rpath 把库的完整路径确定下来。
比如现在 Graphics 库发布了 1.1.0 和 1.2.0 两个版本,这两个版本可以不必是二进制兼容。用户的代码从 1.1.0 升级到 1.2.0 的时候要重新编译一下,反正他们要用新功能都是要重新编译代码的。如果要原地打补丁,那么 1.1.1 应该和 1.1.0 二进制兼容,而 1.2.1 应该和 1.2.0 兼容。如果要加入新的功能,而新的功能与 1.2.0 不兼容,那么应该发布到 1.3.0 版本。
为了便于检查二进制兼容性,可考虑把库的代码的暴露情况分辨清楚。muduo 的头文件和 class 就有意识地分为用户可见和用户不可见两部分,见。对于用户可见的部分,升级时要注意二进制兼容性,选用合理的版本号;对于用户不可见的部分,在升级库的时候就不必在意。另外 muduo 本身设计来是以静态库方式发布,在二进制兼容性方面没有做太多的考虑。
如果库的使用范围很广,用户很多,各家的 release cycle 不尽相同,那么推荐 [2, item 43],并考虑多采用 non-member non-friend function in namespace [1, item 23] [2, item 44 abd 57] 作为接口。这里以前面的 Graphics 为例,说明 pimpl 的基本手法。
1. 暴露的接口里边不要有虚函数,而且 sizeof(Graphics) == sizeof(Graphics::Impl*)。class Graphics
Graphics(); // outline ctor
~Graphics(); // outline dtor
void drawLine(int x0, int y0, int x1, int y1);
void drawLine(Point p0, Point p1);
void drawRectangle(int x0, int y0, int x1, int y1);
void drawRectangle(Point p0, Point p1);
void drawArc(int x, int y, int r);
void drawArc(Point p, int r);
boost::scoped_ptr&Impl&
2. 在库的实现中把调用转发 (forward) 给实现 Graphics::Impl ,这部分代码位于 .so/.dll 中,随库的升级一起变化。#include &graphics.h&
class Graphics::Impl
void drawLine(int x0, int y0, int x1, int y1);
void drawLine(Point p0, Point p1);
void drawRectangle(int x0, int y0, int x1, int y1);
void drawRectangle(Point p0, Point p1);
void drawArc(int x, int y, int r);
void drawArc(Point p, int r);
Graphics::Graphics()
: impl(new Impl)
Graphics::~Graphics()
void Graphics::drawLine(int x0, int y0, int x1, int y1)
impl-&drawLine(x0, y0, x1, y1);
void Graphics::drawLine(Point p0, Point p1)
impl-&drawLine(p0, p1);
3. 如果要加入新的功能,不必通过继承来扩展,可以原地修改,且很容易保持二进制兼容性。先动头文件:--- old/graphics.h
15:34:06. +0800
+++ new/graphics.h
15:14:12. +0800
@@ -7,19 +7,22 @@
class Graphics
Graphics(); // outline ctor
~Graphics(); // outline dtor
void drawLine(int x0, int y0, int x1, int y1);
void drawLine(double x0, double y0, double x1, double y1);
void drawLine(Point p0, Point p1);
void drawRectangle(int x0, int y0, int x1, int y1);
void drawRectangle(double x0, double y0, double x1, double y1);
void drawRectangle(Point p0, Point p1);
void drawArc(int x, int y, int r);
void drawArc(double x, double y, double r);
void drawArc(Point p, int r);
boost::scoped_ptr&Impl&
然后在实现文件里增加 forward,这么做不会破坏二进制兼容性,因为增加 non-virtual 函数不影响现有的可执行文件。--- old/graphics.cc
15:15:20. +0800
+++ new/graphics.cc
15:15:26. +0800
@@ -1,35 +1,43 @@
#include &graphics.h&
class Graphics::Impl
void drawLine(int x0, int y0, int x1, int y1);
void drawLine(double x0, double y0, double x1, double y1);
void drawLine(Point p0, Point p1);
void drawRectangle(int x0, int y0, int x1, int y1);
void drawRectangle(double x0, double y0, double x1, double y1);
void drawRectangle(Point p0, Point p1);
void drawArc(int x, int y, int r);
void drawArc(double x, double y, double r);
void drawArc(Point p, int r);
Graphics::Graphics()
: impl(new Impl)
Graphics::~Graphics()
void Graphics::drawLine(int x0, int y0, int x1, int y1)
impl-&drawLine(x0, y0, x1, y1);
+void Graphics::drawLine(double x0, double y0, double x1, double y1)
impl-&drawLine(x0, y0, x1, y1);
void Graphics::drawLine(Point p0, Point p1)
impl-&drawLine(p0, p1);
采用 pimpl 多了一道 explicit forward 的手续,带来的好处是可扩展性与二进制兼容性,通常是划算的。pimpl 扮演了的作用。
pimpl 不仅 C++ 语言可以用,C 语言的库同样可以用,一样带来二进制兼容性的好处,比如 libevent2 里边的 struct event_base 是个 opaque pointer,客户端看不到其成员,都是通过 libevent 的函数和它打交道,这样库的版本升级比较容易做到二进制兼容。
为什么 non-virtual 函数比 virtual 函数更健壮?因为 virtual function 是 bind-by-vtable-offset,而 non-virtual function 是 bind-by-name。加载器 (loader) 会在程序启动时做决议(resolution),通过 mangled name 把可执行文件和动态库链接到一起。就像使用 Internet 域名比使用 IP 地址更能适应变化一样。
万一要跨语言怎么办?很简单,暴露 C 语言的接口。Java 有 JNI 可以调用 C 语言的代码;Python/Perl/Ruby 等等的解释器都是 C 语言编写的,使用 C 函数也不在话下。C 函数是 Linux 下的万能接口。
本文只谈了使用 class 为接口,其实用 free function 有时候更好(比如 muduo/base/Timestamp.h 除了定义 class Timestamp,还定义了 muduo::timeDifference() 等 free function),这也是 C++ 比 Java 等纯面向对象语言优越的地方。留给将来再细谈吧。
[1] Scott Meyers, 《Effective C++》 第 3 版,条款 35:考虑 virtual 函数以外的其他选择;条款 23:宁以 non-member、non-friend 替换 member 函数。
[2] Herb Sutter and Andrei Alexandrescu, 《C++ 编程规范》,条款 39:考虑将 virtual 函数做成 non-public,将 public 函数做成 non-virtual;条款 43:明智地使用 pimpl;条款 44:尽可能编写 nonmember, nonfriend 函数;条款 57:将 class 和其非成员函数接口放入同一个 namespace。
[3] 孟岩,《》,《》中的“四个半抽象”。
[4] 陈硕,《》,《》。
本作品采用进行许可。
陈硕 (giantchen_AT_gmail)
Blog.csdn.net/Solstice
本文主要讨论 Linux x86/x86-64 平台,偶尔会举 Windows 作为反面教材。
C/C++ 的二进制兼容性 (binary compatibility) 有多重含义,本文主要在“头文件和库文件分别升级,可执行文件是否受影响”这个意义下讨论,我称之为 library (主要是 shared library,即动态链接库)的 ABI (application binary interface)。至于编译器与操作系统的 ABI 留给下一篇谈 C++ 标准与实践的文章。
什么是二进制兼容性
在解释这个定义之前,先看看 Unix/C 语言的一个历史问题:open() 的 flags 参数的取值。open(2) 函数的原型是
int open(const char *pathname, int flags);
其中 flags 的取值有三个: O_RDONLY,& O_WRONLY,& O_RDWR。
与一般人的直觉相反,这几个值不是按位或 (bitwise-OR) 的关系,即 O_RDONLY | O_WRONLY != O_RDWR。如果你想以读写方式打开文件,必须用 O_RDWR,而不能用 (O_RDONLY | O_WRONLY)。为什么?因为 O_RDONLY, O_WRONLY, O_RDWR 的值分别是 0, 1, 2。它们不满足按位或。
那么为什么 C 语言从诞生到现在一直没有纠正这个不足之处?比方说把 O_RDONLY, O_WRONLY, O_RDWR 分别定义为 1, 2, 3,这样 O_RDONLY | O_WRONLY == O_RDWR,符合直觉。而且这三个值都是宏定义,也不需要修改现有的源代码,只需要改改系统的头文件就行了。
因为这么做会破坏二进制兼容性。对于已经编译好的可执行文件,它调用 open(2) 的参数是写死的,更改头文件并不能影响已经编译好的可执行文件。比方说这个可执行文件会调用 open(path, 1) 来写文件,而在新规定中,这表示读文件,程序就错乱了。
以上这个例子说明,如果以 shared library 方式提供函数库,那么头文件和库文件不能轻易修改,否则容易破坏已有的二进制可执行文件,或者其他用到这个 shared library 的 library。操作系统的 system call 可以看成 Kernel 与 User space 的 interface,kernel 在这个意义下也可以当成 shared library,你可以把内核从 2.6.30 升级到 2.6.35,而不需要重新编译所有用户态的程序。
所谓“二进制兼容性”指的就是在升级(也可能是 bug fix)库文件的时候,不必重新编译使用这个库的可执行文件或使用这个库的其他库文件,程序的功能不被破坏。
见 QT FAQ 的有关条款:
在 Windows 下有恶名叫 DLL Hell,比如 MFC 有一堆 DLL,mfc40.dll, mfc42.dll, mfc71.dll, mfc80.dll, mfc90.dll,这是动态链接库的本质问题,怪不到 MFC 头上。
有哪些情况会破坏库的 ABI
到底如何判断一个改动是不是二进制兼容呢?这跟 C++ 的实现方式直接相关,虽然 C++ 标准没有规定 C++ 的 ABI,但是几乎所有主流平台都有明文或事实上的 ABI 标准。比方说 ARM 有 EABI,Intel Itanium 有 ,x86-64 有仿 Itanium 的 ABI,SPARC 和 MIPS 也都有明文规定的 ABI,等等。x86 是个例外,它只有事实上的 ABI,比如 Windows 就是 Visual C++,Linux 是 G++(G++ 的 ABI 还有多个版本,目前最新的是 G++ 3.4 的版本),Intel 的 C++ 编译器也得按照 Visual C++ 或 G++ 的 ABI 来生成代码,否则就不能与系统其它部件兼容。
C++ ABI 的主要内容:
函数参数传递的方式,比如 x86-64 用寄存器来传函数的前 4 个整数参数
虚函数的调用方式,通常是 vptr/vtbl 然后用 vtbl[offset] 来调用
struct 和 class 的内存布局,通过偏移量来访问数据成员
name mangling
RTTI 和异常处理的实现(以下本文不考虑异常处理)
C/C++ 通过头文件暴露出动态库的使用方法,这个“使用方法”主要是给编译器看的,编译器会据此生成二进制代码,然后在运行的时候通过装载器(loader)把可执行文件和动态库绑到一起。如何判断一个改动是不是二进制兼容,主要就是看头文件暴露的这份“使用说明”能否与新版本的动态库的实际使用方法兼容。因为新的库必然有新的头文件,但是现有的二进制可执行文件还是按旧的头文件来调用动态库。
这里举一些源代码兼容但是二进制代码不兼容例子
给函数增加默认参数,现有的可执行文件无法传这个额外的参数。
增加虚函数,会造成 vtbl 里的排列变化。(不要考虑“只在末尾增加”这种取巧行为,因为你的 class 可能已被继承。)
增加默认模板类型参数,比方说 Foo&T& 改为 Foo&T, Alloc=alloc&T& &,这会改变 name mangling
改变 enum 的值,把 enum Color { Red = 3 }; 改为 Red = 4。这会造成错位。当然,由于 enum 自动排列取值,添加 enum 项也是不安全的,除非是在末尾添加。
给 class Bar 增加数据成员,造成 sizeof(Bar) 变大,以及内部数据成员的 offset 变化,这是不是安全的?通常不是安全的,但也有例外。
如果客户代码里有 new Bar,那么肯定不安全,因为 new 的字节数不够装下新 Bar。相反,如果 library 通过 factory 返回 Bar* (并通过 factory 来销毁对象)或者直接返回 shared_ptr&Bar&,客户端不需要用到 sizeof(Bar),那么可能是安全的。
如果客户代码里有 Bar* pB pBar-&memberA =,那么肯定不安全,因为 memberA 的新 Bar 的偏移可能会变。相反,如果只通过成员函数来访问对象的数据成员,客户端不需要用到 data member 的 offsets,那么可能是安全的。
如果客户调用 pBar-&setMemberA(xx); 而 Bar::setMemberA() 是个 inline function,那么肯定不安全,因为偏移量已经被 inline 到客户的二进制代码里了。如果 setMemberA() 是 outline function,其实现位于 shared library 中,会随着 Bar 的更新而更新,那么可能是安全的。
那么只使用 header-only 的库文件是不是安全呢?不一定。如果你的程序用了 boost 1.36.0,而你依赖的某个 library 在编译的时候用的是 1.33.1,那么你的程序和这个 library 就不能正常工作。因为 1.36.0 和 1.33.1 的 boost::function 的模板参数类型的个数不一样,其中一个多了 allocator。
这里有一份黑名单,列在这里的肯定是二级制不兼容,没有列出的也可能二进制不兼容,见 KDE 的文档:
哪些做法多半是安全的
前面我说“不能轻易修改”,暗示有些改动多半是安全的,这里有一份白名单,欢迎添加更多内容。
只要库改动不影响现有的可执行文件的二进制代码的正确性,那么就是安全的,我们可以先部署新的库,让现有的二进制程序受益。
增加新的 class
增加 non-virtual 成员函数
修改数据成员的名称,因为生产的二进制代码是按偏移量来访问的,当然,这会造成源码级的不兼容。
还有很多,不一一列举了。
反面教材:COM
在 C++ 中以虚函数作为接口基本上就跟二进制兼容性说拜拜了。具体地说,以只包含虚函数的 class (称为 interface class)作为程序库的接口,这样的接口是僵硬的,一旦发布,无法修改。
比方说 M$ 的 COM,其 DirectX 和 MSXML 都以 COM 组件方式发布,我们来看看它的带版本接口 (versioned interfaces):
IDirect3D7, IDirect3D8, IDirect3D9, ID3D10*, ID3D11*
IXMLDOMDocument, IXMLDOMDocument2, IXMLDOMDocument3
换话句话说,每次发布新版本都引入新的 interface class,而不是在现有的 interface 上做扩充。这样一样不能兼容现有的代码,强迫客户端代码也要改写。
回过头来看看 C 语言,C/Posix 这些年逐渐加入了很多新函数,同时,现有的代码不用修改也能运行得很好。如果要用这些新函数,直接用就行了,也基本不会修改已有的代码。相反,COM 里边要想用 IXMLDOMDocument3 的功能,就得把现有的代码从 IXMLDOMDocument 全部升级到 IXMLDOMDocument3,很讽刺吧。
tip:如果遇到鼓吹在 C++ 里使用面向接口编程的人,可以拿二进制兼容性考考他。
采用静态链接
这个是王道。在分布式系统这,采用静态链接也带来部署上的好处,只要把可执行文件放到机器上就行运行,不用考虑它依赖的 libraries。目前 muduo 就是采用静态链接。
通过动态库的版本管理来控制兼容性
这需要非常小心检查每次改动的二进制兼容性并做好发布计划,比如 1.0.x 系列做到二进制兼容,1.1.x 系列做到二进制兼容,而 1.0.x 和 1.1.x 二进制不兼容。《程序员的自我修养》里边讲过 .so 文件的命名与二进制兼容性相关的话题,值得一读。&
Java 是如何应对的
Java 实际上把 C/C++ 的 linking 这一步骤推迟到 class loading 的时候来做。就不存在“不能增加虚函数”,“不能修改 data member” 等问题。在 Java 里边用面向 interface 编程远比 C++ 更通用和自然,也没有上面提到的“僵硬的接口”问题。
&&&& 摘要: 版本管理(version controlling)是每个程序员的基本技能,C++ 程序员也不例外。版本管理的基本功能之一是追踪代码变化,让你能清楚地知道代码是如何一步步变成现在的这个样子,以及每次 check-in 都具体改动了哪些内部。所谓“有利于版本管理”的代码格式,就是指在代码中合理使用换行符,对 diff 工具友好,让 diff 的结果清晰明了地表达代码的改动。&&
共6页:&&&&4&&&
2829303112345678910111213141516171819202122232425262728293012345678
评论 - 347
阅读排行榜
评论排行榜