参照パラメーターの使用方法を理解しようとしています。私のテキストにはいくつかの例がありますが、それらが複雑すぎて、その理由と使用方法を理解することはできません。
参照を使用する方法と理由は何ですか?パラメーターを参照せず、代わりに&
をオフのままにするとどうなりますか?
たとえば、これらの機能の違いは何ですか:
int doSomething(int& a, int& b);
int doSomething(int a, int b);
フォーマル->参照を変更するために参照変数が使用されることを理解しています。これにより、双方向のパラメーター交換が可能になります。しかし、それは私の知識の範囲であり、より具体的な例が大いに役立つでしょう。
参照を エイリアス と考えてください。参照で何かを呼び出すと、参照が参照するオブジェクトで実際に呼び出します。
_int i;
int& j = i; // j is an alias to i
j = 5; // same as i = 5
_
機能に関しては、次のことを考慮してください。
_void foo(int i)
{
i = 5;
}
_
上記の_int i
_は値であり、渡される引数はby by valueで渡されます。つまり、次のように言えば:
_int x = 2;
foo(x);
_
i
は、x
の-copyになります。したがって、i
を5に設定しても、x
は変更されるため、x
には影響しません。ただし、i
を参照にする場合:
_void foo(int& i) // i is an alias for a variable
{
i = 5;
}
_
そして、foo(x)
はx
のコピーを作成しなくなります。 i
isx
。したがって、foo(x)
と言うと、関数内の_i = 5;
_は_x = 5;
_とまったく同じであり、x
が変更されます。
うまくいけば、少しわかりやすくなります。
何でこれが大切ですか?プログラムするとき、neverコードをコピーして貼り付けます。 1つのタスクを実行し、それが適切に機能する関数を作成します。そのタスクを実行する必要があるときはいつでも、その機能を使用します。
したがって、2つの変数を交換したいとしましょう。次のようになります。
_int x, y;
// swap:
int temp = x; // store the value of x
x = y; // make x equal to y
y = temp; // make y equal to the old value of x
_
いいですねswap(x, y);
の方がはるかに読みやすいため、これを関数にしたいと思います。だから、これを試してみましょう:
_void swap(int x, int y)
{
int temp = x;
x = y;
y = temp;
}
_
これは機能しません!問題は、これが2つの変数のスワップコピーであるということです。あれは:
_int a, b;
swap(a, b); // hm, x and y are copies of a and b...a and b remain unchanged
_
参照が存在しないCでは、解決策はこれらの変数のアドレスを渡すことでした。つまり、ポインターを使用します*:
_void swap(int* x, int* y)
{
int temp = *x;
*x = *y;
*y = temp;
}
int a, b;
swap(&a, &b);
_
これはうまく機能します。ただし、使用するのは少し面倒で、実際には少し安全ではありません。 swap(nullptr, nullptr)
は、2つの何も交換せず、nullポインターを逆参照します...未定義の動作です!いくつかのチェックで修正可能:
_void swap(int* x, int* y)
{
if (x == nullptr || y == nullptr)
return; // one is null; this is a meaningless operation
int temp = *x;
*x = *y;
*y = temp;
}
_
しかし、コードがどれほど不器用になったかに見えます。 C++では、この問題を解決するための参照が導入されています。変数を単にエイリアス化できる場合、探していたコードを取得します。
_void swap(int& x, int& y)
{
int temp = x;
x = y;
y = temp;
}
int a, b;
swap(a, b); // inside, x and y are really a and b
_
使いやすく安全です。 (誤ってnullを渡すことはできません。null参照はありません。)これは、関数の内部で発生するスワップが、関数の外部でエイリアスされている変数で実際に発生するためです。
(注:swap
関数を記述しないでください。:)ヘッダー_<algorithm>
_には既に1つ存在し、どの型でも機能するようにテンプレート化されています。)
別の用途は、関数を呼び出すときに発生するコピーを削除することです。非常に大きなデータ型があると考えてください。このオブジェクトのコピーには多くの時間がかかりますが、それを避けたいと思います。
_struct big_data
{ char data[9999999]; }; // big!
void do_something(big_data data);
big_data d;
do_something(d); // ouch, making a copy of all that data :<
_
しかし、本当に必要なのは変数のエイリアスだけなので、それを示しましょう。 (繰り返しになりますが、Cに戻り、ビッグデータ型のアドレスを渡し、コピーの問題を解決しますが、不器用なものを導入します。):
_void do_something(big_data& data);
big_data d;
do_something(d); // no copies at all! data aliases d within the function
_
これが、プリミティブ型でない限り、常に参照渡しする必要があると言われるのが聞こえる理由です。 (Cのように、エイリアスを内部的に渡すことはおそらくポインターで行われるためです。小さなオブジェクトの場合、コピーを作成してからポインターを心配する方が速いです。)
Const-correctであることを忘れないでください。これは、関数がパラメーターを変更しない場合、const
としてマークすることを意味します。上記の_do_something
_だけを見て、data
を変更しなかった場合、const
としてマークします。
_void do_something(const big_data& data); // alias a big_data, and don't change it
_
コピーを避けますand「ちょっと、これは変更しません」と言います。これには他の副作用(一時変数など)がありますが、今は心配する必要はありません。
対照的に、エイリアスを実際に変更しているため、swap
関数をconst
にすることはできません。
これがさらに明確になることを願っています。
*ラフポインターチュートリアル:
ポインターは、別の変数のアドレスを保持する変数です。例えば:
_int i; // normal int
int* p; // points to an integer (is not an integer!)
p = &i; // &i means "address of i". p is pointing to i
*p = 2; // *p means "dereference p". that is, this goes to the int
// pointed to by p (i), and sets it to 2.
_
したがって、ポインターバージョンのスワップ関数を見た場合は、スワップする変数のアドレスを渡し、値を取得および設定するために逆参照して、スワップを実行します。
引数をインクリメントするincrement
という名前の関数の簡単な例を見てみましょう。考慮してください:
void increment(int input) {
input++;
}
実際のパラメーターで関数に渡された引数のコピーで変更が行われるため、機能しません。そう
int i = 1;
std::cout<<i<<" ";
increment(i);
std::cout<<i<<" ";
出力として1 1
を生成します。
渡された実際のパラメーターで関数を機能させるには、そのreference
を関数に次のように渡します。
void increment(int &input) { // note the &
input++;
}
関数内でinput
に加えられた変更は、実際のパラメーターに実際に加えられています。これにより、1 2
の期待される出力が生成されます
GManの答えは、参考文献の詳細を提供します。参照を使用する必要がある非常に基本的な関数swap
を示したいと思いました。これは2つの変数を交換します。 int
sの場合(要求どおり):
// changes to a & b hold when the function exits
void swap(int& a, int& b) {
int tmp = a;
a = b;
b = tmp;
}
// changes to a & b are local to swap_noref and will go away when the function exits
void swap_noref(int a, int b) {
int tmp = a;
a = b;
b = tmp;
}
// changes swap_ptr makes to the variables pointed to by pa & pb
// are visible outside swap_ptr, but changes to pa and pb won't be visible
void swap_ptr(int *pa, int *pb) {
int tmp = *pa;
*pa = *pb;
*pb = tmp;
}
int main() {
int x = 17;
int y = 42;
// next line will print "x: 17; y: 42"
std::cout << "x: " << x << "; y: " << y << std::endl
// swap can alter x & y
swap(x,y);
// next line will print "x: 42; y: 17"
std::cout << "x: " << x << "; y: " << y << std::endl
// swap_noref can't alter x or y
swap_noref(x,y);
// next line will print "x: 42; y: 17"
std::cout << "x: " << x << "; y: " << y << std::endl
// swap_ptr can alter x & y
swap_ptr(&x,&y);
// next line will print "x: 17; y: 42"
std::cout << "x: " << x << "; y: " << y << std::endl
}
一時的なものを必要としないint
sの賢いスワップ実装があります。ただし、ここでは賢いというよりも明確なことを重視しています。
参照(またはポインター)なし、swap_noref
は渡された変数を変更できません。つまり、単に機能することはできません。 swap_ptr
は変数を変更できますが、乱雑なポインターを使用します(ただし、参照で完全にカットされない場合は、ポインターで処理できます)。 swap
は全体的に最も単純です。
ポインタを使用すると、参照と同じことができます。ただし、ポインターは、プログラマーとそれらが指すメモリーを管理する責任をプログラマーに課します(「 memory management "–今のところは心配しないでください」というトピック)。結果として、現時点では、リファレンスが優先ツールになります。
変数は、値を保存するボックスにバインドされた名前と考えてください。定数は、値に直接バインドされた名前です。どちらも名前を値にマップしますが、定数の値は変更できません。ボックスに保持されている値は変更できますが、名前のボックスへのバインドは変更できません。そのため、参照を変更して別の変数を参照することはできません。
変数に対する2つの基本的な操作は、現在の値を取得すること(変数の名前を使用して単純に行われます)と、新しい値を割り当てることです(代入演算子 '=')。値はメモリに保存されます(値を保持するボックスは、メモリの連続した領域です)。例えば、
int a = 17;
結果は次のようになります(注:以下では、「foo @ 0xDEADBEEF」は、アドレス「0xDEADBEEF」に保存された「foo」という名前の変数を表します。メモリアドレスは構成されています)。
____
a @ 0x1000: | 17 |
----
メモリに保存されているものにはすべて開始アドレスがあるため、もう1つの操作があります。値のアドレスを取得します(「&」はアドレスの演算子です)。ポインターは、アドレスを格納する変数です。
int *pa = &a;
結果:
______ ____
pa @ 0x10A0: |0x1000| ------> @ 0x1000: | 17 |
------ ----
ポインターは単にメモリアドレスを保存するだけなので、ポインターが指す名前にはアクセスできないことに注意してください。実際、ポインターは名前のないものを指すことができますが、それは別の日のトピックです。
ポインターにはいくつかの操作があります。ポインター( "*"演算子)を逆参照できます。これにより、ポインターが指すデータが提供されます。参照解除は、アドレスを取得することの反対です:*&a
はa
と同じボックスです、&*pa
はpa
と同じ値で、*pa
はa
と同じボックスです。特に、例のpa
は0x1000を保持します。 * pa
は、「場所paのメモリ内のint」、または「場所0x1000のメモリ内のint」を意味します。 「a」は「メモリ位置0x1000のint」でもあります。ポインターに対する他の操作は加算と減算ですが、それはまた別の日のトピックです。
// Passes in mutable references of a and b.
int doSomething(int& a, int& b) {
a = 5;
cout << "1: " << a << b; // prints 1: 5,6
}
a = 0;
b = 6;
doSomething(a, b);
cout << "2: " << a << ", " << b; // prints 2: 5,6
あるいは、
// Passes in copied values of a and b.
int doSomething(int a, int b) {
a = 5;
cout << "1: " << a << b; // prints 1: 5,6
}
a = 0;
b = 6;
doSomething(a, b);
cout << "2: " << a << ", " << b; // prints 2: 0,6
またはconstバージョン:
// Passes in const references a and b.
int doSomething(const int &a, const int &b) {
a = 5; // COMPILE ERROR, cannot assign to const reference.
cout << "1: " << b; // prints 1: 6
}
a = 0;
b = 6;
doSomething(a, b);
参照は変数の場所を渡すために使用されるため、スタック上で新しい関数にコピーする必要はありません。
メタファーによる方法:関数がjar内のBeanをカウントするとします。 Beanのjarファイルが必要であり、戻り値にはならない結果を知る必要があります(理由はさまざまです)。 jarと変数値を送信できますが、値が変更されるかどうか、または何に変更されるかはわかりません。代わりに、リターンアドレス付きエンベロープを介してその変数を送信する必要があります。そのため、その中に値を入れて、そのアドレスの値に結果が書き込まれたことを知ることができます。
これが最も基本的なものかどうかはわかりませんが、ここに行きます...
_typedef int Element;
typedef std::list<Element> ElementList;
// Defined elsewhere.
bool CanReadElement(void);
Element ReadSingleElement(void);
int ReadElementsIntoList(int count, ElementList& elems)
{
int elemsRead = 0;
while(elemsRead < count && CanReadElement())
elems.Push_back(ReadSingleElement());
return count;
}
_
ここでは、参照を使用して、要素のリストをReadElementsIntoList()
に渡します。このようにして、関数は要素をリストに直接ロードします。参照を使用しなかった場合、elems
は、渡されたリストのcopyになり、要素が追加されますが、elems
は、関数が戻るときに破棄されます。
これは両方の方法で機能します。 count
の場合、渡されたカウントを変更したくないため、do n'tを参照にします。代わりに、読み取った要素の数を返します。これにより、呼び出し元のコードは、実際に読み取られた要素の数を要求された数と比較できます。それらが一致しない場合、CanReadElement()
はfalse
を返さなければならず、すぐにそれ以上読み込もうとすると失敗する可能性があります。それらが一致する場合、count
が使用可能な要素の数よりも少ない可能性があり、さらに読み込む必要があります。最後に、ReadElementsIntoList()
がcount
を内部的に変更する必要がある場合、呼び出し側を台無しにすることなく変更できます。
私が間違っているが、参照は間接参照されたポインタにすぎない場合、私を修正しますか?
ポインターとの違いは、NULLを簡単にコミットできないことです。