最近研究C++中的数组怎么作为参数传入到函数中,自然而然引出了这篇博客的标题,即数组和指针的爱恨情仇。。。
想要知道数组和指针交织在一起会摩擦出怎样的火花,那就先要了解数组和指针各自的语法和特点。这里不想写一些非常专业的定义(咱也没看),就是针对一些常用的用法做一个直观的认识。
首先是数组,实际上就是一些相同数据类型的变量放在一起,就构成了一个数组,这里的重点是“一起”,即它们的存储地址是连续的(这也是为什么可以通过指针来依次访问)。此外,就是在定义数组时一定要用常量指定一个数组的大小,即包含多少个元素。
其次是指针,一般常用的语法为int *p = NULL
,表示定义一个指针变量(指针变量一定要养成定义即初始化的好习惯!),它存储的内容是一个int
类型变量对应的地址,那如果直接输出这个地址(即指针变量的值)呢?一般会得到一个比较大的数,它对应的内存中的某个位置:
#include
using namespace std;
int main()
{
int a = 10;
int *p = &a;
cout << "p=" << p << endl << "*p=" << *p;
return 0;
}
//输出:
//p=0x61fe14
//*p=10
此外,最为重要的是,指针指向的数据类型其实就决定了这个指针的 “步长”,即运行p++
或++p
后它移动的内存大小。举个例子:
#include
using namespace std;
int main()
{
int a = 10; double b = 1.0;
int *p1 = &a; double *p2 = &b;
cout << "p1 = " << p1 << endl << "p1+1 = " << (++p1) << endl;
cout << "p2 = " << p2 << endl << "p2+1 = " << (++p2) << endl;
cout << "size of pointer:" << sizeof(p1) <<" "<< sizeof(p2) << endl;
return 0;
}
//输出:
//p1 = 0x61fe0c
//p1+1 = 0x61fe10 //int类型4个字节
//p2 = 0x61fe00
//p2+1 = 0x61fe08 //double类型8个字节
//size of pointer:8 8 //指针都是8个字节(可能是64位系统?)
一般定义好一个数组后,最常用的访问方式是下标访问数组中的元素,但这种方式要保证有整个数组,才能够通过下标来访问,而如果用指针来访问,只需要一个指针变量即可(顶多加上一个数组长度),不管是作为返回值还是参数都十分方便,效率更高。
首先说一个众所周知的知识点:一维数组的名称实际上就是一个指针变量,指向的是数组首元素地址。
#include
using namespace std;
int main()
{
int a[5] = {1,2,3,4,5};
for(int i=0; i<5; i++)
cout << *(a+i) << " "; //等价于 cout << a[i]
return 0;
}
//输出:1 2 3 4 5
需要注意,用
*
访问和用[]
访问是等价的。
那么问题来了,一维数组的数组名是一个指针,那二维数组的数组名是不是指针的指针(二级指针)呢?来看一下试验结果:
#include
using namespace std;
int main()
{
int a[2][2] = {0};
**a = 1;
cout << a[0][0] << endl; //输出的是刚赋值的1
int **p = a; //会报错: "cannot convert 'int (*)[2]' to 'int**' in initialization"
return 0;
}
因此,对于二维数组,虽然可以使用**
去访问其中的元素,但是它并不是一个二维指针,其实也很容易理解,从前面说的步长的概念来看,二级指针中的第二级指针,由于指针变量为8个字节,所以所有的二级指针移动时(+1)都是移动8个字节,和数组数据类型无关,因此无法实现逐个访问数组元素。然后从上面的报错可以看出二维数组的真实数据类型为int (*)[2]
,这是啥意思呢?埋个伏笔,请看下文分解。
提到数组和指针,就不得不提二者的结合体:数组指针和指针数组了,复杂度一下子上了一个台阶,堪称1+1>2。
事先说明,这里探讨的数组指针和指针数组,其“数组”都是指一维数组,但其实更高维的数组也可以类推。
首先是区分这两者的概念,最简单的方式就是看后两个字——到底是指针还是数组。
所谓指针数组,首先是一个数组,其次是这个数组的每个元素都是一个指针,都占8个字节。
所谓数组指针,首先是一个指针,其次是这个指针指向的内容是一个数组,注意:这里的指针指向的是数组首地址,不是数组首元素地址,这两个地址值是相同的(即指针变量的取值),但是当赋予同一个指针时,各自的步长不同,具体可以看一下下面这个例子。
#include
using namespace std;
int main()
{
int a[2] = {0};
int (*p1)[2] = &a; //数组指针,指向数组首地址
cout << p1 << " " << 1+p1 << endl;
int *p2 = *p1; //普通指针,指向的是数组首元素地址
cout << p2 << " " << 1+p2 << endl;
return 0;
}
//输出: 0x61fe08 0x61fe10 //步长为8个字节
// 0x61fe08 0x61fe0c //步长为4个字节
可以发现,对于指向数组首元素地址的指针,它的步长就是一个数组元素所占字节大小;而对于指向数组首地址的数组指针,它的步长是整个数组所占字节大小,而且这两者之间还存在一个关系,即数组首地址 = &(数组首元素地址)
或者数组首元素地址 = *(数组首地址)
,记住这个表达式,后面要考。
了解完了基本概念,再来看看其基本语法有什么区别:
int a=1,b=2;
int *p[2] = {&a, &b}; //指针数组,长度可指定可不指定(参考一般数组初始化方法)
int A[2] = {1,2};
int (*p)[2] = &A; //数组指针,既然是指针,在定义时要初始化
乍一看似乎长得很像?感觉就是加了个括号?那怎么记忆呢?目前我看到的最好的方法是按照符号优先级来记忆:() > [] > *
,所以,对于int *p[]
,p
先和[]
结合,构成一个数组变量,然后是int *
修饰数组中的内容,表示数组中全为指向int
类型的指针;对于int (*p)[2]
,先看()
内,*
表示p
是一个指针变量,然后再看[]
和int
,表示p
指向的变量为一个int
类型的数组,即决定了这个指针的步长。
此外,数组指针还可以按照一般的变量定义的格式来表达,即int(*)[2] p
,前面的int(*)[2]
即为该数组指针的数据类型,其中(*)
表示变量为一个指针,int [2]
指定了指针指向变量的内存大小,即步长。
这个表达式似乎看着很眼熟?没错,就是上面第2节最后提到的报错信息。经过试验发现,这种写法不能用在变量定义中,但能用在类型转换中,例如:int (*)[3] p = NULL;
【×】;int (*p)[3] = (int(*)[3])a;//a是一个指针
【√】
学会了基本语法,再来看看怎么应用。
对于指针数组,没啥好说的,可以把它视为一般数组进行赋值或通过下标取值,但要注意,取出来的是指针,如果要取指针指向的数值,还要加上星号。下面以一个访问二维数组的例子来说明:
#include
using namespace std;
int main()
{
int a[2][2] = {0};
int *p[2] = {NULL}; //包含两个指针的指针数组,记得初始化
for(int i=0; i<2; i++)
p[i] = a[i]; //将数组a的每一行首元素地址赋给p中对应位置元素
p[0][1] = 1; //此时p和a基本等价,所以也可以通过下标访问
*(*p + 3) = 3; //或者通过指针来访问
cout << a[0][1] << endl;
cout << a[1][1] << endl;
return 0;
}
//输出:
//1
//3
可以发现这里使用的指针数组中的每一个元素,即指针变量,其指向的都是数组每一行首元素地址,其步长就是这个数组中元素的数据类型所占内存大小。所以可以通过下标或者取指针的方式依次访问数组中的每个元素。
对于数组指针,在二维数组应用当中,常称为行指针,因为它往往指向的是一个二维数组的一行数组首地址,它的步长就是二维数组的一行,所以p+i
就是访问第i+1
行的首地址。那么问题来了?数组指针指向数组首地址,它的步长是一行?那岂不是不能访问一行的某个元素了?好像有点道理,但是还记得上面提到的那个公式吗?数组首元素地址 = *数组首地址
,可以先取一个*
来得到每一行首元素地址,再访问数组中的每个元素。然后,由于使用*
和[]
是等价的,所以很多时候数组指针的使用和二维数组差不多。同样看一个访问二维数组的例子。
#include
using namespace std;
int main()
{
int a[2][2] = {0};
int (*p)[2] = a; //数组指针,指向第一行一维数组首地址
int *q = *p; //一般指针,指向第一行首元素地址
q[0] = -1; //a[0][0]
*(q+1) = 2; //a[0][1]
p++; //指向变为a[1]首地址
**p = 1; //a[1][0]
cout << a[0][0] << " " << a[0][1] << endl;
cout << a[1][0] << " " << a[1][1] << endl;
return 0;
}
//输出:
// -1 2
// 1 0
通过比对指针数组和数组指针访问二维数组的例子,可以发现二者的语法似乎是一样的。有点神奇~
上文提到,数组指针的数据类型为 int(*)[2]
且可以进行强制类型转换,那能不能不按照数组的每一行为步长来进行访问呢?答案是可以的,看下面这个例子。
#include
using namespace std;
int main()
{
int a[3][4] = {0};
int (*p)[2] = (int(*)[2])a; //强行将其步长变成2,但地址不变
for(int i=0; i<6; i++)
**(p+i) = 1; //内侧星号是取到数组首元素地址,外侧星号是取该地址对应元素值
for(int i=0; i<3; i++)
for(int j=0; j<4; j++)
cout << a[i][j] << ' ';
return 0;
}
//输出:1 0 1 0 1 0 1 0 1 0 1 0
虽然原数组是3*4的,但是通过数组指针,可以将原数组以6*2的方式进行访问,相当于一个矩阵形状变换的操作(reshape),这个关键还是在于不管是一维数组还是二维数组,它们的存储空间都是连续的,所以能够使用指针来访问。
最后,让我们再回到最开始的目标:数组作为参数传入函数。这里有两种常用的方式:即以数组形式或以指针形式。
数组形式简单明了,比较好用。
对于一维数组,常用void func(int a[])
,来传数组参数,在访问数组元素时直接使用a[i]
来访问,因为是传入的地址,所以数组内容会被修改,如果是全局变量,可以不用返回;
对于二维数组,常用void func(int a[][4])
来传递参数,注意,第一个括号为空,第二个括号必须是一个常数,即数组每一行的元素个数得确定,在访问数组元素时直接通过a[i][j]
来访问,同样这个数组也会被修改。
对于一维数组,前面我们提到,数组名本质上就是一个指针,因此可以把指针作为参数或返回值应用到函数中。比如void func(int *a)
,这种比较直观,理解起来没有什么难度。
对于二维数组,可能有点小复杂,前面我们提到,二维数组的数组名本质上是一个数组指针,它对应的步长为二维数组的一行,同时它指向的是一行的数组首地址(而不是每一行数组首元素地址),它的数据类型是int(*)[2]
(2为每一行数据的个数),而不是int *
,所以这里一般有两种方式:不进行类型转换、将数组进行强制类型转换成int*
,看下面这个代码
#include
using namespace std;
void func1(int (*a)[4]) //数组指针,数据类型为int(*)[4]
{
a[0][0] = 10; //访问时和一般的数组类似,可以用下标
int i=1, j=2;
*(*a + i*4 +j) = 5; //也可以用指针
}
void func2(int *a) //将二维数组的数据类型强制转换为一级指针,此时该指针指向数组【首元素地址】
{
int i=1, j=1;
*(a+i*4+j) = 7; //取值时只需要用一级星号即可
}
int main()
{
int a[3][4] = {0};
func1(a);
func2((int*)a);
for(int i=0; i<3; i++)
for(int j=0; j<4; j++)
cout << a[i][j] << ' ';
return 0;
}
//输出:
//10 0 0 0 0 7 5 0 0 0 0 0
此外,这里可能会存在一个误区,那就是会考虑将二维数组转换为二维指针,因为数组首地址 = &(数组首元素地址)
,里面还有一级取地址的操作,但实际上这里数组的取地址和一般变量取地址似乎是两个概念,因为这里取完地址,它们的数值仍然是相同的,但是一般的变量取两次地址,实际上是将上一级指针的值作为下一级指针指向的内容,如下所示。
#include
using namespace std;
int main()
{
int a[2] = {0};
int b = 0;
int *p = &b;
cout << a << " " << &a << endl;
cout << p << " " << &p << endl;
return 0;
}
//输出:
//0x61fe18 0x61fe18
//0x61fe14 0x61fe08
因此,从这个例子中可以看出,使用int**
对二维数组进行强制类型转换是不合适的,如果强行将a[2][2]
转换为int**
类型,其内部逻辑是先将数组首地址变成数组首元素地址,然后将首元素的值视为地址继续访问,因此,往往会报内存错误,如Segmentation fault
。除非这个数值恰好在地址范围内,但也会得到奇怪的结果。
在一些场合,常常会需要用到某个变量来定义一个数组的大小,但是C语言规定数组的长度必须是常数。遇到这种情况,就可以考虑使用指针来实现。
#include
using namespace std;
int main()
{
int c; //定义数组的长度
cin >> c;
int *p = new int[c];//一定要注意先输入c再定义数组
...
}
在使用指针时,我认为只需要关注两个内容:指针的取值和指针的步长,其中步长也可以理解为指针指向的数据类型,这决定了指针在移动时跳过多大的内存。
数组再取地址得到的值仍然为该地址,改变的只是指针对应的步长,和一般的变量取地址是不同的。