C++指针和引用
★ 指针与引用相同点:
1. 都是地址的概念;
指针指向一块内存,它的内容是所指内存的地址;引用是某块内存的别名。
★ 区别:
1. 指针是一个实体,而引用仅是个别名;
2. 引用使用时无需解引用(*),指针需要解引用;
3. 引用只能在定义时被初始化一次,之后不可变;指针可变;引用“从一而终” ^_^
4. 引用没有 const,指针有 const,const 的指针不可变;
5. 引用不能为空,指针可以为空;
6. “sizeof 引用”得到的是所指向的变量(对象)的大小,而“sizeof 指针”得到的是指针本身(所指向的变量或对象的地址)的大小;typeid(T) == typeid(T&) 恒为真,sizeof(T) == sizeof(T&) 恒为真,但是当引用作为成员时,其占用空间与指针相同(没找到标准的规定)。
7. 指针和引用的自增(++)运算意义不一样;
★ 联系
1. 引用在语言内部用指针实现。
2. 对一般应用而言,把引用理解为指针,不会犯严重语义错误。引用是操作受限了的指针(仅容许取内容操作)。
几种指针类型
(1)T *&p 的指针(语法上同时使用*&)
在下列函数声明中,为什么要同时使用*和&符号?以及什么场合使用这种声明方式?
void func1( MYCLASS *&pBuildingElement );
论坛中经常有人问到这样的问题。本文试图通过一些实际的指针使用经验来解释这个问题。
仔细看一下这种声明方式,确实有点让人迷惑。在某种意义上,"*"和"&"是意思相对的两个东西,把它们放
在一起有什么意义呢?。为了理解指针的这种做法,我们先复习一下C/C++编程中无所不在的指针概念。我
们都知道MYCLASS*的意思:
指向某个对象的指针,此对象的类型为MYCLASS。
void func1(MYCLASS *pMyClass);
// 例如:
MYCLASS* p = new MYCLASS; func1(p);
上面这段代码的这种处理方法想必谁都用过,创建一个MYCLASS对象,然后将它传入func1函数。现在假设
此函数要修改
pMyClass:void func1(MYCLASS *pMyClass) {
DoSomething(pMyClass);
pMyClass = // 其它对象的指针
}
第二条语句在函数过程中只修改了pMyClass的值。并没有修改调用者的变量p的值。如果p指向某个位于
地址0x008a00的对象,当func1返回时,它仍然指向这个特定的对象。(除非func1有bug将堆弄乱了,完
全有这种可能。)
现在假设你想要在func1中修改p的值。这是你的权利。调用者传入一个指针,然后函数给这个指针赋值。
以往一般都是传双指针,即指针的指针,例如 CMyClass**。
MYCLASS* p = NULL;
func1(&p); void func1(MYCLASS** pMyClass)
{
*pMyClass = new MYCLASS;
……
}
调用func1之后,p指向新的对象。在COM编程中,你到处都会碰到这样的用法--例如在查询对象接口的
QueryInterface
函数中:
interface ISomeInterface
{
HRESULT QueryInterface(IID &iid, void** ppvObj);
……
};
LPSOMEINTERFACE p=NULL;
pOb->QueryInterface(IID_SOMEINTERFACE, &p);
此处,p是SOMEINTERFACE类型的指针,所以&p便是指针的指针,在QueryInterface返回的时候,如果
调用成功,则变量p包含一个指向新的接口的指针。
如果你理解指针的指针,那么你肯定就理解指针引用,因为它们完全是一回事。如果你象下面这样声明函数:
void func1(MYCLASS *&pMyClass){
pMyClass = new MYCLASS;
……
}
其实,它和前面所讲得指针的指针例子是一码事,只是语法有所不同。传递的时候不用传p的地址&p,
而是直接传p本身:
MYCLASS* p = NULL; func1(p);
在调用之后,p指向一个新的对象。一般来讲,引用的原理或多或少就象一个指针,从语法上看它就是一个
普通变量。所以只要你碰到*&,就应该想到**。也就是说这个函数修改或可能修改调用者的指针,而调用者
象普通变量一样传递这个指针,不使用地址操作符&。
至于说什么场合要使用这种方法,我会说,极少。MFC在其集合类中用到了它--例如,CObList,它是一
个CObjects指针列表。
class CObList : public CObject {
// 获取/修改指定位置的元素
CObject*& GetAt(POSITION position);
CObject* GetAt(POSITION position) const;
};
这里有两个GetAt函数,功能都是获取给定位置的元素。区别何在呢?
区别在于一个让你修改列表中的对象,另一个则不行。所以如果你写成下面这样:
CObject* pObj = mylist.GetAt(pos);
则pObj是列表中某个对象的指针,如果接着改变pObj的值:
pObj = pSomeOtherObj;
这并改变不了在位置pos处的对象地址,而仅仅是改变了变量pObj。但是,如果你写成下面这样:
CObject*& rpObj = mylist.GetAt(pos);
现在,rpObj是引用一个列表中的对象的指针,所以当改变rpObj时,也会改变列表中位置pos处的对象地
址--换句话说,替代了这个对象。这就是为什么CObList会有两个GetAt函数的缘故。一个可以修改指针的
值,另一个则不能。注意我在此说的是指针,不是对象本身。这两个函数都可以修改对象,但只有*&版本
可以替代对象。
(2)数组名可以退化成指针
数组名可以退化成指向数组首元素的地址的指针!
数组名和指针的本质区别:指针是一个变量,有自己对应的存储空间,而数组名仅仅是一个符号,不是变
量,因而没有自己对应的存储空间,“数组名永远不等于指针”。
int arrays[9]={1,2,3,4,5,6,7,8,9};
int* pArray;
pArray = &arrays;
(3)指向常量的指针
const T* p1; // 形式1
T const *p2; // 形式2
const int a = 5;
const int b = 6;
T1 = &a;
T1 = &b; // ok!
解释:指向常量的指针! 他们指的常量不能被修改,但指针地址可以指向其他地方!
注意:一个指向常量的指针可以指向一个非常量的对象!
const int* p1;
int b = 0;
p1 = &b; //ok!
注意:指向非常量的指针不可以指向常量的对象!
int* p1;
const int a = 5;
p1 = &a; //error!
(4)指针常量
T *const p3;
int a = 5;
int b = 6;
p3 = &a;
*p3 = 6; // ok!
p3 = &b; // error!
解释:一个指针的常量,指向非常量的值;这种指针不能修改指针地址,但他所指的值可以修改
(5)指向常量的指针常量
const T* const p4;
int a = 5;
int b = 6;
p4 = &a;
*p4 = 7; // error!
p4 = &b; // error!
解释:既不能修改指针地址,也不能修改指针所指的值
(6)指向类成员函数的指针并非是指针
为了访问某成员函数,需要将对象的地址用作this指针的值,进行函数调用,他们通常实现一个小型结构,其中包
含了虚函数表,一个偏移量,以及其他信息!
(7)指向类成员的指针并非是指针
其实指向数据成员的指针是一个偏移量,为了访问某类的成员,需要将对象的地址加上这个偏移量相加.
(8)函数指针
void (*pf)(int); //其中pf是一个指向返回类型为void,形参为一个int的函数的指针!
void print(int b){
cout<<b<<endl;
}
pf = print;
int bb = 1234;
pf(bb);
(9)多级指针
同(1)二级指针
//使用二级指针引用字符串。
#include <iostream>
#define SIZE 5
main()
{
char *pc[]={" Beijing", "Shanghai", "Tianjing", "Guangzhou", "Chongqing" };
char **p;
int i;
for ( i=0; i<SIZE;i++)
{
p = pc+i;
printf ("%s\n", *p);
}
}
//用多级指针还可以引用整型二维数组
int a[3][3]={1, 2, 3, 4, 5, 6, 7, 8, 9};
int *b[]={a[0],a[1],a[2]};
int **p=b;
main( )
{
int i,j;
for ( i=0; i<3; i++ )
for ( j=0; j<3; j++ )
printf("%d,%d,%d\n", *(b[i]+j), *(*(p+i)+j), *(*(a+i)+j) );
}
运行结果:
1,1,1
2,2,2
3,3,3
4,4,4
5,5,5
6,6,6
7,7,7
8,8,8
9,9,9
注意: 一个指向“指向非常量的指针”的指针不能转换为一个指向“指向常量的指针”的指针。
在类派生中,一个指向基类指针的指针并不是一个指向子类指针的指针。
(10)其他复杂类型
int *f1() //一个返回int*的函数
int (*f1)() //一个函数指针
int (**ap2)[N]; //一个指针,它指向一个指针,后者则指向一个具有N个int元素的数组
int *(*ap2)[N]; //一个指针,指向一个具有N个int*元素的数组
int (**const fp2)()=0; //一个常量指针,指向一个指向函数的指针!
int *(*ap2)() //一个指针,指向一个返回为int*的函数!
int (*ap[N])() //一个函数指针的数组
遇到这类复杂声明,请用typedef!
指针运算
1.两个同类型的指针可以进行减法运算,其结果是参与运算的两个指针之间的元素个数.如果第一个指针大于第
二个指针,结果为正,否则为负,空指针或者指向同一个对象的指针,结果都为0.
2.指针的递增或递减运算,等于是请求它指向的对象大小比例的下一个元素或上一个元素.
引用(reference)
一、引用的概念
引用引入了对象的一个同义词。定义引用的表示方法与定义指针相似,只是用&代替了*。例如:
Point pt1(10,10);
Point &pt2=pt1;
义了pt2为pt1的引用。通过这样的定义,pt1和pt2表示同一对象。需要特别强调的是引用并不产生对象的
副本,仅仅是对象的同义词。因此,当下面的语句执行后:
pt1.offset(2,2);
pt1和pt2都具有(12,12)的值。引用必须在定义时马上被初始化,因为它必须是某个东西的同义词。你不 能先定义一个引用后才初始化它。例如下面语句是非法的:
Point &pt3;
pt3=pt1;
那么既然引用只是某个东西的同义词,它有什么用途呢?下面讨论引用的两个主要用途:作为函数参数以及从 函数中返回左值。
二、引用参数
1、传递可变参数
传统的c中,函数在调用时参数是通过值来传递的,这就是说函数的参数不具备返回值的能力。所以在传统
的c中,如果需要函数的参数具有返回值的能力,往往是通过指针来实现的。比如,实现两整数变量值交换
的c程序如下:
void swapint(int *a,int *b)
{
int temp;
temp=*a;
a=*b;
*b=temp;
}
使用引用机制后,以上程序的c++版本为:
void swapint(int &a,int &b)
{
int temp;
temp=a;
a=b;
b=temp;
}
调用该函数的c++方法为:swapint(x,y); c++自动把x,y的地址作为参数传递给swapint函数。
2、给函数传递大型对象
当大型对象被传递给函数时,使用引用参数可使参数传递效率得到提高,因为引用并不产生对象的副本,
也就是参数传递时,对象无须复制。下面的例子定义了一个有限整数集合的类:
const maxCard=100;
Class Set
{
int elems[maxCard]; // 集和中的元素,maxCard 表示集合中元素个数的最大值。
int card; // 集合中元素的个数。
public:
Set () {card=0;} //构造函数
friend Set operator * (Set ,Set ) ; //重载运算符号*,用于计算集合的交集 用对象作为传值参数
// friend Set operator * (Set & ,Set & ) 重载运算符号*,用于计算集合的交集 用对象的引用作为传值参数
...
}
先考虑集合交集的实现
Set operator *( Set Set1,Set Set2)
{
Set res;
for(int i=0;i<Set1.card;++i)
for(int j=0;j>Set2.card;++j)
if(Set1.elems[i]==Set2.elems[j])
{
res.elems[res.card++]=Set1.elems[i];
break;
}
return res;
}
由于重载运算符不能对指针单独操作,我们必须把运算数声明为 Set 类型而不是 Set * 。每次使用*做交集运
算时,整个集合都被复制,这样效率很低。我们可以用引用来避免这种情况。
Set operator *( Set &Set1,Set &Set2)
{
Set res;
for(int i=0;i<Set1.card;++i)
for(int j=0;j>Set2.card;++j)
if(Set1.elems[i]==Set2.elems[j])
{
res.elems[res.card++]=Set1.elems[i];
break;
}
return res;
}
三、引用返回值
如果一个函数返回了引用,那么该函数的调用也可以被赋值。这里有一函数,它拥有两个引用参数并返回
一个双精度数的引用:
double &max(double &d1,double &d2)
{
return d1>d2?d1:d2;
}
由于max()函数返回一个对双精度数的引用,那么我们就可以用max() 来对其中较大的双精度数加1:
max(x,y)+=1.0
C++引用与指针的比较
引用是C++中的概念,初学者容易把引用和指针混淆一起。 以下程序中,n是m的一个引用(reference),
m是被引用物(referent)。
int m;
int &n = m;
n相当于m的别名(绰号),对n的任何操作就是对m的操作。 所以n既不是m的拷贝,也不是指向m的指针,
其实n就是m它自己。
引用的规则:
(1)引用被创建的同时必须被初始化(指针则可以在任何时候被初始化)。
(2)不能有NULL引用,引用必须与合法的存储单元关联(指针则可以是NULL)。
(3)一旦引用被初始化,就不能改变引用的关系(指针则可以随时改变所指的对象)。
以下示例程序中,k被初始化为i的引用。 语句k = j并不能将k修改成为j的引用,只是把k的值改变成为6。
由于k是i的引用,所以i的值也变成了6。
int i = 5;
int j = 6;
int &k = i;
k = j; // k和i的值都变成了6;
引用的主要功能是传递函数的参数和返回值。 C++语言中,函数的参数和返回值的传递方式有三种:
值传递、指针传递和引用传递
(1)值传递
由于Func1函数体内的x是外部变量n的一份拷贝,改变x的值不会影响n, 所以n的值仍然是0。
void Func1(int x)
{
x = x + 10;
}
...
int n = 0;
Func1(n);
cout << "n = " << n << endl; // n = 0
(2)指针传递
由于Func2函数体内的x是指向外部变量n的指针,改变该指针的内容将导致n的值改变,所以n的值成为10。
void Func2(int *x)
{
(* x) = (* x) + 10;
}
...
int n = 0;
Func2(&n);
cout << "n = " << n << endl; // n = 10
(3)引用传递
由于Func3函数体内的x是外部变量n的引用,x和n是同一个东西,改变x等于改变n,所以n的值成为10。
void Func3(int &x)
{
x = x + 10;
}
...
int n = 0;
Func3(n);
cout << "n = " << n << endl; // n = 10
对比上述三个示例程序,会发现"引用传递"的性质象"指针传递",而书写方式象"值传递"。 实际上"引用"
可以做的任何事情"指针"也都能够做,为什么还要"引用"这东西? 答案是"用适当的工具做恰如其分的工作"。
指针能够毫无约束地操作内存中的任何东西,尽管指针功能强大,但是非常危险。 如果的确只需要借用一下
某个对象的"别名",那么就用"引用",而不要用"指针",以免发生意外。