【C++入门】函数重载,引用

目录

一、函数重载

1.函数重载概念

2.函数重载的原理 

 二、引用

1.引用概念

 2、引用特性

3.常引用

4. 引用的使用场景

1).做参数

2).做返回值

5.引用和指针的区别


一、函数重载

1.函数重载概念

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

void f(int a)
{
	cout << "f(int a)" << endl;
}
  • 参数类型不同:
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;
}
  • 类型顺序不同
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;
}
  • 注:单单改变函数的返回类型是不能构成重载的。

2.函数重载的原理 

  1. 实际项目通常是由多个头文件和多个源文件构成。首先,我们假设有a.cpp、b.cpp两个文件,在b.cpp中定义了Add函数,a.cpp调用了b.cpp中定义的Add函数。 通过C语言编译链接的知识,我们可以知道,编译后链接前,a.o的目标文件中没有Add的函数地址,因为Add是在b.cpp中定义的,所以Add的地址在b.o中。
  2. 链接阶段就会专门处理这种问题,链接器看到a.o调用Add,但是没有Add的地址,就会到b.o的符号表中找Add的地址,然后链接到一起
  3. 那么链接时,面对Add函数,链接接器会使用哪个名字去找呢?这里每个编译器都有自己的函数名修饰规则
  4. 由于Windows下vs的修饰规则过于复杂,而Linux下g++的修饰规则简单易懂,下面我们使用了g++演示了这个修饰后的名字。
  5. 通过下面我们可以看出,C语言用gcc编译后的函数修饰名字不变而C++用g++编译后的函数修饰名变成【_Z+函数长度+函数名+类型首字母】。

     gcc编译结果:

  • 结论:在linux下,采用gcc编译完成后,函数名字的修饰没有发生改变

       g++编译结果: 

  • 结论:在linux下,采用g++编译完成后,函数名字的修饰发生改变,编译器将函数参数类型信息添加到修改后的名字中。

通过上文我们就理解了C语言为什么没办法支持重载,因为同名函数没办法区分。而C++是通过函数修饰规则来区分,只要参数不同,修饰出来的名字就不一样,就支持了重载。

同时也解释了为什么单单的改变函数返回类型不能构成重载,在调用函数时我们都是直接用函数名调用,并不会写明返回类型,所以返回值并不会作为函数信息加入到修饰名字中

 二、引用

1.引用概念

概念:引用不是新定义一个变量,而 是给已存在变量取了一个别名 ,编译器不会为引用变量开辟内存空间,它和它引用的变量 共用同一块内存空间
格式:类型& 引用变量名(对象名)=引用实体,如下代码。
void TestRef()
{
    int a = 10;
    int& ra = a;//<====定义引用类型
    printf("%p\n", &a);
    printf("%p\n", &ra);
}

 这时变量ra就是变量a的别名,在内存中a和ra的地址相同,对ra修改,就等同于对a修改。

 2、引用特性

引用具有三大特性:
  1. 引用在 定义时必须初始化
  2. 一个变量 可以有多个引用
  3. 引用一旦引用一个实体,再不能引用其他实体
下面对他们进行解释。

 引用在定义时必须初始化

 可见,如果如果引用时没有初始化,还没有编译就会报错。


一个变量可以有多个引用

int main()
{
	int a = 10;
	int& ra = a;
	int& rra = ra;
	int& rrra = rra;
}

 在VS调试后发现,a,ra,rra,rrra都是a的别名,都是a的引用,可见一个变量可以有多个引用。


3.常引用

在给变量取别名时,不是可以随便取的,要注意变量的权限。

引用变量的权限只能等于或小于原变量的权限范围。(权限只能缩小不能放大

用const修饰一个变量,变量就会变成一个常变量,他的权限就从能读写变成了只能读权限被缩小了

 权限放大会报错

 如图,由于a加了const,它的权限降低为只能读,此时让b一个具有读写两种权限的变量成为a的应用显然是不合理的,编译器会发生报错。


权限统一

为了保持权限的统一,我们可以在b之前也假设const来降低b的权限,实现两者的权限相同,编译器就不会再报错。

int main()
{
	const int a = 10;
	//权限相同
	const int& b = a;
}

权限缩小 

能读写的a,取一个只能读的别名b同样也合理

int main()
{
	int a = 10;
	//权限缩小
	const int& b = a;
}

拓展1:常量取引用

我们可以给常量取别名,但是要加上const。如下。

const int& c = 20;

拓展2:临时变量具有常性

如下代码很明显存在问题,会发生报错:

int main()
{
	double a = 10.28;
	int& b = a;//error
}

 报错如下:

 但是在b前面加上const就可以解决问题。对比如下图:

这里,我们就要结合C语言中学的隐式类型转换来理解了,大给小会发生截断,小给大会发生提升。在double转换为int的过程中,就发生了截断,但是这需要借助一个临时变量来实现。double类型的a会把自生的值给临时变量tmp,同时发生截断,然后编辑器会把临时变量tmp的值赋给int类型的b,最终完成赋值。

 由于临时变量tmp为常量,只具有读的权限,赋值给有读写权限的int自然就会发生报错。只要在前面加上const让他们权限统一,就不会报错

但是我们需要注意的是,此时的b不再是a的别名,而是临时变量tmp的别名。由下图可见,两者的地址并不相同,不是同一个变量。

4. 引用的使用场景

1).做参数

引用做参数就是我们经常提到的函数传指针,这就是引用做参数的典型例子。

//函数传指针
void Swap(int* pa, int* pb)
{
	int tmp = *pa;
	*pa = *pb;
	*pb = tmp;
}

//函数传引用
void Swap(int& x, int& y)
{
	int tmp = x;
	x = y;
	y = tmp;
}

显而易见的是传引用的方法会更加的方便和简洁,不需要再通过指针进行繁琐的操作。


此外,引用还可以用作输出形参数。同样使代码更加的简洁

int* preorderTraversal(struct TreeNode* root, int* returnSize) {
    //……
}
int main()
{
    preorderTraversal(tree, &size);
}


int* preorderTraversal(struct TreeNode* root, int& returnSize) {
    //……
}
int main()
{
    preorderTraversal(tree, size);
}

总结:引用做参数的好处

  • 作输出形参数,优化代码
  • 减少指针的使用,简化代码

2).做返回值

根据函数栈帧的知识我们可知,当一个函数执行结束时,栈帧的使用权限会被归还给操作系统,为了防止返回值丢失,这时就需要创建一个临时变量来存储要返回的结果。如果返回类型如果较小,就会存储在寄存器中,如果较大,就会开辟新的空间来存储,这会导致程序效率降低。

证明存在中间的临时变量:

可见图中的发生了和上文提到的同样的报错,其原因就是临时变量具有常性,只能读。有读写两种权限的ret做临时变量的引用存在权限放大的问题。 (如下代码能解决这个问题)

//引用作为返回值
int& fun()
{
	static int  n = 0;
	n++;
	return n;
}

int main()
{
    //用引用ret来接收
	int& ret = fun();
	return 0;
}

这时加上引用之后,依然会产生一个临时变量n的值依然会赋值给这个临时变量,只是这个临时变量的类型时int*mian函数中再通过ret来接收临时变量,所以ret和临时变量其实就是n的别名,不需要额外开空间,实现了效率的提升。

总结:

  • 传值返回:会有一个拷贝
  • 传引用返回:没有这个拷贝了,返回的直接就是返回变量的别名

 但是有一点需要加以思考,如果把代码中的static去掉,引用作为返回值还是否正确?

                                                 答案是否定的!!!

 由于传引用的返回值是函数中那个变量的别名,如果函数中的变量不像static变量一样能在函数被销毁之后保留变量的值,那么传引用就会造成非法地址的访问

总结:

  • ​​​​​只有返回值是在函数销毁后依然存在的静态量时才能采用传引用。
  • 一般的变量只能采用传值返回

5.引用和指针的区别

引用和指针的不同点:

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值