网站首页

家园论坛

老版论坛

家园博客

业界新闻

技术文档

下载中心

速查中心

图片中心

硬件资讯
上一篇:C语言初学者入门讲座 第十六讲 文件(2) 下一篇:用VC 6.0实现串行通信的三种方法
C++的风格与技巧

来源: 作者: 添加日期:2005-9-4 19:19:56 点击次数:

Bjarne Stroustrup的FAQ:C++的风格与技巧

翻译:左轻侯

http://www.wushuang.net/article/bsfaq.htm

(译注:本文的翻译相当艰苦。Bjarne Stroustrup不愧是创立C++语言的一代大师,不但思想博大精深,而且在遣词造句上,也非常精微深奥。有很多地方,译者反复斟酌,都不能取得理想的效果,只能尽力而为。

Html格式的文档见译者主页:http://www.wushuang.net

如果你对这个翻译稿有任何意见和建议,请发信给译者:onekey@163.com。

原文的地址为:http://www.research.att.com/~bs/bs_faq2.html)



(Bjarne Stroustrup博士,1950年出生于丹麦,先后毕业于丹麦阿鲁斯大学和英国剑挢大学,AT&T大规模程序设计研究部门负责人,AT&T 贝尔实验室和ACM成员。1979年,B. S开始开发一种语言,当时称为"C with Class",后来演化为C++。1998年,ANSI/ISO C++标准建立,同年,B. S推出其经典著作The C++ Programming Language的第三版。)



这是一些人们经常向我问起的有关C++的风格与技巧的问题。如果你能提出更好的问题,或者对这些答案有所建议,请务必发Email给我(bs@research.att.com)。请记住,我不能把全部的时间都花在更新我的主页上面。



更多的问题请参见我的general FAQ。



关于术语和概念,请参见我的C++术语表(C++ glossary.)。



请注意,这仅仅是一个常见问题与解答的列表。它不能代替一本优秀教科书中那些经过精心挑选的范例与解释。它也不能象一本参考手册或语言标准那样,提供详细和准确的说明。有关C++的设计的问题,请参见《C++语言的设计和演变》(The Design and Evolution of C++)。关于C++语言与标准库的使用,请参见《C++程序设计语言》(The C++ Programming Language)。



目录:

我如何写这个非常简单的程序?

为什么编译要花这么长的时间?

为什么一个空类的大小不为0?

我必须在类声明处赋予数据吗?

为什么成员函数默认不是virtual的?

为什么析构函数默认不是virtual的?

为什么不能有虚拟构造函数?

为什么重载在继承类中不工作?

我能够在构造函数中调用一个虚拟函数吗?

有没有“指定位置删除”(placement delete)?

我能防止别人继承我自己的类吗?

为什么不能为模板参数定义约束(constraints)?

既然已经有了优秀的qsort()函数,为什么还需要一个sort()?

什么是函数对象(function object)?

我应该如何对付内存泄漏?

我为什么在捕获一个异常之后就不能继续?

为什么C++中没有相当于realloc()的函数?

如何使用异常?

怎样从输入中读取一个字符串?

为什么C++不提供“finally”的构造?

什么是自动指针(auto_ptr),为什么没有自动数组(auto_array)?

可以混合使用C风格与C++风格的内存分派与重新分配吗?

我为什么必须使用一个造型来转换*void?

我如何定义一个类内部(in-class)的常量?

为什么delete不会将操作数置0?

我能够写“void main()”吗?

为什么我不能重载点符号,::,sizeof,等等?

怎样将一个整型值转换为一个字符串?

“int* p”正确还是“int *p”正确?

对于我的代码,哪一种布局风格(layout style)是最好的?

我应该将“const”放在类型之前还是之后?

使用宏有什么问题?



我如何写这个非常简单的程序?



特别是在一个学期的开始,我常常收到许多关于编写一个非常简单的程序的询问。这个问题有一个很具代表性的解决方法,那就是(在你的程序中)读入几个数字,对它们做一些处理,再把结果输出。下面是一个这样做的例子:



#include

#include

#include

using namespace std;



int main()

{

vector v;



double d;

while(cin>>d) v.push_back(d); // 读入元素

if (!cin.eof()) { // 检查输入是否出错

cerr << "format error\n";

return 1; // 返回一个错误

}



cout << "read " << v.size() << " elements\n";



reverse(v.begin(),v.end());

cout << "elements in reverse order:\n";

for (int i = 0; i


return 0; // 成功返回

}



对这段程序的观察:



这是一段标准的ISO C++程序,使用了标准库(standard library)。标准库工具在命名空间std中声明,封装在没有.h后缀的头文件中。



如果你要在Windows下编译它,你需要将它编译成一个“控制台程序”(console application)。记得将源文件加上.cpp后缀,否则编译器可能会以为它是一段C代码而不是C++。



是的,main()函数返回一个int值。



读到一个标准的向量(vector)中,可以避免在随意确定大小的缓冲中溢出的错误。读到一个数组(array)中,而不产生“简单错误”(silly error),这已经超出了一个新手的能力——如果你做到了,那你已经不是一个新手了。如果你对此表示怀疑,我建议你阅读我的文章“将标准C++作为一种新的语言来学习”("Learning Standard C++ as a New Language"),你可以在本人著作列表(my publications list)中下载到它。



!cin.eof()是对流的格式的检查。事实上,它检查循环是否终结于发现一个end-of-file(如果不是这样,那么意味着输入没有按照给定的格式)。更多的说明,请参见你的C++教科书中的“流状态”(stream state)部分。



vector知道它自己的大小,因此我不需要计算元素的数量。



这段程序没有包含显式的内存管理。Vector维护一个内存中的栈,以存放它的元素。当一个vector需要更多的内存时,它会分配一些;当它不再生存时,它会释放内存。于是,使用者不需要再关心vector中元素的内存分配和释放问题。



程序在遇到输入一个“end-of-file”时结束。如果你在UNIX平台下运行它,“end-of-file”等于键盘上的Ctrl+D。如果你在Windows平台下,那么由于一个BUG它无法辨别“end-of-file”字符,你可能倾向于使用下面这个稍稍复杂些的版本,它使用一个词“end”来表示输入已经结束。



#include

#include

#include

#include

using namespace std;



int main()

{

vector v;



double d;

while(cin>>d) v.push_back(d); // 读入一个元素

if (!cin.eof()) { // 检查输入是否失败

cin.clear(); // 清除错误状态

string s;

cin >> s; // 查找结束字符

if (s != "end") {

cerr << "format error\n";

return 1; // 返回错误

}

}



cout << "read " << v.size() << " elements\n";



reverse(v.begin(),v.end());

cout << "elements in reverse order:\n";

for (int i = 0; i


return 0; // 成功返回

}



更多的关于使用标准库将事情简化的例子,请参见《C++程序设计语言》中的“漫游标准库”("Tour of the Standard Library")一章。



为什么编译要花这么长的时间?



你的编译器可能有问题。也许它太老了,也许你安装它的时候出了错,也许你用的计算机已经是个古董。在诸如此类的问题上,我无法帮助你。



但是,这也是很可能的:你要编译的程序设计得非常糟糕,以至于编译器不得不检查数以百计的头文件和数万行代码。理论上来说,这是可以避免的。如果这是你购买的库的设计问题,你对它无计可施(除了换一个更好的库),但你可以将你自己的代码组织得更好一些,以求得将修改代码后的重新编译工作降到最少。这样的设计会更好,更有可维护性,因为它们展示了更好的概念上的分离。



看看这个典型的面向对象的程序例子:



class Shape {

public: // 使用Shapes的用户的接口

virtual void draw() const;

virtual void rotate(int degrees);

// ...

protected: // common data (for implementers of Shapes)

Point center;

Color col;

// ...

};



class Circle : public Shape {

public:

void draw() const;

void rotate(int) { }

// ...

protected:

int radius;

// ...

};



class Triangle : public Shape {

public:

void draw() const;

void rotate(int);

// ...

protected:

Point a, b, c;

// ...

};



设计思想是,用户通过Shape的public接口来操纵它们,而派生类(例如Circle和Triangle)的实现部分则共享由protected成员表现的那部分实现(implementation)。



这不是一件容易的事情:确定哪些实现部分是对所有的派生类都有用的,并将之共享出来。因此,与public接口相比,protected成员往往要做多得多的改动。举例来说,虽然理论上“中心”(center)对所有的图形都是一个有效的概念,但当你要维护一个三角形的“中心”的时候,是一件非常麻烦的事情——对于三角形,当且仅当它确实被需要的时候,计算这个中心才是有意义的。



protected成员很可能要依赖于实现部分的细节,而Shape的用户(译注:user此处译为用户,指使用Shape类的代码,下同)却不见得必须依赖它们。举例来说,很多(大多数?)使用Shape的代码在逻辑上是与“颜色”无关的,但是由于Shape中“颜色”这个定义的存在,却可能需要一堆复杂的头文件,来结合操作系统的颜色概念。



当protected部分发生了改变时,使用Shape的代码必须重新编译——即使只有派生类的实现部分才能够访问protected成员。



于是,基类中的“实现相关的信息”(information helpful to implementers)对用户来说变成了象接口一样敏感的东西,它的存在导致了实现部分的不稳定,用户代码的无谓的重编译(当实现部分发生改变时),以及将头文件无节制地包含进用户代码中(因为“实现相关的信息”需要它们)。有时这被称为“脆弱的基类问题”(brittle base class problem)。



一个很明显的解决方案就是,忽略基类中那些象接口一样被使用的“实现相关的信息”。换句话说,使用接口,纯粹的接口。也就是说,用抽象基类的方式来表示接口:



class Shape {

public: //使用Shapes的用户的接口

virtual void draw() const = 0;

virtual void rotate(int degrees) = 0;

virtual Point center() const = 0;

// ...



// 没有数据

};



class Circle : public Shape {

public:

void draw() const;

void rotate(int) { }

Point center() const { return center; }

// ...

protected:

Point cent;

Color col;

int radius;

// ...

};



class Triangle : public Shape {

public:

void draw() const;

void rotate(int);

Point center() const;

// ...

protected:

Color col;

Point a, b, c;

// ...

};



现在,用户代码与派生类的实现部分的变化之间的关系被隔离了。我曾经见过这种技术使得编译的时间减少了几个数量级。



但是,如果确实存在着对所有派生类(或仅仅对某些派生类)都有用的公共信息时怎么办呢?可以简单把这些信息封装成类,然后从它派生出实现部分的类:



class Shape {

public: //使用Shapes的用户的接口

virtual void draw() const = 0;

virtual void rotate(int degrees) = 0;

virtual Point center() const = 0;

// ...



// no data

};



struct Common {

Color col;

// ...

};



class Circle : public Shape, protected Common {

public:

void draw() const;

void rotate(int) { }

Point center() const { return center; }

// ...

protected:

Point cent;

int radius;

};



class Triangle : public Shape, protected Common {

public:

void draw() const;

void rotate(int);

Point center() const;

// ...

protected:

Point a, b, c;

};



为什么一个空类的大小不为0?



要清楚,两个不同的对象的地址也是不同的。基于同样的理由,new总是返回指向不同对象的指针。

看看:



class Empty { };



void f()

{

Empty a, b;

if (&a == &b) cout << "impossible: report error to compiler supplier";



Empty* p1 = new Empty;

Empty* p2 = new Empty;

if (p1 == p2) cout << "impossible: report error to compiler supplier";

}



有一条有趣的规则:一个空的基类并不一定有分隔字节。

struct X : Empty {

int a;

// ...

};



void f(X* p)

{

void* p1 = p;

void* p2 = &p->a;

if (p1 == p2) cout << "nice: good optimizer";

}



这种优化是允许的,可以被广泛使用。它允许程序员使用空类以表现一些简单的概念。现在有些编译器提供这种“空基类优化”(empty base class optimization)。



我必须在类声明处赋予数据吗?



不必须。如果一个接口不需要数据时,无须在作为接口定义的类中赋予数据。代之以在派生类中给出它们。参见“为什么编译要花这么长的时间?”。



有时候,你必须在一个类中赋予数据。考虑一下复数类的情况:



template class complex {

public:

complex() : re(0), im(0) { }

complex(Scalar r) : re(r), im(0) { }

complex(Scalar r, Scalar i) : re(r), im(i) { }

// ...



complex& operator+=(const complex& a)

{ re+=a.re; im+=a.im; return *this; }

// ...

private:

Scalar re, im;

};



设计这种类型的目的是将它当做一个内建(built-in)类型一样被使用。在声明处赋值是必须的,以保证如下可能:建立真正的本地对象(genuinely local objects)(比如那些在栈中而不是在堆中分配的对象),或者使某些简单操作被适当地inline化。对于那些支持内建的复合类型的语言来说,要获得它们提供的效率,真正的本地对象和inline化都是必要的。



为什么成员函数默认不是virtual的?



因为很多类并不是被设计作为基类的。例如复数类。



而且,一个包含虚拟函数的类的对象,要占用更多的空间以实现虚拟函数调用机制——往往是每个对象占用一个字(word)。这个额外的字是非常可观的,而且在涉及和其它语言的数据的兼容性时,可能导致麻烦(例如C或Fortran语言)。



要了解更多的设计原理,请参见《C++语言的设计和演变》(The Design and Evolution of C++)。



为什么析构函数默认不是virtual的?



因为很多类并不是被设计作为基类的。只有类在行为上是它的派生类的接口时(这些派生类往往在堆中分配,通过指针或引用来访问),虚拟函数才有意义。



那么什么时候才应该将析构函数定义为虚拟呢?当类至少拥有一个虚拟函数时。拥有虚拟函数意味着一个类是派生类的接口,在这种情况下,一个派生类的对象可能通过一个基类指针来销毁。例如:



class Base {

// ...

virtual ~Base();

};



class Derived : public Base {

// ...

~Derived();

};



void f()

{

Base* p = new Derived;

delete p; // 虚拟析构函数保证~Derived函数被调用

}



如果基类的析构函数不是虚拟的,那么派生类的析构函数将不会被调用——这可能产生糟糕的结果,例如派生类的资源不会被释放。



为什么不能有虚拟构造函数?



虚拟调用是一种能够在给定信息不完全(given partial information)的情况下工作的机制。特别地,虚拟允许我们调用某个函数,对于这个函数,仅仅知道它的接口,而不知道具体的对象类型。但是要建立一个对象,你必须拥有完全的信息。特别地,你需要知道要建立的对象的具体类型。因此,对构造函数的调用不可能是虚拟的。



当要求建立一个对象时,一种间接的技术常常被当作“虚拟构造函数”来使用。有关例子,请参见《C++程序设计语言》第三版15.6.2.节。



下面这个例子展示一种机制:如何使用一个抽象类来建立一个适当类型的对象。



struct F { // 对象建立函数的接口

virtual A* make_an_A() const = 0;

virtual B* make_a_B() const = 0;

};



void user(const F& fac)

{

A* p = fac.make_an_A(); // 将A作为合适的类型

B* q = fac.make_a_B(); // 将B作为合适的类型

// ...

}



struct FX : F {

A* make_an_A() const { return new AX(); } // AX是A的派生

B* make_a_B() const { return new BX(); } // AX是B的派生

};



struct FY : F {

A* make_an_A() const { return new AY(); } // AY是A的派生

B* make_a_B() const { return new BY(); } // BY是B的派生



};



int main()

{

user(FX()); // 此用户建立AX与BX

user(FY()); // 此用户建立AY与BY

// ...

}



这是所谓的“工厂模式”(the factory pattern)的一个变形。关键在于,user函数与AX或AY这样的类的信息被完全分离开来了。



为什么重载在继承类中不工作?



这个问题(非常常见)往往出现于这样的例子中:



#include

using namespace std;



class B {

public:

int f(int i) { cout << "f(int): "; return i+1; }

// ...

};



class D : public B {

public:

double f(double d) { cout << "f(double): "; return d+1.3; }

// ...

};



int main()

{

D* pd = new D;



cout << pd->f(2) << '\n';

cout << pd->f(2.3) << '\n';

}



它输出的结果是:



f(double): 3.3

f(double): 3.6



而不是象有些人猜想的那样:



f(int): 3

f(double): 3.6



换句话说,在B和D之间并没有发生重载的解析。编译器在D的区域内寻找,找到了一个函数double f(double),并执行了它。它永远不会涉及(被封装的)B的区域。在C++中,没有跨越区域的重载——对于这条规则,继承类也不例外。更多的细节,参见《C++语言的设计和演变》和《C++程序设计语言》。



但是,如果我需要在基类和继承类之间建立一组重载的f()函数呢?很简单,使用using声明:



class D : public B {

public:

using B::f; // make every f from B available

double f(double d) { cout << "f(double): "; return d+1.3; }

// ...

};



进行这个修改之后,输出结果将是:



f(int): 3

f(double): 3.6



这样,在B的f()和D的f()之间,重载确实实现了,并且选择了一个最合适的f()进行调用。



我能够在构造函数中调用一个虚拟函数吗?



可以,但是要小心。它可能不象你期望的那样工作。在构造函数中,虚拟调用机制不起作用,因为继承类的重载还没有发生。对象先从基类被创建,“基类先于继承类(base before derived)”。



看看这个:



#include

#include

using namespace std;



class B {

public:

B(const string& ss) { cout << "B constructor\n"; f(ss); }

virtual void f(const string&) { cout << "B::f\n";}

};



class D : public B {

public:

D(const string & ss) :B(ss) { cout << "D constructor\n";}

void f(const string& ss) { cout << "D::f\n"; s = ss; }

private:

string s;

};



int main()

{

D d("Hello");

}



程序编译以后会输出:



B constructor

B::f

D constructor



注意不是D::f。设想一下,如果出于不同的规则,B::B()可以调用D::f()的话,会产生什么样的后果:因为构造函数D::D()还没有运行,D::f()将会试图将一个还没有初始化的字符串s赋予它的参数。结果很可能是导致立即崩溃。



析构函数在“继承类先于基类”的机制下运行,因此虚拟机制的行为和构造函数一样:只有本地定义(local definitions)被使用——不会调用虚拟函数,以免触及对象中的(现在已经被销毁的)继承类的部分。



更多的细节,参见《C++语言的设计和演变》13.2.4.2和《C++程序设计语言》15.4.3。



有人暗示,这只是一条实现时的人为制造的规则。不是这样的。事实上,要实现这种不安全的方法倒是非常容易的:在构造函数中直接调用虚拟函数,就象调用其它函数一样。但是,这样就意味着,任何虚拟函数都无法编写了,因为它们需要依靠基类的固定的创建(invariants established by base classes)。这将会导致一片混乱。



有没有“指定位置删除”(placement delete)?



没有,不过如果你需要的话,可以自己写一个。



看看这个指定位置创建(placement new),它将对象放进了一系列Arena中;



class Arena {

public:

void* allocate(size_t);

void deallocate(void*);

// ...

};



void* operator new(size_t sz, Arena& a)

{

return a.allocate(sz);

}



Arena a1(some arguments);

Arena a2(some arguments);



这样实现了之后,我们就可以这么写:



X* p1 = new(a1) X;

Y* p2 = new(a1) Y;

Z* p3 = new(a2) Z;

// ...



但是,以后怎样正确地销毁这些对象呢?没有对应于这种“placement new”的内建的“placement delete”,原因是,没有一种通用的方法可以保证它被正确地使用。在C++的类型系统中,没有什么东西可以让我们确认,p1一定指向一个由Arena类型的a1分派的对象。p1可能指向任何东西分派的任何一块地方。



然而,有时候程序员是知道的,所以这是一种方法:



template void destroy(T* p, Arena& a)

{

if (p) {

p->~T(); // explicit destructor call

a.deallocate(p);

}

}



现在我们可以这么写:



destroy(p1,a1);

destroy(p2,a2);

destroy(p3,a3);



如果Arena维护了它保存着的对象的线索,你甚至可以自己写一个析构函数,以避免它发生错误。



这也是可能的:定义一对相互匹配的操作符new()和delete(),以维护《C++程序设计语言》15.6中的类继承体系。参见《C++语言的设计和演变》10.4和《C++程序设计语言》19.4.5。



我能防止别人继承我自己的类吗?



可以,但你为什么要那么做呢?这是两个常见的回答:



效率:避免我的函数被虚拟调用

安全:保证我的类不被用作一个基类(例如,保证我能够复制对象而不用担心出事)



根据我的经验,效率原因往往是不必要的担心。在C++中,虚拟函数调用是如此之快,以致于它们在一个包含虚拟函数的类中被实际使用时,相比普通的函数调用,根本不会产生值得考虑的运行期开支。注意,仅仅通过指针或引用时,才会使用虚拟调用机制。当直接通过对象名字调用一个函数时,虚拟函数调用的开支可以被很容易地优化掉。



如果确实有真正的需要,要将一个类封闭起来以防止虚拟调用,那么可能首先应该问问为什么它们是虚拟的。我看见过一些例子,那些性能表现不佳的函数被设置为虚拟,没有其他原因,仅仅是因为“我们习惯这么干”。



这个问题的另一个部分,由于逻辑上的原因如何防止类被继承,有一个解决方案。不幸的是,这个方案并不完美。它建立在这样一个事实的基础之上,那就是:大多数的继承类必须建立一个虚拟的基类。这是一个例子:



class Usable;



class Usable_lock {

friend class Usable;

private:

Usable_lock() {}

Usable_lock(const Usable_lock&) {}

};



class Usable : public virtual Usable_lock {

// ...

public:

Usable();

Usable(char*);

// ...

};



Usable a;



class DD : public Usable { };



DD dd; // 错误: DD::DD() 不能访问

// Usable_lock::Usable_lock()是一个私有成员



(来自《C++语言的设计和演变》11.4.3)



为什么不能为模板参数定义约束(constraints)?



可以的,而且方法非常简单和通用。



看看这个:



template

void draw_all(Container& c)

{

for_each(c.begin(),c.end(),mem_fun(&Shape::draw));

}



如果出现类型错误,可能是发生在相当复杂的for_each()调用时。例如,如果容器的元素类型是int,我们将得到一个和for_each()相关的含义模糊的错误(因为不能够对对一个int值调用Shape::draw的方法)。



为了提前捕捉这个错误,我这样写:



template

void draw_all(Container& c)

{

Shape* p = c.front(); // accept only containers of Shape*s



for_each(c.begin(),c.end(),mem_fun(&Shape::draw));

}



对于现在的大多数编译器,中间变量p的初始化将会触发一个易于了解的错误。这个窍门在很多语言中都是通用的,而且在所有的标准创建中都必须这样做。在成品的代码中,我也许可以这样写:



template

void draw_all(Container& c)

{

typedef typename Container::value_type T;

Can_copy(); // accept containers of only Shape*s



for_each(c.begin(),c.end(),mem_fun(&Shape::draw));

}



这样就很清楚了,我在建立一个断言(assertion)。Can_copy模板可以这样定义:



template struct Can_copy {

static void constraints(T1 a, T2 b) { T2 c = a; b = a; }

Can_copy() { void(*p)(T1,T2) = constraints; }

};



Can_copy(在运行时)检查T1是否可以被赋值给T2。Can_copy检查T是否是Shape*类型,或者是一个指向由Shape类公共继承而来的类的对象的指针,或者是被用户转换到Shape*类型的某个类型。注意这个定义被精简到了最小:



一行命名要检查的约束,和要检查的类型

一行列出指定的要检查的约束(constraints()函数)

一行提供触发检查的方法(通过构造函数)



注意这个定义有相当合理的性质:



你可以表达一个约束,而不用声明或复制变量,因此约束的编写者可以用不着去设想变量如何被初始化,对象是否能够被复制,被销毁,以及诸如此类的事情。(当然,约束要检查这些属性的情况时例外。)

使用现在的编译器,不需要为约束产生代码

定义和使用约束,不需要使用宏

当约束失败时,编译器会给出可接受的错误信息,包括“constraints”这个词(给用户一个线索),约束的名字,以及导致约束失败的详细错误(例如“无法用double*初始化Shape*”)。



那么,在C++语言中,有没有类似于Can_copy——或者更好——的东西呢?在《C++语言的设计和演变》中,对于在C++中实现这种通用约束的困难进行了分析。从那以来,出现了很多方法,来让约束类变得更加容易编写,同时仍然能触发良好的错误信息。例如,我信任我在Can_copy中使用的函数指针的方式,它源自Alex Stepanov和Jeremy Siek。我并不认为Can_copy()已经可以标准化了——它需要更多的使用。同样,在C++社区中,各种不同的约束方式被使用;到底是哪一种约束模板在广泛的使用中被证明是最有效的,还没有达成一致的意见。



但是,这种方式非常普遍,比语言提供的专门用于约束检查的机制更加普遍。无论如何,当我们编写一个模板时,我们拥有了C++提供的最丰富的表达力量。看看这个:



template struct Derived_from {

static void constraints(T* p) { B* pb = p; }

Derived_from() { void(*p)(T*) = constraints; }

};



template struct Can_copy {

static void constraints(T1 a, T2 b) { T2 c = a; b = a; }

Can_copy() { void(*p)(T1,T2) = constraints; }

};



template struct Can_compare {

static void constraints(T1 a, T2 b) { a==b; a!=b; a
Can_compare() { void(*p)(T1,T2) = constraints; }

};



template struct Can_multiply {

static void constraints(T1 a, T2 b, T3 c) { c = a*b; }

Can_multiply() { void(*p)(T1,T2,T3) = constraints; }

};



struct B { };

struct D : B { };

struct DD : D { };

struct X { };



int main()

{

Derived_from();

Derived_from();

Derived_from();

Derived_from();

Derived_from();



Can_compare();

Can_compare();

Can_multiply();

Can_multiply();

Can_multiply();



Can_copy();

Can_copy();

Can_copy();

}



// 典型的“元素必须继承自Mybase*”约束:



template class Container : Derived_from {

// ...

};



事实上,Derived_from并不检查来源(derivation),而仅仅检查转换(conversion),不过这往往是一个更好的约束。为约束想一个好名字是很难的。



既然已经有了优秀的qsort()函数,为什么还需要一个sort()?



对于初学者来说,



qsort(array,asize,sizeof(elem),elem_compare);



看上去太古怪了,而且比这个更难理解:



sort(vec.begin(),vec.end());



对于专家来说,在元素与比较方式(comparison criteria)都相同的情况下,sort()比qsort()更快,这是很重要的。而且,qsort()是通用的,所以它可以用于不同容器类型、元素类型、比较方式的任意有意义的组合。举例来说:



struct Record {

string name;

// ...

};



struct name_compare { // 使用"name"作为键比较Record

bool operator()(const Record& a, const Record& b) const

{ return a.name
};



void f(vector& vs)

{

sort(vs.begin(), vs.end(), name_compare());

// ...

}



而且,很多人欣赏sort()是因为它是类型安全的,使用它不需要进行造型(cast),没有人必须去为基本类型写一个compare()函数。



更多的细节,参见我的文章《将标准C++作为一种新的语言来学习》(Learning C++ as a New language),可以从我的文章列表中找到。



sort()胜过qsort()的主要原因是,比较操作在内联(inlines)上做得更好。



什么是函数对象(function object)?



顾名思义,就是在某种方式上表现得象一个函数的对象。典型地,它是指一个类的实例,这个类定义了应用操作符operator()。



函数对象是比函数更加通用的概念,因为函数对象可以定义跨越多次调用的可持久的部分(类似静态局部变量),同时又能够从对象的外面进行初始化和检查(和静态局部变量不同)。例如:



class Sum {

int val;

public:

Sum(int i) :val(i) { }

operator int() const { return val; } // 取得值



int operator()(int i) { return val+=i; } // 应用

};



void f(vector v)

{

Sum s = 0; // initial value 0

s = for_each(v.begin(), v.end(), s); // 求所有元素的和

cout << "the sum is " << s << "\n";



//或者甚至:

cout << "the sum is " << for_each(v.begin(), v.end(), Sum(0)) << "\n";

}



注意一个拥有应用操作符的函数对象可以被完美地内联化(inline),因为它没有涉及到任何指针,后者可能导致拒绝优化。与之形成对比的是,现有的优化器几乎不能(或者完全不能?)将一个通过函数指针的调用内联化。



在标准库中,函数对象被广泛地使用以获得弹性。



我应该如何对付内存泄漏?



写出那些不会导致任何内存泄漏的代码。很明显,当你的代码中到处充满了new 操作、delete操作和指针运算的话,你将会在某个地方搞晕了头,导致内存泄漏,指针引用错误,以及诸如此类的问题。这和你如何小心地对待内存分配工作其实完全没有关系:代码的复杂性最终总是会超过你能够付出的时间和努力。于是随后产生了一些成功的技巧,它们依赖于将内存分配(allocations)与重新分配(deallocation)工作隐藏在易于管理的类型之后。标准容器(standard containers)是一个优秀的例子。它们不是通过你而是自己为元素管理内存,从而避免了产生糟糕的结果。想象一下,没有string和vector的帮助,写出这个:



#include

#include

#include

#include

using namespace std;



int main() // small program messing around with strings

{

cout << "enter some whitespace-separated words:\n";

vector v;

string s;

while (cin>>s) v.push_back(s);



sort(v.begin(),v.end());



string cat;

typedef vector::const_iterator Iter;

for (Iter p = v.begin(); p!=v.end(); ++p) cat += *p+"+";

cout << cat << '\n';

}



你有多少机会在第一次就得到正确的结果?你又怎么知道你没有导致内存泄漏呢?



注意,没有出现显式的内存管理,宏,造型,溢出检查,显式的长度限制,以及指针。通过使用函数对象和标准算法(standard algorithm),我可以避免使用指针——例如使用迭代子(iterator),不过对于一个这么小的程序来说有点小题大作了。



这些技巧并不完美,要系统化地使用它们也并不总是那么容易。但是,应用它们产生了惊人的差异,而且通过减少显式的内存分配与重新分配的次数,你甚至可以使余下的例子更加容易被跟踪。早在1981年,我就指出,通过将我必须显式地跟踪的对象的数量从几万个减少到几打,为了使程序正确运行而付出的努力从可怕的苦工,变成了应付一些可管理的对象,甚至更加简单了。



如果你的程序还没有包含将显式内存管理减少到最小限度的库,那么要让你程序完成和正确运行的话,最快的途径也许就是先建立一个这样的库。



模板和标准库实现了容器、资源句柄以及诸如此类的东西,更早的使用甚至在多年以前。异常的使用使之更加完善。



如果你实在不能将内存分配/重新分配的操作隐藏到你需要的对象中时,你可以使用资源句柄(resource handle),以将内存泄漏的可能性降至最低。这里有个例子:我需要通过一个函数,在空闲内存中建立一个对象并返回它。这时候可能忘记释放这个对象。毕竟,我们不能说,仅仅关注当这个指针要被释放的时候,谁将负责去做。使用资源句柄,这里用了标准库中的auto_ptr,使需要为之负责的地方变得明确了。



#include

#include

using namespace std;



struct S {

S() { cout << "make an S\n"; }

~S() { cout << "destroy an

 
设为首页 | 加入收藏 | 业务办理 | 友情链接 | 论坛版面 | 浙ICP备07502118号 |