`
un_overload
  • 浏览: 16445 次
  • 性别: Icon_minigender_1
  • 来自: 北京
最近访客 更多访客>>
社区版块
存档分类
最新评论

“语法”之所以成为“语法”--从类的“引用成员”说起。

阅读更多
还是王富涛啊,留言问了一个和 “类的引用成员数据” 有关的问题。

什么叫“类的引用成员”,看看:

Code:
class Coo 

 
    int& ref; //ref 是一个引用 
 
}; 
“引用”是什么? 实现上,引用记的是另一个变量的地址,引用自己并没有“真身”。这一点和指针很像了,除了使用语法不同外,重要不同是,C++规定不能有“空引用”(java语言存在空引用)。也就是说,引用是只“鬼”,它想投生时,一定要找个肉体附身,下面的代码无法通过编译:

Code:
void foo() 

   int& ri; //编译不过去,因为ri是一只游魂野鬼 (没有初始化)
}; 
所以,真正使用“引用”时 ,肯定有一个事先的变量等着它附身:

Code:
void foo() 

   int i; 
   int& ref = i; //或 int ref(i); OK!附体在i上。 
   //... 

那,类中的引用成员,需要初始化这样吗?

Code:
class Coo 

   int& ref = i; //???? 
}; 
这当然是错的,从语法上讲, 这里只是在定义一种类型的对象,应该“长什么样子”,并不真正产生内存对象,从功能及逻辑上讲,定义类,如果可以这样初始化引用成员,那被附身的那个i,应该从哪里来?

但如果没一个办法来让类解决其引用成员数据的初始化,那么,假设我们定义一个对象:

Code:
Coo o; // o 中的ref附体何处? 
解决办法是,C++规定,当存在“引用成员数据”时,必须在类的构造函数的“成员数据初始化列表”中初始化之。

Code:
class Coo 

public: 
    Coo(int& i) //注意,参数i也是一个引用,为什么? 
      : ref(i) 
    {} 
}; 
05行就叫做“成员数据初始化列表”。

既然 ref 附身在 i,那就要求i的“寿命”必须至少和ref一样长,如果i一会儿就“死了(内存空间)”,那这只鬼也太倒霉了。所以,04行的i必须也是一个引用,否则的话,i就是函数调用栈内存,它会在当运行代码出了“初始化列表”,进入Coo构造函数的函数体时,就死了……ref最终附在i的尸体上(结果会如何呢?不好说)。

插话:上例中ref是“引用的引用”吗?当然不是,因为很少有语言中“引用的引用”这种概念,C++也没有。

王富涛 同学的问题来了,为了突出问题本质,我简化了他的代码:

他先是定义一个“鸟”类。

Code:
struct Bird 

   string name; 
}; 
然后定义一个鸟巢类:

Code:
class BirdHouse 

public: 
   BirdHouse(Bird& b) 
     : bird(b) 
   {} 
 
private: 
   Bird& bird; 
}; 
为什么“鸟巢”里住着一个“鸟”的引用,这个我们先不管,因为这是练习题嘛。那一切看起来很正确,构造函数有了,并且也如前所述,初始化了引用成员:bird;



但很快地,和许多陷在语言之语法中的初学者一样,王同学开始和C++语法较上真了。他想要一个无参的构造函数:

Code:
class BirdHouse 

public: 
    BirdHouse() //一个无参构造函数…… 
    { }  //bird怎么初始化? 
 
    //... 
}; 
回帖有人出一个“主意”,说是可以用“参数默认值”,来营造无参构造的效果。这话倒没大错,但用在引用的初始化上,就犯错了:

Code:
class BirdHouse 

public: 
    BirdHouse(Bird b = Bird())  : bird(b)
    {} 
     
     //... 
}; 
真是一个“险恶”的主意啊(发贴的人还补了一句“这个实现有个问题,卖个关子,你自己看能不能发现”)。逻辑上,Bird()每回构造一只新鸟,这样初始化有意义吗?更可怕的是,这只鸟立即就死了。实在要这样,你得:

Code:
class BirdHouse 

public: 
    BirdHouse() 
     : bird (*(new Bird)) 
    {} 
 
    ~BirdHouse() 
    { 
        Bird* p = &bird; 
        delete p; 
    }
  //...
}; 
附身的bird是每次new出来的,所以它不会自己死掉。这也是加上析构函数的原因,得我们自己杀死它。

但,这就是答案吗?

显然不是,这是什么代码啊? 玩语法游戏乎? 如果要的是这个结果,那为什么不直接定义一个指针呢?多干净明了啊:

Code:
class BirdHouse 

public: 
    BirdHouse() 
    { 
       bird = new Bird; //也可以放到初始化列表中去,但意义不大 
    } 
 
   ~BirdHouse() 
    { 
       delete bird; 
    } 
private:
    Bird* bird;
}; 


这就是我想提醒王同学思考的地方了:语法之所以成为语法?原因是什么?只是因为语言的发明人凭空想出来的吗?如果是,这样的语法肯定非常难于理解,也难于记忆。如果不是,那就对学习者,提出一个新的要求了:你不是仅仅要理解并记住某个语法点,而是要能“用”明白,为什么有这个语法点?

基于这一点,我给了简单的回复:

“语法要抠,但重要一点你可能忽略了,任何一个语法点,都是从实际需要出发的。”

“你的代码,一个‘鸟巢’中含用:值、指针、引用三只‘鸟’……呵呵,你这样子写,目的就是为了练习(引用初始化这一语法点)是不是? 能不能从另一个更真实的角度去学习呢?就是给自己设想一个需求,它要求你必须(或者说最好)在某个对角里,带有另外一个对象的引用。”

“提示一下,基本上这样的需求,都可以用‘带有另外一个对象的指针’得以替代,并且也更常见的(因引指针可以方便进初始化为 NULL/0),并且可以变化,但如果确实不需要考虑空指针(这也就意味它必须有一个初始值),这时考虑使用‘引用’就是一个好的设计了。”


“一旦你有这样的设计,你就会发现,实际需求只有两种:一是这个类的对象,必须衔着金勺出生(必须有一个入参来初始化那个引用成员);或者,这个类中那个引用成员,可以被默认‘绑定’到一个全局的变量”



想一想,我觉得这样的说教并不妥当,还是给个简单的例子:

比如,有一个‘鸟’类:

Code:
class Bird 

public: 
    void Eat() { cout << "eat" << endl; } 
    void Fly() { cout << "fly" << endl; } 
    void Sing() { cout << "sing" << endl;} 
}; 
平常我们让这只鸟自由地吃啊,飞啊,唱啊~~但有一天这只鸟被派去当一只卧底的鸟,这时,它的每一个动作,我们都希望知道,怎么办?写一个派生的鸟?一来 原来些Eat,Fly,Sing等,全都不是virtual的,二来,也不符合设计原则。解决办法是给这只鸟加下一个保护外壳!

Code:
class SpyBird

public: 
    SpyBird (Bird& b) 
        : bird(b) 
    {} 
    
    Bird* operator -> () 
    { 
        cout << "黑鸟在行动!" << endl; 
        return &bird; 
    } 
    
private: 
    Bird& bird; 
}; 
这里就非常优雅和合理用到 类的引用成员!第一,SpyBird不能是一只新鸟,所以可以用指针指向原来的鸟,或者用引用绑定,第二,它包装的鸟,不应该是一只“空鸟”,因为在逻辑上没有意义。

Code:
void test_birdcontroler() 

    Bird b; 
    SpyBird bc(b); 
    
    bc->Eat();  //会输出“黑鸟在行动”,下同
    bc->Fly(); 
    bc->Sing(); 

王同学又问到这个“SpyBird”如何实现 “Copy Construct/拷贝构造” 函数。

似乎还是没有转到从“需求”出发学习语法点的点上来,不过没关系,有问题永远比没有问题好。

我补了回答:

并不是每个类都需要“深拷贝”,甚至大多数类,不需要“拷贝”构造,比如我们写一个窗体程序,通常各类窗体(包括对话框),都不需要复制这个动作。

SypBird 这个卧底鸟,如果需要复制,现在看来,在两个对象之间共享同一个“真实身份的鸟”,是合乎逻辑的。对应到现实,就是:有一个人,他有两个卧底身份。典型的如双料间谍。



这样的学习,我们可以从基本的语法记忆,上升到学习语言的“惯用法”,然后倒过来理解某个语法为什么会这样或那样。

结合本例,我们来想想,类的引用成员,它做什么用呢?假设有类A,它含有一个B类型的引用成员数据。这是一种“惯用法”,它至少表达这样一些信息:

1).A类对象一定会拥有一个(通常是来自外部的)B类对象。

2).A类对象在其自身整个生命周期内,从一而终,就使用这个一开始初始化的B对象。

本例中,“鸟”是事先拥有的,但“间谍鸟”是临时需要的。如果直觉是想到让后者派生自前者,这样的直觉不能说完全错,但有点违反我们接受的多年教育。为什么?因为如果存在“间谍鸟”这种类型,那就意味着会产生“一生下来就是间谍,直到死都是间谍”的无间鸟了,这符合鸟性吗?若让我们处理这几类人:人,穷人,福人,把后两者当成“人”的派生类,合适吗?不合适。几千年前,好像是陈胜吴广吧,就喊出了心声:“将相王侯,宁有种乎”?意思就是说“TMD,那些当官的人,难道是一种单独的类型吗?(有人天生就是当官的命吗?)”。

间谍鸟也不是这样,平常它就是一只普通的鸟,过着普通的生活,直到因为种种原因走入这一条无间道……通常情况下,间谍鸟有机会回归成普通鸟,并且,间谍鸟总是要尽量表现得像一只鸟,这就是我们重载“->”操作符的目的。这又是一个例子,为什么C++要让我们可以重载“->”操作符呢?就是为了让类有机会“玩欺骗”。玩什么欺骗?以为什么要玩欺骗?我觉得这类思索,是学习编程语言中,非常需要的——也就一句老话:知其然,知其所以然。
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics