07、函数介绍
函数是代码块,主要用于执行特定的任务或计算并返回一个值。函数是我们编码不可或缺的一部分,有了函数的存在,代码才更具模块化、可重用性和可读性。
可以这么说,能做到合理封装好函数的程序员才是一个合格的程序员。
下面详细介绍。
正文
函数的声明与定义
声明
函数声明告诉编译器函数的名称、返回类型以及它接受的参数类型。声明通常放在头文件(.h
或.hpp
)或源文件的开始部分。
// 函数声明
int add(int a, int b);
定义
函数定义提供了函数的实际实现。定义包括函数体,即函数要执行的代码。
// 函数定义
int add(int a, int b) {
return a + b;
}
有看出区别吗?大体你可以认为函数参数表后面带分号的就是函数的声明,带大括号的就是函数的定义。
- 函数声明的格式:返回值类型 函数名(参数表);
- 函数定义的格式:返回值类型 函数名(参数表) { 语句块; }
在声明一个函数的时候,参数是没有实际值的,只是起到一个占位的作用,所以一般称为形式参数,也就是形参。
因为没有实际值,所以即便你声明函数时不添加参数的名字也是没问题的,比如:
int func(int, int);
这样也是可以的,但是平时编程过程中不建议这么写,当然像我上面那么写也是不优雅的,一般函数名和参数名字都要能清晰的表达用意,比如:
int sum(int first, int second) {
return first + second;
}
这样我们能明确知道这个函数作用就是求两数之和。
介绍了形参,还需要介绍与之对应的一个概念,就是实参,也就是实际参数。
比如:
int value = sum(a, b);
这里面调用了函数sum,传入了两个参数a和b,这里的a和b就是实际参数,它们是真正能起到实际作用的参数。
参数与返回值
返回值
函数可以返回一个值给调用者。返回值的类型在函数声明和定义中指定。
int multiply(int a, int b) {
return a * b;
}
在上述例子中,multiply
函数返回两个整数的乘积。
参数处理
函数参数用于在函数调用时向函数传递数据。参数的类型、数量和逻辑关系决定了函数如何接收和使用这些数据。
- 按值传递:参数在函数调用时被复制,函数内部对参数的修改不会影响外部变量。
void increment(int &x) {
x++;
}
- 按引用传递:参数传递的是变量的引用,函数内部对参数的修改会影响外部变量。
void increment(int &x) {
x++;
}
- 默认参数:允许为参数提供默认值,调用时可以省略这些参数。
void printInfo(string name, int age = 20) {
cout << "Name: " << name << ", Age: " << age << endl;
}
作用域与生命周期
作用域
变量的作用域决定了变量的可见性和可访问性。
- 局部变量:在函数内部声明的变量,其作用域仅限于该函数内部。
void example() {
int localVar = 10; // 局部变量
}
- 全局变量:在所有函数外部声明的变量,其作用域是整个程序。
int globalVar = 20; // 全局变量
生命周期
变量的生命周期决定了变量存在的时间。
- 局部变量的生命周期:从声明开始,到函数返回时结束。
void example() {
int localVar = 10; // 局部变量,生命周期从声明到函数返回
}
- 全局变量的生命周期:从程序开始到程序结束。
int globalVar = 20; // 全局变量,生命周期从程序开始到程序结束
递归函数
递归函数是一种调用自身的函数。递归函数必须有一个终止条件,否则会导致无限递归和程序崩溃。
递归函数的编写
递归函数主要涉及两个关键部分:
- 递归终止条件:当满足某个条件时,函数停止递归调用。
- 递归步骤:函数调用自身,但每次调用都向终止条件靠近。
举个经典的阶乘的例子:
// 计算阶乘的递归函数
int factorial(int n) {
if (n <= 1) { // 终止条件
return 1;
} else { // 递归步骤
return n * factorial(n - 1);
}
}
注意事项
- 递归深度:过深的递归调用可能导致栈溢出错误,要控制好递归的深度。
- 尾递归优化:某些编译器可以优化尾递归,使其效率更高,但不是所有编译器都支持这种优化。
什么是尾递归?可以自己尝试搜索下。
延伸
- 一个程序内,相同的函数可以声明无数次,但是只能有一个定义,如果有多个定义,编译器也会懵的,它会不知道究竟选择哪个好,所以它会报错,比如:
#include <iostream>
int func(int a, int b);
int func(int a, int b) {
return a;
}
int func(int a, int b) {
return b;
}
int main(){}
这种程序就会报错:
test.cc:10:5: error: redefinition of 'func'
int func(int a, int b)
^
test.cc:5:5: note: previous definition is here
int func(int a, int b)
^
1 error generated.
- 在调用某个函数时,这个函数一定要在调用之前声明或者定义过,比如:
#include <iostream>
int main() {
func(1, 2);
}
int func(int a, int b) {
return b;
}
这种形式它会报错:
test.cc:5:5: error: use of undeclared identifier 'func'
func(1, 2);
^
1 error generated.
尽管定义了函数func,但是它的调用是在上方,编译器是从上往下扫描代码的,在调用时它找不到函数的声明或者定义就会报错,要改动也很简单,在它上面加一个声明就好:
#include <iostream>
int func(int a, int b);
int main() {
func(1, 2);
}
int func(int a, int b) {
return b;
}
总结
函数是编程的基础,合理使用函数,可以提高代码的可读性、可维护性和可重用性,一定要掌握函数的灵活使用。
练习
- 编写一个函数
int factorial(int n)
,该函数接收一个整数n
作为参数,并返回n
的阶乘(n! = n * (n-1) * ... * 2 * 1
)。如果n < 0
,则返回-1
表示错误。 - 编写一个函数
void reverseString(char* str)
,该函数接收一个字符数组(C语言中的字符串)作为参数,并就地(in-place)反转该字符串。注意,字符串以\0
结尾。 - 编写一个函数
int gcd(int a, int b)
,该函数接收两个整数a
和b
作为参数,并返回它们的最大公约数(GCD)。你可以使用欧几里得算法来实现。
