文章目录
前言
👧个人主页:@小沈YO.
😚小编介绍:欢迎来到我的乱七八糟小星球🌝
📋专栏:C++ 心愿便利店
🔑本章内容:函数重载、引用
记得 评论📝 +点赞👍 +收藏😽 +关注💞哦~
提示:以下是本篇文章正文内容,下面案例可供参考
🌟一、函数重载
自然语言中,一个词可以有多重含义,人们可以通过上下文来判断该词真实的含义,即该词被重载了。
比如:以前有一个笑话,国有两个体育项目大家根本不用看,也不用担心。一个是乒乓球,一个是男足。前者是“谁也赢不了!”,后者是“谁也赢不了!"
🌏1.1.函数重载概念
函数重载:是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数 或 类型 或 类型顺序)不同,常用来处理实现功能类似数据类型不同的问题
#include using namespace std;int Add(int left, int right){cout << "int Add(int left, int right)" << endl;return left + right;}double Add(double left, double right){cout << "double Add(double left, double right)" << endl;return left + right;}int main(){cout << Add(1, 2) << endl;cout << Add(1.1, 2.2) << endl;return 0;}
#include using namespace std;void f(){cout << "f()" << endl;}void f(int a){cout << "f(int a)" << endl;}int main(){f();f(10);return 0;}
#include using namespace std;void f(int a, char b){cout << "f(int a,char b)" << endl;}void f(char b, int a){cout << "f(char b, int a)" << endl;}int main(){f(10, 'a');f('a', 10);return 0;}
#include using namespace std;void f(int a, char b){cout << "f(int a,char b)" << endl;}void f(int b, char a){cout << "f(int b, char a)" << endl;}
#include using namespace std;void f(char a, int b){cout << "f(int a,char b)" << endl;}int f(char a, int b){cout << "f(int a, char b)" << endl;}int main(){return 0;}
#include using namespace std;//构成函数重载void func(int a){cout << "void func(int a)" << endl;}void func(int a, int b = 1){cout << "void func(int a,int b)" << endl;}int main(){func(1,2);//调用存在歧义func(10);return 0;}
重载和缺省参数碰撞是可以构成重载的(参数个数不同),但是会出现调用歧义(当调用func(1,2)是不会出现问题的,但是调用func(10),编译器就不知道调用哪个因为两个都符合调用)
🌏1.2.C++支持函数重载的原理 – 名字修饰
在C/C++中,一个程序要运行起来,需要经历以下几个阶段:预处理、编译、汇编、链接,最终形成一个可执行程序
对于C语言来说,当经过预处理、编译、汇编、链接
//#include //using namespace std;void func(int i, double d){//cout << "void func(int i, double d)" << endl;}void func(double d, int i){//cout << "double func(double d, int i)" << endl;}int main(){func(1, 5.2); func(5.2, 1);return 0;}
#include using namespace std;void func(int i, double d){//cout << "void func(int i, double d)" << endl;}void func(double d, int i){//cout << "double func(double d, int i)" << endl;}int main(){func(1, 5.2); func(5.2, 1);return 0;}
先提前说明一下,这部分不懂得可以看C语言—程序环境和预处理(底层原理万字详解)
实际项目通常是由多个头文件和多个源文件构成,而通过C语言阶段学习的编译链接,我们可以知道,【当前a.cpp中调用了b.cpp中定义的Add函数时】,编译后链接前,a.o的目标文件中没有Add的函数地址,因为Add是在b.cpp中定义的,所以Add的地址在b.o中。那么怎么办呢?
- 所以链接阶段就是专门处理这种问题,链接器看到a.o调用Add,但是没有Add的地址,就会到b.o的符号表中找Add的地址,然后链接到一起。
- 那么链接时,面对Add函数,链接接器会使用哪个名字去找呢?这里每个编译器都有自己的函数名修饰规则。
- 使用g++修饰后的名字通过下面我们可以看出gcc的函数修饰后名字不变。而g++的函数修饰后变成【_Z+函数长度+函数名+类型首字母】
结论:在linux下,采用gcc编译完成后,函数名字的修饰没有发生改变
结论:在linux下,采用g++编译完成后,函数名字的修饰发生改变,编译器将函数参
数类型信息添加到修改后的名字中
#include using namespace std;int func(double d, int i){cout << "void func(int i, double d)" << endl;return 0;}void func(double d, int i){cout << "double func(double d, int i)" << endl;}int main(){func(1.1, 1);func(1, 1.1);return 0;}
- 通过这里就理解了C语言没办法支持重载,因为同名函数没办法区分。而C++是通过函数修饰规则来区分,只要参数不同,修饰出来的名字就不一样,就支持了重载。
- 如果两个函数函数名和参数是一样的,返回值不同是不构成重载的,因为调用时编译器没办法区分。
🌟二、引用
🌏2.1.引用的概念
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
比如:孙悟空,唐僧称为"悟空",江湖上人称"齐天大圣"。
类型& 引用变量名(对象名) = 引用实体;
void TestRef(){int a = 10;int& ra = a;//<====定义引用类型printf("%p\n", &a);printf("%p\n", &ra);}
🌏2.2.引用特性
- 引用在定义时必须初始化
- 一个变量可以有多个引用
- 引用一旦引用一个实体,再不能引用其他实体
void TestRef(){1. 引用在定义时必须初始化int a = 10;// int& ra; // 该条语句编译时会出错2. 一个变量可以有多个引用int& ra = a;//给a取别名int& rra = a;//给a取别名int& raa = ra;//给b(a的别名)取别名也是可以的3. 引用一旦引用一个实体,再不能引用其他实体(C++的引用不可以改变指向但是Java可以)int x = 1;b = x; //这里是赋值而不是把b变成x的别名}
🌏2.3.常引用
void TestConstRef(){权限的放大:就相当于带上金箍圈的孙悟空(const)摘下了金箍圈变得肆无忌惮const int a = 10;//int& ra = a; // 该语句编译时会出错,a为常量 --- 权限的放大在这里去掉了const也就是去掉了金箍圈const int& ra = a;--- 权限的平移:带上金箍圈无论是孙悟空还是齐天大圣它都有限制不会肆无忌惮权限的缩小:本来是大闹天宫的齐天大圣被戴上了金箍圈(const)就有了限制int x=10;const int& y=x;double d = 12.34;//int& rd = d; // 该语句编译时会出错,类型不同const int& rd = d;在C/C++中有规定:发生类型转换,会产生一个临时变量,例如上面这一小段代码const int& rd = d,转换时会有一个int类型的临时变量,临时变量再给rd,但是临时变量具有常性int& rd = d这里就是权限的放大不能通过编译,所以这种是对的const int& rd = d// int& b = 10; // 该语句编译时会出错,b为常量 - 不能变成常量对象的别名const int& b = 10;--- 权限平移}
在引用的过程中:
- 权限可以平移
- 权限可以缩小
- 权限不可以放大
#include using namespace std;int func(){int a = 0;return a;}int main(){const int& ret = func();return 0;}
🌏2.4.使用场景
#include using namespace std;void Swap(int& left, int& right){int tmp = left;left = right;right = tmp;}int main(){int i = 3, j = 6;Swap(i, j);cout << i << endl;cout << j << endl;return 0;}
实际原理:是会生成一个临时变量(可能寄存器充当也可能其他方式),n会在返回值之前拷贝给临时变量,临时变量不会在Count函数的栈帧,一般是在寄存器或者上一层函数的栈帧
#include using namespace std;int Count(){int n = 0;n++;// ...return n;}int main(){int ret = Count();return 0;}
会出现类似野指针的危险:n都销毁了,在访问n的别名
问:n都销毁了还能访问它的别名吗?
答:可以,因为空间销毁并不是这块空间就没了而是被系统回收,就像酒店里的房间,退房后房间不会消失而是被回收你的入住权租给别人或者空着,而野指针就是退房后你还偷偷藏了房间的钥匙,然后偷偷跑进房间。但是这里不是野指针,返回n的别名是不合法的
#include 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和随机值------>调用函数返回n的别名,当int ret=Count(),函数Count()栈帧已经销毁了,再去访问这块空间就会出现两种可能性第一种是1拷贝给ret,还有一种可能是随机值(取决于栈帧销毁后空间是否会被置成随机值取决于编译系统)
这种代表ret也是n的别名,第一次访问打印ret是1,第二次就变成了随机值为什么
知识点补充:cout是一个函数调用,(调用函数先传参)第一次先传参取到的还是1然后进行函数调用,Count函数的栈帧销毁,第一次函数调用占用的还是那块空间只不过可能比之前Count函数栈帧大或者小此时函数调用覆盖这块空间而ret还是这块空间的别名所以取到的就是一个随机值,但也不一定是随机值,当Count函数栈帧很大n在下面,就不会被覆盖
#include using namespace std;int& Count(){int n = 0;n++;// ...return n;}int main(){int& ret = Count();cout << ret << endl;cout << ret << endl;return 0;}
int& Add(int a, int b){ int c = a + b; return c;}int main(){ int& ret = Add(1, 2); Add(3, 4); cout << "Add(1, 2) is :"<< ret <<endl; return 0;}
注意:如果函数返回时,出了函数作用域,如果返回对象还在(还没还给系统),则可以使用引用返回,如果已经还给系统了,则必须使用传值返回。
🌏2.5.传值、传引用效率比较
以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低
#include using namespace std;#include 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;}
#include using namespace std;#include struct A { int a[10000]; };A a; //值返回A TestFunc1() { return a; } //引用返回A& TestFunc2() { return a; }void TestReturnByRefOrValue(){ //以值作为函数的返回值类型size_t begin1 = clock();for (size_t i = 0; i < 100000; ++i)TestFunc1();size_t end1 = clock(); //以引用作为函数的返回值类型size_t begin2 = clock();for (size_t i = 0; i < 100000; ++i)TestFunc2();size_t end2 = clock(); //计算两个函数运算完成之后的时间cout << "TestFunc1 time:" << end1 - begin1 << endl;cout << "TestFunc2 time:" << end2 - begin2 << endl;}int main(){TestReturnByRefOrValue();return 0;}
通过上述代码的比较,发现传值和指针在作为传参以及返回值类型上效率相差很大。
传引用传参(任何时候都可以用)
- 提高效率
- 输出型参数(形参的修改,影响的实参)
传引用返回(出了函数作用域对象还在才可以用)
- 提高效率
- 修改返回对象(末尾彩蛋处有体现)
🌏2.6.引用和指针的区别
#include using namespace std;int main(){int a = 10;int& ra = a;cout << "&a = " << &a << endl;cout << "&ra = " << &ra << endl;return 0;}
#include using namespace std;int main(){int a = 0;int* p1 = &a;int& ref = a;++(*p1);++ref;return 0;}
引用和指针的不同点:
- 引用概念上定义一个变量的别名,指针存储一个变量地址。
- 引用在定义时必须初始化,指针没有要求
- 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体
- 没有NULL引用,但有NULL指针
- 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)
- 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
- 有多级指针,但是没有多级引用
- 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
- 引用比指针使用起来相对更安全
🌟三、末尾彩蛋(带你回溯时空联想之前)
#include #include #include struct SeqList{int a[10];int size;};//读取第i个位置的接口int SLAT(struct SeqList* ps, int i){assert (i <ps->size) ;return ps->a [i];}//修改第i个位置的接口void SLModify(struct SeqList* ps, int i, int x){assert(i < ps->size);ps->a[i] = x;}int main(){return 0;}
#include #include #include using namespace std;struct SeqList{int a[10];int size;};int& SLAT(struct SeqList& ps, int i){assert(i < ps.size);return (ps.a[i]);}int main(){struct SeqList s;s.size = 3;SLAT(s, 0) = 10;SLAT(s, 1) = 20;SLAT(s, 2) = 30;cout << SLAT(s, 0) << endl;cout << SLAT(s, 1) << endl;cout << SLAT(s, 2) << endl;return 0;}
- 读取i位置:减少了拷贝,返回此时位置的别名
- 修改i位置:数组中第i个位置的值出了作用域肯定还在,因为结构体在外面不在函数的栈帧里面所以存在,出了作用域不会销毁,得到它的别名后,通过赋值加加等就会修改
- 如果不用引用返回的就是它的临时拷贝打印是没有问题的修改却是不可以的因为临时对象具有常性不能修改