函数
基础
1)一个典型的函数包括四个部分:返回类型、函数名、函数参数列表以及函数体。
其中返回类型、函数名、参数列表称为函数的三要素,描述了函数的借口以及调用函数时所需的 全部信息。
T function_name ([parameter1,parameter2,...]) //中括号表示形参可以没有
{
//函数体
}
2)通过
调用运算符来执行函数,调用运算符是一对圆括号,它作用于一个表达式,该表达式可以是函数或者指向函数的指针。
//定义add函数
int add(int x, int y)
{
return x + y;
}
//调用add函数
int a;
a = add(x1 , y1);//()为调用运算符,此处的x1,y1为实参,用来初始化add函数中的形参x,y
3)函数执行的第一步是定义并初始化其形参,而当执行函数时遇到return语句是结束函数的执行,返回主调函数。return语句的作用是:返回return语句中的值;将控制权从被调函数转移到主调函数。函数的返回值用来初始化调用表达式的结果,之后继续主调函数的其余部分。
4)实参是形参的初始值,实参的类型必须和对应的形参类型相匹配或者能够相互转换的类型。函数的形参列表可以为空但不能省略,可以使用在形参列表用void表示没有形参。在声明或者定义函数的时候,我们可以为形参命名以方便在函数体中使用,对于在函数中不会使用的形参可以不对其命名,未命名的形参在函数体中无法使用。但是不论函数形参是否被命名,调用函数时都需要传入对应数量的实参。
//定义add函数
int add(int x, int y, int )
{
return x + y;
}
//调用add函数
int a;
a = add(x1 , y1,1);//()为调用运算符,此处的x1,y1 ,,用来初始化add函数中的形参x,y;1用来初始化未命名的形参
5)形参和函数体内部定义的变量统称为
局部变量。在所有函数体之外定义的对象存在程序执行的整个过程中,而局部变量的生命周期依赖于定义的方式。对于普通局部变量对应的对象(
自动对象)来说,在变量定义的时候被创建,在达到定义所在块的末尾时被销毁;而
局部静态对象
(static类型)在对象定义处被创建,在程序终止时才销毁。
6)函数的名字在使用前必须声明。在函数的声明中,函数的形参通常会省略名字。函数声明也称作
函数原型。但一般建议形参名,有助于理解函数的作用。函数的声明和定义最好实现分离,将声明写在头文件中,而定义在代码的源文件中。
int add(int , int);//add 函数原型
实参
默认实参
某些函数中可能存在一些值基本不变的参数,我们将其称之为函数的默认实参。调用含有默认实参的函数是,可以包含该实参也可以省略该实参。1)一旦某个形参赋予了默认值,之后的形参必须都赋予默认值。
2)在给定的作用域中,一个形参只能被赋予一次默认实参,即已经被赋予过默认值对 形参不能再次修改默认值。
3)局部变量不能作为默认实参,除此之外只要表达式类型能转换为形参所需类型均可作为默认实参。
形参
每次调用函数都会重新创建其形参并且传递实参来初始化形参,
形参的初始化机制和变量的初始化一样。常见的参数传递方式有:传值调用和传引用调用。传值调用是将实参的值拷贝给形参,形参和实参是两个不同且相互独立的对象;传引用调用是将引用形参绑定到实参对象上,也就是引用形参名是实参的一个别名。
指针形参
指针形参是将指针的值进行拷贝传递。拷贝之后两个指针是不同的指针对象,但两个指针指向的是同一个对象。因此可以通过指针形参改变函数外部对象的值,这一点和引用形参类似。在函数体内修改指针的值(并不是指针指向的对象的值)并不影响实参指针的值也不会影响指针指向的对象的值。
//return_type function_name(type * parameter)
int add(int *p);
引用形参
引用形参绑定初始化它的对象。通过使用引用形参,允许函数改变一个或多个实参的值。当拷贝大的类类型对象或者容器对象,操作过程比较低效,或者在某些对象不支持拷贝的情况下,可以使用引用形参来访问该类型的对象。一个普通的引用必须用同类型的对象来初始化。
//return_type function_name(type & parameter)
int add(int &p);
const形参
当形参含有顶层const时,在实参对形参进行初始化时会对顶层const进行忽略,即传递给顶层const形参的实参可以是常量或者非常量。由于存在对顶层const的忽略,容易造成 函数的重复定义,即在定义重载函数时顶层const并不满足重载函数判定条件。我们可以使用一个非常量来初始化底层const对象,但反过来不行。即:可以用非常量来初始化const形参。对不会改变的形参应当尽量定义成常量引用。
//return_type function_name(const type & parament)
int add(const int &p);
数组形参
数组有两个性质:不允许拷贝数组和在使用数组时通常会将其转换成指针。因此数组不存在值传递,当我们为函数传递一个数组时,其实传递的是一个指向数组首元素的指针。传递数组时最好能够提供额外的信息以保证数组的访问过程中不会越界。
//return_type function_name(type & parameter)
int add(int * ) ;//3种定义方式一样
int add(int [] ) ;
int add(int [10]);//其中的10只是一种提示作用,具体有多少个元素不一定;但是在定义中则有意义!!!
int add(const int *);//定义成指向常量的指针
int add(int (&arr)[]);//数组的引用
int add(int matrix[][10]);//指向含有10个整数元素的指针,等价于下面的表达,注意与int matrix[10][10]的不同
int add(int (*matrix)[10]);//指向含有10个整数元素的指针
可变形参
有时候无法提前知道该向函数传递多少个实参,为了处理此种情况,有三种方法:如果所有实参类型相同,可以传递一个名为initializer_list 的标准库类型;如果所有实参类型不 完全相同,可以编写可变参数模板函数;还可以使用特殊的形参类型(省略符)。//省略符形参
void foo(parm_list,...);
void foo(...)
函数指针形参
类似于数组,虽然不能定义函数类型的形参,但是可以是指向函数的指针。返回类型及return语句
return语句终止当前函数并将控制权转给调用该函数的程序。
return;
return expression;
无返回值函数
无返回值的return语句只能用在void类型的函数中。而且一般void函数会隐式地包含return语句,因此在void函数中可以不写return语句。无返值函数的return语句也可以采用第二种return语句,不过其中的expression必须是一个返回void的函数。
有返回值函数
1) return语句返回值的类型必须与函数的返回类型一致,或者能够进行隐式转换。
2) 返回一个值的方式和初始化一个变量或者形参的方式一样:返回的值用于初始化调用点的一个临时量,该临时量就是函数调用的结果。
3) 不要返回局部对象的引用或者指针。函数执行完后,局部对象已经被销毁。
4) 引用返回左值。一个函数的返回类型是引用,则其返回结果得到的是左值,其他类型是右值。这意味着,
一个非常量引用函数可以放在赋值运算符的左边。
int & add(int &x)
{
x += 2;
return x;
}
add(2) *= 2;
5) 函数可以返回花括号包围的值的列表。如果返回的是内置类型,那么花括号内的列表最多包含一个值。此种返回方式就是用类别内的值来初始化函数返回时创建的临时量。如果列表为空,则根据类型的不同对创建的临时量执行相应的值初始化。
vector<int> add(int x)
{
return {x+1 , x+2 ,x+3};
}
//类型别名
typedef int arrT[10];
using arrT = int[10];
arrT* func(int i);
// 返回数组指针的函数形式: T (*function(parameter_list))[dimension]
//如果大家将func(int a)换成一个变量名a,那么可以写成int (*a)[10],
//结合变量的声明含义此时可能很好理解这个函数声明了。
int (*func(int i))[10];
// func(int i): 函数名为func,接受int型实参
//*func(int i):可以被解引用,说明返回的是指针
// (*func(int i))[10]:指针指向的大小为10的数组
//int (*func(int i))[10]:数组元素的类型是int型
//尾置返回类型:函数名前加auto,形参列表后添加 -> ,再紧跟类型名
auto func(int i) -> int (*) [10]; //此处的括号不能少
7) 返回函数指针:利用类型别名;使用尾置返回类型;利用auto或者decltype。
函数重载
基本概念
如果同一作用域内的几个函数名字相同但形参列表不同( 不考虑返回类型是否相同),称之为重载函数。但main函数不能重载,具有惟一性。
1)编译器会根据实参的类型确定调用哪一个函数。
2)不允许两个函数除了返回类型之外其他所有的要素相同:注意参数名字的省略、顶级const修饰、类型别名。结合函数的三要素,单纯的返回类型不同不能算函数重载且会引发重定义的问题,也就是说参数 列表必须具有“实质性”的不同才能作为重载函数。
//以下几个声明并不会构成函数重载
typedef int wing;
int add(int x);
double add(int x);//不允许只有返回类型不同
int add(wing x);//类型别名
int add(int );//忽略形参名
int add(const int x);//顶级const会被忽略
3)利用const_cast等强制类型转换方法对对象类型进行转换,然后调用已有的函数实现重载函数的编写。可以将const版本的函数重载为非const版本。
const int &min(const int &x , const int &y)
{
return x < y ? x : y ;
}
int &min(int &x , int &y)
{
auto & r = min(const_cast<const int&>(x) , const_cast<const int&>(y));
return const_cast<int&>(r);
}
函数匹配
函数匹配是指把函数调用和一组重载函数中某一个关联起来,函数匹配也叫
重载确定。
1) 调用重载函数时有三种结果:最佳匹配、无匹配、二义性调用。a)最佳匹配:编译器找到一个与实参最佳匹配的函数,并生成调用该函数的代码;b)无匹配:找不到任何一个与调用的实参匹配的函数,此时编译器发出无匹配的错误信息;c)二义性调用:有多于一个函数可以匹配,但没一个都不是最佳选择。此时编译器也会发出错误信息。
2) 函数匹配的过程:
a:在所有函数中确定候选函数。候选函数有两个特征:与被调用函数同名;其声明在调用点可见。
b:在候选函数中确定可行函数。可行函数有两个特征:形参数量与调用提供的实参数量相同(注意默认实参可以在调用时不必写出);实参与对应形参类型相同或能转换。没有找到可行函数,则会发生没有无匹配的结果。
c:在可行函数中确定最佳匹配。根据实参类型和形参类型来确定最匹配的函数,实参类型与形参类型越接近则越匹配。
3) 当含有多个形参需要匹配时,确定可行函数之后,在逐一地对每个实参进行最佳匹配。匹配不成功则会发生二义性调用的错误。匹配成功的条件:
a:该函数的每个实参的匹配都不劣于其他可行函数需要的匹配;
b:至少有一个实参的匹配由于其他可行函数提供的匹配。
函数指针
1) 函数指针指向的是函数而非对象。指针一般指向某种类型,而函数指针指向函数类型。函数类型有函数返回类型和参数列表决定。
bool lengthCompare(const string &,const string &);//函数类型为:bool (const string &,const string &)
bool (*pf)(const string &,const string &);//函数指针
2) 类似于数组名,当函数名当作一个值使用时,函数会自动的转化为指针:
pf = lengthCompare;//等价于下面的表达式
pf = & lengthCompare;//取址符是可选的
3) 用函数指针调用函数时,可以不用提前解引用指针:
//三个等价的调用
bool b1 = pf("a" , "b");
bool b2 = (*pf)("a" , "b");
bool b3 = lengthCompare("a" , "b");
4) 指向不同函数类型的指针不存在转换规则,但可以被赋予 nullptr 或者0的常量表达式。
5) 考虑到函数类型有点冗长,可以使用类型别名进行简化处理。
//Func,Func2,Func3是函数类型
typedef bool Func(const string &,const string &);
typedef decltype(lengthCompare) Func2;
using Func3 = bool (const string &,const string &);
//FuncP,FuncP2,FuncP3是函数指针
typedef bool (*FuncP)(const string &,const string &);
typedef decltype(lengthCompare) *FuncP2;
using FuncP3 = bool(*)(const string &,const string &);
内联函数与constexpr函数
1)内联函数是将其在调用点上“内联地展开”,有点类似于用函数代码替换调用位置的代码,从而达到减小调用开销的目的。
2)constexpr函数是指能用于常量表达式的函数:函数的返回类型以及所有形参的类型都得是字面值类型且函数体中只有return语句。
3)内联函数和constexpr函数可以在程序中多次定义,但多个定义必须完全一致。
小技巧tricks
1)使用指针形参和引用形参,可以直接作用于函数体外的变量,而不用通过返回值来进行修改;
2)使用引用形参可以操作不能拷贝的对象,也可以避免类类型对象、容器对象拷贝时引发的低效性能;
3)返回多个值的方法:a)定义多个返回对象组成复合数据类型(结构体、类),然会返回这个新的数据类型;b) 将需要返回的类型作为指针实参或者引用形参传入,从而达到多值返回的目的。
4)函数调用中防止数组访问越界的方法:a)利用数组自带的元素标记来判定,如C风格字符串末尾的空字符;b)利用函数begin,end获得数组的首元素和尾元素之后下一个位置的指针;c)显式地传递一个表示数组大小的形参。
类与函数
在类中定义的函数称为类的成员函数或者类的方法。
class A
{
int a;
int b;
int add();
}
int A::add()
{
return a+2;
}
A a_1;//
a_1.add();//可以看成 A::add(&a_1);
int add() const;
3)构造成员函数:定义类的数据成员的初始化过程。它没有返回类型,函数名与类名相同且不能被声明为const。构造函数可以拥有初始值列表,负责为新创建的对象的数据成员赋初值。
A():a(0),b(0){ }//初始值方式接受值初始化和花括号初始化两种方式
5)拷贝函数:控制对象拷贝的函数,可以被编译器默认合成。
6)赋值函数:控制对对象赋值的函数,可以被编译器默认合成。