C语言之函数(上)

在学习了指针以后,我们的C语言学习就算真正入门了。那么,随着我们自己编写的程序规模越来越大,我们写的代码也越来越繁杂,经常会碰到大脑不够用,或者名字不会起了这类看似很搞笑的问题。再者,就是往往要更改代码的时候牵一发而动全身,稍微出差错,就只能推倒重来。

那么,在做开发,尤其是多人协作的大型开发的时候,程序模块化是十分重要的,你只需要预留一些供他人使用的必须的接口,然后把你定义的方法写个文档方便他人开发就行了。那么这种思维,就是结构化编程

比如,我们经常使用的printf函数,就是C语言开发者写好在stdio.h里面的。有了这个函数,我们就可以很简单地输出文本,而不需要去关注底层到底是如何实现的。你会发现,有函数,我们就可以把更多的注意力放在逻辑的实现上,而不需要去关注太多的细枝末节。

函数的定义和声明

绪论

虽然C语言的内置函数已经十分丰富,可以帮我们实现大部分的问题。什么字符串的处理啊,数学方面的计算啊,等等……但是,作为一门高级语言,C语言还可以让我们自己来定义函数,把我们的一些方法抽象封装出来,用于其他的对象上。

我们举个简单的例子,封装一个输出字符图的函数:

//Example 01
#include <stdio.h>
//------定义开始------//
void print_house(void)
{
    printf("¤╭⌒╮ ╭⌒╮:∴★∵**☆.\n");
    printf("╱◥██◣ :∴☆∵**★.\n");
    printf("|田︱田田|:∴★∵**☆.\n");
    printf("╬╬╬╬╬╬╬╬╬╬╬╬╬╬ \n");
}
//------定义结束------//
int main(void)
{
    print_house();//调用函数
    return 0;
}

运行结果如下:

//Consequence 01
¤╭⌒╮ ╭⌒╮:∴★∵**☆.
╱◥██◣ :∴☆∵**★.
|田︱田田|:∴★∵**☆.
╬╬╬╬╬╬╬╬╬╬╬╬╬╬ 

函数的定义

定义函数的方法,和我们使用main函数的方法差不多:

类型名 函数名(参数列表)
{
    函数体
}
  • 类型名就是函数返回值的类型,如果不希望函数返回任何类型,那么就应该使用void(无类型,表示没有返回值)。
  • 函数名就是函数的名字,自己想怎么命名都可以,但是不要和保留字重复。
  • 参数列表指定了参数的类型名字,如果不需要传递参数,则写上void即可。
  • 函数体是指函数的具体算法,是函数中关键部分

函数的声明

所谓的声明,就是将函数先告诉编译器,但是具体算法,则在main函数之后开发。

如果不做声明,把上面的程序改成这样则会出错:

//Example 01
#include <stdio.h>
int main(void)
{
    print_house();//调用函数
    return 0;
}
//------定义开始------//
void print_house(void)
{
    printf("¤╭⌒╮ ╭⌒╮:∴★∵**☆.\n");
    printf("╱◥██◣ :∴☆∵**★.\n");
    printf("|田︱田田|:∴★∵**☆.\n");
    printf("╬╬╬╬╬╬╬╬╬╬╬╬╬╬ \n");
}
//------定义结束------//

因为道理上,程序是从上往下编译的,如果不提前告知,那么main函数里面的print_house,编译器便不知道这是个什么东西。

如果你做了函数的声明,那么这个程序就可以正常执行:

//Example 01
#include <stdio.h>
void print_house(void);//声明函数
int main(void)
{
    print_house();//调用函数
    return 0;
}
//------定义开始------//
void print_house(void)
{
    printf("¤╭⌒╮ ╭⌒╮:∴★∵**☆.\n");
    printf("╱◥██◣ :∴☆∵**★.\n");
    printf("|田︱田田|:∴★∵**☆.\n");
    printf("╬╬╬╬╬╬╬╬╬╬╬╬╬╬ \n");
}
//------定义结束------//

原则上来说,函数必须先定义,再使用。况且在以后的开发尤其是团队协作的情况下,先使用函数,后开发函数的情况比较多,因此养成先定义,后使用的良好习惯是最好的。

函数的参数和返回值

向函数传入参数,可以使函数的功能更加丰富。比如,我们刚刚定义了一个输出字符画的函数,但是这个函数无论在什么时候,它执行出来的结果都是一样的。无法实现更加个性化的功能。比如:

//Example 02
#include <stdio.h>
void print(int);

int main(void)
{
	int a;
	scanf("%d", &a);
	print(a);
	return 0;
}

void print(int a)
{
	for (int i = a; i > 0; i--)
	{
		printf("-");
	}
}

运行结果如下:

//Consequence 02 - 01
5
-----
//Consequence 02 - 02
7
-------

你看,输出的内容会随着用户输入的数字而改变,就像定制一样。

那么函数的返回值又是什么呢?

我们发现,我们刚刚写的函数,都没有返回值,因为在函数体的执行过程中,我们要得到的结果已经执行完毕了,并不需要返回什么东西。但是,有些函数就不一样了。

比如,我们现在要创建一个函数,能够实现阶乘,这时候,就需要返回值了。

//Example 03 - 迭代法
#include <stdio.h>
int factorial(int);

int main(void)
{
    int a;
    scanf("%d",&a);
    printf("%d", factorial(a));
    return 0;
}

int factorial(int a)
{
    int sum = 1;
    for (int i = a; i > 0; i--)
    {
        sum *= i;
    }
    return sum;
}
//Example 03 - 递归法
#include <stdio.h>
int factorial(int);

int main(void)
{
    int a;
    scanf("%d", &a);
    printf("%d", factorial(a));
    return 0;
}

int factorial(int a)
{
    int sum;
    if (a == 1)
    {
        return 1;
    }
    else
    {
        return a * factorial(a - 1);
    }
}

执行结果如下:

//Consequence 03
5
120

形参和实参

所谓形参,就是形式参数,如:

...
int f(int a, int b);
...

这其中的ab就是形式参数,此时只是作占位符而已,而在函数真正使用的时候:

...
int main(void)
{
    f(x, y);
    ...
}
...

此时的xy就是实参。因为此时传递进去的值是真正程序里面拥有的值,是实实在在的。

其实形参和实参的作用就是用来传递数据的,当我们自己定义的函数被调用的时候,实参会将值传递给形参(单向传递)。形参变量只有在函数被调用的时候才会分配内存,调用结束后,立即释放内存,因此形参变量只有在函数内部有效,对函数外的变量不影响

传值和传址

//Example 04 - 01
#include <stdio.h>
void swap(int, int);
void swap(int a, int b)
{
    int temp;
    printf("swap中,交换前:a = %d, b = %d\n", a, b);
    temp = a;
    a = b;
    b = temp;
    printf("swap中,交换前:a = %d, b = %d\n", a, b);
}

int main(void)
{
    int a = 1, b = 2;
    printf("main中,交换前:a = %d, b = %d\n", a, b);
    swap(a, b);
    printf("main中,交换前:a = %d, b = %d\n", a, b);
    return 0;
}

运行结果为:

//Consequence 04 - 01
main中,交换前:a = 1, b = 2
swap中,交换前:a = 1, b = 2
swap中,交换前:a = 2, b = 1
main中,交换前:a = 1, b = 2

可以看到,在swap函数内,传入的值被互换,但是在主函数内,值依旧没有被改变。因此可以得出结论:函数内部无法改变实参的值

但是,如果换成指针会怎样呢?

//Example 04 - 02
#include <stdio.h>
void swap(int*, int*);
void swap(int* a, int* b)
{
    int temp;
    printf("swap中,交换前:a = %d, b = %d\n", *a, *b);
    temp = *a;
    *a = *b;
    *b = temp;
    printf("swap中,交换前:a = %d, b = %d\n", *a, *b);
}

int main(void)
{
    int a = 1, b = 2;
    printf("main中,交换前:a = %d, b = %d\n", a, b);
    swap(&a, &b);
    printf("main中,交换前:a = %d, b = %d\n", a, b);
    return 0;
}

运行结果为:

//Consequence 04 - 02
main中,交换前:a = 1, b = 2
swap中,交换前:a = 1, b = 2
swap中,交换前:a = 2, b = 1
main中,交换前:a = 2, b = 1

欸?怎么这会儿就能变了呢?有人可能会说,Harris你是不是刚刚骗我?

其实并不是,我们看这句:swap(&a, &b);,我们其实传入的实参是两个变量的地址。

打个比方,如果变量a的在内存里面住的是1号房间,变量b在内存中住的是2号房间。那么函数处理了之后,他俩住的地方并没有改变,也就是说,两个变量的内存地址并没有改变,只是函数把这个地址对应的值改变了而已。函数内对指针进行解引用,实际上就是间接地访问了变量的值。

传数组

既然是可以传递指针,那么数组按理来说,应该也是可以传的。

//Example 05
#include <stdio.h>
void get_array(int);
void get_array(int a[10])
{
	for (int i = 0; i < 10; i++)
	{
		printf("a[%d] = %d\n", i, a[i]);
	}
}

int main(void)
{
	int a[10] = { 1,2,3,4,5,6,7,8,9,10 };
	get_array(a);
	return 0;
}
//Consequence 05
a[0] = 1
a[1] = 2
a[2] = 3
a[3] = 4
a[4] = 5
a[5] = 6
a[6] = 7
a[7] = 8
a[8] = 9
a[9] = 10

如果我们尝试着在函数内改变数组的值呢?

//Example 06
#include <stdio.h>
void get_array(int);
void get_array(int a[10])
{
	a[4] = 1;//更改一个值
	for (int i = 0; i < 10; i++)
	{
		printf("a[%d] = %d\n", i, a[i]);
	}
}

int main(void)
{
	int a[10] = { 1,2,3,4,5,6,7,8,9,10 };
	get_array(a);
	return 0;
}
//Consequence 06
a[0] = 1
a[1] = 2
a[2] = 3
a[3] = 4
a[4] = 1
a[5] = 6
a[6] = 7
a[7] = 8
a[8] = 9
a[9] = 10

可以看到,在主函数里面输出数组,也是被改变了的。于是可以断定,其实传入的并不是一个数组,而是这个数组的首地址而已。

可变参数

可能有的同学会纳闷,我们定义的函数,都是给参数预留了位置的,那如果像printf()这种函数,它的参数是取决于占位符的数量,这种函数是怎么定义的呢?

这里,我们就要将一个以前没有用过的头文件<stdarg.h>。这个头文件中有一个类型和三个宏是需要用到的:

一个类型是va_list,三个宏是va_start va_arg va_end,其中,va就是指variable-argument(可变参数)

//Example 07
#include <stdio.h>
#include <stdarg.h>
int sum(int n, ...);
int sum(int n, ...)//第一个参数n代表后面可变参数的数量,三个点代表不确定参数个数
{
	int sum = 0;
	va_list arg;//定义参数列表
	va_start(arg, n);//初始化参数列表,n为第一个参数的名称
	for (int i = 0; i < n; i++)
	{
		sum += va_arg(arg, int);//依次获取参数值
	}
	va_end(arg);//结束参数列表
	return sum;
}

int main(void)
{
	int result;
	result = sum(5, 1, 2, 3, 4, 5);
	printf("result = %d\n", result);
	return 0;
}

结果如下:

//Consequence 07
result = 15

指针函数和函数指针

指针函数

函数的类型,实际上就是函数返回值的类型,那么顾名思义,指针函数就是返回指针的函数。

//Example 08
#include <stdio.h>
char* getWord(char);
char* getWord(char c)
{
    switch (c)
    {
    case 'A':return "Apple";
    case 'B':return "Boy";
    case 'C':return "Cat";
    case 'D':return "Dog";
    default:return "None";
    }
}
int main(void)
{
    char input;
    scanf("%c", &input);
    printf("%s", getWord(input));
    return 0;
}

运行结果:

//Consequence 08
A
Apple

有的小伙伴可能会说,为啥这个switch语句中不用加break呢?

因为return实际上就代表函数执行的结束,因此不会执行到下面的case

这个例子就是让函数返回字符串(指针)。

另外,不要将函数中局部变量的值作为返回值,因为局部变量的值的作用域(有效范围)只有函数内部,因此返回局部变量是不合法的(详细将在下一节中讲到)。

函数指针

指针函数 -> int* f();
函数指针 -> int (*p)();

本质上,函数表示法就是指针表示法。因为函数的名字经过取值会变成函数的地址,所以在定义了函数指针以后,给它传递一个已经被定义的函数名,即可通过该指针进行调用。

//Example 09
#include <stdio.h>
int square(int);
int square(int a)
{
    return a * a;
}
int main(void)
{
    int num;
    int (*fp)(int);
    scanf("%d", &num);
    fp = square;
    printf("%d * %d = %d\n", num, num, (*fp)(num));
    return 0;
}

结果如下:

//Consequence 10
5
5 * 5 = 25

这里fp = square可以写成fp = &square(*fp)(num)可以写成fp(num),可能更加符合我们之前的习惯。

函数指针作为参数

函数指针也可以作为参数传递,这样函数就可以实现更加丰富的功能。

//Example 11
#include <stdio.h>

int add(int, int);
int sub(int, int);
int calc(int (*fp)(int, int), int, int);

int add(int a, int b)
{
    return a + b;
}
int sub(int a, int b)
{
    return a - b;
}
int calc(int (*fp)(int, int), int a, int b)
{
    return (*fp)(a, b);
}

int main(void)
{
    printf("1 + 2 = %d\n", calc(add, 1, 2));
    printf("1 - 2 = %d\n", calc(sub, 1, 2));
    return 0;
}

运行结果为:

//Consequence 11
1 + 2 = 3
1 - 2 = -1

函数指针作为返回值

假设现在有个问题,让用户输入一个表达式,然后根据用户输入的运算符来确定应该调用哪一个函数进行运算。

//Example 12
#include <stdio.h>

int add(int, int);
int sub(int, int);
int calc(int (*fp)(int, int), int, int);
int (*select(char op))(int, int);

int add(int a, int b)
{
    return a+b;
}
int sub(int a, int b)
{
    return a-b;
}
int calc(int (*fp)(int, int), int a, int b)
{
    return (*fp)(a, b);
}
int (*select(char op))(int, int)
{
    switch(op)
    {
        case '+':return add;
        case '-':return sub;
    }
}
int main(void)
{
    int a, b;
    char op;
    int (*fp)(int, int);
    printf("请输入一个式子,如1+2:");
    scanf("%d%c%d", &a, &op, &b);
    fp =  select(op);
    printf("%d %c %d = %d\n", a, op, b, calc(fp, a, b));
    return 0;
}

运行结果如下:

//Consequence 12
请输入一个式子,如1+2:3+4
3 + 4 = 7

函数的知识太多太繁杂,所以Harris这次采用分节式来写博客。本期的就到这里啦!如果没有理解的,好好去消化下,理解了的就等我下一篇吧!