C语言之指针

一级指针

char *p[4], 指针数组, 由于[]的优先级高于*,所以p先和[]结合,p[]是一个数组,数组中的每个元素是指针。

char (*p)[4],数组指针,强制改变优先级,*先与p结合,使p成为一个指针,这个指针指向一个具有4个char型数据的数组。

char *f(char, char),指针函数,()的优先级高于*,故f是函数,返回值时char *类型。

char (*f)(char, char), 函数指针, *与f结合成为一个指针,这个指针指向函数的入口地址。

二级指针

当一个指针变量指向另一个指针变量时,则形成二级指针。
使用二级指针可以建立在复杂的数据结构时提供较大的灵活性。定义如下:

类型标识符 **二级指针变量名;

二级指针的用处与作用如下:

1.动态申请二维数组。

C++中动态申请一维数组,一般形式为

T *arr_t = new T[N];

也就是说,在堆上分配出N*sizeof(T)的空间,并让arr_t指向这块空间的起始地址。同理,动态申请二维数组的一般形式为:

T **arr_t = new T*[N]; //先申请存储N个T指针的空间for (int i = 0; i < N; ++i) {  arr_t[i] = new T[M];}

这里的arr_t指向的是一个指针数组的起始位置。也就是说,先在堆上分配一块空间,这块空间里面存的全都是指针。然后,在for循环内,对于每个指针,都要在堆上重新开辟一块大小为M*sizeof(T)的空间,这块空间才真正存储我们想要的数据,然后让指针指向这块空间的起始地址。而那块叫做arr_t的内存里,实际上存储的是指针数组的起始位置。

2.用于返回参数。

参考一个MSDN上的例子,假设有一个接口叫IDrawable,同时,有一个叫做Shape的类实现了这个接口。再假设有个图形库里面有这样一个函数:

HRESULT CreateShape(IDrawable** ppShape);

这个函数用于创建一个Shape对象。现在问题来了,创建好的Shape对象总要返回吧?而这个函数又恰好需要返回一个表示成功或失败的、类型为HRESULT的代码,那么创建好的Shape让谁返回呢?显然,需要通过参数ppShape来返回。这个函数的使用场景如下:

IDrawable *pShape;HRESULT hr = CreateShape(&pShape);if (SUCCEEDED(hr)){    // Use the Shape object.}else{    // An error occurred.}

显然,pShape的作用就是指向堆上的某块空间,这个空间里存着一个Shape类型的对象。也就是说,我们之所以要把pShape传给函数CreateShape,就是想改变pShape的当前值(当前值可能为NULL,或者指向其他某个不确定的内存位置),让pShape真正指向堆上的Shape对象。于是,问题转化成怎样真正改变传入函数的参数值的问题。显然,在C++里,传引用或者传指针都能达到这个效果。这里用传指针的方法,如下图所示:

&pShape,即ppShape,是指向pShape所在地址的指针,也就是一个二级指针。这样就可以在CreateShape内部为pShape赋值了:

*ppShape = new Shape; 

于是,pShape真正指向了堆上的Shape对象。
二级指针作为函数参数的作用:在函数外部定义一个指针p,在函数内给指针赋值,函数结束后对指针p生效,那么我们就需要二级指针。

void func(int **p) {    *p = &b;}

二维数组

1、假想中的二维数组布局
我们前面讨论过,数组里面可以存任何数据,除了函数。下面就详细讨论讨论数组里面存数组的情况。Excel 表,我相信大家都见过。我们平时就可以把二维数组假想成一个excel表,比如:
char a[3][4];

2、内存与尺子的对比
实际上内存不是表状的,而是线性的。见过尺子吧?尺子和我们的内存非常相似。一般尺子上最小刻度为毫米,而内存的最小单位为1 个byte。平时我们说32 毫米,是指以零开始偏移32 毫米;平时我们说内存地址为0x0000FF00 也是指从内存零地址开始偏移0x0000FF00 个byte。既然内存是线性的,那二维数组在内存里面肯定也是线性存储的。实际上其内存布局如下图:

以数组下标的方式来访问其中的某个元素:a[i][j]。编译器总是将二维数组看成是一个一维数组,而一维数组的每一个元素又都是一个数组。a[3]这个一维数组的三个元素分别为:

a[0],a[1],a[2]。每个元素的大小为sizeof(a[0]),即sizof(char)*4。由此可以计算出a[0],a[1],a[2]三个元素的首地址分别为& a[0],& a[0]+ 1*sizof(char)*4,& a[0]+ 2*sizof(char)*4。亦即a[i]的首地址为& a[0]+ i*sizof(char)*4。这时候再考虑a[i]里面的内容。就本例而言,a[i]内有4个char 类型的元素,其每个元素的首地址分别为&a[i],&a[i]+1*sizof(char),&a[i]+2*sizof(char)&a[i]+3*sizof(char),即a[i][j]的首地址为&a[i]+j*sizof(char)。再把&a[i]的值用a 表示,得到a[i][j]元素的首地址为:a+ i*sizof(char)*4+ j*sizof(char)。同样,可以换算成以指针的形式表示:*(*(a+i)+j)。

经过上面的讲解,相信你已经掌握了二维数组在内存里面的布局了。下面就看一个题:

#include intmain(int argc,char * argv[]){   int a [3][2]={(0,1),(2,3),(4,5)};   int *p;   p=a [0];   printf("%d",p[0]);}

问打印出来的结果是多少?

很多人都觉得这太简单了,很快就能把答案告诉我:0。不过很可惜,错了。答案应该是1。如果你也认为是0,那你实在应该好好看看这个题。花括号里面嵌套的是小括号,而不是花括号!这里是花括号里面嵌套了逗号表达式!其实这个赋值就相当于
int a [3][2]={ 1, 3,5};
所以,在初始化二维数组的时候一定要注意,别不小心把应该用的花括号写成小括号
了。

3、&p[4][2] - &a[4][2]的值为多少?
上面的问题似乎还比较好理解,下面再看一个例子:
int a[5][5];
int (*p)[4];
p = a;
问&p[4][2] - &a[4][2]的值为多少?

这个问题似乎非常简单,但是几乎没有人答对了。我们可以先写代码测试一下其值,然后分析一下到底是为什么。在Visual C++6.0 里,测试代码如下:

int main(){   int a[5][5];   int (*p)[4];   p = a;   printf("a_ptr=%#p,p_ptr=%#p\n",&a[4][2],&p[4][2]);   printf("%p,%d\n",&p[4][2] - &a[4][2],&p[4][2] - &a[4][2]);   return 0;}

经过测试,可知&p[4][2] - &a[4][2]的值为-4。这到底是为什么呢?下面我们就来分析一下:前面我们讲过,当数组名a 作为右值时,代表的是数组首元素的首地址。这里的a 为二维数组,我们把数组a 看作是包含5 个int 类型元素的一维数组,里面再存储了一个一维数组。

如此,则a 在这里代表的是a[0]的首地址。a+1 表示的是一维数组a 的第二个元素。a[4]表示的是一维数组a 的第5 个元素,而这个元素里又存了一个一维数组。所以&a[4][2]表示的是&a[0][0]+45sizeof(int) + 2*sizeof(int)。

根据定义,p 是指向一个包含4 个元素的数组的指针。也就是说p+1 表示的是指针p 向后移动了一个包含4 个int 类型元素的数组。这里1 的单位是p 所指向的空间,即4sizeof(int)。所以,p[4]相对于p[0]来说是向后移动了4 个包含4 个int 类型元素的数组,即&p[4]表示的是&p[0]+44sizeof(int)。由于p 被初始化为&a[0],那么&p[4][2]表示的是&a[0][0]+44sizeof(int)+2 sizeof(int)。

再由上面的讲述,&p[4][2] 和&a[4][2]的值相差4 个int 类型的元素。现在,上面测试出来的结果也可以理解了吧?其实我们最简单的办法就是画内存布局图:

二维数组与指针的关系

1. 二维数组一维化

其实我这里也只是表述的方便才叫这么一个题目,我们怎么利用一个数组的访问方式来访问二维数组呢?下面来看一个具体的例子。

首先,定义一个二维数组。

int iArr[2][3]={0,1,2,3,4,5};  

我们可以用一个指向int型的指针变量来访问这个数组,下面的代码是将数组一维化:

int* p = iArr[0];  

上面的iArr[0]就是代表第一个数组的首地址,由于二维数组在内存中的存储也是先行后列的方式,所以第二行也紧跟第一行之后,这样就可以用p来访问数组的元素值了,访问的方式有下标和指针方式。

printf("%d,",p[3]);  printf("%d\n",*(p+3));  

最后输出的结果都是3。讲完了一维化之后,下面来继续看二维数组的函数名到底是什么意思?

2. 关于二维数组名的探索

可能想当然的话,二维数组不就是一个二级指针吗?真是这样吗?下面用代码来验证下:

int **pp = iArr;  

不出意外,会出现下面的编译错误:

error C2440: 初始化: 无法从int [2][3]转换为int **

其实二维数组名是一个数组指针,那什么是数组指针?数组指针是指向一个数组首地址的指针,它实际上也是一种指针类型,类似于函数指针。它声明如下:

int (*pArr)[3]  

它说明pArr是一个数组指针,它指向的是一个数组元素为int类型并且数组元素的个数为3的一个数组指针,奇怪,中间的怎么还有一个括号是啥玩意?呵呵,这个括号还真是不可少的。少了它就变为另外一种类型了:指针数组。指针数组是数组类型,代表数组的每一个元素是指针类型,它声明如下:int *pArr[3]。

既然二维数组的数组名是指向第一行数组的首地址,我们也叫它行指针。那么我们可以用这种数组名或者指针来访问二维数组的元素。

int (*pArr)[3] = iArr;  

下面,我要访问第一行第二列的元素,我可以用下面的代码来访问

*(*(pArr+1) + 2)  

也可以用数组名来访问:

*(*(iArr+1) + 2)  

这种方式是不是一下很难看懂,为什么两个星号啊?下面就我的理解来作一下解释。仅以pArr做说明

首先,pArr是一个指向数组的指针,在这个指针上加减一个整数都是移动整行,而不是一个元素。比如说,pArr+1代表的现在指针已经指向第一行元素了,也就是实际中的第二行,而要取得指针所指的对象,就要用到解引用运算符,所以(pArr+1)就代表第一行数组,是整个这一行元素就取到了,那现在要取这一行的第二个元素,只须将指针再移动两个元素,即*(iArr+1) + 2,这样就指向了这个元素的地址,再解引用取得元素的值即可。说的有点啰嗦,或许有错误,望高手别喷就是了。

参考

Was this helpful?

0 / 0

发表回复 0