还是王富涛啊,留言问了一个和 “类的引用成员数据” 有关的问题。
什么叫“类的引用成员”,看看:
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++要让我们可以重载“->”操作符呢?就是为了让类有机会“玩欺骗”。玩什么欺骗?以为什么要玩欺骗?我觉得这类思索,是学习编程语言中,非常需要的——也就一句老话:知其然,知其所以然。
什么叫“类的引用成员”,看看:
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++要让我们可以重载“->”操作符呢?就是为了让类有机会“玩欺骗”。玩什么欺骗?以为什么要玩欺骗?我觉得这类思索,是学习编程语言中,非常需要的——也就一句老话:知其然,知其所以然。
发表评论
-
我用spring写的一个项目
2009-08-24 18:42 592我用spring写的一个项目 http://code.goog ... -
工信部组织三大运营商应对海缆中断事故
2009-08-20 09:47 89717日FNAL海缆在香港至韩 ... -
热点讨论:Java编程风格的改变
2009-08-20 09:46 690最近Stephan Schmidt在博客中发表了题为《下一代J ... -
独家:继Java 6 Update 16之后 Sun又推出JDK 7预览版
2009-08-20 09:45 682http://www.kingofcoders.com/vie ... -
Zune HD威胁iPod的5大理由 不只是播放器
2009-08-17 11:52 9518月14日消息,据国外媒体报道,有分析人士认为,即将上市的微软 ... -
一个专为操作系统开发者兴及汇编高手而设的x86-ia32调试器
2009-08-07 02:32 915一个专为操作系统开发者兴及汇编高手而设的x86-ia32调试器 ... -
阿里巴巴将推出拼音输入法 正内部测试
2009-08-06 12:26 914编程王网站 http://www.kingofcoders.c ... -
龙芯终获国外专利授权:中国芯背负血统之争
2009-08-03 19:34 959《IT时代周刊》记者/秦 ... -
独家:Google发布“Simple”编程语言
2009-08-01 02:00 863根据国外媒体报道,Google日前发布了一款类BASIC的简易 ... -
IBM将斥资12亿美元收购分析软件开发商SPSS
2009-08-01 01:59 474北京时间7月28日消息,据国外媒体报道,IBM今天表示将以现金 ... -
REST的主要优势到底是什么?
2009-07-30 10:46 713http://www.kingofcoders.com/vie ...
相关推荐
Vue-基础语法Vue-基础语法Vue-基础语法Vue-基础语法Vue-基础语法Vue-基础语法Vue-基础语法Vue-基础语法Vue-基础语法Vue-基础语法Vue-基础语法Vue-基础语法Vue-基础语法Vue-基础语法Vue-基础语法Vue-基础语法Vue-基础...
Vue v-for computed 生命周期和模板引用 语法示例演示代码Vue v-for computed 生命周期和模板引用 语法示例演示代码Vue v-for computed 生命周期和模板引用 语法示例演示代码Vue v-for computed 生命周期和模板引用 ...
易语言教程-第三章-易语言语法-第五节-类型-新类型-类
2.7_语法速通-列表渲染|uni-app_语法基础|uni-app_&_uniCloud_从零入门开发《IT技术资讯类跨端应用
LR分析器从右端符号开始,逐步构建语法树;而LL分析器则从输入的开始符号开始,向左解析。 4. **上下文无关文法**:为了进行语法分析,我们需要定义C-minus的上下文无关文法(CFG)。这是一个形式化的规则集,描述...
java-c语法5.1--do while循环---马克-to-win java视频 子函数
Python计划. 面向对象语法精讲面. 面向对象进阶-对象的引用5.mp4
Python计划. 面向对象语法精讲面. 面向对象进阶-对象的引用1.mp4
2.8_语法速通-基础组件|uni-app_语法基础|uni-app_&_uniCloud_从零入门开发《IT技术资讯类跨端应用
Hadoop-2.8.0-HA-Hive安装部署与HQL12.hive的基本语法--数据导入--从本地--从hdfs.mp4
Agilent3070_BT-BASIC_输入输出语法设置Agilent3070_BT-BASIC_输入输出语法设置Agilent3070_BT-BASIC_输入输出语法设置Agilent3070_BT-BASIC_输入输出语法设置Agilent3070_BT-BASIC_输入输出语法设置Agilent3070_BT-...
本文将深入探讨编译器的核心组成部分之一——语法分析器,以及它是如何使用C语言实现的。 语法分析器是编译器的第二阶段,其主要任务是从词法分析器(也称为扫描器)生成的标记流中构建抽象语法树(AST)。这个过程...
model v-if 语法示例演示代码Vue v-on v-model v-if 语法示例演示代码Vue v-on v-model v-if 语法示例演示代码Vue v-on v-model v-if 语法示例演示代码Vue v-on v-model v-if 语法示例演示代码Vue v-on v-model v-if...
易语言教程-第三章-易语言语法-第四节-代码-函数-程序
2.9_语法速通-自定义组件|uni-app_语法基础|uni-app_&_uniCloud_从零入门开发《IT技术资讯类跨端应
MySQL语法手册是学习和理解MySQL操作的重要参考资料,涵盖了从基本的查询到复杂的存储过程等所有方面。 一、SQL基础 SQL是用于管理关系数据库的语言,MySQL语法手册首先会介绍SQL的基础概念,包括数据类型(如整数...
java语法 非矩形数组 马克-to-win java视频 二重循环
java语法 while循环 马克-to-win java视频 数组 子程序
java语法 数组 马克-to-win java视频 array 的详细介绍
新日本语能力考试N2语法 练习篇 () (z-lib.org).pdf