三十三.认清函数的真面目
声明和定义:1.程序中的声明可理解为预先告诉编译器实体的存在,如:变量,函数等
2.程序中的定义明确指示编译器实体的意义
函数参数:1.函数参数在本质上与局部变量相同,都是在栈上分配空间
2.函数参数的初始值是函数调用时的实参数
int f(int i,int j)
{
printf("%d,%d\n",i,j);
}
int main()
{
int k = 1;
f(k,k++);
printf("%d\n",k);
return 0
}
gcc输出:2,1,2 在k++给函数形参传递的时候,先把k存入一个栈上临时变量temp,结算后直接将temp传给函数,所以还是1
但是k传递给函数是直接在内存中取值,所以是2
解析:函数参数的求值顺序依赖于编译器的实现
C语言中大多数运算符对其操作数求值的顺序都是依赖于编译器的实现,例如:int i = f() * g();f和g的调用顺序取决于编译器的实现
程序中的顺序点:指的是执行过程中修改变量值的最晚时刻,在程序到达顺序点的时候,之前所做的一切操作必须反映到后续的访问中
1.每个完整表达式结束时
2.&&,||,?:,以及逗号表达式的每个运算对象计算之后
3.函数调用中对所有实际参数的求值完成之后()
#include <stdio.h>
int main()
{
int k = 2;
int a = 1;
k = k++ + k++;
printf("k = %d\n", k);
if( a-- && a )
{
printf("a = %d\n", a);
}
return 0;
}
输出:k = 6; k = k++ + k++;结算点在“;“,所以在;的位置才进行结算,此时加法已经进行完毕。k=2+2,然后结算k,k++,k++,所以k=6;
这个结算点理论c语言规范并没有明确规定,属于编译器自行判断范围,可以说没有正确答案
三十四.可变参数与宏分析
可变参数:参数可变函数的实现依赖于stdarg.h,va_list变量与va_start,va_end和va_arg配合使用能够访问参数值
例子:
#include <stdio.h>
#include <stdarg.h>
float average(int n, ...)
{
va_list args;
int i = 0;
float sum = 0;
va_start(args, n);
for(i=0; i<n; i++)
{
sum += va_arg(args, int);
}
va_end(args);
return sum / n;
}
int main()
{
printf("%f\n", average(5, 1, 2, 3, 4, 5));
printf("%f\n", average(4, 1, 2, 3, 4));
return 0;
}
可变参数的限制:1.可变参数必须从头到尾按照顺序逐个访问,无法直接访问参数列表中间的参数值
2.参数列表中至少要存在一个确定的命名参数
3.可变参数宏无法判断实际存在的参数的类型
4.可变参数宏无法判断参数的实际类型
警告: va_arg中如果指定了错误的类型,那么结果是不可预测的。
三十五.函数调用行为
活动记录:1.临时变量域:用来存放临时变量的值,如k++的中间结果
2.局部变量域: 用来存放函数本次执行中的局部变量
3.机器状态域:用来保存调用函数之前有关机器状态的信息,包括各种寄存器的当前值和返回地址等
4.实参数域:用于保存函数的实参信息
5.返回值域:为调用者函数保存返回值
参数入栈:函数参数的计算次序是依赖编译器的实现,参数入栈的顺序取决于调用约定
调用约定:描述参数是怎么传递到栈空间,以及栈空间由谁维护
参数传递顺序:从右到左依次入栈-----__stdcall,__cdecl,__thiscall
从左到右依次入栈-----__pascall,__fastcall
注:以上均为修饰符修饰函数
调用堆栈清理:1.调用者清除栈;2. 被调用函数返回后清除栈
小结:1.函数调用是c语言的核心机制
2.活动记录中保存了函数调用以及返回所需要的一切信息
3.调用约定是调用者和被调用者之间的调用协议,常用于不同开发者编写的库函数之间
三十六.函数递归和函数设计技巧
递归函数:1.递归点--以不同参数调用自身;2.出口--不再递归调用
函数设计技巧:1.不要在函数中使用全局变量,尽量让函数从意义上是一个独立的功能模块
2.参数名要能够体现参数的意义
3.如果参数是指针,且仅作输入参数用,则应在类型前加const,以防止该指针在函数体内被意外修改
void str_copy(char* str_dest,const char * str_src)
4.不要省略返回值的类型,如果函数没有返回值,那么应声明为void类型
5.在函数体的入口处,对参数的有效性进行检查,对指针的检查尤为重要
6.语句不可返回指向“栈内存”的指针,因为该内存在函数体结束时被自动销毁
7.函数体的规模要小,尽量控制在80行代码之内
8. 相同的输入应当产生相同的输出,尽量避免函数带有记忆功能
9.避免函数有太多的参数,参数尽量控制在4个以内
10.有时函数不需要返回值,但为了增加灵活性,如支持链式表达,可以附加返回值
char s[64];
int len = strlen(strcpy(s,"android"));
注:getchar()返回的是int型,不能用char型变量判断返回值,因为int型包括EOF的定义。