一、引用与指针基本概念的异同
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的虚函数。
引用变量间的赋值运算由赋值运算符的重载函数来决定,而引用变量作为函数参数传递时只会传入地址值。
可见引用变量间的传递并不会产生对象切片现象。
指针与引用,在More Effective C++ 的条款一有详细讲述,我给你转过来
条款一:指针与引用的区别
指针与引用看上去完全不同(指针用操作符‘*’和‘->’,引用使用操作符‘&’),但是它们似乎有相同的功能。指针与引用都是让你间接引用其他对象。你如何决定在什么时候使用指针,在什么时候使用引用呢?
首先,要认识到在任何情况下都不能用指向空值的引用。一个引用必须总是指向某些对象。因此如果你使用一个变量并让它指向一个对象,但是该变量在某些时候也可能不指向任何对象,这时你应该把变量声明为指针,因为这样你可以赋空值给该变量。相反,如果变量肯定指向一个对象,例如你的设计不允许变量为空,这时你就可以把变量声明为引用。
“但是,请等一下”,你怀疑地问,“这样的代码会产生什么样的后果?”
char *pc = 0; // 设置指针为空值
char& rc = *pc; // 让引用指向空值
这是非常有害的,毫无疑问。结果将是不确定的(编译器能产生一些输出,导致任何事情都有可能发生),应该躲开写出这样代码的人除非他们同意改正错误。如果你担心这样的代码会出现在你的软件里,那么你最好完全避免使用引用,要不然就去让更优秀的程序员去做。我们以后将忽略一个引用指向空值的可能性。
因为引用肯定会指向一个对象,在C里,引用应被初始化。
string& rs; // 错误,引用必须被初始化
string s("xyzzy");
string& rs = s; // 正确,rs指向s
指针没有这样的限制。
string *ps; // 未初始化的指针
// 合法但危险
不存在指向空值的引用这个事实意味着使用引用的代码效率比使用指针的要高。因为在使用引用之前不需要测试它的合法性。
void printDouble(const double& rd)
{
cout << rd; // 不需要测试rd,它
} // 肯定指向一个double值
相反,指针则应该总是被测试,防止其为空:
void printDouble(const double *pd)
{
if (pd)
{ // 检查是否为NULL
cout << *pd;
}
}
指针与引用的另一个重要的不同是指针可以被重新赋值以指向另一个不同的对象。但是引用则总是指向在初始化时被指定的对象,以后不能改变。
string s1("Nancy");
string s2("Clancy");
string& rs = s1; // rs 引用 s1
string *ps = &s1; // ps 指向 s1
rs = s2; // rs 仍旧引用s1,
// 但是 s1的值现在是
// "Clancy"
ps = &s2; // ps 现在指向 s2;
// s1 没有改变
总的来说,在以下情况下你应该使用指针,一是你考虑到存在不指向任何对象的可能(在这种情况下,你能够设置指针为空),二是你需要能够在不同的时刻指向不同的对象(在这种情况下,你能改变指针的指向)。如果总是指向一个对象并且一旦指向一个对象后就不会改变指向,那么你应该使用引用。
还有一种情况,就是当你重载某个操作符时,你应该使用引用。最普通的例子是操作符[].这个操作符典型的用法是返回一个目标对象,其能被赋值。
vector<int> v(10); // 建立整形向量(vector),大小为10;
// 向量是一个在标准C库中的一个模板(见条款35)
v[5] = 10; // 这个被赋值的目标对象就是操作符[]返回的值
如果操作符[]返回一个指针,那么后一个语句就得这样写:
*v[5] = 10;
但是这样会使得v看上去象是一个向量指针。因此你会选择让操作符返回一个引用。(这有一个有趣的例外,参见条款30)
当你知道你必须指向一个对象并且不想改变其指向时,或者在重载操作符并为防止不必要的语义误解时,你不应该使用指针。而在除此之外的其他情况下,则应使用指针假设你有
void func(int* p, int&r);
int a = 1;
int b = 1;
func(&a,b);
指针本身的值(地址值)是以pass by value进行的,你能改变地址值,但这并不会改变指针所指向的变量的值,
p = someotherpointer; //a is still 1
但能用指针来改变指针所指向的变量的值,
*p = 123131; // a now is 123131
但引用本身是以pass by reference进行的,改变其值即改变引用所对应的变量的值
r = 1231; // b now is 1231
尽可能使用引用,不得已时使用指针。
当你不需要“重新指向”时,引用一般优先于指针被选用。这通常意味着引用用于类的公有接口时更有用。引用出现的典型场合是对象的表面,而指针用于对象内部。
上述的例外情况是函数的参数或返回值需要一个“临界”的引用时。这时通常最好返回/获取一个指针,并使用 NULL 指针来完成这个特殊的使命。(引用应该总是对象的别名,而不是被解除引用的 NULL 指针)。
注意:由于在调用者的代码处,无法提供清晰的的引用语义,所以传统的 C 程序员有时并不喜欢引用。然而,当有了一些 C++ 经验后,你会很快认识到这是信息隐藏的一种形式,它是有益的而不是有害的。就如同,程序员应该针对要解决的问题写代码,而不是机器本身。
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的虚函数。
引用变量间的赋值运算由赋值运算符的重载函数来决定,而引用变量作为函数参数传递时只会传入地址值。
可见引用变量间的传递并不会产生对象切片现象。
指针与引用,在More Effective C++ 的条款一有详细讲述,我给你转过来
条款一:指针与引用的区别
指针与引用看上去完全不同(指针用操作符‘*’和‘->’,引用使用操作符‘&’),但是它们似乎有相同的功能。指针与引用都是让你间接引用其他对象。你如何决定在什么时候使用指针,在什么时候使用引用呢?
首先,要认识到在任何情况下都不能用指向空值的引用。一个引用必须总是指向某些对象。因此如果你使用一个变量并让它指向一个对象,但是该变量在某些时候也可能不指向任何对象,这时你应该把变量声明为指针,因为这样你可以赋空值给该变量。相反,如果变量肯定指向一个对象,例如你的设计不允许变量为空,这时你就可以把变量声明为引用。
“但是,请等一下”,你怀疑地问,“这样的代码会产生什么样的后果?”
char *pc = 0; // 设置指针为空值
char& rc = *pc; // 让引用指向空值
这是非常有害的,毫无疑问。结果将是不确定的(编译器能产生一些输出,导致任何事情都有可能发生),应该躲开写出这样代码的人除非他们同意改正错误。如果你担心这样的代码会出现在你的软件里,那么你最好完全避免使用引用,要不然就去让更优秀的程序员去做。我们以后将忽略一个引用指向空值的可能性。
因为引用肯定会指向一个对象,在C里,引用应被初始化。
string& rs; // 错误,引用必须被初始化
string s("xyzzy");
string& rs = s; // 正确,rs指向s
指针没有这样的限制。
string *ps; // 未初始化的指针
// 合法但危险
不存在指向空值的引用这个事实意味着使用引用的代码效率比使用指针的要高。因为在使用引用之前不需要测试它的合法性。
void printDouble(const double& rd)
{
cout << rd; // 不需要测试rd,它
} // 肯定指向一个double值
相反,指针则应该总是被测试,防止其为空:
void printDouble(const double *pd)
{
if (pd)
{ // 检查是否为NULL
cout << *pd;
}
}
指针与引用的另一个重要的不同是指针可以被重新赋值以指向另一个不同的对象。但是引用则总是指向在初始化时被指定的对象,以后不能改变。
string s1("Nancy");
string s2("Clancy");
string& rs = s1; // rs 引用 s1
string *ps = &s1; // ps 指向 s1
rs = s2; // rs 仍旧引用s1,
// 但是 s1的值现在是
// "Clancy"
ps = &s2; // ps 现在指向 s2;
// s1 没有改变
总的来说,在以下情况下你应该使用指针,一是你考虑到存在不指向任何对象的可能(在这种情况下,你能够设置指针为空),二是你需要能够在不同的时刻指向不同的对象(在这种情况下,你能改变指针的指向)。如果总是指向一个对象并且一旦指向一个对象后就不会改变指向,那么你应该使用引用。
还有一种情况,就是当你重载某个操作符时,你应该使用引用。最普通的例子是操作符[].这个操作符典型的用法是返回一个目标对象,其能被赋值。
vector<int> v(10); // 建立整形向量(vector),大小为10;
// 向量是一个在标准C库中的一个模板(见条款35)
v[5] = 10; // 这个被赋值的目标对象就是操作符[]返回的值
如果操作符[]返回一个指针,那么后一个语句就得这样写:
*v[5] = 10;
但是这样会使得v看上去象是一个向量指针。因此你会选择让操作符返回一个引用。(这有一个有趣的例外,参见条款30)
当你知道你必须指向一个对象并且不想改变其指向时,或者在重载操作符并为防止不必要的语义误解时,你不应该使用指针。而在除此之外的其他情况下,则应使用指针假设你有
void func(int* p, int&r);
int a = 1;
int b = 1;
func(&a,b);
指针本身的值(地址值)是以pass by value进行的,你能改变地址值,但这并不会改变指针所指向的变量的值,
p = someotherpointer; //a is still 1
但能用指针来改变指针所指向的变量的值,
*p = 123131; // a now is 123131
但引用本身是以pass by reference进行的,改变其值即改变引用所对应的变量的值
r = 1231; // b now is 1231
尽可能使用引用,不得已时使用指针。
当你不需要“重新指向”时,引用一般优先于指针被选用。这通常意味着引用用于类的公有接口时更有用。引用出现的典型场合是对象的表面,而指针用于对象内部。
上述的例外情况是函数的参数或返回值需要一个“临界”的引用时。这时通常最好返回/获取一个指针,并使用 NULL 指针来完成这个特殊的使命。(引用应该总是对象的别名,而不是被解除引用的 NULL 指针)。
注意:由于在调用者的代码处,无法提供清晰的的引用语义,所以传统的 C 程序员有时并不喜欢引用。然而,当有了一些 C++ 经验后,你会很快认识到这是信息隐藏的一种形式,它是有益的而不是有害的。就如同,程序员应该针对要解决的问题写代码,而不是机器本身。