第九章 函数
实际参数
- 在函数执行过程中,对形式参数的改变不会影响实际参数的值,这是因为形式参数中包含的是实际参数值的副本。
- 实际参数的转换
- 编译器在调用前遇到原型。就像使用赋值一样,每个实际参数的值被隐式地转换成相应形式参数的类型。 例如,如果把int类型的实际参数传递给期望得到double类型数据的函数,那么实际参数会被自动转换成double类型。
- 编译器在调用前没有遇到原型。编译器执行默认的实际参数提升: (1) 把float类型的实际参数转换成double类型,(2)执行整值提升,即把char类型和shot类型的实际参数转换成int类型。
- 默认的实际参数提升可能无法产生期望的结果。
思考下面的例子:
#include <stdio.h>
//调用前没有遇到原型
int main(void)
{
double x = 3.0;
printf(“Square: %d\n”, square(x));
return 0;
}
int square(int n)
{
return n * n;
}
在调用square函数时,编译器没有遇到原型,所以它不知道square函数期望有int类型的实际参数。
因此,编译器在变量x上执行了没有效果的默认的实际参数提升。
因为square函数期望有int类型的实际参数,但是却获得了double类型值,所以square的数将产生无效的结果。
通过把square的实际参数强制转换为正确的类型,可以解决这个问题:
printf(“Square: %d\n”, square((int) x));
当然,更好的解决方案是在调用square前提供该函数的原型。
在C99中,调用square之前不提供声明或定义是错误的。
数组型实际参数
- 注意,在把数组名传递给函数时,不要在数组名的后边放置方括号:
total = sum_array(b[], LEN); /*** WRONG ***/
- 一个关于数组型实际参数的重要论点:函数无法检测传入的数组长度的正确性。
我们可以利用这一点来告诉函数, 数组的长度比实际情况小。假设虽然数组b有100个元素。 但是实际仅存储了50个数。通过书写下列语句可以对数组的前50个元素进行求和:
total = sum_array(b, 50); /* sums first 50 elements*/
sum_array函数将忽略另外50个元素。(事实上,sum_array函数甚至不知道另外50个元素的存在!)
- 注意不要告诉函数,数组型实际参数比实际情况大:
- total = sum_array(b, 150); /*** WRONG **/
在这个例子中,sum_array函数将超出数组的末尾,从而导致未定义的行为。
-
关于数组型实际参数的另一个重要论点是:函数可以改变数组型形式参数的元素,且改变会在相应的实际参数中体现出来。
例如,下面的函数通过在每个数组元素中存储0来修改数组:
vold store_zero(int a[], int n)
{
int i;
for(i = 0; i < n; i++)
a[i] = 0;
}
函数调用
store_zeros(b, 100);
会在数组b的前100个元素中存储0。
数组型实际参数的元素可以修改似乎与C语言中实际参数的值传递相矛盾。事实上这并不矛盾,详见下面第12章。 -
- 如果形式参数是多维数组,声明参数时只能省略第一维的长度。
int sum(int a[][LEN], int n)
- 如果形式参数是多维数组,声明参数时只能省略第一维的长度。
-
- 递归深度不能无限深;
与n成线性或幂次关系的递归不提倡。
递归出口。
- 递归深度不能无限深;
问与答
- 一些C语言书出现了采用了不同于“形式参数”和“实际参数”的术语,是否有标准术语?
- 在程序的形式参数列表的后边,我们遇见过把形式参数的类型用单独的声明进行说明的例子:
double average(a, b)
double a, b;
{
return (a*b) / 2;
}
这种实践是合法的吗? - 一些编程语言允许过程和函数互相嵌套。C语言是否允许函数定义嵌套呢?
- 为什么编译器允许函数名不跟着圆括号?
- 在函数调用 f(a, b) 中,编译器如何知道逗号是标点符号还是运算符呢?
- 函数原型中的形式参数的名字是否需要和后面函数定义中给出的名字相匹配?
- 我始终不明白为什么要提供函数原型,只要把所有函数的定义放置在main函数的前面,不就没有问题了吗?
- 我看到有的函数声明忽略掉了形式参数的全部信息:
double average();
这种做法是合法的吗? - 为什么有的程序员在函数原型中故意省略参数名字?保留这些名字不是更方便吗?
- 把函数的声明放在另一个函数体内是否合法?
- 如果几个函数具有相同的返回类型。能否把它们的声明合并?例如,既然print_pun函数和print_count函数都具有void型的返回类型,那么下面的声明合法吗?
- 如果指定一维数组型形式参数的长度,会发生什么?
- 为什么可以留着数组中第一维的参数不进行说明,但是其他维数必须说明呢?
- 为什么一些程序员把return语句中的表达式用圆括号括起来?
- 非void函数试图执行不带表达式的return语句时会发生什么?
- 如何测试main的返回值来判断程序是否正常终止?
- 在编译main函数时,为么编译器会产生"control reaches end of non-void function"这样的警告?
- 对于前一个问题,为什么不把main函数的返回类型定义为void呢?
- 如果函数f1调用函数f2,而函数f2又调用了函数f1,这样合法吗?
第十章 程序结构
暂略
第11章 指针
核心图
- 不要把间接寻址运算符用于未初初始化的指针变量
如果指针变量p没有初始化,那么试用使用p的值会导致未定义的行为。
int p;
pintf("%d", p); /* WRONG **/
给p赋值尤其危险,如果p恰好具有有效的内存地址,下面的赋值会试图修改存储在该地址的数据:
int *p;
*p = 1; //WRONG
如果上述赋值改变的内存单元属于该程序,那么可能会导致不规律的行为。
如果改变的内存单元属于操作系统,那么很可能会导致系统崩溃。编译器可能会给山警告消息,告知p未初始化,所以请留意获得的警告消息。
指针赋值
int i, j, *p, *q;
p = &i;
q = p;
- 这条语句是把p的内容(即i的地址)复制给q,效果是把q指向了p所指向的地方。
- 注意不要把q = p和*q = *p搞混。
指针作为参数
- 向函数传递需要的指针却失败了可能会产生严重的后果。
假设我们在调用decompose函数时没有在i和d前面加上&运算符:
decompose (3.14159, i, d);
decompose函数期望第二个和第三个实际参数是指针,但传入的却是i和d的值。
decompose函数没有办法区分,所以它将会把i和d的值当成指针来使用,它会修改未知的内存地址,而不是修改i和d。
如果已经提供了decompose函数的原型(当然,应该始终这样做),那么编译器将告诉我们实际参数的类型不对。
然而,在scanf的例子中,编译器通常不会检查出传递指针失败,因此scanf函数特别容易出错。
指针作为返回值
- 当给定指向两个整数的指针时,下列函数返回指向两整数中较大数的指针:
int *max(int *a,int *b)
{
if(*a > *b)
return a;
else
return b;
}
- 永远不要返回指向自动局部变最的指针
Int *f(void)
{
int i;
……
retum &i;
}
一旦f返回,变量i就不存在了,所以指向变量i的指针将是无效的。有的编译器会在这种情况下给出类似"function returns address of local variable"的警告。
问与答
- 指针总是和地址一样吗?
- 如果指针可以指向程序中的数据,那么使指针指向程序代码是否可能?
- 我觉得声明
int *p = &i;
和
p = &i;
不一致。为什么在语句中p没有像其在声明中那样前面加 * 号呢? - 有没有办法显示变量的地址?
- 下列声明使人糊涂
void f(const int *p);
这是说函数f不能修改p吗? - 声明指针类型的形式参数时,像下面这样在参数名前面放置单调const是否合法?
void f(int * const p);
第12章 指针和数组
指针的算术运算
- 如果p指向数组元素a[i],那么p+j指向a[i+j]
- 如果p指向数组元素a[i],那么p-j指向a[i-j]
- 当两个指针相减时,结果为指针之间的距离(用数组元素的个数来度量)。
因此,如果p指向a[i]且q指向a[j],那么p-q就等于i-j。
在一个不指向任何数组元素的指针上执行算术运算会导致未定义的行为。此外,只有在两个指针指向同一个数组时, 把它们相减才有意义。
指针比较
- 可以用关系运算符进行指针比较。只有在两个指针指向同一数组时,用关系运算符进行的指针比较才有意义。比较的结果依赖于数组中两个元素的相对位置。
*运算符和++运算符的组合
- 后缀++的运算级高于间接寻址*
用一维数组名作为指针
-
可以用一维数组的名字作为指向数组第一个元素的指针。
-
int a[10];
a+i 等同于 &a[i]
*(a+i) 等同于 a[i] -
&a是整个a[10]的首地址,是以整个数组为角度来看,虽然它与a[0]的值相同,但是&a+1就已经增加了10个int类型的字节的长度了。
-
虽然可以把数组名用作指针,但是不能给数组名赋新的值。试图使数组名指向其他地方是错误的:
while (a != 0)
a++; /** WRONG ***/
这一限制不会对我们造成什么损失:我们可以把a复制给一个指针变量, 然后改变该指针变量:
p=a;
while (*P != 0)
p++;
数组型实际参数(9改进版)
int find_largest(int a[], int n)
{
int i,max;
max = a[0];
for(i=1;i<n;i++)
if(a[i]>max)
max = a[i];
return max;
}
largest = find_largest(b, N);
这个调用会把指向数组b第一个元素的指针赋值给a,数组本身并没有被复制。
- 把数组型形式参数看作是指针会产生许多重要的结果:
- 在给函数传递普通变量时,变量的值会被复制;任何对相应的的形式参数的改变都不会影响到变量。反之,因为没有对数组本身进行复制,所以作为实际参数的数组是可能被改变的。
为了指明数组型形式参数不会被改变,可以在其声明中包含单词const:
int find_largest(const int a[], int n)
如果参数中有const,编译器会核实find_largest函数体中确实没有对a中元素的赋值。 - 给函数传递数组所需的时间与数组的大小无关。因为没有对数组进行复制,所以传递大
数组不会产生不利的结果。 - 如果需要, 可以把数组型形式参数声明为指针。声明a是指针就相当于声明它是数组。编译器把这两类声明看作是完全一样的。
- 对于形式参数而言,声明为数组跟声明为指针是一样的;
但是对变量而言, 声明为数组跟声明为指针是不同的。
声明
int a[10];
会导致编译器预留10个整数的空间,但声明
int *a;
只会导致编译器为一个指针变量分配空间。在后一种情况下, a不是数组,试图把它当作数组来使用可能会导致极糟的后果。例如,赋值
*a = 0; /… WRONG ***/
将在a指向的地方存储0。因为我们不知道a指向哪里,所以对程序的影响是无法预料的。
用指针作为数组名
- int a[10];
int *p = a;
p[i] 等价于 * (p+i)
处理多维数组的行
- 处理二维数组的一行中的元素,该怎么办呢?
再次选择使用指针变量p。
为了访问到第i行的元素,需要初始化p使其指向数组a中第i行的元素0:
p = &a[i][0];
对于任意的二维数组a来说。由于表达式a[i]是指向第i行中第一个元素(元素0)的指针
所以,上面的语句可以简写为
p = a[i];
为了了解上述简写的原理,回顾一下把数组取下标和指针算术运算关联起来的那个神奇公式:
对于任意数组a来说,表达式a[i]等价于*(a+i)。因此&a[i][0]等同于&(*(a[i]+0)),而后者等价于&*a[i],又因为 & 和 * 运算符可以抵消,也就等同于a[i]
下面的循环对数组a的第i行清零
int a[NUM_ROWS][NUM_COLS], *p, i;
for(p = a[i]; p<a[i]+NUM_COLS; p++)
{
*p = 0;
}
- 因为a[i]是指向数组a的第i行的指针,所以可以把a[i]传递给需要用一维数组作为实际参数的函数。换句话说,使用一维数组的函数也可以使用二维数组中的一行。
确定二维数组a中第i行的最大元素:
int find_largest(int a[], int n)
{
int i,max;
max = a[0];
for(i=1;i<n;i++)
if(a[i]>max)
max = a[i];
return max;
}
largest = find_largest(a[i], NUM_COLS);
处理多维数组的列
- 处理二维数组的一列中的元素就没那么容易了, 因为数组是按行而不是按列存储的。
下面的循环对数组a的第i列清零:
int a[NUM_ROWS][NUM_COLS], (*p)[NUM_COLS], i;
for(p = &a[0]; p < &a[NUM_ROWS]; p++)
{
(*p)[i] = 0;
}
- 这里把p声明为指向长度为NUM_COLS的整型数组的指针。
在(*p) [NUM_COLS]中,*p是需要使用括号的。
如果没有括号,编译器将认为p是指针数组,而不是指向数组的指针。
表达式p++把p移到下一行的开始位置。
在表达式(*p)[i]中, *p代表a的整行, 因此(*p)[i]选中了该行第i列的那个元素。(p)[i]中的括号是必要的,因为编译器会将p[i]解释为 *(p[i])。
用多维数组名作为指针
- 就像一维数组的名字可以用作指针一样, 无论数组的维数是多少都可以采用任意数组的名字作为指针。
但是,需要特别小心。思考下列数组:
int a[NUM_ROWS][NUM_COLS];
a不是指向a[0][0]的指针,而是指向a[0]的指针。
从C语言的观点来看,这样是有意义的。
C语言认为,a不是二维数组而是一维数组,且这个一维数组的每个元素又是一维数组。
用作指针时,a的类型是int (*)[NUM_COLS] (指向长度为NUM_COLS的整型数组的指针)。
-
了解这些有助于简化处理二维数组元素的循环。
例如,为了把数组a的第i列清零,可以用
for (p = &a[0]; p< &a[NUM_ROWS]; p++)
(*p)[i] = 0;
取代
for (p = a; p< a + NUM_ROWS; p++)
(*p)[i] = 0; -
另一种应用是巧妙地让函数把多维数组看成是一维数组。
例如,思考如何使用find_largest函数找到二维数组a中的最大元素。
我们把a(数组的地址)作为find_largest函数的第一个实际参数,
NUM_ROWS * NUM_COLS (数组a中的元素总数量)作为第二个实际参数:
largest = find_largest(a, NUM_ROWS * NUM_COLS); /* WRONG */
这条语句不能通过编译,因为a的类型为int ( *) [NUM_COLS]而find_largest函数期望的实际参数类型是int *。
正确的调用是:
largest = find_largest(a[0], NUM_ROWS * NUM_COLS);
a[0]指向第0行的元素0,类型为int *(编译器转换以后),所以这一次调用将正确地执行。
总结
*(b+i) 是取第i行指针
*(b+i)+j 偏移y
所以,b[i][j] 等价于 *( *(b+i)+j )