06. 函数

一、什么是函数

  函数其实就是封装好的代码块,并且指定一个名字,调用这个名字就可以执行代码并返回一个结果。函数一般都是实现固定功能的模块,它把参数看成 “输入”,返回结果看成 “输出”,函数就是一个输入到输出的映射关系。

二、函数的定义

  C++ 的库函数在编写程序时时可以直接调用的,而自定义函数则必须由用户对其进行定义,在其函数的定义中完成函数特定的功能,这样才能被其它函数调用。

返回值类型 函数名(形参列表)
{
    函数体;
    return 返回值;
}

【1】、返回值类型

  • 如果方法有返回值,则必须在方法声明时指定返回值类型,同时,方法中需要使用 return 关键字返回指定类型的变量或常量:"retrun 数据;"。
  • 如果方法没有返回值,则方法声明时,使用 void 关键字。通常没有返回值的的方法中,就不需要使用 return,但是,如果使用的话,只能 "return;" 表示结束此方法的意思。

return 关键字可以用来结束方法;

如果函数返回的类型和 return 语句中表达式的值不一致,则以函数返回类型为准,即函数返回类型决定返回值的类型。对数值型数据,可以自动进行类型转换。

如果函数返回的类型和 return 语句中表达式的值不一致,而它又无法自动进行类型转换,程序则会报错。

【2】、方法名

  方法名属于标识符,遵循标识符的规格和规范,见名知意;

【3】、形参列表

  在定义函数时指定的形参,在未出现函数调用时,它们并不占内存中的存储单元,因此称它们是形式参数或虚拟参数,简称形参,表示它们并不是实际存在的数据,所以,形参里的变量不能赋值。

  • 方法可以声明 0 个、1 个、或多个形参。多个形参用 , 分割
  • 格式:数据类型1 形参1,数据类型2 形参2,……

【4】、函数体

  函数体是由一对大括号括起来的,大括号决定了函数体的范围。函数要实现的特定功能,都是在函数体部分通过代码完成的,最后通过 return 语句返回是实现的结果。

void getSum(int num1,int num2)
{
    int result = num1 + num2;
    return result;
}

方法不调用就不执行;

方法和方法之间是平级关系,不能相互嵌套定义;

方法的编写顺序和执行顺序无关;

三、函数声明

  如果使用用户自己定义的函数,而该函数与调用它的函数(即主调函数)不在同一文件中,或者函数定义的位置在主调函数之后,则必须在调用此函数之前对被调用的函数作声明。

  所谓函数声明,就是在函数尚在未定义的情况下,事先将该函数的有关信息通知编译系统,相当于告诉编译器,函数在后面定义,以便使编译能正常进行。函数的声明是要编辑器知道函数的名称、参数、返回值类型等信息。

  函数的声明格式函数返回值类型函数命名函数列表分号 四个部分组成的,函数声明格式如下:

返回值类型 函数名(参数列表);
int getSum(int num1,int num2);
int getSum(int,int);

  函数定义 和 声明的区别:

  函数定义 是指对函数功能的确立,包括指定函数名、函数类型、形参及其类型、函数体等,它是一个完整的、独立的函数单位。

  函数声明 的作用则是把函数的名字、函数类型以及形参的个数、类型和顺序(注意,不包括函数体)通知编译系统,以便在对包含函数调用的语句进行编译时,据此对其进行对照检查(例如函数名是否正确,实参与形参的类型和个数是否一致)。

四、函数的调用

  定义函数后,我们需要调用此函数才能执行到这个函数里的代码段。这和 main() 函数不一样,main() 为编译器设定好自动调用的主函数,无需人为调用,我们都是在 main() 函数里调用别的函数,一个 C++ 程序里有且只有一个 main() 函数。

  函数调用的格式如下:

数据类型 变量名 = 函数名(参数列表);

  在函数调用过程中传递的参数称为实参(实际参数),实参由具体的值。在函数定义过程中,参数称为形参(形式参数)。在函数调用过程中,将实参传递给形参。在函数调用结束,被调函数会在内存中自动销毁。

  • 如果是调用无参函数,则不能加上“实参”,但括号不能省略。
  • 实参与形参的个数应相等,类型应匹配(相同或赋值兼容)。实参与形参按顺序对应,一对一地传递数据。
  • 实参可以是常量、变量或表达式,无论实参是何种类型的量,在进行函数调用时,它们都必须具有确定的值,以便把这些值传送给形参。
  • 如果函数定义有返回值,这个返回值我们根据用户需要可用可不用,但是,假如我们需要使用这个函数返回值,我们需要定义一个匹配类型的变量来接收。
int result = getSum(10, 20);

方法在调用的时候,参数的数量与类型必须与方法定义中小括号里面的变量一一对应,否则程序将报错。

在 C++ 中,函数的定义都是互相平行、独立的,也就是说,一个函数体内不能包含定义的另一个函数,但是可以嵌套调用函数,也就是说,在一个函数体内可以调用另外一个函数。

当调用(执行)一个函数时,就会开辟一个独立的空间(栈),每个栈空间都是相互独立的。如果函数有返回值,则将返回值赋给接收的变量。当函数执行完毕后或者执行到 return 语句,该函数对应的栈空间就会销毁,程序会返回到调用函数的位置,继续执行。

调用函数时,开辟的函数栈,都会完整的执行函数代码;

五、函数的返回值

  如果函数的返回值类型与函数声明的返回值类型不一致时,实际得到的返回值相当于把函数中指定的返回值赋给与函数类型相同的变量所得到的值。

#include <iostream>

using namespace std;

int division(double num1, double num2);

int main(void)
{
    double num1 = 5, num2 = 2;
    double result = 0;
  
    result = division(num1, num2);
  
    cout << result << endl;

    return 0;
}

int division(double num1, double num2)
{
    double result = num1 / num2;
    return result;
}

六、函数的参数

  在调用函数时,大多数情况下,主调函数 和 被调函数 之间由数据传递关系。函数参数的作用时传递数据给函数使用,函数利用接收的数据进行具体的操作处理。

6.1、函数的形参和实参

  在定义函数时,函数名后面括号中的变量名称为 “形式参数”,在函数调用之间,传递给函数的值将被复制到这些形式参数中。形参出现在函数定义中,在整个函数体内都可以使用,离开该函数则不能使用。

  在调用一个函数时,也就是真正使用一个函数时,函数名后面括号中的参数为 “实际参数”。实参出现在主调函数中,进入被调函数后,实参也不能使用。

  实参单元与形参单元是不同的单元。调用结束后,形参单元被释放,函数调用结束返回主调函数后则不能再使用该形参变量。实参单元仍保留并维持原值。因此,在执行一个被调用函数时,形参的值如果发生改变,并不会改变主调函数中实参的值。

#include <iostream>

using namespace std;

int expansion10(int num);

int main(void)
{
    int num = 10;
    int result = 0;

    result = expansion10(num);

    cout << "num: " << num << endl;
    cout << "result: " << result << endl;

    return 0;
}

int expansion10(int num)
{
    num *= 10;
    return num;
}

函数定义时,列表的参数就是形式参数;而函数调用时,传递进来的参数就是实际参数

6.2、值传递与地址传递

  C++ 传递参数(或者赋值)可以是 值传递(pass by value),也可以 传递指针(a pointer passed by value),传递指针也叫 地址传递。默认 传递值 的类型有 基本数据类型(整型类型、浮点类型、字符类型)、结构体共用体。默认 传递地址 的类型有 指针数组。值传递是指将变量指向的存储内容,在传递/赋值时,拷贝一份给接收变量。地址传递分为两种情况:如果是指针,就将指针变量所存储的地址值传递给接收变量,如果是数组,就将数组的首地址传递给接收变量。

  传值调用的函数的形参和实参分别占有不同内存块,对形参的修改不会影响实参。传址调用是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式。这种传参方式可以让函数和函数外边的变量建立起真正的联系,也就是函数内部可以直接操作函数外部的变量。

  其实,不管是 值传递 还是 引用传递,传递给函数的都是变量的副本,不同的是,值传递 的是 值的拷贝,引用传递 是 地址的拷贝。一般来说,地址拷贝效率高,因为数据量小,而值拷贝决定拷贝的数据的大小,数据越大,效率越低。

  我们可以数组名作为函数参数。当数组作为函数的实际参数时,只传递数组的地址,而不是将这个数组赋值到函数中。当用数组名作为函数为实际参数调用函数时,指向该数组的第一个元素的指针就被传递到函数中。

#include <iostream>

using namespace std;

void arrayExpansion10(int array[] ,int length);

int main(void)
{
    int array[] = {1,2,3,4,5,6,7,8,9};
    int length = sizeof(array)/sizeof(int);

    arrayExpansion10(array, length);
    for (int i = 0; i < length; i++)
    {
        cout << array[i] << "\t";
    }
    cout << endl;

    return 0;
}

void arrayExpansion10(int array[] ,int length)
{
    int i = 0;
    for(i = 0; i < length; i++)
    {
        array[i] *= 10;
    }
}

C++ 中没有任何下标的数组名就是一个指向该数组第一个元素的指针;

如果希望函数内的变量能修改函数外的变量,可以传入变量的地址(&),函数以指针的方式操作变量(*指针);

6.3、引用传递

#include <iostream>

using namespace std;

int expansion10(int& num);

int main(void)
{
    int n = 10;
    int result = 0;

    result = expansion10(n);

    cout << "num: " << n << endl;
    cout << "result: " << result << endl;

    return 0;
}

int expansion10(int& num)
{
    num *= 10;
    return num;
}

  由于使用引用作为形参,函数调用时就可以直接传入实参 n 的值,而不用传入地址了。形参 num 只是实参 n 的别名,修改 num 就修改了 n。

6.4、默认参数

  默认参数指的是当函数调用中省略实参时自动使用的一个值。如果要使用默认参数,我们需要在函数原型指定默认值。只有原型指定了默认值。函数定义与没有默认参数时完全相同。对于带参数列表额函数,必须从右到左添加默认值。也就是说,要为某个参数设置默认值,则必须为它右边的所有参数提供默认值。

#include <iostream>

using namespace std;

int expansion(int& num, int spread = 1);

int main(void)
{
    int n = 10;
    int result = 0;

    result = expansion(n);

    cout << "num: " << n << endl;
    cout << "result: " << result << endl;

    return 0;
}

int expansion(int& num, int spread)
{
    num *= spread;
    return num;
}

七、局部变量与全局变量

  作用域的作用就是决定程序中的哪些语句是可用的,换句话说,就是在程序中的可见性。作用域 包括 局部作用域全局作用域。局部变量具有局部作用域,而全局变量具有全局作用域。

7.1、局部变量

  在一个函数内部定义的变量是 局部变量。这些变量声明在函数内部,无法被其它函数所使用。函数的形式参数也属于局部变量,作用范围只限于函数内部的所有语句块。

  在 C++ 中位于不同作用域的变量可以使用相同的标识符,也就是可以为变量起相同的名称。如果内层作用域中定义的变量和已经声明的某个外层作用域中的变量具有相同的名字,在内层中使用这个变量名,那么内层作用域中的变量将屏蔽外层作用域中的那个变量,直到结束内层作用域为止,即 局部优先原则

#include <iostream>

using namespace std;

int getSum(int num1,int num2);

int main(void)
{
    int num1 = 10, num2 = 20;
    int result = 0;

    result = getSum(num1,num2);
    cout << result << endl;

    return 0;
}

int getSum(int num1,int num2)
{
    int result = num1+num2;
    return result;
}

在语句块内声明的变量仅在该语句块内部起作用,当然也包括嵌套在其中的子语句块。

7.2、全局变量

  如果一个变量在所有函数的外部声明,这个变量就是 全局变量。全局变量可以在程序中的任何位置进行访问的变量。定义全局变量的作用是增加函数间联系的渠道。由于同一个文件中的所有函数都能引用全局变量的值,因此如果在一个函数中改变了全局变量的值,就能影响到其它函数,相当于各个函数间有直接传递通道。

  如果不对全局变量进行初始化,那么它会自动初始化为默认值:

数据类型 默认值
整型 0
浮点型 0.0
字符型 '\0'
指针 NULL
#include <iostream>

using namespace std;

double getCircleArea(int radius);
double getCirclePerimeter(int radius);

double pi = 3.14;

int main(void)
{
    double radius = 0;
    double perimeter = 0;
    double area = 0;

    cout << "请输入圆的半径:";
    cin >> radius;

    perimeter = getCirclePerimeter(radius);
    cout << "圆的周长为:" << perimeter << endl;

    area = getCircleArea(radius);
    cout << "圆的面积为:" << area << endl;

    return 0;
}

double getCircleArea(int radius)
{
    return pi * radius * radius;
}

double getCirclePerimeter(int radius)
{
    return 2 * pi * radius;
}

全局变量不属于某个函数,而属于整个源文件。但是如果外部函数要进行使用,则要用 extern 关键字进行修饰。

八、静态变量

  局部变量 被 static 关键字修饰后,称为 静态局部变量。静态局部变量在声明时未赋初值,编译器也会把它初始化为默认值。静态局部变量存储与进程的静态存储区,它只会被初始化一次,即使函数返回,它的值也会保持不变。

#include <iostream>

using namespace std;

int getCount(void);

int main(void)
{
    int count = 0;

    count = getCount();
    count = getCount();
    count = getCount();
    cout << "getCount()函数执行了" << count << "次" << endl;

    return 0;
}

int getCount(void)
{
    static int num;
    return ++num;
}

static 修饰局部变量改变了局部变量的生命周期,本质上是改变了变量的存储类型。

  static 修饰全局变量,使得这个全局变量只能在自己所在的源文件(.c)内部可以使用,其它源文件不能使用;全局变量在其它源文件内部可以被使用,是因为全局变量具有外部链接属性,但是被 static 修饰后,就变成了内部链接属性。这时,其它源文件不能连接到这个静态的全局变量了。

#include <iostream>

using namespace std;

static double pi = 3.14;

int main(void)
{
    cout << pi << endl;

    return 0;
}

九、内部函数和外部函数

  函数是 C语言 程序中的最小单位,往往把一个函数或多个函数保存为一个文件,这个文件称为源文件。定义一个函数,该函数就会被另外的函数所调用。但当一个源程序由多个源文件组成时,可以指定函数不能被其它文件调用。这样,C语言 又把函数分为两类:一个是 内部函数,另一个是 外部函数

9.1、内部函数

  定义一个函数,如果希望这个函数只被所在的源文件使用,那么就称这样的函数为 内部函数内部函数 又称为 静态函数。使用内部函数,可以是函数只局限在函数所在的源文件中,如果在不同的源文件中又同名的内部函数,则这些同名函数是互不干扰的。如果要使用内部函数,需要在函数的返回值前面加上 static 关键字进行修饰。

static 返回值类型 函数名(参数列表)
{
    方法体;
    返回值;
}
#include <iostream>

using namespace std;

static void printSomething(char something[]);

int main(void)
{
    printSomething("hello world!\n");

    return 0;
}

static void printSomething(char something[])
{
    cout << something << endl;
}

static 修饰函数,使得函数只能在自己所在的源文件内部使用,不能再其它源文件内部使用。本质上,static 是将函数的外部链接属性变成了内部链接属性;

不同的文件可以使用相同的静态函数,互不影响;

9.2、外部函数

  外部函数是可以被其它源文件调用的函数。定义外部函数的格式与普通的函数一样,但是需要在函数声明的时候加上 extern 关键字进行修饰。

extern 返回值类型 方法名(形参列表);
#include <iostream>

using namespace std;

extern void printSomething(char something[]);

int main()
{
    printSomething("hello world!\n");

    return 0;
}

extern void printSomething(char something[])
{
    cout << pi << endl;
}

在 C++ 定义函数时,如果不指明函数是内部函数还是外部函数,那么默认将函数指定为外部函数,也就是说,定义外部函数时可以省略关键字 extern;

十、多文件编程

  在实际开发中,我们往往需要在不同的文件中,去调用其它文件的定义的函数,因此我们将 函数声明 和 宏定义 可以放在头文件中。头文件是扩展名为 .h 的文件,它可以被多个源文件中引用共享。在程序中要使用头文件,需要使用 C预处理指令 #include 来引用它。#include 的处理过程很简单,就是将头文件的内容插入到该命令所在的位置,从而把头文件和当前源文件连接成一个源文件,这与复制粘贴的效果类似。

  在 C语言 中,include 指定的书写格式有两种,分别是 #include <头文件名.h>#include "头文件名.h",它们的区别如下:

  #include <头文件名.h> 引用的是编译器的类库路径的头文件,用于引用 系统头文件

  #include "头文件名.h" 引用的是你程序目录的相对路径中的头文件,如果在程序目录中没有找到引用的头文件则到编译器的类库路径的目录下查找该头文件,用于引用 用户自定义的头文件

  所以,引用 系统头文件,两种形式都可以,#include <头文件名.h> 的效率高,引用 用户自定义的头文件,只能使用 #include "头文件名.h"

  把函数声明放在头文件 xxx.h 中,在主函数中包含相应头文件。在头文件对应的 xxx.c 中实现 xxx.h 声明的函数。当一个项目比较大时,往往都是分文件,这时候有可能不小心把同一个头文件 include 多次,或者头文件嵌套包含。为了避免同一个文件被 include 多次,C/C++中有两种方式,一种是 #ifndef 方式,一种是 #pragma once 方式。

  方式一:使用 #ifndef 方式

#ifndef __SOMEFILE_H__
#define __SOMEFILE_H__

// 声明语句

#endif

  方式二:使用 #pragma once 的方式

#pragma once

// 声明语句

  template.c 内容如下:

#include <iostream>
#include "test.h"

using namespace std;

int main(void)
{
    printSomething("hello world!\n");

    return 0;
}

  test.c 内容如下:

#include <iostream>

void printSomething(char something[])
{
    cout << pi << endl;
}

  test.h 内容如下:

#pragma once

void printSomething(char something[]);

建议把所有的常量、宏、系统全局变量 和 函数原型 写在头文件中,在需要的时候随时引用这些头文件;

引用头文件相当于复制头文件的内容;

源文件的名字可以不和头文件一样,但是为了好管理,一般头文件名和源文件名一样;

一个 #include 指令只能包含一个头文件,多个头文件需要多个 #include 命令引入;

如果一个文件多次引用头文件,多次引入的效果和一次引入的效果相同,引文头文件在代码层面上有防止重复引入的机制;

一个被包含的文件(.c)中又可以包含另一个文件头文件(.h);

不管是标准头文件,还是自定义头文件,最好只包含变量和函数的声明,不能包含定义,否则在多次引入时,可能会引起重复定义错误;

十一、函数递归

  C++ 允许函数调用自己,这种调用过程称为递归(recursion)。也就是说,每个函数都可以直接或间接调用自己。所谓的间接调用,是指在递归函数调用的下层函数中再调用自己。

  递归之所以能实现,是因为函数的每次执行过程在栈中都有自己的形式参数和局部变量的副本,这些副本和该函数的其它执行过程不发生关系。假定某个调用函数调用了一个被调用函数,再假定被调用函数又反过来调用了调用函数,那么第二个调用就称为调用函数的递归,因为它发生再调用函数的当前执行过程运行完毕之前。而且,因为原先的调用函数、现在的被调函数在栈中较低的位置有它独立一组参数和自变量,原先的参数和变量将不受任何影响,所以递归调用能正常工作。

  递归的必要条件:

  1. 存在限制条件,当满足这个限制条件的时候,递归便不再继续;
  2. 每次递归调用之后越来越接近这个限制条件;

  递归的基本原理:

  1. 每级函数调用都有自己的变量;
  2. 每次函数调用都会返回一次。当函数执行完毕后,控制权将被传回上一级递归。程序必须按顺序逐级返回递归;
  3. 递归函数中位于递归调用之前的语句,均按被调函数的顺序执行;
  4. 递归函数中位于递归调用之后的语句,均按被调回函数相反的顺序执行;
  5. 虽然每级递归都有自己的变量,但是并没有拷贝函数的代码。程序按顺序执行函数中的代码,而递归调用就是相当于又从头开始执行函数的代码。除了为每次递归调用创建变量外,递归调用非常类似于一个循环语句。
  6. 最后,递归函数必须包含让递归调用停止的语句。
#include <iostream>

using namespace std;

void binary(int n);

int main(void)
{
    int num = 32;
  
    binary(num);
  
    return 0;
}

void binary(int num)
{
    int n;

    n = num % 2;
    if( num >= 2)
        binary(num / 2);
    cout << n ? '1' : '0';
}

十二、内联函数

  编译过程的最终产品是可执行程序 —— 由一组机器语言指令组成。运行程序时,操作系统将这些指令载入到计算机内存中,因此每条指令都有特定的内存地址。计算机随后将逐步这些指令。有时将跳过一些指令,向前后向后跳到特定的内存地址。

  执行到函数调用指令时,程序将在函数调用后立即存储该指令的内存地址,并将函数参数复制到堆栈,跳到标记函数起点的内存单元,执行函数代码,然后跳回到地址被保存的指令处。来回跳跃位置意味着使用函数时,需要一定的开销。

  C++ 的内联函数提供了另一种选择。内联函数的编译代码与其它程序代码 “内联” 起来了。也就是说,编译器将使用相应的函数代码替换函数调用。对于内联代码,程序无需跳到另一个位置处开始执行代码,再跳回来。因此,内联函数的运行速度比常规函数稍快,但代价是需要占用更多内存。

  如果我们需要使用内联函数,需要:

  • 在函数声明前加上关键字 inline。
  • 在函数定义前加上关键字 inline。
#include <iostream>

using namespace std;

inline double square(double x);

int main(void)
{
    double num = 10;
    double result = square(num);
  
    cout << result << endl;
  
    return 0;
}

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

十三、函数重载

13.1、什么是函数重载

  在 C++ 中,同一个作用域下,同一个函数名是可以定义多次的,前提是形参列表不同。这种名字相同但形参列表不同的函数,叫作 “重载函数”。

  函数重载的关键是函数的参数列表 —— 也成为函数特征标。如果两个函数的参数数目和类型相同,同时参数的排列顺序也相同,则它们的特征标相同,二变量名是无关紧要的。C++ 允许定义名称相同的函数。条件是它们的特征标不同。如果参数数目或参数类型不同,则特征标也不同。

  使用被重载参数时,需要在函数调用中使用正确的参数类型。

#include <iostream>

using namespace std;

#define SIZE 10

void print(int array[], int length);
void print(int (&array)[SIZE]);

int main(void)
{
    int array[SIZE] = {1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10};

    print(array, SIZE);
    print(array);

    return 0;
}

void print(int array[], int length)
{
    for (int i = 0; i < length; i++)
    {
        cout << array[i] << " ";
    }
    cout << endl;
}

void print(int (&array)[SIZE])
{
    for (int i = 0; i < SIZE; i++)
    {
        cout << array[i] << " ";
    }
    cout << endl;
}

13.2、有const形参的重载

  当形参有 const 修饰时,要区分它对于实参的要求是什么,是否要进行值的拷贝。如果是传值参数,传入实参时会发生值的拷贝,那么实参是变量还是常量其实是没有区别的。

void fun(int x);
void fun(const int x);    // int常量做形参,跟不加const等价
void fun2(int* p)
void fun2(int* const p);  // 指针常量做形参,也跟不加const等价

  这种情况下,const 不会影响传入函数的实参类型,所以跟不加 const 的定义一样,这叫作 “顶层 const”。这时两个函数相同,如法进行函数重载。

  另一种情况则不同,那就是传引用参数。这时,如果有 const 修饰,就成了 “常量的引用”。对于一个常量,只能用常量引用绑定,而不能使用普通引用。类似地,对于一个常量的地址,只能由 “指向常量的指针” 来指向它,而不能用普通指针。

void fun3(int &x);
void fun3(const int &y);      // 形参类型式常量引用,这是一个新函数
void fun4(int* p);
void fun4(const int* p);      // 形参类型是指向常量的指针,这是一个新的函数

  这种情况下,const 限制了间接访问的数据对象是常量,这叫做 “底层 const”。当实参是常量时,不能对不带 const 的引用进行初始化,所以只能调用常量引用做形参的函数。而如果实参式变量,就会优先匹配不带 const 的普通引用。这就实现了函数重载。

13.3、函数匹配

  如果传入的实参跟形参类型不同,只要能通过隐式类型转换变成需要类型,函数也可以正常调用。那假如有几个不同的重载函数,它们的形参类型可以进行自动转换,这时传入实参应该调用哪个哪个函数呢?

  1. 创建候选函数列表。其中包含了与被调函数的名称相同的函数和模板函数。
  2. 使用候选列表创建可行函数列表。这些都是参数数目正确的函数,为此有一个隐式转换序列,其中包含实参类型与相应的形参类型完全匹配的情况。
  3. 确定是否有最佳的可行函数,如果有,则使用它,否则该函数调用出错。
#include <iostream>

using namespace std;

void f();
void f(int x, int y=1);
void f(double x, double y=1.5);

int main(void)
{
    f(3);

    return 0;
}

void f()
{
    cout << "f()" << endl;
}

void f(int x, int y)
{
    cout << "f(int x, int y=1)" << endl;
}

void f(double x, double y)
{
    cout << "f(double x, double y=1.5)" << endl;
}

  确认到底调用哪个函数的过程,叫作 “函数匹配”。函数匹配的第一步,就是确认 “候选函数”,也就是先找到对应的重载函数集。候选函数有两个要求:

  1. 与调用的函数同名。
  2. 函数的声明,在函数的调用点是可见的。

  接下来需要从候选函数中,选出跟传入的实参匹配的函数。这些函数叫作 “可行函数”。可行函数也有两个要求:

  1. 形参个数与调用传入的实参数量相等。
  2. 每个实参的类型与对应形参的类型相同,或者可以转换成形参的类型。

  最后就是在可行函数中,选择 “最佳匹配”。简单来说,实参类型与形参类型越接近,它们就匹配得越好。所以,能不进行转换就实际匹配的,要优于需要转换的。如果实参的数量不止一个,那么就需要逐个比较每个参数。同样,类型能够精确匹配的要优于需要转换的。这时寻找的最佳匹配的原则如下:

  1. 完全匹配,但常规函数由于模板函数。
  2. 提升转换(例如,char 和 short 自动转换为 int,float 自动转换为 double)。
  3. 标准转换(例如,int 转换为 char,long 转换为 double)。
  4. 用户定义的转换,如类声明中定义的转换。

  重载解析将寻找最匹配的函数。如果只存在一个这样的函数,则选择它。如果存在多个这样的函数,但其中只有一个是非模板函数,则选择该函数。如果存在多个适合的函数,且它们都为模板函数,但其中有一个函数比其它函数更具体,则选择该函数。如果有多个同样适合的非模板函数或模板函数,但没有一个函数比其它函数更具体,则函数的调用是不确定的,那么编译器就会报错,这被称为 “二义性调用”。如果不存在匹配的函数,则也是错误的。

十四、函数模板

  函数模板是通用的函数描述,也就是说,它们使用泛型来定义函数,其中泛型可用具体的类型(如 int 或 double)替换。通过将类型作为参数传递给模板,可使编译器生成该类型的函数。由于模板允许以泛型(而不是具体类型)的方式编写程序,因此也称为通用编程。

#include <iostream>

using namespace std;

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

int main(void)
{
    int a = 10, b = 20;
    double c = 10.5, d = 20.5;

    // 使用模板的方式1:自动类型推导
    Swap(a, b);
    cout << "a = " << a << ", b = " << b << endl;

    // 使用模板的方式2:显示指定类型
    Swap<double>(c, d);
    cout << "c = " << c << ", d = " << d << endl;

    return 0;
}

// 不要使用swap(),这是因为C++底层定义了swap()函数
template <typename T>
void Swap(T& a, T& b)
{
    T temp = a;
    a = b;
    b = temp;
}
template <typename T>

  这行指出,要建立一个模板,并将类型命名为 T。关键字 template 和 typename 是必须的,除了可以使用关键字 class 替代 typename。另外必须使用尖括号。类型名可以任意选择。

  在标准 C++98 添加关键字 typename 之前,C++ 使用关键字 class 创建模板。

template <class T>

  在代码中包含函数模板本身并不会生成函数定义,它只是用于生成函数定义的方案。编译器使用模板为特定类型生成函数定义时,得到的是模板实例。

  我们可以像重载常规函数定义那样重载模板定义。和常规重载一样,被重载的模板的函数的特征标必须不同。如果函数模板和普通函数都可以调用,优先调用普通函数。如果函数模板可以产生更好的匹配,优先调用函数模板。我们可以通过空模板参数列表强制调用函数模板。

#include <iostream>

using namespace std;

template <typename T>
void print(T a, T b);

template <typename T>
void print(T something);

void print(double a, double b);

int main(void)
{
    int a = 10, b = 20;
    double c = 10.5, d = 20.5;

    // 优先调用普通函数
    print(c, d);

    // 使用空模板参数列表,强制调用模板函数
    print<>(c, d);

    // 如果函数模板可以产生更好的匹配,优先调用函数模板
    print(a, b);

    // 函数模板也可以重载
    print("Sakura");

    return 0;
}

// 模板函数
template <typename T>
void print(T a, T b)
{
    cout << "template --> a: " << a << ", b: " << b << endl;
}

template <typename T>
void print(T something)
{
    cout << "template --> something: " << something << endl;
}

// 普通函数
void print(double a, double b)
{
    cout << "normal --> a: " << a << ", b: " << b << endl;
}

  模板并不是万能的,有些特定的数据类型,需要用具体化方式做特殊实现。

#include <iostream>

using namespace std;

struct Person
{
    string name;
    int age;
};

template <typename T>
bool Compare(const T &a, const T &b);
template<> bool Compare(const Person &p1, const Person &p2);

int main(void)
{
    int a = 10, b = 10;
    bool result = Compare(a, b);
    cout << (result ? "a == b" : "a != b") << endl;

    Person p1 = {"Sakura", 10};
    Person p2 = {"Sakura", 12};

    result = Compare(p1, p2);
    cout << (result ? "p1 == p2" : "p1 != p2") << endl;

    return 0;
}

// 不要使用compare(),这是因为C++底层定义了compare()函数
template <typename T>
bool Compare(const T &a, const T &b)
{
    return (a == b) ? true : false;
}

// 利用具体化的Person的版本实现代码,具体化的代码优先调用
template<> bool Compare(const Person &p1, const Person &p2)
{
    return (p1.name == p2.name && p1.age == p2.age) ? true : false;
}

函数模板使用时,如果利用自动类型推导,必须推导出一致的数据类型,才可以使用,它不会发生自动类型转换。但如果利用显示指定类型的方式,可以发生隐式类型转换。

模板使用时必须要确定数据类型,才可以使用。

posted @ 2023-04-08 19:24  星光樱梦  阅读(18)  评论(0编辑  收藏  举报