吉林大学面向对象编程笔记(5)函数

133 阅读8分钟

函数

函数声明

  • 即函数原型(函数签名/signature)指明函数的名字、参数个数、参数顺序、参数类型、异常说明等。

  • 格式:[extern] [调用约定] <返回类型><函数名> (<参数表>)[const][异常说明];

    • 从右到左进行参数压栈

    • 谁来负责清栈或恢复堆栈?

      • 调用者负责:可处理变参和确定参数个数的函数;
      • 被调用者负责:只能处理确定参数个数的函数;
    • 被调用函数的名字如何表示?

      • 同一个项目中的函数名字表示,限定名不同即可区分;
      • 不同项目中的函数名字表示,需要区分;

    调用约定

image-20230624154153774.png

  • 返回类型、缺省参数(编译时匹配)值不能作为区分标志。

    详细见函数重载

函数重载

多个同名函数,但它们具有不同参数类型、参数顺序、参数个数、const修饰、异常说明数时,可以同时存在,称为函数重载。

  • 函数参数的个数、类型、顺序、const修饰、异常等不完全相同
  • 返回值类型不作为区分标志
  • 缺省参数不作为区分标志值
  • 类型参数的const型与非const型不作为区分标志
  • 引用和指针类型参数,是否可以改变实参,可作为区分标志

从函数重载的角度理解给定的函数声明:

int func();
int func(int);
int func(int, int);
int func(int, int) const;
int func(int) throw();
int func(int) noexcept;
int func(int) throw(int, MyE, YourE);
int func(int) noexcept(false);
int func(MyClass obj);

这些函数声明展示了函数重载的概念,即在同一个作用域内定义了多个同名函数,但它们具有不同的参数列表、返回类型或其他特征,以便根据函数调用时的参数类型或约束条件来选择匹配的函数。

在给定的声明中,我们可以看到以下情况:

  • func():没有参数的函数重载。
  • func(int):接受一个整数参数的函数重载。
  • func(int, int):接受两个整数参数的函数重载。
  • func(int, int) const:接受两个整数参数的常量成员函数重载。
  • func(int) throw():接受一个整数参数且不会抛出异常的函数重载(适用于 C++98 标准)。
  • func(int) noexcept:接受一个整数参数且不会抛出异常的函数重载(适用于 C++11 及以后的标准)。
  • func(int) throw(int, MyE, YourE):接受一个整数参数且可能抛出 intMyEYourE 类型异常的函数重载(适用于 C++98 标准)。
  • func(int) noexcept(false):接受一个整数参数且可能抛出异常的函数重载(适用于 C++11 及以后的标准)。
  • func(MyClass obj):接受一个 MyClass 类型参数的函数重载。

通过函数重载,可以根据函数调用时提供的参数类型、异常规范和其他特征,选择最匹配的函数进行调用。这样可以提供更灵活和多样化的函数行为。

函数重载的实现-名字重整

由编译器在函数名字的基本信息之上,添加必要的参数信息,形成新的函数名,用于区分不同的重载函数。

例:void fun(int)经重整后,生成类似_fun_int的新函数名

缺点: 一个程序中难以确定另一个程序中的经重整的函数名

函数的参数表

缺省参数的解释和整理:

  • 缺省参数:缺省参数是指在函数定义时为参数提供默认值,使得在函数调用时可以省略对应的实参。缺省参数的目的是为了提供更灵活的函数调用方式。
  • 函数原型中提供缺省参数:通常情况下,应该在函数的声明或原型中提供缺省参数的默认值。函数原型指的是函数的声明,它通常放在头文件中,用于告知函数的存在和接口。
  • 函数定义中提供缺省参数:当函数没有原型或声明时,可以在函数的定义中为参数提供默认值。这种情况下,默认参数的定义必须在调用它的代码之前。
  • 无名参数:在函数定义中,可以省略参数的名称,只提供类型。这在函数没有使用到对应参数的情况下可以使用,但在函数体内无法直接访问该参数。

以下是一个示例,展示了缺省参数、函数原型和无名参数的用法:

// 函数原型中提供缺省参数的默认值
void foo(int x, int y = 0);
​
// 函数定义中提供缺省参数的默认值(无函数原型)
void bar(int x, int y = 0) {
    // 函数体
}
​
// 函数定义中使用无名参数
void baz(int, int); // 声明/原型void baz(int, int) {
    // 函数体
}

在上述示例中,foo() 的函数原型中提供了缺省参数 y 的默认值,而 bar() 的函数定义中也提供了相同的缺省参数默认值。baz() 的函数定义中使用了无名参数。

需要注意的是,缺省参数的定义通常应放在函数的声明或原型中,以便在不同的编译单元中能够正确使用该默认值。此外,在使用无名参数时需要注意,无名参数只是在函数定义中省略了参数名称,但仍然需要提供参数类型。

实参与形参的匹配

  • 压栈顺序由调用约定确定

  • 但计算顺序是不确定的

  • 缺省参数的匹配

    • 在编译时匹配,而不是运行时;

    • 第一个带缺省值参数的右侧必须都有缺省值;

      void f(int,char=‘c’,int); //非法

      void f(int,char=‘c’,int=9);//正确

  • 实参的类型转换

    若不存在完全匹配的函数,尝试类型转换(每个参数只一次)。

    void f(int);

    void f(double);

    调用:f(2.5f);

  • 值传递

    cppCopy codeint a = 10;
    const int b = 20;
    My mObj;
    const My cmObj;
    ​
    void f(int, My);
    或
    void f(const int, const My);
    ​
    f(a, mObj);    // 正确,传递非常量整数和非常量 My 对象
    f(b, mObj);    // 正确,传递常量整数和非常量 My 对象
    f(a, cmObj);   // 正确,传递非常量整数和常量 My 对象
    f(b, cmObj);   // 正确,传递常量整数和常量 My 对象
    
  • 指针传递

    cppCopy codeint a = 10;
    My mObj;
    int* pa = &a;
    My* pmy = &mObj;
    const int* cpa = pa;
    const My* cpmy = pmy;
    ​
    void f(int*, My*);
    void f(const int*, const My*);
    ​
    f(pa, pmy);      // (1) 使用非常量指针作为参数
    f(pa, cpmy);     // (2) 使用非常量指针和常量指针作为参数
    f(cpa, pmy);     // (2) 使用常量指针和非常量指针作为参数
    f(cpa, cpmy);    // (2) 使用常量指针作为参数
    
  • 数组的传递

    int a[3]={1,2,3};
    //以下几个等价
    void f(int[ ]);
    void f(int a[ ]);
    void f(int a[3]);
    void f(int a[5]);
    void f(int * a);
    ​
    
  • 引用传递

    int a = 1;
    const int& b = a;
    const int c = 2;
    ​
    void f(int, int);
    void f(int&, int&);
    void f(int, const int&);
    void f(int&, const int&);
    void f(const int&, const int&);
    ​
    f(a, a);               // 调用 f(int, int)
    f(a, b);               // 调用 f(int, int)
    f(a, c);               // 调用 f(int, const int&)
    f(a, 3);               // 调用 f(int, int)f(a, a);               // 调用 f(int&, int&)
    f(a, b);               // 调用 f(int&, int&)
    f(a, c);               // 调用 f(int&, const int&)
    f(a, 3);               // 调用 f(int&, int)f(a, a);               // 调用 f(int, const int&)
    f(a, b);               // 调用 f(int, const int&)
    f(a, c);               // 调用 f(int, const int&)
    f(a, 3);               // 调用 f(int, const int&)
    f(b, 3);               // 调用 f(int, const int&)f(a, a);               // 调用 f(int&, const int&)
    f(a, b);               // 调用 f(int&, const int&)
    f(a, c);               // 调用 f(int&, const int&)
    f(a, 3);               // 调用 f(int&, const int&)
    f(b, b);               // 调用 f(int&, const int&)f(a, a);               // 调用 f(const int&, const int&)
    f(a, b);               // 调用 f(const int&, const int&)
    f(a, c);               // 调用 f(const int&, const int&)
    f(a, 3);               // 调用 f(const int&, const int&)
    f(b, b);               // 调用 f(const int&, const int&)
    f(3, 3);               // 调用 f(const int&, const int&)
    

返回引用

下面是对返回引用的解释和整理:

T& f();
// 不等价于
const T& f();
// 必须返回一个有效对象的引用
​
int global = 100; // 全局变量
​
int& other(int&, const int&);
​
int& f(int a, int& b, const int& c) {
     int lVar = 88;  // 局部临时变量
     static int sVar = 99;  // 局部静态变量
​
     return a;  // 返回 a 的引用
     return b;  // 返回 b 的引用
     return c;  // 返回 c 的引用
     return global;  // 返回全局变量 global 的引用
     return global + 8;  // 返回表达式 global+8 的引用
     return lVar;  // 返回局部临时变量 lVar 的引用 (不安全)
     return sVar;  // 返回局部静态变量 sVar 的引用
     return sVar + 2;  // 返回表达式 sVar+2 的引用
     return sVar += 3;  // 返回表达式 sVar+=3 的引用
     return other(lVar, b);  // 返回函数 other(lVar, b) 的引用
     return other(sVar + 2, global);  // 返回函数 other(sVar+2, global) 的引用
}

在给定的代码中,我们有以下情况:

  • T& f();:定义了一个函数 f,返回一个类型为 T 的非常量引用。
  • const T& f();:定义了一个函数 f,返回一个类型为 T 的常量引用。
  • 返回引用:函数 f 中的返回语句返回不同的对象的引用,包括函数参数、全局变量、局部变量和其他函数的返回值。

需要注意的是:

  • 返回局部临时变量的引用(例如 lVar)是不安全的,因为在函数执行完毕后,该局部变量将不再存在,引用将会变成悬空引用。
  • 返回静态局部变量的引用(例如 sVar)是有效的,因为静态局部变量在整个程序生命周期内都存在。
  • 返回其他函数的返回值的引用(例如 other(lVar, b)other(sVar+2, global))取决于函数 other 的返回值类型和生命周期。