通过指针或带下标的数组名都可以访问数组中的元素,哪一种方式更好呢?
与使用下标相比,使用指针能使C编译程序更容易地产生优质的代码。假设你的程序中有这样一段代码:
/* X la some type */
X a[MAX];
X *p; /*pointer*/
X x; /*element*/
int i; /*index*/
为了历数组a中的所有元素,你可以采用这样一种循环方式(方式a)
/*version (a)*/
for (i = 0; i<MAX; ++i)
{
x=a[i];
/* do something with x * /
}
你也可以采用这样一种循环方式(方式b)
/*veraion(b)*/
for (p = a; p<&a[MAX]; ++p )
{
x=*p;
/* do aomething with x * /
}
这两种方式有什么区别呢?两种方式中的初始情况和递增运算是相同的,作为循环条件的比较表达式也是相同的(下文中将进一步讨论这一点)。区别在于“x=a[]”和“x=*p”,前者要确定a[i]的地址,因此需要将i和类型x的大小相乘后再与数组a中第一个元素的地址相加;
后者只需间接引用指针p。间接引用是快速的,而乘法运算却比较慢。
这是一种“微效率”现象,它可能对程序的总体效率有影响,也可能没有影响。对方式a来说,如果循环体中的操作是将数组中的元素相加,或者只是移动数组中的元素,那么每次循环中大部分时间就消耗在使用数组下标上;如果循环体中的操作是某种I/O操作,或者是函数调用,那么使用数组下标所消耗的时间是微不足道的。
在有些情况下,乘法运算的开销会降低。例如,当类型x的大小为1时,经过优化就可以将乘法运算省去(一个值乘以1仍然等于这个值);当类型x的大小是2的幂时(此时类型x通常是系统固有类型),乘法运算就可以被优化为左移位运算(就象一个十进制的数乘以10一样)。
在方式b中,每次循环都要计算&a[MAX],这需要多大代价呢?这和每次计算a[i]的代价相同吗?答案是不同,因为在循环过程中&a[MAX]是不变的。任何一种合格的编译程序都只会在循环开始时计算一次&a[MAX],而在以后的每次循环中重复使用这次计算所得的值。
在编译程序确认在循环过程中a和MAX都不变的前提下,方式b和以下代码的效果是相同的:
/* how the compiler implements version (b) */
X *temp = &a[MAX]; /* optimization */
for (p = a; p< temp; ++p )
{
x =*p;
/*do something with x * /
}
遍历数组元素还可以有另外两种方式,即以递减而不是递增的顺序遍历数组元素。对按顺序打印数组元素这样的任务来说,后两种方式没有什么优势,但是对数组元素相加这样的任务来说,后两种方式比前两种方式更好。通过下标并且以递减顺序遍历数组元素的方式(方式c)如下所示(人们通常认为将一个值和。比较的代价要比将一个值和一个非零值比较的代价小:
/* version (c) */
for (i = MAX - 1; i>=0; --i)
{
x=a[i];
/* do aomcthing with x * /
}
通过指针并以递减顺序遍历数组元素的方式(方式d)如下所示,其中作为循环条件的比较表达式显得很简洁:
/* version (d) */
for (p = &a[MAX - 1]; p>=a; --p )
{
x =*P;
/*do something with x * /
}
与方式d类似的代码是很常见的,但不是绝对正确的,因为循环结束的条件是p小于a,而这有时是不可能的(见9.3)。
通常人们会认为“任何合格的能优化代码的编译程序都会为这4种方式产生相同的代码”,但实际上许多编译程序都没能做到这一点。笔者曾编写过一个测试程序(其中类型x的大小不是2的幂,循环体中的操作是一些无关紧要的操作),并用4种差别很大的编译程序编译这个程序,结果发现方式b总是比方式a快得多,有时要快两倍,可见使用指针和使用下标的效果是有很大差别的(有一点是一致的,即4种编译程序都对&a[MAX]进行了前文提到过的优化)。
那么在遍历数组元素时,以递减顺序进行和以递增顺序进行有什么不同呢?对于其中的两种编译程序,方式c和方式d的速度基本上和方式a相同,而方式b明显是最快的(可能是因为其比较操作的代价较小,但是否可以认为以递减顺序进行要比以递增顺序进行慢一些呢?);对于其中的另外两种编译程序,方式c的速度和方式a基本相同(使用下标要慢一些),但方式d的速度比方式b要稍快一些。
总而言之,在编写一个可移植性好、效率高的程序时,为了遍历数组元素,使用指针比使用下标能使程序获得更快的速度;在使用指针时,应该采用方式b,尽管方式d一般也能工作,但编译程序为方式d产生的代码可能会慢一些。
需要补充的是,上述技巧只是一种细微的优化,因为通常都是循环体中的操作消耗了大部分运行时间,许多C程序员往往会舍本求末,忽视这种实际情况,希望你不要犯相同的错误。
/* X la some type */
X a[MAX];
X *p; /*pointer*/
X x; /*element*/
int i; /*index*/
为了历数组a中的所有元素,你可以采用这样一种循环方式(方式a)
/*version (a)*/
for (i = 0; i<MAX; ++i)
{
x=a[i];
/* do something with x * /
}
你也可以采用这样一种循环方式(方式b)
/*veraion(b)*/
for (p = a; p<&a[MAX]; ++p )
{
x=*p;
/* do aomething with x * /
}
这两种方式有什么区别呢?两种方式中的初始情况和递增运算是相同的,作为循环条件的比较表达式也是相同的(下文中将进一步讨论这一点)。区别在于“x=a[]”和“x=*p”,前者要确定a[i]的地址,因此需要将i和类型x的大小相乘后再与数组a中第一个元素的地址相加;
后者只需间接引用指针p。间接引用是快速的,而乘法运算却比较慢。
这是一种“微效率”现象,它可能对程序的总体效率有影响,也可能没有影响。对方式a来说,如果循环体中的操作是将数组中的元素相加,或者只是移动数组中的元素,那么每次循环中大部分时间就消耗在使用数组下标上;如果循环体中的操作是某种I/O操作,或者是函数调用,那么使用数组下标所消耗的时间是微不足道的。
在有些情况下,乘法运算的开销会降低。例如,当类型x的大小为1时,经过优化就可以将乘法运算省去(一个值乘以1仍然等于这个值);当类型x的大小是2的幂时(此时类型x通常是系统固有类型),乘法运算就可以被优化为左移位运算(就象一个十进制的数乘以10一样)。
在方式b中,每次循环都要计算&a[MAX],这需要多大代价呢?这和每次计算a[i]的代价相同吗?答案是不同,因为在循环过程中&a[MAX]是不变的。任何一种合格的编译程序都只会在循环开始时计算一次&a[MAX],而在以后的每次循环中重复使用这次计算所得的值。
在编译程序确认在循环过程中a和MAX都不变的前提下,方式b和以下代码的效果是相同的:
/* how the compiler implements version (b) */
X *temp = &a[MAX]; /* optimization */
for (p = a; p< temp; ++p )
{
x =*p;
/*do something with x * /
}
遍历数组元素还可以有另外两种方式,即以递减而不是递增的顺序遍历数组元素。对按顺序打印数组元素这样的任务来说,后两种方式没有什么优势,但是对数组元素相加这样的任务来说,后两种方式比前两种方式更好。通过下标并且以递减顺序遍历数组元素的方式(方式c)如下所示(人们通常认为将一个值和。比较的代价要比将一个值和一个非零值比较的代价小:
/* version (c) */
for (i = MAX - 1; i>=0; --i)
{
x=a[i];
/* do aomcthing with x * /
}
通过指针并以递减顺序遍历数组元素的方式(方式d)如下所示,其中作为循环条件的比较表达式显得很简洁:
/* version (d) */
for (p = &a[MAX - 1]; p>=a; --p )
{
x =*P;
/*do something with x * /
}
与方式d类似的代码是很常见的,但不是绝对正确的,因为循环结束的条件是p小于a,而这有时是不可能的(见9.3)。
通常人们会认为“任何合格的能优化代码的编译程序都会为这4种方式产生相同的代码”,但实际上许多编译程序都没能做到这一点。笔者曾编写过一个测试程序(其中类型x的大小不是2的幂,循环体中的操作是一些无关紧要的操作),并用4种差别很大的编译程序编译这个程序,结果发现方式b总是比方式a快得多,有时要快两倍,可见使用指针和使用下标的效果是有很大差别的(有一点是一致的,即4种编译程序都对&a[MAX]进行了前文提到过的优化)。
那么在遍历数组元素时,以递减顺序进行和以递增顺序进行有什么不同呢?对于其中的两种编译程序,方式c和方式d的速度基本上和方式a相同,而方式b明显是最快的(可能是因为其比较操作的代价较小,但是否可以认为以递减顺序进行要比以递增顺序进行慢一些呢?);对于其中的另外两种编译程序,方式c的速度和方式a基本相同(使用下标要慢一些),但方式d的速度比方式b要稍快一些。
总而言之,在编写一个可移植性好、效率高的程序时,为了遍历数组元素,使用指针比使用下标能使程序获得更快的速度;在使用指针时,应该采用方式b,尽管方式d一般也能工作,但编译程序为方式d产生的代码可能会慢一些。
需要补充的是,上述技巧只是一种细微的优化,因为通常都是循环体中的操作消耗了大部分运行时间,许多C程序员往往会舍本求末,忽视这种实际情况,希望你不要犯相同的错误。