1.7 指针和引用

指针和引用是C++语言中非常重要的概念。指针的使用比较复杂,但一旦正确熟练地掌握后,便能使程序变得简洁高效。因此,在学习时要注意领会其特点和本质。

1.7.1 指针和指针变量

如果在程序中定义了一个变量,在编译时系统就会给这个变量分配内存单元,并根据程序中定义的变量类型,分配一定长度的内存空间,每个内存单元中存放着变量的值。为了便于内存单元的存取,系统为每一个内存单元分配一个地址。在变量的生存期内,不管以后对该变量如何赋值,其内存地址总是固定不变的。

反映内存单元的位置(地址),称为单元的指针。为了能直接访问这些内存单元,C++引入了“指针变量”这个概念。指针变量(pointer)就是存放内存地址的变量。例如:

int  i=5;
int  *p=&i;

其中,i是一个初值为5的整型变量,p是一个整型指针变量,&i是取变量i在内存中的地址。于是p的数值就等于变量i在内存中的地址值,当指针变量的值是变量存储在内存中的地址时,称指针变量“指向”这个变量。因此p是一个指向变量i的指针。

指针变量和所有变量一样,遵循先定义后使用的规则。在C++中定义一个指针变量可按下列格式:

<数据类型> *<指针变量名1>[,*<指针变量名2>,…];

式中的“*”是一个定义指针变量的说明符,它不是指定变量名的一部分。每个指针变量前面都需要这样的“*”来标明。例如:

int  *pInt1,*pInt2;          //pInt1,pInt2是指向整型变量的指针
float     *pFloat;           //pFloat是一个指向实型变量的指针
char *pChar;                 //pChar是一个指向字符型变量的指针,它通常用来处理字符串

在定义一个指针后,系统也会给指针分配一个内存单元,但分配的空间大小都是相同的,因为指针变量的数值是某个变量的地址,而地址值的长度是一样的。需要说明的是,绝大多数情况下,都可以将指针变量简称为“指针”。

1.7.2 &和*运算符

C++中有两个专门用于指针的运算符:&(取地址运算符)F和*(取值运算符)

运算符“&”只能对变量操作,作用是取该变量的地址。运算符“*”的作用是取指针或地址所指内存单元中存储的内容。例如:

int  a=3;                // 整型变量,初值为3
int  *p=&a;              // 指向整型变量的指针,其值等于a的地址
int  b=*p;               // 将指针所指的地址中的内容赋值给b,值为3

上述赋值是在指针变量定义时进行的。当然,也可以在程序中进行赋值。例如:

int  a=3;                  // 整型变量,初值为3
int  *pi;                  // 指向整型变量的指针
pi=p;                      // 将指针p的地址赋给指针pi,使得它们都是指向a的指针
                           // 它等价于pi=&a; 注意在pi前没有*

另外,还需要说明的是:

(1)在使用指针变量前,一定要进行初始化或有确定的地址数值。例如下面的操作会产生致命的错误:

int  *pInt,  a=10;
*pInt = 10;

*pInt = a;

(2)指针变量只能赋一个指针的值,若给指针变量赋了一个变量的值而不是该变量的地址,或者赋了一个常量的值,则系统会以这个值作为地址。根据这个“地址”读写的结果将是致命的。

(3)给两个指针变量进行赋值,必须使这两个指针变量类型相同。否则,结果将是不可预测的。例如:

int *pi;
float  f=1.22,  *pFloat=&f;
pi=pFloat;                    // 尽管本身的赋值没有错误,但结果是不可预测的
                              // 因为(*pi)的值不会等于1.22,也不会等于1

(4)给指针变量赋值实际上是“间接”地给指针所指向的变量赋值。例如:

int a=11,  *p=&a;
(*p)++;                       // 结果a的值为12

下面看一个示例。

例Ex_CompUsePointer】 输入a和b两个整数,按大小顺序输出

#include <iostream.h>
int  main()
{
    int *p1, *p2, *p, a, b;
    cout<<"输入两个整数:";
    cin>>a>>b;
    p1=&a;      p2=&b;
    if  (a<b)
    {
        p=p1;   p1=p2;  p2=p;
    }
    cout<<"a = "<<a<<" , b = "<<b<<endl;
    cout<<"最大的值是:"<<*p1<<", 最小的值是:"<<*p2<<"\n";
    return 0;
}

程序中,当a≥b时,指针变量p1指向a p,2指向b;而当a<b时,通过指针交换p,1指向b,p2指向a。因此,上述程序代码总是使p1指向数值最大的变量,使p2指向数值最小的变量。

程序运行结果如下:

输入两个整数:1128↵

a = 11 , b = 28

最大的值是:28, 最小的值是:11

1.7.3 指针运算

除了前面的赋值运算外,指针还有算术运算和关系运算。

1.指针的算术运算

在实际应用中,指针的算术运算主要是对指针加上或减去一个整数。指针的这种运算的意义和通常情况下数值的加减运算的意义是不一样的。例如,若有:

int *ptr;

指针变量ptr加上整数n后,即ptr = ptr + n。编译器会把指针ptr的值加上sizeof(int)*n,在32位机器中,sizeof(int)等于4。由于地址是以字节为单位的,故ptr所指向的存储单元向高地址方向移动了sizeof(int)*n字节。这里的int是指针变量ptr的数据类型,若定义成float型,则ptr =ptr + n是使ptr向高地址方向移动了sizeof(float)*n字节。因此,

<指针变量> = <指针变量> + n

它是使指针变量所指向的存储单元向高地址方向移动了sizeof(指针变量类型)*n字节。类似的:

<指针变量> = <指针变量> - n

它是使指针变量所指向的存储单元向低地址方向移动了sizeof(指针变量类型)*n字节。

当n为1时,指针变量的上述加减运算就是指针变量的自增(++)、自减(--)运算。

2.指针的关系运算

两个指针变量的关系运算要根据两个指针变量值的大小来进行比较。在实际应用中,通常是比较两个指针反映地址的前后关系或判断指针变量的值是否为0。

例Ex_PointerOp】 将字符数组a中的n个字符按相反顺序存放

#include <iostream.h>
int  main()
{
     char a[]="Chinese";
     char*p1=a,*p2=a,temp;
     while(*p2!='\0')p2++;
     p2--;                                  // 将p2指向a的最后一个元素
     while(p1<p2)
     {
        temp=*p1;   *p1=*p2;   *p2=temp;    // 交换内容
        p1++;
        p2--;
     }
     cout<<a<<endl;                         // 输出结果
     return 0;
}

程序中,先将指针p1和p2分别指向同一个字符数组a,然后将p2指向字符数组a的最后一个元素,p1从数组a的首地址向后移动,p2从数组a的末地址向前移动,当p1的地址在p2之前时,交换地址的内容即交换字符数组a的元素内容,从而实现数组a中的字符按相反顺序存放。结果如下:

esenihC

1.7.4 指针和数组

数组中所有元素都是依次存储在内存单元中的,每个元素都有相应的地址。C++又规定数组名代表数组中下标为0的元素的地址,即数组的首地址。注意:数组名表示的首地址是一个地址(指针)常量。例如,当有下列数组定义时:

int  a[5];

则a所表示的地址就是元素a[0]的地址,a是一个地址(指针)常量,a++是不合法的。需要说明的是,下标运算符[]具有下列含义:

a[i] = *(a+i)

这是因为a是一个地址(指针),a+i表示a[i]的地址值,它等价于&a[i],因而a[i]=*(a+i)。

在指针操作中,若定义了下列指针:

int  *pi;

pi=a;                            //  等价于pi=&a[0];

通过指针能引用数组元素。例如:

*(pi+1) = 1;

a[1] = 1;

是等价的。由于指针变量和数组的数组名在本质上是一样,都反映地址值,因此,指向数组的指针变量实际上也可像数组变量那样使用下标,而数组变量又可像指针变量那样使用指针。例如, pi[i]与*(pi+i)及a[i]是等价的,*(a+i)与*(pi+i) 是等价的。

例Ex_SumUsePointer】 用指针运算来计算数组元素的和

#include <iostream.h>
int  main()
{
     int  a[6]={1,2,3,4,5,6};
     int*p=a;                          // 用数组名a将指针初始化
     int sum=0;
     for(int i=0;i<6;i++)
     {
          sum+=*p;
          p++;
    }
    cout<<sum<<endl;                   // 输出结果
     return 0;
}

运行结果为21。用指针运算时,要注意分析。

例Ex_ArrayAndPointer】 分析下列程序的输出结果

#include <iostream.h>
int  main()
{
    int  a[]={5,8,7,6,2,7,3};
    int  y,*p=&a[1];
    y = (*--p)++;
    cout<<y<<endl;
    return 0;
}

程序中,最难理解的语句是“y = (*--p)++;”,由于取值运算符“*”和前缀自减运算符“--”有相同的优先级,但它们的结合方向是从右至左,因此先运算--p,也就是a[0]的地址,(*--p)是元素a[0]的值,为5;再运算“y = (*--p)++;”,它相当于“y = (*--p); (*--p) = (*--p)+1;”,故最终结果为5。

上述一维数组的指针比较容易理解,但对于多维数组的指针则要复杂许多。为了叙述方便,下面以二维数组的指针为例来进一步阐述(对于三维、四维数组等也可同样分析)。

设有二维数组a,它有2×3个元素,如下面的定义:

int  a[2][3]={{1,2,5},{7,9,11}};

可以理解成:a是数组名,a数组包含两个元素a[0]和a[1],而每个元素又是一个一维数组,例如a[0]有a[0][0]、a[0][1]和a[0][2]三个元素,它可以用一个指针来表示,例如:

int *p1, *p2;
p1 = a[0];
p2 = a[1];

而数组名a代表整个二维数组的首地址,又可理解成是指向一维数组的指针的一维数组,也就是说,a可以用指向指针的指针来表示:

int**p;                           // 该指针又称为二级指针
p = a;

其中,p[0]或*p等价于p1或a[0],p[1]或*(p+1)等价于p2或a[1]。

例Ex_MultiArrayAndPointer】 分析下列程序的输出结果

#include <iostream.h>
int  main()
{
    int  a[3][3]={1,2,3,4,5,6,7,8,9};
    int  y=0;
    for (int i=0; i<3; i++)
        for (int j=0; j<3; j++)
            y += (*(a+i))[j];
    cout<<y<<endl;
    return 0;
}

程序中,理解“y += (*(a+i))[j];”是本程序的关键。事实上,*(a+i)就是a[i],因而(*(a+i))[j]就是a[i][j]。这里的“y += (*(a+i))[j];”语句就是求数组a中各个元素之和,结果是45。

1.7.5 指针和函数

指针既可以作为函数的形参和实参,又可以作为返回值,应用非常广泛。

1.指针作为函数的参数

函数的参数可以是C++语言中任意的合法变量,自然,也可以是一个指针。如果函数的某个参数是指针,对这一个函数的调用就是按地址传递的函数调用,简称传址调用。由于函数形参指针和实参指针指向同一个地址,因此形参内容的改变必将影响实参。在实际应用中,函数可以通过指针类型的参数带回一个或多个值。

例Ex_SwapUsePointer】 指针作为函数参数的调用方式

#include <iostream.h>
void swap(int *x, int *y);
int  main()
{
    int  a=7,  b=11;
    swap(&a, &b);
    cout<<"a = "<<a<< ", b = "<<b<<"\n";
    return 0;
}
void swap(int *x, int *y)
{
    int temp;
    temp=*x;  *x=*y;  *y=temp;
    cout<<"x = "<<*x<<", y = "<<*y<<"\n";
}

程序运行结果如下:

x = 11, y = 7

a = 11, b = 7

传递指针的函数调用实现过程如下:

① 函数声明中指明指针参数,即示例中的“void swap(int *x, int *y);”。

② 函数调用的实参中指明变量的地址,即示例中的“swap(&a, &b);”。

③ 函数定义中对形参进行间接访问。对*x和*y的操作,实际上就是访问函数的实参变量a和b,通过局部变量temp的过渡,使变量a和b的值被修改。

2.返回指针的函数

函数可以返回一个指针,该指针指向一个已定义的任一类型的数据。定义返回指针的函数格式如下:

<函数类型> * <函数名>( <形式参数表> ){ <函数体> }

它与一般函数定义基本相同,只不过在函数名前面增加了一个“*”号,用来指明函数返回的是一个指针,该指针所指向的数据类型由函数类型决定。

例Ex_PointerReturn】 返回指针的函数:用来将一个字符串逆序输出

#include <iostream.h>
char* flip(char *str)
{
    char *p1, *p2, ch;
    p1 = p2 = str;
    while (*p2 != '\0') p2++;
    p2-- ;
    while (p1<p2)
    {
        ch=*p2;  *p2=*p1; *p1=ch; // 交换字符
        p1++;    p2--;
    }
    return str;
}
int  main()
{
    char str[] = "ABCDEFGH";
    cout<<flip(str)<<"\n";
    return 0;
}

程序运行结果如下:

HGFEDCBA

代码中,函数flip定义成返回一个指向字符串的指针的函数,该函数的目的是将字符串str逆序后返回。

3.指向函数的指针

同变量相似,每一个函数都有地址。指向函数地址的指针称为“函数指针”。函数指针指向内存空间中的某个函数,通过函数指针可以调用相应的函数。

函数指针的定义如下:

<函数类型>( * <指针名>)( <参数表> );

例如:

int (*func)(char a, char b);

就是定义的一个函数指针。int为函数的返回类型,*表示后面的func是一个指针变量名。该函数具有两个字符型参数a和b。

需要说明的是,由于()的优先级大于*,所以下面是返回指针的函数定义而不是函数指针定义:

int *func(char a, char b);

一旦定义了函数指针变量,就可以给它赋值。由于函数名表示该函数的入口地址,因此可以将函数名赋给指向函数的指针变量。但一般来说,赋给函数指针变量的函数的返回值类型与参数个数、顺序要和函数指针变量相同。例如:

int fn1(char a, char b);
int *fn2(char a, char b);
int fn3(int n);
int (*fp1)(char x, char y);
int (*fp2)(int x);
fp1=fn1;                  // 正确,fn1函数与指针fp1指向的函数一致
fp1=fn2;                  // 错误,fn2函数的返回值类型与指针fp1指向的函数不一致
fp2=fn3;                  // 正确,fn3函数与指针fp2指向的函数一致
fp2=fp1;                  // 错误,两个指针指向的函数不一致
fp2=fn3(5);               // 错误,函数赋给函数指针时,不能加括号

函数指针变量赋值后,就可以使用指针来调用函数了。调用函数的格式如下:

( * <指针名>)( <实数表> );

<指针名>( <实数表> );

例如:

(*fp2)(5); 或  fp2(5);

例Ex_FuncPointer1】 函数指针的使用

#include <iostream.h>
double add(double x, double y)
{    return(x+y);       }
double mul(double x, double y)
{    return(x*y);       }
int  main()
{
     double   (*func)(double,double);            // 定义一个函数指针变量
     double  a,b;
     char op;
     cout<<"输入两个实数及操作方式,'+'表示加,'*'表示乘:";
     cin>>a>>b>>op;
     if(op=='+')func=add;                        // 将函数名赋给指针
     else func=mul;
     cout<<a<<op<<b<<"="<<func(a,b)<<endl;       // 函数调用
     return 0;
}

程序运行结果如下:

函数指针变量可用做函数的参数。

例Ex_FuncPointer2】 函数指针变量用做函数的参数

#include <iostream.h>
double add(double x, double y)
{    return(x+y);       }
double mul(double x, double y)
{    return(x*y);       }
void op(double(*func)(double,double), double x, double y)
{
    cout<<"x = "<<x<<", y = "<<y<<", result = "<<func(x,y)<<"\n";
}
int  main()
{
    cout<<"使用加法函数: ";
    op(add, 3, 7);
    cout<<"使用乘法函数: ";
    op(mul, 3, 7);
    return 0;
}

程序运行结果如下:

使用加法函数: x = 3, y = 7, result = 10

使用乘法函数: x = 3, y = 7, result = 21

代码中,op函数的第一个参数为函数指针,该指针指向的函数有两个double参数并返回double类型值。定义的add和mul函数也有两个double参数并返回double类型值,因此它们可以作为实参赋给函数指针func。

与一般变量指针数组一样,函数指针也可构成指针数组。

例Ex_FuncPointerArray】 函数指针数组的使用

#include <iostream.h>
void add(double x, double y)
{    cout<<x<<"+"<<y<<"="<<x+y<<"\n";   }
void sub(double x, double y)
{    cout<<x<<"-"<<y<<"="<<x-y<<"\n";    }
void mul(double x, double y)
{    cout<<x<<"*"<<y<<"="<<x*y<<"\n";    }
void div(double x, double y)
{    cout<<x<<"/"<<y<<"="<<x/y<<"\n";    }
void(*func[4])(double,double)={add,sub,mul,div};  // 函数指针数组定义和初始化
int  main()
{
    double x = 3, y = 7;
    char op;
    do{
        cout <<"+------- 相加\n"
            <<"-------- 相减\n"
            <<"*------- 相乘\n"
            <<"/------- 相除\n"
            <<"0------- 退出\n";
        cin>>op;
        switch(op)
        {
            case '+': func[0](x, y); break;
            case '-': func[1](x, y); break;
            case '*': func[2](x, y); break;
            case '/': func[3](x, y); break;
            case '0': return;
        }
    }while(1);
    return 0;
}

1.7.6 new和delete

在C++中,使用运算符new和delete能有效、直接地进行动态内存的分配和释放。

运算符new返回指定类型的一个指针,如果分配失败(如没有足够的内存空间)则返回0。例如:

double *p;
p = new double;
*p=30.4;                     // 将值存放在开辟的单元中

系统自动根据double类型的空间大小开辟一个内存单元,并将地址放在指针p中。当然,也可在开辟内存单元时,对单元里的值进行初始化。例如上述代码可写成:

double *p;
p = new double(30.4);

运算符delete操作是释放new请求到的内存。例如:

delete p;

它的作用是将p指针的内存单元释放,指针变量p仍然有效,它可以重新指向另一个内存单元。

需要注意的是:

(1)new和delete必须配对使用。也就是说,用new为指针分配内存,当使用结束之后,一定要用delete来释放已分配的内存空间。

(2)运算符delete必须用于先前new分配的有效指针。如果使用了未定义的其他任何类型的指针,就会带来严重问题,如系统崩溃等。

(3)new可以为数组分配内存,但当释放时,也可告诉delete数组有多少个元素。例如:

int  *p;
p=new int[10];                       // 分配整型数组的内存,数组中有10个元素
if ( !p )
{
     cout<<”内存分配失败!”;
     exit(1);                       // 中断程序执行
}
for (int i=0; i<10; i++)
     p[i]=i;                        // 给数组赋值
//…
delete  [10]p;                      // 告诉delete数组有多少个元素,或delete[]p;

1.7.7 引用和引用传递

C++提供了一个与指针密切相关的特殊数据类型——引用。引用是一个变量的别名。定义引用类型变量,实质上是给一个已定义的变量起一个别名,系统不会为引用类型变量分配内存空间,只是使引用类型变量与其相关联的变量使用同一个内存空间。

1.引用定义和使用

定义引用类型变量的一般格式为:

<数据类型>  &<引用名>=<变量名>

<数据类型>  &<引用名>(<变量名>)

其中,变量名必须是一个已定义过的变量。例如:

int a = 3;
int &ra = a;

这样,ra就是一个引用,它是变量a的别名。所有对这个引用ra的操作,实质上就是对被引用对象a的操作。例如:

ra = ra +2;

实质上是a+2,a的结果为5。但是如果给引用赋一个新值,结果会怎样呢?

例Ex_Reference】 给引用重新赋值

#include <iostream.h>
int  main()
{
    int a;
    int &ra = a;
    a = 5;
    cout<<"a = "<<a<<"\n";
    cout<<"ra = "<<ra<<"\n";
    cout<<"a的地址是:"<<&a<<"\n";
    cout<<"ra的地址是:"<<&ra<<"\n";
    int b = 8;
    ra = b;
    cout<<"a = "<<a<<"\n";
    cout<<"b = "<<b<<"\n";
    cout<<"ra = "<<ra<<"\n";
    cout<<"a的地址是:"<<&a<<"\n";
    cout<<"b的地址是:"<<&b<<"\n";
    cout<<"ra的地址是:"<<&ra<<"\n";
    return 0;
}

程序运行结果如下:

a = 5

ra = 5

a的地址是:0x0012FF7C

ra的地址是:0x0012FF7C

a = 8

b = 8

ra = 8

a的地址是:0x0012FF7C

b的地址是:0x0012FF74

ra的地址是:0x0012FF7C

程序中,引用ra被重新赋值为变量b。但从运行结果可以看出,ra与a的地址仍然相同,只不过它们的值都等于b的值。

从这个例子可以看出,引用与指针的最大区别是:指针是一个变量,可以把它再赋值成指向别处的地址,而引用一旦初始化后,其地址不会再改变。

当然,在使用引用时,还需要注意的是:

(1)定义引用类型变量时,必须将其初始化。而且引用变量类型必须与为它初始化的变量类型相同。例如:

float fVal;
int&rfVal=fVal;                    // 错误:类型不同

(2)若所引用类型变量的初始化值是常数,则必须将该引用定义成const类型。例如:

const int&ref=2;                   //const类型的引用

(3)可以引用一个结构体,但不能引用一个数组,这是因为数组是某个数据类型元素的集合,数组名表示该元素集合空间的起始地址,它自己不是一个真正的数据类型。例如:

int a[10];
int&ra=a;                        // 错误:不能建立数组的引用

(4)引用本身不是一种数据类型,所以没有引用的引用,也没有引用的指针。例如:

int a;
int &ra = a;
int&rra=ra;                       // 正确,变量a的另一个引用
int&*p=&ra;                       // 错误:企图定义一个引用的指针

2.引用传递

前面已提到过,当指针作为函数的参数时,形参改变后,相应的实参也会改变。但是如果在函数中反复使用指针,容易产生错误且难以阅读和理解。如果以引用作为参数,则既可以实现指针所带来的功能,又简便自然。

使一个函数能使用引用传递的方式是在函数定义时将形参前加上引用运算符“&”。

例Ex_SwapUseReference】 引用作为函数参数的调用方式

#include <iostream.h>
void swap(int &x, int &y);
int  main()
{
    int  a(7),  b(11);
    swap(a, b);
    cout<<"a = "<<a<< ", b = "<<b<<"\n";
    return 0;
}
void swap(int &x, int &y)
{
    int temp;
    temp=x;  x=y;  y=temp;
    cout<<"x = "<<x<<", y = "<<y<<"\n";
}

程序运行结果如下:

x = 11, y = 7

a = 11, b = 7

函数swap中的&x和&y就是形参的引用说明。在执行swap(a, b);时,虽然看起来是简单的变量传递,但实际上传递的是实参a, b的地址,也就是说,形参的任何操作都会改变相应的实参的数值。引用除了可作为函数的参数外,还可作为函数的返回值。

例Ex_RefReturn】 返回引用的函数的使用

#include <iostream.h>
double area;
double &CalArea(double r)
{
    area = 3.141593 * r * r;
    return area;
}
int  main()
{
    double c = CalArea(5.0);
    double &d = CalArea(10.0);
    cout<<c<<"\n";
    cout<<d<<"\n";
    return 0;
}

程序运行结果如下:

78.5398

314.159

注意:

绝对不要返回不在作用域内的变量的引用,因为一旦变量退出作用域,对它的引用也没有意义了。