C++ Style and Technique FAQ (中文版)-------
C++ Style and Technique FAQ (中文版)
Q: 这个简单的程序……我如何把它搞定?
#include<iostream> #include<vector> #include<algorithm> using namespace std; int main() { vector<double> v; double d; while(cin>>d) v.push_back(d); // read elements if (!cin.eof()) { // check if input failed cerr << "format error\n"; return 1; // error return } cout << "read " << v.size() << " elements\n"; reverse(v.begin(),v.end()); cout << "elements in reverse order:\n"; for (int i = 0; i<v.size(); ++i) cout << v[i] << '\n'; return 0; // success return }
- 这是一个用标准C++写的程序,使用了标准库[译注:标准库主要是将原来的C运行支持库(Standard C Library)、iostream库、STL(Standard Template Library,标准模板库)等标准化而得的] 。标准库提供的功能都位于namespace std之中,使用标准库所需包含的头文件是不含.h扩展名的。[译注:有些编译器厂商为了兼容性也提供了含.h扩展名的头文件。]
- 如果你在Windows下编译,你需要把编译选项设为“console application”。记住,你的源代码文件的扩展名必须为.cpp,否则编译器可能会把它当作C代码来处理。
- 主函数main()要返回一个整数。[译注:有些编译器也支持void main()的定义,但这是非标准做法]
- 将输入读入标准库提供的vector容器可以保证你不会犯“缓冲区溢出”之类错误——对于初学者来说,硬是要求“把输入读到一个数组之中,不许犯任何‘愚蠢的错误’”似乎有点过份了——如果你真能达到这样的要求,那你也不能算完全的初学者了。如果你不相信我的这个论断,那么请看看我写的《Learning Standard C++ as a New Language》一文。 [译注:CSDN文档区有该文中译。]
- 代码中“ !cin.eof() ”是用来测试输入流的格式的。具体而言,它测试读输入流的循环是否因遇到EOF而终止。如果不是,那说明输入格式不对(不全是数字)。还有细节地方不清楚,可以参看你使用的教材中关于“流状态”的章节。
- Vector是知道它自己的大小的,所以不必自己清点输入了多少元素。
- 这个程序不含任何显式内存管理代码,也不会产生内存泄漏。Vector会自动配置内存,所以用户不必为此烦心。
- 关于如何读入字符串,请参阅后面的“我如何从标准输入中读取string”条目。
- 这个程序以EOF为输入终止的标志。如果你在UNIX上运行这个程序,可以用Ctrl-D输入EOF。但你用的Windows版本可能会含有一个bug(http://support.microsoft.com/support/kb/articles/Q156/2/58.asp?LN=EN-US&SD=gn&FR=0&qry=End of File&rnk=11&src=DHCS_MSPSS_gn_SRCH&SPR=NTW40),导致系统无法识别EOF字符。如果是这样,那么也许下面这个有稍许改动的程序更适合你:这个程序以单词“end”作为输入终结的标志。
#include<iostream> #include<vector> #include<algorithm> #include<string> using namespace std; int main() { vector<double> v; double d; while(cin>>d) v.push_back(d); // read elements if (!cin.eof()) { // check if input failed cin.clear(); // clear error state string s; cin >> s; // look for terminator string if (s != "end") { cerr << "format error\n"; return 1; // error return } } cout << "read " << v.size() << " elements\n"; reverse(v.begin(),v.end()); cout << "elements in reverse order:\n"; for (int i = 0; i<v.size(); ++i) cout << v[i] << '\n'; return 0; // success return }
Q: 为何我编译一个程序要花那么多时间?
不过,也有可能原因在于你的程序——看看你的程序设计还能不能改进?编译器是不是为了顺利产出正确的二进制码而不得不吃进成百个头文件、几万行的源代码?原则上,只要对源码适当优化一下,编译缓慢的问题应该可以解决。如果症结在于你的类库供应商,那么你大概除了“换一家类库供应商”外确实没什么可做的了;但如果问题在于你自己的代码,那么完全可以通过重构(refactoring)来让你的代码更为结构化,从而使源码一旦有更改时需重编译的代码量最小。这样的代码往往是更好的设计:因为它的藕合程度较低,可维护性较佳。
我们来看一个OOP的经典例子:
class Shape { public: // interface to users of 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; // ... };
- 要确认“哪些功能会被所有的继承类用到,而应在基类中实作”可不是件简单的事。所以,基类的保护成员或许会随着要求的变化而变化,其频度远高于公共界面之可能变化。例如,尽管我们把“center”作为所有形状的一个属性(从而在基类中声明)似乎是天经地义的,但因此而要在基类中时时维护三角形的中心坐标是很麻烦的,还不如只在需要时才计算——这样可以减少开销。
- 和抽象的公共界面不同,保护成员可能会依赖实作细节,而这是Shape类的使用者所不愿见到的。例如,绝大部分使用Shape的代码应该逻辑上和color无关;但只要color的声明在Shape类中出现了,就往往会导致编译器将定义了“该操作系统中颜色表示”的头文件读入、展开、编译。这都需要时间!
- 当基类中保护成员(比如前面说的center,color)的实作有所变化,那么所有使用了Shape类的代码都需要重新编译——哪怕这些代码中只有很少是真正要用到基类中的那个“语义变化了的保护成员”。
所以,在基类中放一些“对于继承类之实作有帮助”的功能或许是出于好意,但实则是麻烦的源泉。用户的要求是多变的,所以实作代码也是多变的。将多变的代码放在许多继承类都要用到的基类之中,那么变化可就不是局部的了,这会造成全局影响的!具体而言就是:基类所倚赖的一个头文件变动了,那么所有继承类所在的文件都需重新编译。
这样分析过后,解决之道就显而易见了:仅仅把基类用作为抽象的公共界面,而将“对继承类有用”的实作功能移出。
class Shape { public: // interface to users of Shapes virtual void draw() const = 0; virtual void rotate(int degrees) = 0; virtual Point center() const = 0; // ... // no data }; 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: // interface to users of 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; };
Q: 为何空类的大小不是零?
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"; }
Q: 为什么我必须把数据放到类的声明之中?
template<class Scalar> 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; };
class Implementer; // forward declaration
class Interface { public: // interface
private: Implementer impl; };
class Implementer {
public:
// implementation details, including data members
上述代码中的注释处可以存放提问者所说的“数据”,而Implementer的声明代码不需暴露给用户。不过,Proxy模式也不是十全十美的——Interface通过impl指针间接调用实作代码带来了额外的开销。或许读者会说,C++不是有内联机制吗?这个开销能通过内联定义而弥补吧。但别忘了,此处运用Proxy模式的目的就是把“实作”部分隐藏起来,这“隐藏”往往就意味着“实作代码”以链接库中的二进制代码形式存在。目前的C++编译器和链接器能做到既“代码内联”又“二进制隐藏”吗?或许可以。那么Proxy模式又能否和C++的模板机制“合作愉快”呢?(换句话说,如果前面代码中Interface和Implementer的声明均不是class,而是template,又如何呢?)关键在于,编译器对内联和模板的支持之实作是否需要进行源码拷贝,还是可以进行二进制码拷贝。目前而言,C#的泛型支持之实作是在Intermediate Language层面上的,而C++则是源码层面上的。Bjarne给出的复数类声明代码称“数据必须出现在类声明中”也是部分出于这种考虑。呵呵,扯远了……毕竟,这段文字只是FAQ的“译注”而已,此处不作更多探讨,有兴趣的读者可以自己去寻找答案 :O) ]
Q: 为何成员函数不是默认为虚?
另外,有虚函数的类有虚机制的开销[译注:指存放vtable带来的空间开销和通过vtable中的指针间接调用带来的时间开销],通常而言每个对象增加的空间开销是一个字长。这个开销可不小,而且会造成和其他语言(比如C,Fortran)的不兼容性——有虚函数的类的内存数据布局和普通的类是很不一样的。[译注:这种内存数据布局的兼容性问题会给多语言混合编程带来麻烦。]
《The Design and Evolution of C++》 中有更多关于设计理念的细节。
Q: 为何析构函数不是默认为虚?
那么,何时我该让析构函数为虚呢?哦,答案是——当类有其它虚函数的时候,你就应该让析构函数为虚。有其它虚函数,就意味着这个类要被继承,就意味着它有点“interface”的味道了。这样一来,程序员就可能会以基类指针来指向由它的继承类所实例化而来的对象,而能否通过基类指针来正常释放这样的对象就要看析构函数是否为虚了。 例如:
class Base { // ... virtual ~Base(); }; class Derived : public Base { // ... ~Derived(); }; void f() { Base* p = new Derived; delete p; // virtual destructor used to ensure that ~Derived is called }
Q: C++中为何没有虚拟构造函数?
struct F { // interface to object creation functions 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(); // make an A of the appropriate type B* q = fac.make_a_B(); // make a B of the appropriate type // ... } struct FX : F { A* make_an_A() const { return new AX(); } // AX is derived from A B* make_a_B() const { return new BX(); } // BX is derived from B }; struct FY : F { A* make_an_A() const { return new AY(); } // AY is derived from A B* make_a_B() const { return new BY(); } // BY is derived from B }; int main() { user(FX()); // this user makes AXs and BXs user(FY()); // this user makes AYs and BYs // ... }
Q: 为何无法在派生类中重载?
#include<iostream> 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
换句话说,在D和B之间没有重载发生。你调用了pd->f(),编译器就在D的名字域里找啊找,找到double f(double)后就调用它了。编译器懒得再到B的名字域里去看看有没有哪个函数更符合要求。记住,在C++中,没有跨域重载——继承类和基类虽然关系很亲密,但也不能坏了这条规矩。详见《The Design and Evolution of C++》或者《The C++ Programming Language》第三版。
不过,如果你非得要跨域重载,也不是没有变通的方法——你就把那些函数弄到同一个域里来好了。使用一个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
Q: 我能从构造函数调用虚函数吗?
#include<string> #include<iostream> 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 。 究竟发生了什么?f()是在B::B()中调用的。如果构造函数中调用虚函数的规则不是如前文所述那样,
而是如一些人希望的那样去调用D::f()。那么因为构造函数D::D()尚未运行,字符串s还未初始化,所以当D::f()试图将参数
赋给s时,结果多半是——立马当机。
析构则正相反,遵循从继承类到基类的顺序(拆房子总得从上往下拆吧?),所以其调用虚函数的行为和在构造函数中一样:虚函数此时此刻被绑定到哪里(当然应该是基类啦——因为继承类已经被“拆”了——析构了!),调用的就是哪个函数。
更多细节请见《The Design and Evolution of C++》,13.2.4.2 或者《The C++ Programming Language》第三版,15.4.3 。
有时,这条规则被解释为是由于编译器的实作造成的。[译注:从实作角度可以这样解释:在许多编译器中,直到构造函数调用完毕,vtable才被建立,此时虚函数才被动态绑定至继承类的同名函数。] 但事实上不是这么一回事——让编译器实作成“构造函数中调用虚函数也和从其他函数中调用一样”是很简单的[译注:只要把vtable的建立移至构造函数调用之前即可]。关键还在于语言设计时的考量——让虚函数可以求助于基类提供的通用代码。[译注:先有鸡还是先有蛋?Bjarne实际上是在告诉你,不是“先有实作再有规则”,而是“如此实作,因为规则如此”。]
Q: 有"placement delete"吗?
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; // ...
template<class T> void destroy(T* p, Arena& a) { if (p) { p->~T(); // explicit destructor call a.deallocate(p); } }
destroy(p2,a2);
destroy(p3,a3);
如何在类继承体系中定义配对的operator new() 和 operator delete() 可以参看 《The C++ Programming Language》,Special Edition,15.6节 ,《The Design and Evolution of C++》,10.4节,以及《The C++ Programming Language》,Special Edition,19.4.5节。[译注:此处按原文照译。前面有提到“参见《The C++ Programming Language》第三版”的,实际上特别版(Special Edition)和较近重印的第三版没什么区别。]
A: 可以的,但何必呢?好吧,也许有两个理由:
- 出于效率考虑——不希望我的函数调用是虚的
- 出于安全考虑——确保我的类不被用作基类(这样我拷贝对象时就不用担心对象被切割(slicing)了)[译注:“对象切割”指,将派生类对象赋给基类变量时,根据C++的类型转换机制,只有包括在派生类中的基类部分被拷贝,其余部分被“切割”掉了。]
如果为了和“虚函数调用”说byebye,那么确实有给类继承体系“封顶”的需要。在设计前,不访先问问自己,这些函数为何要被设计成虚的。我确实见过这样的例子:性能要求苛刻的函数被设计成虚的,仅仅因为“我们习惯这样做”!
好了,无论如何,说了那么多,毕竟你只是想知道,为了某种合理的理由,你能不能防止别人继承你的类。答案是可以的。可惜,这里给出的解决之道不够干净利落。你不得不在在你的“封顶类”中虚拟继承一个无法构造的辅助基类。还是让例子来告诉我们一切吧:
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; // error: DD::DD() cannot access
// Usable_lock::Usable_lock(): private member
Q: 为什么我无法限制模板的参数?
让我们来看这段代码:
template<class Container> void draw_all(Container& c) { for_each(c.begin(),c.end(),mem_fun(&Shape::draw)); }
为了早点捕捉到这个错误,我们可以这样写代码:
template<class Container> 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)); }
template<class Container> void draw_all(Container& c) { typedef typename Container::value_type T; Can_copy<T,Shape*>(); // accept containers of only Shape*s for_each(c.begin(),c.end(),mem_fun(&Shape::draw)); }
template<class T1, class T2> struct Can_copy { static void constraints(T1 a, T2 b) { T2 c = a; b = a; } Can_copy() { void(*p)(T1,T2) = constraints; } };
- 你可以不通过定义/拷贝变量就表达出constraints[译注:实则定义/拷贝变量的工作被封装在Can_copy模板中了] ,从而可以不必作任何“那个类型是这样被初始化”之类假设,也不用去管对象能否被拷贝、销毁(除非这正是constraints所在)。[译注:即——除非constraints正是“可拷贝”、“可销毁”。如果用易理解的伪码描述,就是template <typename T as Copy_Enabled> xxx,template <typename T as Destructible> xxx 。]
- 如果使用现代编译器,constraints不会带来任何额外代码
- 定义或者使用constraints均不需使用宏定义
- 如果constraints没有被满足,编译器给出的错误消息是容易理解的。事实上,给出的错误消息包括了单词“constraints” (这样,编码者就能从中得到提示)、constraints的名称、具体的出错原因(比如“cannot initialize Shape* by double*”)
既然如此,我们干吗不干脆在C++语言本身中定义类似Can_copy()或者更优雅简洁的语法呢?The Design and Evolution of C++分析了此做法带来的困难。已经有许许多多设计理念浮出水面,只为了让含constraints的模板类易于撰写,同时还要让编译器在constraints不被满足时给出容易理解的出错消息。比方说,我在Can_copy中“使用函数指针”的设计就来自于Alex Stepanov和Jeremy Siek。我认为我的Can_copy()实作还不到可以标准化的程度——它需要更多实践的检验。另外,C++使用者会遭遇许多不同类型的constraints,目前看来还没有哪种形式的带constraints的模板获得压倒多数的支持。
已有不少关于constraints的“内置语言支持”方案被提议和实作。但其实要表述constraint根本不需要什么异乎寻常的东西:毕竟,当我们写一个模板时,我们拥有C++带给我们的强有力的表达能力。让代码来为我的话作证吧:
template<class T, class B> struct Derived_from { static void constraints(T* p) { B* pb = p; } Derived_from() { void(*p)(T*) = constraints; } }; template<class T1, class T2> struct Can_copy { static void constraints(T1 a, T2 b) { T2 c = a; b = a; } Can_copy() { void(*p)(T1,T2) = constraints; } }; template<class T1, class T2 = T1> struct Can_compare { static void constraints(T1 a, T2 b) { a==b; a!=b; a<b; } Can_compare() { void(*p)(T1,T2) = constraints; } }; template<class T1, class T2, class T3 = T1> 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<D,B>(); Derived_from<DD,B>(); Derived_from<X,B>(); Derived_from<int,B>(); Derived_from<X,int>(); Can_compare<int,float>(); Can_compare<X,B>(); Can_multiply<int,float>(); Can_multiply<int,float,double>(); Can_multiply<B,X>(); Can_copy<D*,B*>(); Can_copy<D,B*>(); Can_copy<int,B*>(); } // the classical "elements must derived from Mybase*" constraint: template<class T> class Container : Derived_from<T,Mybase> { // ... };
Q: 我们已经有了 "美好的老qsort()",为什么还要用sort()?
qsort(array,asize,sizeof(elem),elem_compare);
sort(vec.begin(),vec.end());
struct Record { string name; // ... }; struct name_compare { // compare Records using "name" as the key bool operator()(const Record& a, const Record& b) const { return a.name<b.name; } }; void f(vector<Record>& vs) { sort(vs.begin(), vs.end(), name_compare()); // ... }
另外,还有许多人欣赏sort()的类型安全性——要使用它可不需要任何强制的类型转换。对于标准类型,也不必写compare()函数,省事不少。如果想看更详尽的解释,参看我的《Learning Standard C++ as a New Language》一文。
另外,为何sort()要比qsort()快?因为它更好地利用了C++的内联语法语义。
Q: 什么是function object?
Function object的涵义比通常意义上的函数更广泛,因为它可以在多次调用之间保持某种“状态”——这和静态局部变量有异曲同工之妙;不过这种“状态”还可以被初始化,还可以从外面来检测,这可要比静态局部变量强了。我们来看一个例子:
class Sum { int val; public: Sum(int i) :val(i) { } operator int() const { return val; } // extract value int operator()(int i) { return val+=i; } // application }; void f(vector v) { Sum s = 0; // initial value 0 s = for_each(v.begin(), v.end(), s); // gather the sum of all elements cout << "the sum is " << s << "\n"; // or even: cout << "the sum is " << for_each(v.begin(), v.end(), Sum(0)) << "\n"; }
在标准库中function objects被广泛使用,这给标准库带来了极大的灵活性和可扩展性。
[译注:C++是一个博采众长的语言,function object的概念就是从functional programming中借来的;而C++本身的强大和表现力的丰富也使这种“拿来主义”成为可能。一般而言,在使用function object的地方也常可以使用函数指针;在我们还不熟悉function object的时候我们也常常是使用指针的。但定义一个函数指针的语法可不是太简单明了,而且在C++中指针早已背上了“错误之源”的恶名。更何况,通过指针调用函数增加了间接开销。所以,无论为了语法的优美还是效率的提高,都应该提倡使用function objects。
下面我们再从设计模式的角度来更深入地理解function objects:这是Visitor模式的典型应用。当我们要对某个/某些对象施加某种操作,但又不想将这种操作限定死,那么就可以采用Visitor模式。在Design Patterns一书中,作者把这种模式实作为:通过一个Visitor类来提供这种操作(在前面Bjarne Stroustrup的代码中,Sum就是一个Visitor的变体),用Visitor类实例化一个visitor对象(当然,在前面的代码中对应的是s);然后在Iterator的迭代过程中,为每一个对象调用visitor.visit()。这里visit()是Visitor类的一个成员函数,作用相当于Sum类中那个“特殊的成员函数”——operator();visit()也完全可以被定义为内联函数,以去除间接性,提高性能。在此提请读者注意,C++把重载的操作符也看作函数,只不过是具有特殊函数名的函数。所以实际上Design Patterns一书中Visitor模式的示范实作和这里function object的实作大体上是等价的。一个function object也就是一个特殊的Visitor。]
Q: 我应该怎样处理内存泄漏?
#include<vector> #include<string> #include<iostream> #include<algorithm> using namespace std; int main() // small program messing around with strings { cout << "enter some whitespace-separated words:\n"; vector<string> v; string s; while (cin>>s) v.push_back(s); sort(v.begin(),v.end()); string cat; typedef vector<string>::const_iterator Iter; for (Iter p = v.begin(); p!=v.end(); ++p) cat += *p+"+"; cout << cat << '\n'; }
请注意这里没有显式的内存管理代码。没有宏,没有类型转换,没有溢出检测,没有强制的大小限制,也没有指针。如果使用function object和标准算法[译注:指标准库中提供的泛型算法],我连Iterator也可以不用。不过这毕竟只是一个小程序,杀鸡焉用牛刀?
当然,这些方法也并非无懈可击,而且说起来容易做起来难,要系统地使用它们也并不总是很简单。不过,无论如何,它们的广泛适用性令人惊讶,而且通过移去大量的显式内存分配/释放代码,它们确实增强了代码的可读性和可管理性。早在1981年,我就指出通过大幅度减少需要显式加以管理的对象数量,使用C++“将事情做对”将不再是一件极其费神的艰巨任务。
如果你的应用领域没有能在内存管理方面助你一臂之力的类库,那么如果你还想让你的软件开发变得既快捷又能轻松得到正确结果,最好是先建立这样一个库。
如果你无法让内存分配和释放成为对象的“自然行为”,那么至少你可以通过使用资源句柄来尽量避免内存泄漏。这里是一个示例:假设你需要从函数返回一个对象,这个对象是在自由内存堆上分配的;你可能会忘记释放那个对象——毕竟我们无法通过检查指针来确定其指向的对象是否需要被释放,我们也无法得知谁应该负责释放它。那么,就用资源句柄吧。比如,标准库中的auto_ptr就可以帮助澄清:“释放对象”责任究竟在谁。我们来看:
#include<memory> #include<iostream> using namespace std; struct S { S() { cout << "make an S\n"; } ~S() { cout << "destroy an S\n"; } S(const S&) { cout << "copy initialize an S\n"; } S& operator=(const S&) { cout << "copy assign an S\n"; } }; S* f() { return new S; // who is responsible for deleting this S? }; auto_ptr<S> g() { return auto_ptr<S>(new S); // explicitly transfer responsibility for deleting this S } int main() { cout << "start main\n"; S* p = f(); cout << "after f() before g()\n"; // S* q = g(); // caught by compiler auto_ptr<S> q = g(); cout << "exit main\n"; // leaks *p // implicitly deletes *q }
这里只是内存资源管理的例子;至于其它类型的资源管理,可以如法炮制。
如果在你的开发环境中无法系统地使用这种方法(比方说,你使用了第三方提供的古董代码,或者远古“穴居人”参与了你的项目开发),那么你在开发过程中可千万要记住使用内存防漏检测程序,或者干脆使用垃圾收集器(Garbage Collector)。
Q: 为何捕捉到异常后不能继续执行后面的代码呢?
嗯,从异常处理代码返回到异常抛出处继续执行后面的代码的想法很好[译注:现行异常机制的设计是:当异常被抛出和处理后,从处理代码所在的那个catch块往下执行],但主要问题在于——exception handler不可能知道为了让后面的代码正常运行,需要做多少清除异常的工作[译注:毕竟,当有异常发生,事情就有点不太对劲了,不是吗;更何况收拾烂摊子永远是件麻烦的事],所以,如果要让“继续执行”能够正常工作,写throw代码的人和写catch代码的人必须对彼此的代码都很熟悉,而这就带来了复杂的相互依赖关系[译注:既指开发人员之间的“相互依赖”,也指代码间的相互依赖——紧耦合的代码可不是好代码哦 :O) ],会带来很多麻烦的维护问题。
在我设计C++的异常处理机制的时候,我曾认真地考虑过这个问题;在C++标准化的过程中,这个问题也被详细地讨论过。(参见《The Design and Evolution of C++》中关于异常处理的章节)如果你想试试看在抛出异常之前能不能解决问题然后继续往下执行,你可以先调用一个“检查—恢复”函数,然后,如果还是不能解决问题,再把异常抛出。一个这样的例子是new_handler。
Q: 为何C++中没有C中realloc()的对应物?
在C++中,处理内存重分配的较好办法是使用标准库中的容器,比如vector。[译注:这些容器会自己管理需要的内存,在必要时会“增长尺寸”——进行重分配。]
Q: 我如何使用异常处理?
Q: 我如何从标准输入中读取string?
#include<iostream> #include<string> using namespace std; int main() { cout << "Please enter a word:\n"; string s; cin>>s; cout << "You entered " << s << '\n'; }
如果你需要一次读一整行,可以这样:
#include<iostream> #include<string> using namespace std; int main() { cout << "Please enter a line:\n"; string s; getline(cin, s); cout << "You entered " << s << '\n'; }
Q: 为何C++不提供“finally”结构?
class File_handle { FILE* p; public: File_handle(const char* n, const char* a) { p = fopen(n,a); if (p==0) throw Open_error(errno); } File_handle(FILE* pp) { p = pp; if (p==0) throw Open_error(errno); } ~File_handle() { fclose(p); } operator FILE*() { return p; } // ... }; void f(const char* fn) { File_handle f(fn,"rw"); // open fn for reading and writing // use file through f }
另外,请看看《The C++ Programming Language》附录E中的资源管理例子。
Q: 那个auto_ptr是什么东东啊?为什么没有auto_array?
#include<memory> using namespace std; struct X { int m; // .. }; void f() { auto_ptr<X> p(new X); X* q = new X; p->m++; // use p just like a pointer q->m++; // ... delete q; }
如果在代码用// ...标注的地方抛出异常,那么p会被正常删除——这个功劳应该记在auto_ptr的析构函数头上。不过,q指
向的X类型对象就没有被释放(因为不是用auto_ptr定义的)。详情请见《The C++ Programming Language》14.4.2节。
Auto_ptr是一个轻量级的类,没有引入引用计数机制。如果你把一个auto_ptr(比如,ap1)赋给另一个auto_ptr(比如,ap2),那么ap2将持有实际指针,而ap1将持有零指针。例如:
#include<memory> #include<iostream> using namespace std; struct X { int m; // .. }; int main() { auto_ptr<X> p(new X); auto_ptr<X> q(p); cout << "p " << p.get() << " q " << q.get() << "\n"; }
p 0x0 q 0x378d0
这里,语义似乎是“转移”,而非“拷贝”,这或许有点令人惊讶。特别要注意的是,不要把auto_ptr作为标准容器的参数——标准容器要求通常的拷贝语义。例如:
std::vector<auto_ptr<X> >v; // error
一个auto_ptr只能持有指向单个元素的指针,而不是数组指针:
void f(int n) { auto_ptr<X> p(new X[n]); // error // ... }
那么,看来我们应该用一个使用delete[]来释放指针的,叫auto_array的类似东东来放数组了?哦,不,不,没有什么auto_array。理由是,不需要有啊——我们完全可以用vector嘛:
void f(int n) { vector<X> v(n); // ... }
Q: C和C++风格的内存分配/释放可以混用吗?
不可以——从你无法delete一个以malloc()分配而来之对象的意义上而言。你也无法free()或realloc()一个由new分配而来的对象。
C++的new和delete运算符确保构造和析构正常发生,但C风格的malloc()、calloc()、free()和realloc()可不保证这点。而且,没有任何人能向你担保,new/delete和malloc/free所掌控的内存是相互“兼容”的。如果在你的代码中,两种风格混用而没有给你造成麻烦,那我只能说:直到目前为止,你是非常幸运的 :O)
如果你因为思念“美好的老realloc()”(许多人都思念她)而无法割舍整个古老的C内存分配机制(爱屋及乌?),那么考虑使用标准库中的vector吧。例如:
// read words from input into a vector of strings: vector<string> words; string s; while (cin>>s && s!=".") words.push_back(s);
我的《Learning Standard C++ as a New Language》一文中给出了其它例子,可以参考。
Q: 我想从void *转换,为什么必须使用换型符?
A: 在C中,你可以隐式转换,但这是不安全的,例如:
#include<stdio.h> int main() { char i = 0; char j = 0; char* p = &i; void* q = p; int* pp = q; /* unsafe, legal C, not C++ */ printf("%d %d\n",i,j); *pp = -1; /* overwrite memory starting at &i */ printf("%d %d\n",i,j); }
int* pp = (int*)q;
int* pp = static_cast<int*>(q);
在C中一类最常见的不安全换型发生在将malloc()分配而来的内存赋给某个指针之时,例如:
int* p = malloc(sizeof(int));
int* p = new int;
- new不会“偶然”地分配错误大小的内存
- new自动检查内存是否已枯竭
- new支持初始化
typedef std::complex<double> cmplx; /* C style: */ cmplx* p = (cmplx*)malloc(sizeof(int)); /* error: wrong size */ /* forgot to test for p==0 */ if (*p == 7) { /* ... */ } /* oops: forgot to initialize *p */ // C++ style: cmplx* q = new cmplx(1,2); // will throw bad_alloc if memory is exhausted if (*q == 7) { /* ... */ }
A: 如何在类中定义常量?
Q: 如果你想得到一个可用于常量表达式中的常量,例如数组大小的定义,那么你有两种选择:
class X { static const int c1 = 7; enum { c2 = 19 }; char v1[c1]; char v2[c2]; // ... };
一眼望去,c1的定义似乎更加直截了当,但别忘了只有static的整型或枚举型量才能如此初始化。这就很有局限性,例如:
class Y { const int c3 = 7; // error: not static static int c4 = 7; // error: not const static const float c5 = 7; // error not integral };
我还是更喜欢玩“enum戏法”,因为这种定义可移植性好,而且不会引诱我去使用非标准的“类内初始化”扩展语法。
那么,为何要有这些不方便的限制?因为类通常声明在头文件中,而头文件往往被许多单元所包含。[所以,类可能会被重复声明。]但是,为了避免链接器设计的复杂化,C++要求每个对象都只能被定义一次。如果C++允许类内定义要作为对象被存在内存中的实体,那么这项要求就无法满足了。关于C++设计时的一些折衷,参见《The Design and Evolution of C++》。
如果这个常量不需要被用于常量表达式,那么你的选择余地就比较大了:
class Z { static char* p; // initialize in definition const int i; // initialize in constructor public: Z(int ii) :i(ii) { } }; char* Z::p = "hello, there";
class AE { // ... public: static const int c6 = 7; static const int c7 = 31; }; const int AE::c7; // definition int f() { const int* p1 = &AE::c6; // error: c6 not an lvalue const int* p2 = &AE::c7; // ok // ... }
Q: 为何delete操作不把指针置零?
delete p; // ... delete p;
哦,不不,这个主意不够“好”。一个理由是,被delete的指针未必是左值。我们来看:
delete p+1; delete f(x);
T* p = new T; T* q = p; delete p; delete q; // ouch!
如果你觉得释放内存时把指针置零很重要,那么不妨写这样一个destroy函数:
template<class T> inline void destroy(T*& p) { delete p; p = 0; }
不妨把delete带来的麻烦看作“尽量少用new/delete,多用标准库中的容器”之另一条理由吧 :O)
请注意,把指针作为引用传递(以便delete可以把指针置零)会带来额外的效益——防止右值被传递给destroy() :
int* f(); int* p; // ... destroy(f()); // error: trying to pass an rvalue by non-const reference destroy(p+1); // error: trying to pass an rvalue by non-const reference
Q: 我可以写"void main()"吗?
void main() { /* ... */ }
int main() { /* ... */ }
int main(int argc, char* argv[]) { /* ... */ }
#include<iostream> int main() { std::cout << "This program returns the integer value 0\n"; }
#include<iostream> main() { /* ... */ }
Q: 为何我不能重载“.”、“::”和“sizeof”等操作符?
而“sizeof”无法被重载是因为不少内部操作,比如指针加法,都依赖于它,例如:
X a[10]; X* p = &a[3]; X* q = &a[3]; p++; // p points to a[4] // thus the integer value of p must be // sizeof(X) larger than the integer value of q
在N::m中,N和m都不是表达式,它们只是编译器“认识”的名字,“::”执行的实际操作是编译时的名字域解析,并没有表达式的运算牵涉在内。或许有人会觉得重载一个“x::y”(其中x是实际对象,而非名字域或类名)是一个好主意,但这样做引入了新的语法[译注:重载的本意是让操作符可以有新的语义,而不是更改语法——否则会引起混乱],我可不认为新语法带来的复杂性会给我们什么好处。
原则上来说,“.”运算符是可以被重载的,就像“->”一样。不过,这会带来语义的混淆——我们到底是想和“.”后面的对象打交道呢,还是“.”后面的东东所实际指向的实体打交道呢?看看这个例子(它假设“.”重载是可以的):
class Y { public: void f(); // ... }; class X { // assume that you can overload . Y* p; Y& operator.() { return *p; } void f(); // ... }; void g(X& x) { x.f(); // X::f or Y::f or error? }
这个问题有好几种解决方案。在C++标准化之时,何种方案为佳并不明显。细节请参见《The Design and Evolution of C++》。
Q: 我怎样才能把整数转化为字符串?
#include<iostream> #include<string> #include<sstream> using namespace std; string itos(int i) // convert int to string { stringstream s; s << i; return s.str(); } int main() { int i = 127; string ss = itos(i); const char* p = ss.c_str(); cout << ss << " " << p << "\n"; }
Q: “int* p;”和“int *p;”,到底哪个正确?
不过如果让人来读,两者的含义就有所不同了。代码的书写风格是很重要的。C风格的表达式和声明式常被看作比“necessary evil”[译注:“必要之恶”,意指为了达到某种目的而不得不付出的代价。例如有人认为环境的破坏是经济发展带来的“necessary evil”]更糟的东西,而C++则很强调类型。所以,“int *p”和“int* p”之间并无对错之分,只有风格之争。
一个典型的C程序员会写“int *p”,而且振振有词地告诉你“这表示‘*p是一个int’”——听上去挺有道理的。这里,*和p绑在了一起——这就是C的风格。这种风格强调的是语法。
而一个典型的C++程序员会写“int* p”,并告诉你“p是一个指向int的指针,p的类型是int*”。这种风格强调的是类型。当然,我喜欢这种风格 :O) 而且,我认为,类型是非常重要的概念,我们应该注重类型。它的重要性丝毫不亚于C++语言中的其它“较为高级的部分”。[译注:诸如RTTI,各种cast,template机制等,可称为“较高级的部分”了吧,但它们其实也是类型概念的扩展和运用。我曾写过两篇谈到C++和OOP的文章发表在本刊上,文中都强调了理解“类型”之重要性。我还曾译过Object Unencapsulated (这本书由作者先前所著在网上广为流传的C++?? A Critique修订而来)中讲类型的章节,这本书的作者甚至称Object Oriented Programming应该正名为Type Oriented Programming——“面向类型编程”!这有点矫枉过正了,但类型确是编程语言之核心部分。]
当声明单个变量时,int *和int*的差别并不是特别突出,但当我们要一次声明多个变量时,易混淆之处就全暴露出来了:
int* p, p1; // probable error: p1 is not an int*
int *p, p1; // probable error?
int* p = &i; int p1 = p; // error: int initialized by int*
每当达到某种目的有两条以上途径,就会有些人被搞糊涂;每当一些选择是出于个人喜好,争论就会无休无止。坚持一次只声明一个指针并在声明时顺便初始化,困扰我们已久的混淆之源就会随风逝去。如果你想了解有关C的声明语法的更多讨论,参见《The Design and Evolution of C++》 。
Q: 何种代码布局风格为佳?
我个人喜欢使用“K&R”风格,如果算上那些C语言中不存在的构造之使用惯例,那么人们有时也称之为“Stroustrup”风格。例如:
class C : public B { public: // ... }; void f(int* p, int max) { if (p) { // ... } for (int i = 0; i<max; ++i) { // ... } }
正确的缩进非常重要。
一些设计问题,比如使用抽象类来表示重要的界面、使用模板来表示灵活而可扩展的类型安全抽象、正确使用“异常”来表示错误,远远要比代码风格重要。
[译注:《The Practice of Programming》中有一章对“代码风格”问题作了详细的阐述。]
Q: 我该把const写在类型前面还是后面?
const int a = 1; // ok int const b = 2; // also ok
为什么会这样?当我发明“const”(最早是被命名为“readonly”且有一个叫“writeonly”的对应物)时,我让它在前面和后面都行,因为这不会带来二义性。当时的C/C++编译器对修饰符很少有强加的语序规则。
我不记得当时有过什么关于语序的深思熟虑或相关的争论。一些早期的C++使用者(特别是我)当时只是单纯地觉得const int c = 10;要比int const c = 10;好看而已。或许,我是受了这件事实的影响:许多我早年写的例子是用“readonly”修饰的,而readonly int c = 10;确实看上去要比int readonly c = 10;舒服。而最早的使用“const”的C/C++代码是我用全局查找替换功能把readonly换成const而来的。我还记得和几个人讨论过关于语法“变体”问题,包括Dennis Ritchie。不过我不记得当时我们谈的是哪几种语言了。
另外,请注意:如果指针本身不可被修改,那么const应该放在“*”的后面。例如:
int *const p1 = q; // constant pointer to int variable int const* p2 = q; // pointer to constant int const int* p3 = q; // pointer to constant int
Q: 宏有什么不好吗?
#include "someheader.h"
struct S {
int alpha;
int beta;
};
如果有人(不明智地)写了一个叫“alpha”或者“beta”的宏,那么这段代码无法通过编译,甚至可能更糟——编译出一些你
未曾预料的结果。比方说:如果“someheader.h”包含了如下定义:
#define alpha 'a' #define beta b[2]
那么前面的代码就完全背离本意了。
不幸的是,你无法确保其他程序员不犯你所认为的“愚蠢的”错误。比方说,近来有人告诉我,他们遇到一个含“goto”语句的宏。我见到过这样的代码,也听到过这样的论点——有时宏中的“goto”是有用的。例如:
#define prefix get_ready(); int ret__ #define Return(i) ret__=i; do_something(); goto exit #define suffix exit: cleanup(); return ret__ void f() { prefix; // ... Return(10); // ... Return(x++); //... suffix; }
一个常见而微妙的问题是,函数风格的宏不遵守函数参数调用规则。例如:
#define square(x) (x*x) void f(double d, int i) { square(d); // fine square(i++); // ouch: means (i++*i++) square(d+1); // ouch: means (d+1*d+1); that is, (d+d+1) // ... }
#define square(x) ((x)*(x)) /* better */
我知道有些(其它语言中)被称为“宏”的东西并不象C/C++预处理器所处理的“宏”那样缺陷多多、麻烦重重,但我并不想改进C++的宏,而是建议你正确使用C++语言中的其他机制,比如内联函数、模板、构造函数、析构函数、异常处理等。
yinzi:
http://www.research.att.com/~bs/bstechfaq.htm