首页 > 编程语言 > 深入了解c++数组与指针
2020
10-09

深入了解c++数组与指针

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],&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}; //不允许中间跳过某项

2.指针

2.1指针的定义

指针是用来存放地址值的变量,相应的数据类型成为指针类型。在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 *p=NULL;

//将指针指向某个变量
int i;
int *p=&i;

//动态申请内存空间
int* p=new int[10];

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[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

以上就是深入了解c++数组与指针的详细内容,更多关于c++数组与指针的资料请关注自学编程网其它相关文章!

编程技巧