sherrylso

C++博客 首页 新随笔 联系 聚合 管理
  18 Posts :: 0 Stories :: 124 Comments :: 0 Trackbacks

        C++之父Bjarne stroustrup曾经说过:不需要了解所有的c++细节,也能够写出好的c++程序;不应该注重语言方面的特征,而应该注重软件设计技术本身。很显然,我的这篇文章,与这两句话背道而驰:).的确,我们程序员,不应该把精力放在c++本身语言的特征上,而是应该思考软件设计技术本身。那么,在我们需要提高对c++理解的同时,是不是我们从下面几个方面为着眼点
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++程序的基本。
未完待续:)

 

 


 

posted on 2007-11-11 14:56 爱上龙卷风 阅读(4597) 评论(26)  编辑 收藏 引用

Feedback

# re: c++晦涩语法背后的故事(一) 2007-11-11 15:16 lovedday
关于

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!!!
}

很显然编译报错是对的,因为函数原型不一样,所以不构成overwrite。

既然不构成overwrite,那么你调用的就是Derived中的f函数,编译器检查后发现没有匹配的函数,当然报错。

至于后面的解释个人认为不着调。  回复  更多评论
  

# re: c++晦涩语法背后的故事(一) 2007-11-11 15:25 <a href=http://minidx.com>minidxer</a>
同意lovedday的观点  回复  更多评论
  

# re: c++晦涩语法背后的故事(一) 2007-11-11 15:25 lovedday
#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;
}

这种代码基本上属于滥用运算符重载,实际上C++的不少机制都会导致被滥用,结果就是产生晦涩难懂且充满bug的垃圾代码,当然这种代码做为反面教材还是不错的,但若是在项目中使用这种代码,那将是恶梦。
  回复  更多评论
  

# re: c++晦涩语法背后的故事(一) 2007-11-11 15:29 lovedday
个人认为尽量少重载运算符,比如printf就比<<好用,实在没有什么必要去重载<<,使用函数更好。  回复  更多评论
  

# re: c++晦涩语法背后的故事(一) 2007-11-11 15:34 lovedday
ps,说这些没别的意思,只是说在实际项目中不应该这么使用运算符重载,当然sherrylso只是为了说明C++语法而编写这些代码。  回复  更多评论
  

# re: c++晦涩语法背后的故事(一) 2007-11-11 15:37 lovedday
C++里还有函数重载和参数默认值,实际上这两个机制是滋生bug的温床,如果配合多态,将会导致十分难以理解的行为,所以别用函数重载和参数默认值。

看看这个例子,绝对让你抓狂,猜猜看输出的i和j值是多少?

#include <stdio.h>

class PARENT
{
public:
virtual int doIt( int v, int av = 10 )
{
return v * v;
}
};

class CHILD : public PARENT
{
public:
int doIt( int v, int av = 20 )
{
return v * av;
}
};

int main()
{
PARENT *p = new CHILD();

int i = p->doIt(3);
printf("i = %d\n", i);

CHILD* q = new CHILD();

int j = q->doIt(3);
printf("j = %d\n", j);

return 0;
}  回复  更多评论
  

# re: c++晦涩语法背后的故事(一) 2007-11-11 16:20 foobar
@lovedday
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!!!
}

这个例子只有声明没有定义,编译肯定有错
但是
在c++里,子类通过函数名字隐藏父类函数,而不是通过函数签名!

这个分析应该没什么问题
子类确实通过函数名字隐藏了父类的函数
  回复  更多评论
  

# re: c++晦涩语法背后的故事(一) 2007-11-11 16:38 lovedday
在c++里,子类通过函数名字隐藏父类函数,而不是通过函数签名!

这个分析应该没什么问题
子类确实通过函数名字隐藏了父类的函数

-------------------------------------------------------

你的这个论断显然是错的。

#include <stdio.h>

class Base
{
public:
virtual void f(int x)
{
printf("call base f function\n");
}
};

class Derived: public Base
{
public:
virtual void f(double pd)
{
printf("call derived f function\n");
}
};

int main()
{
Base* pb = new Derived;
pb->f(10.5);

return 0;
}

输出:

call base f function  回复  更多评论
  

# re: c++晦涩语法背后的故事(一) 2007-11-11 16:47 foobar
@lovedday
pb是个父类指针,所以调用父类的f

Derived* pb = new Derived;
pb->f(10);
  回复  更多评论
  

# re: c++晦涩语法背后的故事(一) 2007-11-11 17:00 lovedday
@foobar

很怀疑你是不是C++没学好。  回复  更多评论
  

# re: c++晦涩语法背后的故事(一) 2007-11-11 17:05 lovedday
#include <stdio.h>

class Base
{
public:
virtual void f(int x)
{
printf("call base f function\n");
}
};

class Derived: public Base
{
public:
virtual void f(double pd)
{
printf("call derived f function\n");
}
};

int main()
{
Base* pb = new Derived;
pb->f(10.5);

return 0;
}

输出:

call base f function

你说只要函数名称相同子类就可以覆盖父类函数的行为,但很显然,这个例子说明了仅函数名称相同是无法覆盖的。如果你没看懂,是不是说明还没把多态搞清楚?  回复  更多评论
  

# re: c++晦涩语法背后的故事(一) 2007-11-11 17:07 foobar
@lovedday
上面说的不够确切
你c++学的好,知道多态这个概念,但是c++有很多缺点
楼主的那个例子,你在理解上是有误的
子类的确实用函数签名hide了父类的函数
所以你调用f的时候,肯定调用了f(double)

但是用dynamic type的时候,情况就不是这样了

nt main()
{
Base* pb = new Derived;
pb->f(10.5);

return 0;
}

输出当然是base
但是绝对不是子类有个函数输出base
而确实调用了父类的函数

  回复  更多评论
  

# re: c++晦涩语法背后的故事(一) 2007-11-11 17:10 foobar
@lovedday
多态是c++的一个特性 但是 c++远比这个复杂
你看看下面的代码,希望能明白我的意思

#include <iostream>
class Base
{
public:
virtual void foo() { std::cout << "Base::foo\n"; }
void foo(int) { std::cout << "Base::foo(int)\n"; }
};

class Derived : public Base
{
public:
void foo() { std::cout << "Derived::foo\n"; }
};

int main()
{
Base b;
b.foo(); // Base's foo() is called
b.foo(3); // Base's foo(int) is called
Derived d;
d.foo(); // Derived's foo() is called. The one in Base is hidden
d.foo(3); // error, that function is also hidden
Base &ref = d;
ref.foo(); // Derived's foo() is called. It overrides the Base version
ref.foo(3); // Base's foo(int) is called
}  回复  更多评论
  

# re: c++晦涩语法背后的故事(一) 2007-11-11 17:45 lovedday
@foobar

谢谢,我明白了作者的意思,但实际上正如文章所指出的那样,这是一个糟糕的设计,实际上这些都导致了含混不清的语义和容易出错的代码,所以我说不要去重载函数。

C++的复杂很大一部分原因是因为其糟糕的设计。  回复  更多评论
  

# re: c++晦涩语法背后的故事(一) 2007-11-12 12:50 pzy
我想可以这么总结:
派生类中的函数会override基类中与它具有相同签名的虚函数,同时会hide基类中所有与它具有相同名称(而非签名)的非虚函数。无论如何,派生类函数不会overload基类函数。

lovedday提供的那个例子(参数默认值)的结果实在是匪夷所思,仿佛此时参数默认值也是签名的一部分。谁能解释一下啊?  回复  更多评论
  

# re: c++晦涩语法背后的故事(一)[未登录] 2007-11-14 00:43 romangol
@pzy
下面是对应函数调用的反汇编代码中重要的几行
int i = p->doIt(3);
0040133B MOV DWORD PTR SS:[ESP+8],0A ;压入默认参数10
00401343 MOV DWORD PTR SS:[ESP+4],3 ;压入调用参数3
0040134E MOV DWORD PTR SS:[ESP],EAX
00401351 MOV EAX,DWORD PTR DS:[EDX]
00401353 CALL EAX

int j = q->doIt(3);
00401389 MOV DWORD PTR SS:[ESP+8],14 ;压入默认参数20
00401391 MOV DWORD PTR SS:[ESP+4],3 ;压入调用参数3
00401399 MOV EAX,DWORD PTR SS:[EBP-10]
0040139C MOV DWORD PTR SS:[ESP],EAX
0040139F MOV EAX,DWORD PTR DS:[EDX]
004013A1 CALL EAX

注意到两个默认参数是在编译时就已经写死了,也就是说编译器在编译器就为两个doIt调用分别决议了默认参数,然而在运行期它们却是通过动态的方式
Call EAX
来运行,实际上在调试中也可以看到它们调用的都是子类的doIt函数(调用地址是相同的)。

我个人的理解是,虚函数的调用是通过虚函数表来完成的,故父类指针可以调用子类的函数,然而默认参数作为对象内存模型的一部分,是分别属于不同的对象的,故编译器为父类指针的函数调用还是赋予了父类对象的参数值。

请高手们指教  回复  更多评论
  

# re: c++晦涩语法背后的故事(一) 2007-11-14 00:45 march rabbit
赞~  回复  更多评论
  

# re: c++晦涩语法背后的故事(一) 2007-11-15 22:11 爱上龙卷风
@foobar
非常感谢foobar对本文详尽的解释。  回复  更多评论
  

# re: c++晦涩语法背后的故事(一) 2007-11-15 23:00 foobar
@爱上龙卷风

不要感谢foobar,还是感谢google和c++编程思想吧。。。  回复  更多评论
  

# re: c++晦涩语法背后的故事(一) 2007-11-16 12:30 pzy
@romangol

哈哈~我不太懂汇编,但应该明白你的意思了:lovedday的例子中,PARENT指针和CHILD指针调用的都是CHILD::doIt(),只不过在调用之前(编译阶段),编译器已经它们填上默认参数值了,分别是10和20。

也就是说虚函数调用有多态性,而默认参数是静态的,不存在运行时决策(如你所说,默认参数是在编译时就已经写死了)。

这个例子是有意设计成让PARANT::doIt()与CHILD::doIt()的函数体相同,才有迷惑人的效果。

PS. 由此我进一步理解了C#为什么不支持默认参数值,也不鼓励在库中使用公开常量。默认参数值与常量都是在编译后就“死”的,这个事实的后果是,修改库后,用户代码需要重编译。  回复  更多评论
  

# re: c++晦涩语法背后的故事(一) 2007-12-27 22:06 秦歌
顶一下!  回复  更多评论
  

# re: c++晦涩语法背后的故事(一) 2008-01-07 22:42 abettor.org
大家分析的都很好,学习了!  回复  更多评论
  

# re: c++晦涩语法背后的故事(一) 2008-07-03 14:12 md6600
C++代码本来就很明了精简。 只不过 人比较懒惰,所以越来越简。就会这样咯。 写句注释嘛 有多难呢111  回复  更多评论
  

# What a joy to find such clear thinikng. Thanks for posting! 2011-05-26 10:20 Mateen
What a joy to find such clear thinikng. Thanks for posting!  回复  更多评论
  

# re: c++晦涩语法背后的故事(一) 2013-02-27 11:10 bluemei
用重写来弥补一下:
class Base
{
public:
virtual void f(){
TRACE("Base f()\n");
}
virtual void f(int i){
TRACE("Base f(int)\n");
}
};
class Child : public Base
{
public:
virtual void f(){
TRACE("Child f()\n");
}
virtual void f(int i){
__super::f(i);
}
};

Child *p=new Child();
p->f();
p->f(10);  回复  更多评论
  


只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理