在学习了指针以后,我们的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);
...
这其中的a
和b
就是形式参数,此时只是作占位符而已,而在函数真正使用的时候:
...
int main(void)
{
f(x, y);
...
}
...
此时的x
和y
就是实参。因为此时传递进去的值是真正程序里面拥有的值,是实实在在的。
其实形参和实参的作用就是用来传递数据的,当我们自己定义的函数被调用的时候,实参会将值传递给形参(单向传递)。形参变量只有在函数被调用的时候才会分配内存,调用结束后,立即释放内存,因此形参变量只有在函数内部有效,对函数外的变量不影响。
传值和传址
//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这次采用分节式来写博客。本期的就到这里啦!如果没有理解的,好好去消化下,理解了的就等我下一篇吧!