[函数探幽]——C++ Prime Plus ch8

本文深入探讨C++中的内联函数、引用变量、默认参数、函数重载及模板等高级特性,详细讲解各种概念的应用场景与注意事项。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

①C++内联函数

1.介绍

对于常规函数的调用,程序会在函数调用后立马存储该指令的内存地址,并将函数参数复制到堆栈中,跳到标记函数起点的内存单元,执行函数代码,然后跳回到地址被保存的指令处,来回跳跃的并记录条约位置意味着需要一定的时间成本。

内联函数则是使用相应的函数代码替换函数调用,无需进行程序的跳转,因此,内联函数的运行速度比常规函数快,但代价是需要占用更多内存。所以应该有选择地使用内联函数。如果代码执行时间很短并且函数经常被调用,此时在使用内联函数。

2.内联函数的使用:

在函数声明前加上关键字 inline;在函数定义前加上关键字inline。

通常的做法是省略原型,将整个定义放在本应该提供原型的地方。

程序员请求内联函数时,编译器不一定会满足这种要求,它认为函数过大或者自己调用自己(内联函数不能递归)时,不将其作为内联函数。

#include<iostream>
inline double square(double x) { return x * x; }   //内联函数
int main()
{
	using namespace std;
	double a, d;
	double c = 13;
	a = square(5.0);
	d = square(3.0 + 2.0);
	cout << "a = " << a << endl;
	cout << "d = " << d << endl;
	return 0;
}

内联函数与常规函数一样,同样是值传递来进行参数的传递。

3.内联函数与宏定义区别

宏定义则是通过文本替换,而不是值传递,如果使用C语言的宏执行了类似函数的功能,应考虑将他们转换为C++内联函数。

②引用变量

1.介绍

引用的已定义的变量的别名,引用变量的主要用途是作为函数的形参,通过将引用变量作为参数,函数将使用原始数据,而不是其副本。这样除指针之外,引用也为函数处理大型结构提供了一种非常方便的途径。

2.引用变量的创建

int rats;
int & rodents = rats;   //引用变量的创建
int *prats = &rats;

&不是地址运算符,而是类型标识符的一部分,int & 指的是指向int的引用,即rats 和 rodents指向相同的值和内存单元,将rodents加1将影响两个变量。

rodents = rats = *prats;        prats = &rats =&rodents;

引用变量必须在声明引用时将其初始化(同const类型),而不能像指针那样,先声明,再赋值。

int rats =101;
int & rodents = rats;   //引用变量的创建
int *prats = &rats;
int bunnies = 50;
rodents = bunnies;         //可以吗?

rodents = bunnies 本质上是 rats = bunnies;不管怎么样,rodents永远效忠于rats.

3.将引用用作函数参数

引用经常被用作函数参数,使得函数中的变量名称为调用层序中的变量的别名,这种传递参数的方法称为按引用传递。按引用传递的函数能够访问调用函数中的变量。换句话说,传递引用时,函数可以使用原始数据。

#include<iostream>
void swap(int& a, int& b);  //引用传递
int main()
{
	using namespace std;
	int a = 1;
	int b = 10;
	swap(a, b);
	cout << "a = " << a << endl
		<< "b = " << b << endl;
	return 0;
}

void swap(int& a, int& b)
{
	int temp;
	temp = a;
	a = b;
	b = temp;
}

在使用引用参数时,一定要记得参数信息被修改了,(值传递不会有这个问题)因此,如果程序员的意图是让函数使用传递给它的信息,而不对这些信息进行修改,同时又想使用引用,则应使用常量引用,在函数原型中函数头使用const;

void swap(const int& a,const  int& b)

这样声明后,如果编译器发现修改了a和b的值,将生成错误信息。

此外,如果要编写使用基本数值类型作为参数的函数,应采用按值传递的方式,而不要采用按引用传递的方式。当数据量比较大(如结构体和类)时,引用参数会很有用。

按值传递的函数,可使用多种类型的实参(左值,非左值,表达式),而对于引用传递的函数,传递引用的限制很严格(因为引用是一个变量的别名,则实参应该是变量)。如果实参和引用参数不匹配,C++将生成临时变量(当且仅当参数为const引用时才会这样),或者生成错误信息。

当实参的类型正确,但不是左值(不是变量);实参的类型不正确,但是可以转换为正确的类型(是变量,但是数据类型不匹配)。

左值:可被引用的数据对象(变量,数组元素,结构成员,引用和解引用的指针等),非左值包括字面常量(用括号引起来的字符串除外,它们由其地址表示)和包含多项的表达式。

编译器生成一个临时匿名变量,并让引用参数指向它,这些临时变量只在函数调用期间存在,此后编译器便将其删除,但是临时变量的存在导致参数的修改修改的是匿名变量的值,而不是传递进去的变量值,所以只有const引用变量才会有创建临时匿名变量的情况,否则会生成错误信息。

将引用参数声明应该尽可能的使用const:

1.使用const可以避免无意修改数据的编程错误;

2.能够处理const和非const实参,否则只能接受非const数据;

3.能够使函数正确生成并使用临时变量。

C++11新增了右值引用,是使用&&声明的,第18章讨论如何使用右值引用来实现移动语义。对于&声明的引用称为左值引用。

4.将引用用于结构

1.介绍:

引用非常适合用于结构和类,使用结构引用参数的方式与使用基本变量的引用相同,只需要在生命结构参数前使用引用运算符即可。

void display(const free_throws& ft)
{
	std::cout << "name:" << ft.name << std::endl
		<< "Made:" << ft.made << std::endl
		<< "Attempts:" << ft.attempts << std::endl
		<< "Percent" << ft.percent << std::endl;
}

void set_pc(free_throws& ft)
{
	if (ft.attempts != 0)
		ft.percent = 100.0f * (float)(ft.made) / (ft.attempts);
	else
		ft.percent = 0;
}

free_throws& accumulate(free_throws& target, const free_throws& source)
{
	target.attempts += source.attempts;
	target.made += source.made;
	set_pc(target);
	return target;                   //返回指向结构体的引用
}

2.返回引用

accumulate(dup,five) = four(同类型结构体)

accumulate(dup,five);
dup = four;

这条语句将值付给函数调用,这是可行的,因为函数返回了一个引用。本质相当于底下的两条代码。

为什么要返回引用?

如果不返回引用,同样的返回值需要复制到一个临时位置,调用程序使用的是临时值,返回引用则直接把数据给调用程序使用,提高了效率。

返回引用需要注意的问题:

不能返回临时变量的引用(注意引用创建临时变量),所以引用的类型和左值必须都要对应,为了避免这种情况的发生,将作为参数的引用进行返回。另一种方法是用new来分配新的存储空间。

注意:是不能返回指向临时变量的引用,而不是不能返回临时变量。临时变量被return后才销毁,所以是可以的,但是指向临时变量的引用,本体都没有了,指向它的复制体怎么可以存在呢?

为什么要将const用于引用返回类型?

假设要引用返回值,但又不允许像上边赋值情况的出现,只需要将返回类型声明为const引用。

5.将引用用于类对象

string version1(const string & s1, const string & s2)
{
	string temp;
	temp = s2 + s1 + s2;
	return temp;                   //对的,先返回,返回被调用程序保存了,后销毁
}

string& version2(const string & s1, const string & s2)
{
	string temp;
	temp = s2 + s1 + s2;
	return temp;                          //错的,因为temp指向的内存被释放了,temp的存在没有了意义
}

C-风格字符串用作string对象引用参数:

如果形参类型是 const string &,再调用函数时,使用的实参可以为 string对象 或C风格字符串(字符串字面量、以\0结尾的char数组,指向char的指针变量)。

6.对象、继承和引用

继承:将特性从一个类传递给另一个类的语言特性被称为继承,简单地说,ostream是基类,ofstream是派生类,派生类继承了基类的方法。

继承的另一个特性是,基类引用可以指向派生类对象,而无需进行强制类型转换。现实意义是,可以定义一个接受基类引用作为参数的函数,调用该函数时,可以将基类对象作为参数,也可以将派生类对象作为参数。

7.何时使用引用参数

使用引用参数的主要原因有两个:

①程序员能够修改调用函数中的数据对象

②通过引用传递而不是值传递,可以提高程序运行速度(数据对象较大的时候,第二个原因比较重要)

指导原则:

①对于传递的值而不作修改的函数:

1.数据对象很小,如内置类型或小型结构: 按值传递;

2.数据对象是数组:唯一选择,指向const数组的指针;

3.数据对象是较大的结构: const指针或const引用;

4.类对象: const引用(传递类对象参数的标准方式是按引用传递)

②对于修改调用函数中的数据:

1.数据对象很小: 使用指针,因为很明显提醒这是传递地址,变量内容会修改;

2.数据对象是数组:唯一选择,指针;

3.数据对象是结构:指针或引用;

4,类对象:引用

③默认参数

默认参数指的是函数调用中省略了实参时自动使用的一个值。如果有实参输入,实参会覆盖掉默认值。必须通过函数原型来设置默认值,这是因为编译器通过查看原型来了解函数所使用的参数数目。

char* left(const char*str,int n = 1);

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

实参从左到右的顺序依次被赋给相应的形参,而不能跳过任何参数。

只有原型指定了默认值,函数定义与没有默认参数时完全相同。

#include<iostream>
char* left(const char* str, int n = 1);
int main()
{
	const int SIZE = 80;
	using namespace std;
	char str[SIZE];
	char* pt;
	cout << "Enter a string:\n";
	cin.get(str, SIZE);
	pt = left(str, 4);      //给定参数
	cout << pt << endl;
	pt = left(str);        //默认参数
	cout << pt << endl;
	delete[] pt;
	return 0;
}

char* left(const char* str, int n)
{
	int len = strlen(str);
	n = (len > n)?n: len;          //防止浪费空间
	char* pt = new char [n + 1];
	int i = 0;
	while (i < n && str[i])
	{
		pt[i] = str[i];
		i++;
	}
	pt[i] = '\0';
	return pt;
}





//结果
//Enter a string:
//forthcoming
//fort
//f

④函数重载

默认参数能够让我们使用不同数目的参数调用同一个函数,而函数多态(函数重载)能够使用多个同名函数。可以通过函数重载来设计一系列函数——他们完成相同的工作,但使用不同的参数列表,C++使用上下文来确定所要使用的重载函数版本。

函数重载的关键是函数的参数列表——也成为函数的特征标。如果两个函数参数数目、类型和参数的顺序相同,则他们的特征标相同。是特征标使得函数可以重载,而不是函数类型(函数返回类型)。

使用被重载的函数时,需要在函数调用中使用正确的参数类型。如果没有匹配的原型并不会自动停止使用其中的某个函数,因为C++将尝试使用标准类型转换强制进行匹配。如果有多个参数列表都能够强制类型转换其对应的数据类型,C++将拒绝这种函数调用,并将其视为错误(我可以勉强匹配一个,有好多个我都能勉强匹配,那我就不干了)。

函数重载中,类型引用和类型本身视为同一特征标,并且 const和非const变量是两种不同的特征标(const标可以接受const 和 非const数据,非const标只能接受非const数据)。

如果参数都能适配多个函数,系统则会调用最匹配的版本。

当且仅当函数基本执行相同任务,但使用不同形式的数据时,才应该采用函数重载。而且不要忘了默认参数(默认参数相比函数重载,只需编写一个函数,程序也只需为一个函数请求内存,并且修改函数时,也只需要修改一个);然而,如果需要使用不同类型的参数,默认参数不管用,应该使用函数重载。

C++通过编辑器进行的名称修饰(名称矫正)来跟踪每一个重载函数。

#include<iostream>
char* left(const char* str, int n = 1);
unsigned long left(unsigned long num, unsigned n);
int main()
{
	const int SIZE = 80;
	using namespace std;
	char str[SIZE];
	char* pt;
	cout << "Enter a string:\n";
	cin.get(str, SIZE);
	pt = left(str, 4);      //给定参数
	cout << pt << endl;
	pt = left(str);        //默认参数
	cout << pt << endl;
	delete[] pt;
	unsigned long num = 123456;
	unsigned long out;
	out = left(num, 2);
	cout << out << endl;
	return 0;
}

char* left(const char* str, int n)
{
	int len = strlen(str);
	n = (len > n)?n: len;          //防止浪费空间
	char* pt = new char [n + 1];
	int i = 0;
	while (i < n && str[i])
	{
		pt[i] = str[i];
		i++;
	}
	pt[i] = '\0';
	
	return pt;
}

unsigned long left(unsigned long num, unsigned int n)
{
	unsigned temp = num;
	if (num == 0 || n == 0)
		return 0;              //空
	unsigned int i = 1;
	while (temp /= 10)     //不能为num,不然就为0 了
		i++;
	if (i > n)             //小于数字位数
	{
		n = i - n;
		while (n--)
			num /= 10;
		return num;
	}
	else
		return num;
}

⑤函数模板

1.介绍:

函数模板使用泛型来定义函数,其中的泛型可用具体的类型(如int 或 double)替换。通过将类型作为参数传递给模板,可使得编译器生成该类型的函数。

模板的建立:

template<typename T>  //<class T>
void Swap(T& a, T& b) //开始编写函数  用 T代替数据类型的位置
{
	T temp;
	temp = a;
	a = b;
	b = temp;
}

注:函数模板不能缩短可执行程序。调用两次函数模板,最终仍将由两个独立的函数定义,最终的代码不包含任何的模板,而只包含了为程序生成的实际函数。更常见的情形是将模板放在头文件中,并在需要使用模板的文件中包含头文件。

2.模板的重载:

需要对多个不同类型使用同一种算法的函数时,可使用模板。并非所有的类型都使用相同的算法。为满足这种需求,可以像重载常规函数定义那样重载模板定义,被重载的模板的函数特征标必须不同,并非所有的模板参数都必须是模板参数类型,也可以是确定的参数类型。

#include<iostream>
template<typename T>   //模板一定在前边
void Swap(T& a, T& b)
{
	T temp;
	temp = a;
	a = b;
	b = temp;
}

template<typename T>     //模板的重载
void Swap(T ar1[], T ar2[], int n)
{
	for (int i = 0; i < n; i++)
	{
		T temp;
		temp = ar1[i];
		ar1[i] = ar2[i];
		ar2[i] = temp;
	}
}

template<typename T>
void Show(T ar1[], int n)
{
	for (int i = 0; i < n; i++)
		std::cout << ar1[i] << "  ";
	std::cout << std::endl;
}
int main()
{
	using namespace std;
	const int SIZE = 10;
	int a, b;
	double c, d;
	int a1[SIZE] = { 1,2,3,4,5,6,7,8,9,10 };
	int b1[SIZE] = { 11,21,31,41,51,61,71,81,91,100 };
	double c1[SIZE] = { 1.0,2.0,3.0,4.0,5.0,6.0,7.0,8.0,9.0,10.0 };
	double d1[SIZE] = { 11.0,21.0,31.0,41.0,51.0,61.0,71.0,81.0,91.0,101.0 };
	a = 1;
	b = 10;
	c = 2.0;
	d = 20.0;
	cout << "a = " << a << ",b = " << b << endl << "c = " << c << ",d = " << d << endl;
	cout << endl;
	Swap(a, b);
	cout << "exchange:\n";
	cout << "a = " << a << ",b = " << b << endl << "c = " << c << ",d = " << d << endl;
	
	
	Show(a1, SIZE);
	Show(b1, SIZE);
	cout << "exchange:\n";
	Swap(a1, b1, SIZE);
	Show(a1, SIZE);
	Show(b1, SIZE);


	Show(c1, SIZE);
	Show(d1, SIZE);
	cout << "exchange:\n";
	Swap(c1, d1, SIZE);
	Show(c1, SIZE);
	Show(d1, SIZE);
	return 0;
}

3.模板的局限性:

函数模板中,通常假定了可执行哪些操作。比如代码规定了赋值操作、乘法操作等,但是对于结构体,数组来说,该函数并不成立,也就是说,编写的模板很可能无法处理某些类型,解决方案是为特定的类型提供具体化的模板。

4.显示具体化:

函数模板无法使用Swap()函数来交换结构体中某些成员的值,可以提供一个具体化函数定义——显示具体化,其中包含所需的代码。当编译器找到与函数调用匹配的具体化定义时,将使用该定义,而不再寻找模板。

如果有多个原型,编译器在选择原型时,非模板优先于显示具体化和模板版本,而显式具体化优先于使用模板生成的版本。

struct job
{
         char name[40];
         double salary;
         int floor;
};

//非模板函数

void Swap(job& a1,job& a2)   //可引用,可指针,这里是引用

//模板函数

template<typename T>
void Swap(T & a, T & b);

//显式具体化
template<> void Swap<job>(job & a1,job & a2);      //<job>可选,也就是说 显式具体化比非模板函数多了template<> 

5.实例化和具体化

5.1实例化

在代码中包含函数模板本身并不会生成函数定义,它只是用于生成函数定义的方案。当我使用模板,为模板传入数据类型参数时,编译器使用模板为我写类型生成函数定义,这时得到的是模板实例。实例化有两种形式,分别为显式实例化和隐式实例化。

 隐式实例化(implicit instantiation)

使用模板(进行调用),生成的函数定义就是隐式实例化。

显式实例化(explicit instantiation)
显式实例化意味着在调用前,直接命令编译器创建特定的实例,而不是等传入参数才知道要为模板传入什么数据类型(从而生成相关函数定义)。显示实例化声明方式:

比如存在这么一个模板函数

template <typename T>
void Swap(T &a, T &b)

第一种方式是声明所需的种类,用<>符号来指示类型,并在声明前加上关键词template,如下:
template void Swap<int>(int &, int &);

第二种方式是直接在程序中使用函数创建,如下:

Swap<int>(a,b);

显式实例化直接使用了具体的函数定义,而不是让程序去自动判断。

#include<iostream>
struct job
{
	char name[40];
	double salary;
	int floor;
};
template<typename T>
void Swap(T& a, T& b);
void show(const job j);
template<>void Swap<job>(job& a1, job& a2);

int main()
{
	using namespace std;
	int i = 10, j = 20;
	cout << "i = " << i << " j = " << j << endl;
	Swap(i, j);
	cout<<"now i = " << i << " j = " << j << endl;
	job Sue = { "Sue",675555.7,9 };
	job sidney = { "sidney",1111.1,3 };
	show(Sue);
	show(sidney);
	Swap(Sue, sidney);
	cout << "now\n";
	show(Sue);
	show(sidney);
	return 0;
}


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

template <> void Swap<job>(job& a1, job& a2)        //显示具体化
{
	double s;
	int f;
	s = a1.salary;
	a1.salary = a2.salary;
	a2.salary = s;
	f = a1.floor;
	a1.floor = a2.floor;
	a2.floor = f;
}
void show(const job j)
{
	std::cout << j.name << ": $ " << j.salary << " on floor " << j.floor << std::endl;
 }


5.2具体化


显式具体化将不会使用Swap()模板来生成函数定义,而应使用专门为该特定类型显式定义的函数类型。

区分: template<> ——具体化   没有<> ——实例化

警告:试图在同一个文件(或转换单元)中使用同一种类型的显式实例和显式具体化将出错。

6.编译器选择使用哪个版本

对于函数重载、模板重载和函数模板重载,C++需要一个定义良好的策略,来决定为函数调用选择使用哪一个函数定义,这个过程称为重载解析。大致过程如下:

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

②使用候选函数列表创建可行函数列表(参数数目正确,隐式转换不行的拿掉(char int float可以隐式转换的也要留下));

③确定是否有最佳可行函数。

如何确定最佳可行函数:

①完全匹配>②提升转换(char short转换为int, float转换为double)>③标准转换(int 转换为char,long转换为double)>④用户自定义转换(如类声明中的定义转换)

什么是完全匹配?

进行完全匹配的时候,C++允许某些无关紧要的转换,表8.1,Type可以是任何类型(引用,指针等),如果有多个匹配的原型,编译器无法完成重载解析,没有最佳的可行函数,编译器会生成一条错误信息。然而,有时候,即使两个函数都完全匹配,仍可完成重载解析。

1.对于指针和引用,指向非const数据的指针和引用优先于非const指针和引用参数匹配;

2.非模板函数优先于模板函数,如果两个都是模板函数,较具体的优先。最具体并不意味着显式具体化,而是指编译器推断使用哪种类型时执行的转换最少。如果有多个同样合适的非模板或模板函数,但没有一个函数比其他函数更具体,则函数调用是不确定的,因此是错误的,如果不存在匹配的韩式,则也是错误的。

创建自定义选择:

在有些情况下,可通过编写合适的函数调用,引导编译器做出希望的选择。

#include<iostream>
template<typename T>
T lesser(T a, T b);
int lesser(int a, int b);
int main()
{
	using namespace std;
	int m = 20, n = -30;
	double x = 15.5;
	double y = 25.9;
	cout << lesser(m, n) << endl;    //调用int
	cout << lesser(x, y) << endl;    //模板并且传入double
	cout << lesser<>(m, n) << endl;   //<>表明用模板
	cout << lesser<int>(x, y) << endl;    //显示实例化,强制转换为int
	return 0;
}
template<typename T>
T lesser(T a, T b)
{
	return a < b ? a : b;
}

int lesser(int a, int b)
{
	a = a < 0 ? -a : a;
	b = b < 0 ? -b : b;
	return a < b ? a : b;
}

对于多个参数的函数调用与有多个参数的原型进行匹配的时候,编译器必须考虑所有参数的匹配情况。一个函数比其他函数都合适,所有参数的匹配情况都必须不比其他函数差,同时至少有一个参数的匹配程度比其他函数都高。

7.模板函数的发展

问题:

template<class T1,class T2>
void ft(T1 X, T2 Y)
{
	xpy = x + y;
}

请问 xpy是什么类型呢?

解决方案(C++11)

1.关键字decltype:

int x;

decltype(x) y;    //使y有着和x一样的类型

给decltype提供的参数可以是表达式,也可以将语句合二为一:

    int x;

	decltype(x) y; //使y有着x一样的数据类型

	decltype (x + y) xpy;
	 xpy = x + y; 
	 //合并到一起
	decltype (x + y) xpy = x + y;

decltype比这些示例岩石的复杂些,为确定类型,编译器必须便利一个核对表,假设有如下声明:

decltype(expression) var;

第一步: 如果expression是一个没有用括号括起来的标识符,则var的类型与该标识符类型相同,包括const等限定符;

第二步:(第一步不满足)如果expression是一个函数调用,则var的类型与函数的返回类型相同(不需要调用,编译器看一眼函数原型就知道了);

第三步:(一二步都不满足)如果expression是一个左值,则 var为指向其类型的引用

注:expression是一个用括号括起来的标识符:


	double xx = 4.4;
	decltype((xx))  r2 = xx;

括号并不会改变表达式的值和左值性。

第四步:(前边三步都不满足)则var的类型与expression类型相同:

decltype(100L) i1;   //i1为long类型

2.另一种函数声明语法(C++11后置返回类型)

有一个问题是decltype无法解决的。

void ft(T1 X, T2 Y)
{
	
	...
	
	return x + y;
}

此时未声明x和y,无法使用decltype,为此,C++新增了一种声明和定义函数的方法。

double h(int x, float y);   //原来

auto h(int x,float y) -> double; //新增

double是后置返回类型,auto是一个占位符,表示后置返回类型提供的类型。可用于函数定义,也可通过结合使用这种语法和decltype指定返回类型。

auto ft(T1 X, T2 Y) ->decltype(X+Y)
{
	
	...
	
	return x + y;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值