作者:爱上龙卷风 来源:C++博客 酷勤网收集 2007-11-13
1) 从编译原理的角度
2) 从技术需求的角度
3) 从软件设计技术的角度
从以上的几个角度,来重新审视c++一些晦涩语法,或许,我们能从中获益。在这里,我要说的是,我们不单单是要记住这些c++语言特性怎么样的使用,而是应该知道这些语言特性背后隐藏的故事,以便于我们更深层次地理解c++,理解软件设计。
一、子类通过函数名字隐藏父类函数。
如下例:
class Base

{
public:
virtual void f(int x);
};
class Derived: public Base

{
public:
virtual void f(double* pd);
};
int main()

{
Derived* pd = new Derived();
pd->f(10); //compile error!!!
} 当我们编译pd->f(10)操作时,编译器报错。按照我们常规的理解是:父类的函数void f(int x)与子类的函数void f(double*pd),由于参数类型不同,其函数签名也是不一样的,按照这样的逻辑,在这个类继承体系中,这两个函数完全应该是互不隐藏的,我们完全可以认为是符合overloaded规则的两个函数。
但是,在c++里,子类通过函数名字隐藏父类函数,而不是通过函数签名!c++给出的解释也是合理的:试想一种情况:你使用了别人写的类库,继承其中的某个类,写了你自己的子类。
如上面的例子,你的子类就是Derived,而类库中的父类就是Base.当你根本不知道在父类中还有这样一个f(int x)函数时,在调用子类Derived的f函数时,你犯了错误,参数类型传成了int类型(或者不是你犯的错误,编译器帮你自动转化为int类型),结果是:程序可以正常运行,但是,执行的结果却不是你所期望的,是f(int x)调用,而不是你自己的实现:f(double* pd)调用!
这就是c++为什么通过函数名字隐藏父类函数的原因。
说到这里,我们需要补充几句:虽然c++在语言层面上给我们提供了这样的保证,但是,子类hide父类的函数,这是一个非常不好的设计。从OO的角度出发,应该讲求的是Liskov Substitution Principle。即:suntypes must be substitutable fro their base types.很显然,当hide行为发生时,从接口的角度来讲,子类与父类是不能互为替代的。父类的protected or public的方法,应该很自然地由其所有子类所继承,而不是被隐藏。隐藏行为的发生,相当于在这套继承体系中开的一个后门。很显然,C++帮助我们自动隐藏了父类的方法,但是,作为程序开发的我们,应该意识到这一点,也应该避免这样的设计。
二、c++的per-class allocator语法规则
在D&E of C++一书中,Stroustrup给出了几点c++提供per-class allocator的理由,这些理由也是我们使用class level的allocator的原因,所以,有必要我们总结一下:
第一、许多程序应用,需要在运行的过程中,大量地Create和Delete对象。这些对象,诸如:tree nodes,linked list nodes,messages等等。如果在传统的heap完成这些对象的创建,销毁,由于大量的内存申请,释放,势必会造成内存碎片。这种情况下,我们需要对内存分配进行细粒度的控制。
第二、一些应用需要长时间跑在内存受限的装置上,这也需要我们对内存分配进行细粒度的控制,而不是无限制地分配,释放。
主要基于以上的两点,c++提供了per-class allocator语言支持。
如下例:
class X

{
public:
void* operator new(size_t sz); //allocate sz bytes
void operator delete(void* p) //free p;
};
new操作符函数负责对象X的内存分配。对这样一个语法规则,我们好奇的是,为什么声明了一个我们从来都不使用的参数size_t sz.我们的使用语法如下: X* px = new X;
C++也给出了解释:per-class allocator机制将适用整个类的继承体系。例如:
class Y: public X //ojects of class Y are also allocated using X::operator new

{
//
//
}; 对于子类Y,其内存分配函数也是X::operator new()。但是,在这里,内存分配的大小,不应该是sizeof(X),而是sizeof(Y).问题的关键在这里:C++通过提供多余的参数size_t sz,而给开发者提供了更大的灵活性,也即:per-class allocator是面向类的继承体系的内存管理机制,而不单单是面向单个类。
三、Koenig Lookup机制。
大家对Andrew Koenig应该很熟悉,c++大牛,是AT&T公司Shannon实验室大规模编程研究部门中的成员,同时他也是C++标准委员会的项目编辑。他拥有超过30年的编程经验,其中有15年的C++使用经验。
Koenig Lookup,就是以Andrew Koenig命名的查找规则。在看这个定义之前,我们先弄清楚函数所在的域的分类,一般来讲,分为:
1:类域(函数作为某个类的成员函数(静态或非静态))
2:名字空间域
3:全局域(即C++默认的namespace)
而Koenig Lookup机制,就是当编译器对无限定域的函数调用进行名字查找时,除了当前名字空间域以外,也会把函数参数类型所处的名字空间加入查找的范围。
如下例:
#include <iostream>
using namespace std;
namespace Koenig

{
class MyArg
{
public:
ostream& print(ostream& out) const
{
out<<"this is MyArg."<<endl;
}
};
inline ostream& operator<<(ostream& out, const MyArg& myArg)
{
return myArg.print(out);
}
}
int main()

{
Koenig::MyArg myArg;
cout<<myArg;
return 0;
} 如上的代码,使用operator<<操作符函数,打印对象的状态,但是函数ostream& operator<<(ostream& out, const MyArg& myArg) 的定义域是处于名字空间Koenig中,为什么编译器在解析main函数(全局域)里面的operator<<调用时,它能够正确定位到Koenig名字空间里面的operator<<?这是因为根据Koenig查找规则,编译器需要把参数类型MyArg所在的名字空间Koenig也加入对ostream& operator<<(ostream& out, const MyArg& myArg) 调用的名字查找范围中。
如果没有Koenig查找规则,我们就无法直接写cout<<myArg;,而是需要写类似Koenig::operator<<(std::cout, myArg); 这样的代码(使用完全限定名)。这样的结果是,即不直观也不方便。
其实在C++里,提供了很多类似于Koenig查找规则的机制,以保证程序语法上的简洁,明了。例如:许多的操作符函数,COPY构造函数。而这些,也是我们写出专业的C++程序的基本。
未完待续:)
来自:http://www.cppblog.com/sherrylso/archive/2007/11/11/36375.html

