C++蓝桥杯之函数与递归

1. 函数是什么

概念

数学中我们其实就见过函数的概念,比如:一次函数 y = kx + b ,k和b都是常数,给一个任意的 x ,就得到一个 y 值。其实在C/C++语⾔中就引⼊了函数(function)的概念,有些翻译为:子程 序,子程序这种翻译更加准确⼀些。函数就是⼀个完成某项特定的任务的一小段代码,这段代码是有 特殊的写法和用方法的。

其实大家在前面学习的时候,就见过函数了,比如:main函数、scanf函数、printf函数、pow函数、 sqrt函数等。 C/C++语言程序其实是由若干个小的函数组合而成的,也可以说:一个大的计算任务可以分解成若干个较小的函数(对应较小的任务)完成。同时一个函数如果能完成某项特定任务的话,这个函数也是可以复用的,提升了开发软件的效率。

如果我们要完成这样下面这样的任务:

cpp 复制代码
#include <iostream>

using namespace std;
int main()
{
	int arr[10] = { 0 };
    //打印数组的内容
 
    //给数组的元素赋值为1~10 
    
    //打印数组
	
	return 0; 
}

如果没有函数,我们能写出的代码如下的代码

cpp 复制代码
#include<iostream>
using namespace std;
int main()
{
    int arr[10] = { 0 };
    //打印数组的内容
 
    int i = 0;
    for (i = 0; i < 10; i++)
    {
        cout << arr[i] << " ";
    }
    cout << endl;
    //给数组的元素赋值为1~10 
    for (i = 0; i < 10; i++)
    {
        arr[i] = i + 1;
    }
    //打印数组
 
    for (i = 0; i < 10; i++)
    {
        cout << arr[i] << " ";
    }
    cout << endl;
    return 0;
}

仔细看,这里面有两部分是重复的,那如果我能把这部分封装成一个函数,在需要的时候调用,就方便很多,看下面

cpp 复制代码
#include<iostream>
using namespace std;

void print_arr(int arr[])
{
    int i = 0;
    for (i = 0; i < 10; i++)
    {
        cout << arr[i] << " ";
    }
    cout << endl;
}
int main()
{
    int arr[10] = { 0 };
    //打印数组的内容
 
    print_arr(arr);
    //给数组的元素赋值为1~10 
    for (int i = 0; i < 10; i++)
    {
        arr[i] = i + 1;
    }
    //打印数组
 
    print_arr(arr);
    return 0;
}
//输出 
//0 0 0 0 0 0 0 0 0 0
//1 2 3 4 5 6 7 8 9 10

上述代码print_arr 就是我们自己定义的一个函数,使用函数能有什么好处呢?

  1. 模块化开发,一个大的功能,总能拆分成各种子功能,每个子功能都可以设计成一个函数,每个函数可以作为一个独立的模块存在,程序的逻辑更加清晰,逻辑关系更加明确。

  2. 代码可以复用,只要根据需要定义出一个函数,需要这个功能的地方,直接调用函数就行,降低了代码的冗余,提升了开发效率。

  3. 方便多个程序员之间协作开发,方便程序的多个模块之间互相交互。

  4. 熟悉函数的使用之后,代码的编写、阅读、调试、维护都变得更加容易。

2. 函数的分类

在C/C++中,函数一般分为库函数和自定义函数,那我们接下来就介绍一下。

库函数

库函数介绍

前面内容中提到的 printf 、 scanf 都是库函数,库函数是标准库中提供的现成的函数,我们只要学习函数的功能,就能直接使用。有了库函数,一些常见的功能就不需要程序员自己实现了,一定程 度提升了效率;同时库函数的质量和执行效率上都更有保证。 编译器的标准库中提供了一系列的库函数,这些库函数根据功能的划分,都在不同的头文件中进行了 声明。 库函数的学习和查阅工具很多,比如:

C/C++官方参考手册:https://zh.cppreference.com/w/cpp/header

C/C++第三方网站:https://legacy.cplusplus.com/reference/

库函数中有数学相关的,有日期相关的,有算法相关的等等。这些库函数相关的信息都有自己对应的头文件,每一个头文件中都包含了相关的一组函数和类型等信息,库函数的学习不用着急一次性全部学会,慢慢学习,各个击破就行。

C++是兼容C语言,所以在C++中也包含了⼀些来自C语言的头文件,这些头文件的后缀是 .h ,如果需要也可以直接包含使用;有一些来自C语言的头文件,在C++中会在原来C语言的头文件进行了封装, 在C++程序中更推荐C++的头文件写法。

库函数使用举例

举例:sqrt

• sqrt 是函数名

• x 是函数的参数,表示调用

• do uble 是返回值类型,表示函数计算的结果是 sqrt 函数需要传递一个 double 类型的值 double 类型的值

功能:

Computesquareroot 计算平方根

Returns the square root of x.(返回平方根)

头文件包含:

库函数是在标准库中对应的头文件中声明的,所以库函数的使用,务必包含对应的头文件,不包含是可能会出现一些问题的。

cpp 复制代码
using namespace std;
int main()
 {
    double d = 16.0;
    double r = sqrt(d);
    cout << r << endl;
    //输出 4 
    
    return 0;
    
}

自定义函数

了解了库函数,其实库函数的功能是有限的,实际开发过程中还是得根据需要将代码封装成自定义的函数;自定义的函数就是自己设计和实现的函数。

函数的语法形式

其实⾃定义函数和库函数是一样的,形式如下:

cpp 复制代码
ret_type fun_name(形式参数)
{
 
 
 }

语法说明如下:

• ret_type 是用来表示函数计算结果的类型,有时候返回类型可以是void,表示什么都不返回

• fun_name 是为了方 便使用函数;就像人的名字⼀样,有了名字方便称呼,函数有了名字反便调 用,所以函数名尽量要根据函数的功能起的有意义。

• 函数的参数就相当于工厂加工需要的原材料,函数的参数也可以是 void ,明确表示函数没有参 数。如果有参数,多个参数用逗号隔开,每个参数要分别交代清楚参数的类型和名字。

• {} 括起来的部分被称为函数体,函数体就是完成计算的过程。

我们可以把函数想象成小型的⼀个加工厂,工厂得输入原材料,经过工厂加工才能生产出产品,那函数也是一样的,函数一般会输⼊一些值(可以是0个,也可以是多个),经过函数内的计算,得出结果。

函数定义

那函数是如何定义的,接下来我们举一个实际的例子,来讲解。 例如:写一个加法函数,完成2个整型变量的加法操作。

cpp 复制代码
#include<iostream>
using namespace std;
int main()
 {
    int a = 0;
    int b = 0;
    //输入 
 
    cin >> a >> b;
	//任务:调用加法函数,完成a和b的相加
 
    //求和的结果放在r中
    //to do

    //输出
    cout << r << endl;
    return 0;
    
}

我们根据要完成的功能,给函数取名:Add,函数Add 需要接收2个整型类型的参数,函数计算的结果也是整型。

所以我们根据上述的分析写出函数:

cpp 复制代码
#include<iostream>
using namespace std;
 //这就是函数的定义
 
int Add(int x, int y)
{
    int z = 0;
    z = x + y;
    return z;
}

int main()
{
    int a = 0;
    int b = 0;
    //输入 3 4
 
    cin >> a >> b;
    //输出
 
    //调用加法函数,完成a和b的相加
 
    //求和的结果放在r中 
    int r = Add(a, b);
    
    cout << r << endl;
    //输出7 
    
    return 0;
 }

Add 函数也可以简化为:

cpp 复制代码
int Add(int x, int y)
{
    return x + y;
}

函数的参数部分需要交代清楚:参数个数,每个参数的类型是啥,形参的名字叫啥。 上⾯只是一个例子,未来我们是根据实际需要来设计函数,函数名、参数、返回类型都是可以灵活变化的。函数的实现就是函数的定义。

3. 函数参数和返回值

实参和形参

实际上,在函数定义和使用的过程中,函数的参数要被分为两种:

• 实际参数,简称实参

• 形式参数,简称形参

为了更好进行解释上面的概念,先回顾我们前面写的代码:

cpp 复制代码
#include<iostream>
using namespace std;
int Add(int x, int y)
{
    int z = 0;
    z = x + y;
    return z;
 }
int main()
{
    int a = 0;
    int b = 0;
    //输入 
 
   cin >> a >> b;
   //调用加法函数,完成a和b的相加
 
   //求和的结果放在r中
 
   int r = Add(a, b);
   //输出
 
   cout << r << endl;
   
   return 0;
   
}

实参

实际参数就是真实传递给函数的参数 。 在上面代码中,第4~6行是 Add 函数的定义,有了函数后,再第19行调用Add 函数的。 我们把第19行调用Add 函数时,传递给函数的参数 a 和 b ,称为实际参数,简称实参

形参

在上行代码中,第4行定义函数的时候,在函数名 Add 后的括号中写的 x 和 y ,称为形式参数,简称形参。

为什么叫形式参数呢?实际上,如果只是定义了 Add 函数,而不去调用的话, Add 函数的参数 x 和 y 只是形式上存在的,不会向内存申请空间,不会真实存在的,所以叫形式参数。形式参数只有在 函数被调用的过程中为了存放实参传递过来的值,才向内存申请空间,这个过程就是形式的实例化。

实参和形参的关系

虽然我们提到了实参是传递给形参的,他们之间是有联系的,但是形参和实参各自是独立的内存空 间。

这个现象是可以通过调试来观察的。请看下面的代码和调试演示:

cpp 复制代码
#include<iostream>
using namespace std;
int Add(int x, int y)
{
    int z = 0;
    z = x + y;
    return z;
 }

int main()
{
    int a = 0;
    int b = 0;
    //输入 
    cin >> a >> b;
    //调用加法函数,完成a和b的相加
    
    //求和的结果放在r中
    int r = Add(a, b);
    
    //输出
    cout << r << endl;
    
    return 0;
 }

我们在调试的可以观察到, x 和 y 确实得到了 a 和 b 的值,但是 x 和 y 的地址和 a 和 b 的地址 是不一样的,当 a 和 b 传参给形参x和y的时候, x 和 y 只是得到了 a 和 b 的值,他们得有自己独立的空间。所以我们可以理解为形参是实参的一份临时拷贝。

函数传参

数组做函数参数

在使用函数解决问题的时候,难免会将数组作为参数传递给函数,在函数内部对数组进行操作。 比如:写一个函数将一个整型数组的内容,全部置为-1 ,再写一个函数打印数组的内容。 简单思考一下,基本的形式应该是这样的:

cpp 复制代码
#inculude <iostream>
using namespace std;
int main()
{
    int arr[] = {1,2,3,4,5,6,7,8,9,10};
    set_arr();     
	//设置数组内容为-1             
    print_arr();
	//打印数组内容
                 
    return 0;
 }

这里的 set_arr 函数要能够对数组内容进行设置,就得把数组作为参数传递给函数,同时函数内部 在设置数组每个元素的时候,也得遍历数组,需要知道数组的元素个数。所以我们需要给set_arr 传递2个参数,一个是数组,另外一个是数组的元素个数。仔细分析 print_arr 也是⼀样的,只有拿到了数组和元素个数,才能遍历打印数组的每个元素。

数组作为参数传递给了 set_arr 和 print_arr 函数了,那这两个函数应该如何设计呢?

这里我们需要知道数组传参的几个重点知识:

• 函数的实参的名字和形参的名字可以相同,也可以不同

• 函数的形式参数要和函数的实参个数匹配

• 函数的实参是数组,形参也写成数组形式的

• 形参如果是一维数组,数组大小可以省略不写

• 形参如果是二维数组,行可以省略,但是列不能省略

• 数组传参,形参是不会创建新的数组的

形参操作的数组和实参的数组是同一个数组

根据上述信息,我们可以实现以下两个函数

cpp 复制代码
void set_arr(int arr[], int sz)
{
    int i = 0;
    for(i = 0; i < sz; i++)
    {
        arr[i] = -1;
    }
}
void print_arr(int arr[], int sz)
{
    int i = 0;
    for(i = 0; i < sz; i++)
    {
        cout << arr[i] << " ";
    }
    cout << endl;
 }

字符串做函数参数

那如果将字符串做函数参数呢?其实也很简单,直接在形参的部分使用字符串来接收就可以。这里的形参s也是实参s的一份临时拷贝,对形参的修改不能影响实参。

cpp 复制代码
void test(string s) 
{
    cout << s << endl;
}

int main()
{
    string s("hello world");
    test(s);
}

全局变量不用传参

全局变量的作用域很大,在整个程序中都可以使用,那么只要把变量、数组等定义成全局变量,在函数中使用,是可以不用传参的,在竞赛中为了方便经常使用,但是在软件工程中很少这么使用。

cpp 复制代码
#include <iostream>
using namespace std;

int arr[10] = { 0 };

void print_arr()
{
    int i = 0;
    for (i = 0; i < 10; i++)
    {
        cout << arr[i] << " ";
    }
    cout << endl;
}
 
int main()
{
    //打印数组的内容
 
    print_arr();
    //给数组的元素赋值为1~10 
    for (int i = 0; i < 10; i++)
    {
        arr[i] = i + 1;
    }
    
    //打印数组
    print_arr();
    
    return 0;
}

当然,有时候变量或者数组,定义成全局的时候,是不能解决问题,⽐如:递归等场景,这时候,就 得考虑传参的问题。

返回值

我们在设计的函数的时候,函数在经过计算后,有时候需要带回一些计算好的数据,这时候往往使用 return 来返回,这里我们就讨论一下使用 return 返回。

1. return 后边可以是一个数值,也可以是一个表达式,如果是表达式则先执行表达式,再返回表 达式的结果。函数返回的值,可以使用变量来接收,如果不需要这个返回值,也可以不接收。

cpp 复制代码
#include <iostream>

// 这里使用简化版本的加法函数
int Add(int x, int y)
{
   return x + y;       
}
 // 1. 先执行x+y,得到该表达式计算的结果
 //2. 执行return,返回结果 

return 0; 
}

////在main函数中使用return 0; 返回的就是一个数值

2.return 后边也可以什么都没有,直接写return,这种写法适合函数返回类型是 void 的情况

cpp 复制代码
void test(int n)
{
    if(n == 2)
        return;
		//只要执行这里的return,函数就提前返回,
		//不再打印数据
 
    cout << n << endl;
 }

3.return 返回的值和函数返回类型不一致,系统会自动将返回的值的类型隐式转换为函数的返回 类型。

cpp 复制代码
#include <iostream>
using namespace std;
int test()
{
    return 3.14;
}

int main()
{
    int ret = test();
    cout << ret << endl;
    
    return 0;
 }

4. return 语句执行后,函数就彻底返回,后边的代码不再执行。

cpp 复制代码
#include <iostream>
using namespace std;

void print_arr(int arr[], int n)
{
    int i = 0;
    for(i=0; i<n; i++)
    {
        if(i == 5)
            return;
			//对比换成break 
        cout << arr[i] << " ";
    }
    cout << endl;
    cout << "打印完毕" << endl;
 }
 
int main()
{
	
    int arr[10] = {1,2,3,4,5,6,7,8,9,10};
 
    print_arr(arr, 10);
 //输出1 2 3 4 5 
    return 0;
}

如果函数中eturn 语句被执行,则函数直接返回,即使函数中 return 语句后还有其他代码也不 再执行。

4. 函数的声明和调用

函数声明

⼀般在使用函数的方式写代码的时候,函数定义好之后,在后续的代码中对函数进行调用,例如 代码 1 。有时候我们也可能将函数定义放在了函数调用的后边,例如 代码 2 ,这时候编译器在编译的时候 就会有警告出现,提这个函数可能不存在,为了消除这种警告,我们⼀般在函数的调用 之前先声明一下这个函数,这就是函数声明。函数声明就是告诉编译器,有⼀个函数名字叫什么,参数是什么,返回类型是什么,至于这个函数是否真的存在,是要看函数的定义了。函数调用必须满足先声明后使用。

cpp 复制代码
#include <iostream>
using namespace std;

//函数定义
 
int Add(int x, int y) 
{
    return x + y;
 }

int main()
{
    int a = 10;
    int b = 20;
    int c = Add(a, b);
    cout << c << endl;
    //输出30 
    
    return 0;
 }
cpp 复制代码
#include <iostream>
using namespace std;
 //函数声明
int Add(int x, int y);

int main()
{
    int a = 10;
    int b = 20;
    int c = Add(a, b);
    cout << c << endl;
    
    return 0;
}
 
int Add(int x, int y) 
{
   //函数定义
 
    return x + y;
}
//输出30 

函数调用

函数调用的方式,一般会分为两类:传值调用和传址(引用)调用 ,我们前面讲过的都是传值调用。因为暂时还没有学习指针,这里下面我们会讲解一下传引用调用

传值调用

写⼀个函数Max,求两个整数的较大值。

cpp 复制代码
#include <iostream>
 
using namespace std;

int Max(int x, int y)
{
    return x > y ? x : y;
}

int main()
{
    int a = 0;
    int b = 0;
    cin >> a >> b;
    //5 3
    int c = Max(a, b);
    cout << c << endl;
    //5
    return 0;
}

第14行调用 Max 函数时,就是传值调用。传值调用就是将实参的数据直接传递给形参。这个过程其 实是将实参的值拷贝一份给 Max 函数使用,这份副本其实就是形参变量。这时形参和实参是不同的变 量,所以对形参的修改,不会影响实参。这种情况下参数传递的方式只能从实参到形参,也就是单向 传递。为了理解什么是单向传递,我们看一下下面的代码。

写一个函数Swap,交换两个整型变量的值。 如果我们按照常规的思路,写出下面的代码。

cpp 复制代码
#include <iostream>
using namespace std;

void Swap(int x, int y)
{
    int z = x;
    x = y;
    y = z;
 }

int main()
{
    int a = 0;
    int b = 0;
    cin >> a >> b;
    //3 5
    Swap(a, b);
    
    cout << "交换前, a = " << a << " b = " << b << endl;
    cout << "交换后, a = " << a << " b = " << b << endl;
    
    //输出 
    //交换前, a = 3 b = 5
    //交换后, a = 3 b = 5
    return 0;
 }

运行程序你会发现,其实 Swap 函数中的 x 和 y 确实会交换,但是 main 函数的中的 a 和 b 其实 没有交换。这就是值传递的特点,形参和实参是不同的内存空间,对形参的修改不会影响实参,数据 仍然是单向传递的,所以交换的效果没有达到。

这里我想给大家两个启发:

  1. 传值调用的方式在什么时候使用?

其实传值调用的方式一般应方的场景就是:

仅需要通过传参的方式将实参的值传递给被调函数,被调 函数就可以完成工作,而不需要改变实参的值。比如,案例1中的Max函数,将 a 和 b 的值传递给形参 x 和 y ,在 Max 函数中使用 x 和 y 来求较大值,就能得到 a 和 b 中的较大值。

  1. 如何改造 Swap 函数才能实现交换的效果呢? 这里可以使用指针,也可以使用引用。接下来我们讲一下C++中的引用。

引用

引用概念

引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间, 它和它引用的变量是同一块内存空间。比如:李逵,在家称为"铁牛",江湖上⼈称"黑旋风"

引用使用格式:

类型&引用变量名=引用实体;

cpp 复制代码
void TestRef()
{
    int a = 10;
    int& ra = a;            
    // 定义引用类型
    //通过printf输出a,ra的地址
 
    printf("%p\n", &a);
    printf("%p\n", &ra);
}
引用特性

1. 引用在定义时必须初始化

2. 一个变量可以有多个引用

3. 引用一旦引用一个实体,再不能引用其他实体

cpp 复制代码
void TestRef()
{
    int a = 10;
 // int& ra;  
  // 该条语句编译时会出错
 
    int& ra = a;
    int& rra = a;
    
    printf("%p %p %p\n", &a, &ra, &rra);
}

传址(引用)调用

有了前面引用的学习,那就可以利用引用来重新改造前面写的 Swap 函数了。

cpp 复制代码
#include <iostream>
using namespace std;
void Swap(int& x, int& y)
{
    int z = x;
    x = y;
    y = z;
 }
 
int main()
{
    int a = 0;
    int b = 0;
    cin >> a >> b;
	   //5 3
    Swap(a, b);

    cout << "交换前, a = " << a << " b = "<< b << endl;
    cout << "交换后, a = " << a << " b = "<< b << endl;
 
    //交换前, a = 3 b = 5
    //交换后, a = 3 b = 5
    
    return 0;
}

上面这种实现 Swap 函数实现方式就是函数的传引用调用。这种调用方式的本质是将实参变量的地址传递给了形参,而形参使用指针直接找到实参来进行操作。所以修改形参的时候,直接改变的就是实参,这就是引用的应用。当然这里也可以使用指针来实现,比引用稍微复杂一些,这里等学习到指针,自然就明白了。

拓展学习:swap函数

其实在C++的STL中也提供了⼀个库函数 swap ,这个函数可以用来交换两个变量,也可以交换两个数组(容器的值),如果掌握了,我们就不需要自己再自己实现交换的逻辑,直接使用现成的函数。

swap 函数需要的头文件 utility。

cpp 复制代码
#include <iostream>
#include <utility>
using namespace std;

int main()
{
    int a = 0;
    int b = 0;
    cin >> a >> b;
    cout << "交换前, a = " << a << " b = " << b << endl;
    swap(a, b);
	//直接使用库函数swap交换两个变量
    cout << "交换后, a = " << a << " b = " << b << endl; 
    
    
    return 0;
 }

使用 swap 交换两个数组也是可以的:

cpp 复制代码
#include <iostream>    
#include <utility>  
//swap函数需要    
 
using namespace std;
int main () 
{
    int arr1[4];   
	// arr1: 0  0  0  0                   
    int arr2[] = {10,20,30,40};    
	// arr1: 0  0  0  0    arr2: 10 20 30 40   
    swap(arr1, arr2); 
    // arr1: 10 20 30 40   arr2: 0  0  0  0
	                
    for (int e: arr1) 
        cout << e << " ";
        
    cout << endl;
    //输出 10 20 30 40 

    return 0;
}

那么字符串能不能使用引用的方式传参呢?当时是可以的。

cpp 复制代码
#include <iostream>
using namespace std;

void printString(string& s) 
{
    cout << s << endl;
 }
 
int main()
{
    string s("hello world");
    printString(s);
    //输出 hello wold 
}

那有些函数在实现时候,既可以采用传值调用的设计,又可以采用传址(引用)调用的设计,那么我们该如何选择呢?

传值、传引用效率比较

定义了一个全局字符串 s ,然后以传值和传引用的方式进行对比,看效率的差异。

cpp 复制代码
#include<iostream>
#include<ctime>
using namespace std;

 //定义全局字符串s 
string s("hello world");

void TestFunc1(string s) {}
void TestFunc2(string& s) {}

void Test()
{
 // 以值作为函数参数
 
    size_t begin1 = clock();
    for (size_t i = 0; i < 10000000; ++i)
    {
        TestFunc1(s);
    }
    size_t end1 = clock();
    // 以引用作为函数参数
 
    size_t begin2 = clock();
    for (size_t i = 0; i < 10000000; ++i)
    {
        TestFunc2(s);
    }
    size_t end2 = clock();

 // 分别计算两个函数运用结束后的时间
 
    cout << "TestFunc1(string)-time:" << end1 - begin1 << endl;
    cout << "TestFunc2(string&)-time:" << end2 - begin2 << endl;
}

int main() 
{
    Test();
    return 0;
 }

//输出 
//TestFunc1(string)-time:77
//TestFunc2(string&)-time:31

采用传值调同过程中,函数传参,将实参传递给形参的时候,形参会创建新的空间,再将实参的数据给形参拷贝一份;但是引用传参的方式,就不存在数据的拷贝,只是在形参的部分建立引用的关系, 形参就是实参。所以引用传参的效率要高于传值调用的方式。

小提示:

数组在传参的时候,形参和实参本来就是同一个数组,所以数组传参的时候,不需要使用引用参数。

5. 函数重载

重载概念

引入:

比如:我们现在要写一个函数,求两个整数的和,那么我们可以这么写:

cpp 复制代码
#include <iostream>
using namespace std;
int IntAdd(int x, int y)
{
    return x + y;
}

int main()
{
    int a = 0;
    int b = 0;
    cin >> a >> b;
    int c = IntAdd(a, b);
    cout << c << endl;
    
    return 0;
}

那如果还想要实现一个函数求两个 double 类型浮点数的和,怎么办呢?

cpp 复制代码
double DoubleAdd(double x, double y)
{
    return x + y;
 }

那么上面的 IntAdd 和 DoubleAdd 函数的功能是类似的,都是完成求两个数的求和,只是参数类 型有差异而已,既然功能是类似的,能不能函数名字统意义下都叫 Add() 呢?这样使用 Add() 函数 的人,不需要记忆很多名字,就比较方便。这是C++中引入的函数重载的功能。

函数重载:C++中的函数重载(FunctionOverloading)是指在同一个作用域中可以有多个同名函数, 它们的函数名称相同,但是参数列表不同。

cpp 复制代码
1 函数返回类型_函数名(参数1, 参数2,...);

这里的"不同"指的是参数的数量、类型或顺序至少有⼀个不同。函数的返回类型并不影响函数的重 载,因为C++编译器不会根据返回类型来区分不同的函数。

重载举例

cpp 复制代码
#include<iostream>
using namespace std;
   // 1、参数类型不同
 
int Add(int a, int b)
{
    return a + b;
}
    double Add(double a, double b)
{
    return a + b;
 }

 // 2、参数个数不同
 
void f()
{
    cout << "f()" << endl;
 }
void f(int a)
 {
    cout << "f(int a)" << endl;
 }
 // 3、参数类型顺序不同
 
void f(int a, char b)
{
    cout << "f(int a,char b)" << endl;
 }
void f(char b, int a)
{
    cout << "f(char b, int a)" << endl;
 }

int main()
{
    Add(10, 20);
    Add(10.1, 20.2);
    f();
    f(10);
    f(10, 'a');
    f('a', 10);
    
    return 0;
 }

拓展

库函数max和min

max

在C++中,max 函数用于返回两个值中的较大值。它是C++标准库 algorithm 头文件中的一个函数。

max 函数可以用于各种类型,包括内置类型(如 int ,double)以及⽤⼾⾃定义类型(如类或结构体),只要这些类型支持比较操作。

cpp 复制代码
#include <algorithm>
template <class T>
const T& max(const T& a, const T& b);
   //默认比较
template <class T, class Compare>
const T& max(const T& a, const T& b, Compare comp); 
//自定义比较器

a: 要较较的第一个值。
b: 要较的第二个值。
comp (可选):
自定义比较函数对象或比较函数,用于确定
"比较"值的标准。

比较函数应当返回一个布尔值,表较第一个参数是否
小于" 第一个参数。

返回值:

• 返回 a 和 b 中较大的那个值。如果两个值相等,则返回 a 。

接下来来看几个代码

cpp 复制代码
#include <iostream>
#include <algorithm>
using namespace std;

int main()
{
    int x = 10;
    int y = 20;
    int m = max(x, y);
    cout << "较较值: " << m << endl;

    return 0;
}
 

再来一个

cpp 复制代码
 
#include <iostream>
#include <algorithm>
#include <string>

bool compareLength(const string &a, const string &b) 
{
    return a.size() < b.size(); 
	//这里必须给一个判断小于的标准
 
}

int main() 
{
    string str1 = "apple";
    string str2 = "banana";
    string max_str = max(str1, str2, compareLength);   

    cout << "?度更?的字符串是" << max_str << endl;
    return 0;
 } 
 
min

在C++中, min 函数用于返回两个值中的较小值。它和 max 函数类似,也是在C++标准库头文件中的一个函数。使用和max函数一模一样,只是实现的效果恰好相反。

cpp 复制代码
#include <iostream>
#include <algorithm>
#include <string>

bool compareLength(const string &a, const string &b) 
{
    return a.size() < b.size(); 
 //这里必须给一个判断小于的标准
 
}

int main() 
{ 
    string str1 = "apple";
    string str2 = "banana";
    string min_str = min(str1, str2, compareLength);
    cout << "长度更长的字符串是" << min_str << endl;
    
    return 0;
 }

有了max函数,那么上面的题目中就可以直接使用了。代码也可以改写为:

cpp 复制代码
 
#include <iostream>
using namespace std;

int Max(int x, int y, int z)
{
    int m = max(x, y);
    return max(m, z);
}

int main()
{
    int a, b, c;
    cin >> a >> b >> c;
    double ret = Max(a, b, c) * 1.0 / (Max(a + b, b, c) * Max(a, b, b + c));
    printf("%.3lf\n", ret);
    
    return 0;
 } 
 
 

7. 递归

递归的概念

递归是一种在计算机科学中非常重要的编程技术,它可以简化许多复杂的问题。递归具体是指函数在 定义的时候直接或间接调用自身0的方式。 这里给大家展示一个最简单的递归程序:

cpp 复制代码
#include <iostream>
using namespace std;
 
int main()
{
    cout << "hehe" << endl;
    main();
	//main函数中又调用了main函数
 
    return 0;
 }
 

上述就是一个简单的递归程序,只不过上⾯的递归只是为了演示递归的基本形式,不是为了解决问 题,代码最终也会陷入死递归,最终导致程序崩溃。

递归的思想

递归可以把一个大型复杂问题层层转化为一个与原问题相似,但规模较小的子问题来求解;直到子问题不能再被拆分,可以直接求解,递归就结束了。递归的思考方式就是把大事化小的过程。 递归中的递就是递推的意思,归就是回归的意思,接下来慢慢来体会。

递归的必要条件

递归在书写的时候,有2个必要条件:

1.递归存在限制条件,当满足这个限制条件的时候,递归便不再继续。

2.每次递归调用之后越来越接近这个限制条件。 在下面的例子中,我们逐步体会这2个限制条件。

题目:计算n的阶乘(不考虑溢出),n的阶乘就是1~n的数字累积相乘。 一个正整数的阶乘(factorial)是所有小于及等于该数的正整数的积,并且0的阶乘为1。 自然数n的阶乘写作n!。

递归分析

n 的阶乘的公式: n ! = n∗(n−1)!

cpp 复制代码
 举例:        
    5! = 5*4*3*2*1
    4! = 4*3*2*1
     
    所以:
	 5! = 5*4!

从这个公式不难看出:如何把一个较大的问题,转换为一个与原问题相似,但规模较小的问题来求解 的。 n-1 的阶乘和 n 的阶乘是相似的问题,但是规模较小,其中有一种有特殊情况是:当 n==0 的时 候, n 的阶乘是 1 ,而其余 n 的阶乘都是可以通过上面的公式计算。

代码实现

那我们就可以写出函数 Fact 求 n 的阶乘,如果 Fact(n) 就是求 n 的阶乘,那么 Fact(n-1) 就 是求 n-1 的阶乘,函数如下:

cpp 复制代码
int Fact(int n)
{
    if (n == 0)
        return 1;
    else
        return n * Fact(n - 1);
 }

得出

cpp 复制代码
#include <iostream>
using namespace std;

int Fact(int n)
{
    if (n == 0)
        return 1;
    else
        return n * Fact(n - 1);
 }
 
int main()
{
    int n = 0;
    cin >> n;
    //输入4 
    int ret = Fact(n);
    cout << ret << endl;
    //输出24
	 
    
    return 0;
 } 

递归和循环(迭代)

关于求 n 的阶乘,如果根据公式很容易写出递归的代码,但其实这个问题也可以是循环的方式解决。 要想计算 n 的阶乘,只要能产生 1 ~ n 的数字,然后累计相乘就可以了。比如:

cpp 复制代码
int Fact(int n)
{
    int i = 0;
    int ret = 1;
    for(i = 1; i <= n; i++)
    {
        ret *= i;
    }
    return ret;
 }

这里简单的对比一下递归和循环的差异:

!!:在C语言中每一次函数调用,都需要为本次函数调用在内存的栈区,申请一块内存空间来保存函数调用期间的各种局部变量的值 ,这块空间被称为运行时堆栈,或者函数栈帧 。 函数不返回,函数对应的栈帧空间就一直占用,所以如果函数调用中存在递归调用的话,每一次递归函数调用都会开辟属于自己的栈帧空间,直到函数递归不再继续,开始回归,才逐 层释放栈帧空间 。 所以如果采用函数递归的方式完成代码,递归层次太深,就会浪费太多的栈帧空间 ,也可能引起栈溢出(stackoverflow)的问题。 所以就当前的问题来看,使用循环来解决更好,效率更高。 我们看到的许多问题是以递归的形式进行解释的,这只是因为它比非递归的形式更加清晰,但是这些问题的迭代实现往往比递归实现效率更高。当一个问题非常复杂,难以使用迭代的方式实现时,此时 递归实现的简洁性便可以补偿它所带来的运行时开销。

eg:求第n个斐波那契数

写一个代码,求第n个斐波那契数,关于斐波那契数列,之前我们做过题目了,斐波那契数列是: 1 2 3 5 8 13 21 34 55 ... ,该数列的特点是前两个数相加等于第三个数。

递推公式即

这样我们很容易写出代码

cpp 复制代码
int Fib(int n)
{
    if(n <= 2)
        return 1;
    else
        return Fib(n-1) + Fib(n-2);
 }

即完整代码为

cpp 复制代码
int Fib(int n)
{
    if(n <= 2)
        return 1;
    else
        return Fib(n-1) + Fib(n-2);
 }


#include <iostream>
using namespace std;

int main()
{
    int n = 0;
    cin >> n;
    
    int ret = Fib(n);
    cout << ret << endl;
    
    return 0;
 }

但是当我们输入50时,程序不给结果了,这是为什么呢?

其实递归程序会不断的展开,在展开的过程中,我们很容易就能发现,在递归的过程中会有重复计 算,而且递归层次越深,冗余计算就会越多。

那我们可以优化为

cpp 复制代码
#include <iostream>
using namespace std;
int count = 0;

int Fib(int n)
{
    if(n == 3)
        count++;
		//统计第3个斐波那契数被计算的次数
 
    if(n <= 2)
        return 1;
    else
        return Fib(n - 1) + Fib(n - 2);
}

int main()
{
    int n = 0;
    cin >> n;
    //输入40 
    int ret = Fib(n);
    cout << ret << endl; 
    cout << "count = " << count << endl;
    //输出 102334155
    //count = 39088169  
    return 0;
 }

这⾥我们看到了,在计算第 40 个斐波那契数的时候,使用递归方式,第 3 个斐波那契数就被重复计 算了 39088169 次,这些计算是非常常冗余的。所以斐波那契数的计算,使用递归是非常不明智的,我们就得想迭代的方式解决。 我们知道斐波那契数的前2个数都1,然后前2个数相加就是第3个数,那么我们从前往后,从小到大计算就行了

这样就有下面的代码:

cpp 复制代码
int Fib(int n)
 {
    int a = 1;
    int b = 1;
    int c = 1;
    while(n > 2)
    {
        c = a + b;
        a = b;
        b = c;
        n--;
    }
    return c;
 }

迭代的方式去实现这个代码,效率就要高出很多了。 所以在编写代码的时候,我们把要斟酌一下,有时候,使⽤递归很方便的写出代码,但是也会引入一 些问题,那么这种情况下,我们就要写出迭代的版本。

cpp 复制代码
#include <iostream>
using namespace std;
int count = 0;

int Fib(int n)
{
    int a = 1;
    int b = 1;
    int c = 1;
    while(n > 2)
    {
        c = a + b;
        a = b;
        b = c;
        n--;
    }
    return c;
    
 }

int main()
{
    int n = 0;
    cin >> n;
    //输入40 
    int ret = Fib(n);
    cout << ret << endl; 
    cout << "count = " << count << endl;
    //输出 102334155
    //count = 0 
    return 0;
 }

好啦~,到此我们函数递归也是完结咯,断断续续的写了近一个星期,希望大家也能继续坚持呀,下一篇预告结构体~

相关推荐
。TAT。3 小时前
C++ - vector
开发语言·c++·学习
杨福瑞3 小时前
C语言数据结构:算法复杂度(1)
c语言·开发语言·数据结构
郭源潮14 小时前
《Muduo网络库:实现one loop per thread设计模式》
开发语言·c++·网络库
linksinke4 小时前
html案例:制作一个图片水印生成器,防止复印件被滥用
开发语言·前端·程序人生·html
^_^ 纵歌4 小时前
rust主要用于哪些领域
开发语言·后端·rust
_OP_CHEN4 小时前
C++基础:(十三)list类的模拟实现
开发语言·c++·反向迭代器·stl·list·list模拟实现·vector和list对比
froginwe114 小时前
R Excel 文件:高效数据处理与可视化分析利器
开发语言
练习时长一年4 小时前
@Scope失效问题
java·开发语言
淘晶驰AK4 小时前
主流的 MCU 开发语言为什么是 C 而不是 C++?
c语言·开发语言·单片机