1
七、指向数组的指针八、二级指针九、指针数组十,void 关键字与 void *型的指针
2
七、指向数组的指针
1.指向数组的指针的定义和性质对于第二个维数相同的二维数组如 {int w[3][5],e[6][5];},
w,e共性地具有类型属性 int(*)[5],第一个维数 3,6协同确定相应数组的总的内存空间,对于地址的映射计算并不起作用。通过引进指向列数为 5的二维数组的指针 x:
int (*x)[5];
x可以分别指向 w,e。
例如,x=w; x=e+2;
指向数组的指针简称为数组指针,一般地指向二维数组
type d[r][c]的指针定义格式为:
type (*q) [c];
类型 (*数组指针名 ) [整型常数 ];
3
如上定义的指针 q为 type(*)[c] 类型的指针,是指向列数为 c的二维数组的指针。
数组指针 q是一个指针,因此仅占一个指针对应的存储空间,其中 c是静态的整型常数,用于界定数组指针的步长增量 m = sizeof (type)*c。
一经定义数组指针 q,则表达式 q[ i ]等价于 (*(q+i))为
type*型右值,这个右值是自动计算出来的,不占数据区内存。
表达式 q[ i ][ j ]等价于 (*(*(q+i)+j))为 type型左值,左值的内存应预先由数组定义语句分配。 编译器不检查 q[ i ][ j ]
是否初始化,也不管是否越界。
4
如果 q=d,则可以等价地通过指针名 q和二维数组名 d来索引二维数组。此时关系表达式 q[ i ]==d [ i ]为真。
在 q=d期间 q [ i ][ j ]等价于 d [ i ][ j ],即对 q [ i ][ j ]的操作就是对 d [ i ][ j ]的等价操作,如果 q是形参,则形参在函数中通过 q [ i ][ j ]或 **q等方式直接操作相应的实参数组。
令 n = sizeof (type),m=n*c,q=d的地址为 xxxx,则
q+j的地址值为 yyyy= xxxx+j*m。
type(*)[c]型指针的位移步长增量为 m。 q+j的地址属性为 type(*)[c]。
5
d [ j ]和 q [ j ]构成 type*型的右值,其步长增量为 n。
q[ j ]的地址为 yyyy,则 q[ j ]+k的地址值为 yyyy+kn,
即指向第 j+1行的第 k+1个元素,也就是指向 q[ j ][ k ],即关系式 q[ j ]+k==&q[ j ][ k ]为真。
q[ j ]或 (*(q+j))与 q+j具有相同的地址值但地址类型属性不同。
这里访问指针运算 (*(q+j))或 q[j]表示降维处理即将
type(*)[c]型的地址降为 type*型的右值地址和自动的寻址计算而不表示直接索引内存数据,只有左值才访问操作内存数据。
d+j表示指向第 j+1行,d[ j ]是第 j+1行的首地址,两者都指向同一内存位置但地址属性不同。
6
二维数组行地址
d [ 0 ]
d [ 1 ]
d [ 2 ]

d [ j ]

d [ r-1 ]
行地址的值
x x x x
x x x x+1m
x x x x+2m

x x x x+jm

x x x x+r m-m
二维数组 type d[r][c]某行的首地址与数组指针的关系
q=d+1;
q+= j-1;
数组指针指向二维数组每行的首地址,赋值一次指针用加运算数组指针向后移动 j-1个位置
m=sizeof (type[c])=nc
7
赋值语句 { q=d; q+= j;}导致 q的值为 xxxx+jm,此时表达式 q[0][k]等价于 d[j][k],**q= q[0][0]=d[j][0]。
d[0][0]总是索引数组 d的第 1个元素,q[0][0]未必。 q++
向前移动一个位移步长即 q从当前位置向后移动 m个字节,
也就是值向下一行的内存地址。
指针只能用相同属性的地址赋值,当指针不平级时,需要进行指针类型转换。语句:
double d[3][5]; double (*q)[5]=d+1;
double *p=q[0];
定义数组指针 q,初数化指向二维数组 d的第 2行,*q和
d[1]为 double *型的右值地址。 d,q具有 double(*) [5] 类型属性,d=q是错误的,d为右值,q为左值。
q- = 1;
//向前移动 1个 double[5]类型的步长,即前移 5*8*1=40个字节,
8
语句 {double(*r)[2];}定义 double(*)[2]型的数组指针
r,r与 q类型属性不同,它们之间不能相互赋值。 r=d,q=r是错误的,等号两边的类型属性不同。 r[ j ][ i ]=d[ i ][ j ]是可以的。 r[ i ],q[ j ]都是 double型的右值地址,r[ i ]=q[ j ]是错误的,而 p = r[ i ],p = q[ j ]是正确的。
对于最后两个维数相同的三维数组如,
{int s[2][4][3],h[5][4][3];},s,h共性地具有类型属性
int (*)[4][3],第一个维数 2,5对于地址的映射不起作用。
引进指向三维数组的指针 q:
int (*q)[4][3];
q可以方便地操纵 s,h。 例如,q=s;q=h+1;
9
设 m,r,c时静态的整型常数,一般地指向三维数组 type
s[ m ][ r ][ c ]的指针定义格式为:
type (*q)[r] [c];
类型 (*数组指针名 ) [常数 2][常数 3];
q的类型属性为 type (*)[r][c],如果 q=s,则可以等价地通过指针 q和数组名 s来索引三维数组。此时关系表达式
q[ i ][ j ][ k ]==s[ i ][ j ][ k ],q[ i ][ j ]==s[ i ][ j ]
q[ i ]==s[ i ] 为真。
下面的关系是等价的,访问指针形式明显缺乏可读性且各级表达式的类型属性含糊,也不容易用键盘输入。因此程序设计中不宜用右边的访问指针形式:
s[ i ][ j ][ k ] == (*(*(*( s+i )+j)+k))
10
[例 ]同一个地址对应不同类型的地址属性
#include <stdio.h>
void main()
{ int b[3][2][2];
//int (*p)[2][2]=b;int (*q)[2]=b[0];int * r=b[0][0];
printf("%p,%p,%p\t",b,b[0],b[0][0]);
} //输出,0066FDC8,0066FDC8,0066FDC8
说明,{int b[3][2][2];}中的 b具有地址属性 int (*)[2][2],
b[0]具有地址属性 int (*) [2],b[0][0]具有地址属性 int *,它们都具有相同的地址值。 b+i,b[i]和 b[i][0]也具有相同的值。
地址值相同地址类型属性不同的现象表示同一片内存可用不同属性的指针寻址访问。
11
2.数组指针形参和二维数组形参
c是静态的整型常数。下面是两个本质一致外在格式不同的函数原型:
void f (int (*q)[c],...); void f (int q[ ][c],...);
形如,int ( *q )[ c ]”的形参为数组指针形参,形如
" int q[ ][c]"的形参为二维数组形参。
两者都视为 int (*)[c]型的指向二维数组的指针,二维数组共性地具有固定列数 c。
函数定义 void f (int (*q) [ c ],...) {;...;}可等价地改为,
void f (int q[ ][ c ],...){;...;} 。
12
[例 ]指向二维数组的指针与二维数组
# include<iostream.h>
void f (float (*q)[4],int m)
{ for (int k=0;k<m;k++)
{ float* p=q[ k ];
for( int j=0;j<4;j++)
cout<< p[ j ]<<"," ;
}
}
void main (void)
{ float d[ ][4]={0,1,2,3,4,5,6,7};
int const M=sizeof (d)/sizeof (*d);
f (d,M);
f (d+1,M-1);
} //输出,0,1,2,3,4,5,6,7,4,5,6,7,
13
[例 ]数组指针形参或二维数组形参实现求数组空间的和
#include<stdio.h>
double sum (double q[ ][3],int n);
void main (void)
{ double b[2][3]={1,2,3,4,5,6};
printf ("%2.0f,%2.0f\n",sum (b,2),sum (b+1,1));
double a[ ]={1,2,3,4,5,6,7};
typedef double (*QA)[3];
printf("%2.0f,%2.0f\n",sum((QA)a,2),sum((QA)(a+4),1));
}
14
double sum (double (*q)[3],int n)
{ double s=0 ; int i,j; double *p;
for ( i=0; i<n; i++,q++)
for (j=0,p=*q; j< 3; j++,p++)
s+=*p;
return s;
}
double sum1(double (*q)[3],int n)
{ double s=0 ;
for (int i=0;i<n;i++) for (int j=0; j<3;j++) s+=q[ i ][ j ];
return s;
}
15
typedef简化数组定义数组指针的定义语句 [int (*p)[N];]定义了一个指针变量
p,定义语句 [int d[M][N];]定义了一个二维数组 d,可以通过
typedef类型声明语句来建立一个数组类的别名,其格式为:
typedef int (*PN)[N];
// PN是 int (*)[N]类型的别名
typedef int AMN[M][N];
// AMN是 int[M][N]类型的别名
typedef int AN[N];
// AN是 int[N]类型的别名,N,M是静定的整数。
16
去掉上面的 typedef就得到数组定义语句。可用类型别名定义数组或指针。
PN p,q,r;
//定义指针变量 p,q,r都拥有数据类型 int (*)[N]
AN a,b;
//定义数组 a,b其类型属性为 int[N],等价于 int a[N],b[N];
AMN c,d;
//相当于定义二维数组 int c[M][N],d[M][N];
17
# include <iostream.h>
const int L=2,M=2,N=2;
typedef int ALMN[ L][ M ][ N ];
typedef int (*PMN)[M][N]; typedef int (*PN)[N];
void main (void)
{ int j,k;
ALMN v= {1,2,3,4,5,6,7,8};
PMN d=v; PN a= v[1];
for (j=0;j<M;j++) for (k=0;k<N; k++)
cout<<d[1][j][k]<<"," ;
for (j=0;j<M;j++) for(k = 0; k <N; k++)
cout<<a[j][k]<<";" ;
} ///输出结果,5,6,7,8,5 ;6 ;7 ;8;
18
八、二级指针二级指针的定义格式为:
type** pp1,**pp2,…,**ppn;
类型 ** 指针名 1,**指针名 2,…,** 指针名 n;
二级指针初始化定义语句的格式为:
type** pp = ptrExpression;
类型 ** 指针名 =指针表达式;
指针表达式是与所定义的指针同类型的地址值。如上定义的指针 pp,pp1,pp2,ppn等抽象地称为 type**类型的指针或二级指针。二级指针的地址对应一个三级指针,基本上不使用三级指针。一级指针的地址是右值,二级指针是一种存放一级指针的地址数据的特殊类型变量,简单地说二级指针是一级指针的地址的变量。
19
二级指针具有下面的特性:
1.二级指针的类型属性,二级指针的类型是其所指向地址变量的类型,二级指针的类型属性限定二级指针的增减以指针类型步长 sizeof(type*)=4或 2为单位。二级指针可在一级指针构成的数组空间中移动。 pp++表示向后移动 4或 2个字节。
2.一经定义二级指针 pp,则表达式 pp[ I ]等价于 (*(pp+i))
为 type*型左值;表达式 pp[ I ][ j ]等价于 (*(*(pp+i)+j))为
type型左值,编译器不负责它们的内存分配,不检查它们是否初始化,也不管是否越界。
20
pp[ j ]或 (*(pp+j))与 pp+j的地址值不同地址属性也不同,
这里访问指针运算 (*(pp+j))或 pp[ j ]既表示降维处理也表示直接索引 pp+j指向的内存数据。 pp+j的地址值自动根据关系式 (char*)pp+j*sizeof (type*)计算,而 pp[ j ]的值由用户确定,
当一级指针指向单一的变量或二级指针指向单一的一级指针时,对于指针的加减运算都是越界行为,应予以避免。
double c,b;
double *p,**pp;
p=&b; pp= &p; *p=5.0; p=&c;
**pp=2.0; //等价于 **pp?*(*(&p))?*(p)?*(&c)?c,c=2.0
2.0&p &c
pp cp
21
九、指针数组
1.指针数组的定义和性质
type*型的指针数组的定义格式为:
type *pa[N]; 类型 * 指针数组名 [数组长度 ];
指针数组具有一维数组的一般性质,占有 N个指针所需的内存空间,而其特点如下:
a,pa为指针数组名,本身代表指针数组的首地址,此地址是 type**型的右值地址。
sizeof (pa)=sizeof (type*[N])= N *sizeof (type*)。
N= sizeof(pa)/ sizeof(pa[0])
22
b.指针数组的每一个元素 pa[i]是 type*型的左值,pa [i]
等价于 (*(pa+i));表达式 pa[ I ][ j ]等价于 (*(*(pa+i)+j))为
type型左值,pa[ I ]的作用相当于一级指针 p的作用,pa+I
指向元素 pa[ I ],pa[ I ]+j或 *(pa+i)+j 指向变量 pa[i][j].指针数组的元素的初始化可由赋值语句进行,也可根据下面的格式进行:
type *pa[ N ] ={initialList};
类型 *指针数组名 [ ]={初始化地址列表 };
初始化地址列表的每一个表达式应是 type*型的地址。
对于 type x,a[N],d[r][c];,如果指针数组元素 pa[ i ]=a,则
pa[ i ][ j ]索引一维数组元素 a[ j ]。如果指针数组元素
pa[ I ]=d[ k ],则 pa[ I ][ j ]索引二维数组元素 d[ k ][ j ]。如果指针数组元素 pa[ k ]=&x,则 *pa[ k ]索引变量 x。
23
例如:
double d[ 3 ][ 2 ]={1,2,{3,4},5,6};
double* y[3]={d[2],d[1],d[0]};
相当于,y[0] =d[2]; y[1] = d[1]; y[2] = d[0];
此时有,y[0][0]=d[2][0]=5 ; y[1][0]=d[1][0]=3 ;
y[2][0]=d[0][0]=1 ; y[0][1]=d[2][1]=6 ;
y[1][1]=d[1][1]=4 ; y[2][1]=d[0][1]=2 ;
指针数组的相邻元素指向的数据不必具有相邻的关系 ;
指针数组的元素可指向生存期稳定的全局变量、静态变量也可指向生存期瞬态变化的局部变量,尚可指向生存期由程序员控制的堆空间。
但应保证指针指向的数据生存期对于指针具有可操作性,或者说指针操作的内存数据的生存期在指针访问时是有效的,是存在的。
24
考虑,typedef char type ;
type t1[7],t2[6],t3[5],t4[4],t5[3],t6[2],t7[1];
type *pa[ ]={t1,t2,t3,t4,t5,t6,t7};
type **pp=pa;
pp+=j;
pp=pa;
指针数组元素
pa[ 0 ]
pa[ 1 ]
pa[ 2 ]

pa[ j ]

pa[N -1]
t1的元素
t1[ 0 ]
t[ 1 ]
t1[ 2 ]

t1[ 4 ]

t1[ 6 ]
指针的初值
t1
t2
t3

t5

t7
图 指针数组的值与二级指针
25
[例 ] 指针数组的元素指向维数大小不同的一维数组
# include<stdio.h>
void show (long * a,int n)
{ for (int k=0;k<n;k++) printf ("%d ",a[ k ]);
printf("-");
}
long t1 [ 7 ]={1,2,3,4,5,6,7};
void main()
{ long t4[ 4 ]={1,2,3,4}; long *pa[ 3 ]={t4,t1};
long ** pp=pa+1;
show (pa[0],4); show (pp[0-1],4);
show (pa[1],7); show (pp[1-1],7);
}//输出结果,1 2 3 4 -1 2 3 4 -1 2 3 4 5 6 7 -1 2 3 4 5 6 7 -
26
2.二级指针形参和指针数组形参,
如下是两个本质一致外在格式不同的函数原型:
int f (int **pp,...); int f (int *pp[],...);
形如 "int**pp"的形参为二级指针形参,形如 "int*pp[]“
的形参为指针数组形参,两者都视为 int**型的表达式。指针数组形参要求实参指针数组的内存分配,每一元素的初始化和相应元素指向的数据的内存分配。二级指针形参表明定位内存地址的基准特性。
函数定义 int f (int *pp[ ],...){;...;}等价于函数定义
int f (int **pp,...) {;函数体代码不变 ;}
引入一级指针用于访问变量或数组元素,引入指针数组用于通过访问指针元素,最终访问指针元素指向的变量。
27
二级指针 pp=pa,pp[ k ]等价于 pa[ k ],对 pp[ k ]的操作就是对一级指针 pa[k] 操作 ;如果 pp是形参,则形参在函数中通过 pp [ i ]或 *pp等方式直接操作相应的实参指针数组空间,
通过 pp[ i ][ j ]或 **pp等方式操作相应的实参变量数组空间,
[例 ]二级指针显示指针数组的元素指向的变量的值
#include <stdio.h>
void f (int**pp,int n)
{ for (int i=0;i<n;i++) printf ("%d,",**pp++); }
void main()
{ int x=1,y[ ]={2,3},u[ ][1]={4,5};
int * px [ ]={&x,&y[0],y+1,u[0],&u[1][0]};
f (px,5); f (px+4,5-4);
for (int** pp=px,i=0; i<5; i++,pp++)
printf ("%d,",**pp); }
28
[例 ]二级指针或指针数组作为形参实现求数组的和
#include<stdio.h>
double sum (double** pp,int n,int m=3);
double a[ ] = {2,3,4};
void main (void)
{ double b[2][3] = {1,2,3,4,5,0};
double* x[ ]= {b[1],b[0],a};
const char*fmt = "%2.0f,%2.0f,%2.0f ";
printf (fmt,sum (x,2),sum (x+1,1),sum (x+2,1));
}
29
double sum (double* pp[ ],int n,int m)
{ double s=0 ; int i,j; double *p;
for ( i=0; i<n; i++,pp++)
for (j=0,p=*pp; j<m; j++,p++)
s+=*p; return
s; }
double sum1 (double* pp[ ],int n,int m)
{ double s=0 ;
for (int i=0; i<n; i++) for (int j=0; j<m; j++) s+=pp
[ i ][ j ];
return
s; }
30
对指针表达式或下标表达式降维过程中,二级指针和指向二维数组的指针降维的结果恰好殊途同归:
double **型指针 pp的下标表达式 pp [ j ]+k是 double *
的地址,pp [ j ][ k ]是 double的变量;
double(*)[3]型的数组指针 q构成的下标表达式 q[ j ]+k
也是 double *的地址,q[ j ][ k ]也是 double的变量。
数组指针指向的内存具有连续递增的特点,q[ j ]是右值; q[ j ]和 q[ j+1 ]具有相近的性质,或者均指向全局数据区,或者都指向局部内存区。
31
而指针 pp[ j ]可作为左值,pp[ j ]和 pp[ j+1]未必具有相临的性质; pp[ j ]指向局部空间的时候,pp[j+1]可以指向全局数组。
q[ j ]是自动计算出来的值,其等于 q+j的值;而 pp[ j ]最终通过赋值得到。
形如 double d [ N ][ M ]的二维数组,如果需要匹配
double * pp [ N ]的指针数组形参,调用之前可通过步骤进行初始赋值:
for (int k=0; k<N; k++) pp[ k ] = d[ k ];
32
对于 [double a[ M ]; double * p=a; ]容易看出表达式
p[ n ]和 p[0]+n是 double型的变量,p [ n ]表示数组的元素
a [n],p[0]+n表示 a[0]加上 n。
类似地,表达式 pp[n]或 *(pp+n)与表达式 pp[0]+n或
*pp+n 是 double*型的地址,但具有不同的含义:
*(pp+n)或 pp[ n ]表示指针数组中的第 n+1个元素为左值,*pp+n 或 pp[ 0 ]+n表示在指针数组当前元素 (第一个元素 ) 位置上后移 n个类型步长,pp[ 0 ]+n为右值指向
pp[ 0 ][ n ]。
33
十,void 关键字与 void *型的指针
void关键字只有三种用法:
1,声明一个无返回值的函数,例如,
[void function(int j);],void 型函数调用一般只单独调用 ;
2,声明一个不需要任何入口参数的函数,例如,
[int funct (void);]说明 funct是一个不需要任何入口参数的函数,其返回值为整型,funct()可作为整型右值参入各种运算 ;
3,定义一个 void*型的指针 (用 type表示区别于 void的特定类型,如算术类型等 );
void*型的指针是一种宽泛类型的指针,void*型的指针需要显式地转换为其它特定类型的指针,其它特定类型的指针可隐含地转换为 void*型的指针。
34
例如,
void* pv; type* pt,a,b; pt=(type*)pv; pv=pt;
void*型的指针本身绝不直接用于访问数据,即
*pv= (void)a 以及 b= (type)(*pv)形式的表达式是无意义的。
void*型的指针涉及到的内在含义,
void*型的指针与 void类型的函数,共借用同样一个关键字,但其内在含义实质上无任何联系。特定类型的指针可以隐含地支付给 void*型的指针,这里所谓的隐含实际上是编译器默许的指针转换,本质上带有强制性质。在采用
void*型指针的值访问内存空间前必须显式地转换或回归到确定类型的指针。
35
void*型的指针存放内存空间的宽泛的地址。将其它类型的地址统一地存放在 void*型指针中是旨在减少显式类型转换引起的代码书写量。
void*型的指针作为一种内存的定位基准,通用于操作内存的数据。 type*型指针的步长增量为 sizeof (type)。
但 void*型指针的类型步长增量是漂移的,没有 void类型的数据,sizeof(void)是不允许的,因此不对 void*型指针进行指针加减和访问运算,如 pv++,*pv等是不允许的。这种类型的宽泛性表明其数据操作的不完备性。
因此用 void*型的指针编写入口形参时务必补充另一个不可或缺的信息,即界定内存空间的精确大小。
36
供应商提供的 void*型函数如 memset,memcpy等必然要伴随一个 size_t类型的参数来完备内存数据的操作,size_t
是由类型声明语句 [typedef unsigned int size_t;]引入的别名,在函数内部另外具有确定类型的指针来进行具体的数据运算,这种确定的指针类型一般隐含地是 C语言中的 char*。
系统的 memset通常直接用汇编语言实现。 memset函数原型为,
void * memset (void *buf,int c,size_t n);
// 返回 void* 类型指针的函数函数原型中 c指出填充内存缓冲区的值,n为填充的元素数目,buf指向缓冲区的地址。函数返回指向 buf的指针。
37
[例 ] void*指针的编程特点,(函数版本模拟系统的
memset作用 )
void* memset (void* pv,int c,size_t n)
{ char * pt= (char *)pv ;
for(unsigned int k=0;k<n;k++) *pt++= (char)c;
return pv;
}
#include <iostream.h>
void main()
{ char str[ ] = "123456789abcd";
cout << str << " ";
char* result = (char*) memset (str,'8',9);
cout << result << endl;
} //输出结果,123456789abcd 888888888abcd
38