【C++】数组和指针的爱恨情仇。。。-创新互联

前言

  最近研究C++中的数组怎么作为参数传入到函数中,自然而然引出了这篇博客的标题,即数组和指针的爱恨情仇。。。

10年积累的成都做网站、网站设计经验,可以快速应对客户对网站的新想法和需求。提供各种问题对应的解决方案。让选择我们的客户得到更好、更有力的网络服务。我虽然不认识你,你也不认识我。但先网站制作后付款的网站建设流程,更有山亭免费网站建设让你可以放心的选择与我们合作。1 数组和指针都是啥?

  想要知道数组和指针交织在一起会摩擦出怎样的火花,那就先要了解数组和指针各自的语法和特点。这里不想写一些非常专业的定义(咱也没看),就是针对一些常用的用法做一个直观的认识。
  首先是数组,实际上就是一些相同数据类型的变量放在一起,就构成了一个数组,这里的重点是“一起”,即它们的存储地址是连续的(这也是为什么可以通过指针来依次访问)。此外,就是在定义数组时一定要用常量指定一个数组的大小,即包含多少个元素。
  其次是指针,一般常用的语法为int *p = NULL,表示定义一个指针变量(指针变量一定要养成定义即初始化的好习惯!),它存储的内容是一个int类型变量对应的地址,那如果直接输出这个地址(即指针变量的值)呢?一般会得到一个比较大的数,它对应的内存中的某个位置:

#includeusing namespace std;
int main()
{int a = 10;
	int *p = &a;
	cout<< "p="<< p<< endl<< "*p="<< *p;
	return 0;
}
//输出:
//p=0x61fe14
//*p=10

  此外,最为重要的是,指针指向的数据类型其实就决定了这个指针的 “步长”,即运行p++++p后它移动的内存大小。举个例子:

#includeusing 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位系统?)
2 用指针访问数组

  一般定义好一个数组后,最常用的访问方式是下标访问数组中的元素,但这种方式要保证有整个数组,才能够通过下标来访问,而如果用指针来访问,只需要一个指针变量即可(顶多加上一个数组长度),不管是作为返回值还是参数都十分方便,效率更高。
  首先说一个众所周知的知识点:一维数组的名称实际上就是一个指针变量,指向的是数组首元素地址。

#includeusing 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

需要注意,用*访问和用[]访问是等价的。

  那么问题来了,一维数组的数组名是一个指针,那二维数组的数组名是不是指针的指针(二级指针)呢?来看一下试验结果:

#includeusing 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],这是啥意思呢?埋个伏笔,请看下文分解。

3 数组指针和指针数组

  提到数组和指针,就不得不提二者的结合体:数组指针和指针数组了,复杂度一下子上了一个台阶,堪称1+1>2。

事先说明,这里探讨的数组指针和指针数组,其“数组”都是指一维数组,但其实更高维的数组也可以类推。

3.1 基本概念

  首先是区分这两者的概念,最简单的方式就是看后两个字——到底是指针还是数组。
  所谓指针数组,首先是一个数组,其次是这个数组的每个元素都是一个指针,都占8个字节。
  所谓数组指针,首先是一个指针,其次是这个指针指向的内容是一个数组,注意:这里的指针指向的是数组首地址,不是数组首元素地址,这两个地址值是相同的(即指针变量的取值),但是当赋予同一个指针时,各自的步长不同,具体可以看一下下面这个例子。

#includeusing 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个字节

  可以发现,对于指向数组首元素地址的指针,它的步长就是一个数组元素所占字节大小;而对于指向数组首地址的数组指针,它的步长是整个数组所占字节大小,而且这两者之间还存在一个关系,即数组首地址 = &(数组首元素地址)或者数组首元素地址 = *(数组首地址),记住这个表达式,后面要考。

3.2 语法

  了解完了基本概念,再来看看其基本语法有什么区别:

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是一个指针【√】

3.3 应用

  学会了基本语法,再来看看怎么应用。
  对于指针数组,没啥好说的,可以把它视为一般数组进行赋值或通过下标取值,但要注意,取出来的是指针,如果要取指针指向的数值,还要加上星号。下面以一个访问二维数组的例子来说明:

#includeusing 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行的首地址。那么问题来了?数组指针指向数组首地址,它的步长是一行?那岂不是不能访问一行的某个元素了?好像有点道理,但是还记得上面提到的那个公式吗?数组首元素地址 = *数组首地址,可以先取一个*来得到每一行首元素地址,再访问数组中的每个元素。然后,由于使用*[]是等价的,所以很多时候数组指针的使用和二维数组差不多。同样看一个访问二维数组的例子。

#includeusing 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]且可以进行强制类型转换,那能不能不按照数组的每一行为步长来进行访问呢?答案是可以的,看下面这个例子。

#includeusing 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),这个关键还是在于不管是一维数组还是二维数组,它们的存储空间都是连续的,所以能够使用指针来访问。

4 数组作为参数传入函数

  最后,让我们再回到最开始的目标:数组作为参数传入函数。这里有两种常用的方式:即以数组形式或以指针形式。

4.1 数组形式

  数组形式简单明了,比较好用。
  对于一维数组,常用void func(int a[]),来传数组参数,在访问数组元素时直接使用a[i]来访问,因为是传入的地址,所以数组内容会被修改,如果是全局变量,可以不用返回;
  对于二维数组,常用void func(int a[][4])来传递参数,注意,第一个括号为空,第二个括号必须是一个常数,即数组每一行的元素个数得确定,在访问数组元素时直接通过a[i][j]来访问,同样这个数组也会被修改。

4.2 指针形式

  对于一维数组,前面我们提到,数组名本质上就是一个指针,因此可以把指针作为参数或返回值应用到函数中。比如void func(int *a),这种比较直观,理解起来没有什么难度。
  对于二维数组,可能有点小复杂,前面我们提到,二维数组的数组名本质上是一个数组指针,它对应的步长为二维数组的一行,同时它指向的是一行的数组首地址(而不是每一行数组首元素地址),它的数据类型是int(*)[2](2为每一行数据的个数),而不是int *,所以这里一般有两种方式:不进行类型转换、将数组进行强制类型转换成int*,看下面这个代码

#includeusing 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

  此外,这里可能会存在一个误区,那就是会考虑将二维数组转换为二维指针,因为数组首地址 = &(数组首元素地址),里面还有一级取地址的操作,但实际上这里数组的取地址和一般变量取地址似乎是两个概念,因为这里取完地址,它们的数值仍然是相同的,但是一般的变量取两次地址,实际上是将上一级指针的值作为下一级指针指向的内容,如下所示。

#includeusing 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。除非这个数值恰好在地址范围内,但也会得到奇怪的结果。

总结

  在使用指针时,我认为只需要关注两个内容:指针的取值和指针的步长,其中步长也可以理解为指针指向的数据类型,这决定了指针在移动时跳过多大的内存。
  数组再取地址得到的值仍然为该地址,改变的只是指针对应的步长,和一般的变量取地址是不同的。

你是否还在寻找稳定的海外服务器提供商?创新互联www.cdcxhl.cn海外机房具备T级流量清洗系统配攻击溯源,准确流量调度确保服务器高可用性,企业级服务器适合批量采购,新人活动首月15元起,快前往官网查看详情吧


名称栏目:【C++】数组和指针的爱恨情仇。。。-创新互联
文章网址:http://azwzsj.com/article/ecgeg.html