数组、函数与指针,动态数组(一维二维)探秘,动态数组(一维二维)探秘

  • A+

参考

The C Programming Language-Chapter 5 Pointers and Arrays

前言

在上一篇文章动态数组(一维二维)探秘介绍了数组的一些知识,在最后碰到了一个如何申请二位数组的问题,这篇文章就延伸一下,介绍介绍数组、函数和指针更深层次的关系。

基础知识

int a[10]  一维数组,数组中有连续的十个元素,每个元素都是int类型。

int *p  指针,保存的是一块数据的地址,这块数据是int类型,也就是当程序访问到p指向的地址的时候,需要按照int类型把后面连续的几块数据按照一个整体读取

int v  int类型的数据

p = &v  把v的地址赋值给p,那么现在p指向的就是v的地址

p = &a[0]  把数组第一个元素的地址赋值给p,那么现在p指向的就是数组a第一个元素的地址

int *p到底是什么

按照The C Programming Language中介绍,这个表达式应该看成int (*p),也就是*p是个变量,它是一个int类型,与int v是等价的。*在变量前表示把当前的指针类型解析成它指向的数据类型,那么去掉*,就表示它是一个指针。

进一步说,就是,p是一个指针,*的作用是把p(指针)解析成它指向的数据,*p就是p指向的数据,类型是int,也就是我们说的p是一个指向int类型的指针

如果这样理解的话,下面这条声明变量的语句就很好理解了

int *p, v;

由于运算符优先级的关系,这个等价于

int *p; int v;

*p和v是等价的。这条语句相当于声明了两个int变量,*p和v。v无法进一步解析,所以v就是一个int变量;*p可以进一步解析,把*去掉,那么p就是一个指针,也就是指向int类型变量的指针。

int a[10]中的a又如何理解

由The C Programming Language中我们可以看到,a指向了a[10]数组开始的地址,与&a[0](数组第一个元素的地址)是一样的,如下图

数组、函数与指针,动态数组(一维二维)探秘,动态数组(一维二维)探秘

设置p等于a或是a[0]的地址,这样p和a的作用就一样了。只不过a是一个常量,p是一个变量,a不能被赋值或是做加减乘除的运算,p可以

p = a;
//或者
p = &a[0];

数组、函数与指针,动态数组(一维二维)探秘,动态数组(一维二维)探秘

数组与指针有着密切联系,数组可以看做指针指向了一块连续的内存区域,实际上也确实如此。如下图

数组、函数与指针,动态数组(一维二维)探秘,动态数组(一维二维)探秘

a[0]就是*(a+0)也就是*(pa+0),从这里就可以理解为什么c/c++的索引是从0开始。

在c/c++中,对指针做加一的操作,相当于把指针移动到下一个数据的位置,移动多少,取决于指针指向的类型是什么,跨度就是这个类型占用的空间。

比如上图,不管a是什么类型的数组,a+1或是p+1,移动的距离就是a数组中元素占用内存的字节数。比如a中是char(占用一个字节),a指向0x0001,那么a+1就指向0x0002;如果是int(占用四个字节),a指向0x0001,a+1就指向0x0005。

再谈二维数组

如下图数组a是一个二维数组,相当于一个数组指针的数组,这个数组有2个元素,每个元素又指向了一个一维数组,每个一维数组的元素个数是5

数组、函数与指针,动态数组(一维二维)探秘,动态数组(一维二维)探秘

那么二维数组的指针是什么呢?我们知道一维数组相当于指针,那么二维数组相当于数组中保存了一维数组,也就是数组中保存了指针,也就是指针数组,那么二维数组就相当于指针的指针。

但是当我们编译下面的代码的时候,会提示error C2440: 'initializing': cannot convert from 'char [2][5]' to 'char **',鼠标放在出错的地方提示a value of type "char (*)[5]" cannot be used to initialize an entity of type "char **"

char a[2][5];
char **p = a;

这是为什么呢?实际上二维数组,或是多维数组的指针不是这样定义的,必须要指定这个数组指针数组中每个元素是什么样的,如下才是正常的

char a[2][5];
char (*p)[5] = a;

实际上我们可以用一个指针操作二维数组,也可以用指针的指针操作二维数组,只需要强转就行。不管是几维的数组,在内存中都是连续的,我们只需指向数组的开始位置,一个个访问就可以了。

char a1[2]  char * a2[2]  char (*a3)[5]  char a4[2][5]傻傻分不清楚

动态数组(一维二维)探秘 中我们碰到了一个问题,就是,从内存中看,申请一个一维数组和二维数组一样,都是一块连续的空间,但是如果用一维数组的方式申请一块连续的空间,我们用指针加1,发现它并不会像真正的二维数组一样,向前跳动一排的元素空间大小,还是跳动一个元素大小。如下示例

char a[2][5];
char *pa = new char[2*5]();

a的地址是0x004CFE00

a+1的地址是0x004CFE05

pa的地址是0x00227D68

pa+1的地址是0x00227D68

这是为什么呢?很明显,pa不管怎么操作,它只是一个char的指针,那么按照c语言的规则,每次跳动只能是一个char的大小,也就是1;而a是一个char [5]的指针,那么每次跳转就是char[5]的大小,也就是5.

上面的4个,有一个是特殊的,就是char (*a3)[5]。其他的都是数组,这个是指针:

  • char a1[2]是一个一维数组,里面有两个char元素
  • char * a2[2]是一个数组,里面有两个char*元素
  • char a4[2][5]是一个二维数组,里面有2*5个char元素,或是说里面有2个char[5]
  • 而char (*a3)[5]是一个指针,指向的类型是char[5]。我们可以用上面的方式拆分一下,char (*a3)[5]是一个数组,里面保存的是有5个char的连续数据,*a3就是这个数组,去掉*,那么a3就是一个指针,指向的是一个char [5]

char a1[5] char a2[2][5] char (*a3)[5]有什么联系

char a1[5]和char a2[2][5]的指针形式都是char (*a3)[5],这就是它们之间的联系。可能看上去有点懵,实际上这个与char b1和char b2[5]的指针形式都是char * b3是一样的。

我们知道char b1和char b2[5]的指针形式都是char * b3,如果b3=b1,那么b3就指向了b1的地址;如果b3=b2,那么b3就指向b2第一个元素的地址,b3++就可以访问b2的第二个元素。

在c语言中,没有越界检测,这即提供了方便,也增加了风险。c语言中最危险的陷阱之一就是越界,也就是野指针。从逻辑或是实现上来说,这又是c语言中的精髓,底层逻辑简单,应用执行速度快,不需要考虑任何额外的操作。如果是指针,那么指针加一,就是跳转到下一块数据,也就是把当前指向的这块数据跳过去。

同样a3=a1,就是指向了一块数据,这个数据类型是一个char[5]。如果是多个char[5]呢?比如a3=a2,那就是一个一行5个元素或是说5列的二维数组了。a3++,就是跳转5个元素的大小,这样就可以直接用a3[1][2]的方式访问了,这就是二维数组。

函数与指针

在c语言中,函数并不是一个变量,但是可以定义成一个指针,允许被赋值、传递等。

int fun1(int *a, int *b);

int * fun2(int *a, int *b);

int (*fun3)(int *a, int *b);

int* (*fun4)(int *a, int *b);
  • fun1是一个函数,函数的返回值是int,函数有两个参数,每个参数都是int指针
  • fun2是一个函数,函数的返回值是int指针,函数有两个参数,每个参数都是int指针
  • fun3是一个指针,指针的类型是一个函数,这个函数的返回值是int,函数有两个参数,每个参数都是int指针
  • fun4是一个指针,指针的类型是一个函数,这个函数的返回值是int指针,函数有两个参数,每个参数都是int指针

我们可以看出fun3就是fun1的指针,fun4就是fun2的指针。

fun3 = fun1;
fun4 = fun2;

做了以上赋值后,调用fun3就相当于调用fun1,同理调用fun4就相当于调用fun2。调用方法如下

int a = 1;
int b = 2;
int ret = 0;
int *pret = nullptr;
ret = fun1(&a, &b);
ret = (*fun3)(&a, &b);
pret = fun2(&a, &b);
pret = (*fun4)(&a, &b);

在这里我们看到好多用法定义都与数组和数组的指针类似。同样fun3与fun1的区别,fun1和fun2是常量,不可以修改赋值,而fun3和fun4可以。

函数指针强转

虽然这是一个小知识点,但是可以帮助我们进一步了解指针,比如我们有一个函数需要传入函数指针,参数是void指针,需要我们把int指针参数的函数强转传入

int testcomp(int a, int b)
{
    return a;
}
int testcomp1(long a, long b)
{
    return b;
}
void test(int (*comp)(int a, int b))
{
    int a = (*comp)(111, 222);
    cout << a;
} test(testcomp); test((
int(*)(int, int))(testcomp1));

这里仅仅是为了测试说明,从long转到int是被禁止的,防止溢出。

我们看到test是一个函数,函数的参数是一个函数指针,在c语言中想要传递函数,也只能通过指针的方式。这个函数指针返回值是一个int,有两个int参数。

第一个调用就是把testcomp传递进去,函数形式与声明的一致,所以不需要强转

第二个调用,testcomp1与test定义的传入的参数不一致,需要转化一下,这里就可以看出来函数的指针形式如何定义

int(*)(int, int)这就是定义了一个函数指针的形式,int是返回值,(*)表示是一个指针,(int, int)表示传入的参数是两个int

令人头晕的各种指针的声明

在The C Programming Language[5.12 Complicated Declarations]中也介绍了,c语言有关指针的声明,有时候非常迷惑,它并不能从左向右按照顺序的解析,往往是从中间一个地方开始,向左右扩展,还要时不时的把一堆表达式看做一个整体。

char *a-char的指针

char **a-指向char指针的指针

char (*a)[13]-指向数组的指针,这个数组是包含13个char类型的数组

char *a[13]-含有13个char指针的数组

char (*a)()-函数指针,这个函数的返回值是char,传入的参数是空

char (*(*x())[2])()-这是一个函数,函数的返回值是一个指向数组的指针,这个数组中包含的是一个函数指针,这个函数模型是返回char,传入参数是空

char(*(*pa)[2])()-这就是上面函数返回值,这是一个数组指针,数组中有两个元素,这个元素是一个函数指针,函数的模型是返回char,传入参数是空

char (*(*x[3])())[5]-这是一个数组,数组中有3个元素,每个元素是一个函数指针,函数的模型是传入参数是空,返回值是一个数组的指针,这个数组有5个元素,每个元素是char

再来一个

https://www.nowcoder.com/questionTerminal/87bba673cc844677baa0c12d32bdc330

int (*p[10])(int*)-这是一个数组,数组有10个元素,每个元素是一个函数指针,这个函数返回值是int,传入参数是int指针

终极解释

我们通过资料和示例可以总结一下关于指针或是c语言中定义类型的时候是如何拆分的了,首先与变量名最近的符号,表明了这个变量的类型,然后一层层向外增加额外的解释,我们就一个个举例实验一下

int a-这里的a是变量名,从这里开始,向两边查找,只有int,那么a就是一个int

int *a-从a开始,有*,表示是一个指针,指针的原形呢?就是int,所以a是一个int指针

int a[10]-从a开始,a右边是一个[],表示是一个数组,数组中有10个元素,元素的类型呢?把a[10]看做整体,就是int,所以a就是含有10个int元素的数组

int *a[10]-从a开始,a右边是一个[],所以a是一个数组,数组中有10个元素,元素的类型呢?把a[10]看做整体,比如XX,那么就变成了int * XX,就与上面的int *a一样了。有*,所以元素是指针,指针的类型是int,所以a就是包含10个int指针的数组

int (*a)[10]-从a开始,因为被()限制,所以a与*结合,那么a是一个指针,指针的类型呢?把(*a)看做一个整体,那么就是int[10],指针的类型就是一个数组,这个数组有10个元素,每个元素是int,所以a就是含有10个int元素数组的指针

int a[2][5]-从a开始,a的右边是[][],所以a是一个二维数组,数组元素是int

int f()-从f开始,f右边是(),所以f是函数,函数参数是空,返回值是int

int *f()-从f开始,f右边是(),所以f是函数,函数的参数是空,返回值是int指针

int (*f)()-从f开始,因为被()限制,所以f与*结合,那么f是指针,指针的类型呢?把(*f)看做整体,那么就是一个函数,所以指针的类型是函数,函数的参数是空,返回值是int,所以f是一个指向参数是空返回值是int的函数的指针

char (*(*x())[2])()-从x开始,x右边有(),所以x是函数。那么把x()看做整体,由于()限制,x()与*结合,表示函数返回值是一个指针。再把(*x())看做整体,与右边的[2]结合,表示这是一个含有2个元素的数组,到这里的解释是,x是一个返回值是一个数组指针的函数。再把(*x())[2]看做整体,前面又有一个*,表示这是一个指针,到这里的解释是,x是一个返回值是一个数组指针的函数,数组中的元素是指针。再把(*(*x())[2])看做整体,就很明显了,这是一个函数,类似于char XX(),函数的返回值是char,参数是空,最终这个表达式的意思是,x是一个函数,函数的返回值是一个数组指针,数组中有2个元素,元素的类型是一个函数指针,函数的返回值是char,参数是空。

数组、函数与指针,动态数组(一维二维)探秘,动态数组(一维二维)探秘

char (*(*x[3])())[5]-从x开始,x右边是[],所以x是数组。数组前面又*,所以数组元素是指针。指针后面有(),所以是函数指针。函数指针前面有*,函数返回值是指针。返回值指针后面有[5],所以函数返回值指针是数组。最后前面是char,所以x是一个含有3个元素的数组,数组的元素是一个函数指针,函数的参数是空,返回值是一个char[5]的数组指针。

数组、函数与指针,动态数组(一维二维)探秘,动态数组(一维二维)探秘

int (*p[10])(int*)-p是数组,数组中的元素是指针,指针的类型是函数,函数的模型是返回值是int,参数是int*

数组、函数与指针,动态数组(一维二维)探秘,动态数组(一维二维)探秘

 

90DIR-CMD