一、函数是什么
函数是C语言的基本模块之一,它是一段执行特定任务的代码集合。使用函数可以提高代码的复用性,可维护性和可读性。一个函数通常由以下几个部分组成:
- 返回类型:函数执行完毕之后返回给调用者的数据类型。如果函数不需要任何返回值,则可以指定为void类型。
- 函数名:用来标识函数的名字。
- 参数列表:括号内的部分,定义了传递给函数的参数的类型和名称。如果没有参数,则括号内为空或写成void。
- 函数体:被花括号{}包围的部分,包含了函数需要执行的具体操作或指令。
C语言中,函数被分为库函数和用户自定义的函数。
二、库函数
为什么会有库函数?
在开发的过程中,每个程序员都有可能需要频繁的使用一些类似与打印,拷贝或者求n次幂等等这样的基础功能,这些并非业务性很强的代码,为了提高程序的可移植性和程序的效率,在C语言的基础库中提供了一系列类似的库函数,方便程序员进行开发。
库函数的种类有很多,不需要记住所有的库函数,这里推荐几个查询的工具:
三、自定义函数
Return_Type Fun_Name(para1, * )
{
statement;//语句项
}
Return_Type返回类型
Fun_Name 函数名
para1 函数参数
举个例子,写一个函数找出两个整数中的最大值。
#include <stdio.h>
int get_max(int a, int b)//在函数名之前的是函数类型,括号内是传入的两个形式参数
{
if (a >= b) return a;
else return b;
//也可以写成这种三目操作符的形式return (a > b) ? a : b;
}
int main()
{
int num1 = 10;
int num2 = 20;
int k = get_max(num1, num2);
printf("较大值为%d", k);
return 0;
}
四、函数参数
在C语言中,函数之间通过参数来通信,在谈论到这个问题的时候,是绕不开形参和实参的。
- 形参:在函数定义时声明的参数称为形参。形参是在函数头部声明的变量,用来接收调用函数时传入的实际参数的值。形参只存在于函数内部,并且只有在函数被调用时才被分配内存空间,所以叫形参,并且函数调用完成后就被自动销毁,因此只在函数中有效。
- 实参:在函数调用时实际传递给函数的参数称为实参。实参是在函数调用时提供的值或者变量,它们的实际值会被复制或者引用到对应的形参中。
#include <stdio.h>
// 函数声明
void swap(int a, int b); // 注意这里 a 和 b 是形参
int main() {
int x = 5, y = 10;
printf("Before swap: x = %d, y = %d\n", x, y);
swap(x, y); // 注意这里 x 和 y 是实参
printf("After swap: x = %d, y = %d\n", x, y);
return 0;
}
// 函数定义
void swap(int a, int b) { // 注意这里的 a 和 b 是形参
int temp = a;
a = b;
b = temp;
printf("Inside swap: a = %d, b = %d\n", a, b);
}
形参实例化之后相当于实参的一份临时拷贝。
五、函数调用
函数调用允许程序员执行之前定义好的函数代码块,以完成特定的任务。函数调用涉及以下几个步骤:
- 函数声明:在函数被调用之前,需要确保编译器知道该函数的存在。这通常通过函数原型声明来完成,声明告诉编译器函数的返回类型、名称以及它期望的参数类型和数量。
返回类型 函数名(参数类型列表);
- 函数定义:这是实际编写函数的地方,定义了函数执行的具体代码。
返回类型 函数名(参数类型 参数名) { // 函数体 }
- 函数调用:在程序的适当位置,可以通过函数名加上圆括号和必要的实参来调用函数。
函数名(实参列表);
传值调用与传址调用
#include <stdio.h>
void Swap1(int x, int y)//传值
{
int tmp = 0;
tmp = x;
x = y;
y = tmp;
}
void Swap2(int* px, int* py)//传地址
{
int tmp = 0;
tmp = *px;
*px = *py;
*py = tmp;
}
int main()
{
int num1 = 1;
int num2 = 2;
Swap1(num1, num2);
printf("Swap1::num1 = %d num2 = %d\n", num1, num2);
Swap2(&num1, &num2);
printf("Swap2::num1 = %d num2 = %d\n", num1, num2);
return 0;
}
传值调用:函数的形参和实参分别占有不同内存块,对形参的修改不会影响实参。
传址调用:
-
传址调用是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式。
-
这种传参方式可以让函数和函数外边的变量建立起真正的联系,也就是函数内部可以直接操作函数外部的变量。
使用场景:
- 当需要改变函数外部变量的状态时,通常使用传址调用。
- 如果只是简单地传递数据而不希望外部变量被改变,可以使用传值调用。
- 对于较大的数据结构,为了避免复制整个结构,通常使用传址调用。
六、函数的嵌套调用和链式访问
函数的嵌套调用:指的是在一个函数的体内调用另一个函数。这种调用方式可以实现多个功能的组合,使得程序更加模块化和易于维护。嵌套调用可以是单层的,也可以是多层的,甚至可以形成递归调用。
下面我们编写一个程序,该程序首先获取两个数的和,然后将这两个数乘以第三个数,我们可以使用嵌套调用的方式来实现:
#include <stdio.h>
// 函数声明
int add(int a, int b);
int multiply(int a, int b);
int main() {
int x = 5, y = 10, z = 3;
int result;
result = multiply(add(x, y), z); // 嵌套调用
printf("Result: %d\n", result);
return 0;
}
// 函数定义
int add(int a, int b) {
return a + b;
}
int multiply(int a, int b) {
return a * b;
}
在这个例子中,multiply(add(x,y),z)表示先调用add(x,y)得到和,然后将这个和作为 multiply函数的第一个参数,z作为第二个参数,最后得到的结果就是result。
函数的链式访问:在 C 语言中,“链式访问”(chained calls)并不是一种正式的语法结构,但它指的是通过连续调用多个函数来实现某种功能的方法。尽管 C 语言本身并不像某些现代编程语言那样支持直接的链式调用语法,但你仍然可以通过适当的函数设计和返回值来模拟链式调用的行为。简单来讲就是将一个函数的返回值作为另一个函数的参数。
#include <stdio.h>
#include <string.h>
int main()
{
char arr[20] = "hello";
int ret = strlen(strcat(arr,"bit"));
printf("%d\n", ret);
return 0;
}
#include <stdio.h>
int main()
{
printf("%d", printf("%d", printf("%d", 43)));
//结果是什么?
return 0;
}
七、函数的声明与定义
函数声明是指在函数被调用之前,感知编译器有关函数的信息,包括函数的返回类型、函数名以及它接受的参数类型和数量但不包括函数的具体实现。函数声明的主要目的是为了让编译器在编译时能够检查函数调用是否正确。函数声明通常出现在需要使用该函数的位置之前,或者放在一个头文件中供其他源文件引用。
int add(int a, int b); // 函数声明
函数定义不仅告诉编译器函数的类型、名称和参数,还包括了函数具体做了什么。函数定义通常包含在源文件中,并且只能定义一次。
int add(int a, int b) {
return a + b; // 函数定义
}
区别:
- 声明:只告诉编译器函数的存在及其签名,不包含函数体。
- 定义:除了包含函数的签名外,还包括函数体内的具体实现。
下面是一个完整的示例,演示函数声明和定义的应用:
#include <stdio.h>
// 函数声明
int add(int a, int b);
int main() {
int x = 5, y = 10;
int result;
// 函数调用
result = add(x, y);
printf("The sum of %d and %d is %d\n", x, y, result);
return 0;
}
// 函数定义
int add(int a, int b) {
return a + b;
}
八、函数递归
什么是递归?
程序调用自身的编程技巧称为递归。 递归做为一种算法在程序设计语言中广泛应用。一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需少量的程序就可以描述出解题过程需要的多次重复计算,大大地减少了程序的代码量。 递归的主要思考方式在于:把大事化小。
递归的两个必要条件:
- 递归存在限制条件,当满足这个限制条件的时候,递归便不再继续。
- 每次递归之后越来越接近这个限制条件。
#include <stdio.h>
// 递归函数定义
int factorial(int n) {
// 基本情况: 如果 n 是0或1,则阶乘为1
if (n == 0 || n == 1) {
return 1;
}
// 递归步骤: 计算 n * (n-1)!
return n * factorial(n - 1);
}
int main() {
int num = 5;
printf("The factorial of %d is %d\n", num, factorial(num));
// 测试其他数值
num = 10;
printf("The factorial of %d is %ld\n", num, (long)factorial(num)); // 使用long防止溢出
return 0;
}
- 基本情况:当n为 0 或者 1 时,我们知道factorial(n)的值为 1。
- 递归步骤:如果n大于 1,那么factorial(n)就等于 n乘以factorial(n-1)。这意味着我们将问题分解成了更小的部分。
另外递归还需要注意以下部分:
-
许多问题是以递归的形式进行解释的,这只是因为它比非递归的形式更为清晰。
-
但是这些问题的迭代实现往往比递归实现效率更高,虽然代码的可读性稍微差些
-
当一个问题相当复杂,难以用迭代实现时,此时递归实现的简洁性便可以补偿它所带来的运行时开销。