C++的学习之旅——函数探幽

目录

一、C++内联函数

二、引用变量

1、创建引用变量

2、将引用用作函数参数

3、将引用用于结构

4、将引用用于类对象

三、函数重载

四、函数模板

1、什么是函数模板

2、重载的模板

扫描二维码关注公众号,回复: 14616325 查看本文章

3、模板的局限性

4、显示具体化

5、实例化和具体化

6、编译器使用哪个函数版本


一、C++内联函数

通常函数调用使得程序跳到另外一个地址(函数地址),调用结束后返回,这样每次调用都需要进行跳跃,执行速度会比较慢(相对内联函数),C++内联函数提供了另外一种选择,内联函数的编译代码与其他程序代码“内联”起来了,也就是说,编译器将相应的函数代码替换函数调用,这样程序无需进行跳转,但是代价是需要占用更多的存储空间,比如,在一个程序中使用了1次函数,那么该程序将包含10个副本。要使用这项特性,需要在函数前面采取下面措施之一:

①在函数声明前面加上关键字inline

②在函数定义前面加上关键字inline

需要注意的是:

①内联函数定义不宜占用多行(10行内最好)

②在内联函数内不允许使用循环语句和开关语句

③内联函数的定义必须出现在内联函数第一次调用之前;

④类结构中所在的类说明内部定义的函数是内联函数。

#include <iostream>

/* run this program using the console pauser or add your own getch, system("pause") or input loop */
using namespace std;

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

int main(int argc, char** argv) {
	
	double a,b;
	double c=13.0;
	a=square(5.0);
	b=square(4.5+7.5);
	cout<<"a="<<a<<",b="<<b<<endl;
	cout<<"c="<<c;
	cout<<", c squared="<<square(c++)<<"\n";
	cout<<"Now c="<<c<<endl;

	return 0;
}

二、引用变量

C++增加了一种复合类型——引用变量。引用是已定义的变量的别名(另一个名称)。如使用twain作为clement变量的引用,则可以交替使用twain和clement来表示该变量。引用变量的主要用途是用作函数的形参,通过将引用变量用作参数,函数将使用原始数据,而不是副本

1、创建引用变量

使用&符号来声明引用 

int rats;
int & rodents=rats;
其中&不是地址运算符,而是类型标识符的一种,就像声明中char*指的是指向char的指针一样,int &指的是指向int 的引用。上述声明允许将rats和rodents互换——他们指向相同的值和内存单元。

2、将引用用作函数参数

引用经常被用作函数参数,使得函数中的变量名称为调用程序中变量的别名。这种传递参数的方法称为引用传递。按引用传递允许被调用的函数能够访问调用函数中的变量。与按指针传递类似,可以对函数外界变量产生影响。 

 按引用传递后,函数中创建了传递参数的别名,这样一个变量就有两个名称(一个在是传递给函数的那个名称,一个是中创建的别名)。在函数中对别名操作,也会影响该变量。

3、将引用用于结构

使用结构引用参数的方式与使用基本变量引用相同,只需在声明结构参数时使用引用运算符&即可。 

struct free_throws
{
    string name;
    int made;
    int attempts;
    float percent;
};

void set_pc(free_throws & ft);//将指向该结构的引用作为参数

void display(const free_throws & ft);//若不希望函数修改传入的结构

 下面通过一个例子来深入了解一下

#include <iostream>

/* run this program using the console pauser or add your own getch, system("pause") or input loop */
using namespace std;

struct free_throws
{
    string name;
    int made;
    int attempts;
    float percent;
};

void set_pc(free_throws & ft);//将指向该结构的引用作为参数

void display(const free_throws & ft);//若不希望函数修改传入的结构

free_throws & accumulate(free_throws & target,const free_throws & source);

int main(int argc, char** argv) {
	//定义多个free_throws类型结构体 
	free_throws one={"Ifelsa Branch",13,14};
	free_throws two={"Andor Knott",10,16};
	free_throws three={"Minnie Max",7,9};
	free_throws four={"Whily Looper",5,9};
	free_throws five={"Long Long",6,14};
	free_throws team={"Throwgoods",0,0};
	
	free_throws dup;
	
	set_pc(one);
	display(one);
	accumulate(team,one);
	display(team);
	
	display(accumulate(team,two));
	accumulate(accumulate(team,three),four);
	display(team);
	
	dup=accumulate(team,five);
	
	cout<<"Display team:"<<endl;
	display(team);
	cout<<"Displaying dup after assignment:"<<endl;
	display(team);
	set_pc(four);
	
	accumulate(dup,five)=four;
	cout<<"Displaying dup after ill-advised assignment:"<<endl;
	display(dup);
	

	return 0;
}

//将传入的 free_throws结构类型成员打印 
void display(const free_throws & ft)
{
	cout<<"Name: "<<ft.name<<endl;
	cout<<" Made: "<<ft.made<<'\t';
	cout<<"Attenpts: "<<ft.attempts<<'\t';
	cout<<"Percent: "<<ft.percent<<endl;
}

//若传入的 free_throws类型结构成员attempts不为0,则将 made/attempts的值乘100赋给percent,否则percent为0 
void set_pc(free_throws & ft)
{
	if(ft.attempts!=0)
	{
		ft.percent=100.0f*float(ft.made)/float(ft.attempts);
	}
	else ft.percent=0;
}

//将传入的右边free_throws类型结构成员中的 attempts和 made加到右边,并返回一个target的引用 
free_throws & accumulate(free_throws & target,const free_throws & source)
{
	target.attempts+=source.attempts;
	target.made+=source.made;
	set_pc(target);
	return target;
}



程序说明:

① accumulate()返回了一个指向结构的引用,提高了效率,若返回一个结构,将把整个结构复制到一个临时位置,再将结构复制给dup,不过需要注意的是,返回指向结构的引用,这个结构不能为临时变量。

②语句accumulate(dup,five)=four代表的含义:先调用accumulate将five的数据添加到dup中,accumulate执行完毕后返回dup结构的引用,再将four的数据赋给dup。若不希望能够执行上述操作,可以将accumulate返回类型声明为const引用:

const free_throws & accumulate(free_throws & target,const free_throws & source)
{
	target.attempts+=source.attempts;
	target.made+=source.made;
	set_pc(target);
	return target;
}

4、将引用用于类对象

将类对象传递给函数时,C++通常的做法是使用引用。例如如下函数

const string  version3(string & s1,const string & s2)
{
    string temp;
    temp=s2+s1+s2;
    return temp;
}

 由于temp是一个临时的string对象,只在函数version3中有效,因而函数返回类型为string,不能为指向string的引用。另外调用该函数时还可以给其传递char*指针,string类定义了一种char*转换到string的功能。

三、函数重载

在C语言,如果函数接受char指针和int参数,则使用该函数时,不能只传递一个参数,但在C++中可以,因为C++支持被称为函数重载的OPP特性。函数重载允许创建多个同名函数,条件是参数列表不同,如使用cin.get(name,ArSize),则编译器将找到使用char*和int作为参数的cin.get()版本。

函数重载的关键是函数的参数列表——也称为函数特征标。如果两个函数的参数数目和类型相同,同时参数的排序也相同,则他们的特征标也相同。而变量名是无关紧要的。C++允许定义名字相同的函数,但是特征标必须不同。 

void print(const char*str,int width);//1 
void print(double d,int width);//2
void print(long l,int width);//3
void print(int i,int width);//4
void print(const char * str);//5

print("Pancakes",15);//使用1
print("Syrup");//使用5
print(1999.0,10);//使用2
print(1999,12);//使用4
print(1999L,15);//使用3

 另外,有一些看起来特征标不同的函数是不能共存的

double cube(double b);
double cube(double & b);

虽然看起来特征标是不同的,但是当执行例如:

cout<<cube(x);

参数x和double x原型和double & x原型都匹配,这时编译器将无法确定使用哪个原型

匹配函数时,并不区分const和非const变量 

void dribble(char * bits);//1
void dribble(const char * cbits);//2
void dabble(char * bits)//3
void drivel(const char * bits)//4

const char p1[20]="How is the weather";
char p2[20]"How is the weather";

dribble (p1);//使用2
dribble (p2);//使用1
dabble (p1);//使用3
dabble (p2);//找不到原型
drivel (p1);//使用4
drivel (p2);//使用4

 dribble 函数有两个版本,编译器可以辨别是否为const来决定使用哪个原型。dabble函数 只与带非const参数调用匹配。drivel函数则可以与带const或非const的调用匹配。出现这种现象主要原因是将非const赋给const变量是合法的,但是反之则非法

此外,还需要注意是特征标而不是函数类型使得可以对函数进行重载。以下两个函数互斥

long gronk(int n,float m);
double gronk(int n,float m);

重载引用参数

void stove(double & r1);//左值引用 1
void stove(const double & r2);//const引用 2
void stove(double && r3);//右值引用 3

double x=55.5;
const double y=32.0;
stove(x);//使用1 
stove(t);//使用2 
stove(x+y);//使用3

左值引用参数r1与可修改的左值参数(如 double 变量)匹配;const 左值引用参数r2与可修改的左值参数、const 左值参数和右值参数(如两个 double 值的和)匹配; 最后,右值引用参数r3 与右值匹配。注意到与 r1或r3 匹配的参数都与 r2 匹配。这就带来了一个问题:如果重载使用这三种参数的函数,结果将如何?答案是将调用最匹配的版本。

 函数重载经常用在参数列表不同,但是执行的内容类似的需求中,不可以滥用

四、函数模板

1、什么是函数模板

现在的 C++编译器实现了 C++新增的一项特性一一函数模板。函数模板是通用的函数描述,也就是说它们使用泛型来定义函数,其中的泛型可用具体的类型(如 int 或 double) 替换。通过将类型作为参数传递给模板,可使编译器生成该类型的函数。由于模板允许以泛型(而不是具体类型)的方式编写程序,因此有时也被称为通用编程。由于类型是用参数表示的,因此模板特性有时也被称为参数化类型(parameterized types)。下面介绍为何需要这种特性以及其工作原理。
当我们定义了一个交换两个 int 值的函数。假设要交换两个 double 值,则一种方法是复制原来的代码,并用 double 替换所有的 nt。如果需要交换两个char 值,可以再次使用同样的技术进行这种修改将浪费宝贵的时间,且容易出错。C++函数模板允许以任意类型的方式来定义函数,例如按照上述要求可以定义一个交换函数模板:

template <typename AnyType> 
void Swap(AnyType &a,AnyType &b)
{
	AnyType temp;
	temp=a;
	a=b;
	b=temp;
}

 第一行指出,要建立一个模板,并将类型命名为 AnyType。关键字 template和 typename 是必需的,除非可以使用关键字 class代替 typename。另外,必须使用尖括号。类型名可以任意选择(这里为AnyType),只要遵守 C++命名规则即可。余下的代码描述了交换两个 AnyIype值的算法。模板并不创建任何函数,而只是告诉编译器如何定义函数。需要交换 int 的函数时,编译器将按模板模式创建这样的函数,并用 int 代替 AnyType。同样,需要交换 double 的函数时,编译器将按模板模式创建这样的函数,并用 double 代替 AnyType。调用函数时只需要正常调用Swap()函数即可,编译器将检查所使用的参数类型,并生成相应的函数。

2、重载的模板

需要多个对不同类型使用同一种算法的函数时,可使用模板。然而,并非所有的类型都使用相同的算法。为满足这种需求,可以像重载常规函数定义那样重载模板定义。和常规重载一样,被重载的模板的函数特征标必须不同。我们可以新增了一个交换模板,用于交换两个数组中的元素。原来的模板的特征标为(AnyType &,AnyType&),而新模板的特征标为(AnyType[ ],AnyTypet[ ],int)。注意,在后一个模板中,最后一个参数的类型为具体类型(int),而不是泛型。并非所有的模板参数都必须是模板参数类型。编译器一个 Swap()函数调用时,发现它有两个int 参数,因此将它与原来的模板匹配。但第二次调用将两个 int 数组和一个 mt 值用作参数,这与新模板匹配。

3、模板的局限性

编写的模板函数有时候无法处理某些类型,例如: 

template <class T>
void f(T a,T b)
{
    if(a>b)
    a=b;
}

 这时,如果传递一个结构,这将不成立。

4、显示具体化

模板函数存在一定的局限性,有时无法满足我们的要求,这时可以提供一个具体化函数定义——称为显示具体化。 其中包含所需要的代码。当编译器找到与函数调用匹配的具体化定义时,将使用该定义,而不使用模板。显示具体化的原型和定义应以template<>开头,并通过名称来指出类型。具体化函数优于常规模板,而非模板函数(先前使用的普通函数)则优于具体化和常规模板。

//非模板函数
void Swap(job &,job &);
//模板函数
template <typename T>
void Swap(T &,T &);
//具体化原型
template <> void Swap<job>(job &,job &);

 其中job为一个结构,在Swap<job>中job是可选的,因为函数的参数类型表明,这时job的一个具体化,因此,函数也可以写成:

template <> void Swap<job>(job &,job &);

5、实例化和具体化

为进一步了解模板,必须理解术语实例化和具体化。记住,在代码中包含函数模板本身并不会生成函数定义,它只是一个用于生成函数定义的方案。编译器使用模板为特定类型生成函数定义时,得到的是模板实例(istantiation)。模板并非函数定义,但前面函数调用使用 int 的模板实例是函数定义。这种实例化方式被称为隐式实例化(implicit instantiation),因为编译器之所以知道需要进行定义,是由于程序调用 Swap()函数时提供了int 参数。

最初,编译器只能通过隐式实例化,来使用模板生成函数定义,但现在 C++还允许显式实例化(expliciinstantiation)。这意味着可以直接命令编译器创建特定的实例,如 Swap<int>()。其语法是,声明所需的种类——用符号<>指示类型,并在声明前加上关键字 template:

template void Swap<int>(int ,int);

 实现了这种特性的编译器看到上述声明后,将使用 Swap()模板生成一个使用nt 类型的实例。也就是说,该声明的意思是“使用 Swap()模板生成int类型的函数定义。”

与显式实例化不同的是,显式具体化使用下面两个等价的声明之一:

template <> void Swap<int>(int &,int &); 
template <> void Swap(int &, int &);

 区别在于,这些声明的意思是“不要使用 Swap()模板来生成函数定义,而应使用专门为 int 类型显式地定义的函数定义”。这些原型必须有自己的函数定义。显式具体化声明在关键字 template 后包含<>,而显式实例化没有。

6、编译器使用哪个函数版本

对于函数重载、函数模板和函数模板重载,C++需要(且有)一个定义良好的策略,来决定为函数调用使用哪一个函数定义,尤其是有多个参数时。这个过程称为重载解析(overloading resolution)。详细解释这个策略将需要将近一章的篇幅,这里简单讲述一下。这个过程进行流程如下:

①创建候选函数列表。其中包含与被调用函数的名称相同的函数和模板函数。

②使用候选函数列表创建可行函数列表。这些都是参数数目正确的函数,为此有一个隐式转换序列,其中包括实参类型与相应的形参类型完全匹配的情况。例如,使用 float 参数的函数调用可以将该参数转换为 double,从而与 double 形参匹配,而模板可以为 float 生成一个实例。

③确定是否有最佳的可行函数。如果有,则使用它,否则该函数调用出错。 


C++的学习笔记持续更新中~

要是文章有帮助的话

就点赞收藏关注一下啦!

感谢大家的观看

欢迎大家提出问题并指正~

猜你喜欢

转载自blog.csdn.net/qq_47134270/article/details/128645379