C函数

函数是一段可以重复使用的代码,包含参数和返回值。

函数定义

语法:

返回值类型 函数名称(类型1 参数1,类型2 参数2)
{

}

定义一个简易计算器

int calc(int a, int b, char ch)
{
    switch (ch) {
    case '+':return a + b;
    case '-':return a - b;
    case '*':return a / b;
    case '/':return a * b;
    case '%':return a % b;
    }
    return -1;
}

函数声明和定义

函数需要先声明才能够调用。如果只有定义部分,调用代码必须在定义代码下方。

一般将多处使用的函数进行声明,放入头文件,函数定义代码和调用者通过头文件引用。

函数声明只需要函数的返回类型、名称和参数类型,其中的形参名称可以不写。建议保留参数名称。

int calc(int a, int b, char ch);

函数调用

函数声明之后,就可以进行使用。

语法:

函数名(参数1,参数2,参数n);
int a = calc(100,200,'+');
int b = calc(9,6,'/');

参数的传递

调用函数时,实参通过栈复制了一个副本,赋值给了形参,调用结束时,形参所占用的栈内存被回收。

由于是副本,对于形参的赋值操作,是不会作用到实参的。要影响实参,需要使用指针。

函数的调用约定

调用约定决定了参数的压栈(即复制)顺序,以及参数被谁回收。

调用约定 压栈顺序 回收
_cdecl 从右向左 调用者
_stdcall 从右向左 函数自身

不定参数

不定参数是指函数的最右边的参数数量可以是任意多个。

标准库中的printf,scanf等都支持不定参数。

要使用不定参数,需要包含stdarg.h头文件,使用其中的类型和宏。

代码 类型 功能
va_list 类型 表示不定参数
void va_start(va_list ap, last) 开始不定参数,last是不定参数左边的参数
type va_arg(va_list ap, type) 获取参数
void va_end(va_list ap) 结束不定参数

求和函数

int sum(int n, ...)
{
    int total = 0;
    int i = 0;

    va_list list;
    va_start(list, n);

    for (i = 0; i < n; i++)
    {
        int value = va_arg(list, int);
        total += value;
    }

    va_end(list);

    return total;
}

传递不定参数

不能在包含不定参数的函数中,把本函数的不定参数直接传递给另一个不定参数。

void f1(int n,...)
{
    //代码
}

void f2(int n,...)
{
    //f1(n,...);//这种写法是不成立的
}

需要为函数定义一个类型为va_list的参数来传递不定参数。


void vf1(int n,va_list args)
{
    //代码
}

void f1(int n,...)
{
    va_list args;
    va_start(args,n);
    vf1(args);
    va_end(args);
}

void f2(int n,...)
{
    va_list args;
    va_start(args,n);
    vf1();
    va_end(args);
}

提供va_list作为参数的函数,再提供另一个带有不定参数的函数调用它。这样,外部也可以选择是直接使用不定参数调用,还是传递自己的不定参数。

标准输出库stdio.h中的vsprintf函数是一个例子。

递归

递归是指在一个函数被调用但还未返回时,再次被调用。

递归存在直接递归和间接递归。

无限递归

void f()
{
    f();
}

不设置退出条件的递归,会无限次的调用自身,导致栈空间被耗尽,产生栈溢出。

带条件的递归

int sum(int n)
{
    if(n<=1)return 1;
    return n+sum(n-1);
}

int main()
{
    printf("%d\n",sum(10));
    return 0;
}

递归是将一个问题分割为更小的问题,直到达到一个最基本的问题。

以上代码问题是求n个数字的和,分割跟小的问题是求比自身小的数的和,最基本问题是求1个数的和。

从功能上看,递归和循环是一致的。但是参数的传递是需要使用栈空间的,递归需要额外的栈空间。因此,大多数情况,我们不使用递归,而使用循环。 然而,有些问题使用递归更加简单明了。

汉诺塔问题

有3个塔源A、中转B、目标C,A上面放有n个盘子,由大到小堆放,现在需要按照原样移动到C。要求是:

这个问题使用递归便很好解决。

必定存在一个状态,A是一个最大的盘子,B放n-1个盘子,C没有盘子,以此来将最大的盘子从A移动到C。

于是问题变成了:将n-1个盘子从A移动到B,将第n个盘子从A移动到C,再将n-1个盘子从B移动到C。 将n-1个盘子从A移动到B是一个子问题,A是A(源),B是C(目标),C是B(中转)。 将n-1个盘子从B移动到C也是一个子问题,B是A(源),C是C(目标),A是B(中转)。 当n为1时,问题是最简问题,将一个盘子将A移动到C。

void hanoi(char a,char b,char c,int n)
{
    if(n==1)
    {
        //当n为1时,问题是最简问题,将一个盘子将A移动到C。
        printf("%c->%c\n",a,c);
    }else
    {
        //将n-1个盘子从A移动到B是一个子问题,A是A(源),B是C(目标),C是B(中转)。
        //将第n个盘子从A移动到B
        //将n-1个盘子从B移动到C也是一个子问题,B是A(源),C是C(目标),A是B(中转)。
        hanoi(a,c,b,n-1);
        printf("%c->%c\n",a,c);
        hanoi(b,a,c,n-1);
    }
}

int main()
{
    int n=3;
    printf("input number of towler:");
    scanf("%d",&n);
    //print(10);
    hanoi('A','B','C',n);
    return 0;
}

使用递归时,参数越多,栈的浪费越多,可以自行定义栈结构,将递归转换为循环。

函数的本质

函数的本质是一段代码,通过调用约定规定如何传递参数和返回值。

函数名就是一个地址,知道这个地址,以及调用约定、参数和返回值,就可以使用这个函数。

打印函数的地址。

printf("%p\n",printf);