函数返回值是否使用引用类型的问题:理解引用、返回值

简介:   在《对象更有用的玻璃罩——常引用》一文中,介绍了对象作为函数的参数时,推荐使用引用的形式。并且,如果实际参数的值不允许改变时,声明为常引用更佳。   在《第8周-任务1-方案3-复数类中运算符重载(与实数运算)》中,又讨论了一个问题,结论是:在类似复数加法运算符重载这样的函数,形式参数用作为常引用最佳,如: friend Complex operator + (const

  在《对象更有用的玻璃罩——常引用》一文中,介绍了对象作为函数的参数时,推荐使用引用的形式。并且,如果实际参数的值不允许改变时,声明为常引用更佳。

  在《第8周-任务1-方案3-复数类中运算符重载(与实数运算)》中,又讨论了一个问题,结论是:在类似复数加法运算符重载这样的函数,形式参数用作为常引用最佳,如:

friend Complex operator + (const Complex &c, const double &d);
friend Complex operator+ (const double&d, const Complex &c);
  这样做的好处在于:(1)保证在运算中,c 与 d 的值不会意外被 修改;(2)d 对应的实际参数可以为常量也可以为变量。

  在本文中,主要讨论函数(尤其是与类相关的函数——成员函数或友元函数)返回值类型何时用引用的问题。


  一、函数返回值为引用的典型案例

  在做输入输出重载时,重载函数返回流对象,如:

//例程1:复数类中运算符的重载
#include <iostream>
using namespace std;
class Complex
{
public:
	Complex(){real=0;imag=0;}
	Complex(double r,double i){real=r;imag=i;}
	Complex operator-();
	//实现输入、输出的运算符重载
	friend ostream& operator << (ostream& output,Complex& c);
	friend istream& operator >> (istream& input,Complex& c);
	//实现加减乘除的运算符重载
	friend Complex operator+(Complex &c1, Complex &c2);
	friend Complex add(double d1, Complex &c2);
private:
	double real;
	double imag;
};

//实现输出的运算符重载
ostream& operator << (ostream& output,Complex& c)
{	output<<"("<<c.real;
	if(c.imag>=0) output<<"+";  
	output<<c.imag<<"i)";    
	return output;
}

//实现输入的运算符重载
istream& operator >> (istream& input,Complex& c)
{	int a,b;
	char sign,i;
	do
	{	cout<<"input a complex number(a+bi或a-bi):";
		input>>a>>sign>>b>>i;
	}
	while(!((sign=='+'||sign=='-')&&i=='i'));
	c.real=a;
	c.imag=(sign=='+')?b:-b;
	return input;
}

//复数相加:(a+bi)+(c+di)=(a+c)+(b+d)i. 
Complex operator+(Complex &c1, Complex &c2)
{
	Complex c;
	c.real=c1.real+c2.real;
	c.imag=c1.imag+c2.imag;
	return c;
}

Complex add(double d1, Complex &c2)
{
	Complex c;
	c.real=d1+c2.real;
	c.imag=c2.imag;
	return c; 
}

int main()
{
	Complex c1,c2,c3;
	double d=11;
	cout<<"c1: "; 
	cin>>c1;          
	cout<<"c2: ";
	cin>>c2;
	cout<<"c1="<<c1<<endl;
	cout<<"c2="<<c2<<endl;
	c3=c1+c2;
	cout<<"c1+c2="<<c3<<endl;
	c3=add(d,c1);
	cout<<"d+c1="<<c3<<endl;

	system("pause");
	return 0;
}

  在例程1的第11和12行,运算符重载函数的返回值类类被声明为引用;在第22行和30行,这两个函数的实现中,分别返回了输入流对象input和输出流对象output(要注意到这两个对象是作为形式参数出现的,且都为引用)。实际上,<<和>>运算符对其他类型重载时也是这样处理的。这样处理的好处在于,函数返回的输入/出流还可以继续用于其他数据的输入/出,使我们能用cin>>i,j;和cout<<i<<","<<j<<endl;的形式输入/出。

  以例程1中的第69行(cout<<"c1="<<c1<<endl; )为例。

  cout<<"c1="<<c1<<endl; 中首先执行的是oprate<<(cout,"c1=")。实际参数——输出流对象cout(本身为引用)传递给形式参数output,在完成输出字符串"c1="的任务后,cout这个引用对象作为返回值返回到被调用处,于是余下的未执行的部分相当于:cout<<c1<<endl;。这是例程1中定义的重载发挥作用的时候了,输出c1后,cout被返回,再次用于输出换行符endl。

  这里值得总结的是:(1)返回值为引用时,返回的变量仍然要继续完成相关的工作;(2)返回的引用值本身也必须是引用,一般是在调用函数中存在的,以引用型形式参数的方式传递到函数中的变量(例程1中的input和output为引用)。


  二、一个令人惊讶的程序:给函数的返回值赋值

  这个例子来自《C++ Primer(第四版)》。

//例程2:给函数的返回值赋值
#include <iostream>
#include<string>
using namespace std;
char &get_val(string &str, string::size_type ix)
{
	return str[ix];
}

int main()
{
	string s("a value");
	cout<<s<<endl;  //输出 a value
	get_val(s,0)='A';   //函数调用一般是不能作为赋值运算的左值的,但这儿居然没有错,只因为返回值为引用
	cout<<s<<endl;  //输出为 A value,不可思议的改变
	system("pause");
	return 0;
}
  正如注释中所讲,返回值为引用,函数调用get_val(s,0)居然可以作为赋值表达式的左值,就这样,其引用的空间(s[0])中所存储的字符被赋值为‘A’,“引用是被返回元素的同义词”,此处完全等同于s[0]='A'。

  程序从语法上讲没有问题,运行结果也达到了举例的目的。本例仅在于展示这种用法,理解引用作为函数返回值。在工程中,这种风格的程序当然不推荐使用,当不希望引用返回值被修改时,将返回值声明为const,即:

  const char &get_val(string &str, string::size_type ix)


  三、加法运算的重载结果也定义为引用,如何?

  对于例程1,将其中的加法重载函数的返回值也定义为引用(当然,这样做纯属撞错),结果会怎样?先给出这样的程序,请注意在第14、15、44和52行中增加的&。

//例程3:复数类中运算符的重载,加法函数返回引用
#include <iostream>
using namespace std;
class Complex
{
public:
	Complex(){real=0;imag=0;}
	Complex(double r,double i){real=r;imag=i;}
	Complex operator-();
	//实现输入、输出的运算符重载
	friend ostream& operator << (ostream& output,Complex& c);
	friend istream& operator >> (istream& input,Complex& c);
	//实现加减乘除的运算符重载
	friend Complex& operator+(Complex &c1, Complex &c2);
	friend Complex& add(double d1, Complex &c2);
private:
	double real;
	double imag;
};

//实现输出的运算符重载
ostream& operator << (ostream& output,Complex& c)
{	output<<"("<<c.real;
	if(c.imag>=0) output<<"+";  
	output<<c.imag<<"i)";     
	return output;
}

//实现输入的运算符重载
istream& operator >> (istream& input,Complex& c)
{	int a,b;
	char sign,i;
	do
	{	cout<<"input a complex number(a+bi或a-bi):";
		input>>a>>sign>>b>>i;
	}
	while(!((sign=='+'||sign=='-')&&i=='i'));
	c.real=a;
	c.imag=(sign=='+')?b:-b;
	return input;
}

//复数相加:(a+bi)+(c+di)=(a+c)+(b+d)i. 
Complex& operator+(Complex &c1, Complex &c2)
{
	Complex c;
	c.real=c1.real+c2.real;
	c.imag=c1.imag+c2.imag;
	return c;
}

Complex& add(double d1, Complex &c2)
{
	Complex c;
	c.real=d1+c2.real;
	c.imag=c2.imag;
	return c; 
}

int main()
{
	Complex c1,c2,c3;
	double d=11;
	cout<<"c1: "; 
	cin>>c1;          
	cout<<"c2: ";
	cin>>c2;
	cout<<"c1="<<c1<<endl;
	cout<<"c2="<<c2<<endl;
	c3=c1+c2;
	cout<<"c1+c2="<<c3<<endl;
	c3=add(d,c1);
	cout<<"d+c1="<<c3<<endl;

	system("pause");
	return 0;
}
  这个程序运行结果 可能与例程1的结果是一样的。的确,在我运行中,结果没有出现过异常。这个程序在VS2008下编译时会有两个警告:

  1>d:\c++\vs2008 project\example\example\example.cpp(49) : warning C4172: 返回局部变量或临时变量的地址
  1>d:\c++\vs2008 project\example\example\example.cpp(57) : warning C4172: 返回局部变量或临时变量的地址

  这两个警告道出了危险所在:第49行和57行返回临时变量c之后,c 的空间将被释放,也就意味着可以由系统进行再分配,作其他用途使用了。而返回值类型为引用时,返回的值仍然在使用着这一块内存区域。结果只能是“可能”正确,毫无保障。好比付款买了房(对方收条都没有开),房产证却放在房管局大厅中,哪一天你被赶出家门,那是活该的。这个简单的例子没有出问题纯属意外,因为操作简单,那片空间还没有被重新分配。

  那什么时候更可能出问题呢?如果返回的对象中包含动态分配的空间(见《何时需要自定义复制构造函数?》),出问题几乎是肯定的。

  但是,最值得警惕的还是那些出问题可能性更小的时候,“不以恶小而为之”,一贯正常,只有很小几率出的错的情况更可怕。

  最重要的,理解了上述道理,要做到:千万不要返回对局部变量的引用

  还有一条类似的:千万不要返回局部变量的指针。


  四、钻个牛角尖:operate+就要返回引用

  重温本文第一部分最后的加粗字:(2)返回的引用值本身也必须是引用,一般是在调用函数中存在的,以引用型形式参数的方式传递到函数中的变量(例程1中的input和output为引用)。要应付这种胡搅蛮缠式的要求,我们也只能围绕这个要求想办法。

  给出的一种解决办法是:

//例程4:复数类中运算符的重载,加法函数返回引用
#include <iostream>
using namespace std;
class Complex
{
public:
	Complex(){real=0;imag=0;}
	Complex(double r,double i){real=r;imag=i;}
	Complex operator-();
	//实现输入、输出的运算符重载
	friend ostream& operator << (ostream& output,Complex& c);
	//实现加减乘除的运算符重载
	friend Complex& operator+(Complex &c1, Complex &c2);
private:
	double real;
	double imag;
};

//实现输出的运算符重载
ostream& operator << (ostream& output,Complex& c)
{	output<<"("<<c.real;
	if(c.imag>=0) output<<"+";  
	output<<c.imag<<"i)";     
	return output;
}

//复数相加:(a+bi)+(c+di)=(a+c)+(b+d)i. 
Complex& operator+(Complex &c1, Complex &c2)
{
	c1.real=c1.real+c2.real;
	c1.imag=c1.imag+c2.imag;
	return c1;
}

int main()
{
	Complex c1(3,4),c2(5,-10),c3;
	cout<<"c1="<<c1<<endl;
	cout<<"c2="<<c2<<endl;
	c3=c1+c2;
	cout<<"c1="<<c1<<endl;
	cout<<"c2="<<c2<<endl;
	cout<<"c1+c2="<<c3<<endl;

	system("pause");
	return 0;
}
  这种实现中,c1在调用operate+()前已经存在,是作为引用进行参数传递的。这样的变量能够保证程序不会出现意外。但是,会出的代价是,c1的值在参与加法运算时被改变了。在第40行执行了c3=c1+c2后,第41行显示的c1的值同c3的值相同,是相加后的结果。

  这种安排也只能接受这种结局。事实上,学习计算机的同学也要接受这种风格,在有些语言(例如,汇编以及被冠以高雅称号的函数式语言)中,运算就是这么完成的。c1+c2怎么完成?add c1, c2; 其结果如何取出?结果就保存到第一个运算量中。

  牛角尖再钻深些,不能这样做!我只能为返回引用再使一招了:提前定义保存结果的变量(例c),并将之作为参数传递到函数中。付出的代价是,加运算的运算量成了3个,operate+()形式是不能用了(运算符重载不能改变其目数)。实际上,返回的那个引用也没有什么意思了,结果已经由引用 c 带回来了,返回值甚至可以为void。这种设计太差了,程序依然贴在下面,读者不看也罢。

//例程5:复数类中运算符的重载,加法函数返回引用
#include <iostream>
using namespace std;
class Complex
{
public:
	Complex(){real=0;imag=0;}
	Complex(double r,double i){real=r;imag=i;}
	Complex operator-();
	//实现输入、输出的运算符重载
	friend ostream& operator << (ostream& output,Complex& c);
	//实现加减乘除的运算符重载
	friend Complex& add(const Complex &c1, const Complex &c2, Complex &c3); //为确保两个运算量不被改变,特加了const
private:
	double real;
	double imag;
};

//实现输出的运算符重载
ostream& operator << (ostream& output,Complex& c)
{	output<<"("<<c.real;
	if(c.imag>=0) output<<"+";  
	output<<c.imag<<"i)";     
	return output;
}

//复数相加:(a+bi)+(c+di)=(a+c)+(b+d)i. 
Complex& add(const Complex &c1, const Complex &c2, Complex &c3)
{
	c3.real=c1.real+c2.real;
	c3.imag=c1.imag+c2.imag;
	return c3;
}

int main()
{
	Complex c1(3,4),c2(5,-10),c,c3;
	cout<<"c1="<<c1<<endl;
	cout<<"c2="<<c2<<endl;
	c3=add(c1,c2,c);
	cout<<"c1="<<c1<<endl;
	cout<<"c2="<<c2<<endl;
	cout<<"c="<<c<<endl;
	cout<<"c1+c2="<<c3<<endl;

	system("pause");
	return 0;
}



  

<本文完>

目录
相关文章
|
编译器 C++ Python
【C/C++ 泡沫精选面试题02】深拷贝和浅拷贝之间的区别?
【C/C++ 泡沫精选面试题02】深拷贝和浅拷贝之间的区别?
456 1
|
存储 测试技术 开发工具
软考中的UML图、数据流图等二十余种示例
软考中的UML图、数据流图等二十余种示例
3081 0
|
7月前
|
设计模式 算法 Java
软考中级软件设计师专项-设计模式篇
备战软考中级软件设计师?本文聚焦高分设计模式模块,详解23种模式的核心意图与场景,结合UML图、Java代码实例及历年真题,覆盖创建型、结构型、行为型三大类,助你打通理论到实战。
605 1
软考中级软件设计师专项-设计模式篇
|
11月前
|
人工智能 小程序 计算机视觉
AI不只有大模型,小模型也蕴含着大生产力
近年来,AI大模型蓬勃发展,从ChatGPT掀起全球热潮,到国内“百模大战”爆发,再到DeepSeek打破算力壁垒,AI技术不断刷新认知。然而,在大模型备受关注的同时,许多小而精的细分模型却被忽视。这些轻量级模型无需依赖强大算力,可运行于手机、手持设备等边缘终端,广泛应用于物体识别、条码扫描、人体骨骼检测等领域。例如,通过人体识别模型衍生出的运动与姿态识别能力,已在AI体育、康复训练、线上赛事等场景中展现出巨大潜力,大幅提升了相关领域的效率与应用范围。本文将带您深入了解这些高效的小模型及其实际价值。
|
存储 编译器 C++
【c++】多态(多态的概念及实现、虚函数重写、纯虚函数和抽象类、虚函数表、多态的实现过程)
本文介绍了面向对象编程中的多态特性,涵盖其概念、实现条件及原理。多态指“一个接口,多种实现”,通过基类指针或引用来调用不同派生类的重写虚函数,实现运行时多态。文中详细解释了虚函数、虚函数表(vtable)、纯虚函数与抽象类的概念,并通过代码示例展示了多态的具体应用。此外,还讨论了动态绑定和静态绑定的区别,帮助读者深入理解多态机制。最后总结了多态在编程中的重要性和应用场景。 文章结构清晰,从基础到深入,适合初学者和有一定基础的开发者学习。如果你觉得内容有帮助,请点赞支持。 ❤❤❤
1474 0
|
设计模式 Java 程序员
【23种设计模式·全精解析 | 概述篇】设计模式概述、UML图、软件设计原则
本系列文章聚焦于面向对象软件设计中的设计模式,旨在帮助开发人员掌握23种经典设计模式及其应用。内容分为三大部分:第一部分介绍设计模式的概念、UML图和软件设计原则;第二部分详细讲解创建型、结构型和行为型模式,并配以代码示例;第三部分通过自定义Spring的IOC功能综合案例,展示如何将常用设计模式应用于实际项目中。通过学习这些内容,读者可以提升编程能力,提高代码的可维护性和复用性。
3614 1
【23种设计模式·全精解析 | 概述篇】设计模式概述、UML图、软件设计原则
|
安全 数据安全/隐私保护 C++
C++一分钟之-成员访问控制:public, private, protected
【6月更文挑战第20天】C++的成员访问控制涉及`public`、`private`和`protected`,影响类成员的可见性和可访问性。`public`成员对外公开,用于接口;`private`成员仅限类内部,保护数据安全;`protected`成员在派生类中可访问。常见问题包括不恰当的访问级别选择、继承中的访问权限误解及过度使用友元。通过示例展示了如何在派生类中访问`protected`成员。正确使用访问修饰符能确保代码的封装性、安全性和可维护性。
846 4
|
小程序 前端开发 定位技术
微信小程序-常用的视图容器类组件
该内容是关于微信小程序组件的分类和部分具体组件的介绍。主要分为9大类:视图容器、基础内容、表单组件、导航组件、媒体组件、地图组件、画布组件、开放能力和无障碍访问。其中详细讲解了`view`、`scroll-view`、`swiper`及`swiper-item`等组件的用途和示例。`view`用于构建页面布局,`scroll-view`支持滚动效果,`swiper`则用于创建轮播图。此外,还提到了`root-portal`、`page-container`等其他特殊用途的组件。
458 0
|
编译器 C++
VS2022查看类内存布局
先右键点击属性, 选择左侧的C/C++==>命令行,然后在其他选项这里写上/d1 reportAllClassLayout,它可以看到所有相关类的内存布局。切切注意, Layout跟指定的结构/类名CTest之间没有空格, 有空格就不对了. 这会只输出指定的结构的内存布局.这个开关输出所有类, 主要是一大堆编译器内部的结构的内存布局, 其实还有一个开关是。
548 0
VScode中C++多文件编译运行问题(使用code runner配置)
VScode中C++多文件编译运行问题(使用code runner配置)