"C语言-数组"

  "C语言基础复习四"

Posted by Xu on January 17, 2018

C语言复习--数组

array

数组名为一个指针常量,不可修改和赋值,但在两种情况下,数组名不能用指针常量来表示-就是当数组名作为sizeof操作符或单目操作符&的操作数的时候,sizeof返回整个数组的长度而不是该指针常量的长度,&产生一个指向数组的地址,而不是一个指向指针常量的地址。

下标引用

除了优先级以外,下标引用和间接访问完全相同

  • array[index]
  • *(array+index)
int array[10];
int *ap = array + 2;

分析表达式ap,*ap = (array+2),ap[0] = (array[2]),ap+6 = (array + 8),*(ap+6) = (array[8]),ap[6] = (array[8]),&ap 合法,ap[-1] = (array[1]),ap[9] 非法,越界

为了表达下标引用和间接访问完全相同,可分析如下例子:2[array] 合法 => *(2 + arrray) =>array[2]

使用下标和指针访问数组效率分析

使用下标绝不会比指针更有效率,但指针有时会比下标更有效率。

两个循环:

int array[10],a;
for(a = 0;a < 10;a ++){
   array[a] = 0;//等同于*(array+a),每一次循环编译器需要取a的值然后与整型的长度4进行一次乘法运算
}

int array[10],*ap;
for (ap = array;ap<array +10;ap++)//这里涉及到的乘法运算为ap++ == \*(ap+1),只需要将1与4相乘一次即可
    *ap = 0;
    

分析使用指针的效率:当指针发生间接引用时,即指针和整型数进行算数运算时,往往在汇编层次需要使用到乘法运算,下标和类型长度相乘决定指针的偏移量。

优化过程代码:

#define SIZE 50
int x[SIZE];
int y[SIZE];
int i;
int *p1,*p2;
//下标版本
void try1(){
   for (i=0;i<SIZE;i++)
       x[i] = y[i];//这里汇编需要计算两次i和整型长度的乘法运算
}

//指针方案
void try2(){
   for(p1 = x,p2 = y;p1 -x < SIZE; ){//这里汇编每次计算p1 -x需要将差值与整型长度4进行除法运算
       *p1++ = *p2++;
   }
}

//重用计数器
void try3(){
    for(p1 = x,p2 = y,i = 0;i < SIZE;i++){//可以取消除法运算,减少汇编目标代码的长度
        *p1++ = *p2++;//在执行该间接访问之前,依然需要讲指针变量复制到地址寄存器
    }
}

//使用寄存器变量
void try4(){
    register int *p1,*p2;
    register int i ; //计数器
    for(p1 = x,p2 = y,i = 0;i < SIZE;i++){
        *p1++ = *p2++;//这里使用寄存器变量不需要复制指针变量,但p1,p2必须要声明为局部变量,这样一开始就保存在寄存器中,不必将指针变量从内存中复制到寄存器。
    }
}

//消除计数器
void try5(){
    register int *p1,*p2;
    for(p1 = x,p2 = y;p1 < &x[SIZE];){//比较p1和x数组的最后一个元素的地址,且&x[SIZE为常量表达式在编译时就已经计算得到],不用使用除法运算,可以消除计数器,使目标汇编代码更会紧凑简单
        *p1++ = *p2++;
    }
}

结论:

  • 当根据固定数量的增量在数组中移动时,使用指针变量比使用下标更有效率,可以减少乘法运算
  • 声明寄存器变量的指针往往比位于静态内存和堆栈中的指针效率更高,因为不需要复制操作
  • 通过测试指针变量的循环条件,可以不必使用计数器来判断循环是否结束,这样减少对计数器变量寄存器的使用
  • 那些必须要在运行时求值的表达式较之如&array[SIZE]和array+SIZE这样的常量表达式(编译时就可以完成)往往代价更高

数组和指针

int a[5];
int *b;

虽然说数组名是一个指针常量,但数组和指针并不完全相等

  • 声明数组时,编译器会根据声明所指定的元素数量为数组保留内存空间,然后再创建数组名,它的值是一个常量。
  • 声明指针变量时,编译器只会为指针本身保留内存空间,并不会为任何整型值分配内存空间

pointer&array

*a 合法,*b 不合法

a++ 不合法,a为常量 ,b++可以通过编译

初始化

静态和自动初始化:数组的初始化方式和变量的初始化方式类似--取决于它们的存储类型。

  • 存储在静态内存中的数组只初始化一次(不需要执行指令,由链接器完成),在程序开始执行之前。
  • 自动变量数组的初始化则是执行流进入该数组所在的作用域时通过多条赋值语句初始化,该初始化时间可能非常可观。。
  • 所以我们初始化一个局部数组时需要考虑,每次进入该函数都对该数组进行初始化是否值得。

不完整初始化 自动计算数组长度 字符数组初始化:char message[] = “hello”;

  • char message[] = “hello”;//初始化一个字符数组的元素,在这里”hello”并不是字符串常量,而是初始化列表的另一种写法
  • char *message = “hello”;//”hello”为字符串常量,message指向它
  • 所以字符串在初始化数组的语句中是一个初始化列表,在其他任何地方都是字符串常量

多维数组

多维数组的存储顺序,按最右边的下标率先变化的原则,称为主序。

所以定义:

int *mp;
int maxtrix[6][10];
mp = matrix[3][8];
//*mp为matrix[3][8]的值,*++mp则指向matrix[3][9]
//但maxtrix则有所不同,因为maxtrix指向第一个元素的指针,而它的第一个元素是一个由十个整型数构成的数组,所以maxtrix+1指向maxtrix[1][0]开头的一维数组,最左边的率先开始变化
//可以将maxtrix看作由三个指针构成的数组,这三个指针又分别指向一个由十个整型值构成的一维数组

下标的间接访问:maxtrix[x][y] = *(*(maxtrix+x)+y)

指向数组的指针:

int vector[10],\*vp = vector;//合法
int maxtrix[3][10],\*vp=maxtrix;//非法,maxtrix并不是一个指向整型的指针,而是指向一位数组的指针
int (*p)[10];//首先声明p是一个指针,然后p指向一个10个整型值构成的一维数组,所以p+1时,1会与十个整型值的长度相乘,然后相加。
int *p[10];//首先声明p是一个一位数组,该数组的元素为指向int类型的指针。

多维数组作为参数传递时,由于多维数组中的元素为一个指向一维数组的元素,所以在定义形参时需要指明该一维数组的维数,其他与一维数组做形参没有区别:

比如:

maxtrix[3][10];
//func (maxtrix);的函数原型如下:
void func(int (*mat)[10]);
//或
void func(int mat[][10]);
//都需要指明该多维数组中元素指向的一维数组的维数

多维数组长度的自动计算,在多维数组中,只有第一维才能根据初始化列表缺省提供长度,以后的维数都需要指明长度。

指针数组和多维数组的区别:

  • 指针数组:需要分配指针数组的内存空间分别保存这些字符串的首地址,其次需要计算数组的长度,但无需分配多余的空间
char const *keyword[] = {//虽然每个字符串只占用它所需要的内存,但额外需要为每个字符串分配一个指针变量的内存空间
     "do",
     "for",
     "if",
     "register",
     "return",
     "switch",
     "while" 
 };
#define N_KEYWORD (sizeof(keyword)/sizeof(keyword[0]))//sizeof(keyword)计算该数组的总长度,sizeof(keyword[0])则是计算数组中每个元素的长度,从而得到元素的个数

当我要遍历这个关键字列表时,需要首先知道该数组的长度N_KEYWORD

  • 二维数组:为每个元素都分配了9个字符的内存空间,造成资源浪费,但它不需要保存指针变量数组
char const keyword[][9] = {
     "do",//虽然占两个字符加一个'\0'字符,但还是为其分配了9个字符的内存空间
     "for",
     "if",
     "register",
     "return",
     "switch",
     "while" 
 };

所以使用指针数组还是多维数组取决于

  • 保存指针数组中的指针变量所占用的空间
  • PK
  • 多维数组中每个字符串都存储固定长度所浪费空间

总结:

  1. 源码的可读性要比程序的运行效率更重要
  2. 只要有可能,函数的指针形参都应该声明为const,因为这样可以表示该指针指向的数据是不能被修改的,编译器可以直接检测到对该数据修改造成的错误
  3. 使用register关键字可提高程序运行效率,可以用在那些运行非常多的关键步骤,因为无需将变量值从内存或堆栈拷贝到寄存器继续运算。