函数
函数是能够实现特定功能的程序模块,它可以是只有一条语句的简单函数,也可以是包含多种子函数的复杂函数。函数之间可以相互调用,可以将联系密切的语句放到一个函数内,也可以将复杂的函数分解为多个子函数。熟练掌握函数可以将程序设计得更加合理。
1. 函数的声明和定义
C++程序的入口都位于main()
函数中。除此之外,开发者可以调用系统内置的库函数,还可以自定义很多函数。main()
函数可以调用其他函数,其他函数之间也可以相互调用。
1.1 函数定义
一个函数应该包括函数头和函数体,语法如下:
返回值类型 函数名(参数列表){
变量声明;
语句块;
}
函数头是函数的入口,包括返回值类型、函数名和形式参数列表,标志着一段函数代码的开始。其中返回值类型可以是整型、字符型、指针型、或对象,函数名应符合标识符命名规则;参数列表是由各种类型变量组成的列表,参数之间用逗号分割。
根据功能,可以将函数分为字符函数、日期函数、数学函数、图形函数、内存函数等。
1.2 函数的声明
调用函数之前,要先对函数进行声明。函数定义是为了让编译器知道有这么一个函数(并不需要知道具体功能),以及函数的名称、参数、返回值类型等信息。
小栗子: 定义一个函数,返回两个数的和。
#include<iostream>
using namespace std;
int add(int x, int y);
int main () {
int res = add(1, 2);
cout << res << endl;
return 0;
}
int add(int x , int y) {
return x + y;
}
2. 函数的参数及返回值
声明函数后,可以在后续的代码中调用该函数,并通过传递参数实现数据间的传递。标准C++是一种强制类型检查语言,调用函数之前必须把函数的参数类型和返回值类型告知编译器。
2.1 函数的形参和实参
声明和定义函数时,函数名后面括号中的参数称为形式参数,简称形参。这些参数只是定义了类型,在实际参数传入前并没有实际意义。调用函数的过程就是真正使用函数的过程,因此调用函数会传递一些实际参与运算的参数给被调用函数,简称实参。
1、定义函数时指定的形参,在未出现函数调用前,并不占用内存中的存储单元。只有在发生函数调用时,函数的形参才被分配内存单元。在调用结束后,所占的内存单元将被释放。
2、实参是确定的值。在调用时将实参的值赋值给形参,如果形参是指针类型,就将地址值传递给形参。
3、实参与形参的类型相同。
4、实参与形参之间是单向传递,只能由实参传递给形参,而不能由形参传递给实参。
实参与形参之间存在一个分配内存空间和参数值传递过程,这个过程是在函数调用时发生的,C++支持引用型变量,但没有值传递过程。
说明
实参可以是常量、变量、数组或指针,还可以是表达式。
2.2 函数的调用与返回值
2.2.1 函数的调用
函数调用发生时,会立即暂停主调函数,转而执行被调用函数中的程序段。直到被调用函数执行完毕,才会再次回到主调函数,继续执行后面的语句。
函数调用的情况说明:
- 被调用的函数必须是已经存在的函数
- 如果使用库函数,需要在主函数中使用
#include
预编译指令引入对应的头文件 - 如果使用用户自定义函数,需要在主调用函数中声明被调用函数
函数的调用方式有三种,语句调用、表达式调用、函数参数调用。
1、函数可以作为一个独立语句出现,这是最常见的调用方式,在语句调用中函数可以有返回值,也可以没有返回值。
add(2, 3);
2、函数调用可以出现在表达式中,此时函数必须返回一个确定的值,作为表达式运算的一部分。
int i = 1 + add(2, 3);
3、函数调用可以出现在函数参数中,此时函数的返回值作为实际参数使用。
doSomething(1, add(2, 3));
2.2.2 函数的返回值
返回值是函数被调用时,执行函数体返回给主调函数的值。函数的返回值通过return
语句返回。
return
语句可以将表达式的值返回给主调函数。关宇返回值的说明如下:
1、函数返回值的类型和函数定义时的类型标识符应保持一致。如果两者不一致,则以函数类型为准,自动进行转化。
2、如函数值为整数,在函数定义时可以省略类型标识符(不推荐)。
3、函数中允许多个return
语句,但每次调用只能有一个被执行。
4、需要返回函数值的函数,可以明确定义为空类型,类型表示为void
。
5、类型标识符为void
的函数不能进行赋值运算及值传递。
说明
为了降低程序出错率,凡是不要求返回值的函数都应定义为void空类型。
2.2.3 传值调用
主函数和被调函数之间存在数据传递关系,主函数需要将实参传递给被调用函数的形参,这种调用方式称为传值调用。传值调用是函数调用的基本方式。需要注意传值调用是单向的,只能把实参传递给形参,却无法将形参的值传回给实参。
小栗子: 定义两个变量x, y。实现一个函数将x, y的值进行交换。
#include<iostream>
using namespace std;
void swap(int a, int b);
int main() {
int x = 5;
int y = 10;
swap(x, y);
cout << 'x' << x << 'y' << y << endl;;
return 0;
}
void swap(int a, int b) {
int temp;
temp = a; // 将a的值赋值给临时变量
a = b; // 将b的值赋值给a
b = temp; // 将赋值给a的临时变量赋值给b就实现了变量值的交换。
cout << 'a' << a << 'b' << b << endl;
}
通过输出可以看到,在swap函数中确实是将x, y的值进行了交换,但是在主函数中x, y的值并没有发生交换,这就是前面提到的,“传值调用是单向的,只能把实参传递给形参,却无法将形参的值传回给实参”,本质上是复制了一份实参的值给形参,形参的值发生了交换,并没有影响到实参。
提示
C++中,参数传递方式有两种:值传递和引用传递。值传递时,程序会将实参值复制一份,传递给函数的形参,改变副本的值不会影响实参;引用传递时,程序会将实参的内存地址作为参数传递给函数形参,改变地址内存放的内容,同时会影响到实参。
要想通过函数调用交换变量的值,必须通过指针和引用传递方式来实现。
2.3 空函数
空函数就是没有参数和返回值,函数作用域为空的函数。
void doSomething() {}
调用空函数时,程序不会执行任何操作。有啥用呢?实际开发中,各个功能模块需要不同的函数去实现,第一阶段只设计最基本的模块,其他一些次要的功能会在后续补充。此时,就需要将后续要补充的函数先写上一个空函数,占用一个位置。
2.4 默认参数
调用带参函数时,如果经常需要传递同一个值,不妨在定义该函数时设置一个默认参数值。设置默认参数的好处是:调用函数时如果省略参数,表示使用默认值做为函数的实际参数。
int add(int x = 10, int y = 20);
int add (int x, int y) {
return x + y;
}
上述代码的作用是返回两个整数值的和,如果传递了参数则正常返回传递参数的和,如果没有传递参数,则返回值就是恒定30。
警告
如果函数有多个参数,应保证有默认值的参数出现在参数列表的右侧,无默认值的参数出现在参数列表的左侧,即默认参数不能出现在非默认值参数的左方。
例如,下面的函数定义时非法的,默认值参数x出现在非默认值参数y的左方,这样会导致编译错误。
int add(int x = 10, y);
正确做法是这样:
int add(int y, int x = 10)
3. 嵌套调用与递归调用
3.1 嵌套调用
在函数体内调用另一个函数,这种调用方式称为嵌套调用。
#include<iostream>
using namespace std;
int add(int x, int y) {
return x + y;
};
void showMessage() {
int s = add(10, 20);
cout << s << endl;
}
int main() {
showMessage();
return 0;
}
警告
注意C++中不允许函数进行嵌套定义,因此不能在一个函数体定义另一个函数。
嵌套调用的层数一般是没有限制的,但是个别编译器会有一些限制。
3.2 递归调用
所谓递归调用,就是函数自己调用自己。从定义中可以看出,函数递归调用时函数嵌套调用的特殊形式。使用递归调用方法解决问题的优点是:问题描述清楚,代码可读性强,结构清晰,代码量比使用非递归方法少。缺点是递归调用运行效率低,无论是从时间复杂度还是空间复杂度都比非递归程序差。
小栗子: 利用递归计算n的阶乘。
#include<iostream>
using namespace std;
int m(int n) {
if (n == 0) {
return 1;
}
else {
return n * m(n - 1);
}
return n;
}
int main() {
int n;
cin >> n;
int sum;
sum = m(n);
cout << sum << endl;
return 0;
}
小栗子二: 使用for循环时n的阶乘求解。
#include<iostream>
using namespace std;
int main() {
int sum = 1;
int n;
cin >> n;
for (int i = 1; i <= n; ++i) {
sum *= i;
}
cout << sum << endl;
return 0;
}
4. 变量的作用域
作用域就是程序中变量的作用范围。局部变量的作用域是局部的,如函数体内,全局变量的作用域则是整个程序。
之前接触的变量基本都是局部变量,这些变量在函数内声明,无法被其他函数所使用。函数的形参也属于局部变量,作用范围仅限于函数内部的语句块。除此之外,在各类语句块(if、while、for等)中定义的变量也是局部变量,作用范围仅限于语句块内部。
全局变量在函数体外声明,因此不属于某个函数,而属于整个C++源文件,可在程序的任何位置访问。
说明
当内部作用域与外部作用域声明了同名变量,内部作用域中的变量会屏蔽外部作用域中的变量。
5. 函数重载
C++中,同一作用域内不能声明同名变量,否则程序会编译出错;也不能定义同名函数,否则会带来冲突问题。但实际开发中,经常需要处理功能几乎相似,仅传入参数不同(参数类型或参数数量不同)的问题,编写大量的函数分别命名非常麻烦,为了提高代码的复用性,C++允许通过函数参数列表来识别同名函数,这就是函数重载。
所谓函数重载,是指多个函数具有相同的函数名,但是参数列表不同,函数调用时,编译器根据传入的参数类型以及参数个数来区分调用那个函数。
小栗子: 计算2个数之和、计算3个数之和
#include<iostream>
using namespace std;
int add(int x, int y);
int add(int x, int y, int z);
int main() {
int res1 = add(10, 20);
int res2 = add(10, 20, 30);
cout << "res1: " << res1 << ' ' << "res2: " << res2 << endl;
return 0;
}
int add(int x, int y) {
return x + y;
}
int add(int x, int y, int z) {
return x + y + z;
}
6. 内联函数
通过inline
关键字可以把函数定义为内联函数,编译器会在每个调用该函数的地方展开一个函数的副本。
例如,创建一个内联函数并进行调用。
#include<iostream>
using namespace std;
inline int add(int x, int y);
int main() {
int a = 10;
int b = 20;
int result = add(a, b);
cout << result;
return 0;
}
inline int add(int x, int y) {
return x + y;
}
使用内联函数可以减少函数调用带来开销(即程序文件中移动指针寻找调用函数地址带来的开销),但它只是一种解决方案,编译器可以忽略内联声明。
建议在函数实现代码很简短或者调用该函数次数相对较小的情况下将函数定义为内联函数,内联函数通常定义一条返回语句,不能包含循环或者switch语句。例如,一个递归函数不能在调用完全展开,一个1000行代码的函数也不可能再调用时展开,内联函数只是在优化程序时使用。在抽象数据类设计中,它对支持信息隐藏起主要作用。
如果某个内联函数要作为外部全局函数,即它将被多个源代码文件使用,那么就把它定义在头文件里,在每个调用该内联函数的源文件中都包含该头文件,这个方法保证对每个内联函数只有一个定义,以防止在程序的声明周期中引起无意的不匹配。
7. 变量的存储类型
除了变量类型之外,变量还分为4中存储类型,分别是auto
、static
、register
、extern
。存储方式不同,变量的作用域和生存期也不同。生存期和作用域,从时间和空间两个不同的纬度描述了一个变量。
7.1 auto变量
auto变量是动态存储变量,只在程序执行到它时才会从栈区分配存储单元,使用完毕后立即释放该存储单元。例如函数的形参就是auto变量,函数定义时并不会为其分配存储单元,函数调用时才会分配,调用完毕后会立即释放。auto变量存放在动态存储区中。
auto是C++默认的存储类型。凡是未知存储类型说明的变量均为auto变量。且auto关键字可以省略。
int i = 0;
// 等价于
auto int i = 0;
auto变量特点:
- 作用域和生存空间仅限于定义该变量的个体内。也就是说,在函数中定义的auto边变量,只在该函数内有效,无法在函数外使用;在复合语句中定义的auto变量,只在该复合语句中有效,无法在复合语句外使用。
int demo () {
auto int x, y;
if(true) {
auto char ch;
cout << ch << endl; // 正确
cout << x << endl; // 正确
}
cout << ch << endl; // 错误,无法访问到复合语句内的auto变量
cout << y << endl; // 正确
}
- 不同的个体(函数或复合语句)中允许定义同名变量,例如,函数内定义的auto变量可以在函数内的复合语句定义auto同名的变量。
7.2 static变量
static是静态存储变量,定义变量时系统会为其分配固定的存储单元,直到整个程序结束,到现在我们接触过的全局变量就是static变量,他们存放在静态存储区中。
static变量属于静态存储方式,具有以下特点:
- 无论是静态全局变量,还是静态局部变量,其生存期均为整个C++源程序运行期间。
- 静态全局变量在函数外定义,作用域是整个程序,即可以在任何地方使用它。
- 静态局部变量的作用域与auto类似,在函数内定义,只能在函数中使用。离开函数后,尽管其值一直存在,但无法被使用。当再次调用函数时,可以继续使用。
- 编译器会为静态局部变量赋予0值。
7.3 register变量
变量通常存储在内存中,当某个变量需要高频读写时,就需要重复访问内存。为了提高效率,C++允许将变量声明为register(寄存器)类型,这种变量将局部变量的值存放在CPU的寄存器中,使用时不再访问内存,而直接从寄存器中读写。
- register变量属于动态存储方式,凡需要采用静态存储方式的变量不能定义为register变量。
- 编译程序会自动决定哪个变量使用寄存器存储,register变量起到程序优化作用。
7.4 extern变量
一个C++程序通常包括多个源文件。由于C++文件中定义的函数和变量,只能被本文件中的函数调用。所以,要想调用其他源文件中的某个全局变量,就需要使用extern关键字声明该变量。
💬 欢迎评论!请确保您已登录 GitHub。