7. 函数

181 阅读21分钟

7. 函数


基本概念

返回值类型 函数名(参数类型1 参数1)
{
    函数体;
    return 返回值;
}
  1. 函数包括三个部分:

    • 原型:

      • 描述了函数到编译器的接口。也就是说他将函数返回值的类型以及参数的类型和数量告诉编译器。
      • 原型中的变量名相当于占位符。可以包括,也可以不包括,也不必与函数定义中的变量名相同。
    • 定义

      • 定义函数的实现
    • 调用

  2. 原型有啥用?

    double cube(double x);为例

    1. 确保编译器正确处理函数返回值。

      • 当函数执行完,将返回值放在指定区域--CPU寄存器或内存中。然后调用函数(指调用这个函数的函数)将从这个位置取得返回值。 因为原型中指出了返回值的类型,所以编译器直到应该检索多少字节并直到如何解释他们。如果没有原型,编译器便无法得知。

    • 确保编译器检查使用的参数数目是否正确

      • 如果在调用函数时,没有传递参数,那么编译器将会指出错误。如果没有函数原型,那么编译器将会使double z = cube()编译通过,编译器将找到cube()调用存放值的位置,并使用这里的值,这会导致不正确的结果。
    • 确保编译器检查使用的参数类型是否正确,如不正确,则转换为正确的类型(如果可能的话)。

      • 当有意义时,编译器才会进行类型转换。如double z = cube(2);传入一个int值,编译器会自动转换类型。
  3. 一个函数的调用过程

    EIP:指令指针,即指向下一条即将执行的指令的地址。EBP:基址指针,常用来指向栈底;ESP为栈指针,常用来指向栈顶。

    1. 传递参数

      包括值传递、引用传递、指针传递

    2. 创建函数栈帧

      将返回地址、保存的寄存器、参数、局部变量、栈指针(指向栈的顶部,最后一个入栈)保存到函数栈帧中,保存现场。

    3. 跳转并执行函数

      PC跳转到函数的入口地址,开始执行函数体

    4. 保存返回值

      将返回值存储到寄存器或者内存中,以便函数调用者使用

    5. 销毁函数栈帧

      函数执行完毕并返回,销毁栈帧释放空间,回复现场。

函数和数组

使用函数来处理数组,一般需要将函数名和数组长度传递给函数。

// 当且仅当用于函数头或函数原型时,arr[] 和 *arr 的含义相同,都意味着arr是一个int指针。
// 所以在此时,下式恒等
// arr[i] == *(arr + i);
// &arr[i] == arr + i; 
int sum_arr(int arr[], int n);
int sum_arr(int *arr, int n);
​

将数组用作参数,相当于传递的是数组的地址,这意味着对数组元素进行修改将会修改其原来的值。

如果要避免修改, 可以使用const关键字来限定。

int sum_arr(const int *arr, int n);  // 使用const限定符,限制不能对数组本身进行修改
// 又一次的处理错误输入
// 仅输入一次
double factor;
while (!(cin >> factor))
{
    cin.clear();
    while(cin.get() != '\n')
        continue;
    cout << "Bad input! reinput!"  << endl;
}

Q: 如何处理数组区间的函数

A: 通过传递两个指针完成,一个标识数组的开头,另一个标识超尾

// 如传入参数为 (cookies, cookies + 4) 则是为处理 0 1 2 3 这四个元素。
int sum_arr(const int * begin, const int * end)

指针与const

有两种不同的方法来使用const修饰指针

  • 让指针指向一个常量对象,来防止指针修改所指向的值。(防止修改第二块内存。目标内存内的值)

    int age = 23;
    const int *pt = &age;  // 这是表明pt指向了一个const int
    // 如何理解?
    // 定义语句的组成: 变量类型 变量名 = 值
    // 所以此刻的 const 修饰的是 int, const 被视为变量类型的一部分
    // * 表明这个变量是指针变量
    ​
    *pt += 1;  // 不允许
    cin >> *pt;  // 不允许
    ​
    *pt = 20;  // 不允许
    age = 20;  // 允许
    

    注意:c++不允许将const地址赋给非const指针

    // 被允许
    const float kGEarth = 9.80;
    const float = pe = & kGEarth;
    ​
    //不被允许
    const float kGMoon = 1.63;
    float *pm = &kGMoon;
  • 将指针本身声明为一个常量,防止修改指针指向的位置。(防止修改第一块内存。指针内存储的目标内存地址)

    int age = 23;
    int const *pt = &age;  // 这表明声明了const *pt 指向了一个int类型
    // 此时const修饰的是指针, 被视为指针的一部分
    // 变量类型为int
    

二维数组

二维数组的话,还是使用[]比较好

// 函数原型
int sum(int arr[][], int sizex, int sizey);
int sum(int **arr, int sizex, int sizey);  // 两次指针。
int sum(int arr[][4], int sizex);  // 这是表明第二维的维度为4。

// 一些元素的使用
arr[2];  // 代表第三个元素,这个元素是一个一维数组,有4个元素。
arr[1][2];  // 第[2][3]个元素

函数与字符串

  1. 字符串的三种表示方式:

    • char数组
    • 用" "括起的字符常量。(其也会自己添加'\0')
    • 被设置为字符串地址的char指针
  2. 使用函数操作字符串不需要传递数组的大小,因为字符串都是以'\0'结尾。

  3. 如何返回一个字符串?

    不需要返回整个字符串,只需要返回字符串的地址。但我们知道,函数内的局部变量的生命周期只在函数的本次执行时存在,当函数执行结束后,函数栈销毁变量将不会存在。那么应该如何做呢?

    1. 直接传入需要存放的数组,借助已经在主函数中声明的指向字符串的指针

    2. 如何让他不自动销毁? 答案是new。使用new声明的变量,编译器不会帮助我们自动管理,其存放在堆区,需要通过delete自行销毁。那么可以在函数中使用new声明一个char指针。这样就在堆区开辟了一段空间用来存储字符串。

      char * buildstr(char c, int n);
      int main()
      {
          char ch;
          int times;
          char * ps = buildstr(ch, times);  // 保存传来的字符串地址
          delete [] ps;  // 释放空间
      }
      
      // pstr在函数执行结束后所使用的内存会被释放,但注意这是释放的pstr这个在栈中的变量,而使用new在堆区存储的字符串并没有销毁。而pstr这个指针的值就是在堆区的地址。
      char * buildstr(char c, int n)
      {
          char *pstr = new char[n+1];  // 使用new声明一个char * 
          
          return pstr;  // 返回char指针,指针也是一个变量,它存储一个地址,这个地址就是刚刚存字符串的地方
      }
      

      image-20240502162447883

函数与结构

  1. 按值传递

  2. 按指针传递

    struct polar
    {
        double x;
        double y;
    }
    
    void Show(polar pda);  // 按值
    void Show(cosnt polar * pda);  // 按指针,不想修改就加上const限定符。 访问使用->箭头运算符,因为这不是结构的名称而是地址。 
    

递归函数

void recurs(argumentlist)
{
    statements1;
    if(test)
        recurs(arguments);
    statements2;
}

当函数被调用时,函数按顺序执行,即调用recurs函数,statements1将会被执行。然后进行if判定,如果为真,那么将会调用recurs(),这是一次新的调用,你可以传递新的参数。然后在新的调用里面又会发生上述这些事情。直到有一次函数的if判定为假,那就开始return了,开始反向执行statements2(本就是返回调用函数的下一个语句,这就是程序运行的规则,并无例外,不是吗?)。

void countdown(int n)
{
    cout << "这是正向调用 "  << n << endl;
    if(n > 0)
        countdown(n-1);
    cout << "这是反向调用 "  << n << endl;
    return;
}

// 当调用这个函数5次,那么输出为:
/*
这是正向调用 5
这是正向调用 4
这是正向调用 3
这是正向调用 2
这是正向调用 1
这是正向调用 0
这是反向调用 0
这是反向调用 1
这是反向调用 2
这是反向调用 3
这是反向调用 4
这是反向调用 5
*/

注意: 每次递归调用都是一次新的调用哦,其参数的地址是不一样的,比如此程序的"n"。

函数指针

  1. 啥叫函数指针?

    1. 函数指针和其他数据类型的指针一样,他是指向函数的指针。即声明了个指针,这个指针内存储的是函数的入口地址。
  2. 如果要把一个算法函数的地址传给另一个函数应该如何做?

    1. 获取被传递函数的地址

      1. 函数名就是函数的地址

        // 假设存在函数think()
        
        process(think);  // 传递函数地址
        process(think());  // 传递函数返回值,相当于先调用函数think(),取得返回值,再将返回值传递给process()
        
    2. 声明一个函数指针,指向(存储函数入口地址)被调用函数。

      1. 如何声明一个函数指针?

        声明基本类型的指针时,需要指定指针类型。声明函数指针时,需要指定指向函数的类型

        什么确定了函数的类型?返回值类型及函数的特征标。所以函数指针如此声明:

        return_type (*pointer_name)(parameter_types);
        
        //ex:
        double pam(int);  // 函数原型
        double (*pf)(int);  // 函数指针  就是将函数名声明为指针(*名),可以用这个指针指向(要指向的函数名)
        // 为提供正确的优先级,使用() 将*pf括起来,表明pf是一个指针,而(*pf)(int) 意味着pf是一个指向函数的指针
        
    3. 使用函数指针调用函数

      有两种用法

      1. 将函数指针当作普通调用函数使用

        double pam(int);
        double (*pf)(int); 
        
        int main()
        {
            pf = pam;  // 给指针赋值, 将函数入口地址给指针。
            // 下面两个相同
            pam(4);
            (*pam)(4);
            
        }
        
      2. 将函数指针作为形参,用以传递函数

        void estimate(int lines, double (*pf)(int));  // 将函数指针作为形参。那么可以将函数名传进来。
        double pam(int);
        double bad(int);
        int main()
        {
            // 将函数名传递。
            estimate(5, pam);
            estimate(9, bad);
        }
        

内联函数

inline double square(double x) {return x * x;}

Q: 如何编写一个内联函数?

A:

  • 在其声明前添加关键字 inline
  • 在其定义其添加关键字inline

而通常的做法是省略函数的原型(声明)。而直接将整个定义放在本应该提供函数原型的地方。

Q: 内敛函数如何工作?

A: 编译器将内联函数插入到每个调用它的地方。这省略了函数调用的时间,但需要更多的内存。

注意: 内敛函数不能递归。

引用变量

  1. 什么是引用变量?

    1. 引用是指引用已经定义好的变量,给已定义的变量的别名。他们指向相同的值和内存单元。所以对原变量进行操作相当于对原始变量进行操作。引用就是给已分配的内存单元一个别名。
  2. 如何声明一个引用变量?

    1. 使用&符号

      int rats = 5;
      int & rodents = rats;  // 声明rodents为变量rats的引用。
      
      // 对任一操作将会影响两个变量。
      rats++;
      // 此时rodents = 6;
      
  3. 引用变量与指针的区别

    1. 引用必须在声明时初始化,而指针不需要。

      int *pt;  // 被允许不初始化,只需要在解除引用之前给其分配地址
      
      int a;
      int &b = a;  
      int &c;  // 不被允许
      
    2. 引用一旦与某个变量关联起来,就永远效忠

      int a, c;
      int &b = a;  
      &b = c;  // 不被允许
      
      // 下面两式等价。
      int & rodents = rats;
      int * const rodents = rats;
      
  4. 引用作为函数的参数

    1. 引用经常被当作函数的参数,使得函数中的变量名成为调用函数中的变量的别名。而注意,对引用变量操作就相当于对全变量操作(引用变量本就是将已经分配的地址添加别名。)

    2. 如果不想修改引用变量,那么应将引用变量声明为const

    3. 为什么要将引用作为函数的参数?

      1. 节省内存, 不用复制一遍,效率高
      2. 可以直接修改实参的值
  5. 将引用作为函数返回值

    1. 主要用于结构和类,用以返回一个最初传给函数的对象。

    2. 效率高,不用再创建一个临时的变量

    3. 返回引用实际上是被引用的变量的别名

    4. 返回const 类型的引用。为了使返回值不能被赋值

      const free_throws &
          accumutlate(free_throws & target, const free_throws & source);
      // 这样使得下面的语句非法
      accumulate(dup, five) = four;  // 试图改变一个const值
      
      // 但一般谁他喵的这么做。
      // 一般将返回值声明为 const,是为了传入的参数本就是const,
      // 因为你不能让一个非const指向const对象
      
  6. 左值和非左值

    1. 左值:具有内存地址的值,可以在程序中找到并修改。

      • 变量
      • 数组元素
      • 引用
    2. 右值:不具有持久性的内存地址,在本条语句完成后就会被销毁的临时值。

      • 字面常量
      • 临时对象
      • 返回右值引用的函数调用
    int x = 6;  // x 具有内存地址,是左值。 6不具有,是临时的,是右值
    int &y = x;  // allow
    
    int &z1 = x * 6;  // 错误, x * 6 是个临时的表达式,是一个右值
    const int &z2 = x * 6;  // 正确, x * 6是一个右值, 但常量引用可以绑定到临时值
    
    int &&z3 = x * 6; // 正确,右值引用
    int &&z4 = x; // 错误,x是一个左值
    
    1. 右值引用有什么用?

      1. 移动语义(Move Semantics) :右值引用允许我们从临时对象“窃取”资源而不是进行深度复制,从而提高程序的性能。这在处理大型数据结构或需要频繁创建临时对象的情况下特别有用。
      2. 完美转发(Perfect Forwarding) :右值引用可以用于实现完美转发,即将参数以原样传递给其他函数,无论这些参数是左值还是右值。这在实现通用代码时非常有用,特别是在泛型编程中。
      3. 移动语义和右值引用的结合:通过使用移动语义和右值引用,我们可以设计更高效的容器和类,例如移动构造函数和移动赋值运算符,以提高资源管理的效率和性能。

默认参数

  1. 需要通过函数原型设置默认值,函数头不能带默认值
  2. 设置默认值需要从右至左添加默认值
  3. 默认参数使得能够通过不同数目的参数调用同一个函数

函数重载(函数多态)

  1. 什么是函数多态?

    1. 同名但不同功能的函数。通过函数的特征标所区分
  2. 如何进行函数多态?

    通过相同函数名,不同特征标来表示不同的函数

    • 参数类型
    • 参数数目
    • 参数顺序

    注意:

    1. 不能通过传递引用和传递值来重载函数,因为值就是创建一个拷贝变量。

      double cube(double x);
      double cube(double &x);
      // 传入x与这两个模型都匹配
      
    2. 但能够通过const与否来重载函数

      void dribble(char * bits);
      void dribble(const char * bits);
      // const 被视为参数类型的一部分 与 非const 构成函数的重载
      // 编译器将根据传递的实参是否为const类型来决定使用哪个函数
      
    3. 能够通过不同版本的引用参数来重载引用参数

      void sink(double & r1);  // 普通引用
      void sink(const double & r2);  // const 引用
      void sink(double && r3);  // 右值引用
      
      int main()
      {
          double x = 55.5;
          const double y = 32.0;
          sink(x);  // 普通引用
          sink(y);  // const引用
          // 右值引用 如果没有定义右值引用,编译器将会调用const引用,因为const引用可以绑定右值。
          //如果const应用也没有,将会报错
          sink(x + y);  
          
      }
      
      void sink(double & r1)
      {
          cout << "这是普通引用"  << endl;
      }
      
      void sink(const double & r2)
      {
          cout << "这是const引用"  << endl;
      }
      
      void sink(double && r3)
      {
          cout << "这是右值引用"  << endl;
      }
      
      /*
      输出:
      这是普通引用
      这是const引用
      这是右值引用
      */
      
    4. 编译器如何跟踪每一个重载函数?

      1. 答案是名称修饰。编译器根据函数中指定的特征标(形参类型、数目、顺序)对每个函数名进行加密

        // 修饰前
        long MyFunctionFoo(int, float);
        // 修饰后  随编译器不同
        ?MyFunctionFoo@@YAXH  
        

函数模板

函数模板

  1. 什么是函数模板

    函数模板是通用的函数描述,通过将类型作为参数传递给模板,可以使编译器生成该类型的函数。,模板并不创建任何函数,只告诉编译器如何定义函数。

  2. 如何实现一个函数模板

    // 使用关键字 template 表明要定义一个模板(或函数,或类)
    // 后面跟尖括号<>,其内部通过typename指出要泛化的类型,
    // 通过传入类型作为参数可以告诉编译器要生成的函数类型。
    
    template <typename T>
    void Swap(T &a, T &b)
    {
        T temp;
        temp = a;
        a = b;
        b = temp;
    }
    
    // 也可以将类型参数作为返回值类型
    template <typename T>
    T Swap(T &a, T &b)
    {
        ...;
        return a;
    }
    
  3. 模板函数的函数头(函数声明)是什么?

    // 注意 函数声明为template和函数名这两行
    template <typename T>
        void Swap(T &b, T &b);
    
    // 函数名是什么?
    // 和普通函数一样
    Swap()
    
  4. 如何调用一个模板函数?

    // 按照正常的函数调用,通过函数名
    
    // 传入类型为int,编译器自动生成Swap的int版本函数
    int x, y;
    Swap(x, y);
    
    // 传入类型为double,编译器自动生成Swap的double版本函数
    double x, y;
    Swap(x, y);
    
  5. 如何重载一个函数模板

    // 与普通函数重载一样,使用不同的特征标
    
    
    template <typename T>
    void Swap(T &b, T &b);
    
    template <typename T>
    void Swap(T &b, T &b, int n);
    
  6. 可以传入多个类型吗?

    // 可以
    template <typename T1, typename T2>
    T1 max(T1 a, T2 b)
    {
          return b < a ? a : b;
    }
    ...
    auto m = max(4, 7.2);
    
  7. typename和class的相同与区别

    • 相同:

      • 在模板定义语法中,typename和class作用完全相同,都是指出后面的变量名是个类型
    • 不同:

      • class可以用来声明一个类

      • typename可以用在嵌套依赖类型。typename可以指出后面的字符串是一个类型名称,而不是泛型内的其他东西(成员变量?函数)

        // 我定义了一个类
        // 并且在类中使用typedef 为int变量定义了一个别名
        class MyArray 
        { 
            publictypedef int LengthType;  // 为int定义了一个别名
        .....
        }
        
        // 如果我想在其他地方使用这个我在这个类中声明的int的别名呢?
        // 比如我要在模板函数中使用它
        // 如果
        template <typename T>
        void foo() {
            // 'typename' 用于告诉编译器 T::LengthType 是一个类型
            typename T::LengthType* ptr;  
            // 如果这条语句这样写 T::LengthType* ptr;  那么编译器可能无法确定这个LengthType是个什么东西。
        }
        
  8. decltype + auto 自动追踪函数返回值类型

    // 问题1:想一下下面的函数模板中某变量应该的类型是什么?
    template <typename T1, typename T2>
    void ft(T1 x, T2 y)
    {
        ...;
        ?type? xyz = x + y;  // xyz 的类型应该声明为什么?
        ...;
    }
    
    // 问题2:如果要根据传入的类型参数指定返回值的类型呢?
    template <typename T1, typename T2>
    ?type? gt(T1 x, T2 y)  // 返回值类型应该如何指定呢?
    {
        return x + y;
    }
    
    // 使用decltype关键字可以在编译时以一个普通表达式对类型进行推导,可以解决问题1
    // ex:
     int i = 4;
     decltype(i) a; //推导结果为int。a的类型为int。
    // 重写问题1
    template <typename T1, typename T2>
    void ft(T1 x, T2 y)
    {
        ...;
        decltype(x + y) xyz = x + y;  // xyz 的类型应该声明为什么?
        ...;
    }
    
    // 使用auto + decltype 可以解决问题2
    // 使用后置返回类型,进行返回类型的自动推导
    template <typename T1, typename T2>
    auto gt(T1 x, T2 y) ->decltype(x + y) // ->后置返回类型
    {
        return x + y;
    }
    
    // decltype 核对表
    decltype(expression) var;
    
    /* 
    1. 如果expression 是没有被括号括起的标识符,则var与此标识符的类型一样,包括const等限定符
    */
    double x = 5.5;
    double &rx = x;
    const double *pd;
    decltype(x) w;  // 声明一个与x类型相同的变量 int
    decltype(rx) u = x;  // 定义一个与rx相同类型的变量  double&
    decltype(pd) pa;  // 声明一个 const double指针
    
    /* 
    2. 如果expression 是函数调用,那么var与函数返回值相同
    */
    long indeed(int);
    decltype (indeed(3)) m;  // m为long。 这并不会调用函数,只是查看原型来获取返回类型
    
    /* 
    3. 如果expression 是一个用括号括起的标识符,则var为指向其类型的引用, 注意是引用
    */
    double xx = 4.4;
    decltype ((xx)) r2 = xx;  // double &
    decltype(xx) w = xx;  // double
    
    /* 
    4. 如果前面都的不满足,则var的类型与expression的类型相同。
    	这主要用于右值
    */
    int j = 6;
    int &k = j;
    int &n = j;
    decltype(j+6) i1;  // int
    decltype(100L) i2;  // long
    decltype(k+n) i3;  // int  k和n是引用,但k+n这个表达式是求两个int的和,所以i3为int
    

显式具体化

  1. 显式具体化模板函数

    C++对于给定的函数名,可以拥有非模板函数、模板函数和显式具体化模板函数以及他们的重载版本。显式具体化模板函数是指显式指出模板中参数的类型。其匹配优先级大于函数模板

  2. 显式具体化模板函数是为什么服务的?

    为非通用类型服务。比如定义了一个结构,你想要交换这个结构中的内容或某些内容,那么普通通用模板无法完成这个事情,需要显式的再定义一个类型。

  3. 如何声明、定义、调用一个显式具体化模板函数

    struct job
    {
        string name;
        int age;
        string job_name;
    }
    
    // 普通模板函数必然无法交换两个job结构类型的变量
    
    // 显式具体化模板函数声明
    template <>
    void Swap<job>(job &a, job &b);
    
    // 定义
    template <>
    void Swap<job>(job &a, job &b)
    {
        ...;
    }
    
    // 调用
    // 同其他函数一样。
    job sue, jam;
    Swap(sue, jam);
    

实例化与具体化

  1. 实例化分为显式实例化和隐式实例化

    1. 隐式实例化

      就是普通的函数调用,编译器根据参数自动推断变量类型

    2. 显式实例化

      通过显式指定函数模板中参数类型的方式调用模板函数

      // 单类型参数
      template <typename T>
      void Swap(T &a, T &b);
      
      // 多类型参数
      template <typename T1, typename T2>
      T1 max(T1 a, T2 b)
      
      int main()
      {
          int x, y;
          
          // 显式实例化
          Swap<int>(x, y);
          max<int, double>)(x, y);
          
          // 多参数的可以不填满, 前面的指定,后面的进行自动推断
          max<double>(x, y); // 会将第一个类型作为double传入
          // 但是不能跳跃指定。
          max<, double>(x, y);  // 不被允许
      
      }
      
    3. 显式具体化

      显式具体化就是不用模板函数,指定显式具体化模板函数

      template <>
      void Swap<job>(job &a, job &b);
      

函数匹配优先级

  1. 如果参数完全匹配。则优先级为:

    • 非模板函数 > 显式具体化模板函数 > 模板函数

    • 但是可以显式的指出使用哪个

      Swap<>(a, b);  // 显式指出使用模板函数
      
  1. 提升转换。

    • char、short转换为int,float转换为double
  2. 标准转换

    • int 转为char, long 转为double
  3. 用户定义的转换,如类生命命中定义的转换

但一般谁他喵的这样定义,为什么要模板函数,就是因为非模板函数重写类型比较麻烦呗。

#include <iostream>

using namespace std;

// 模板函数  优先级:2
template <typename T>
void Swap(T &a, T &b);

// 非模板函数  优先级:0
void Swap(int &a, int &b);

// 显式具体化模板函数  优先级:1
template <>
void  Swap<int>(int &a, int &b);

int main()
{
    int a = 5, b = 6;

    Swap(a, b);
    cout << "a: " << a << "\t" << "b: " << b << endl;

    return 0;
}

template <typename T>
void Swap(T &a, T &b)
{
    T temp;
    temp = a;
    a = b;
    b = temp;
}

template <>
void  Swap<int>(int &a, int &b)
{
    int temp;
    temp = a;
    a = b;
    b = temp;
}

void Swap(int &a, int &b)
{
    int temp;
    temp = a;
    a = b;
    b = temp;
}

ref

函数的调用过程:blog.csdn.net/fu_zk/artic…

typename和class的区别:www.runoob.com/note/12729,写的也一般。

重载函数匹配顺序:blog.csdn.net/qq_28133013…

[]的优先级大于*

// 所以 [] 优先结合,除非使用()改变优先级
int * arr[10];  // 数组指针, 一个指针指向了数组
int (*arr)[10];  // 指针数组, 一个数组,里面全是指针