第6章 指针与数组 七、指向数组的指针 八、二级指针 九、指针数组 十、void 关键字与void *型的指针
七、指向数组的指针 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]; 类型 (*数组指针名) [整型常数];
如上定义的指针q为type(*)[c] 类型的指针,是指向列 增量m = sizeof (type)*c。 一经定义数组指针q,则表达式q[ i ]等价于(*(q+i))为 type*型右值,这个右值是自动计算出来的,不占数据区内 存。 表达式q[ i ][ j ]等价于(*(*(q+i)+j))为type型左值,左值 的内存应预先由数组定义语句分配。编译器不检查q[ i ][ j ] 是否初始化,也不管是否越界。
如果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]。
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行的首地址,两者 都指向同一内存位置但地址属性不同。
m=sizeof (type[c])=nc 二维数组type d[r][c]某行的首地址与数组指针的关系 数组指针指向二维数组 每行的首地址,赋值一次 二维数组行地址 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 q=d+1; q+= j-1; 指针用加运算 数组指针向后移动j-1个位置 m=sizeof (type[c])=nc 二维数组type d[r][c]某行的首地址与数组指针的关系
赋值语句{ 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个字节.
语句{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;
设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))
[例]同一个地址对应不同类型的地址属性 #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]也具有相同的值。 地址值相同地址类型属性不同的现象表示同一片内存 可用不同属性的指针寻址访问。
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 ],...){;...;} 。
[例]指向二维数组的指针与二维数组 # 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,
的和 [例]数组指针形参或二维数组形参实现求数组空间 #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)); }
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 ];
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是静定的整数。
去掉上面的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];
# 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;
八、二级指针 二级指针的定义格式为: type** pp1 , **pp2 ,…,**ppn; 二级指针初始化定义语句的格式为: type** pp = ptrExpression; 类型** 指针名=指针表达式; 指针表达式是与所定义的指针同类型的地址值。如上定 义的指针pp,pp1, pp2, ppn等抽象地称为type**类型的指针 或二级指针。二级指针的地址对应一个三级指针,基本上不 使用三级指针。一级指针的地址是右值,二级指针是一种存 放一级指针的地址数据的特殊类型变量,简单地说二级指针 是一级指针的地址的变量。
二级指针具有下面的特性: 1.二级指针的类型属性,二级指针的类型是其所指向地 址变量的类型,二级指针的类型属性限定二级指针的增减以 指针类型步长sizeof(type*)=4或2为单位。二级指针可在一 级指针构成的数组空间中移动。pp++表示向后移动4或2个 字节。 2.一经定义二级指针pp,则表达式pp[ I ]等价于(*(pp+i)) 为type*型左值;表达式pp[ I ][ j ]等价于(*(*(pp+i)+j))为 type型左值,编译器不负责它们的内存分配,不检查它们是 否初始化,也不管是否越界。
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 c p
九、指针数组 1.指针数组的定义和性质 type*型的指针数组的定义格式为: type *pa[N]; 类型 * 指针数组名[数组长度]; 的内存空间,而其特点如下: a. pa为指针数组名,本身代表指针数组的首地址,此地 址是type**型的右值地址。 sizeof (pa)=sizeof (type*[N])= N *sizeof (type*)。 N= sizeof(pa)/ sizeof(pa[0])
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。
例如: 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 ; 指针数组的相邻元素指向的数据不必具有相邻的关系; 指针数组的元素可指向生存期稳定的全局变量、静态变量也 可指向生存期瞬态变化的局部变量,尚可指向生存期由程序 员控制的堆空间。 但应保证指针指向的数据生存期对于指针具有可操作 性,或者说指针操作的内存数据的生存期在指针访问时是有 效的,是存在的。
考虑: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; 指针数组元素 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 pp+=j; pp=pa; 图 指针数组的值与二级指针
[例] 指针数组的元素指向维数大小不同的一维数组 # 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 -
2.二级指针形参和指针数组形参: 如下是两个本质一致外在格式不同的函数原型: int f (int **pp,...); int f (int *pp[],...); 形如"int**pp"的形参为二级指针形参, 形如"int*pp[]“ 的形参为指针数组形参,两者都视为 int**型的表达式。指 针数组形参要求实参指针数组的内存分配,每一元素的初始 化和相应元素指向的数据的内存分配。二级指针形参表明定 位内存地址的基准特性。 函数定义int f (int *pp[ ],...){;...;}等价于函数定义 int f (int **pp,...) {;函数体代码不变;} 引入一级指针用于访问变量或数组元素,引入指针数组 用于通过访问指针元素,最终访问指针元素指向的变量。
二级指针pp=pa,pp[ k ]等价于pa[ k ],对pp[ k ]的操作 通过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); }
[例]二级指针或指针数组作为形参实现求数组的和 #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)); }
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; }
对指针表达式或下标表达式降维过程中,二级指针和指 向二维数组的指针降维的结果恰好殊途同归: 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 ]具有相近的性质,或者均指向全局数据 区,或者都指向局部内存区。
而指针pp[ j ]可作为左值,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 ];
对于[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 ]。
十、void 关键字与void *型的指针 void关键字只有三种用法: 1. 声明一个无返回值的函数,例如: [void function(int j);],void 型函数调用一般只单独调用; 2. 声明一个不需要任何入口参数的函数,例如: [int funct (void);]说明funct是一个不需要任何入口参数的 函数,其返回值为整型,funct()可作为整型右值参入各种运算; 3. 定义一个void*型的指针(用type表示区别于void的特 定类型,如算术类型等); void*型的指针是一种宽泛类型的指针,void*型的指针 需要显式地转换为其它特定类型的指针,其它特定类型的指 针可隐含地转换为void*型的指针。
例如: void* pv; type* pt, a, b; pt=(type*)pv; pv=pt; void*型的指针本身绝不直接用于访问数据,即 *pv= (void)a 以及b= (type)(*pv)形式的表达式是无意义 的。 void*型的指针涉及到的内在含义 : void*型的指针与void类型的函数,共借用同样一个 关键字,但其内在含义实质上无任何联系。特定类型的指针 可以隐含地支付给void*型的指针,这里所谓的隐含实际上 是编译器默许的指针转换,本质上带有强制性质。在采用 void*型指针的值访问内存空间前必须显式地转换或回归到 确定类型的指针。
void*型的指针存放内存空间的宽泛的地址。将其它 型转换引起的代码书写量。 void*型的指针作为一种内存的定位基准,通用于操 作内存的数据。type*型指针的步长增量为sizeof (type)。 但void*型指针的类型步长增量是漂移的,没有void类 型的数据,sizeof(void)是不允许的,因此不对void*型指针 进行指针加减和访问运算,如pv++,*pv等是不允许的。这 种类型的宽泛性表明其数据操作的不完备性。 因此用void*型的指针编写入口形参时务必补充另一个 不可或缺的信息,即界定内存空间的精确大小。
供应商提供的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的指针。
[例] 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
请打开“第6章(4).ppt”