C++ 学习笔记之(6)-函数、重载和指针

C++ 学习笔记之(6)-函数、重载和指针

函数基础

函数定义包括以下几个部分

返回类型、函数名字、由0个或多个形参组成的列表以及函数体

局部对象

C++语言中,名字有作用域,对象有生命周期

  • 名字的作用域是程序文本的一部分,名字在其中可见
  • 对象的生命周期是程序执行过程中该对象存在的一段时间

自动对象

存在于块执行期间的对象成为自动对象,即执行到变量定义时创建对象,快末尾销毁它。比如形参等。

局部静态对象

在程序的执行路径第一次经过对象定义语句时初始化,并且知道程序终止才被销毁,即使所在的函数执行结束。

size_t count_calls()
{
    static size_t ctr = 0;  // 调用结束后,这个值仍然有效
    return ++ctr;
}
int main()
{
    for(size_t i = 0; i != 10; ++i)
        cout << count_calls() << endl;
    return 0;
}

函数声明

  • 函数原型:即函数声明,包含返回类型,函数名和形参类型。

  • 函数的声明和函数的定义类似,区别是声明无需函数体,用一个分号替代即可

  • 定义函数的源文件应该把含有函数声明的头文件包含进来,编译器负责验证函数的定义和声明是否匹配

参数传递

  • 形参的初始化方式和变量的初始化方式一样

  • 引用传递:形参是引用类型,也被叫做传引用调用,即引用形参是对应实参的别名

  • 值传递:当实参的值拷贝给形参是,形参和实参是两个独立对象,也被称为传值调用

传值参数

  • 指针形参:指针与非引用类型一样,执行拷贝操作时,拷贝的是指针的值。两个指针是不同的指针,但是由于指针可以间接访问所指对象,所以指向的是统一内容

    void reset(int *ip)
    {
      *ip = 0; // 改变指针 ip 所指对象的值
        ip = 0;  // 只改变了 ip 的局部拷贝,实参未被改变
    }

 传引用参数

  • 对于引用的操作实际上是作用在引用所引的对象上

    void reset(int &i)
    {
      i = 0;  // 改变了 i 所引对象的值
    }
  • 使用引用避免拷贝

  • 如果函数无需改变引用形参的值,最好将其声明为常量引用

const 形参和实参

  • 实参初始化形参时会忽略掉顶层const,即形参的顶层const被忽略

    void fcn(const int i) { /* fcn 能够读取 i, 但是不能向 i 写值 */ }
    void fcn(int i) {/* ... */}  // 错误:重复定义 fcn(int)
  • C++语言允许函数名字相同,但前提是形参列表应该有明显区别,由于顶层const被忽略,故上述代码中传入两个函数的参数可以完全一样,故为重定义

  • 非常量能够初始化底层const对象,反之不行

  • 字面值能够初始化常量引用

  • 函数形参尽量使用常量引用

    • 常量:避免函数内修改值,同时可以传入普通参数和常量参数
    • 引用:避免函数对参数进行拷贝,提高效率

知识点

数组形参

数组的两个性质

  • 不允许拷贝数组, 故无法值传递
  • 使用数组时通常会将其转换成指针,故实际上传递给函数的是指向数组首元素的指针
// 下面三个函数等价,每个函数形参都是 const int*
void print(const int*);
void print(const int[]);  // 可以看出,函数的意图是作用于一个数组
void print(const int[10]);  // 维度表示期望数组含有多少元素,但实际不一定

含有可变形参的函数

C++11新标准提供了两个方法处理不同数量实参的函数

  • 若所有实参类型相同,可以传递一个名为initializer_list的标准库类型
  • 若实参不同,则可以拜年可变参数模板(16.4节介绍)

initializer_list

若函数的实参数量未知但全部实参类型相同,则可以使用initializer_list类型的形参, initializer_list是一种标准库类型,用于表示某种特定类型的值的数组。其定义在同名头文件中

initializer_list_operations_methods

  • vector类似,initializer_list也是模板类型,定义时,需要说明类型

  • initializer_list对象中元素必须是常量值,无法改变, 并且放在花括号中

    void error_msg(initializer_list<string> il)
    {
      for(auto beg = il.begin(); beg != il.end(); ++beg)
            cout << *beg << " ";
        cout << endl;
    }
    // 调用语句
    // expected, actual 是 string 对象
    if (expected != actual)
        error_msg({"functionX", expected, actual});
    else
        error_msg({"functionX", "okay"});

省略符形参

省略符形参是为了便于C++程序访问某些特殊的C代码设置的,这些代码使用了varargs的C标准库功能。

  • 省略符形参只能出现在形参列表的最后一个位置,形式有两种

    // 第一种指定了部分形参的类型,对于这部分参数将进行正常的类型检查,省略符形参所对应的实参无需类型检查
    void foo(parm_list, ...);  
    void foo(...);
进阶

返回类型和return语句

return语句终止当前正在执行的函数并将控制权返回到调用该函数的地方,return语句有两种形式

return;
return expression;

无返回值函数

无返回值的return语句用在返回类型是void的函数中,此类函数最后会隐式执行return

有返回值函数

return语句返回值的类型必须与函数的返回类型相同,或能隐式转换成函数的额返回类型

  • 返回值的方式和初始化一个变量或形参的方式完全一样

  • 不要反悔局部对象的引用或指针:因为函数完成后,其存储空间被释放,局部变量的引用指向的内存无效

  • 调用返回引用的函数得到左值,其他返回类型得到右值

  • 列表初始化返回值

    vector<string> process() { return {"return", "example"}};

返回数组指针

函数不能返回数组,所以可以返回数组指针或引用

  • Type (*function (parameter_list)) [dimension]

    // 例子,使用类型别名
    typedef int arrT[10];  // arrT为类型别名,表示类型是含有 10 个整数的数组
    using arrT = int[10];  // arrT的等价声明
    arrT* func(int i); // func 返回一个指向含有 10 个整数的数组的指针
    
    // 例子,没有使用类型别名
    int (*func(int i))[10];
    // 准曾理解该声明含义
    func(int i);  // 表示调用func函数时需要一个int类型的实参
    (*func(int i));  // 意味着我们可以对函数调用的结果执行解引用操作
    (*func(int i))[10];  // 表示解引用func的调用将得到一个大小是10的数组
    int (*func(int i))[10];  // 表示数组中的元素是int 类型
  • 使用尾置返回类型

    // func 接受一个 int 类型的实参,返回一个指针,该指针指向含有 10 个整数的数组
    auto func(int i) -> int(*)[10];
  • 使用 decltype

    int odd[] = {1, 3, 5, 7, 9};
    decltype(odd) *arrPtr(int i);  /* 返回一个指向含有 5 个整数的数组的指针,注意 decltype 的结果是数组,不会把数组类型转换成对应的指针,故需要加 * 符号 */

函数重载

若同一作用域内的几个函数名字相同但形参列表不同,即为重载函数

  • 顶层const形参等价于无顶层const形参

    int lookup(int i);
    int lookup(const int i);  // 重复声明 int lookup(int); 顶层const
    
    int lookup(int *pi);
    int lookup(int * const pi);  // 重复声明 int lookup(int *); 顶层const
  • 底层const可以实现函数重载

    int lookup(int &);  // 函数作用域 int 引用
    int lookup(const int&);  // 新函数,作用域常量引用
    
    int lookup(int *);  // 新函数,作用于指向 int 的指针
    int lookup(const int *);  // 新函数,作用于指向常量的指针
  • const_cast和重载

    // 参数和返回类型都是 const string 的引用
    const string &shorterString(const string &s1, const string &s2)
    {
      return s1.size() <= s2.size() ? s1 : s2;
    }
    // 返回类型是普通引用, 参数为普通变量
    string &shorterString(string &s1, string &s2)
    {
        // 将普通引用转换成对对const的引用,然后调用const版本,返回对const的引用,如果不转换成const,则会根据非常量参数调用非const版本,直至报错
      auto &r = shorterString(const_cast<const string&>(s1), const_cast<const string&>(s2));
        // 将const引用转成普通引用返回
        return const_cast<string&>(r);
    }

特殊用途语言特性

默认实参

  • 某个形参被赋予默认值,则其后面的所有形参都必须有默认值

  • 在给定作用域中,一个形参只能被赋予一次默认实参

    typedef string::size_type sz;
    string screen(sz, sz, char = ' ');
    string screen(sz, sz, char = '*'); // 错误:重复声明
    string screen(sz = 24, sz = 80, char); // 正确:添加默认实参
  • 通常,应该在函数声明中指定默认实参,并将该声明放在合适的头文件中。

  • 局部变量不能作为默认实参,除此之外,只要表达式的类型能转换成形参类型,该表达式就能作为默认实参

内联函数就和constexpr函数

  • 内联函数inline, 即函数在调用点 内联地展开

  • 内联说明只是向编译器发出的请求,编译器可以选择忽略这个请求

  • 很多编译器不支持内联递归函数

  • constexpr函数:能用于常量表达式的函数。 函数的返回类型及所有形参的类型都是字面值类型,函数体中必须有且只有一条 return 语句

    constexpr int new_sz() { return 42; }  // new_sz() 为无参数的 constexpr 函数
  • constexpr函数被隐式地指定为内联函数

  • constexpr函数体内可以包含其他语句,只要这些语句在运行时不执行任何操作,比如空语句,类型别名以及using声明

  • constexpr函数不一定返回常量表达式

    // 若参数 arg 为常量表达式,则 scale(arg) 也是常量表达式
    constexpr size_t scale(size_t cnt) { return new_sz() * cnt; }
  • 内联函数和constexpr函数可以在程序中多次定义,为了多个定义保持完全一致,通常定义在头文件内

调试帮助

  • assert预处理宏:一个预处理变量,类似于内联函数,由预处理器而非编译器管理

    assert(expr);  // 对 exp r求值,若表达式为假(即 0 ),assert输出信息并终止运行;若为真,则什么也不做
  • NDEBUG:预处理变量,assert的行为依赖于次变量的状态, 若定义了NDEBUG, 则assert什么也不错,相当于关闭调试。默认没有定义,此时assert会执行运行时检查

    void print(const int ia[], size_t size)
    {
    
    # ifndef NDEBUG
    
        cerr << __func__ << endl  // 当前调试函数名
            << __FILE__ << endl   // 存放文件名的字符串字面值
            << __LINE__ << endl   // 存放当前行号的整型字面值
            << __TIME__ << endl   // 存放文件编译时间的字符串字面值
            << __DATE__ << endl;  // 存放文件编译时期的字符串字面值
    }

函数匹配

  • 候选函数:调用对应的重载函数集,有两个特征
    • 与被调用函数同名
    • 其声明在调用点可见
  • 可行函数:通过考察调用提供的实参,从候选函数中选出能被这组实参调用的函数,也有两个特征
    • 其形参数量与本次调用提供的实参数量相等
    • 每个实参类型与对应形参类型相同,或能转换成形参类型
  • 实参类型与形参类型能够越接近,匹配的越好
  • 若有多个形参匹配,编译器会一次检查每个实参以确定最佳匹配函数,若未找到,则会报告二义性错误
    • 该函数每个实参的匹配都不劣于其他可行函数需要的匹配
    • 至少有一个实参的匹配由于其他可行函数提供的匹配

实参类型转换

为了确定最佳匹配,编译器将实参类型到形参类型的转换分成等级,如下所示

function_real_arguments_cast

需要类型提升和算数类型转换的匹配

  • 小整形一般会提升到int类型或更大的整数类型

    void ff(int);
    void ff(short);
    ff('a');  // char 提升成 int; 调用 f(int)
  • 所有算数类型转换的级别都一样

    void manip(long);
    void manip(float);
    manip(3.14);  // 错误:二义性调用,因为double可以转换为long 和 float

函数匹配和const形参

若重载函数的区别在于用用类型的形参是否引用了const,或指针类型的形参是否执行const,则编译器通过实参是否为常量决定函数选择

int lookup(int &);  // 参数为 int 引用
int lookup(const int &);  // 参数为常量引用
const int a;
int b;
lookup(a);  // 调用 lookup(const int &)
lookup(b);  // 调用 lookup(int &)

函数指针

函数指针即指向函数的指针,函数指针也是指向某种特定类型。函数类型由其返回类型和形参类型决定,与函数名无关。

// 函数类型是 bool (const string&, const string &)
bool lengthCompare(const string &, const string &); 
// 声明指向该函数的指针,只需要用指针替换函数名即可
// pf 指向一个函数,该函数的参数为两个 const string引用,返回值是 bool 类型
bool (*pf)(const string &, const string &);  // 未初始化
  • 函数名作为值使用时,自动转换为指针

    pf = lengthCompare;  // 等价于 pf = &lengthCompare; 取地址符可选
  • 可直接使用函数指针调用函数,无需解引用

    bool b1 = pf("hello", "goodbye");  // 调用 lengthCompare 函数
    bool b2 = (*pf)("hello", "goodbye");  // 等价于上个调用语句
  • 函数指针不存在类型转换,可为函数指针赋值nullptr0,表示函数指针不指向任何函数

  • 函数指针可做为形参,可以使用类型别名简化形参

    // 第三个形参是函数类型,它会自动转换成指向函数的指针
    void useBigger(const string &s1, const string &s2, bool pf(const string &, const string &));
    // 等价声明,显示地将形参定义成指向函数的指针
    void useBigger(const string &s1, const string &s2, book (*pf)(const string &, const string &));
    // 自动将函数 lengthCompare 转换成指向该函数的指针
    useBigger(s1, s2, lengthCompare);
  • 函数能返回函数指针,与形参不同,编译器不会自动将函数返回类型转换成对应的指针类型。

    using F = int(int *, int);  // F 是函数类型,不是指针
    using PF = int(*)(int *, int);  // PF 是指针类型
    PF f1(int);  // 正确:PF为函数指针,f1 返回函数指针
    F f1(int);  // 错误:F是函数类型,f1不能返回一个函数
    int (*f1(int))(int *, int);  // 等价于PF f1(int);
    auto f1(int) -> int(*)(int *, int);  // 使用尾置返回类型方式声明返回函数指针的函数

结语

  • 函数是命名了的计算单元,每个函数都包括返回类型、名字、(可能为空的)形参列表以及函数体
  • C++中,函数可以被重载:同一个敏子可用于定义多个函数,只要形参数量或形参类型不同即可。

函数相关基础知识已经了解,以后要深入学习函数相关知识。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章