関数の戻り値が参照タイプを使用するかどうかの問題:参照、戻り値の理解


「オブジェクトがより役に立つガラスカバー-常引用」では、関数のパラメータとしてオブジェクトを紹介する際に、参照の形式を推奨します.また、実際のパラメータの値が変更できない場合は、通常参照として宣言したほうがよい.
「第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行目の2つの関数の実装では、入力ストリームオブジェクトinputと出力ストリームオブジェクトoutputをそれぞれ返します(両方のオブジェクトが形式パラメータとして表示され、参照されていることに注意してください).実際には、<<および>>演算子が他のタイプのリロード時にもこのように処理されます.このような処理の利点は、関数が返す入力/アウトフローが他のデータの入力/アウトにも引き続き使用され、cin>>i,j;およびcout<インスタンス1の69行目(cout<//例程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の結果と同じかもしれません.確かに、私が運転している間に、異常は発生しませんでした.このプログラムはVS 2008でコンパイルされると2つの警告があります.
  1>d:\c++\vs2008 project\example\example\example.cpp(49):warning C 4172:ローカル変数または一時変数を返すアドレス1>d:c+vs 2008 projectexampleexampleexampleexample.cpp(57):warning C 4172:ローカル変数または一時変数のアドレスを返す
この2つの警告は、49行目と57行目が一時変数cに戻ると、cの空間が解放され、システムによって再割り当てされ、他の用途として使用できることを意味する危険性を示している.戻り値タイプが参照の場合、戻り値はこのメモリ領域を使用します.結果は「可能」が正しいだけで、保障はない.例えば、お金を払って家を買った(相手の領収書が開いていない)のに、不動産証明書が部屋管理局のロビーに置いてあって、いつ家を追い出されたのか、それは当然です.この簡単な例が問題にならないのは意外です.操作が簡単なので、その空間はまだ再分配されていません.
では、いつ問題が発生する可能性がありますか?返されるオブジェクトに動的に割り当てられたスペースが含まれている場合(『レプリケーションコンストラクタをカスタマイズする必要があるのはいつですか?』)を参照)、問題が発生したのはほとんど肯定的だ.
しかし、最も警戒すべきは、問題が発生する可能性が低いとき、「悪を小さくしない」ことであり、一貫して正常であり、小さな確率で間違いが発生する場合だけが恐ろしい.
最も重要なのは、上記の道理を理解して、決して局所変数への参照を返さないことです.
ローカル変数のポインタを返さないでください.
四、牛の角の先をくぐる:operate+は参照を返す
本明細書の第1部の最後の太字を再確認する:(2)返される参照値自体も参照でなければならない.一般的には呼び出し関数に存在し、参照型形式のパラメータで関数に伝達される変数(ルーチン1のinputとoutputを参照)である.このようなでたらめな要求に対処するには、この要求をめぐって方法を考えるしかない.
解決策の1つは次のとおりです.
//  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;
}
この実装では、c 1はoperate+()を呼び出す前に既に存在し、参照としてパラメータ伝達される.このような変数は、プログラムが予期せぬ事故を起こさないことを保証します.しかし,c 1の値は加算に関与すると変化する.40行目にc 3=c 1+c 2が実行された後、41行目に表示されるc 1の値はc 3の値と同じであり、加算された結果である.
このような手配もこのような結末を受け入れるしかない.実際、コンピューターを学ぶ学生もこのようなスタイルを受け入れなければならない.一部の言語(例えば、アセンブリや上品な称号を冠した関数式言語)では、演算はこのように完成している.c 1+c 2はどのように完成しているのか.add c 1,c 2;その結果はどのように取り出しているのか.結果は最初の演算量に保存されている.
牛の角の先はもっと深く掘って、このようにすることはできません!リファレンスを返すためにもう1つの方法しかありません.保存結果の変数(例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;
}

  
<本文完>