一、引用与指针基本概念的异同
1、引用必须依附一个已存在对象,所以在定义一个引用变量时,必须用一个已存在的对象,对其进行初始化。而指针变量则可以独立存在。例如:
class CBase
{...}
CBase oB1;
CBase &rB1=oB1; //合法,引用变量rB1指向对象oB1.
CBase &rB2; //非法,编译时,编译器报错,必须对其进行初始化。
CBse *pB1=&oB1; //合法,用oB1的地址初始化指针变量pB1。
CBse *pB2; //合法,pB2是一个未初始化的指针变量,指向一个未知空间(有的编译器将其初始化
为null)。
2、引用变量一旦被始始化,就不能指向其它对象。所有对此引用变量的操作(注意:包括赋值操作。),都
会作用到它所引用的对象上。这就是戏称中的引用的“从一而终”的属性。而指针变量显然不同。例如:
在《C++ Primer》中举了以下例子
int ival = 1024;
int ival2 = 2048;
int *pi = &ival;
int *pi2 = &ival2;
pi = pi2; //此时pi与pi2指向了相同的内存空间——变量ival2所占用的内存空间。
int &ri = ival;
int &ri2 = ival2;
ri = ri2; //此时ri仍旧指向ival,只不过ival的值通过此赋值语句变得与ival2相同了。也就是说这个赋值语句
的实际意义是:用ival2的值来改变ival的值。
其实引用变量与指针变量,在本质上都是一个某地址空间的地址值,在这一点上两者是相同的。只不过两者在属性上有所不同而已。假设上例中ival的地址是0xff5f则,ri的实际值就是0xff5f;ival2的地址是0xfffa则ri2的值就是0xfffa。
语句 ri = ri2; 改变的是ri所指向的对象ival的值,此时ri的值仍为0xff5f 而ival的值变成了2048。
3、同为地址变量的引用和指针还有如下不同
a、初始化
在前面的例子里有:
CBase oB1;
CBase &rB1=oB1; //合法,引用变量rB1指向对象oB1.
此时我们可以这样来初始化rB2:
CBase &rB2=oB1;
也可以这样来初始化它:
CBase &rB2=rB1; //此时rB2与rB1引用了同一个对象oB1,或者说对象oB1有了两个别名。
但显然我们可以这样来初始化指针变量pB1:
CBase *pB1=&oB1;
但却不能这样来初始化它:
CBase *pB1=oB1; //非法,类型不匹配
b、赋值
同样在赋值时我们可以这样写:
rB2 = rB1;
也可以这样写:
rB2 = oB1;
但显然我们可以这样写:
CBase *pB2;
pB2 = pB1;
却不能这样写:
pB2 = oB1; //必须用取址运算符&
之所以有这种区别是因为,引用变量虽然是一个地址变量,但它可以被当作它所引用的对象来使用,而我们可以用同类型的对象,来直接对同类型的其它对象进行初始化和赋值操作。也就是说一个引用变量既可以被看作一个地址变量,又可以被看作它所引用的对象。
显然指针变量就不具备这种特点。它只能被当作一个地址变量来看待。
二、我们的思考
如果如上所说引用的赋值,实际上是对被引用对象的值的“拷贝”,那么我们在使用引用时就有下面的问题:
问题一、类类型对象的引用之间在进行赋值操作时,会出现什么情况?
问题二、类类型对象的引用之间在进行赋值操作时,其速度比指针变量间的赋值操作快还是慢?
问题三、类类型对象的引用变量进行传递时,会产生对象切片现象吗?
假设我们有两个类:
class CBase
{
public:
CBase(const char* pszName): m_oName(pszName){}
virtual void Show()
{
cout << "I am Base :" << m_oName.c_str()<< endl;
}
protected:
const string m_oName;
};
class CDerive : public CBase
{
public:
CDerive(const char* pszName):CBase(pszName){}
virtual void Show()
{
cout << "I am Derive :" << m_oName.c_str() << endl;
}
};
考虑第一个问题,我们有:
CBase oB1("One");
CBase oB2("Two");
CBase &rB1=oB1;
CBase &rB2=oB2;
如果我们做如下操作:
rB1 = rB2;
由于引用的赋值操作,实质上是它们所引用的对象间的赋值。所以以上的赋值就相当于:
oB1 = oB2;
因为我们没有对CBase类定义赋值运算,所以编译器会报错。
(很明显,对指针变量来说,就不存在这样的问题。)
由于类类型对象的引用之间进行的赋值运算,要通过重载赋值运算符来进行,也就暗示了它应该是对
对象的成员变量逐值进行拷贝的。这说明从本质上来说它是以对象间的传值方式来进行的。而类类型对象
的指针变量在赋值运算时,仅仅是在两个变量间传递了一个对象的内存地址。
显然,后者的速度要快的多。
现在考虑第三个问题:
按照《C++编程思想》里的说法,对象切片发生在派生类对象向其基类对象进行值传递时(这一般发生在作为
函数参数进行传递时)。比如:
void call(CBase oB)
{
oB.Show();
}
int main()
{
CDerive oD1("aa");
call(oD1);
return 0;
}
其运行结果为显示:"I am Base : aa",而不是"I am Derive : aa"。这是因为在调用call时,oD1会以值传递的方式传入,而call函数内部会生成一个CBase类型的对象,只接受oD1对象的相对应的部分,其余部分则被舍弃(就象是被切除了)。在生成这个CBase对象时,会调用CBase的复制构造函数,使得对象的VPTR指针指向CBase的VTABLE。这样就会在虚函数调用时只调用相应的CBase的虚函数。
引用变量间的赋值运算由赋值运算符的重载函数来决定,而引用变量作为函数参数传递时只会传入地址值。
可见引用变量间的传递并不会产生对象切片现象。