C++数组与指针详解

代码编译运行环境:VS2012+Debug+Win32

1.数组

数组大小(元素个数)一般在编译时决定,也有少部分编译器可以运行时动态绝对数组大小,比如icpc(intel C++编译器)。

1.1数组名的意义

数组名的本质是数组第一个元素的首地址,也是数组的首地址。数组名所代表的地址是在编译阶段确定的,因此 数组名是一个地址常量,但不是常变量,是符号常量,文字常量的一种。重要的事情说三遍,数组名本身不是一个变量,也就不可以寻址,且不允许为数组名赋值。假设定义数组:
int A[10];
那么再定义一个引用:
int* &r=A;
就是错误的,因为变量A是一个文字常量,不可寻址。如果要建立数组A的引用,应该这样定义:
int* const &r=A;
此时,现在数据区开辟一个无名临时变量,将数组A代表的地址常量拷贝到该变量中,再将常引用r与此变量进行绑定。

此外,定义一个数组A,则A、&A[0]、A+0是等价。

在sizeof()运算中,数组名代表的是全体数组元素,而不是哪个单个元素。例如,定义int A[5],生成Win32的程序,sizeof(A)就等于5*sizeof(int)=5*4=20。
示例程序

#include <iostream>
using namespace std;

int main()
{
    int A[4]={1,2,3,4};
    int B[4]={5,6,7,8};
    int (&rA)[4]=A; //建立数组A的引用

    cout<<"A:"<<A<<endl;
    cout<<"&A:"<<&A<<endl;
    cout<<"A+1:"<<A+1<<endl;
    cout<<"&A+1:"<<&A+1<<endl;
    cout<<"B:"<<B<<endl;
    cout<<"rA:"<<rA<<endl;
    cout<<"&rA:"<<&rA<<endl;
}

运行结果:

A:0013F76C
&A:0013F76C
A+1:0013F770
&A+1:0013F77C
B:0013F754
rA:0013F76C
&rA:0013F76C

阅读以上程序,注意如下几点。
(1)A与&A的结果在数值上是一样的,但是A与&A的数据类型却不同。A的类型是int[4],近似于int* const,&A的类型则是int(*)[4]。它们在概念上是不一样的,这就直接导致A+1与&A+1的结果完全不一样。

(2)为变量建立引用的语法格式是type& ref,因为数组A的类型是int[4],因此为A建立引用的是int (&rA)[4]=A;

1.2数组的初始化

定义数组的时候,为数组元素赋初值,叫做数组的初始化。可以为一维数组指定初值,也可以为多维数组指定初值。例如。

Int A[5]={};    //定义长度为5的数组,所有数组元素的值都为0
int A[]={1,2,3}; //定义长度为3的数组,数组元素分别为1,2,3
int A[5]={1,2};  //定义长度为5的数组,A[0],A[1]分别为1,2,其他值均为0
int A[][2]={{1,2},{3,4},{5,6}}; //定义一个类型为int[3][2]的二维数组
int A[][2]={{1},{1},{1}}; //定义一个类型为int[3][2]的二维数组,A[0][0]、A[1][0]、A[2][0]三个元素的值为1,其他元素的值均为0

//以下是几种错误的初始化方法
int A[3]={1,2,3,4}; //初始化项的个数超过数组的长度 
int A[3]={1,,3}; //不允许中间跳过某项

如果一个数组是某个类对象的数据成员,C++语言并没有提供特别的手段来初始化这个数组。只能在构造函数中分别指定数组元素的值,或者是用memset等其它方式来初始化数组。特别的,不能再构造函数的初始化列表中为对象的书组成员进行初始化。

2.指针

2.1指针的定义

指针在C/C++中就是用来存放地址值的变量,相应的数据类型成为指针类型。在32位平台上,任何指针类型所占用的空间都是都是4字节。比如sizeof(int*)、sizeof(double*)、sizeof(float*)等的值都为4。

2.2定义指针的形式

定义指针的形式是:type* p;其中type是指针所指向的对象的数据类型,而*则是指针的标志,p是指针变量的名字。

由于C++中允许定义复合数据类型,因此指向复合数据类型对象的指针的定义方式可能较为复杂。理解指针,关键是理解指针的类型和指针所指向的数据的类型。例如:

int (*p)[5]; //指针p的类型是int(*)[5],指针所指向的数据类型是int[5]
int* p[5];   //p是有5个分量的指针数组,每个分量的类型都是int*(指向int的指针)
int** p;    //指针p的类型是int**,p指向的类型是int*,p是指向指针的指针

2.3指针的初始化

定义指针变量之后,指针变量的值一般是随机值,这样的值对应的地址不是指针变量可以合法访问的地址。指针变量的值合法化的途径通常有两个,
一是让指针指向一个已经存在的变量,如:

int i;
int *p=&i;
//另一种途径就是动态申请空间,然后让指针指向这片空间的第一个字节。如:
int* p=new int[10];
//还有就是将指针置为空指针,待以后使用时再赋有效的地址值,如:
int *p=NULL;

2.4指针可以参与的运算

由于指针是一个变量,所以指针可以参与一些运算。假设定义指针int* p,指针p能够参与的运算有:
(1)解引用运算,即获取指针所指的内存地址处的数据,表达式为*p;
如果指针指向的是一个结构或者类的对象,那么解引用之后访问对象的成员可以采用两种形式:(*p).mem或p->mem。

(2)取地址运算,即获取指针变量的地址,表达式为&p,其数据类型为int**;

(3)指针与整数相加减。表达式p+i(或者p-i),实际上是让指针递增或递减的移动i个int型变量的距离。

(4)两个指针相减,如p-q,其结果是两个指针所存储的地址之间的int型数据的个数。

2.5注意指针的有效性

使用指针的关键,就是让指针变量指向一个它可以合法访问的内存地址,如果不知道它指向何处,请置为空指针NULL或者((void*)0)。

在某些情况下,指针的值开始是合法的,以后随着某些操作的进行,它变成了非法的值。考察如下程序。

#include <iostream>
using namespace std;

int* pPointer;
void SomeFunction(){
    int nNumber=25;
    pPointer=&nNumber; //将指针pPointer指向nNumber
}

void UseStack(){
    int arr[100]={};
}

int main()
{
    SomeFunction();
    UseStack();
    cout<<"value of *pPointer:"<<*pPointer<<endl;
}

输出结果是0,并非想象中的25。原因是函数SomeFunction()运行结束之后,局部变量nNumber已经被清空,其占有的空间在离开函数后归还给系统,之后有分配给函数UseStack()中的局部变量arr。因此指针pNumber的解引用后的值变成了0。所以,要想正确的使用指针,必须保证指针所指向单元的有效性。

3.数组与指针的关系

数组名代表的是数组的首地址,而数组A的某个元素A[i]可以解释成*(A+i),所以数组名本身可以理解为一个指针(地址),只不过是指针常量。所以,在很多情况下,数组与指针的用法是相同的。但是数组与指针本质上存在一些重要的区别。
(1)数组空间是静态分配的,编译时决定大小。而指针在定义时,可以没有合法访问的地址空间,也就是悬挂的野指针。

(2)数组名代表的是一个指针常量,企图改变数组名所代表的地址的操作都是非法的。
例如如下代码:

int arr[5]={0,1,2,3,4};
arr++; //编译错误

(3)函数形参中的数组被解释为指针。
考察如下程序:

void show0(int A[]){
    A++;
    cout<<A[0]<<endl;
}

void show1(int A[5]){
    A++;
    cout<<A[0]<<endl;
}

int main()
{
    int d[5]={1,2,3,4,5};
    show0(d);
    show1(d);
}

以上程序编译通过并输出2和2。程序中形参数组A可以进行自增运算,改变了自身的值,这个说明了形参数组A被当作指针看待。之所以这样处理,原因有两个。一是C++语言不对数组的下标作越界检查,二是数组作为一个整体进行传递时,考虑会有较大运行时开销,为了提高程序的运行效率,所以数组退化成了指针。

(4)如果函数的形参是数组的引用,那么数组的长度将被作为类型的一部分。

实际上,对数组建立引用,就是对数组的首地址建立一个常引用。由于引用是C++引入的新机制,所以在处理引用时使用了一些与传统C语言不同的规范。在传统的C语言中,对数组的下标是不做越界检查,因此在函数的参数说明中,int[5]和int[6]都被理解为int[](也就是int*),C++语言也沿用了这种处理方式。但是,int(&)[5]与int(&)[6]被认为是不同的数据类型,在实参与形参的匹配过程作严格检查。考察如下程序。

#include <iostream>
using namespace std;

void show(int(&A)[5]){
    cout<<"type is int(&)[5]"<<endl;
}

void show(int(&A)[6]){
    cout<<"type is int(&)[5]"<<endl;
}

int main()
{
    int d[5]={1,2,3,4,5};
show(d);
}

程序结果:
type is int(&)[5]

(5)在概念上,指针同一维数组相对应。多维数组也是存储在连续的存储空间,而将多维数组当做一维数据看待时,可以有不同的分解方式。考察如下程序。

#include <iostream>
using namespace std;

void show1(int A[],int n){
    for(int i=0;i<n;++i)
        cout<<A[i]<<" ";
}

void show2(int A[][5],int n){
    for(int i=0;i<n;++i)
        show1(A[i],5);
}

void show3(int A[][4][5],int n){
    for(int i=0;i<n;++i)
        show2(A[i],4);
}

int main()
{
    /*int d[5]={1,2,3,4,5}; show(d);*/
    int d[3][4][5];
    int i,j,k,m=0;
    for(int i=0;i<3;++i)
        for(int j=0;j<4;++j)
            for(int k=0;k<5;++k)
                d[i][j][k]=m++;

    show1((int *)d,3*4*5);
    cout<<endl;
    show2((int(*)[5])d,3*4);
    cout<<endl;
    show3(d,3);
}

程序运行结果可以看出,一下三条输出语句的数据结果是相同的。

show1((int *)d,3*4*5);
show2((int(*)[5])d,3*4);
show3(d,3);

它们的输出结果完全一样,即从0到59。这说明把3维数组d当作一维数组看待,至少可以有以下3中不同的分解方式:
a.数据类型为int,元素个数为3*4*5=60;
b.数据类型为int[5],元素个数为3*4=12;
c.数据类型为int[4][5],元素个数为3。

所以,可以将多为数组看做“数组的数组”。在将多为数组转换为指针的时候,一定要注意多为数组的分解方式,以便进行正确的类型转换。

(6)字符数组与字符指针的区别。
字符数组字符指针在形式上很接近,但在内存空间的分配和使用上还是有重大的差别。如前所述,数组名并不是一个运行实体,它本省不能被寻址。而指针是一个变量(运行时实体),可以被寻址,它所指向的空间是否合法要在运行时决定。错误地使用指针将导致对内存空间的非法访问。考察如下程序。

#include <iostream>
using namespace std;
int main()
{
    char s[]="abc"; //s是字符数组,空间分配在栈上。对字符数组元素的修改是合法的
    char *p="abc"; 
    s[0]='x';
    cout<<s<<endl;
    //p[0]='x';//此句编译出错,指针指向常量区的字符串,对字符串常量的修改是非法的
    cout<<p<<endl;
}

程序输出结果:
xbc
abc

参考文献

[1]陈刚.C++高级进阶教程[M].武汉:武汉大学出版社,2008[5.1(P186-P195)]

你可能感兴趣的:(C++数组与指针详解)