56

参照パラメーターの使用方法を理解しようとしています。私のテキストにはいくつかの例がありますが、それらは複雑すぎて、なぜ、どのように使用するのか理解できません。

どのように、そしてなぜ参照を使用したいですか? パラメータを参照にせず、&オフのままにするとどうなるでしょうか?

たとえば、これらの関数の違いは何ですか:

int doSomething(int& a, int& b);
int doSomething(int a, int b);

形式参照を変更するために参照変数が使用されることを理解しています。これにより、パラメータの双方向交換が可能になります。しかし、それは私の知識の範囲であり、より具体的な例は非常に役立ちます.

4

8 に答える 8

126

参照はエイリアスと考えてください。参照で何かを呼び出すとき、実際には参照が参照するオブジェクトでそれを呼び出しています。

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は値であり、渡される引数はvalue によって渡されます。つまり、次のように言えます。

int x = 2;
foo(x);

iのコピーになりxます。したがってi、5 に設定しても、変更xのコピーであるため、 には影響しませんx。ただし、i参照を作成すると、次のようになります。

void foo(int& i) // i is an alias for a variable
{
    i = 5;
}

次に、 no と言うと、 ;foo(x)のコピーが作成されなくなりました。です。つまり、関数内は とまったく同じで、変化します。xi xfoo(x)i = 5;x = 5;x

うまくいけば、それは少し明確になります。


何でこれが大切ですか?プログラミングするときは、コードをコピーして貼り付けたくありません。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関数を書かないでください。:) 1 つは既に header<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-rect である必要があることに注意してください。これは、関数がパラメーターを変更しない場合、それを としてマークすることを意味しますconstdo_something上記のように を見ただけで変化しなかった場合は、次のようdataにマークしますconst

void do_something(const big_data& data); // alias a big_data, and don't change it

私たちはコピーを避け、「これは変更しません」と言います。これには他の副作用 (一時変数など) がありますが、今は心配する必要はありません。

対照的に、実際にエイリアスを変更しているため、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.

したがって、ポインター バージョン スワップ関数を見たことがある場合は、スワップする変数のアドレスを渡し、逆参照して値を取得および設定してスワップを実行します。

于 2010-04-02T03:45:58.147 に答える
4

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

于 2010-04-02T03:46:37.487 に答える
4

GMan's answer は、参照の詳細を示しています。参照を使用する必要がある非常に基本的な関数をお見せしたかっただけswapです。これは、2 つの変数を交換します。ここにints があります(あなたが要求したとおり):

// 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 実装があります。ただし、ここでは、賢いことよりも明確なことに関心があります。

参照 (またはポインター)swap_norefがないと、渡された変数を変更できません。つまり、単に機能しません。swap_ptr変数を変更することはできますが、ポインターを使用するため、面倒です (ただし、参照がうまくいかない場合は、ポインターで十分です)。swap全体的に最もシンプルです。

ポインターについて

ポインターを使用すると、参照と同じことがいくつかできます。ただし、ポインターを使用すると、ポインターとポインターが指すメモリーを管理する責任がプログラマーに課せられます (「メモリー管理」と呼ばれるトピックですが、今は心配する必要はありません)。結果として、参照は今のところあなたの好みのツールになるはずです。

変数は、値を格納するボックスにバインドされた名前と考えてください。定数は、値に直接バインドされた名前です。どちらも名前を値にマップしますが、定数の値は変更できません。ボックスに保持されている値は変更できますが、ボックスへの名前のバインドは変更できません。そのため、参照を変更して別の変数を参照することはできません。

変数に対する 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」でもあります。ポインターに対するその他の操作は加算と減算ですが、これも別の日のトピックです。

于 2010-04-02T04:13:39.030 に答える
1

オンラインで実行できる簡単な例のペア。

1 つ目は通常の関数を使用し、2 つ目は参照を使用します。


編集 - リンクが気に入らない場合のソースコードは次のとおりです。

例 1

using namespace std;

void foo(int y){
    y=2;
}

int main(){
    int x=1;
    foo(x);
    cout<<x;//outputs 1
}


例 2

using namespace std;

void foo(int & y){
    y=2;
}

int main(){
    int x=1;
    foo(x);
    cout<<x;//outputs 2
}
于 2010-04-02T04:00:31.927 に答える
1
// 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);

参照は変数の場所を渡すために使用されるため、スタック上で新しい関数にコピーする必要はありません。

于 2010-04-02T03:50:47.403 に答える
0

これが最も基本的なものかどうかはわかりませんが、ここに行きます...

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になり、要素が追加されますが、関数が戻ると破棄されます。elems

これは両方の方法で機能します。の場合、count渡されたカウントを変更したくないため、参照にはしません。代わりに、読み取った要素の数を返します。これにより、呼び出し元のコードは、実際に読み取られた要素の数と要求された数を比較できます。それらが一致しない場合は、CanReadElement()が返されたに違いなくfalse、すぐにさらに読み込もうとすると失敗する可能性があります。それらが一致する場合は、count使用可能な要素の数よりも少ない可能性があり、さらに読み取ることが適切です。最後に、内部でReadElementsIntoList()変更する必要がある場合countは、呼び出し元を混乱させることなく変更できます。

于 2010-04-02T03:52:38.680 に答える
0

例えるなら、あなたの関数が瓶の中の豆を数えるとしましょう。豆の瓶が必要であり、戻り値にならない結果を知る必要があります(理由はさまざまです)。jar と変数値を送信することもできますが、値が変更されるかどうか、または変更される内容はわかりません。代わりに、返信先のエンベロープを介してその変数を送信する必要があるため、値をその中に入れて、そのアドレスの値に結果が書き込まれたことを知ることができます。

于 2015-09-06T01:15:35.647 に答える
0

私が間違っている場合は修正してください。ただし、参照は参照解除されたポインターにすぎませんか?

ポインターとの違いは、NULL を簡単にコミットできないことです。

于 2017-10-05T20:02:53.827 に答える