c++函数2
2018-12-31 本文已影响0人
gpfworld
1. 函数重载
(1)函数重载来源于何处
常常需要多个不同的函数完成几乎相同的功能。
比如说,在再java和c++中都提供的比较两个数中那个数做大的函数的,
比函数的名叫max,但是实际上因为数据类型的不同,需要针对int,float
,double给出各种不同的函数实现。
在c中的典型做法就是就是定义成这样
max_int(), max_double(), max_float()。
显然这样的方式有点不够人性化,但是在c中就是正常这样实现,不过
在c中还可以通过函数指针数组的来实现,不过这种实现方式是非主流方
式,不具有普适性。
(2)函数签名
唯一函数签名是:命名空间::函数名(参数列表)
以上组合成为了唯一的函数签名,所以有着相同函数名,但是不同参数列表,
就看构成了不同的函数,这是c++/java等能够实现重载根本原因。
注意,返回值不参与函数签名的构建。
(3)到底在什么时候使用重载
实际上如果多个函数的所做的事情完全重叠,唯一不同只不过就是数据
类型的话,实际上在c++和java往往会使用函数模板(java中叫泛型)
来实现。
比如:
int max_int(int a, int b) {
return a>b? a : b;
}
double max_float(double a, double b) {
return a>b? a : b;
}
由于以上两个函数除了类型不同外,所做的事情几乎完全一致。
我们就会直接使用模板。
template <class T> T max(T a, T b) {
return a ? b a : b;
}
只有当函数实现的大体功能非常相似,但是细节实现上又有非常多的不同的
时候我们才会使用重载。
int max1(int a, int b) {
a++;
a += 10;
return a>b? a : b;
}
double max2(double a, double b) {
return a>b? a : b;
}
以上两个函数除了类型不同外,关键是实现的内容也大不相同,这样的话
就没有办法实现模板,我们就可以使用重载来着,重在时,可以共享相同
的函数名。
int max(int a, int b) {
a++;
a += 10;
return a>b? a : b;
}
/* 重载max函数 */
double max(double a, double b) {
return a>b? a : b;
}
总结起来:
当函数需要重非常相似的功能,但是函数所做的事情又不完全相同时,我
们可就选择重载。
实际上我们完全可以认为函数模板是一种特殊意义上的函数重载。
(4)函数重载与函数重写的区别
重载是在相同区间内完成的事情,如果被重载的是外部函数,其它重载
的函数也要求是外部的,如果被重载的函数在类的类内部,那么所有重载
的函数都是在类内部,如果一个外部函数与某个类内成员函数同名了,这并不是重载。
说到重写的话,就一定涉及父类和子类,所谓重写就是子类对父类成员函数
重写,重写与实现多态有着不可分割的关系,或者说是实现多态的关键。
(5)函数的重载特点
(1)所有重载的函数的名称相同
(2)函数参数列表不能相同,不同体现在三个
(1)参数列表的个数
(2)参数的顺序区分
(3)参数类型
(3)注意:返回值并不能区分重载,因为返回值不参与函数签名的组成。
(4)参数名不参与重载区分,因参数名不参与函数签名的组成。
虽然函数名相同,但是由于参数列表不同,因此重载的函数有着完全不同的
函数签名。
以上重载的规则与java中函数重载规则完全一致。
(6)重载过程涉及到的实参向形参隐式转换的问题
(1)隐式转换举例
#include <iostream>
#include <string>
using namespace std;
double max(double a, double b)
{
return a > b ? a : b;
}
int max(long a, long b)
{
return a > b ? a : b;
}
int main(int argc, char **argv)
{
double a_double = 12.6, b_double = 34.5;
float a_float = 32.4, b_float = 42.67;
long a_long = 10, b_long = 20;
cout << max(a_double, b_double) << endl;
cout << max(a_float, b_float) << endl;
cout << max(a_long, b_long) << endl;
return 0;
}
cout << max(a_double, b_double) << endl;
选择double参数类型的函数。
cout << max(a_long, b_long) << endl;
选择参数为long类型的函数。
cout << max(a_float, b_float) << endl;
float隐式转换为double类型后(向上转型),调用long型参数的函数。
(7)指针参数参与重载时需要注意
当指针作为形参参与重载时,以下类似的情况一定是不同的重载函数,
int fun(float *p, int a);
int fun(int *p, int a);
在前面学习数组时我们知道了 int p[]和int *p其实是一回事,前者的表示方式
只是为了增加可读性。
int fun(int p[], int a){ }
int fun(int *p, int a){ }
表示同一个函数,不属于重载。
(8)引用参数参与重载
(1)需要注意的情况1
int fun(int a){ }
int fun(int &a){ }
进行函数调用
int a = 10;
fun(a);
以上是没有办法进行重载区分的,所以不能通过给定参数类型,然后另
一个是该类型的引用的方式来区分重载。
(2)需要注意的情况2
#include <iostream>
#include <string>
using namespace std;
void max(double a)
{
cout << "double" << endl;
}
void max(int &a)
{
cout << "long" << endl;
}
int main(int argc, char **argv)
{
double a = 0;
max(a);
max(static_cast<int>(a));//max((int)a)
return 0;
}
运行结果:
double
double
在本例中,
max(a);
max((int)a);
实际上调用的都是形参类型为double的这一个函数,虽然 max((int)a);
已经将a强制转换为了int型,但是引用被初始化的只是被转换后的临时
变量空间,由于引用可以修改实参,而这个时候实参又是一个临时变量
空间,修改临时变量会引起不确定结果,因此编译拒绝调用形参为int&型
的函数。
本例修改方法:
(1)将a改为int型,这样就不需要对a进行强制转换了,那么fun函数的
int &a形参被确定的变量空间a初始化,而不是被临时变量初始化。
(2)将fun函数的int &a改为const int &a,只要告诉编译器const防止了修改
临时变量空间的风险,可以放心使用了。
(9)const在重载中起到的作用
(1)形参为基本类型时,const不参与重载的区分
int fun(int a) { }
int fun(const int a){ }
以上这两个函数是完全一样的。
(2)形参为指针类型时,const修饰形参时,需要注意
以下是两个完全不同的重载函数。
int fun(int *a) { }
int fun(const int *a){ }
以下是同一种情况
int fun(int *a) { }
int fun(int * const a){ }
函数只关心指针指向的内容会不会被修改,而不关心指针是否会改变指向。
(3)形参为引用时,const参与重载区分
以下是两个不同的重载函数:
int fun(int &a) { }
int fun(int const &a){ }
具体例子
#include <iostream>
#include <string>
using namespace std;
void max(int const &a)//等价于 const int &a
{
cout << "const:" << a << endl;
}
int * max(int &a)
{
cout << "no const:" << a << endl;
return NULL;
}
int main(int argc, char **argv)
{
int a = 20;
max(a);//调用int * max(int &a)
max(10);//调用void max(int const &a)
return 0;
}
运行结果:
no const:20
const:10
例子分析:
max(a);
由于a是一个变量,所以调用int * max(int &a)函数。
max(10);
由于10是一个常量,所以调用void max(int const &a)函数。
如果注释掉
int * max(int &a)
{
cout << "no const:" << a << endl;
return NULL;
}
这个函数,依然可以运行,但是注释掉另一个函数,就不可以,不可以引用一个常量10.
2. 函数模板
(1)什么是函数模板
函数模板其实就是一个生成具体函数实例的模具,函数模板中的类型都是泛型,函数模板其实
就是java中就是泛型在函数中的使用。
如果程序中有调用模板的话,编译器在编译时会根据模板自动生成一个具体的函数,运行时,
这个生成的具体的函数将会被调用。
(2)什么时候用模板,什么时候用重载
在前面我们已经简略的描述过了什么时候用重载和什么时候用模板,其实可以用模板的情况
都可以使用重载来代替。但是如果可以使用模板的话,就没有必要用重载了,前面说过模板
其实也是一种特殊的重载。
如果各个函数的功能完全相同,唯一不同的只是类型时,就使用模板更好。当各个函数的功能
相似但实现代码并不相同时,我们就只能使用重载实现。
(3)函数模板的定义格式
(1)模板格式举例
template<class T> T mymax(T a, T b)
{
......
}
又比如:
template<class T1, class T2> T1 mymax(T1 a, T2 b)
{
......
}
说明:
template:关键字,表示这段代码是模板
class:关键字,表示T为类型,class可以使用typename替换。
T/T1/T2:T/T1/T2实际上就是一个泛型,所谓泛型表示类型不定,可以使基本数据类型,可以
是组合数据类型,如指针,结构体,类类型等。
我们习惯使用T来表示泛型,实际上完全是可以使用其他字符表示泛型。
对于函数模板来说,一般情况下函数模板的泛型由调用模板时实参的类型决定,如果
有多个类型的话,需要使用","隔开。在java中,由于所有的函数都是在类内部的,因
此java函数的泛型是由类提供的。
尖括号后<>右面的T或者T1:表示函数的返回值为T或者T1
()中的T/T1/T2:表示形参的类型
(2)模板使用实例
#include <iostream>
#include <string>
using namespace std;
template <class T> T mymax(T a, T b)
{
return a > b? a : b;
}
int main(int argc, char **argv)
{
int a = 20;
int b = 10;
cout << "larger is " << mymax(a, b) << endl;
return 0;
}
函数模板参数在调用时根据实参类型决定,在调用模板时,根据具体的
实参类型生成函数实例,相同的函数实例模板只生成一次。
函数模板只是一个模板,编译器在编译时会根据模板生成实际需要的函
数实例,最后在链接时会将函数的实例与函数的调用链接起来。
(3)函数模板的声明
如果函数模板的定义在调用位置的后面,或者函数模板的定义和调用的
位置在不同的编译单元时,需要进行模板的声明,模板的声明与普通函
数的声明没有任何的区别。
(4)调用模板时直接模板参数类型
(1)显示指定模板类举例
比如上例中:
cout << "larger is " << mymax(a, b) << endl;
可以写为:
cout << "larger is " << mymax<long>(a, b) << endl;
本来a和b都是int型,但是我在<>中将类型显式的指定为long型,这样a和b
被隐式向上转型为long型后复制给形参a和b。
(2)为什么需要显示指定模板参数类型
(1)调用时类型模糊
比如还是前面的例子,显然如下代码是无法通过编译的。
int a = 10;
long b = 20l;
mymax(a, b);
编译时会提示如下错误:
no matching function for call to ‘mymax(int&, long int&)’
因为a为int,但是b却为long,但是函数模板要求两个参数的类型是
相同的,但是显然达不到这一要求,因此编译无法通过。
如果将
mymax(a, b);
改为:
mymax<long>(a, b);
就没有问题,因为,显式指定了将a和b都统一成为了long类型。
(2)避免生成太多不同版本的函数实例
比如在调用模板时,可能会生成int,float,long,double等不同的模板实例,
但是为了统一化,实际上我们可能会在调用时统统显式声明为Long型,这样只
会使用参数为long的模板实例。
(3)类型含混不清的时候
template <class T1, class T2> T1 mymax(T2 a, T2 b)
{
return a > b? a : b;
}
按照如下方式进行调用
int a = 10;
int b = 20;
mymax(a, b);
显然函数mymax(a, b);是无法通过编译的,因为没办法通过实参的类型去
决定返返回值的T1的类型,所以我们在调用时需要显式的说明返回值的类型。
mymax<int, int>(a, b);
表示T1是int,T2也是int
mymax<short>(a, b);
这个调用方法也是ok的,因为T2由参数类型决定,T1由short
进行说明,如果返回的值不是short,会被强制转换为short型。
(5)模板特例说明
template <class T1, class T2> T1 mymax(T2 a, T2 b)
{
return a > b? a : b;
}
对于这个模板来说,指定制定模板参数类型为指针时会出现问题。
比如:
int a = 10;
int b = 20;
mymax(&a, &b);
以上的这个调用有两个问题:
问题1:不可以传递指针类型
传递的是地址,所以return a > b? a : b;应该改为
return *a > *b? a : b;
显然这个模板是满足不了这个要求的。
解决方法为对模板做特例例声明,因为a和b为int *,那么针对int *类型的
模板特例定义为
template <> int mymax<int, int *>(int * a, int * b)
{
return *a > *b? *a : *b;
}
表示如果传递的参数为int *时,必须使用这个模板特例。
问题2:无法通过实参指定返回类型T1
调用方式1:
mymax<int>(a, &b);
表示T1是int型,T2由实参类型决定为int *。
调用方式2:
mymax<int,int *>(&a, &b);
这种方式同调用方式1,只不过直接指定了T2为int *型,但是实参实际的类型本
来也是int *型。
以上两种调用情况都会调用到调用模板的特例去实例化一个函数。
template <> int mymax<int, int *>(int * a, int * b)
{
return *a > *b? *a : *b;
}
(6)函数模板和重载
针对前面提到的例子,使用了模板特例来解决问题,实际上还有另外两种解决问题的办法。
(1)函数重载
针对实参为指针/地址的情况重载一个新函数。
#include <iostream>
#include <string>
using namespace std;
template <class T1, class T2> T1 mymax(T2 a, T2 b)
{
cout << "1111" << endl;
return a > b? a : b;
}
/* 对mymax重载一个新的专门针对返回值为int,形参类型为int *的函数 */
int mymax(int *a, int *b)
{
cout << "2222" << endl;
return *a > *b ? *a : *b;
}
int main(int argc, char **argv)
{
int a = 2111120;
int b = 1333330;
cout << "larger is " << mymax(&a, &b) << endl;
return 0;
}
(2)模板重载
上面的例子实际上有一个问题,那就是只能针对返回值为int,形参为int *的情况,
如果返回值为long,形参为long *的情况时就不合适了,因此更加合理的方式是对模板重载一个新的模板。
#include <iostream>
#include <string>
using namespace std;
template <class T1, class T2> T1 mymax(T2 a, T2 b)
{
cout << "1111" << endl;
return a > b? a : b;
}
/* 重载模板 */
template <class T1, class T2> T1 mymax(T2 *a, T2 *b)
{
return *(T2 *)a > *(T2 *)b ? *(T2 *)a : *(T2 *)b;
}
int main(int argc, char **argv)
{
long a = 21;
long b = 13;
cout << "larger is " << mymax<long>(&a, &b) << endl;
return 0;
}
(7)带有多个参数模板
这在前面已经用过。
(8)非类型参数模板
非类型参数模板实际上在java中也有。
请看例子:
#include <iostream>
#include <string>
using namespace std;
template <class T, int lower, int upper> bool range(T a)
{
return ((a<upper) && (a>lower));
}
int main(int argc, char **argv)
{
cout << "larger is " << range<int, 30, 20>(10) << endl;
return 0;
}
例子说明:
在本例子中,range<int, 30, 20>(10)调用模板是,指定了3个说明参数,第一个
说明实参类型,这里实参类型本来也是int型的,第2个和第3个是两个非类型模板
参数。
在这个例子中我们需要注意:
(1)模板中可以直接指定返回值的类型,比如本例子中的模板的返回值类型明确
的指定返回值的类型为bool。实际上模板的形参类型也可以明确指出。
比如这个例子完全可以改成如下样子:
template <class T, int lower, int upper> bool range(int a) {......}
(2)range<int, 30, 20>(10)进行调用时,在这种有非类型模板参数时,第一个
类型模板参数必须指定。
3. 函数指针和指针函数
(1)概念区别
指针函数:返回值为指针的函数,int *fun(){ }
函数指针:存放函数地址的指针变量int (*)()
数组指针:存放数组地址指针变量, int (*)[n]
指针数组:元素为指针变量的数组, int *buf[]
(2)函数指针对于c语言有着非常重要的意义,在c语言实现面向对象的开发中,扮演者具足轻重的角色
(1)把函数指针所谓参数传送(回掉函数)
(2)通过结构封装函数指针,利用结构注册,然后通过结构体实现函数的回掉
(3)结构体封装函数指针实现函数回调,进而实现c语言中的多态,在c语言面向对象的思想中的意义
多态的意义就在于分层结构的实现,使得不同层之间可以相互隔离,可以根据不同的情况
调用不同的接口进行工作。
(4)利用带有函数指针的结构体实现面向对象的开发,实现分层结构,实现大型c语言项目的构建
(5)通过函数指针回掉函数时,不会受到static(内链接属性)的影响
(3)c语言实现面向对象开发的缺陷
(1)无法实现类的集成
(2)无法对结构体成员进行隐藏
(4)利用函数结构体封装函数指针实现的多态,以面向对象的思想(分层)写一个计算器
(5)我们之前写比较复杂一点的链表典型的就是以面向对象的思想来写的
(6)函数指针数组
4. 递归函数
(1)直接或者间接调用自己的函数就是递归函数
(2)递归函数一定要有返回条件,否者会导致函数栈爆掉。
(3)返回条件不能等待时间过久。
(4)递归函数大多都与访问树结构的数据有关。
(5)分析递归函数的方法就是,将递归次数降低为2-6次左右进行分析。
(6)递归的例子
(1)在讲二叉链表时已经使用过了
(2)数据结构中快速排序使用的就是递归算法
5.全局函数和成员函数
(1)c中的函数都是全局
(2)c++函数分为全局函数和成员函数,java等高级语言则只有成员函数,没有
全局函数,实现了类的完全封装。
(3)什么时候定义成员函数,什么时候定义全局函数
(1)如果只与当前类相关就定义成员类的成员函数
(2)如果该函数需要兼容操作很多不同类型的数据,这个时候一般就定义为
全局函数,为了进一步提高兼容性,还会将其定义成全局函数模板。