1
五,const指针六、二维数组
2
五,const指针普通指针是没有 const约束的指针,这样的指针本身既可以变动,其指向的内存单元也可更新。
普通指针 p进行两种运算:
一种是指针的加减寻址运算,如 p = &a[ i ],p++,p+=I
等,这种运算改变指针本身。
另一种是访问指针运算,如 *p=p[j],*(p+j)=p[0],这种运算读写内存,可改变间接变量而不改变指针。
为了清晰界定指针的不同运算,引进 const指针。指针与 const组合派生出三种意义不同的形式。
3
第一种形式定义的指针称为只读指针。
形式为:
const type *r;
const 类名 *只读指针;
const type *r = 地址表达式 ;
const 类名 *只读指针 = 数据区的地址;
第一种形式可以不必同时进行初始化,即可以先定义
r,然后再将该指针指向某个同类型的变量或数组。
只读指针可以在一片只读或非只读的数据区移动,以便将数据只读地复制到其它位置。只读指针既可指向只读区也可指向变量或左值区。定义中的关键字 const限定 *r为右值,具体地说只读指针 r是左值指针,间接变量 *(r+i)或 r[i]约束为右值表达式。
4
第二种形式定义的指针称为固定指针或右值指针。第二种形式为:
type *const s = 左值区的地址 ;
类名 * const 固定指针 = 非只读区的地址;
第二种形式必须同时用变量或左值的地址进行初始化,
除非作为形参。定义中的关键字 const冻结指针 s为右值,*s
或 s[i]是左值。
一维数组名 a就是一个这样的右值地址,r=a是可以的,a=r是错误的,这确保数组元素总是可以通过 a来索引。
右值指针可以用来接受 new运算符申请动态自由空间成功后的结果值,以避免对堆指针加减的误操作。
5
[例 ]固定指针和只读指针 (固定指针和只读指针都可指向普通 (非只读 )的数组 )
void intcpy (int *p,const int*q,int n)
//只读指针形参 q表示 *q在函数体中为右值。
{ for (int k=0; k<n; p++,q++,k++) *p = *q; }
//左边的函数体与下面程序块等价
# include<iostream.h>
// { int k=0; while(k<n){*p=*q; p++;q++;k++; } }
const int c[5] = {1,2,3,4,5};
//定义只读全局数组,每一个数组元素 c[k]为右值
6
void main (void)
{ int a[10];
int * const s=a;
//定义固定指针 s,初始化为数组名 a
const int *r=c;
int k=0;
for (; k<5; k++,r++)
s[k]=*r;
intcpy (a+5,a,5);
for (k=0; k<10; k++) cout<<"-"<<a[k];
}
//输出结果,-1-2-3-4-5-1-2-3-4-5
7
只读指针本身是可变的,只读指针的间接访问不得改写它指向的存储单元。
固定指针本身是不变的,但固定指针指向的存储单元允许更新。
固定指针只读指针都可以指向变量,固定指针和普通指针不指向只读数据区而仅指向左值数据区。
只读指针本身是可变的,只读指针的间接访问不得改写它指向的存储单元。
固定指针本身是不变的,但固定指针指向的存储单元允许更新。
固定指针只读指针都可以指向变量,固定指针和普通指针不指向只读数据区而仅指向左值数据区。
8
例如:
const int c[5]={1,2,3,4,5};
//&c[i]是 const int*型的地址,c[i]是右值表达式
int * const s=c;
//错误,定义固定指针指向只读数组 c,s[i]可以是左值
int * p;
//定义一个普通的指针即非只读的指针 p,p[i]可以是左值
p=&c[0];
//错误,p指向首元素,*p为左值,但 c[0]为右值,矛盾
9
固定指针 s指向只读数组首元素,*s可为左值,间接地导致 c[0]为左值,但这与只读数组的定义矛盾;类似地普通指针 p不指向只读数组。不可以对只读指针进行左值访问:
const char * r="abc";
//字符串安排在只读数据区,其首地址具有 char*属性
*r='m';
//错误,*r是右值表达式左值区可作为右值区访问。
从 int*型转换到 const int*型是默许的。但是反之不然,即不将 const int*型的地址隐含地转换为 int*型或
int*const型的指针,除非强制类型转换。
10
[例 ]指针的强制类型转换攻击只读数据区
#include <stdio.h>
void swap (int*const s)
{ int t=*s;*s=s[1];s[1]=t;
}
void main()
{ const int a[2]={1,2};
printf ("%d,%d;",a[0],*(a+1));
int* p= (int*) (a+1);
printf ("%d,%d-",p[-1]=3,*p=4);
swap ((int*) & a [0]);
printf ("%d,%d",*a,a [1]);
}
11
第三种形式定义的指针可称为只读的固定指针,其格式为:
const type* const x=地址表达式 ;
语句限定 x,*x都为右值,因此 x++,x[0]++是错误的。
例如,const int c=0;int j=0;
const int * const x=&c;
//x指向只读变量 c,&c是 const int *型的地址
const int * const y=&j;
//y指向变量 j,&j是 int *型的地址
12
六、二维数组
1.二维数组的定义一维数组是若干个同一类型有序变量的递增集合,由一个数组名来描述,r个一维数组需要 r个数组名来标识,当 r
很大的时候,相应的定义语句等量增加。
把 r个一维数组整合在一起形成二维数组,通过一个数组名来索引数组中的每一元素。
13
二维数组的定义格式为:
type d [r][c]; 类型 数组名 [常数 1][常数 2];
常数 1指出二维数组的行数 r,常数 2指出二维数组的列数 c。行数 r和列数 c是静态确定的正整数。 例如,
typedef int type;
const int r=8;
int c=6;
type d[r] [sizeof c]; 是允许的,
而 typedef int type;
int r=8,c=6;
type d[r][c]; 是不可以的。
14
二维数组下标表达式中有二个下标如 d[j][k],第一个下标
j用于索引数组的行,第二个下标 k指出相对于数组某行的列。
二维数组一经定义就具有如下性质:
a,行数 r和列数 c一起确定数组的元素个数为 r*c。
b,数组元素,二维数组第 j+1行第 k+1列的元素表示为
d[ j ][ k ],j合理的取值范围为 0~r-1,k合理的取值范围为,
0~c-1。
d[ j ][ k ]等价于访问指针形式 (*(*(d+j)+k)),两者为
type型左值,其作用相当于变量。数组元素占住内存的字节数为 n = sizeof (type) = sizeof (d[ j ][ k ])。
15
c,二维数组名具有两个信息,数组名 d代表数组的首地址和大小。 d+j具有 type (*)[c]型的属性,指向第 j+1行即 d+j
等价于 &d[j]。数组占住内存空间大小为:
sizeof (d)= r*c *n个字节。
sizeof (d+j)=sizeof (type (*)[ c ])= sizeof (type*)。
sizeof (d)不等于 sizeof (d+0)。
d,二维数组某行的首地址,d[j]给出第 j+1行的首地址。可以认为二维数组的某行相当于一个一维数组,即 d[j]
的作用相当于一维数组名的作用。
d[ j ]等价于 *(d+j),两者为 type*型的右值,d[ j ]+k或
*(d+j)+k 等价于 &d[j][k]。 d[j]和 d+j具有相同的地址值但地址属性不同。
r= sizeof(d)/ sizeof(d[0]),sizeof (d[j])=c*sizeof(type)。
而 sizeof (d[j]+k)= sizeof (type*)。
16
e,二维数组的元素在内存空间中是按照行的次序递增排列的,先排列第一行的每一个元素,再排列第二行的每一个元素;
第一行最后一个元素 d[0][c-1]紧邻第二行第一个元素
d[1][0];二维数组第一个元素为 d[0][0],二维数组最后一个元素为 d[r-1][c-1]。
二维数组某行的首地址 d[j]也是顺序连续递增的。每一行可以当作一个一维数组。
17
例如,数组定义语句 {short b[3][4];double d[4][3];}就定义两个二维数组,它们都有 12个数组元素。但 b占有
sizeof (short)*3*4 = 24 个字节的内存,
d占有 sizeof(double)*4*3=96 个字节的内存。
b,d都代表数组的首地址,d具有类型属性 double(*)[3],b
是 short (*)[4] 类型属性的地址。
定义语句中的 3和 4用于界定数组的维数。
b[2][3],d[3][2]分别是这两个数组的最后一个元素,
b[3][4],d[4][3]是合法的表达式,但导致越界,其中的 3和 4
是索引内存的下标。
定义语句分配唯一的一片内存,下标表达式 a[i],d[i][j]
等用于访问或操作内存,编译器不检查下标是否越界。
18
例如,数组 int x[2][4]与 char z[4][2]在内存中的排放次序分别为:
x[0][0],x[0][1],x[0][2],x[0][3],x[1][0],x[1][1],
x[1][2],x[1][3]
z[0][0],z[0][1],z[1][0],z[1][1],z[2][0],z[2][1],
z[3][0],z[3][1]
从上可见元素 z[1][2]与 z[2][0] 距离数组 z的首地址具有相同的偏移,x[0][4]将索引到元素 x[1][0]。
而 x[1][4]的索引方式导致越界。从上可见最后一个下标变化最快。
19
x[ i ]是 int*型的右值地址,z[ i ]是 char*型的右值地址。
x[ i ]和 z[ i ]是不同类型的地址。
x[ i ][ j ]是 int型的左值,z[ i ][ j ]是 char型的左值。
因此存在 x[ i ][ j ] = z[ j ][ i ]和 z[ i ][ j ]= x[ j ][ i ]形式的表达式,它们引起类型转换。
不存在 x[ i ] = z[ j ]和 z[ i ]= x[ j ]形式的表达式,右值不能放在赋值符号的左边,并且不同类型的地址也不许隐含类型转换。
20
[例 ] 从二维数组中查找第一个出现的负数
# include<iostream.h>
void main()
{ const int n=3,m=2;
int d[ n ] [ m ]; //定义二维数组
cout<<"input "<<n*m<<" integers:";
int j ; int I ;
for ( i=0; i<n; i++ ) for(j=0; j<m; j++) cin>>d[ i ][ j ];
for ( i=0; i<n; i++ )
for ( j=0; j<m; j++ ) goto found;
cout << "not found
if ( * ( * ( d+I ) +j )< 0) ! "<<endl;
goto end;
21
found,cout<<"d ["<<i<<"] ["<<j<<"] ="<< d[i][j]<<endl;
end,; }
某次运行的结果,另一次运行的结果,
input 6 integers:2 3 -4 5 6 7? input 6 integers:2 3 4 5 6 7?
a[0][2]=-4 not found!
说明,在负数没有找到之前,双重循环正常运行,若循环结束时未找到负数,则显示 "not found!",然后执行 goto
end语句,转至 end标号处。这里是一个空语句。
由此看出,使用 goto语句转移至某个没有语句的位置时,这个位置应放置一个空语句。这是空语句的一种用法。
在循环中,若找到第一个出现的负数时,用 goto语句退出两层循环,显示找到的负数。然后执行下面的空语句,进而结束程序的执行。
22
2,二维数组的初始化二维数组初始化定义语句的格式为:
type d[r][c]= {initialList};
类型 数组名 [常数 1][常数 2]={初始化列表 };
在初始化列表的初值个数足以确定二维数组的大小时可以省略地第一个常数的大小。即:
type d[ ][c]= {initialList};
类型 数组名 [ ][常数 2]={初始化列表 };
二维数组的初始化相当于多个一维数组的初始化的延拓,
例如,
int d[2][4]={1,2,3,4,5,6,7,8};
23
可省去左边第一个定界的维数,但最右边定界的维数是必须的,
int d[ ][4]={1,2,3,4,{5,6,7,8}};
也可以加上花括号,int d[][4]={{1,2,3,4},{5,6,7,8}};
花括号的情形对于不完整的初始化格式是必须的 ;
int d[ ][4]={{1,2},{5,6,7}};
等价于,int d[2][4];
d[0][0]=1; d[0][1]=2;
d[0][2]=0; d[0][3]=0;
d[1][0]=5; d[1][1]=6;
d[1][2]=7; d[1][3]=0;
24
无花括号的情形,int d[ ][4]={1,2,5,6,7};
对应,int d [2][4];
d[0][0]=1; d[0][1]=2; d[0][2]=5; d[0][3]=6;
d[1][0]=7; d[1][1]=0; d[1][2]=0; d[1][3]=0;
类似地,int d[3][4]={{1},{2},{3}};
导致数组第一列的元素分别初始化为 1,2,3,其余设置为 0。
类似地,int d[3][4]={{1},{2}};
导致数组第一列的元素分别初始化为 1,2,0,其余设置为 0。
不可以写为:
int d[3][4]={{1},{ },{3}};
25
3.多维数组多维数组的定义的格式为:
类型 数组名 [表达式 1] [ 表达式 2]… [ 表达式 n];
type array [max1] [max2],.,[maxn]= {初始化列表 };
初始化语句定义多维数组时,可以不指定第一维的维数,
但其余的维数是必须给定的:
type array[ ][max2]...[maxn]={初始化列表 };
n维数组的下标表达式有 n个下标,如下,
array [sub1] [sub2]...[ subn ]
下标运算符 [ ]是从左向右结合的,最左下标表达式
array [sub1] 首先求值,然后求下标表达 array[sub1][sub2],
依此类推。多维数组在内存单元中是连续的线性存放的。这称为多维空间到一维内存单元的映射。
C/C++对数组元素的安排按照第一个下标优先的顺序进行,
26
对于二维数组先存放第一行,接着存放第二行。对于三维数组先存放第一页,接着存放第二页。这也意味最后一个下标变化最快。 例如,对于三维数组
type s[m][r][c]; /*m,r,c是预先静态给定的正数 */
先存放第一页的 r*c个元素 s[0][r][c];接着存放第二页的
r*c个元素 s[1][r][c]。 s[i]的地位相当于二维数组名 d,对于特定的一页,先存放该页二维数组第一行,接着存放第二行。依此类推。
对于 int s[2][2][4]在内存中的排放次序为:
s[0][0][0],s[0][0][1],s[0][0][2],s[0][0][3],s[0][1][0],
s[0][1][1],s[0][1][2],s[0][1][3],
s[1][0][0],s[1][0][1],s[1][0][2],s[1][0][3],s[1][1][0],
s[1][1][1],s[1][1][2],s[1][1][3]
27