C++基础精讲篇第2讲:函数重载+引用

      这一讲是承接《 C++基础精讲篇第1讲》的内容补充之一。读者们可以先把上一讲的知识学习了以后,再学习这一讲的相关知识。这一讲主要为大家分享C++特有的函数重载引用这两个重要的概念,这是深入学习C++必须掌握的知识点,希望大家在我的文章的帮助下,能有所收获,那我们就开始学习吧。

C++基础精讲篇2—学习思维导图

目录

1、函数重载

1.1概念

1.2 案例分析

 1.3 函数重载的使用注意事项

 1.4 补充知识:extern"C"

2、引用

2.1 类型& 引用变量名(对象名)==引用实体

2.2  引用相关特性

2.2.1 引用在定义时必须初始化

2.2.2 一个变量可以有多个应用

2.2.3 引用一旦引用一个实体,不能再引用其他实体

2.3 引用使用场景

2.3.1 引用做参数

2.3.2 引用做返回值

2.4 常引用

2.4.1 权限平移

2.4.2 权限放大

2.4.3 权限缩小

2.4.4  权限总结

2.4.5  引用权限举例说明

2.4.6 补充(关于隐式类型转换内部逻辑)

2.5 引用和指针的区别

2.5.1、从语法概念角度分析

2.5.2、从底层原理角度分析

2.5.3、不同点总结

3、结语



 1、函数重载

1.1概念

       函数重载是函数的一种特殊情况的,C++中允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数、类型、顺序)必须不同,其常用来处理实现功能类似数据类型不同的问题。

1.2 案例分析

       以下面代码为例分析,函数名相同,但形参列表不同,我们依然可以实现同名调用,而在C语言中,是不允许函数名相同的,这就是C++不同于C语言的地方之一,正因为有了函数重载概念,对我们开发者来说使用非常方便。

#include<iostream>
using namespace std;
//函数重载
//使用场景举例说明:这种函数同名使用在c语言中是不支持的,在c语言中需要用到不同的函数名,如:Swap_1;Swap_2
void Swap(int*p1, int*p2)
{
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}

void Swap(double*p1, double*p2)
{
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}
int main()
{
	int a = 1, b = 2;
	double c = 1.1, d = 2.2;
	Swap(&a, &b);
	Swap(&c, &d);
	//其实在这里利用cout,自动识别类型,利用到了函数重载原理
	cout << a << endl;
	cout << b << endl;

	return 0;
}

       除了上面的例子以外,C++中的输入流和输出流其实也是函数重载的表现,因为在上一讲中我们提到过C++的输入流和输出流不用指定数据类型同样可以识别,其原理就是利用了函数重载。

       之所以C++能够有函数重载,这是因为C++具有不同于C语言的函数名修饰规则,正因为该规则使得在同名函数的条件下,只要形参不同,开发者就可以使用同名函数进行操作,关于函数重载的详细介绍,推荐大家去看这篇博客,个人认为描述得很详细,看完就能基本明白是怎么一回事了

建议阅读的函数重载博客链接:C++的函数重载 - 吴秦 - 博客园 (cnblogs.com)

 1.3 函数重载的使用注意事项

1、当同名函数中形参类型相同,然后不同的是交换了形参的顺序,这种是不被允许的,不属于函数重载;

2、函数名相同,内部形参顺序类型均相同,但返回值不同也不能构成函数重载,因为在调用时是无法区分的;

 1.4 补充知识:extern"C"

      在C++工程中可能需要将某些函数按照C的风格来编译,在函数前加extern "C",意思是告诉编译器,将该函数按照C语言规则来编译。代码示意如下:

extern "C" int Add(int left, int right);
int main()
{
Add(1,2);
return 0;
}

2、引用

       引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。简而言之,引用的作用是对标指针的。

2.1 类型& 引用变量名(对象名)==引用实体

#include<iostream>
//引用,其作用是对标指针,简而言之就是给变量取别名
using namespace std;

int main()
{
	int a = 0;
	int& b = a;//这里&符号表示引用,和int b=a;不一样
	cout << &b << endl;//这里&符号表示取地址
	cout<< endl;
	cout << &a << endl;

	a++;
	b++;
	return 0;
}
使用引用的基本方法

        当读者们学习了我前一讲的文章以后,那么对于上面的程序代码的理解肯定是没有问题的。在上面的程序展示过程中,“int&”中的符号“&”代表的含义就是引用,也就是说a的别名是b,在后面符号“&a”和“&b”表示的就是我们熟知的取地址操作符,通过打印输出可以看到变量a和变量b的地址是相同的。那当我们对变量b修改会改变变量a吗?答案是会改变的,如下代码展示:

#include<iostream>
//引用,其作用是对标指针,简而言之就是给变量取别名
using namespace std;

int main()
{
	int a = 0;
	int& b = a;//这里&符号表示引用,和int b=a;不一样
	//cout << &b << endl;//这里&符号表示取地址
	//cout<< endl;
	//cout << &a << endl;
	a++;
	cout << b << endl ;
	cout << a << endl<<endl;
	b++;
	cout << b << endl;
	cout << a << endl;
	return 0;
}

        从上面中我们可以看到,当变量b是变量a的别名时,使用命令“a++”,此时“a=1”,“b=1”,紧接着使用命令“b++”,此时“a=2”,“b=2”。从上述的代码演示中可以看到:对变量a的改变,变量b也会随之改变,反过来,对变量b的改变,同样也随之改变a,从这里也更加印证了b就是变量a的别名。这就是C++中引用的简单用法。当大家对引用概念有了一定认识以后,下面我们开始详细介绍引用特性及其使用场景。

2.2  引用相关特性

2.2.1 引用在定义时必须初始化

#include <iostream>
using namespace std;
int main()
{
	int a = 1;
	//int& b;//1、引用在定义时必须初始化
    int& b=a;

}
引用必须初始化,不然会编译出错

2.2.2 一个变量可以有多个应用

     在下面的程序中,演示了变量a可以有多个引用,这在编译器中是允许存在的。

#include <iostream>
using namespace std;
int main()
{
	int a = 1;
	int& b = a;//2、一个变量可以有多个引用
	int& c = a;
	int& d = c;
	return 0;
}

2.2.3 引用一旦引用一个实体,不能再引用其他实体

 在下面的程序中,演示了当变量b作为变量a的引用时,不可以再作为其他实体的引用。

#include <iostream>
using namespace std;
int main()
{
	int a = 1;
	//int& b;//1、引用在定义时必须初始化

	int& b = a;//2、一个变量可以有多个引用
	int& c = a;
	int& d = c;

	int x = 10;//3、引用一旦引用了一个实体,再不能引用其他实体

	b = x;//在这里是将x赋值给b,不是将x引用给b
	return 0;
}

2.3 引用使用场景

2.3.1 引用做参数

1、引用做参数的输出型参数

       在下面的程序中,用C++书写了交换函数,和以前写的交换函数不同的时,这里采用的是将引用用作参数的输出型参数,而在之前,我们写交换函数,都是通过指针接收,从这里可以看到引用的作用同指针效果是一致的,通过引用做输出型参数,对r1和r2的改变同样会改变变量a和b。(在这里演示可以用同一个函数名可以更方便,但我采用C语言中的要求为大家演示,下一节在函数重载中会为大家详细介绍)。

#include <iostream>
using namespace std;
//做参数:1、引用做参数的输出型参数
void Swap(int& r1, int& r2)
{
	int tmp = r1;
	r1 = r2;
	r2 = tmp;
}
//用指针接收
void Swap_(int* r1, int* r2)
{
	int tmp = *r1;
	*r1 = *r2;
	*r2 = tmp;
}

int main()
{
	int a = 0;
	int b = 2;
	int c = 1;
	int d = 2;
	//在c语言中,是用指针取地址传过去,而在C++中是直接传变量过去,即引用在这里做输出型参数
	Swap(a, b);
	//用指针接收
	Swap_(&c,&d);
	return 0;
}

2、引用做大对象传参时,利于提高效率

       为了比较引用做大对象传参时的优势,在程序演示中,预先创建结构体A并开设了10000个空间,分别创建函数TestFunc1和函数TestFunc2比较遍历10000个数据所需要的时间。

       其中clock()是时钟函数,用来记录程序运行到此条命令的时间,然后利用前后两个时钟相减即可求得程序经过指定区域内的时间,头文件为:#include<time.h>。

        通过比较两函数运行时间,可以看到,利用引用做大数据传参时,时间耗费很低,小于ms,而正常的传值则需要17ms,很明显,引用做大对象传参能够提高程序执行效率。

//下面是举例说明,比较传值和引用的效率
#include <iostream>
#include <time.h>
using namespace std;
struct A { int a[10000]; };
void TestFunc1(A a) {}
void TestFunc2(A& a) {}
void TestRefAndValue()
{
	A a;
	// 以值作为函数参数
	size_t begin1 = clock();
	for (size_t i = 0; i < 10000; ++i)
		//这里需要传参
		TestFunc1(a);
	size_t end1 = clock();
	// 以引用作为函数参数
	size_t begin2 = clock();
	for (size_t i = 0; i < 10000; ++i)
		//这里不需要传参
		TestFunc2(a);
	size_t end2 = clock();
	// 分别计算两个函数运行结束后的时间
	cout << "TestFunc1(A)-time:" << end1 - begin1 << endl;
	cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl;
}

int main()
{
	TestRefAndValue();
	return 0;
}

        结论:当引用用作输出型参数时,其作用可以同指针作用一样理解,都是不用再重新开辟空间,所以相比传值,会提高效率。

2.3.2 引用做返回值

       在分析引用做返回值时,我们需要对比分析传值返回和传引用返回的不同特性,这样大家才能对引用做返回值有深入的理解。

1、传值返回

#include <iostream>
using namespace std;
//传值返回
int Count()
{
	int n = 0;
	//……
	n++;
	return n;
}

int main()
{
	int ret = Count();
	cout << ret << endl;
	return 0;
}

        传值返回的底层原理: 在上述的程序代码演示中,编译器会开辟两个栈帧:main栈帧和Count栈帧,这两个栈帧都放在栈上。当采用传值返回时,在Count函数中return n返回的n首先会通过一个临时变量接收,然后当程序执行完Count函数时,即跳出该函数,Count栈帧就会被系统销毁,即此时查找Count内部的数据是随机值,所以根据这一特性,需要利用临时变量临时存储返回值,然后将该返回值的内容赋给ret接收,也就是说此时栈帧销毁不销毁,都能把返回值接收,因为返回值已经拷贝在临时变量中,自此就完成了传值过程。

传值返回:生成一个返回对象拷贝作为函数调用返回值

2、传引用返回

情况1:

#include <iostream>
using namespace std;
//引用返回
int& Count()
{
	int n = 0;
	//……
	n++;
	return n;
}

int main()
{
	int ret = Count();
	cout << ret << endl;
	return 0;
}
传引用返回:引用返回的语法含义就是返回n的 返回对象的别名

       上面这种描述的引用返回,ret的结果是未定义的,如果栈帧结束时,系统会清理栈帧置成的随机值,那么这里ret的结果就是随机值,因此,上面程序使用引用返回本质是不对的,结果是没有保障的。

情况2:

#include <iostream>
using namespace std;
int& Count()
{
	int n = 0;
	//……
	n++;
	return n;
}

int main()
{
	int& ret = Count();
	cout << ret << endl;
	cout << ret << endl;

	return 0;
}

       我再详细解释为什么第二次打印会成随机值:第一次之所以能成功传值,是有取巧的行为,即此时系统还并未销毁该空间,或者已经销毁了该空间,但该空间还未被其他变量使用,所以第一次去调用时,能够传值成功,但第二次是随机值的原因就是因为,当我们第一次利用打印输出结果时,其实此时也是在调用打印函数,所以会开辟相应的栈帧,所以系统将以前开辟的栈帧给重新利用了起来,然后再去打印时,就找不到了。 

        结论:如果函数返回时,出了函数作用域,如果返回对象还未还给系统(即返回对象还没有销毁了),那么就可以使用引用返回,如果已经还给系统了,则必须使用传值返回。

情况1+2总结:

      经过前面描述的两种情况,因为使用引用返回会存在一定的风险,但在一定条件下是可以放心使用传引用返回的,比如下面这种情况:在函数中对返回变量附加static,扩大局部变量的其作用域。

#include <iostream>
using namespace std;
int& Count()
{
   //利用static 可以保证n出了作用域其值还在,此时就可以大胆使用传引用返回
	static int n = 0;
	//……
	n++;
	return n;
}

int main()
{
//下面在ret这里采用引用时,可以理解为:ret是中间变量tmp的别名,tmp是n的别名,所以ret就是n的别名,
//然后如果在Count函数中不添加static时,同样的和情况1一样,可能因为栈帧的销毁在调用时发生越界行为。
//所以在这种写法下,我们可以在Count函数中加上static,因为此时变量n就不是在放在栈区,
//而是在静态区,所以栈帧销毁也不会影响函数调用。
	int& ret = Count();
	cout << ret << endl;
	cout << ret << endl;
	return 0;
}
在调用的函数中对变量附加static ,这样就可以扩大局部变量的作用域,就不会出现出栈就销毁了

 

2.4 常引用

       在这里我们会接触到新的名词,即权限。大致分为三类:权限平移、权限放大和权限缩小。下面将带着大家通过代码来理解吧。

2.4.1 权限平移

#include <iostream>
using namespace std;
int main()
{
	//权限的平移
	int a = 0;
	int& b = a;

	//查看数据类型
	cout << typeid(a).name() << endl;
	cout << typeid(b).name() << endl;
	return 0;
}
权限平移代码演示

        从上述代码中我们可以看到:定义变量b作为变量a的别名,通过C++自带的查看类型数据功能可以看到,二者的数据类型是一致的,这就很好的说明二者是同级的(即a具备的功能,b同样也具备)

2.4.2 权限放大

#include <iostream>
using namespace std;
int main()
{
	//权限不能放大,编译不允许
	//const int c = 0;
	//int& d = c;

	此时可利用权限的平移
	//const int c = 0;
	//const int& d = c;

	//查看数据类型
	cout << typeid(c).name() << endl;
	cout << typeid(d).name() << endl;
	return 0;
}
权限不能放大

2.4.3 权限缩小

#include <iostream>
using namespace std;
int main()
{
	//权限可以缩小
	int e = 3;//这里的e的权限是针对空间是可读可写的
	const int& f = e;//而这里的f的权限是只能是可读的,不能被修改

	//查看数据类型
	cout << typeid(e).name() << endl;
	cout << typeid(f).name() << endl;
	return 0;
}

       在上面的程序中,变量e是可读可写的,而引用的变量f是被const修饰,即只是可读的,这种行为是被编译器允许的,这也就是权限的缩小。

2.4.4  权限总结

1、权限可以平移;

2、权限不能被放大;

3、权限可以缩小。

2.4.5  引用权限举例说明

我在代码注释中描述得很详细,大家可以查看代码注释,进行理解。

#include <iostream>
using namespace std;

//fun1函数是传值传参
void fun1(int n)
{
	//
}

如果使用引用传参,函数内如果不改变n,那么建议尽量用const引用传参
//void fun2(int& n)
//{}

//此时主函数中的程序就可以执行通过
void fun2(const int& n)
{}

int main()
{
	int a = 10;
	const int b = 20;
	fun1(a);
	fun1(b);
	fun1(30);
	//从上面这三个例子可以看出:权限的放大或者缩小只针对引用和指针,
    //变量之间的赋值不遵循权限的放大缩小问题,因为是拷贝,即n的改变不会影响a,b等原变量

	fun2(a);
	fun2(b);//这里权限对于注释的fun2函数而言会放大,不允许,传参传不过去,所以采用修改后的fun2函数
	fun2(30);//这里是一个常量,对于注释的fun2函数而言,权限属于放大,传参传不过去,所以采用修改后的fun2函数

	double d = 1.11;
	//在这里变量d作为参数传值,因为其是double类型,在传值过程中,会首先将值传递给中间临时变量tmp,
    //因为fun2函数能接受的参数类型是int类型,所以该中间变量通过系统类型转换成int类型,
   //而中间临时变量具有常性,所以如果采用注释的fun2函数接受,则权限会放大,编译不允许。
	fun2(d);
	fun2(1.21);
	return 0;
}

2.4.6 补充(关于隐式类型转换内部逻辑)

大家可以查看代码注释,我写得很清晰,就不再赘述了。

#include <iostream>
using namespace std;
int main()
{
	下面这个发生隐式类型转换

	//int ii = 1;
	//double dd = ii;//这种转换会产生中间临时变量,此时的中间临时变量是double类型的
	上面这个类型转换补充:类型转换(强制类型/隐式类型/整形提升)都会产生中间变量,
   //但并不会改变原变量的数据类型


	下面这种写法则不可以,权限不匹配
	double& rdd = ii;


	下面这种写法可以
	因为这里的转换也会先产生中间临时变量,但由于临时变量是具有常性的,
   //所以从权限的角度出发,double& rdd = ii这种写法得到的rdd是可读可写的,权限是被放大了的
	当在前面加了const,则就是权限的平移,即rdd只是可读的,则书写就是正确的。
	//const double& rdd = ii;

	const可以引用这种常量,可以发现其具有很强的接收度。
	//const int& x = 10;
	return 0;
}

2.5 引用和指针的区别

2.5.1、从语法概念角度分析

       在语法概念上,引用就是一个别名,没有独立的空间,和其引用的实体共用同一块空间。如下面代码展示一样,引用和实体的地址是一样的,这也进一步印证了前面的分析。

#include<iostream>
using namespace std;

//引用和指针的区别
int main()
{
	int a = 10;
	int& ra = a;
	cout << "&a = " << &a << endl;
	cout << "&ra = " << &ra << endl;
	return 0;
}
从语法角度分析实体和引用

       分别比较实体指针和引用可以看出,二者指向的地址是一致的,说明二者在语法方面想表达的作作用效果是一致的。 

#include<iostream>
using namespace std;

int main()
{
	int a = 10;
	//用引用赋值
	int& ra = a;
	ra = 20;
	//用指针赋值
	int* pa = &a;
	*pa = 20;
	return 0;
}

2.5.2、从底层原理角度分析

        还是用上面分析的代码,我们通过观察指针和引用的反汇编,可以发现,在底层实现上引用是按照指针的方式实现的,也就是说在底层实现方面,引用实际是有空间的。

#include<iostream>
using namespace std;

int main()
{
	int a = 10;
	//用引用赋值
	int& ra = a;
	ra = 20;
	//用指针赋值
	int* pa = &a;
	*pa = 20;
	return 0;
}
引用和指针底层原理分析

2.5.3、不同点总结

经过前面关于引用的使用、用法、特性等分析,我们对指针和引用做一定的总结:

  1. 引用在定义时必须初始化,指针没有要求;
  2. 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体;
  3.  没有NULL引用,但有NULL指针;
  4. 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节);
  5.  引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小;
  6. 有多级指针,但是没有多级引用;
  7. 访问实体方式不同,指针需要显式解引用,引用编译器自己处理;
  8. 引用比指针使用起来相对更安全;

总的来说:指针更强大、更危险、更复杂,而引用相对局限一些、也更安全、更简单。

3、结语

      今天这一讲主要详细为大家讲解了函数重载和引用这两个C++非常重要的概念,细节内容比较繁杂,博主以及尽力用通俗的语言为大家讲解,希望读者们阅读了这篇博客能有不错的收获。制作不易,欢迎大家点赞、支持、关注!!!

  • 7
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值