函数

一、函数的定义

常量:const int a=10;

变量: int a;

int sum = 0;
for(int i=1; i <= 5; ++i)
{
    sum = sum + i;
}
cout << sum;

这个只能做从1加到5,我们想从1加到10,加到n呢?我们只能改程序中的for的条件,

int sum = 0;
int n = 5;

for(int i=1; i <= n; ++i)
{
    sum = sum + i;
}
cout << sum;

爸爸给小明买了一个复读机,不管小明说什么,复读机都能一摸一样的读出来。

在程序里,复读机可以设计成一个函数,每次调用这个函数,传入一句话(字符串)作为参数,函数就会输出这句话。

void repeat(string s) {
	cout << s << endl;  
}

1、函数的简介

函数是实现程序模块化结构的重要手段,用来封装重复代码,提炼成某一类特定的任务,供程序调用。

  • 有些函数系统已经封装好了,可以直接调用,例如cmath类库里的min函数求最小值,sqrt,abs,fabs,strlen。
  • 有些函数则需要根据自己的需求进行封装,例如封装一个输出5个星号的函数。

2、函数的声明

返回值类型  函数名(参数列表);
int min(int a, int b);

函数的声明由以下几部分组成:

  • 返回值类型:声明这个函数要返回的结果的类型,void表示无返回。
  • 函数名:描述函数的意思和用途,调用的时候要一摸一样。
  • 参数列表:小括号里面,表示函数要传入哪些参数。

3、函数的调用

根据函数的声明,就可以对函数进行调用:

返回值类型 变量 = 函数名(传入参数列表);
   int a = min(3, 5);

void:void类型表示无返回,调用的时候,不需要返回 ,例如:

show(10);

4、函数的定义

如果自己封装一个函数,就要在声明的基础上进行定义(增加函数体部分):

返回值类型  函数名(参数列表)
{
    //函数体,在这里实现函数的代码,并且返回结果
}
  • 函数体里面默认定义了参数列表里的局部参数。
  • 使用return 语句返回结果,类型要和声明的返回值类型一致,例如return 0。
  • 一般return语句在最后执行,如果中间被执行到,则后面的语句不会继续执行。
  • void表示不需要返回,但也可以在函数体中间直接使用return语句返回。

注意:不能在函数里面定义函数。

5、先定义后调用

  • c++语言采用先定义、后调用的方式,函数定义需要在调用代码之前。
  • 假如想把定义放到调用之后,可以把函数的声明先放到调用前面,然后在调用后面定义函数。
    //函数声明
    int abs(int a);
    
    int main() {
      int a = abs(3); //调用
      cout << a << endl;
      return 0;
    }
    
    //函数定义
    int abs(int a) {
      //函数体实现
    }
    

6、函数定义与编译过程(选)

计算机只能识别和执行机器语言,编译就是指将高级语言翻译成机器语言的过程。

编译包括:预处理、编译、链接三个过程。

  • 预处理:处理#include(包含)、#define(宏定义)等。
  • 编译:将每个代码文件翻译成目标文件(后缀是.o)。
  • 链接:将多个目标文件链接起来,组成可执行文件(win下后缀是.exe)

代码的运行就是指调用最后链接成的可执行文件。

函数声明后,可以调用,此时能通过编译,但是在链接的时候会报错(因为函数具体的实现没有),导致可执行文件不能生成。

7、重名错误(选)

如果有变量和函数重名了,就会报重名的错误。

例如:

  • 如果有个变量名为min,再调用min函数,就会报错。

    int min=0;
      int a = min(3, 5);
    
  • 但如果变量名在函数调用后面,就不会报错。

    int a = min(3, 5);
      int min = 0;
    

8、例题:一本通p1150

参考代码:

#include <iostream>
#include <cstring>
using namespace std;
int n;
int isFullNum(int k);
int isFullNum(int k)
{
    // k = a * b ,a <= b;
    int sum = 1;
    for (int i = 2; i * i <= k; ++i)
    {
        if (k % i == 0)
        {
            sum = sum + i + k / i;
        }
    }
    if (sum == k)
    {
        return 1;
    }
    return 0;
}
int main(void)
{
    cin >> n;
    //2...n 的每一个数都去看一看,这个数的因子和是否与这个数相同
    for (int i = 2; i <= n; ++i)
    {
        //i是这个数,i是否是完全数
        if (isFullNum(i))
        {
            cout << i << endl;
        }
    }

    return 0;
}

一本通1152

#include <iostream>
#include <cstdio>
#include<cmath>
#include <cstring>
using namespace std;
int a, b, c;//全局变量
int max3(int x, int y, int z);// 函数声明
int main(void)
{
    cin >> a >> b >> c;
    float m;
    float m1, m2, m3;
    m1 = max3(a, b, c);
    m2 = max3(a + b, b, c);
    m3 = max3(a, b, b + c);
    m = m1 / (m2 * m3);
    printf("%.3f\n", m);

    return 0;
}
int max3(int x,int y,int z) // 函数定义
{
    return max(max(x, y), z);
}

二、函数的参数

1、形参和实参

  • 形参:函数定义的参数称之为形式参数(简称形参),形参的名字是可以自由修改的,相当于在函数体里定义变量。

    int add(int a, int b) {
        return a + b;
      }
    
  • 实参:调用函数的时候,传递给函数的为实际参数(简称实参)。

    • 实参要与形参个数一致、顺序一致、类型一致(会自动转换)。
    • 调用时,实参前面不需要加参数类型,
    • 实参可以是变量、常量、表达式、函数调用等。
      add(1, 2);//变量
      add(n,  3); //常量
      add(5*4, 10); //表达式
      add(add(3, 4), 5); //函数调用
      
  • 默认值:可以给形参指定默认值,如果调用时不传入该实参,则使用默认值。

    int add(int a, int b=10) { return a+b; }
     int sum = add(3);  //结果是3+10 = 13
    
  • 内存:当函数被调用的时候,为形参分配内存,把实参的值赋给形参(值传递),当函数结束时,内存被释放。多次函数调用时,分配的是多个内存地址。

2、值传递

实参和形参的传递有几种值传递、引用传递、指针传递等。

  • 值传递:在调用时,把实参的值拷贝给形参,这种传递是单向的,在函数体里面修改了形参的值,函数结束后不会影响到实参变量的值。
  • 引用传递和指针传递:这两种传递方式是双向的,函数体里面修改了形参的值,函数结束后会影响到实参变量的值。

值传递和引用传递:

void change(int a) {//值传递
//void change(int &a) {//引用传递
   a++; 
}

int n = 3;
change(n); 
cout << n; //n还是3,没有改变

3. 局部变量和全局变量

3.1 局部变量:
  • 函数体里定义的变量,为局部变量,函数外面不能调用这个变量。
  • 形参可以看成是函数的局部变量。
  • 代码块(用大括号框起来的)里定义的变量,只能在该代码块中调用。
  • 局部变量需要显式初始化,否则初始值不确定。
3.2 全局变量:
  • 在函数外面定义的变量,为全局变量,后面的所有函数都能调用这个全局变量。
  • 全局变量会被自动初始化为00
3.3 同名冲突
  • 两个全局变量同名,会报错。
  • 同一个函数里两个局部变量同名,会报错。
  • 函数的局部变量和函数里代码块的局部变量同名,会报错。
  • 不同函数的局部变量同名,没关系,调用自己函数的局部变量。
  • 两个代码块里面的局部变量同名,没关系,调用自己代码块的局部变量。
  • 全局变量与函数局部变量同名,没关系,函数里调用的是局部变量,函数外面调用的是全局变量。
  • 如果函数里想调用同名的全局变量,使用::变量名。

三、函数的嵌套

1、函数嵌套

函数可以继续调用其它函数,但main函数不能给别的函数调用。

调用函数的时候,就会经历一次保存现场、进入调用函数、调用函数返回、恢复现场继续往下执行的过程,如果产生多级嵌套调用,那么保存现场和恢复现场的过程就会形成一种深V的结构。

enter f1                f1 return
enter   f2           f2    return
enter      f3     f3       return
enter          f4          return

2、函数嵌套的主要作用

函数封装、嵌套的主要作用:代码封装、模块化、重用,小步编码和测试。

四、函数递归

1、递归调用

函数直接或间接调用了自己,称之为递归调用。

如果递归调用一直没有停止,则无限次保存现场(没有恢复)的结果就会导致栈溢出,所以,正常的递归一定要有停止条件,我们可以理解成“有去有回”。

12

2、递归函数

正常递归的两大要素:

  • 递归关系式:描述本次调用和上次调用的关系。
  • 停止条件:当递归到某一个条件时,不需要继续递归下去,可直接处理结果。

例子: f(n)= 1 + 2 + 3 + 4 +... + n。

  • 递归关系式: f(n) = n + f(n-1); n > 1
  • 停止条件: f(1) = 1; n = 1

这两点合起来,就可以定义为为递归函数。

3、代码框架

int f(int n) {
  if (n == 1) return 1;  //最小问题,直接返回
  return n + f(n-1);  //问题范围变小
}

4、递归和循环的差异?(选)

  • 循环递推:从前往后推,如果没有结束条件会死循环。
  • 递归函数:从后往前推,函数自己调用自己,有入栈出栈的操作,如果没有终止条件会栈溢出。

5、主要场景(选)

函数递归的主要场景:解问题,将大问题转化为小问题,直到可直接求解的规模,再依次返回。

6、尾递归(选)

如果递归的最后一步是直接返回下一次运算结果,例如:return f(n-1)。则编译器会进行优化,使之底层通过循环递推来实现。

如果最后一步还需要在下一次运算结果上进行运算,例如:return 1 + f(n-1)。则编译器没法优化,需要占用入栈出栈的操作。

五、例题

####一本通 1160:倒序数

【1160:倒序数】

解法一:(字符串)

#include <iostream>
#include <cmath>
#include <cstdio>
#include <cstring>
using namespace std;

int main()
{
    string s;
    cin >> s;
    for (int i = s.length()-1; i >= 0; --i)
    {
        cout << s[i];
    }
    return 0;
}

解法二:(递推)

#include <iostream>
#include <cmath>
#include <cstdio>
#include <cstring>
using namespace std;

int a;
int main()
{
    cin >> a;
    while(a)
    {
        cout << a % 10;
        a = a / 10;
    }
    return 0;
}

解法三:(递归)

#include <iostream>
#include <cmath>
#include <cstdio>
#include <cstring>
using namespace std;

int a;
void f(int n);
void f(int n)
{
    if(n <= 10) 
    {
        cout << n;
        return;
    }
    cout << n % 10;
    f(n / 10);

}
int main()
{
    cin >> a;

    f(a);
    return 0
        
}

附:二进制与十进制

}