目录
一些基本的东西
- 函数原型(prototype)
函数的原型是函数与编译器之间的接口(interface),方便编译器编译,其作用是:
#帮助编译器正确处理函数返回值
#帮助编译器检查使用的参数个数是否正确
#帮助编译器检查使用的参数类型是否正确,不正确则转换成合适的类型
C++函数的原型声明代码如下:
函数的返回类型可以是没有返回值类型(void),也可以是基本类型、结构、指针(包括数组、结构指针、函数指针)。//返回类型 函数名(参数类型1 参数名1,参数类型2 参数名2,...); //声明原型时不要忘记分号; void showArray(int *arr);//无返回类型的函数 void show();//无返回类型、参数的函数 int sum(int a,int b);//返回类型为int的函数 const char * strcmp(char * str_1, const char * str_2);//返回类型、参数为指向常量的指针的函数 //假设Student是一个定义好的结构 Student * AddStudent(int ID,char * class);//返回值为一个结构指针的函数
函数的参数列表也可以是以上类型。函数的参数列表的参数类型、个数、排列顺序称为特征标。
- 函数定义(definition)
函数的定义由函数头和函数体组成,是实现函数功能的代码。其定义格式是:
在函数定义处,函数的参数列表中的变量需要变量名,称为形参(parameter,亦称为参量),而原型处的变量名是可以省略的。//函数头 //{ // 函数体 //} int sum(int a,int b)// { return a+b; }//函数定义的代码块最后不需要分号;
- 函数调用
调用函数很简单,只需要在某一代码文件中调用函数名(参数)即可。调用处使用的参数称为实参(argument,实际上是实际的变量或者字面量literal,统称为参数)。
在运行可执行文件时,操作系统遇到调用函数的指令时,会跳到函数定义的指令的地址,并在执行完函数体、获得返回值后,重新返回到调用处的地址。
参数传递类型与按引用传递
- 函数传递参数的类型
C++中,根据实参与形参的关系,函数传递参数的方式分为按值传递、按引用传递、按指针传递。
#按值传递
形参与实参的值相同,但形参与实参不是一个变量,二者的地址不同,形参实际上是实参的一个副本。
形参是函数私有的,实际上是一个自动变量,函数调用后,计算机会给形参分配内存,在执行完函数以后其内存会被释放。
#按引用传递
形参和实参可以视为同一个变量,其值和地址都相同。函数调用时使用的是实参本身,而不是再分配一个内存使用其副本。
引用实际上像一个const指针,其在声明时就进行初始化,且只能指向实参,不能改变。
#按指针传递
形参与实参均为一个指针,但存储形参与实参的地址(即指向形参、实参的指针)是不同的,指针的值(即某一个变量的地址)是相同的。#include<iostream> using namespace std; void showPtrAdd(int *b); int main() { int a=2; cout<<"a at:"<<&a<<endl; showPtrAdd(&a); cin.get(); return 0; } void showPtrAdd(int *b) { cout<<"value:"<<b<<endl; cout<<"parameter at"<<&b<<endl; } ---输出--- a at:00EFFB60 value:00EFFB60 parameter at:00EFFA8C
- 按引用传递与引用变量
引用最常用的地方是作为函数参数以提高函数运行效率(尤其是需要使用一些大型结构、类时)。它也可以作为普通变量、返回值。
引用变量的声明代码如下,其需要使用&(这里不是地址运算符,但其实有点像地址运算符):
//类型名 &引用变量名=被引用的变量名; int a=6; int &b=a;//b实际上只是a的一个别名 b++;//等同于a++
需要强调的是,引用变量在声明时就需要被初始化,不可以将声明、初始化语句分开,并且经初始化以后,引用变量的指向不可改变。
在函数参数中使用引用的代码如下://返回类型 函数名(参数类型1 &参数名1 ,...); int sum(int &a,int &b);//使用引用的函数的原型
在函数中返回一个引用的代码如下:
//返回类型名 & 函数名(参数列表); //Student是一个定义好的结构, arStu是Student类数组 Student & FindBestStu(Student *StuArr);//返回一群学生中成绩最好的学生 Student & BestStu=FindBestStu(arStu);
注意接受返回引用的函数返回值的,也是一个引用变量。
返回一个引用的好处是能够将函数的返回值直接复制给另一个变量。若不返回引用,则需要先将返回值储存在一个临时区域,再将这个区域的值复制给另一个变量。 - 按引用传递的两种生成临时变量的情况
当函数采用按引用传递时,若:
#实参的类型正确,但不是左值(即可被引用的对象,包括变量、数组元素、结构、引用、解除引用的指针。不包括任何字面常量和多项式表达式)时;
#实参类型不正确,但可以转换为正确的类型时;
#函数采用const引用作为传参方式时(为了防止数据被更改);
编译器将生成一个临时变量,使得函数定义中的引用变量成为该临时变量的引用。
- 引用与指针的比较
#引用变量与被引用变量的值、地址都相同;指针与被指向的值不一定是相同类型的量(指针本身是指针,而被指向的值可能是指针,也可以是其他类型),指针的值是被指向值的地址,*指针名即为被指向的值。
#引用变量其在声明时就进行初始化,且只能指向实参,不能改变;指针声明与初始化语句可以分离
#按引用传递时,形参与实参是同一个量;按照指针传递时,形参与实参均为一个指针,但存储形参与实参的地址(即指向形参、实参的指针)是不同的,指针的值(即某一个变量的地址)是相同的,不是一个量
- 引用与const
同其他变量一样,引用变量也可以使用限定符const以防止引用被修改,这在不修改参数的函数中十分常用:
此时也和其他const变量一样,使用非const实参调用show()时,也可以通过编译。但不能用const量调用使用非const引用的函数。//Student是定义好的结构 void show(const Student &stu) { ... }
此外,也可以然后函数返回const引用:
和指针一样,引用也应尽量写成const引用(要留心const与非const类型是否匹配!)。//返回类型名 & 函数名(参数列表); //Student是一个定义好的结构, arStu是Student类数组 const Student & FindBestStu(Student *StuArr);//返回一群学生中成绩最好的学生 const Student & BestStu=FindBestStu(arStu);
在返回引用时,千万不要返回自动变量的引用,因为在执行完相应代码块以后,自动变量的内存会被系统自动释放,此时引用变量会指向不存在的对象://Student是一个定义好的类, arStu是Student类数组 const Student & FindBestStu(Student *StuArr) { Student resultStu=new Student(); Student & result=resultStu; ... return result;//不要这么干! }
内联函数
内联函数常用于提升函数调用效率,但代价是占用了更多内存,因为使用了许多代码的副本。
- 内联
内联指的是将某一个代码段放在另一个代码段中执行。内联和内联函数并不等价。 - 一般函数、内联函数的执行过程
#程序中一般函数的执行过程
计算机执行到函数调用指令时:
》》程序将在调用后立即储存该指令的内存地址,将函数参数复制到堆栈,
》》后跳到标记函数起点的内存,执行函数体,将返回值放入到寄存器中
》》跳回函数调用处。
在执行过程中,调用函数、返回函数值的寻找内存(即跳到某一内存)需要开销。
#内联函数的执行过程
编译器会将相应的函数代码替换函数调用。程序运行时,不需要跳到另一个位置执行代码然后再跳回调用处。
- 内联函数的声明、定义
内联函数的声明、定义需要在返回类型前加上关键字inline,如下所示:
内联函数一般直接省略原型提供定义,尤其是在类声明中,直接在类声明的文件中提供函数定义,函数将自动成为内联函数。inline int sum(int a,int b);//内联函数的声明 inline int sum(int a,int b)//内联函数的定义 { ...; } /*一般可以将原型与inline省略,直接将定义放在提供原型的地方(例如类声明中) 这样函数会自动成为内联函数: int sum(int a,int b){...;} */
- 内联函数的优缺点
内联函数可以减少调用过程的开销,因此能够提高函数执行的效率,但是代价是占用了更多的内存。
由于内联也会导致代码量的增加,因此也需要选择性的使用内联函数。当函数调用所占的时间为主要短板时,可以考虑使用内联函数。
函数的默认参数
当函数调用缺少参数时,带默认参数的函数将使用定义时提供的默认参数。
- 默认参数的函数的声明、定义
带默认参数的函数的声明、定义很简单,只需要在声明原型时于参数列表中给相应的值赋默认值即可,但应使得不使用默认参数的参量只能位于使用默认参数的参量的右边。
//返回类型 函数名(非默认参数参量名1 参量名1,...,默认参数参量名1 参量名1=默认参数值1,...); int sum(int a=0,int b=0;)//两个默认参数的原型 int sum(int a,int b)//定义时的函数头不需要带默认参数 { ...; }
- 默认参数函数在类中的应用
默认参数函数还可以作为类的默认构造函数,只需要将相应默认值赋予相应量即可。
函数重载
函数重载使得函数可以使用不同的参数类型、个数来实现同一个目的,其前提条件是各同名函数的特征标是不同的。
- 特征标(function signature)
函数的特征标指的是函数的参数列表,其包括参量的类型、个数、排列顺序,但不考虑参量名。只要参量的类型、个数、排列顺序相同,即认为函数的特征标相同。
需要注意的一点是,编译器会把类型引用与类型本身视为同一个特征标;编译器会把const与非const变量视作不同特征标。
- 函数重载的声明、定义与调用
函数重载的声明、定义、调用和普通函数没什么不同。需要注意的就是让函数名相同而特征标不同。
若重载函数时的返回类型不同,那么其特征标也必须不同。int sum(int a,int b);//将两个整型求和 int * sum(int *a,int *b);//将两个整型数组求和
- 函数重载时的函数匹配
调用重载函数时,编译器将会根据使用的参数类型,自动匹配特征标最适合的函数进行编译。
函数模板
函数模板可以针对某一类相同的变量(如各种整型、各种浮点数)编写相同的函数(前提是它们在函数中的行为存在着相同的部分),从而减少开发的代码量,尤其适用于将算法用于不同数据类型的情况(但不同数据类型可能意味着需要使用不同的算法)。
函数模板本身并不是函数定义,根据传入的类型生成的函数实例才是函数定义
- 函数模板的声明、定义、调用
一般而言,将函数模板放在头文件中,于实现代码中包含头文件即可使用该模板。
#声明、定义
函数模板的声明需要使用关键字template,其格式如下:
其实很好记:只需要将具体类型换成泛型(即typename T),并在原型前加上template<typename 泛型名>,以泛型替代具体类型即可。//template<typename 类型名的别名> 返回类型 函数名(以各别名代替原类型名的参数列表); //参数列表中也可以含有其他除非别名外的类型 template<typename T> T sum(T a,T b);//模板原型 template<typename T> T sum(T a,T b)//模板的定义 { return a+b; }
#调用调用函数模板和调用普通函数一样,只需要将相应类型的参数传入函数,后编译器会自动生成相应类型的函数代码:
//使用之前提供的sum()函数模板 int res_int=sum(1,2); double res_doub=sum(2.5,3.5);
#编译过程
使用函数模板的代码,在编译过程中会根据调用匹配模板函数,后根据调用时的参数生成相应类型的具体函数(而不是模板)。因此模板不能提高程序运行速度,只是提高了开发的效率。 - 函数模板的具体化与实例化
#实例化
实例化指的是编译器根据传入的实参类型,根据模板自动生成相应类型的函数。正常调用过程均属于实例化,如上文代码段中的sum(1,2)。
实例化分为隐式实例化(implicit specification)、显式实例化(explicit specification)两种,其调用代码如下:
上述显式实例化代码提示编译器,直接根据函数模板生成一个float类型的函数实例。而隐式实例化则需要编译器根据输入的参数类型判断该生成什么类型的函数。理论上来说,显式实例化的编译效率会更高一些(因为编译器明确知道了类型?)//显示实例化的创建代码: //template 返回类型 模板函数名<类型名>(参数列表) template int sum<int>(int a,int b); //显式实例化的调用代码 //模板函数名<类型名>(参数列表) double b=sum(1.0,3.5);//隐式实例化 float c=sum<float>(1.0,2);//显式实例化
#具体化(explicit instantiation)
具体化指的是针对模板中的某一特定的类型编写相应的专用模板,当调用模板时,编译器会自动匹配这一类型的模板函数。
其代码如下:template<typename T> T sum(T a,T b);//模板原型 template<typename T> T sum(T a,T b)//模板的定义 { return a+b; } //模板具体化的格式:(以下两种均可以) //template <> 返回类型 函数名<具体类型名>(使用具体类型名的参数列表); //template <> 返回类型 函数名(使用具体类型名的参数列表); template <> job sum(job)(job a,job b);//将两个job结构相加的具体函数定义
- 函数模板的重载
函数模板和函数一样,也允许重载,条件也相同:特征标必须不同。template<typename T> T sum(T a,T b);//模板原型 template<typename T> T sum(T a,T b)//模板的定义 { return a+b; } template<typename T> sum(T[] a,T[] b,int ArrSize);//模板原型 template<typename T> sum(T[] a,T[] b,int ArrSize)//模板的定义 { }
函数调用时的匹配问题(重载解析)
当存在重载函数、函数模板、模板重载时,C++编译器需要根据实际调用时的实参类型选择合适的函数/模板,执行合适的函数定义,该过程称为重载解析(overloading resolution)。这个过程需要考虑比较多的东西,此处只记录一些原则。
其大致的步骤是:
Step 1:(根据调用的函数名、实际调用所用的参数类型与个数)筛选候选函数列表
Step 2:使用候选函数列表创建可行函数列表,此过程还需要执行适当的类型转换。
Step 3:确定是否有最佳函数匹配,如果有则使用,无则报错。