4.8 指针、数组和指针算术
指针和数组基本等价的原因在于指针算术和C++内部处理数组的方式。首先,我们来看一看算术。将整数变量加1后,其值将增加1;但将指针变量加1后,增加的量等于它指向的类型的字节数。将指向double的指针加1后,如果系统对double使用8个字节存储,则数组将增加8;将指向short的指针加1后,如果系统对short使用2个字节存储,则指针值将增加2。程序4.19演示了这种令人吃惊的现象,它还说明另一点;C++将数组名解释为地址。
程序4.19 addpntrs.cpp
//addpntrs.cpp——pointer addition
#include<iostream>
int main()
{
using namespace std;
double wages[3] = {10000.0 , 20000.0 , 30000.0};
short stacks[3] = {3,2,1};
double * pw = wages;
short * ps = &stacks[0];
cout<<"pw = "<<pw<<" , *pw = "<<*pw<<endl;
pw=pw+1;
cout<<"and 1 to the pw pointer\n";
cout<<"pw = "<<pw<<" , *pw = "<<*pw<<endl<<endl;
cout<<"ps = "<<ps<<" , *ps = "<<*ps<<endl;
ps=ps+1;
cout<<"and 1 to the ps pointer\n";
cout<<"ps = "<<ps<<" , *ps = "<<*ps<<endl<<endl;
cout<<"access two elements with array notation\n";
cout<<"stacks[0] = "<<stacks[0]
<<", stacks[1] = "<<stacks[1]<<endl;
cout<<"access two elements with array notation\n";
cout<<"access two elements with pointer notation\n";
cout<<"*stacks = "<<*stacks
<<", *(stacks+1) = "<<*(stacks+1)<<endl;
cout<<sizeof(wages)<<" = size of wages array\n";
cout<<sizeof(pw)<<" = size of pw pointer\n";
return 0;
}
下面是该程序的输出:
4.8.1 程序说明
在多数情况下,C++将数组名解释为数组第1个元素的地址。因此,下面的语句将pw声明为指向double类型的指针,然后将它初始化为wages——wages数组中第1个元素的地址:
double * pw = wages;
和所有数组一样,wages也存在下面的等式:
wages = &wages[0] = address of first element of array
为表明情况确实如此,该程序在表达式&stacks[0]中显式地使用地址元算符来将ps指针初始化为stakes数组的第1个元算。
接下来,程序查看pw和*pw的值。前者是地址,后者是存储在该地址中的值。由于pw指向第1个元素,因此*pw显示的值为第1个元素的值,即10000。接着,程序将pw加1。正如前面指出的,这样数字地址值将增加8,这使得pw的值为第2个元素的地址。因此,*pw现在的值是20000——第2个元素的值。
此后,程序对ps执行相同的操作,这一次由于ps指向的是short类型,而short占用2个字节,因此将指针加1时,其值将增加2。结果是,指针也指向数组中下一个元素。
注意:将指针变量加1后,其增加的值等于指向的类型占用的字节数。
现在来看一看数组表达式stacks[1]。C++编译器将该表达式看作是*(stacks + 1),这意味着先计算数组第2个元素大的地址,然后找到存储在那里的值。最后的结果便是stacks[1]的含义(运算符优先级要求使用括号,如果不使用括号,将给*stacks加1,而不是给stacks加1)。
从该程序的输出可知,*(stacks + 1)和stacks[1]是等价的。同样,*(stacks + 2)和stacks[2]也是等价的。通常,使用数组表示法时,C++都执行下面的转换:
arrayname[i] becomes * (arrayname + i);
如果使用的是指针,而不是数组名,则C++也将执行同样的转换:
pointername [i] becomes * (pointername + i);
因此,在很多情况下,可以相同的方式使用指针名和数组名。对于它们,可以使用数组方括号表示法,也可以使用解除引用运算符(*)。在多数表达式中,它们都表示地址。区别之一是,可以修改指针的值,而数组名是常量:
pointername = pointername + 1;
arrayname = arrayname + 1;
另一个区别是,对数组应用sizeof与那算符得到的是数组的长度,而对指针应用sizeof得到的是指针的长度,即使指针指向的是一个数组。例如,在程序4.19中,pw和wages指的是同一个数组,但对它们应用sizeof运算符得到的结果如下:
24 = size of wages array << displaying sizeof wages
4 = size of pw pointer << displaying sizeof pw
这种情况下,C++不会将数组名解释为地址
数组的地址
对数组取地址时,数组名也不会被解释为其地址。等等,数组名难道不被解释为数组的地址吗?不完全如此:数组名被解释为其第一个元素的地址,而对数组名应用地址运算符时,得到的是整个数组的地址:
short tell [10];
cout << tell <<endl;
cout << &tell << endl;
从数字上说,这两个地址相同;但从概念上说,&tell[0](即tell)是一个2字节内存块的地址,而&tell是一个20字节内存块的地址。因此,表达式tell+1将地址值加2,而表达式&tell+2将地址加20。换句话说,tell是一个short指针(*short),而&tell是一个这样的指针,即指向包含20个元素的short数组(short(*)[20])。
您可能会问,前面有关&tell的类型描述是如何来的呢?首先,您可以这样声明和初始化这种指针:
short (*pas) [20] = &tell;
如果省略括号,优先级规则将使得pas先于[20]结合,导致pas是一个short指针数组,它包含20个元素,因此括号是必不可少的。其次,如果要描述变量的类型,可将声明中的变量名删除。因此,pas的类型为short(*)[20]。另外,由于pas被设置为&tell,因此*pas于tell等价,所以(*pas)[0]为tell数组的第一个元素。
总之,使用new来创建数组以及使用指针来访问不同的元素很简单。只要把指针当作数组名对待即可。然而,要理解为何可以这样做,将是一种挑战。要想真正了解数组和指针,应认真复习它们的相互关系。
4.8.2 指针小结
刚才已经介绍了大量指针的知识,下面对指针和数组做一总结。
1. 声明指针
要声明指向特定类型的指针,请使用下面的格式:
typeName * pointerName;
下面是一些示例:
double * pn;
char *pc;
其中,pn和pc都是指针,而double *和char *是指向double的指针和指向char的指针。
2. 给指针赋值
应将内存地址赋给指针。可以对变量名应用&运算符,来获得被命名的内存的地址,new运算符返回未命名的内存和地址。
下面是一些示例:
double * pn ;
double * pa ;
char * pc ;
double bubble = 3.2 ;
pn = &bubble ;
pc = new char;
pa = new double [30] ;
3. 对指针解除引用
对指针解除引用意味着获得指针的值。对指针应用解除引用或间接值运算符(*)来解除引用。因此,如果像上面的例子中那样,pn是指向bubble的指针,则*pn是指向的值,即3.2。
下面是一些示例:
cout<<*pn;
*pc = ‘S’;
另一种对指针解除引用的方法是使用数组表达法,例如,pn[0]与*pn是意义的。决不要对未被初始化为适当地址的指针解除引用。
4.区分指针和指针所指向的值
如果pt是指向int的指针,则*pt不是指向int的指针,而是完全等同于一个int类型的变量。pt才是指针。
下面是一个示例:
int * pt = new int ;
* pt = 5;
5.数组名
在多数情况下,C++将数组名视为数组的第一个元素的地址。
下面是一个示例:
int tacos[10];
一种例外情况是,将sizeof运算符用于数组名时,此时将返回整个数组的长度(单位为字节)。
6.指针算术
C++允许将指针和整数相加。加1的结果等于原来的地址值加上指向的对象占用的总字节数。还可以将一个指针减去另一个指针,获得两个指针的差。后一种运算将得到一个整数,仅当两个指针指向同一个数组(也可以指向超出结尾的一个位置)时,这种运算才有意义;这将得到两个元素的间隔。
下面是一些示例:
int tacos [10] = {5,2,8,4,1,2,2,4,6,8};
int * pt = tacos ;
pt = pt + 1;
int *pe = &tacos[9];
pe = pe - 1;
int diff = pe - pt ;
7. 数组的动态联编和静态联编
使用数组声明来创建数组时,将采用静态联编,即数组的长度在编译时设置:
int tacos[10];
使用new[]运算符创建数组时,将采用动态联编(动态数组),即将在运行时为数组分配空间,其长度也将在运行时设置。使用完这种数组后,应使用delete[]释放其占用的内存:
int size;
cin>>size;
int * pz = new int [size];
...
delete [] pz;
8. 数组表示法和指针表示法
使用方括号数组表示法等同于对指针解除引用:
tacos[0] means * tacos means the value at address tacos
tacos[3] means * (tacos + 3) means the value at address tacos + 3
数组名和指针变量都是如此,因此对于指针和数组名,既可以使用指针表示法,也可以使用数组表示法。
下面是一些示例:
int * pt = new int [10];
*pt = 5;
pt[0] = 6;
pt[9] = 44;
int coats[10];
*(coats + 4) = 12;