指针


6.1 指针的概念

请务必弄清楚一个内存单元的地址与内存单元的内容这两个概念的区别。在程序中一般是通过变量名来对内存单元进行存取操作的。其实程序经过编译以后已经将变量名转换为变量的地址,对变量值的存取都是通过地址进行的。这种按变量地址存取变量值的方式称为直接存取方式,或直接访问方式。

还可以采用另一种称为间接存取(间接访问)的方式。可以在程序中定义这样一种特殊的变量,它是专门用来存放地址的。

图6.2是直接访问和间接访问的示意图。为了将数值3送到变量中,可以有两种方法:

(1)   直接将数3送到整型变量i所标识的单元中。见图6.2(a)。

(2)   将3送到指针变量i_pointer所指向的单元(这就是变量i所标识的单元)中。见图6.2(b)。

所谓指向,就是通过地址来体现的。

由于通过地址能找到所需的变量单元,因此可以说,地址指向该变量单元。因此将地址形象化地称为“指针”。一个变量的地址称为该变量的指针。

如果有一个变量是专门用来存放另一变量地址(即指针)的,则它称为指针变量。指针变量的值(即指针变量中存放的值)是地址(即指针)。

6.2 变量与指针

指针变量是一种特殊的变量,它和以前学过的其他类型的变量的不同之处是: 用它来指向另一个变量。为了表示指针变量和它所指向的变量之间的联系,在C++中用“*”符号表示指向,例如,i_pointer是一个指针变量,而*i_pointer表示i_pointer所指向的变量,见图6.3。

下面两个语句作用相同:

①  i=3;

②  *i_pointer=3;

6.2.1 定义指针变量

C++规定所有变量在使用前必须先定义,即指定其类型。在编译时按变量类型分配存储空间。对指针变量必须将它定义为指针类型。先看一个具体例子:

int i,j;                           //定义整型变量i,j

int *pointer_1, *pointer_2;       //定义指针变量*pointer_1,*pointer_2

第2行开头的int是指: 所定义的指针变量是指向整型数据的指针变量。也就是说,指针变量pointer_1和pointer_2只能用来指向整型数据(例如i和j),而不能指向浮点型变量a和b。这个int就是指针变量的基类型。指针变量的基类型用来指定该指针变量可以指向的变量的类型。

定义指针变量的一般形式为

基类型 *指针变量名;

下面都是合法的定义:

float *pointer_3;               // pointer_3是指向单精度型数据的指针变量

char *pointer_4;                // pointer_4是指向字符型数据的指针变量

请注意: 指针变量名是pointer_3和pointer_4,而不是*pointer_3和*pointer_4,即“*”不是指针变量名的一部分,在定义变量时在变量名前加一个“*”表示该变量是指针变量。

那么,怎样使一个指针变量指向另一个变量呢?只需要把被指向的变量的地址赋给指针变量即可。例如:

pointer_1=&i;              //将变量i的地址存放到指针变量pointer_1中

pointer_2=&j;              //将变量j的地址存放到指针变量pointer_2中

这样,pointer_1就指向了变量i,pointer_2就指向了变量j。见图6.4。

一般的C++编译系统为每一个指针变量分配4个字节的存储单元,用来存放变量的地址。

在定义指针变量时要注意:

(1) 不能用一个整数给一个指针变量赋初值。

(2) 在定义指针变量时必须指定基类型。

6.2.2 引用指针变量

有两个与指针变量有关的运算符:

(1) &取地址运算符。

(2)  *指针运算符(或称间接访问运算符)。

例如: &a为变量a的地址,*p为指针变量p所指向的存储单元。

例6.1 通过指针变量访问整型变量。

#include <iostream>

using namespace std;

int main( )

{int a,b;                                  //定义整型变量a,b

int *pointer_1,*pointer_2;                //定义指针变量*pointer_1,*pointer_2

a=100;b=10;                               //对a,b赋值

pointer_1=&a;                             //把变量a的地址赋给pointer_1

pointer_2=&b;                             //把变量a的地址赋给pointer_2

cout<<a<<″ ″<<b<<endl;                    //输出a和b的值

cout<<*pointer_1<<″ ″<<*pointer_2<<endl;  //输出*pointer_1和*pointer_2的值

return 0;

}

运行结果为

100 10                                     (a和b的值)

100 10                                     (*pointer_1和*pointer_2的值)

请对照图6.5分析。

下面对“&”和“*”运算符再做些说明:

(1) 如果已执行了“pointer_1=&a;”语句,请问&*pointer_1的含义是什么?“&”和“*”两个运算符的优先级别相同,但按自右至左方向结合,因此先进行*pointer_1的运算,它就是变量a,再执行&运算。因此,&*pointer_1与&a相同,即变量a的地址。

如果有pointer_2=&*pointer_1;它的作用是将&a(a的地址)赋给pointer_2,如果pointer_2原来指向b,经过重新赋值后它已不再指向b了,而也指向了a,见图6.6。图6.6(a)是原来的情况,图6.6(b)是执行上述赋值语句后的情况。

(3)   *&a的含义是什么?先进行&a的运算,得a的地址,再进行*运算,即&a所指向的变量,*&a和*pointer_1的作用是一样的(假设已执行了“pointer_1=&a;”),它们等价于变量a。即*&a与a等价,见图6.7。

例6.2 输入a和b两个整数,按先大后小的顺序输出a和b(用指针变量处理)。

解此题的思路是: 设两个指针变量p1和p2,使它们分别指向a和b。使p1指向a和b中的大者,p2指向小者,顺序输出*p1,*p2就实现了按先大后小的顺序输出a和b。按此思路编写程序如下:

#include <iostream>

using namespace std;

int main( )

{

int *p1,*p2,*p,a,b;

cin>>a>>b;                             //输入两个整数

p1=&a;                                 //使p1指向a

p2=&b;                                 //使p2指向b

if(a<b)                                //如果a<b就使p1与p2的值交换

{p=p1;p1=p2;p2=p;}                    //将p1的指向与p2的指向交换

cout<<″a=″<<a<<″ b=″<<b<<endl;

cout<<″max=″<<*p1<<″ min=″<<*p2<<endl;

return 0;

}

运行情况如下:

4578↙

a=45 b=78

max=78 min=45

输入a的值45,b的值78,由于a<b,将p1的值和p2的值交换,即将p1的指向与p2的指向交换。交换前的情况见图6.8(a),交换后的情况见图6.8(b)。

请注意,这个问题的算法是不交换整型变量的值,而是交换两个指针变量的值。

6.2.3 指针作为函数参数

函数的参数不仅可以是整型、浮点型、字符型等数据,还可以是指针类型。它的作用是将一个变量的地址传送给被调用函数的形参。

例6.3 题目同例6.2,即对输入的两个整数按大小顺序输出。

这里用函数处理,而且用指针类型的数据作函数参数。

程序如下:

#include  <iostream>

using namespace std;

int main( )

{ void swap(int *p1,int *p2);     //函数声明

int *pointer_1,*pointer_2,a,b;  //定义指针变量pointer_1,pointer_2,整型变量a,b

cin>>a>>b;

pointer_1=&a;                       //使pointer_1指向a

pointer_2=&b;                       //使pointer_2指向b

if(a<b) swap(pointer_1,pointer_2);  //如果a<b,使*pointer_1和*pointer_2互换

cout<<″max=″<<a<<″ min=″<<b<<endl;//a已是大数,b是小数

return 0;

}

void swap(int *p1,int *p2)            //函数的作用是将*p1的值与*p2的值交换

{ int temp;

temp=*p1;

*p1=*p2;

*p2=temp;

}

请注意交换*p1和*p2的值是如何实现的。如果写成以下这样就有问题了:

void swap(int *p1,int *p2)

{int *temp;

*temp=*p1;            //此语句有问题

*p1=*p2;

*p2=*temp;

本例采取的方法是交换a和b的值,而p1和p2的值不变。这恰和例6.2相反。

可以看到,在执行swap函数后,主函数中的变量a和b的值改变了。这个改变不是通过将形参值传回实参来实现的。请读者考虑一下能否通过调用下面的函数实现a和b互换。

void swap(int x,int y)

{int temp;

temp=x;

x=y;

y=temp;

}

在main函数中用“swap(a,b);”调用swap函数,会有什么结果呢?在函数调用时,a的值传送给x,b的值传送给y,如图6.10(a)所示。执行完swap函数最后一个语句后,x和y的值是互换了,但main函数中的a和b并未互换,如图6.10(b)所示。也就是说由于虚实结合是采取单向的“值传递”方式,只能从实参向形参传数据,形参值的改变无法回传给实参。

为了使在函数中改变了的变量值能被main函数所用,不能采取把要改变值的变量作为参数的办法,而应该用指针变量作为函数参数。在函数执行过程中使指针变量所指向的变量值发生变化,函数调用结束后,这些变量值的变化依然保留下来,这样就实现了“通过调用函数使变量的值发生变化,在主调函数中使用这些改变了的值”的目的。

如果想通过函数调用得到n个要改变的值,可以采取下面的步骤: ①在主调函数中设n个变量,用n个指针变量指向它们;②编写被调用函数,其形参为n个指针变量,这些形参指针变量应当与主调函数中的n个指针变量具有相同的基类型;③在主调函数中将n个指针变量作实参,将它们的值(是地址值)传给所调用函数的n个形参指针变量,这样,形参指针变量也指向这n个变量;④通过形参指针变量的指向,改变该n个变量的值;⑤在主调函数中就可以使用这些改变了值的变量。

请注意,不能企图通过改变形参指针变量的值而使实参指针变量的值改变。请分析下面程序:

#include  <iostream>

using namespace std;

int main( )

{ void swap(int *p1,int *p2);

int *pointer_1,*pointer_2,a,b;

cin>>a>>b;

pointer_1=&a;

pointer_2=&b;

if(a<b) swap(pointer_1,pointer_2);

cout<<″max=″<<a<<″ min=″<<b<<endl;

return 0;

}

void swap(int *p1,int *p2)

{ int *temp;

temp=p1;

p1=p2;

p2=temp;

}

实参变量和形参变量之间的数据传递是单向的“值传递”方式。指针变量作函数参数也要遵循这一规则。调用函数时不会改变实参指针变量的值,但可以改变实参指针变量所指向变量的值。

函数的调用可以(而且只可以)得到一个返回值(即函数值),而使用指针变量作函数参数,就可以通过指针变量改变主调函数中变量的值,相当于通过函数调用从被调用的函数中得到多个值。如果不用指针变量是难以做到这一点的。

例6.4 输入a,b,c 3个整数,按由大到小的顺序输出。

用上面介绍的方法,用3个指针变量指向3个整型变量,然后用swap函数来实现互换3个整型变量的值。

程序如下:

#include <iostream>

using namespace std;

int main( )

{ void exchange(int *,int *,int *);    //对exchange函数的声明

int a,b,c,*p1,*p2,*p3;

cin>>a>>b>>c;                        //输入3个整数

p1=&a;p2=&b;p3=&c;                   //指向3个整型变量

exchange(p1,p2,p3);                  //交换p1,p2,p3指向的3个整型变量的值

cout<<a<<″ ″<<b<<″ ″<<c<<endl;       //按由大到小的顺序输出3个整数

}

void exchange(int *q1,int *q2,int *q3)

{void swap(int *,int *);              //对swap函数的声明

if(*q1<*q2) swap(q1,q2);             //调用swap,将q1与q2所指向的变量的值互换

if(*q1<*q3) swap(q1,q3);             //调用swap,将q1与q3所指向的变量的值互换

if(*q2<*q3) swap(q2,q3);             //调用swap,将q2与q3所指向的变量的值互换

}

void swap(int *pt1,int *pt2)           //将pt1与pt2所指向的变量的值互换

{int temp;

temp=*pt1;

*pt1=*pt2;

*pt2=temp;

}

运行情况如下:

12 -56 87↙

87 12 -56

6.3 数组与指针

6.3.1 指向数组元素的指针

一个变量有地址,一个数组包含若干元素,每个数组元素都在内存中占用存储单元,它们都有相应的地址。指针变量既然可以指向变量,当然也可以指向数组元素(把某一元素的地址放到一个指针变量中)。所谓数组元素的指针就是数组元素的地址。

int a[10];          //定义一个整型数组a,它有10个元素

int *p;             //定义一个基类型为整型的指针变量p

p=&a[0];            //将元素a[0]的地址赋给指针变量p,使p指向a[0]

在C++中,数组名代表数组中第一个元素(即序号为0的元素)的地址。因此,下面两个语句等价:

p=&a[0];

p=a;

在定义指针变量时可以给它赋初值:

int *p=&a[0];            //p的初值为a[0]的地址

也可以写成

int *p=a;                //作用与前一行相同

可以通过指针引用数组元素。假设p已定义为一个基类型为整型的指针变量,并已将一个整型数组元素的地址赋给了它,使它指向某一个数组元素。如果有以下赋值语句:

*p=1;                //对p当前所指向的数组元素赋予数值1

如果指针变量p已指向数组中的一个元素,则p+1指向同一数组中的下一个元素。

(2) *(p+i)或*(a+i)是p+i或a+i所指向的数组元素,即a[i]。

可以看出,[]实际上是变址运算符。对a[i]的求解过程是: 先按a+i×d计算数组元素的地址,然后找出此地址所指向的单元中的值。

(3) 指向数组元素的指针变量也可以带下标,如p[i]与*(p+i)等价。

根据以上叙述,引用一个数组元素,可用以下方法:

(1) 下标法,如a[i]形式;

(2) 指针法,如*(a+i)或*(p+i)。其中a是数组名,p是指向数组元素的指针变量。如果已使p的值为a,则*(p+i)就是a[i]。可以通过指向数组元素的指针找到所需的元素。使用指针法能使目标程序质量高。

例6.5 输出数组中的全部元素。

假设有一个整型数组a,有10个元素。要输出各元素的值有3种方法:

(1) 下标法

#include <iostream>

using namespace std;

int main( )

{ int a[10];

int i;

for(i=0;i<10;i++)

cin>>a[i];                   //引用数组元素a[i]

cout<<endl;

for(i=0;i<10;i++)

cout<<a[i]<<″ ″;             //引用数组元素a[i]

cout<<endl;

return 0;

}

运行情况如下:

9 8 7 6 5 4 3 2 1 0↙            (输入10个元素的值)

9 8 7 6 5 4 3 2 1 0              (输出10个元素的值)

(2) 指针法

将上面程序第7行和第10行的“a[i]”改为“*(a+i)”,运行情况与(1)相同。

(3) 用指针变量指向数组元素

#include <iostream>

using namespace std;

int main( )

{ int a[10];

int i,*p=a;           //指针变量p指向数组a的首元素a[0]

for(i=0;i<10;i++)

cin>>*(p+i);        //输入a[0]~a[9]共10个元素

cout<<endl;

for(p=a;p<(a+10);p++)

cout<<*p<<″ ″;      //p先后指向a[0]~a[9]

cout<<endl;

return 0;

}

运行情况与前相同。请仔细分析p值的变化和*p的值。

对3种方法的比较:

方法(1)和(2)的执行效率是相同的。第(3)种方法比方法(1)、(2)快。这种方法能提高执行效率。

用下标法比较直观,能直接知道是第几个元素。用地址法或指针变量的方法都不太直观,难以很快地判断出当前处理的是哪一个元素。

在用指针变量指向数组元素时要注意: 指针变量p可以指向有效的数组元素,实际上也可以指向数组以后的内存单元。如果有

int a[10],*p=a;                 //指针变量p的初值为&a[0]

cout<<*(p+10);                  //要输出a[10]的值

在使用指针变量指向数组元素时,应切实保证指向数组中有效的元素。

指向数组元素的指针的运算比较灵活,务必小心谨慎。下面举几个例子:

如果先使p指向数组a的首元素(即p=a),则:

(1)   p++(或p+=1)。使p指向下一元素,即a[1]。如果用*p,得到下一个元素a[1]的值。

(2) *p++。由于++和*同优先级,结合方向为自右而左,因此它等价于*(p++)。作用是: 先得到p指向的变量的值(即*p),然后再使p的值加1。例6.5(3)程序中最后一个for语句:

for(p=a;p<a+10;p++)

cout<<*p;

可以改写为

for(p=a;p<a+10;)

cout<<*p++;

(2)   *(p++)与*(++p)作用不同。前者是先取*p值,然后使p加1。后者是先使p加1,再取*p。若p的初值为a(即&a[0]),输出*(p++)得到a[0]的值,而输出*(++p)则得到a[1]的值。

(4) (*p)++表示p所指向的元素值加1,即(a[0])++,如果a[0]=3,则(a[0])++的值为4。注意: 是元素值加1,而不是指针值加1。

(5) 如果p当前指向a[i],则

*(p--)    先对p进行“*”运算,得到a[i],再使p减1,p指向a[i-1]。

*(++p)   先使p自加1,再作*运算,得到a[i+1]。

*(--p)   先使p自减1,再作*运算,得到a[i-1]。

将++和--运算符用于指向数组元素的指针变量十分有效,可以使指针变量自动向前或向后移动,指向下一个或上一个数组元素。例如,想输出a数组100个元素,可以用以下语句:

p=a;                               p=a;

while(p<a+100)       或        while(p<a+100)

cout<<*p++;                      {cout<<*p;

p++;}

在用*p++形式的运算时,很容易弄错,一定要十分小心,弄清楚先取p值还是先使p加1。

6.3.2 用指针变量作函数参数接收数组地址

在第5章5.4节中介绍过可以用数组名作函数的参数。前面已经多次强调: 数组名代表数组首元素的地址。用数组名作函数的参数,传递的是数组首元素的地址。很容易推想: 用指针变量作函数形参,同样可以接收从实参传递来的数组首元素的地址(此时,实参是数组名)。下面将第5章5.4节中的例5.7程序改写,用指针变量作函数形参。

例6.6 将10个整数按由小到大的顺序排列。

在例5.7程序的基础上,将形参改为指针变量。

#include <iostream>

using namespace std;

int main( )

{void select_sort(int *p,int n);                  //函数声明

int a[10],i;

cout<<″enter the originl array:″<<endl;

for(i=0;i<10;i++)                                //输入10个数

cin>>a[i];

cout<<endl;

select_sort(a,10);                              //函数调用,数组名作实参

cout<<″the sorted array:″<<endl;

for(i=0;i<10;i++)                               //输出10个已排好序的数

cout<<a[i]<<″  ″;

cout<<endl;

return 0;

}void select_sort(int *p,int n)                   //用指针变量作形参

{int i,j,k,t;

for(i=0;i<n-1;i++)

{k=i;

for(j=i+1;j<n;j++)

if(*(p+j)<*(p+k)) k=j;                 //用指针法访问数组元素

t=*(p+k);*(p+k)=*(p+i);*(p+i)=t;

}

}

实际上在函数调用时并不存在一个占有存储空间的形参数组,只有指针变量。

实参与形参的结合,有以下4种形式:

实  参          形  参

数组名                    数组名   (如例5.7)

数组名                 指针变量    (如例6.6)

指针变量         数组名

指针变量          指针变量

在此基础上,还要说明一个问题: 实参数组名a代表一个固定的地址,或者说是指针型常量,因此要改变a的值是不可能的。如

a++;              //语法错误,a是常量,不能改变

而形参数组名是指针变量,并不是一个固定的地址值。它的值是可以改变的。在函数调用开始时,它接收了实参数组首元素的地址,但在函数执行期间,它可以再被赋值。如

f(array[],int n)

{ cout<<array;                //输出array[0]的值

array=array+3;              //指针变量array的值改变了,指向array[3]

cout<<*arr<<endl;           //输出array[3]的值

}

6.3.3 多维数组与指针

用指针变量可以指向一维数组中的元素,也可以指向多维数组中的元素。

1. 多维数组元素的地址

设有一个二维数组a,它有3行4列。它的定义为

int a[3][4]={{1,3,5,7},{9,11,13,15},{17,18,21,23}};

a是一个数组名。a数组包含3行,即3个元素:a[0],a[1],a[2]。而每一元素又是一个一维数组,它包含4图6.14个元素(即4个列元素),例如,a[0]所代表的一维数组又包含4个元素: a[0][0], a[0][1], a[0][2], a[0][3],见图6.14。可以认为二维数组是“数组的数组”,即数组a是由3个一维数组所组成的。

从二维数组的角度来看,a代表二维数组首元素的地址,现在的首元素不是一个整型变量,而是由4个整型元素所组成的一维数组,因此a代表的是首行的起始地址(即第0行的起始地址,&a[0]),a+1代表a[1]行的首地址,即&a[1]。

a[0],a[1],a[2]既然是一维数组名,而C++又规定了数组名代表数组首元素地址,因此a[0]代表一维数组a[0]中0列元素的地址,即&a[0][0]。a[1]的值是&a[1][0],a[2]的值是&a[2][0]。

0行1列元素的地址可以直接写为&a[0][1],也可以用指针法表示。a[0]为一维数组名,该一维数组中序号为1的元素显然可以用a[0]+1来表示,见图6.16。

欲得到a[0][1]的值,用地址法怎么表示呢?既然a[0]+1是a[0][1]元素的地址,那么,*(a[0]+1) 就是a[0][1]元素的值。而a[0]又是和*(a+0)无条件等价的,因此也可以用*(*(a+0)+1)表示a[0][1]元素的值。依此类推,*(a[i]+j)或*(*(a+i)+j)是a[i][j]的值。

2. 指向多维数组元素的指针变量

(1) 指向数组元素的指针变量

例6.7 输出二维数组各元素的值。

这里采用的方法是用基类型为整型的指针变量先后指向各元素,逐个输出它们的值。

#include <iostream>

using namespace std;

int main( )

{ int a[3][4]={1,3,5,7,9,11,13,15,17,19,21,23};

int *p;                              //p是基类型为整型的指针变量

for(p=a[0];p<a[0]+12;p++)

cout<<*p<<″ ″;

cout<<endl;

return 0;

}

运行结果如下:

1 3 5 7 9 11 13 15 17 19 21 23

说明:

① p是指向整型数据的指针变量,在for语句中对p赋初值a[0],也可以写成“p=&a[0][0]”。

② 循环结束的条件是“p<a[0]+12”,只要满足p<a[0]+12,就继续执行循环体。

③ 执行“cout<<*p;”输出p当前所指的列元素的值,然后执行p++,使p指向下一个列元素。

(2) 指向由m个元素组成的一维数组的指针变量

可以定义一个指针变量,它不是指向一个整型元素,而是指向一个包含m个元素的一维数组。这时,如果指针变量p先指向a[0](即p=&a[0]),则p+1不是指向a[0][1],而是指向a[1],p的增值以一维数组的长度为单位,见图6.17。

例6.8 输出二维数组任一行任一列元素的值。

#include <iostream>

using namespace std;

int main( )

{ int a[3][4]={1,3,5,7,9,11,13,15,17,19,21,23};

int (*p)[4],i,j;

cin>>i>>j;

p=a;

cout<<*(*(p+i)+j)<<endl;

return 0;

}

运行情况如下:

2 3↙

23

由于执行了“p=a”,使p指向a[0]。因此p+2是二维数组a中序号为2的行的起始地址(由于p是指向一维数组的指针变量,因此p加1,就指向下一个一维数组),见图6.18。*(p+2)+3是a数组2行3列元素地址。*(*(p+2)+3)是a[2][3]的值。

3. 用指向数组的指针作函数参数

一维数组名可以作为函数参数传递,多维数组名也可作函数参数传递。

例6.9 输出二维数组各元素的值。

题目与例6.7相同,但本题用一个函数实现输出,用多维数组名作函数参数。

#include <iostream>

using namespace std;

int main( )

{ void output(int (*p)[4]);                       //函数声明

int a[3][4]={1,3,5,7,9,11,13,15,17,19,21,23};

output(a);                                  //多维数组名作函数参数

return 0;

}

void output(int (*p)[4])               //形参是指向一维数组的指针变量

{ int i,j;

for(i=0;i<3;i++)

for(j=0;j<4;j++)

cout<<*(*(p+i)+j)<<″ ″;

cout<<endl;

}

运行情况如下:

0      3 5 7 9 11 13 15 17 19 21 23

6.4 字符串与指针

在C++中可以用3种方法访问一个字符串(在第5章介绍了前两种方法)。

1. 用字符数组存放一个字符串

例6.10 定义一个字符数组并初始化,然后输出其中的字符串。

#include <iostream>

using namespace std;

int main( )

{ char str[]=″I love CHINA!″;

cout<<str<<endl;

return 0;

}

运行时输出:

I love CHINA!

2. 用字符串变量存放字符串

例6.11 定义一个字符串变量并初始化,然后输出其中的字符串。

#include <string>

#include <iostream>

using namespace std;

int main( )

{ string str=″I love CHINA!″;

cout<<str<<endl;

return 0;

}

3. 用字符指针指向一个字符串

例6.12 定义一个字符指针变量并初始化,然后输出它指向的字符串。

#include <iostream>

using namespace std;

int main( )

{ char *str=″I love CHINA!″;

cout<<str<<endl;

return 0;

}

对字符串中字符的存取,可以用下标方法,也可以用指针方法。

例6.13 将字符串str1复制为字符串str2。

定义两个字符数组str1和str2,再设两个指针变量p1和p2,分别指向两个字符数组中的有关字符,通过改变指针变量的值使它们指向字符串中的不同的字符,以实现字符的复制。

#include <iostream>

using namespace std;

int main( )

{ char str1[]=″I love CHINA!″,str2[20],*p1,*p2;

p1=str1;p2=str2;

for(;*p1!=′\\0′;p1++,p2++)

*p2=*p1;

*p2=′\\0′;

p1=str1;p2=str2;

cout<<″str1 is: ″<<p1<<endl;

cout<<″str2 is: ″<<p2<<endl;

return 0;

}

6.5 函数与指针

6.5.1 用函数指针变量调用函数

指针变量也可以指向一个函数。一个函数在编译时被分配给一个入口地址。这个函数入口地址就称为函数的指针。可以用一个指针变量指向函数,然后通过该指针变量调用此函数。

例6.14 求a和b中的大者。

先按一般方法写程序:

#include <iostream>

using namespace std;

int main( )

{int max(int x,int y);              //函数声明

int a,b,m;

cin>>a>>b;

m=max(a,b);                        //调用函数max,求出最大值,赋给m

cout<<″max=″<<m<<endl;

return 0;

}

int max(int x,int y)

{int z;

if(x>y) z=x;

else z=y;

return(z);

}

#include <iostream>

using namespace std;

int main( )

{int max(int x,int y);              //函数声明

int (*p)(int,int);                 //定义指向函数的指针变量p

int a,b,m;

p=max;                             //使p指向函数max

cin>>a>>b;

m=p(a,b);

cout<<″max=″<<m<<endl;

return 0;

}

请注意第7行的赋值语句“p=max;”。此语句千万不要漏写,它的作用是将函数max的入口地址赋给指针变量p。这时,p才指向函数max。见图6.20。

指向函数的指针变量的一般定义形式为

函数类型 (*指针变量名)(函数形参表);

6.5.2 用指向函数的指针作函数参数

在C语言中,函数指针变量常见的用途之一是作为函数的参数,将函数名传给其他函数的形参。这样就可以在调用一个函数的过程中根据给定的不同实参调用不同的函数。

例如,利用这种方法可以编写一个求定积分的通用函数,用它分别求5个函数的定积分:每次需要求定积分的函数是不一样的。可以编写一个求定积分的通用函数integral,它有3个形参: 下限a、上限b,以及指向函数的指针变量fun。函数原型可写为

double integral (double a,double b,double (*fun)(double));

分别编写5个函数f1,f2,f3,f4,f5, 用来求上面5个函数的值。然后先后调用integral函数5次,每次调用时把a,b以及f1,f2,f3,f4,f5之一作为实参,即把上限、下限以及有关函数的入口地址传送给形参fun。在执行integral函数过程中求出各函数定积分的值。

在面向对象的C++程序设计中,这种用法就比较少了。有兴趣的读者可参阅作者所著的《C程序设计(第二版)》一书中的有关章节。

6.6 返回指针值的函数

一个函数可以带回一个整型值、字符值、实型值等,也可以带回指针型的数据,即地址。其概念与以前类似,只是带回的值的类型是指针类型而已。返回指针值的函数简称为指针函数。

定义指针函数的一般形式为

类型名 *函数名(参数表列);

例如

int *a(int x,int y);

6.7 指针数组和指向指针的指针

6.7.1 指针数组的概念

如果一个数组,其元素均为指针类型数据,该数组称为指针数组,也就是说,指针数组中的每一个元素相当于一个指针变量,它的值都是地址。一维指针数组的定义形式为

类型名*数组名[数组长度];

例如

int *p[4];

可以用指针数组中各个元素分别指向若干个字符串,使字符串处理更加方便灵活。

例6.15 若干字符串按字母顺序(由小到大)输出。

#include <iostream>

using namespace std;

int main( )

{ void sort(char *name[],int n);                //声明函数

void print(char *name[],int n);               //声明函数

char *name[]={″BASIC″,″FORTRAN″,″C++″,″Pascal″,″COBOL″};  //定义指针数组

int n=5;

sort(name,n);

print(name,n);

return 0;

}

void sort(char *name[],int n)

{ char *temp;

int i,j,k;

for(i=0;i<n-1;i++)

{k=i;

for(j=i+1;j<n;j++)

if(strcmp(name[k],name[j])>0) k=j;

if(k!=i)

{ temp=name[i];name[i]=name[k];name[k]=temp;}

}

}

void print(char *name[],int n)

{ int i;

for(i=0;i<n;i++)

cout<<name[i]<<endl;

}

运行结果为

BASIC

COBOL

C++

FORTRAN

Pascal

print函数的作用是输出各字符串。name[0]~name[4]分别是各字符串的首地址。print函数也可改写为以下形式:

void print(char *name[],int n)

{ int i=0

char *p;

p=name[0];

while(i<n)

{p=*(name+i++);

cout<<p<<endl;

}

}

其中“*(name+i++)”表示先求*(name+i)的值,即name[i](它是一个地址)。将它赋给p,然后i加1。最后输出以p地址开始的字符串。

6.7.2 指向指针的指针

在掌握了指针数组的概念的基础上,下面介绍指向指针数据的指针,简称为指向指针的指针。从图6.22可以看到,name是一个指针数组,它的每一个元素是一个指针型数据(其值为地址),分别指向不同的字符串。数组名name代表该指针数组首元素的地址。name+i是name[i]的地址。由于name[i]的值是地址(即指针),因此name+i就是指向指针型数据的指针。还可以设置一个指针变量p,它指向指针数组的元素(见图6.23)。p就是指向指针型数据的指针变量。

怎样定义一个指向指针数据的指针变量呢?如下:

char *(*p);

从附录B可以知道,*运算符的结合性是从右到左,因此“char *(*p);”可写成

char **p;

例6.16 指向字符型数据的指针变量。

#include <iostream>

using namespace std;

int main( )

{ char **p;                //定义指向字符指针数据的指针变量p

char *name[]={″BASIC″,″FORTRAN″,″C++″,″Pascal″,″COBOL″};

p=name+2;                //见图6.23中p的指向

cout<<*p<<endl;          //输出name[2]指向的字符串

cout<<**p<<endl;         //输出name[2]指向的字符串中的第一个字符

}

运行结果为

C++

C

指针数组的元素也可以不指向字符串,而指向整型数据或单精度型数据等。

在本章开头已经提到了“间接访问”一个变量的方式。利用指针变量访问另一个变量就是“间接访问”。如果在一个指针变量中存放一个目标变量的地址,这就是“单级间址”,见图6.24(a)。指向指针的指针用的是“二级间址”方法。见图6.24(b)。从理论上说,间址方法可以延伸到更多的级,见图6.24(c)。但实际上在程序中很少有超过二级间址的。

6.8 有关指针的数据类型和指针运算的小结

6.8.1 有关指针的数据类型的小结

表6.1 有关指针的数据类型

6.8.2 指针运算小结

前面已用过一些指针运算(如p++,p+i等),现在把全部的指针运算列出如下。

(1) 指针变量加/减 一个整数

例如: p++,p--,p+i,p-i,p+-i,p-=i等。

C++规定,一个指针变量加/减一个整数是将该指针变量的原值(是一个地址)和它指向的变量所占用的内存单元字节数相加或相减。如p+i代表这样的地址计算: p+i*d,d为p所指向的变量单元所占用的字节数。这样才能保证p+i指向p下面的第i个元素。

(2) 指针变量赋值

将一个变量地址赋给一个指针变量。如

p=&a;                //将变量a的地址赋给p

p=array;             //将数组array首元素的地址赋给p

p=&array[i];         //将数组array第i个元素的地址赋给p

p=max;               //max为已定义的函数,将max的入口地址赋给p

p1=p2;               //p1和p2都是同类型的指针变量,将p2的值赋给p1

(3) 指针变量可以有空值,即该指针变量不指向任何变量,可以这样表示:

p=NULL;

实际上NULL代表整数0,也就是使p指向地址为0的单元。这样可以使指针不指向任何有效的单元。实际上系统已先定义了

NULL: #define NULL 0

在iostream头文件中就包括了以上的NULL定义,NULL是一个符号常量。应注意,p的值等于NULL和p未被赋值是两个不同的概念。

任何指针变量或地址都可以与NULL作相等或不相等的比较,如

if(p==NULL) p=p1;

(4) 两个指针变量可以相减

如果两个指针变量指向同一个数组的元素,则两个指针变量值之差是两个指针之间的元素个数,见图6.25。

假如p1指向a[1],p2指向a[4],则p2-p1=(a+4)-(a+1)=4-1=3。

但p1+p2并无实际意义。

(5) 两个指针变量比较

若两个指针指向同一个数组的元素,则可以进行比较。指向前面的元素的指针变量小于指向后面元素的指针变量。如图6.25中,p1<p2,或者说,表达式“p1<p2”的值为真,而“p2<p1”的值为假。注意,如果p1和p2不指向同一数组则比较无意义。

(6) 对指针变量的赋值应注意类型问题。

在本章前几节中介绍了指针的基本概念和初步应用。应该说明,指针是C和C++中重要的概念,是C和C++的一个特色。使用指针的优点是: ①提高程序效率;②在调用函数时,如果改变被调用函数中某些变量的值,这些值能为主调函数使用,即可以通过函数的调用,得到多个可改变的值;③可以实现动态存储分配。

但是同时应该看到,指针使用实在太灵活,对熟练的程序人员来说,可以利用它编写出颇有特色的、质量优良的程序,实现许多用其他高级语言难以实现的功能,但也十分容易出错,而且这种错误往往难以发现。

6.9 引用

6.9.1 什么是变量的引用

对一个数据可以使用“引用”(reference),这是C++对C的一个重要扩充,引用是一种新的变量类型,它的作用是为一个变量起一个别名。假如有一个变量a,想给它起一个别名b,可以这样写:

int a;                       //定义a是整型变量

int &b=a;                    //声明b是a的引用

以上语句声明了b是a的引用,即b是a的别名。经过这样的声明后,a或b的作用相同,都代表同一变量。

注意: 在上述声明中,&是引用声明符,并不代表地址。不要理解为“把a的值赋给b的地址”。声明变量b为引用类型,并不需要另外开辟内存单元来存放b的值。b和a占内存中的同一个存储单元,它们具有同一地址。声明b是a的引用,可以理解为: 使变量b具有变量a的地址。见图6.26,如果a的值是20,则b的值也是20。

在声明一个引用类型变量时,必须同时使之初始化,即声明它代表哪一个变量。在声明变量b是变量a的引用后,在它们所在函数执行期间,该引用类型变量b始终与其代表的变量a相联系,不能再作为其他变量的引用(别名)。下面的用法不对:

int  a1,a2;

int  &b=a1;

int  &b=a2;                //企图使b又变成a2的引用(别名)是不行的

6.9.2 引用的简单使用

例6.17 引用和变量的关系。

#include <iostream>

#include <iomanip>

using namespace std;

int main( )

{ int a=10;

int &b=a;                  //声明b是a的引用

a=a*a;                     //a的值变化了,b的值也应一起变化

cout<<a<<setw(6)<<b<<endl;

b=b/5;                     //b的值变化了,a的值也应一起变化

cout<<b<<setw(6)<<a<<endl;

return 0;

}

a的值开始为10,b是a的引用,它的值当然也应该是10,当a的值变为100(a*a的值)时,b的值也随之变为100。在输出a和b的值后,b的值变为20,显然a的值也应为20。

运行记录如下:

100  100             (a和b的值都是100)

20    20              (a和b的值都是20)

6.9.3 引用作为函数参数

有了变量名,为什么还需要一个别名呢?C++之所以增加引用类型, 主要是把它作为函数参数,以扩充函数传递数据的功能。

到目前为止,本书介绍过函数参数传递的两种情况。

(1) 将变量名作为实参和形参。这时传给形参的是变量的值,传递是单向的。如果在执行函数期间形参的值发生变化,并不传回给实参。因为在调用函数时,形参和实参不是同一个存储单元。

例6.18 要求将变量i和j的值互换。下面的程序无法实现此要求。

#include <iostream>

using namespace std;

int main( )

{ void swap(int,int);                 //函数声明

int i=3,j=5;

swap(i,j);                          //调用函数swap

cout<<i<<″ ″<<j<<endl;              //i和j的值未互换

return 0;

}

void swap(int a,int b)      //企图通过形参a和b的值互换,实现实参i和j的值互换

{ int temp;

temp=a;                             //以下3行用来实现a和b的值互换

a=b;

b=temp;

}

运行时输出3 5i和j的值并未互换。见图6.27示意。

为了解决这个问题,采用传递变量地址的方法。

#include <iostream>

using namespace std;

int main( )

{ void swap(int *,int *);

int i=3,j=5;

swap(&i,&j);                            //实参是变量的地址

cout<<i<<″ ″<<j<<endl;                  //i和j的值已互换

return 0;

}

void swap(int *p1,int *p2)              //形参是指针变量

{ int temp;

temp=*p1;                              //以下3行用来实现i和j的值互换

*p1=*p2;

*p2=temp;

}

在Pascal语言中有“值形参”和“变量形参”(即var形参),对应两种不同的传递方式,前者采用值传递方式,后者采用地址传递方式。在C语言中,只有“值形参”而无“变量形参”,全部采用值传递方式。C++把引用型变量作为函数形参,就弥补了这个不足。

C++提供了向函数传递数据的第(3)种方法,即传送变量的别名。

例6.20 利用“引用形参”实现两个变量的值互换。

#include <iostream>

using namespace std;

int main( )

{ void swap(int &,int &);

int i=3,j=5;

swap(i,j);

cout<<″i=″<<i<<″  ″<<″j=″<<j<<endl;

return 0;

}

void swap(int &a,int &b)             //形参是引用类型

{ int temp;

temp=a;

a=b;

b=temp;

}

输出结果为

i=5 j=3

在swap函数的形参表列中声明a和b 是整型变量的引用。

① 使用引用类型就不必在swap函数中声明形参是指针变量。指针变量要另外开辟内存单元,其内容是地址。而引用变量不是一个独立的变量,不单独占内存单元,在例6.20中引用变量a和b的值的数据类型与实参相同,都是整型。

② 在main函数中调用swap函数时,实参不必用变量的地址(在变量名的前面加&),而直接用变量名。系统向形参传送的是实参的地址而不是实参的值。

这种传递方式相当于Pascal语言中的“变量形参”,显然,这种用法比使用指针变量简单、直观、方便。使用变量的引用,可以部分代替指针的操作。有些过去只能用指针来处理的问题,现在可以用引用来代替,从而降低了程序设计的难度。

例6.21 对3个变量按由小到大的顺序排序。

#include <iostream>

using namespace std;

int main( )

{ void sort(int &,int &,int &); //函数声明,形参是引用类型

int a,b,c;                                //a,b,c是需排序的变量

int a1,b1,c1;                             //a1,b1,c1最终的值是已排好序的数列

cout<<″Please enter 3 integers:″;

cin>>a>>b>>c;                            //输入a,b,c

a1=a;b1=b;c1=c;

sort(a1,b1,c1);                          //调用sort函数,以a1,b1,c1为实参

cout<<″sorted order is ″<<a1<<″ ″<<b1<<″ ″<<c1<<endl;//此时a1,b1,c1已排好序

return 0;

}

void sort(int &i,int &j,int &k)            //对i,j,k 3个数排序

{ void change(int &,int &);                //函数声明,形参是引用类型

if (i>j) change (i,j);                    //使i<=j

if (i>k) change (i,k);                    //使i<=k

if (j>k) change (j,k);                    //使j<=k

}

void change (int &x,int &y)                 //使x和y互换

{ int temp;

temp=x;

x=y;

y=temp;

}

运行情况如下:

Please enter 3 integers: 23 12 -345↙

sorted order is -345 12 23

可以看到: 这个程序很容易理解,不易出错。由于在调用sort函数时虚实结合使形参i,j,k成为实参a1,b1,c1的引用,因此通过调用函数sort(a1,b1,c1)既实现了对i,j,k排序,也就同时实现了对a1,b1,c1排序。同样,执行change (i,j)函数,可以实现对实参i和j的互换。

引用不仅可以用于变量,也可以用于对象。例如实参可以是一个对象名,在虚实结合时传递对象的起始地址。这会在以后介绍。

当看到&a这样的形式时,怎样区别是声明引用变量还是取地址的操作呢?当&a的前面有类型符时(如int &a),它必然是对引用的声明;如果前面无类型符(如cout<<&a),则是取变量的地址。

09C++指针的更多相关文章

  1. iOS学习09C语言函数指针

    本次主要学习和理解函数指针 1.函数指针 void printValue(int number) { printf("number = %d\n", number); } int ...

  2. 09C语言指针

    C语言指针 地址 地址就是数据元素在内存中的位置表示: &变量名 #include <stdio.h> int main(){ int aa; unsigned int bb = ...

  3. TODO:Golang指针使用注意事项

    TODO:Golang指针使用注意事项 先来看简单的例子1: 输出: 1 1 例子2: 输出: 1 3 例子1是使用值传递,Add方法不会做任何改变:例子2是使用指针传递,会改变地址,从而改变地址. ...

  4. enote笔记法使用范例(2)——指针(1)智能指针

    要知道什么是智能指针,首先了解什么称为 “资源分配即初始化” what RAII:RAII—Resource Acquisition Is Initialization,即“资源分配即初始化” 在&l ...

  5. C++虚函数和函数指针一起使用

    C++虚函数和函数指针一起使用,写起来有点麻烦. 下面贴出一份示例代码,可作参考.(需要支持C++11编译) #include <stdio.h> #include <list> ...

  6. C++11 shared_ptr 智能指针 的使用,避免内存泄露

    多线程程序经常会遇到在某个线程A创建了一个对象,这个对象需要在线程B使用, 在没有shared_ptr时,因为线程A,B结束时间不确定,即在A或B线程先释放这个对象都有可能造成另一个线程崩溃, 所以为 ...

  7. c 数组与指针的使用注意事项

    数组变量和指针变量有一点小小的区别 所以把数组指针赋值给指针变量的时候千万要小心 加入把数组赋值给指针变量,指针变量只会包含数组的地址信息 而对数组的长度一无所知 相当于指针丢失了一部分信息,我们把这 ...

  8. Marshal.Copy将指针拷贝给数组

    lpStatuss是一个UNITSTATUS*的指针类型实例,并包含SensorDust字段 //定义一个数组类型 byte[] SensorDust = new byte[30] //将指针类型拷贝 ...

  9. C++智能指针

    引用计数技术及智能指针的简单实现 基础对象类 class Point { public: Point(int xVal = 0, int yVal = 0) : x(xVal), y(yVal) { ...

随机推荐

  1. appium学习【二】:用try捕获异常后,用例的执行结果为pass

    在代码中用try..except捕获异常截图后,HTMLTestRunner生成的测试报告中,用例的执行结果为pass 解决办法为: 在try..except后加raise,只要再加个raise就行了 ...

  2. RedHat/CentOS安装五笔输入法(转载)

    转自:http://www.zhukun.net/archives/5939 使用centos 仓库里的 ibus,我的 CenOS6.3 自带了 ibus 包.打开 System – prefere ...

  3. Linux 系统管理命令 - iostat - I/O 信息统计

    命令详解 重要星级: ★★★★☆ 功能说明: iostat 是 I/O statistics ( 输入/输出统计 ) 的缩写,其主要功能是对系统的磁盘 I/O 操作进行监视.它的输出主要是显示磁盘读写 ...

  4. 大数据技术之_25_手机APP信息统计系统项目_01_APP 数据生成模块 + 数据收集模块 + 数据处理模块框架搭建 + 业务需求处理 + 数据展示模块 +项目总结 + 问题总结

    一 项目概述1.1 角色1.2 业务术语1.3 项目效果展示二 项目需求三 项目概要3.1 项目技术架构3.2 项目目录结构3.3 项目技术选型3.4 项目整体集群规划3.5 创建项目工程四 APP ...

  5. Latex排版工具的使用(二) 分类: Latex 2014-06-14 23:01 389人阅读 评论(0) 收藏

    Latex可以支持中文排版,如何实现中文支持可以到网上查找教程. 下面编写一段对中文排版的Latex源文档: 新建文件second.tex: \documentclass{article} \usep ...

  6. Android 线程池系列教程(5)与UI线程通信要用Handler

    Communicating with the UI Thread 上一课 下一课 1.This lesson teaches you to Define a Handler on the UI Thr ...

  7. Hackonacci Matrix Rotations 观察题 ,更新了我的模板

    https://www.hackerrank.com/contests/w27/challenges/hackonacci-matrix-rotations 一开始是没想到观察题的.只想到直接矩阵快速 ...

  8. 给定一个整数 n,返回 n! 结果尾数中零的数量。

    示例 1: 输入: 3 输出: 0 解释: 3! = 6, 尾数中没有零. 示例 2: 输入: 5 输出: 1 解释: 5! = 120, 尾数中有 1 个零. 代码部分 class Solution ...

  9. JSR-303原生支持的限制

    @Null: 限制只能为null@NotNull: 限制必须不为null@AssertFalse: 限制必须为false@AssertTrue: 限制必须为true@DecimalMax(value) ...

  10. Android基础夯实--重温动画(五)之属性动画 ObjectAnimator详解

    只有一种真正的英雄主义 一.摘要 ObjectAnimator是ValueAnimator的子类,它和ValueAnimator一样,同样具有计算属性值的功能,但对比ValueAnimator,它会更 ...