8 函数进阶
8.1 函数的递归调用
当一个函数在它的函数体内,直接或间接地调用它自身时,称为 递归调用 。
案例: 使用递归方式,求阶乘(n!)。
阶乘计算方式:n! = n * (n - 1)!
规定: 1! 和 0! 为 1。
int factorial(int n){
if (n == 0 || n == 1)
return 1;
else
return n * factorial(n - 1);
}
int main(){
cout << n << "!=" << factorial(n) << endl;
}
警告:
从一个函数转跳到另一个函数时,电脑需要保存当前代码运行的必要信息到内存中,而递归调用,会连续多次存入信息,直到最后一次调用,函数结束后,才会开始逐步释放这些内存。而如果递归无法终止,程序会因为占用过大的内存空间而崩溃。
8.2 内联函数
要解决的问题:
在调用函数时,系统需要做很多准备工作,如断点现场保护、参数入栈等。之后才可以执行被调用的函数。执行后,系统还需要保存返回值、恢复现场和断点等。
因此,如果被调用的函数,长度较短,功能较简单,这时系统执行这个函数的时间,与系统为调用这个函数做的准备时间和收尾时间相比,就显得微不足道。
这样的函数如果被多次调用,将大大影响程序整体运行效率。
作用机制:
内联函数的函数体代码,将直接插入到函数调用处。这样,虽然会增加最终程序的大小,但可以明显改善程序运行效率。
语法:
inline 类型 函数名(参数列表){...}
说明:
- 内联函数应足够简单,包含循环、 switch 和复杂嵌套 if 语句的情况不应该使用内联函数。
- 内联函数仅仅是程序员建议编译器这样做,至于编译器会不会使用内联函数的方式处理内联函数,由编译器自己决定。
- 类中的函数为内联函数。(涉及未学习的知识:类)
8.3 函数重载
在同一个作用域内,可以声明几个功能类似的同名函数,但是这些同名函数的形参(指参数的个数、类型或者顺序)必须不同。不能仅通过返回类型的不同来重载函数。
int getSum(int a, int b){
return a + b;
}
double getSum(double a, double b){
return a + b;
}
int main(){
cout << getSum(1, 2) << endl;
cout << getSum(1.2, 3.4) << endl;
}
8.4 带有默认参数的函数
我们可以在声明函数时,为形参赋予默认值,如果我们在调用函数时,没有为参数赋值,那函数就会使用形参默认值执行函数体。
示例:
double getArea(double r = 1.0){
return r * r * 3.14;
}
int main(){
cout << getArea() << endl; // 输出3.14
cout << getArea(2.0) << endl; // 输出12.56
}
说明:
- 不限制默认参数个数,但所有默认参数必须在参数列表最后面。
int f1(int a, int b = 1){...} //正确
int f2(int a = 1, int b){...} //错误
- 函数声明与函数定义同在时,只能在其中一处指定默认值。此时,函数声明中的形参名称依旧可以省略。
int f1(int, int = 1);
int f1(int a, int b){...} //这里不可以再给出默认值
8.5 局部变量和全局变量
作用域是在运行时代码中的某些特定部分中变量,函数和对象的可访问性。变量等,只有在作用域内,才能被访问到。根据作用域不同,我们可以把变量分为局部变量和全局变量。
8.5.1 局部变量
局部变量,又称内部变量,指在函数中定义的变量(包括形参)或定义在花括号中定义的变量(复合语句),其作用域在本函数范围内,或本花括号范围内。
复合语句: 不是函数,也不是 if 等语句的一部分,但写在花括号({}
)内的语句。由于复合语句有自己独立的作用域,因此,可以通过复合语句来定义重名变量。
int main(){
int a = 100;
{
int a = 10;
cout << a << endl; // 输出10
}
cout << a << endl; // 输出100
}
8.5.2 全局变量
全局变量,又称外部变量。它定义在函数外部,作用域是整个源文件。
int a = 100;
int main(){
cout << a << endl; // 输出100
}
说明:
- 全局变量定义后,如果不赋初值,编译时会自动赋初值为0。
- 若在同一源文件中,全局变量和局部变量同名,那么在局部变量的作用域内,全局变量不会起作用。
- 如果要在局部变量作用域内使用全局变量,可以使用作用域运算符
::
来限定使用全局变量。
int a = 100;
int main(){
int a = 10;
cout << a << endl; // 输出10
cout << ::a << endl; // 输出100
}
- 全局变量可以作为函数间联系的通道。对于需要多个返回值的函数来说,可以使用全局变量,使函数多返回几个值。
- 全局变量可以被多个函数使用,这会破坏程序的模块化结构,使程序难以理解和调试。因此,要尽量少用或者不用全局变量。
8.6 变量的生命周期
变量的生命周期,又称“变量的生存期”。
变量本质上是一块内存区域,而内存是有限的,程序不可以一直占用一块内存不放,否则电脑会因为内存全部被占用而崩溃。变量的生命周期,就是指一块内存区域被分配给这个变量,到内存区域被回收的过程。
全局变量的生命周期,是从程序开始运行到程序运行结束。局部变量则在程序运行到其作用域时,才会开辟内存空间,程序运行离开其作用域后,就会释放其内存。
8.7 auto 关键字
auto 关键字有两个不同的涵义。
8.7.1 早期 C++ 标准
在早期 C++ 标准中,auto被解释为一个自动存储变量的关键字,也就是申明一块临时的变量内存。
其实就是说,这个变量是个局部变量,然而C++编译器也不需要使用关键字来判断一个变量是不是局部变量,所以这个关键字就是毫无用处。
int main()
{
auto int a = 10;
return 0;
}
8.7.2 C++11 标准
auto 不再是一个存储类型指示符,而作为一个新的类型指示符来指示编译器,auto 声明的变量类型必须由编译器在编译时期推导而得。
可以使用typeid().name 去打印对象的类型。
int main()
{
int a = 10;
auto b = a;
auto c = 'a';
cout << typeid(b).name() << endl; //int
cout << typeid(c).name() << endl; //char
return 0;
}
说明:
- auto 类型的变量,必须在定义时赋初值,不然就不能推测变量的具体类型了。
- 现在我们学过的数据类型还比较短,但数据类型名可以很长,对于这种数据类型,直接用 auto 是很方便的。
8.8 static 关键字
static 关键字含义较多,这里仅对其修饰变量时的含义进行介绍。
8.8.1 static 修饰局部变量(静态局部变量)
对于局部变量,一般来说,程序运行离开局部变量作用域后,系统会回收所有局部变量占用的内存。而 static 修饰的局部变量,其占用的内存被回收的时间是程序运行结束。也就是说,这个局部变量所在函数再次运行时,不会再分配内存空间,仍保留上次运行结束后的值。
int f(){
static int a = 10;
a += 2;
return a;
}
int main()
{
cout << f() << endl; // 输出12
cout << f() << endl; // 输出14
return 0;
}
声明:
- 如果静态局部变量在定义时被赋初值,那么再次运行到这一行时,不再赋初值。即静态局部变量只赋初值一次。
- 静态局部变量和全局变量一样,如果不赋初值,会被自动赋值为0(对于字符变量,为空字符'\0')。
- 静态局部变量虽然生命周期很长,但是作用域仍和局部变量相同,不能再作用域外,访问静态局部变量。
8.8.2 static 修饰全局变量或函数(静态全局变量和内部函数)
现在,我们往往有一个 .cpp 文件就可以将一个 C++ 程序项目作完,因为我们现在做的程序还太过简单。
对于较大项目来说,一个文件写完所有代码,代码结构一定会非常混乱,而且大项目往往需要多人合作完成,多人很难在同一个文件上工作。所有多个源文件组成一个项目的现象很常见。
那么这就涉及到多个源文件之间的互相联系与互相屏蔽。我们在一个源文件中声明的全局变量或者函数,我们可能希望它可以在其他源文件中使用,也可能希望它不能在其他源文件中使用。
如果我们希望一个变量或者函数,不能被其他源文件使用,我们就可以在声明时,使用 static 修饰。被修饰的变量叫做 静态全局变量 ,被修饰的函数叫做 内部函数 。
8.9 register 关键字(没什么用)
一般来说,变量存储在内存中,但内存中的数据终究要读取到 CPU 中,才能进行处理。那么,我们直接把变量存储到 CPU 的寄存器中,我们就可以获得更快的运行速度。register 关键字让我们可以建议将这个变量存入寄存器(如果可能)。
int main()
{
register int a = 10;
return 0;
}
大部分编译器不接受用户对寄存器变量的请求,毕竟寄存器空间相对极小,而且极为重要。所以 这个关键字就像一张美味的大饼,但是不能吃,因为是画在地上的 。
8.10 extern 关键字
我们提到过,全局变量和全局函数,它们的生命周期都是从程序运行开始到程序运行结束,并且肯定有初值。它们的作用域,如果没有明确使用 static 关键字限定,那么作用域就是整个项目的所有源文件。
虽然如此,但是我们并不能随意地使用来自其他源文件,或者声明在同一源文件,但位置比使用位置靠后的全局变量和函数。我们必须在使用前,声明函数原型,或者声明变量是外部变量。
注意声明与定义的区别:声明仅仅是表示有这样一个变量(或函数),不为其分配内存空间,也就不能进行赋初值,和明确函数体,这样的操作。而定义不仅指出有这样一个变量(或函数),还未其分配内存空间,因此需要为其赋初值(或者隐式赋初值),可以明确函数体。
int main()
{
extern int a;
cout << a << endl;
return 0;
}
int a = 10;
对于全局变量和全局函数来说,定义时如果不用 static 关键字限定,其实默认是省略了 extern 关键字,表示这个变量或者函数可以被用于其他源文件(这种函数叫做 外部函数 )。
在其他源文件使用来自其他源文件的全局变量或者全局函数时,需要先对其进行声明( 注意是声明,因为全局变量和函数,在整个程序运行过程中,仅可定义一次!!! ),声明时,前面要使用 extern 关键字修饰,表明这个变量或者函数的 定义 ,在其他地方。但是,函数在声明时, extern 关键字是可以省略的,所以,我们可以通过声明函数原型的方式,使用来自其他源文件的全局函数。但变量声明时, extern 关键字不可以省略。
我们之前学过使用头文件引入全局变量和全局函数,而 extern 关键字是头文件的作用原理 。
const 关键字修饰的常量,默认作用域仅为其声明的源文件,如果希望它可以被用于其他源文件,需要使用 extern 关键字修饰。