这一讲是承接《 C++基础精讲篇第1讲》的内容补充之一。读者们可以先把上一讲的知识学习了以后,再学习这一讲的相关知识。这一讲主要为大家分享C++特有的函数重载和引用这两个重要的概念,这是深入学习C++必须掌握的知识点,希望大家在我的文章的帮助下,能有所收获,那我们就开始学习吧。
![](https://i-blog.csdnimg.cn/blog_migrate/5ac5d6ab9cf4b82311d9b251114ad556.png)
目录
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;
}
![](https://i-blog.csdnimg.cn/blog_migrate/eb355bcccfc2dd8834bcd360d69752ad.png)
当读者们学习了我前一讲的文章以后,那么对于上面的程序代码的理解肯定是没有问题的。在上面的程序展示过程中,“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;
}
![](https://i-blog.csdnimg.cn/blog_migrate/e14cdb40c2b40471eb66c47a5439da65.png)
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接收,也就是说此时栈帧销毁不销毁,都能把返回值接收,因为返回值已经拷贝在临时变量中,自此就完成了传值过程。
![](https://i-blog.csdnimg.cn/blog_migrate/205ec0ddfdb1eee45a83b5dcf243cc13.png)
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;
}
![](https://i-blog.csdnimg.cn/blog_migrate/e44fcde693b80d32f1ca8045c92430a8.png)
上面这种描述的引用返回,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;
}
![](https://i-blog.csdnimg.cn/blog_migrate/8b8e3a8862527c6facf33ce65571c3dc.png)
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;
}
![](https://i-blog.csdnimg.cn/blog_migrate/f02385a5528ed59f716b0df09d47b231.png)
从上述代码中我们可以看到:定义变量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;
}
![](https://i-blog.csdnimg.cn/blog_migrate/39e5add3a7d94be42b8df18fe2f833ec.png)
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;
}
![](https://i-blog.csdnimg.cn/blog_migrate/d72b9badd69055b604e41e24dcb61c7b.png)
分别比较实体指针和引用可以看出,二者指向的地址是一致的,说明二者在语法方面想表达的作作用效果是一致的。
#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;
}
![](https://i-blog.csdnimg.cn/blog_migrate/d6c3f398a5cc0a02e7a8cf49ca1d4233.png)
2.5.3、不同点总结
经过前面关于引用的使用、用法、特性等分析,我们对指针和引用做一定的总结:
- 引用在定义时必须初始化,指针没有要求;
- 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体;
- 没有NULL引用,但有NULL指针;
- 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节);
- 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小;
- 有多级指针,但是没有多级引用;
- 访问实体方式不同,指针需要显式解引用,引用编译器自己处理;
- 引用比指针使用起来相对更安全;
总的来说:指针更强大、更危险、更复杂,而引用相对局限一些、也更安全、更简单。
3、结语
今天这一讲主要详细为大家讲解了函数重载和引用这两个C++非常重要的概念,细节内容比较繁杂,博主以及尽力用通俗的语言为大家讲解,希望读者们阅读了这篇博客能有不错的收获。制作不易,欢迎大家点赞、支持、关注!!!