初识Cpp之 五、函数

五、函数

​ 与C语言相比,C++引入了许多新特性包括内联函数、按引用传递变量、默认的参数值、函数重载(多态)以及模板函数。

(1)内联函数

​ C++内联函数对函数的调用提供了另一种选择。内联函数的编译代码与其他程序代码“内联”起来了,也就是说,编译器将使用相应函数的代码替换函数的调用,对于内敛代码,函数无需跳入另一个位置处执行代码,再跳转回来;而是直接采用代码替换的方式。

Image

要想使用内联函数这一特性,就必须使用关键字inline,具体是做法如下:

1、在函数声明前加上关键字inline

2、在函数定义前加上关键字inline

3、通常的做法是省略函数原型,将整个函数的定义放在本应该提供函数原型的位置。

​ 内联函数和常规函数一样,也是按值进行传递参数的,这里说明一下内联函数和宏的差别:C语言C++中的宏并不是通过传递参数实现的,而是通过文本替换来实现的。但是内联函数虽然是进行代码块的替换,但是内联函数却是通过传递参数来进行实现的。举个例子:

#define SQUARE(X) X * X		//定义一个宏
SQUARE(3 + 4);		//结果为3 + 4 * 3 + 4 = 19, 宏是通过替换来实现的
inline int square(x) {return x * x};
square(3 + 4);		//结果为 (3+4)*(3+4) = 49, 内联函数是通过参数传递来实现的

(2)引用变量

​ C++新增了一种复合类型——引用变量,引用是已定义的变量的别名。引用变量的主要作用是用作函数的形参,函数将引用变量用作参数,函数将使用原始数据,而不是数据的副本。这样,除指针外,引用变量也为函数处理大型的结构体提供了一种非常方便的途径。引用的作用就是给一个变量创建一个别名,两者指向同一内存空间。

创建引用变量数据类型 &别名 = 原名。C语言和C++使用&符号来指示变量的地址。C++使用符号&来声明一个引用。创建引用的通用做法是必须在创建引用时就给该引用赋值,而不能先创建后赋值:

typeName somevalue;					//创建一个变量
typeName& somerefer = somvalue;		//创建该变量的引用,通用写法

例如:

int a = 10;
int& b = a;		//创建了一个对a变量的引用b变量

​ C++新增了右值引用,这种引用可以指向右值,使用&&来进行声明。例如:

double j = 15.0;
double&& jref = j * 2.0 + 15.0;		//j*2.0 + 15.0是一个右值,这里创建了对其的引用

引用的注意事项:1、引用必须进行初始化;2、引用在初始化后就不能发生改变了。

引用作函数的参数:引用经常被用作函数的参数,使得函数中的变量名称为调用程序中的变量的别名,这种传递参数的方式称为按引用传递。按引用传递允许被调用的函数能够访问调用函数中的变量。这项新增特性是对C语言的一种超越,C语言只能够按值进行传递,按值传递导致被调用函数使用调用程序的值的拷贝,当然,C语言也允许避开按值传递的限制,采用按指针传递的方式(本质上也是一种按值传递)。

Image

使用示例:

#include <iostream>
void swapr(int& a, int& b);		//按引用传递
void swapp(int* p, int* q);		//按指针传递
void swapv(int a, int b);		//按值传递
int main(void)
{
	using namespace std;
	int wallet1 = 300;
	int wallet2 = 350;
	cout << "wallet1 = $ " << wallet1;
	cout << " wallet2 = $ " << wallet2 << endl;
	cout << "Using reference to swap contens:\n";
	swapr(wallet1, wallet2);
	cout << "wallet1 = $ " << wallet1;
	cout << " wallet2 = $ " << wallet2 << endl;
	cout << "Using pointers to swap contens again:\n";
	swapp(&wallet1, &wallet2);
	cout << "wallet1 = $ " << wallet1;
	cout << " wallet2 = $ " << wallet2 << endl;
	cout << "Trying to use passing by value:\n";
	swapv(wallet1, wallet2);
	cout << "wallet1 = $ " << wallet1;
	cout << " wallet2 = $ " << wallet2 << endl;
	return 0;
}
void swapr(int& a, int& b)
{
	a = a ^ b;
	b = a ^ b;
	a = a ^ b;
}
void swapp(int* p, int* q)
{
	*p = *p ^ *q;
	*q = *p ^ *q;
	*p = *p ^ *q;
}
void swapv(int a, int b)
{
	a = a ^ b;
	b = a ^ b;
	a = a ^ b;
}

引用变量的特性:传递引用变量的限制很严格,当引用变量名是另一个变量的别名的时候,则实参是该变量,不能使用该变量的表达式来进行参数传递,也就是说,当引用形参的实参是一个右值表达式的时候,这时引用是非法的。举个例子:

double cube(double& ra);		//函数原型
double x = 3.0;
cube(x + 3.0);			//这种调用是非法的,因为表达式x+3并不是变量,不能被引用

能否被引用就涉及到左值和右值的问题:左值是什么呢?左值参数是可以被引用的数据对象,例如,变量、数组元素、结构体成员、引用和解除引用的指针都是左值。非左值包括字面常量(用引号括起来的字符串除外,它们由其地址表示)和包含多项的表达式。

临时变量、引用参数和constconst修饰引用称为常量引用,主要用途是防止使用引用对原数据进行修改。在参数被const修饰的时候,如果实参和引用参数不匹配,C++将自动生成临时变量空间,并且将引用指向这一临时空间,操作引用对象时实际上是在操作这一临时空间。C++在以下两种情况下才会生成临时变量

1、实参的类型正确,但不是左值。

2、实参的类型不正确,但可以转换为正确的类型。

例如:

double cube(const double& ra)
{
    return ra * ra * ra;
}
double side = 3.0;
long edge = 5L;
double c1 = cube(side);			//正常的传入引用参数
double c2 = cube(side + 3.0);		//实参的类型正确,但是side+3.0并不是左值,因此自动创建临时对象
double c3 = cube(edge);			//实参的类型不正确,但是可以转化为正确的类型,自动创建临时对象

引用参数作为函数的返回值:需要注意一点,不要让引用参数返回对局部变量的引用,因为这样在函数运行结束的时候,局部变量已经销毁,此时将不存在对内存单元的引用。为了避免这种问题,最好的做法是返回对函数参数列表的引用,因为这些参数列表是对调用函数所使用数据的引用,因此返回的引用也将指向这些数据。1、不要返回对局部变量的引用;2、函数的调用可用作为左值。示例:

int& test(void)
{
    int a;
    return a;
}
int& ref = test();		//这样做是非法的,1、不能返回局部变量的引用,因为局部变量位于栈区会自动销毁

int& test(void)
{
    static int a;
    return a
}
test() = 1000;			//2、引用作为返回值时函数的调用可用用作左值,这里就相当于将存放于全局区的静态变量a赋值为1000

引用的本质:引用在C++内部是通过指针常量来实现的,这就解释了为什么引用的指向是不可更改的。

(3)默认参数

​ C++支持设置默认参数——默认参数是值当函数调用中省略了实参时自动使用一个值。设置默认参数必须通过函数原型,这是因为编译器会通过函数原型来查看程序所需要的参数数目,因此函数原型也必须将可能的默认参数告诉程序。请注意,如果函数声明有默认参数,那么函数定义则不能有默认参数,只需要向函数原型提供默认参数,而函数的定义与没有默认参数的时候完全相同。如果没有指定函数的原型而是直接进行函数的定义,那么在没有函数原型的时候直接在函数定义中添加默认参数。函数声明和函数定义只能有一个有默认参数

​ 对于带参数的列表的函数,必须从右往左添加默认参数,也就是说,要为某个参数设置默认参数,则必须为它的右边的所有参数都提供默认参数。例如:

char* left(const char* str, int n = 1, int m = 2);		//函数原型,并且提供了两个默认参数
char* left(const char* str, int n, int m)			//函数定义无需提供默认参数
{
    ...
}
char* left(const char* str, int n = 1, int m = 2)		//在没有函数原型的时候直接在函数定义里添加默认参数
{
    ...
}

(4)占位参数

​ 函数占位参数,占位参数只有参数类型声明,而没有参数名声明,一般情况下,在函数体内部无法使用占位参数。占位参数可以指定默认值。示例:

void func1(int a, int)		//该函数的第二个参数即为占位参数
{
    std::cout << "Hello World!" << std::endl;
}
void func2(int a, int = 10)		//该函数的第二个参数即为占位参数,并且指定了默认值
{
    std::cout << "Hello World!" << std::endl;
}

使用占位参数的目的是为了让函数的参数列表不同,使之满足函数重载的条件。示例:

//在重载前置++和后置++的过程中使用int
MyInteger& operator++()		//重载前置++
{
    ...
}
//int是一个占位参数,可以用于区分前置和后置递增,并且只能用int
MyInteger& operator++(int)	//重载后置++
{
    ...
}

(5)函数重载

​ 函数多态是C++在C语言的基础上的新增的功能。默认参数能够使用不同数目的参数列表调用同一个函数,而函数多态(函数重载)能够使用多个同名的函数。

​ 函数重载的关键在于函数的参数列表——也称为函数特征标(function signature)。如果在同一作用域下,两个函数的参数数目和类型相同,同时参数的排列顺序也相同,则它们的特征标相同,而变量是允许不同的。C++允许定义名称相同的函数,前提是它们的特征标不同。在使用同名的函数的时候,编译器将会根据函数的参数列表来选取对应的函数进行执行。例如:

void print(const char* str, int width);		//函数1
void print(const long l, int width);		//函数2
void print(const char* str);				//函数3,这些都是函数重载的示例

​ 当引用作为重载的条件:需要注意的是,编译器在检查函数特征标的时候,将类型引用和类型本身视作同一个特征标。例如:

double cube(double x);				//函数1
double cube(double& x);				//函数2,这种函数重载是错误的

需要牢记是特征标,而不是函数类型使得可以对函数进行重载。例如:

long gronk(int n, float m);			//函数1
double gronk(int n, float m);		//函数2,这种重载是错误的,因为重载的时候允许返回类型不同,但是函数的特征标也必须不同

函数重载遇到默认参数:函数的重载没有问题,但函数的调用有可能会出现二义性的情况,示例:

void func (int a, int b = 10)
{
    ...
}
void func (int a)			//函数的重载没问题,因为函数的参数列表不同
{
    ...
}
func(10);			//但是这样调用函数的时候,两个函数定义均满足,所有会出现二义性的问题
func(10, 20);		//这样调用不会出现二义性

到底什么时候使用函数重载?仅当函数基本上执行相同的任务,但使用不同形式的数据时,才应该采用函数重载。简而言之函数重载需要满足三个条件

1、在同一个作用域下;

2、函数名称相同;

3、函数参数类型不同,或个数不同,或顺序不同。

(6)函数模板

在C++添加关键字typename之前,C++使用关键字class来创建模板,也就是说模板还可以这样定义:

template <class AnyType>
void Swap(AnyType &a, AnyType &b)		//定义了一个交换两对象值的函数
{
    AnType temp;
    temp = a;
    a = b;
    b = temp;
}

一个完整的示例:

#include <iostream>
template <typename T>			//模板和函数原型写在一起
void Swap(T& a, T& b);
int main(void)
{
	using namespace std;
	int i = 10;
	int j = 20;
	cout << "i, j = " << i << ", " << j << ".\n";
	cout << "Using complier-generated int swapper:\n";
	Swap(i, j);
	cout << "Now i, j = " << i << ", " << j << ".\n";
	double x = 24.5;
	double y = 81.7;
	cout << "x, y = " << x << ", " << y << ".\n";
	cout << "Using complier-generated double swapper:\n";
	Swap(x, y);
	cout << "Now x, y = " << x << ", " << y << ".\n";
	return 0;
}

template <typename T>		//用模板来完成函数的定义
void Swap(T& a, T& b)
{
	T temp;
	temp = a;
	a = b;
	b = temp;
}

常用的做法是将模板放置在头文件中,并在需要使用模板的文件中包含该头文件。

(7)显示具体化

​ 使用模板能够极大的精简我们的代码量,尤其是在容器类或者一些通用函数中。但是,模板不能帮助我们解决所有问题。有时候我们需要针对特定的场景提供一个特殊的函数(或者类)定义,这个就是具体化。要注意实例化的具体化的区别,可以从编译器的角度来看这两者之间的区别,实例化是指编译器生成特定函数(或者类)定义;而具体化可以看做是一个特殊版本的用来生成函数(或者类)定义的方案,它只是一个方案,因此编译器不会生成定义。

​ 显示具体化机制(explicit sepcialization)提供一个具体化的函数定义,当编译器找到与函数调用匹配的具体化定义时,将使用该定义,而不再寻找模板。总的来说,如果有多个原型,则编译器在选择原型时,非模板版本优先于显示具体化版本和模板版本;而显示具体化版本又优先于模板生成的版本。优先级:普通函数>显式具体化>显式实例化>普通模版

​ C++采用的显示具体化方法的做法:

1、对于给定的函数名,可用有非模板函数、模板函数和显示具体化模板函数以及它们的重载版本。

2、显式具体化的原型和定义应该以template <>打头,并通过名称来指出类型。

3、具体化优先于常规模板,而非模板函数优先于具体化和常规模板。

具体化的写法示例:

struct ST;
/* 显示具体化的原型和定义应以template <> 开头 */
template <> void Swap<ST>(ST & st1, ST & st2);			//一个具体化的函数原型,这个函数设计用于ST结构体

(8)实例化和具体化

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

编译器自动使用模板生成函数定义称为隐式实例化

当我们手动直接命令编译器创建特定的实例称为显式实例化

对于某些特殊类型,可能不适合模板实现,需要重新定义实现,此时就是使用显示具体化的场景这种用途称为显示具体化

隐式实例化是编译器自动使用模板生成函数的实例,所有隐式实例化无需特定的语法。

template <typename T>
void Swap(T& a, T&b);			//隐式实例化自动生成函数定义

显式实例化其语法是声明所需的种类——用==<>符号指示类型,并且声明前加上关键字template==

template void Swamp<int> (int& a, int& b);		//显示实例化,生成了指定特定参数的函数定义,这里为int类型

显式具体化其语法是声明所需的种类——用==<>符号指示类型,并且声明前加上关键字template <>==

struct job;
template <> void Swamp <job>(job& a, job& b);	//显式具体化,为专门的类别job显示进行函数定义

小结:从含义上,显示具体化告诉编译器不要使用Swap()的函数模板来生成函数定义,而使用专门的job类型显式地定义函数定义 是为了提供一个具体化的函数定义,其中包括所需的代码,当编译器找到与函数调用匹配的具体化定义时,将使用该定义,而不再寻找模板,而显示实例化则是生成了一个指出特定参数类型的实例函数

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

木心

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值