C++学习 第八章

本文详细介绍了C++中的内联函数与普通函数的工作原理和使用方式,强调了内联函数可以提高效率但可能导致内存占用增加。接着,探讨了引用的概念,包括创建方式、特点及在函数参数传递中的应用。文中还对比了引用和指针传递的区别,以及在函数参数中使用引用的注意事项。此外,文章提到了C++中的默认参数、函数重载和模板等高级特性,以及如何利用这些特性来优化代码和提高程序效率。

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

1.C++的内联函数和普通函数区别

  • C++中普通函数的工作原理:
    当程序运行到调用函数的代码时,首先程序会在函数调用后的第一时间存储该指令的内存地址,然后通过程序告知的函数所在的内存地址,并且将函数的参数复制到堆栈中,随后跳转到函数起点所在的内存单元,执行完程序之后,如果由返回值,会将返回值放到指定的内存单元或cpu寄存器中,然后跳回到原来保存的内存单元,继续向后执行程序。跳转的过程由一定的开销
  • C++内联函数的工作原理:
    内联函数不再像普通函数那样,需要跳转到函数存放的单元,内联函数的编译代码和其他程序代码”内联“起来了。也就是编译器会使用对应的函数代码来代替函数调用,也就是说程序无需再通过记录内存位置,跳转,再跳转的方式来执行函数。而是直接执行代码即可。这样的执行方式会节省跳转以及找寻内存单元的时间。不过也可能导致内存占用空间变大,因为内联函数是使用的是函数代码,也就是说,无论程序有多少处使用此函数的地方,就一定有多少个函数副本在内存中。所以使用的时候要注意。

2.C++内联函数的使用方式

  • 在函数声明前加关键字 inline
  • 在函数定义前加关键字 inline
    通常的做法是省略函数原型,将原本放函数原型的位置放函数的整个定义

不过内联函数还有几点需要注意的地方:

  • 内联函数不能使用递归
  • 内联函数不建议(不能)过大
#include<iostream>
using namespace std;
inline double square (double num) { return num*num; }
int main()
{
double x = 5.0;
cout << square(x) << endl;
return 0;
}

3.引用变量的定义与创建
引用变量是一种复合类型的变量,引用是一定义的变量的别名。下面介绍创建方式:

//复习
// 地址运算符& 在之前是用来得到目标变量的地址而使用的
// 而今天,&将被赋予另一项权能,那就是声明引用

//创建引用变量
int i;
int &b = i;
//这里的&与*一样 代表类型说明符
//*代表声明对象是一个指针
//而&代表声明的对象是一个引用

引用变量和原变量指向相同的值和内存单元,所以,简而言之,引用变量就好像是一个别名,就好像你大名叫王二狗,小名叫铁柱一样,他们都代表了你这个人。
下面的程序也说明了这一点:

#include<iostream>
using namespace std;
int main()
{
	int wangergou = 52;
	int &tiezhu = wangergou; //从现在开始铁柱就是二狗的小名了
	cout << wangergou << endl;
	cout << tiezhu << endl;
	tiezhu++;
	cout << wangergou << endl;
	cout << tiezhu << endl;
	return 0;
}

注意:必须在声明引用的时候将他初始化。而不能在之后初始化。
引用更加接近const指针,必须在创建的时候进行初始化,而且一旦指向了一个变量,就不能再改变。(意外的忠贞)

int boy1 = 250;   
int * son = &boy1;  // ①
int &boy1_anotherName = *son ;    // ②
int boy2 = 521;
son = &boy2;  //③
//首先①中声明了一个指针son,指向boy1
//也就是*son和boy1拥有同样的数据,指向同样的地址
//换句话说 son 和 boy1是一个人 
//然后语句②中声明了一个引用变量boy1_anotherName 指向了 *son
//目前的*son 代表的是 boy1 他们拥有相同的内存地址和值
//那么此时boy1_anotherName 也拥有了和boy1以及son相同的内存地址
//而且根据引用特性 这句话与 int* const boy1_anotherName = *son; 同义
//也就是说 boy1_anotherName 永远的和 代表boy1或者son的这块内存地址相连
//最后③中,son指针指向了boy2,也就是son舍弃了原来的内存地址
//son拥有了新的内存地址,和新的数值
//而boy1_anotherName依然代表的是原来的内存地址
//代表的是boy1和250

4.将引用用作函数参数
引用变量常用于函数的参数传递之中,将引用变量作为参数传递,使得函数名调用的是程序中变量的别名,这种传递方法是C++较C语言新增加的特性—按引用传递。

//编写一个按引用传递的函数
//函数原型
void swap (int & a, int & b);
//函数体
void swap (int &a, int &b)
{
	int temp;
	temp = a;
	a = b;
	b = temp;
}
  • 为什么要使用按引用传递?
    在C++的按值传递函数中,被调用函数实际上使用的是参数的副本,也就是无法对于原来参数进行修改,这样导致需要修改原参数的函数束手无策。当然可以通过指针的方式进行修改,所谓的指针传递也是一种按值传递,只不过传递的值变成了指针值,也就是地址。而传递过去的地址值,就可以通过函数找到所需修改的值的地址,然后对值进行修改了。而引用传递也可以如同指针那样,完成修改原参数的工作。

5.指针传递和引用传递的区别

  • 声明函数参数的方式不同
  • 使用指针传递的参数时,需要使用解除引用运算符

6.引用的属性和特别之处
举个小例子:

//伪代码
double x = 3.0;
cout << square (x) << x ;

double square (double &x)
{
	return x *= x * x;
}

在这种情况下,输出的结果将会是27,27,也就是不光结果值会输出,而且x的值会随之改变,这也就是引用变量的特点。但是有时候我们并不希望这个值改变,最好的办法当然是将他生成为按值传递的函数,不过可能会有一些情况确实要使用到引用和变量,那么我们就要把他生成为const类型的引用变量。

//一个好办法
double square (double x)
{
	return x *= x * x;
}
//仍然要使用引用变量
double square (const double &x)
{
	return x *= x * x;
}

7.引用变量的参数

//在值传递中
square(x+2.0);
square(x*3);
//这样的语句都是允许的

//但是在引用传递中并不是如此
//因为引用传递的内容相当于是一个变量
square(x+2.0);
//这样的代码不合理也很好说明
//因为x+2.0并不是一个变量

//但是在早期的C++中这种语法是允许的(现在不允许)

在早期的C++中,遇到引用变量的(x+2.0)这种表示方法,系统判断出x+2.0并不是属于double类型的变量,那么系统会生成一个无名变量,其将其初始化为x+2.0表达式的结果。然后x将成为该临时变量的引用。下面我们来介绍一下什么时候会创建这种临时变量,什么时候不会创建。

8.临时变量,引用参数 和 const
如果实参和引用参数不匹配,C++会生成临时变量,目前,仅当const作为参数引用时,才会允许这么做。那么const作为引用参数时,什么情况会生成临时变量呢?

  • 实参的类型正确,但不是左值
    左值:左值参数是可被引用的数据对象,例如:变量,数组元素,结构成员,引用和解除引用的指针。【const和常规变量都可以看作左值,因为都能通过地址找到他们】
    非左值:包括字面常量(用括号扩起的字符串除外,他们由地址表示)和包含多项的表达式。

  • 实参的类型不正确,但是可转换为正确的类型

  • 为什么C++会禁止非const类型作为引用参数时的临时变量呢?
    因为当C++发现实参和引用参数不匹配的时候,C++会创建临时变量,而让引用参数指向新建成的临时变量,这导致引用参数和原本要使用的参数之间断开联系,也就是说,对于引用参数的修改不能传递会给我们的实参,这可能会导致要修改原参数的函数出现错误。所以C++后来禁止了这种情况下临时无名变量的产生。

  • 那为什么仍保留着const类型作为引用参数时的临时变量?
    因为const类型属于常量,他的值不允许被修改,所以就排除了需要修改实参这种情况,所以临时变量不会造成任何不好的影响,甚至还可以使能够传递的参数种类更加通用。

应该尽可能使用const

  • 使用const可以避免无意修改数据的编程错误
  • 使用const使函数能够处理const和非const实参,否则只能接受非const数据
  • 使用const引用使函数能正确生成并使用临时变量

9.右值引用 &&
之前我们的引用方法成为左值引用,顾名思义,是用来对左值进行引用的,左值的概念我们以及清楚了,下面就举几个小例子来形象理解一下:

//左值引用
//左值包含 引用和解除引用的指针 变量 const常量 数组元素 结构成员
//也就是可以被引用的数据对象
//所有有名字的量都是左值
int i; //i是左值
int *p; //*p是左值
const int i; //i是左值
int i[20]; //i[0],i[1]...是左值
string str = "foo";//str也是左值
//对于左值可使用左值引用
int & b = i; //允许
int & c = *p; //允许
int & d = c; //允许
int & e = i[2]; //允许
//左值引用常用来向函数传递比较大的参数,或者是可以进行修改的参数

//右值引用
//右值包括含有多项的表达式,字面常量
int i = 2; //这里的2是右值
int i = x + y; //x+y也是右值
//右值不允许使用左值传递
int &a = 2; //不允许
int &a = x + y; //不允许
//C++11中加入了右值引用,用来对右值进行引用
int &&a = 2*x + y; //允许
//右值引用常用于移动语义

10.引用用于结构中

//引用用于结构之中
#include<iostream>
#include<string>
using namespace std;
struct student
{
	string name;
	int made;
	int attempts;
	float percent;
};
void display (const student & s);
void set_pc (student & s);
student & sum_pc (student & s1, const student & s2);
int main()
{
	//创建了五个学生变量
	//percent成员没有赋值,系统自动填零
	student stu1 = {"shiyuqi",1,1};
	student stu2 = {"gaofengxuan",5,1};
	student stu3 = {"wangziyi",10,2};
	student stu4 = {"jinshuyu",50,5};
	student stu5 = {"wangzihao",20,3};
	student team = {"team",0,0};
	//定义一个空的student结构变量
	student dup;
	
	set_pc(stu1);
	display(stu1);
	sum_pc(team,stu1);
	display(team);
	//注意这里使用sum_pc()函数的返回值作为display的参数
	//已知display的参数类型应该为student的引用
	//所以sum_pc返回值的类型也是引用类型
	//那么为什么使用引用类型作为返回值呢
	//返回结构这种比较占用内存的类型时,使用引用可以有效的节约空间和时间
	//如果返回的是结构本身的话,函数需要先将返回的结构复制到指定内存或寄存器中
	//然后需要使用该值的时候再去该位置寻找这个值,然后使用
	//而返回引用类型就相当于直接将值赋给引用的实参,而不需要一个中间过程
	//还需要注意的一点是,返回的内容究竟是谁?
	//因为返回的内容是引用,所以返回值就相当于被引用的那个变量
	//本程序中就是team
	//所以本语句相当于 sum_pc(team,stu2); display(team);合二为一
	display(sum_pc(team,stu2));
	//同上
	sum_pc(sum_pc(team,stu3),stu4);
	display(team);
	//函数赋值,这里传给dup的值就是函数的返回值
	//之前我们也说过,这个函数的返回值是引用
	//所以这条语句相当于是 sum_pc(team,five); dup = team;
	dup = sum_pc(team,five);
	//这条语句乍一看就是一条错误语句
	//因为我们知道,值是不能反向给变量传递的
	//也就是常量不能作为左值
	//然而注意本返回值的类型并不是常量
	//而是引用,也就是说返回的内容其实就是due
	//所以本语句在语法上是合理的,但是逻辑上无法说通
	//本语句相当于是sum_pc(due,five);  due = four; 合二为一
	//也就是说计算好的due值又被four覆盖掉了
	//这就使函数的计算值被淹没 所以逻辑上不能说得通
	sum_pc(dup,five) = four;
	display(due);
	return 0;
}
void display (const student & s)
{
	cout << s.name << endl;
	cout << s.made << endl;
	cout << s.attempts << endl;
	cout << s.percent << endl;
}
void set_pc (student & s)
{
	if (s.attempts !=0)
		s.percent = 100f * float(s.made) / float(s.attempts);
	else
		s.percent = 0;
}
student & sum_pc (student & s1, const student & s2)
{
	s1.made += s2.made;
	s1.attempts += s2.attempts;
	set_pc(s1);
	return s1; 
}

将引用作为返回值需要注意的事情:

  • 避免返回函数终止时不再存在的内存单元引用
    举个栗子
const student & clone(student & s)
{
	student s1;
	s1 = s;
	return s1;
}

本函数返回的时一个临时变量的引用,但是函数运行结束之后,函数内定义的变量生存周期已经结束,变量会被销毁,所以这个引用不能再被使用。
为了解决这个问题,最简单的办法就是返回一个作为参数传递给函数的引用。作为参数的引用将指向调用函数使用的诗句,因此返回的引用也指向这些数据。
还有一种方式就是使用动态存储,通过new分配内存空间,如下例:

const student & clone(student & s)
{
	student *pt = new student;
	*pt = s;
	return pt;
}

不过使用这种方式的时候,一定不能忘记不用的时候将该变量delete如果忘记可能会导致内存泄漏。不过这种隐式调用很可能忘记delete,以后会介绍解决方法。

  • 为什么将const用于引用返回类型
//还记得吗
sum_pc(dup,five) = four;

这个荒诞的语句在语法上是可行的,在赋值语句中,左边的值必须是可以修改的左值,也就是说,赋值语句中位于左边的子表达式必须标识一个可修改的内存块。而本函数的返回值是一个引用,而这种引用确实就是这样一个可修改的内存块,所以这条语句是合法的。
而在常规函数中,返回值的类型是右值,这种表达式只能在赋值表达式的右边,其他右值还包括字面值和表达式,之所以常规函数返回的值是右值,是因为他所在的位置是一个临时内存单元,一旦执行到下一个语句,他就可能灰飞烟灭,所以他的地址没有意义。
如果您需要使用返回值,又不希望出现上面代码那种荒诞的行为,您可以将返回值类型声明为const,const类型虽然是左值,但是他并不允许被修改,属于不能修改的左值,这种值不能作为赋值语句的左子表达式。

11.将引用用于类对象
C++中可以使用引用,让函数把类的对象作为参数。

//函数原型
string link (const string & s1, const string s2);
const string & link2 (string & s1, const string s2);
const string & link3 (string & s1, const string s2); //错误的
int main()
{
	string str = "shiyuqi";
	link(str," love! ");
	link2(str," love! ");
	link3(str," love! ");
	return 0;
}
//函数体
string link (const string & s1, const string s2)
{
	string temp;
	temp = s2 + s1 + s2;
	return temp;
}
const string & link2 (string & s1, const string s2)
{
	s1 = s2 + s1 +s2;
	return s1;
}
// 失败了 知道为什么失败吗?
// 一个临时变量 准备返回啦
// 可能下一秒就灰飞烟灭了 程序也找不到他 还想成为引用
// 程序肯定崩溃鸭
const string & link3 (string & s1, const string s2)
{
	string temp;
	temp = s2 + s1 + s2;
	return temp;
}

注意:如果参数类型是const string & ,调用函数时实参可以是string对象或者C风格字符串

12.对象 继承 和引用
还记得我们之前文本文件处理的时候学到的ofstream,ifstream和之前我们学习控制台输入用到的istream和ostream嘛。正如之前所说的,ofstream对象可以使用ostream类的方法,这使得文件输入/输出格式和控制台输入/输出格式相同。
注意:使得能够将一个特性从一个类转移到另一个类的语言特性叫做继承。
继承的特征:

  • 使得能够将一个特性从一个类转移到另一个类
  • 基类引用可以指向派生类对象,无需进行强制转换

也就是说,我们可以定义一个接受基类引用作为参数的函数,调用该函数时,可以将基类对象作为参数,也可以将派生类对象作为参数。
例如:参数类型为 osteam & 的函数可以接受ostream对象也可以接受ofstream对象

13.何时使用引用参数

  • 程序员能够修改调用参数中的数据对象
  • 通过传递引用而不是整个数据对象 可以提高程序的运行速度。

14.如果不需要修改参数的指导原则

  • 如果数据对象很小,如内置数据类型或小型结构
  • 如果对象是数组,则使用指针,并将指针声明为const类型
  • 如果数据对象是很大的结构,则使用const指针或者const引用
  • 如果数据对是类对象。则使用const引用

15.如果需要修改参数的指导原则

  • 如果数据对象是内置数据类型,则使用指针。
  • 如果数据对象是数组,则只能使用指针
  • 如果数据对象是结构,使用指针或者数组
  • 如果数据对象是类对象,则使用引用

16.默认参数
所谓的默认参数就是当函数调用省略了实参时默认使用的一个值。
注意:对于带参数列表的函数,必须从右向做添加默认值,也就是如果要给某个参数添加默认值,则必须给他右侧的参数也添加默认值
注意:实参按从左到右的顺序以此被赋给相应的形参,而不能跳过任何参数,因此有一些调用是不允许的

//默认参数的使用方法
//函数原型
char * left (const char * str , int n = 1);
//添加默认值
int harpo (int n, int m = 4, int j =5); //允许
int chico (int n, int m = 4, int j); //不允许
//调用参数
int harpo (int n, int m = 4, int j =5); //允许
beeps = harpo(2); //允许
beeps = harpo(2, ,8); //不允许

17.函数重载
函数重载可以让您的程序中拥有很多的同名函数,它是多态性的一种体现,多态是面向对象的三大特性之一。

  • 实现函数重载的方式
    函数的参数列表—也叫函数特征标,当两个函数的参数数目相同,而且参数类型也相同,同时参数排序还相同,则他们的特征标相同,否则他们的特征标不同。
    而定义名称相同的函数,条件就是他们的特征标不同。
//定义重载函数
//因为五个函数的特征标不同,所以可以定义为重载函数
void print (const char * str, int width);  //#1
void print (double d, int width); //#2
void print (long l, intd width); //#3
void print (int i, int width); //#4
void print (const char * str); //#5
  • 函数重载需要注意的情况1
unsigned int year = 3210;
print(year,6);

注意:使用被重载的函数时,需要在函数调用中使用正确的参数类型。print()调用时不与任何原型匹配,但是C++编译器并不会对print()的调用进行停止,C++将尝试用标准类型转换强制进行匹配,如果#2是print()唯一的原型,则会将year转换为double类型,然后执行。但是上面的代码中右三个将数字作为第一个参数的原型,则有三种转化方式,这种情况下,C++会拒绝调用,并且视作错误情况。

  • 函数重载需要注意的情况2
double show (double x);
double show (double & x);
// 矛盾点
double x = 10;
cout << show(x); 

这种情况不能作为函数重载,因为如果这种情况允许被看作函数重载的话,下面矛盾点中的show()函数并不能确定使用的是哪个函数原型,会导致程序出现紊乱,所以这种重载是不允许的。

  • 函数重载需要注意的情况3
    是特征标,而非函数类型使得函数可以进行重载。
double print (double a);
int print (double a);

以上两个声明是互斥的,只有返回值类型不同,参数列表相同的函数不构成重载。
但是,函数重载允许返回值类型不同:
返回值不同,特征标也不同的两个函数构成函数重载

double print (double a);
int print (int a);

18.重载的引用参数

//重载函数的引用参数
//#1
void sink (double & r1);       //左值引用参数,和可修改的左值相匹配
//#2
void sink (const double & r2); //const左值引用参数,和可修改左值,const左值和右值参数匹配
//#1
void sunk (double && r1);      //右值引用参数,和右值相匹配
//#2
void sunk (double & r2);       
//#3
void sunk (const double & r3); 

double x = 55.5;
const double y = 65.0;
sink(x); //调用的 #1
sink(y); //调用的 #2
sink(x+y); //调用的 #2
sunk(x); //调用的 #1
sunk(y); //调用的 #2
sunk(x+y); //调用的 #3

注意:如果重载使用这三种函数,将会调用最匹配的版本。

19.使用重载函数的时机
当函数基本上执行相同的任务,但是使用不同格式的数据时,应使用函数重载。

20.名称修饰
C++如何跟踪每一个重载函数呢?他会给每个函数指定秘密身份,他会根据函数原型中指定的形参类型对每个函数名进行加密,对参数数目和类型进行编码,添加的一组符号随函数特征标而异。

21.函数模板定义
函数模板是通用的函数描述,他们使用泛型来定义函数,其中的反省可以用一些具体类型替换,通过将类型作为参数传递给模板,可使编译器生成该类型的参数。模板特性也被成为参数化类型。

//函数模板允许以任意类型的方式来定义参数
template <typename AnyType>
void Swap (AnyType & a, AnyType & b)
{
	AnyType temp;
	temp = a;
	a = b;
	b = temp;
}

//旧版本使用class的实现
template <class AnyType>
void Swap (AnyType & a, AnyType & b)
{
	AnyType temp;
	temp = a;
	a = b;
	b = temp;
}
  • 使用函数模板的方式
    1.第一行指出,要建立一个模板,关键字template和typename是必须的,除非可以使用关键字class代替typename。另外必须使用尖括号<>,类型名可以随意选择,遵守命名规则即可。
    2.余下的代码描述了一个交换顺序的算法,模板并不会创建任何函数,只是会告诉编译器如何定义函数,需要交换double参数时,编译器按照模板创建函数,并且将其转换为double类型,即使用double代替AnyType。
    3.如果不考虑向后兼容的问题,并且愿意输入较长的单词,建议使用typename。
  • 模板与重载
    需要对多个不同类型的参数使用同一种算法的时候可以使用模板。然而并非所有的类型都使用相同的算法,这时候我们也可以重载模板函数,就像重载普通函数那样。
template<typename T>
//两个模板函数构成重载
void swap(T & a, T & b);
template<typename T>
void swap(T a[], T b[], int n);

template<typename T>
void swap (T & a, T & b)
{
	T temp;
	temp = a;
	a = b;
	b = temp;
}
template<typename T>
void swap(T a[], T b[], int n)
{
	T temp;
	for(int i = 0; i < n; i++)
	{
		temp = a[i];
		a[i] = b[i];
		b[i] = temp;
	}
}

22.函数模板的局限性与具体化
由于泛型可以支持所有类型的数据,不过数据和数据之间的类型不同,一些符号也并不通用。例如我们并不能将数组进行 a = b;的这种操作。我们并不能对结构进行 a > b的这种表达式的书写。有一些表达式可能语法上满足要求,但是结果也并不是我们所想要的。为了解决函数模板局限性的这个问题,我们可以重载运算符,也可以使用为特定类型提供具体化的模板定义的方法。

  • 显式具体化
//定义一个小结构
#include<string>
struct student
{
	string name;
	int num;
	double score;
};
//如果我想将结构中的num和score两部分内容进行交换位置,使用原来的swap()是没办法实现的
//而我们的参数列表也没有任何改变,所需的参数还是两个student类型变量
//此时我们就可以通过显式具体化的方式
//提供一个具体化的函数定义,其中包含所需的代码,当编译器找到与函数调用匹配的具体化定义
//则调用该定义,不再寻找模板
  • 具体化方法
    1.对于给定的函数名,可以有非模板函数,模板函数和显示具体化模板函数以及他们的重载版本。
    2.显示具体化的原型和定义应该以template<>开头,通过名称来指出类型
    3.具体化优先于常规模板,非模板函数优先于具体化和常规模板。
//student交换num和score的具体化模板
//用于交换job结构的非模板函数,模板函数,具体化的原型
//非模板
void Swap(student & , student &);
//模板
template<typename,T>
void Swap(T & , T &);
//具体化
template <> void Swap<student>(student  &, student &);
//优先级:非模板 > 具体化 > 模板
//具体化的函数体
template <> void Swap<student>(student & stu1, student & stu2)
{
	int t0;
	double t1;
	t0 = stu1.num;
	stu1.num = stu2.num;
	stu2.num = t0; 
	t1 = stu1.score;
	stu1.score = stu2.score;
	stu2.score = t1; 
}

23.实例化与具体化

  • 隐式实例化
    在代码中包含函数模板本身并不会生成函数定义,它只是一个用于生成函数定义的方案。编译器使用模板为特定类型生成函数定义时,得到的是模板实例,例如函数调用Swap(i,j)会导致编译器生成Swap的模板实例,实例为int类型。模板不是函数定义,但是使用int的模板实例是函数定义,这种实例化方式成为隐式实例化。
  • 显式实例化
    也就是意味着程序员可以直接命令编译器创建特定类型的实例。语法为
template Swap<int>(int, int); //命令编译器生成int类型实例
//是不是感觉和显示具体化很相似,下面我们来看看二者区别
  • 显式实例化和显式具体化的区别
    1.显式具体化的template后面有<>而显示实例化后面没有。
    2.显式具体化的意思是不要使用Swap()模板来生成函数定义,而要使用专门为int类型显式的定义的函数定义。而显示实例化是生成一个指定类型的函数实例。
    3.不应该在同一个文件中同时使用显式实例化和具体化。
//显式实例化
template void Swap<int>(int, int);
//显式具体化
template<> void Swap<int>(int, int);

隐式实例化,显式实例化,显式具体化都统称为具体化。他们表示的都是使用具体类型的函数定义,而非通用描述。

template<typename T>
void Swap(T &, T &); //函数模板
template<> void swap<student> (student &, student &); //显式具体化
int main()
{
	template void Swap<char>(char &, char &); //显式实例化
	short a, b;
	Swap(a, b); //模板生成short类型的函数定义,然后使用 隐式实例化
	student stu1, stu2;
	Swap(stu1, stu2); //使用显式具体化的函数定义
	char c1, c2;
	Swap(c1,c2); //在处理处理c1,c2的时候会使用显式具体化创建的模板
	Swap<int>(a, b); //在函数中使用显式实例化
}

24.编译器选择使用哪个函数版本—重载解析
重载解析用于在有多个参数的时候,决定函数调用哪一个函数定义。运行方式如下:

  • 第一步:创建候选函数列表。其中包含与被调用参数名才能相同的函数和模板函数。
  • 第二步:使用候选函数列表创建可行函数列表。这些都是参数数目正确的函数,为此有一个隐式转换序列,其中包括实参类型和相应的形参类型完全匹配的情况。
  • 第三步:确定是否有最佳的可行参数。如果有,则使用它,否则函数调用出错。
    举个例子
//考虑只有一个参数的情况,重载解析的执行方式
may('B');
//首先,编译器将寻找候选者,即名称为may()的函数和函数模板。
//然后寻找那些可以用一个参数调用的函数
//下述的函数符合要求
void may(int);  //#1
float may(float, float = 3);  //#2
void may(char);  //#3
char * may(const char *); //#4
char may(const char &); //#5
template<class T> void may(const T &);  //#6  具体化
template<class T> void may(T *);    //#7  具体化
//只考虑特征标,并不考虑返回值类型,其中#4,#7不可行,因为'B'为右值,不能被隐式的转化为指针类型。
//剩余的五个函数均可以被使用,对于这五个函数来说,如果他们是唯一被声明的函数
//那么均可以被使用
//下面编译器需要确定使用哪个函数是最佳的
//他查看为使函数调用参数与可行的候选函数的参数匹配所需要进行的转化,通常按最优到最差排序
//最优到最差的顺序如下述:
//1.完全匹配,但常规函数优先于模板
//2.提升转换,short char 提升为int float 提升为double
//3.标准转换,int转换为char long转换为double
//4.用户定义的转换
/*
例如上述的剩余的五个函数原型,#1的参数为int型,我们的参数为char型,属于提升转换。
#2的函数为float型,char与float为标准转换
#3的函数为char型,属于完全匹配
#5,#6也都是char型,都属于完全匹配
所以#3 #5 #6 优于 #1 优于 #2
那么遇到这种三个都是完全匹配的要如何比较呢?
*/
  • 完全匹配与最佳匹配
    进行完全匹配的时候,C++允许一些无关紧要的转换。
从实参到形参
TypeType &
Type &Type
Type []*Type
Type (argument-list)Type (*) (argument-list)
Typeconst Type
Typevolatile Type
Type *const Type
Type *volatile Type *

如果有多个匹配的原型,则编译器则无法完成重载的解析过程,如果没有最佳的可行函数,则编译器将生成一条错误消息。不过C++中对于完全匹配还是有一定的优先级的,具体如下:

  • 指向非const数据的指针和引用 优先与 非const指针和引用参数相配
  • 非模板函数优先于模板函数(模板函数和显式具体化函数)
  • 俩个模板函数中,较具体的函数优先于普通模板函数
    所谓的具体,并不只是代表显式具体化,而是意味着编译器推断使用哪种类型时执行的转换最少
template<class type> void recycle (Type t); //#1
template<class type> void v(Type *t); //#2

struct blot {int a; char b[10];};
blot ink = {25,"spots"};
recycle(&ink);
//调用recycle函数时,#1,#2均完全匹配
//其中#1的 Type 被解释为blot *
//而#2的 Type 被解释为blot
//这两个隐形实例被发送到可执行函数池中
//这两个模板函数,#2被认为是更具体的,因为他需要进行的转换更少
//#2已经显示的指出,参数是指向Type的指针,因此可以直接用blot来表示Type 

这种找到最具体模板的规则被称为函数模板的部分排序规则。是C++98新增特性。

25.创建自定义选择
有些情况下,可以通过编写何是的函数调用,引导编译器做出您希望的选择。
下面举个小例子:

#include<iostream>
using namespace std;
template<typename T> T lesser (T a, T b); //#1
int lesser (int a, int b); //#2
int main()
{
	int m = 20;
	int n = -30;
	double x = 10.0;
	double y = -35.2;
	cout << lesser(m,n) << endl; //m,n类型为int  将使用标准函数#1
	cout << lesser(x,y) << endl; //x,y类型为double 将使用模板#2 double型
	cout << lesser<>(m,n) << endl; //lesser<>显式实例化,使用模板#2 int型
	cout << lesser<int>(x,y) << endl; //lesser<>显示实例化,使用int型 #2
}
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;
}

26.模板函数的发展

  • C++98中的问题 类型问题
tamplate<typename T1, typename T2> 
void ft(T1 x , T2 y)
{
	?type? xpy = x + y;
}

问题就出现在这里了,因为x,y均为泛型,所以并不知道x,y具体类型是什么,那么xpy的类型也就不确定了,因为如果x为int,y为double,那么xpy是double类型。如果x是short,而y是int,那么xpy就是int类型。在x,y的类型没有确定下来之前,我们不知道如何给xpy来确定类型。

  • 关键字decltype C++11中的解决方案
//关键字decltype
decltype(x) y;
//意味着y的变量类型和x一致
//括号中的内容还可以为表达式

//在C++11中
decltype(x+y) z;
tamplate<typename T1, typename T2> 
void ft(T1 x , T2 y)
{
	decltype(x+y) xpy = x + y;
}
  • decltype的执行原理
    0.关键字的声明 decltype(expression) var
    1.第一步 如果expression是没有括号括起的标识符,则var类型与该标识符相同,包含const等限定符
    2.第二步 如果expression是一个函数调用,则var的类型与函数的返回类型相同
    3.第三步 如果expression是一个左值,则var为指向其类型的引用。注意:这里的expression需要是用括号括起的。
    4.第四步 如果上述条件都不满足,则var的类型与expression的类型相同
int xx = 66;
decltype((xx)) y ; // y的类型为 int &
decltype(xx) z; // z的类型为 int

27.另一种函数声明语法
有一个问题是decltype无法解决的,这个问题如下:

template<class T1, class T2>
?type? gt(T1 x, T2 y)
{
	return x+y;
}

这里的返回值类型是不确定的,因为x和y的类型都不确定,但是这个时候并没有办法使用decltype(x+y)。因为这个时候x,y还没有定义,并不在x,y的生存空间中。所以这种语法是不正确的。那么为了解决这个问题,我们有了一种新的原型。

//对于下面的原型
double h(int, int);
//等价于
auto h(int, int) -> double;
//上面的问题也可以通过这个方式解决
template<typename T1, typename T2>
auto gt(T1 x, T2 y) -> decltype(x+y);

template<typename T1, typename T2>
auto gt(T1 x, T2 y) -> decltype(x+y)
{
	return x+y;
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值