#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 }
#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 }
不过,也有可能原因在于你的程序——看看你的程序设计还能不能改进?编译器是不是为了顺利产出正确的二进制码而不得不吃进成百个头文件、几万行的源代码?原则上,只要对源码适当优化一下,编译缓慢的问题应该可以解决。如果症结在于你的类库供应商,那么你大概除了“换一家类库供应商”外确实没什么可做的了;但如果问题在于你自己的代码,那么完全可以通过重构(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; // ... };
所以,在基类中放一些“对于继承类之实作有帮助”的功能或许是出于好意,但实则是麻烦的源泉。用户的要求是多变的,所以实作代码也是多变的。将多变的代码放在许多继承类都要用到的基类之中,那么变化可就不是局部的了,这会造成全局影响的!具体而言就是:基类所倚赖的一个头文件变动了,那么所有继承类所在的文件都需重新编译。
这样分析过后,解决之道就显而易见了:仅仅把基类用作为抽象的公共界面,而将“对继承类有用”的实作功能移出。
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; };
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"; }
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
另外,有虚函数的类有虚机制的开销[译注:指存放vtable带来的空间开销和通过vtable中的指针间接调用带来的时间开销],通常而言每个对象增加的空间开销是一个字长。这个开销可不小,而且会造成和其他语言(比如C,Fortran)的不兼容性——有虚函数的类的内存数据布局和普通的类是很不一样的。[译注:这种内存数据布局的兼容性问题会给多语言混合编程带来麻烦。]
《The Design and Evolution of C++》 中有更多关于设计理念的细节。
那么,何时我该让析构函数为虚呢?哦,答案是——当类有其它虚函数的时候,你就应该让析构函数为虚。有其它虚函数,就意味着这个类要被继承,就意味着它有点“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 }
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 // ... }
#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
#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实际上是在告诉你,不是“先有实作再有规则”,而是“如此实作,因为规则如此”。]
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); } }
如何在类继承体系中定义配对的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: 可以的,但何必呢?好吧,也许有两个理由:
如果为了和“虚函数调用”说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
让我们来看这段代码:
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; } };
既然如此,我们干吗不干脆在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> { // ... };
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++的内联语法语义。
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(vectorv) { 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。]
#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)。
嗯,从异常处理代码返回到异常抛出处继续执行后面的代码的想法很好[译注:现行异常机制的设计是:当异常被抛出和处理后,从处理代码所在的那个catch块往下执行],但主要问题在于——exception handler不可能知道为了让后面的代码正常运行,需要做多少清除异常的工作[译注:毕竟,当有异常发生,事情就有点不太对劲了,不是吗;更何况收拾烂摊子永远是件麻烦的事],所以,如果要让“继续执行”能够正常工作,写throw代码的人和写catch代码的人必须对彼此的代码都很熟悉,而这就带来了复杂的相互依赖关系[译注:既指开发人员之间的“相互依赖”,也指代码间的相互依赖——紧耦合的代码可不是好代码哦 :O) ],会带来很多麻烦的维护问题。
在我设计C++的异常处理机制的时候,我曾认真地考虑过这个问题;在C++标准化的过程中,这个问题也被详细地讨论过。(参见《The Design and Evolution of C++》中关于异常处理的章节)如果你想试试看在抛出异常之前能不能解决问题然后继续往下执行,你可以先调用一个“检查—恢复”函数,然后,如果还是不能解决问题,再把异常抛出。一个这样的例子是new_handler。
在C++中,处理内存重分配的较好办法是使用标准库中的容器,比如vector。[译注:这些容器会自己管理需要的内存,在必要时会“增长尺寸”——进行重分配。]
#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'; }
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中的资源管理例子。
#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); // ... }
不可以——从你无法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》一文中给出了其它例子,可以参考。
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;
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) { /* ... */ }
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 // ... }
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
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() { /* ... */ }
而“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++》。
#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"; }
不过如果让人来读,两者的含义就有所不同了。代码的书写风格是很重要的。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++》 。
我个人喜欢使用“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》中有一章对“代码风格”问题作了详细的阐述。]
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
#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++语言中的其他机制,比如内联函数、模板、构造函数、析构函数、异常处理等。