【浅尝C++】C++基础第二弹=>函数重载与引用(含函数名修饰规则详解、权限缩小放大问题)

在这里插入图片描述

🏠专栏介绍:浅尝C++专栏是用于记录C++语法基础、STL及内存剖析等。
🚩一些备注:之前的文章有点杂乱,这里将前面的知识点重新组织了,避免了过多冗余的废话。
🎯每日努力一点点,技术变化看得见。


函数重载

日常生活中,我们可以使用一句话表达不同的意思。“哦”既可以表示肯定,也可以表示疑惑。那如果想使用同一个函数名实现不同的功能,在C++中能够实现吗?那我们就得先来了解一下函数重载了。
在这里插入图片描述

函数重载概念

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

1.参数类型不同

下面代码中,实现了两个同名的函数PrintNum,但两个函数的参数列表不同,一个参数为整型,一个参数为双精度浮点型。对于这种参数不同的情况,在函数调用处,会根据实参的类型调用对应的函数。(代码执行结果如下图所示)

#include <iostream>
using namespace std;
void PrintNum(int num)
{
	cout << "打印整型:" << num << endl;
}
void PrintNum(double num)
{
	cout << "打印双精度浮点型:" << num << endl;
}
int main()
{
	int i_num = 1;
	double d_num = 1.11;
	PrintNum(i_num);
	PrintNum(d_num);
	return 0;
}

在这里插入图片描述
2. 参数个数不同

下面代码中Repeat函数接收的参数的个数不同,一个接收一个,一个接收两个,这两个函数也构成重载。(代码执行结果如下图所示)

#include <iostream>
using namespace std;
void Repeat(int a)
{
	cout << "get a num, values " << a << endl;
}
void Repeat(int a, int b)
{
	cout << "get two num, values " << a << " and " << b << endl;
}
int main()
{
	int a = 1, b = 2;
	Repeat(a);
	Repeat(a, b);
	return 0;
}

在这里插入图片描述
3. 参数类型顺序不同

下面代码中一个函数第一个参数是int型第二个参数是double型,另一个参数第一个参数是double型第二个参数是int型。像这种参数类型的顺序不同的情况,也能构成重载。代码执行结果如下图所示)

#include <iostream>
using namespace std;
void Repeat(int a, double b)
{
	cout << "get an int and a double" << endl;
}
void Repeat(double a, int b)
{
	cout << "get a double and an int" << endl;
}
int main()
{
	int a = 1;
	double b = 2;
	Repeat(a, b);
	Repeat(b, a);
}

在这里插入图片描述

C++支持函数重载的原理——名字修饰

在介绍函数重载的原理之前,我们需要回顾一下源文件怎么变成可执行程序的↓↓↓
在这里插入图片描述
预处理阶段:会将头文件展开、条件编译、宏替换、去除注释,最终生成预处理文件。
编译阶段:该阶段会检查语法、类型等是否正确(词法、语法、语义分析,符号汇总),并将预处理文件编译成汇编文件。
汇编阶段:该阶段会形成符号表、将编译形成的中间代码转换成二进制机器码。
链接阶段:会合并段表、符号表,并进行符号表的重定位。

上面介绍的符号表,就是咱们定义的变量、函数等。当我们调用了Add函数时,链接阶段就会到Add函数的定义处找到这个函数的保存地址,将调用的地方和这个Add函数链接到一起。

下面我们使用下面的代码进行演示↓↓↓

#include <iostream>
using namespace std;

int Add(int a, int b)
{
	return a + b;
}
int Add(double a, double b)
{
	return a + b;
}
int main()
{
	Add(1, 2);
	Add(1.1, 2.1);
	return 0;
}

对上面代码执行g++ -o test.exe test.cpp,将它编译成可执行文件。使用objdump -S test.exe查看反汇编指令。我们可以发现两个Add函数名被修饰成下图红框内的样子。本质上,C++修改了Add函数名,将它改成了_Z+函数长度+函数名+类型首字母的形式。像这种将原函数名修改成新函数名的操作称为函数名修饰规则(名称修饰规则)。
在这里插入图片描述
通过这里,我们就能理解C语言为什么不支持函数重载了,C语言中没有使用函数名修饰规则,在链接阶段无法区分两个同名函数;而C++中支持函数名修饰,能够区分两个函数名相同的函数。

引用

生活中,我们常常会给自己好基友取外号。在C++中也有一种给变量取外号的方式——引用。
在这里插入图片描述

引用概念

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

引用的语法格式如下:

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

下图中,a是引用实体,b是引用变量,它与a指向同一片内存空间,相当于给a取了别名。
在这里插入图片描述

注意:引用类型必须和引用实体同一种类型

引用特性

  1. 引用在定义时必须初始化
#include <iostream>
int main()
{
	int a = 10;
	int& b = a;
	int& c;//error!引用必须初始化
}
  1. 一个变量可以有多个引用
#include <iostream>
int main()
{
	int a = 10;
	int& ra1 = a;//我是a的引用
	int& ra2 = a;//我也是a的引用
	int& ra3 = a;//Me Too!
}
  1. 引用一旦引用一个实体,再不能引用其他实体
#include <iostream>
using namespace std;
int main()
{
	int a = 10;
	int& ra = a;//ra称为a的别名
	int b = 20;
	ra = b;//这里不是引用其他实体,这里是给ra赋值为b的数值(20)
	//上一行执行完,ra和a的值的均变为20,因为它们指向同一片内存空间
	cout << a << " " << ra << endl;
	return 0;
}

常引用

如果在给引用变量的类型前加上const关键字,那这个引用就是常引用。常引用的值不能修改。

const 类型名& 引用变量名 = 引用实体

场景一:权限缩小
下面代码中a不是const类型,因而它的值可以修改。此时给a定义一个常引用b,由于常引用不能被修改,因而不能通过b来修改a的值。但a再被取别名后,仍能修改指向的内存空间的数值。

像这种引用实体可以修改,常引用无法修改的情况,称为权限缩小。权限缩小是允许的。

#include <iostream>
int main()
{
	int a = 10;
	a = 20;//OK!!
	const int& b = a;
	b = 80;//error!常引用无法修改
}

场景二:权限放大
下面代码中定义了一个常变量a,此时如果使用普通引用b引用a,则在定义引用时就会报错。因为a自身没有权限修改指向空间数值,则它的引用也不应该修改该空间的数值,应该定义常引用而不是普通引用。

像上面这种,给常变量定义普通引用的场景称为权限方法。因为普通引用能够修改地址空间数值,权限大于原变量,故称为权限放大。权限方法是不允许的。

#include <iostream>
using namespace std;
int main()
{
	const int a = 10;
	int& b = a;//error!
}

总之,在定义引用变量时,引用变量的权限只能小于等于引用实体,不能高于引用实体。

使用场景

1.做参数

下面代码能实现变量a和变量b的数值交换。由于引用和引用实体指向同一块内存空间,那么对引用的修改就是对引用实体的修改。如果希望函数形参的改变能够影响实参,可以将函数参数定义为引用类型。

#include <iostream>
using namespace std;
void swap(int& ra, int& rb)
{
	int tmp = ra;
	ra = rb;
	rb = tmp;
}
int main()
{
	int a = 0;
	int b = 10;
	cout << "交换前:a = " << a << " b = " << b << endl;
	swap(a,b);
	cout << "交换后:a = " << a << " b = " << b << endl;
	return 0;
}

2.做返回值

下面代码中,使用了引用做返回值。

#include <iostream>
using namespace std;

typedef struct Array
{
	int arr[1000];
	char ch[888];
}Array;

Array& test()
{
	static Array arr;
	return arr;
}

int main()
{
	Array a = test();
	return 0;
}

但不是什么什么情况都可以使用引用做返回值的。例如下面代码↓↓↓

#include <iostream>
using namespace std;
int& Add(int a, int b)
{
	int ret = a + b;
	return ret;
}
int main()
{
	int& sum = Add(1,2);
	cout << sum << endl;
	return 0;
}

上面程序中,执行main函数时,栈中会保存main函数的栈帧,在调用Add函数时会创建Add函数栈帧,Add函数内的ret局部变量会保存在该栈帧上。当Add函数执行完时,Add函数整个栈空间被释放,如果让sum指向ret的内存空间,由于ret空间已经被释放了,这里就会出现非法访问。
在这里插入图片描述
因此,如果函数返回时,出了函数作用域,如果返回对象还在(还没还给系统),则可以使用
引用返回,如果已经还给系统了,则必须使用传值返回。

传值、传引用效率比较

引用传参和值传参的性能比较

执行下方代码我们可以发现引用传参和引用做返回值的效率明显高于只传参和值返回。

#include <iostream>
#include <time.h>
using namespace std;
struct Array
{
	int arr[1000000];
}
Array testByValue(Array a)
{
	return a;
}
Array& testByReference(Array& a)
{
	return a;
}
int main()
{
	Array local;
	size_t begin1 = clock();
	testByValue(local);
	size_t end1 = clock();

	size_t begin2 = clock();
	testByReference(local);
	size_t end2 = clock();
	
	cout << "By Value :" << end1 - begin1 << endl;
	cout << "By Erference :" << end2 - begin2 << endl;

	return 0;
}

以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低。

上面程序中,调用testByReference之后,将local拷贝给testByReference栈帧空间中的a变量,在函数执行前,testByReference函数会在函数调用处,也就是main函数栈帧内开辟一个临时空间,并将返回值Array 放到这个空间内;在函数执行完毕后,再将临时数据拷贝给local。这个过程中总共创建2次local的副本,并进行了3次拷贝工作。因而使用值传递既浪费实际又浪费空间。
在这里插入图片描述

引用和指针的区别

在语法概念上引用就是一个别名,没有独立空间,与其引用实体共用一块内存空间。但在底层实现上实际是有空间的,因为引用是按照指针方式来实现的。(类似于指针常量,指针指向的值可以改,指针的指向不能修改)

#include <iostream>
using namespace std;
int main()
{
	int a = 10;
	int& ra = a;
	ra = 20;
	int* pa = &a;
	*pa = 30;
	return 0;
}

通过查看上述代码的反汇编,可以查找到a、ra、pa的地址,它们的地址各不相同,说明ra本质上还是占内存空间的。
在这里插入图片描述
引用和指针的区别(总结)

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

🎈欢迎进入浅尝C++专栏,查看更多文章。
如果上述内容有任何问题,欢迎在下方留言区指正b( ̄▽ ̄)d

  • 23
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值