参照はシンタックス シュガーであることはわかっているので、コードは読みやすく、書きやすくなっています。
しかし、ポインター変数と参照変数の違いは何でしょうか?
ポインターは再割り当てできます。
int x = 5;
int y = 6;
int *p;
p = &x;
p = &y;
*p = 10;
assert(x == 5);
assert(y == 10);
参照は再バインドできず、初期化時にバインドする必要があります。
int x = 5;
int y = 6;
int &q; // error
int &r = x;
ポインター変数には独自の ID があります。つまり、単項演算子で取得できる明確な可視メモリ アドレスと、演算&
子で測定できる一定量のスペースですsizeof
。これらの演算子を参照で使用すると、参照がバインドされているものに対応する値が返されます。参照自身のアドレスとサイズは見えません。このように参照は元の変数の同一性を前提としているため、参照を同じ変数の別の名前と考えると便利です。
int x = 0;
int &r = x;
int *p = &x;
int *p2 = &r;
assert(p == p2); // &x == &r
assert(&p != &p2);
追加レベルの間接化を提供するポインターへのポインターを任意にネストすることができます。参照は、1 レベルの間接化のみを提供します。
int x = 0;
int y = 0;
int *p = &x;
int *q = &y;
int **pp = &p;
**pp = 2;
pp = &q; // *pp is now q
**pp = 4;
assert(y == 4);
assert(x == 2);
ポインターは割り当てることができますがnullptr
、参照は既存のオブジェクトにバインドする必要があります。十分に努力すれば、 への参照をバインドできますがnullptr
、これは未定義であり、一貫した動作をしません。
/* the code below is undefined; your compiler may optimise it
* differently, emit warnings, or outright refuse to compile it */
int &r = *static_cast<int *>(nullptr);
// prints "null" under GCC 10
std::cout
<< (&r != nullptr
? "not null" : "null")
<< std::endl;
bool f(int &r) { return &r != nullptr; }
// prints "not null" under GCC 10
std::cout
<< (f(*static_cast<int *>(nullptr))
? "not null" : "null")
<< std::endl;
ただし、値が であるポインタへの参照を持つことはできますnullptr
。
ポインターは配列を反復処理できます。++
ポインターが指している次の項目+ 4
に移動したり、5 番目の要素に移動したりするために使用できます。これは、ポインターが指すオブジェクトのサイズに関係ありません。
ポインタが指すメモリ位置にアクセスするには、ポインタを逆参照する必要がありますが*
、参照は直接使用できます。クラス/構造体へのポインターは->
そのメンバーにアクセスするために使用しますが、参照は.
.
参照を配列に入れることはできませんが、ポインターは入れることができます (ユーザー @litb が言及)
const 参照は一時的にバインドできます。ポインターはできません (何らかの間接化がないわけではありません)。
const int &x = int(12); // legal C++
int *y = &int(12); // illegal to take the address of a temporary.
これによりconst &
、引数リストなどで使用するのがより便利になります。
参照は、自動間接参照を使用した定数ポインター(定数値へのポインターと混同しないでください!)と考えることができます。つまり、コンパイラーが*
演算子を適用します。
すべての参照はnull以外の値で初期化する必要があります。そうしないと、コンパイルは失敗します。参照のアドレスを取得することはできません(アドレス演算子は代わりに参照値のアドレスを返します)。また、参照に対して算術演算を行うこともできません。
Cプログラマーは、間接参照が発生した場合、または引数が関数の署名を見ずに値またはポインターによって渡された場合に明確でなくなるため、C++参照を嫌う可能性があります。
C ++プログラマーは、ポインターが安全でないと見なされるため、ポインターの使用を嫌う可能性があります。ただし、参照は、最も些細な場合を除いて、定数ポインターよりも実際には安全ではありませんが、自動間接参照の利便性に欠け、異なるセマンティックな意味を持ちます。
C++FAQからの次のステートメントを検討してください。
参照は、基になるアセンブリ言語のアドレスを使用して実装されることがよくありますが、参照をオブジェクトへの見栄えの悪いポインタとは考えないでください。参照はオブジェクトです。オブジェクトへのポインタでも、オブジェクトのコピーでもありません。オブジェクトです。
しかし、参照が実際にオブジェクトである場合、どのようにしてぶら下がっている参照が存在する可能性がありますか?管理されていない言語では、参照をポインタよりも「安全」にすることは不可能です。一般に、スコープの境界を越えて値を確実にエイリアスする方法はありません。
Cのバックグラウンドから来ると、C ++参照はややばかげた概念のように見えるかもしれませんが、可能な場合はポインターの代わりにそれらを使用する必要があります。自動間接参照は便利であり、参照はRAIIを処理するときに特に役立ちますが、安全性が認識されているためではありません。利点ですが、むしろ、慣用的なコードの記述が煩わしくないためです。
RAIIはC++の中心的な概念の1つですが、コピーのセマンティクスと自明ではない相互作用をします。参照によってオブジェクトを渡すと、コピーが含まれないため、これらの問題を回避できます。参照が言語に存在しない場合は、代わりにポインターを使用する必要があります。これは使用が面倒であるため、ベストプラクティスのソリューションは代替ソリューションよりも簡単であるという言語設計の原則に違反します。
あなたが本当にペダンティックになりたいのなら、ポインタではできないことを参照でできることが1つあります: 一時オブジェクトの寿命を延ばすことです。C++ では、const 参照を一時オブジェクトにバインドすると、そのオブジェクトの有効期間が参照の有効期間になります。
std::string s1 = "123";
std::string s2 = "456";
std::string s3_copy = s1 + s2;
const std::string& s3_reference = s1 + s2;
この例では、s3_copy は連結の結果である一時オブジェクトをコピーします。本質的に s3_reference は一時オブジェクトになります。これは実際には、参照と同じ有効期間を持つ一時オブジェクトへの参照です。
なしでこれを試すと、const
コンパイルに失敗するはずです。非 const 参照を一時オブジェクトにバインドすることも、そのアドレスを取得することもできません。
シンタックス シュガーとは別に、参照はconst
ポインターです ( aへのポインターではありませんconst
)。参照変数を宣言するときに参照するものを確立する必要があり、後で変更することはできません。
更新: もう少し考えてみると、重要な違いがあります。
const ポインターのターゲットは、そのアドレスを取得して const キャストを使用することで置き換えることができます。
参照のターゲットは、UB 以外で置き換えることはできません。
これにより、コンパイラは参照に対してより多くの最適化を行うことができます。
一般的な意見に反して、NULL の参照を持つことは可能です。
int * p = NULL;
int & r = *p;
r = 1; // crash! (if you're lucky)
確かに、参照を使用するのははるかに困難です。しかし、それを管理すると、それを見つけようとして髪をかきむしるでしょう。C++ では、参照は本質的に安全ではありません。
技術的には、これは無効な参照であり、null 参照ではありません。C++ は、他の言語で見られるように、null 参照を概念としてサポートしていません。他の種類の無効な参照もあります。無効な参照は、無効なポインターを使用した場合と同様に、未定義の動作の亡霊を引き起こします。
実際のエラーは、参照への割り当て前の NULL ポインターの逆参照にあります。しかし、その条件でエラーを生成するコンパイラについては知りません。エラーはコードのさらに先のポイントに伝播します。それが、この問題を非常に厄介なものにしている理由です。ほとんどの場合、NULL ポインターを逆参照すると、その場所ですぐにクラッシュし、それを理解するのに多くのデバッグは必要ありません。
上記の私の例は短くて不自然です。より現実的な例を次に示します。
class MyClass
{
...
virtual void DoSomething(int,int,int,int,int);
};
void Foo(const MyClass & bar)
{
...
bar.DoSomething(i1,i2,i3,i4,i5); // crash occurs here due to memory access violation - obvious why?
}
MyClass * GetInstance()
{
if (somecondition)
return NULL;
...
}
MyClass * p = GetInstance();
Foo(*p);
繰り返しますが、null 参照を取得する唯一の方法は不正なコードを使用することであり、取得すると未定義の動作が発生します。null 参照をチェックしても意味がありません。たとえば、試すことはできますif(&bar==NULL)...
が、コンパイラーはステートメントを最適化して存在しないようにする可能性があります! 有効な参照が NULL になることは決してないため、コンパイラの観点からは比較は常に false であり、if
句をデッド コードとして自由に削除できます。これが未定義の動作の本質です。
問題を回避する適切な方法は、参照を作成するために NULL ポインターを逆参照しないようにすることです。これを実現するための自動化された方法を次に示します。
template<typename T>
T& deref(T* p)
{
if (p == NULL)
throw std::invalid_argument(std::string("NULL reference"));
return *p;
}
MyClass * p = GetInstance();
Foo(deref(p));
優れたライティング スキルを持つ人物によるこの問題の古い見方については、Jim Hyslop と Herb Sutter によるNull Referencesを参照してください。
null ポインターを逆参照することの危険性の別の例については、Raymond Chen による別のプラットフォームにコードを移植しようとするときに未定義の動作を公開するを参照してください。
参照はポインターに非常に似ていますが、コンパイラーの最適化に役立つように特別に作成されています。
例として:
void maybeModify(int& x); // may modify x in some way
void hurtTheCompilersOptimizer(short size, int array[])
{
// This function is designed to do something particularly troublesome
// for optimizers. It will constantly call maybeModify on array[0] while
// adding array[1] to array[2]..array[size-1]. There's no real reason to
// do this, other than to demonstrate the power of references.
for (int i = 2; i < (int)size; i++) {
maybeModify(array[0]);
array[i] += array[1];
}
}
最適化コンパイラは、a[0] と a[1] に大量にアクセスしていることに気付く場合があります。アルゴリズムを次のように最適化したいと考えています。
void hurtTheCompilersOptimizer(short size, int array[])
{
// Do the same thing as above, but instead of accessing array[1]
// all the time, access it once and store the result in a register,
// which is much faster to do arithmetic with.
register int a0 = a[0];
register int a1 = a[1]; // access a[1] once
for (int i = 2; i < (int)size; i++) {
maybeModify(a0); // Give maybeModify a reference to a register
array[i] += a1; // Use the saved register value over and over
}
a[0] = a0; // Store the modified a[0] back into the array
}
このような最適化を行うには、呼び出し中に配列 [1] を変更できないことを証明する必要があります。これはかなり簡単です。i が 2 未満になることはないため、array[i] が array[1] を参照することはありません。MaybeModify() には参照として a0 が与えられます (別名 array[0])。「参照」演算がないため、コンパイラは、多分変更が x のアドレスを決して取得しないことを証明する必要があり、配列 [1] を変更するものがないことが証明されています。
また、a0 に一時的なレジスタ コピーがある間、将来の呼び出しで a[0] を読み書きする方法がないことを証明する必要があります。多くの場合、参照がクラス インスタンスのような永続的な構造に決して格納されないことは明らかであるため、これを証明するのは簡単なことです。
ポインターで同じことを行います
void maybeModify(int* x); // May modify x in some way
void hurtTheCompilersOptimizer(short size, int array[])
{
// Same operation, only now with pointers, making the
// optimization trickier.
for (int i = 2; i < (int)size; i++) {
maybeModify(&(array[0]));
array[i] += array[1];
}
}
動作は同じです。既にポインターを与えているため、maybeModify が array[1] を変更しないことを証明するのは、今のところはるかに困難です。猫は袋から出ています。ここで、はるかに難しい証明を行う必要があります。maybeModify を静的に分析して、&x + 1 に書き込みを行わないことを証明します。トリッキーです。
最新のコンパイラは、静的解析においてますます良くなっていますが、それらを支援し、参照を使用することは常に素晴らしいことです.
もちろん、そのような巧妙な最適化がなければ、コンパイラは必要に応じて参照をポインターに変換します。
編集:この回答を投稿してから5年後、参照が同じアドレス指定概念の別の見方とは異なるという実際の技術的な違いを見つけました。参照は、ポインタではできない方法で一時オブジェクトの寿命を変更できます。
F createF(int argument);
void extending()
{
const F& ref = createF(5);
std::cout << ref.getArgument() << std::endl;
};
通常、 への呼び出しによって作成されたような一時オブジェクトcreateF(5)
は、式の最後で破棄されます。ただし、そのオブジェクトを参照にバインドすることによりref
、C++ はその一時オブジェクトの有効期間ref
を範囲外になるまで延長します。
実際、参照は実際にはポインタのようなものではありません。
コンパイラは変数への「参照」を保持し、名前をメモリアドレスに関連付けます。コンパイル時に変数名をメモリアドレスに変換するのがその仕事です。
参照を作成するときは、ポインター変数に別の名前を割り当てることだけをコンパイラーに伝えます。そのため、参照は「nullを指す」ことができません。これは、変数が存在できず、存在できないためです。
ポインタは変数です。他の変数のアドレスが含まれているか、nullになる可能性があります。重要なことは、ポインターには値があり、参照には参照している変数しかないということです。
次に、実際のコードについて説明します。
int a = 0;
int& b = a;
a
ここでは、 ;を指す別の変数を作成していません。の値を保持しているメモリコンテンツに別の名前を追加しているだけですa
。このメモリには2つの名前とがa
ありb
、どちらの名前でもアドレス指定できます。
void increment(int& n)
{
n = n + 1;
}
int a;
increment(a);
関数を呼び出すとき、コンパイラは通常、コピー先の引数用のメモリスペースを生成します。関数シグニチャは、作成する必要のあるスペースを定義し、これらのスペースに使用する必要のある名前を指定します。パラメータを参照として宣言することは、メソッド呼び出し中に新しいメモリ空間を割り当てる代わりに、入力変数のメモリ空間を使用するようにコンパイラに指示するだけです。関数が呼び出し元のスコープで宣言された変数を直接操作すると言うのは奇妙に思えるかもしれませんが、コンパイルされたコードを実行するときは、スコープがなくなることを忘れないでください。単なるフラットメモリがあり、関数コードは任意の変数を操作できます。
これで、extern変数を使用する場合など、コンパイル時にコンパイラが参照を認識できない場合があります。したがって、参照は、基になるコードのポインターとして実装される場合とされない場合があります。しかし、私があなたに与えた例では、それはおそらくポインターで実装されないでしょう。
参照をすることはできませんNULL
。
参照とポインターの両方が別の値に間接的にアクセスするために使用されますが、参照とポインターの間には2つの重要な違いがあります。1つ目は、参照が常にオブジェクトを参照することです。初期化せずに参照を定義するのはエラーです。割り当ての動作は、2番目の重要な違いです。参照に割り当てると、参照がバインドされているオブジェクトが変更されます。別のオブジェクトへの参照を再バインドしません。初期化されると、参照は常に同じ基になるオブジェクトを参照します。
これらの2つのプログラムフラグメントについて考えてみます。最初に、あるポインタを別のポインタに割り当てます。
int ival = 1024, ival2 = 2048;
int *pi = &ival, *pi2 = &ival2;
pi = pi2; // pi now points to ival2
割り当て後、ival、piによってアドレス指定されたオブジェクトは変更されません。割り当てにより、piの値が変更され、別のオブジェクトを指すようになります。次に、2つの参照を割り当てる同様のプログラムについて考えてみます。
int &ri = ival, &ri2 = ival2;
ri = ri2; // assigns ival2 to ival
この割り当ては、参照自体ではなく、riによって参照される値であるivalを変更します。割り当て後も、2つの参照は元のオブジェクトを参照し、それらのオブジェクトの値も同じになります。
コンピュータ言語を抽象的またはアカデミックな方法で研究することに慣れていない場合、難解に見えるかもしれない意味上の違いがあります。
最高レベルでは、参照の考え方は、それらが透過的な「エイリアス」であるということです。あなたのコンピュータはそれらを機能させるためにアドレスを使用するかもしれませんが、あなたはそれについて心配する必要はありません: それらを既存のオブジェクトの「単なる別の名前」と考えるべきであり、構文はそれを反映しています。これらはポインタよりも厳密であるため、ダングリング ポインタを作成しようとしているときよりも、ダングリング参照を作成しようとしているときにコンパイラがより確実に警告することができます。
それを超えて、もちろん、ポインターと参照の間にはいくつかの実際的な違いがあります。それらを使用する構文は明らかに異なり、参照を「再配置」したり、無への参照を持ったり、参照へのポインターを持ったりすることはできません。
参照は別の変数のエイリアスですが、ポインターは変数のメモリアドレスを保持します。参照は通常、関数パラメーターとして使用されるため、渡されるオブジェクトはコピーではなくオブジェクト自体になります。
void fun(int &a, int &b); // A common usage of references.
int a = 0;
int &b = a; // b is an alias for a. Not so common to use.
実際には (コードを実行しないと) スペースが占有されることによる副作用は見られないため、スペースがどれだけ占有されるかは問題ではありません。
一方、参照とポインターの主な違いの 1 つは、const 参照に割り当てられた一時変数は、const 参照がスコープ外になるまで存続することです。
例えば:
class scope_test
{
public:
~scope_test() { printf("scope_test done!\n"); }
};
...
{
const scope_test &test= scope_test();
printf("in scope\n");
}
印刷されます:
in scope
scope_test done!
これは、ScopeGuard が機能することを可能にする言語メカニズムです。
これはチュートリアルに基づいています。書かれていることはそれをより明確にします:
>>> The address that locates a variable within memory is
what we call a reference to that variable. (5th paragraph at page 63)
>>> The variable that stores the reference to another
variable is what we call a pointer. (3rd paragraph at page 64)
それを思い出すだけで、
>>> reference stands for memory location
>>> pointer is a reference container (Maybe because we will use it for
several times, it is better to remember that reference.)
さらに、ほぼすべてのポインター チュートリアルを参照できるように、ポインターはポインター演算によってサポートされるオブジェクトであり、ポインターを配列に似たものにします。
次のステートメントを見てください。
int Tom(0);
int & alias_Tom = Tom;
alias_Tom
として理解できます(である とはalias of a variable
異なります) 。の参照を作成するなどのステートメントの用語を忘れてもかまいません。typedef
alias of a type
Tom
Tom
参照は、一部のメモリに付けられた別の名前ではありません。これは、使用時に自動的に逆参照される不変のポインターです。基本的には、次のように要約されます。
int& j = i;
内部的には
int* const j = &i;
C++ ではポインターへの参照は可能ですが、その逆は、参照へのポインターが不可能であることを意味します。ポインターへの参照は、ポインターを変更するためのより明確な構文を提供します。この例を見てください:
#include<iostream>
using namespace std;
void swap(char * &str1, char * &str2)
{
char *temp = str1;
str1 = str2;
str2 = temp;
}
int main()
{
char *str1 = "Hi";
char *str2 = "Hello";
swap(str1, str2);
cout<<"str1 is "<<str1<<endl;
cout<<"str2 is "<<str2<<endl;
return 0;
}
そして、上記のプログラムの C バージョンを考えてみましょう。C では、ポインタ ツー ポインタ (複数の間接参照) を使用する必要があり、混乱を招き、プログラムが複雑に見える場合があります。
#include<stdio.h>
/* Swaps strings by swapping pointers */
void swap1(char **str1_ptr, char **str2_ptr)
{
char *temp = *str1_ptr;
*str1_ptr = *str2_ptr;
*str2_ptr = temp;
}
int main()
{
char *str1 = "Hi";
char *str2 = "Hello";
swap1(&str1, &str2);
printf("str1 is %s, str2 is %s", str1, str2);
return 0;
}
ポインターへの参照の詳細については、次を参照してください。
私が言ったように、参照へのポインターは不可能です。次のプログラムを試してください。
#include <iostream>
using namespace std;
int main()
{
int x = 10;
int *ptr = &x;
int &*ptr1 = ptr;
}
これらのいずれかが必要でない限り、参照を使用します。
Null ポインターは番兵値として使用でき、多くの場合、関数のオーバーロードや bool の使用を回避する安価な方法です。
ポインタに対して算術演算を実行できます。例えば、p += offset;
ポインターと参照の間には、誰も言及していない基本的な違いが 1 つあります。参照によって、関数の引数で参照渡しのセマンティクスが可能になります。ポインターは、最初は見えませんが、そうではありません。値渡しのセマンティクスを提供するだけです。これは、この記事で非常にうまく説明されています。
よろしく、&rzej
もう 1 つの違いは、void 型へのポインターを使用できる (つまり、何かへのポインターを意味する) ことですが、void への参照は禁止されています。
int a;
void * p = &a; // ok
void & p = a; // forbidden
私は、この特定の違いに本当に満足しているとは言えません。私は、アドレスを持つものへの意味のある参照で許可され、それ以外の場合は参照と同じ動作が許可されることを望んでいます。これにより、参照を使用して memcpy などの C ライブラリ関数に相当するものを定義できます。
また、インライン化された関数へのパラメーターである参照は、ポインターとは異なる方法で処理される場合があります。
void increment(int *ptrint) { (*ptrint)++; }
void increment(int &refint) { refint++; }
void incptrtest()
{
int testptr=0;
increment(&testptr);
}
void increftest()
{
int testref=0;
increment(testref);
}
ポインター バージョン 1 をインライン化するとき、多くのコンパイラーは実際にメモリへの書き込みを強制します (明示的にアドレスを取得しています)。ただし、より最適なレジスタに参照を残します。
もちろん、インライン化されていない関数の場合、ポインターと参照は同じコードを生成します。組み込み関数が変更されず、関数によって返されない場合は、参照よりも値で渡す方が常に優れています。
参照のもう1つの興味深い使用法は、ユーザー定義型のデフォルト引数を提供することです。
class UDT
{
public:
UDT() : val_d(33) {};
UDT(int val) : val_d(val) {};
virtual ~UDT() {};
private:
int val_d;
};
class UDT_Derived : public UDT
{
public:
UDT_Derived() : UDT() {};
virtual ~UDT_Derived() {};
};
class Behavior
{
public:
Behavior(
const UDT &udt = UDT()
) {};
};
int main()
{
Behavior b; // take default
UDT u(88);
Behavior c(u);
UDT_Derived ud;
Behavior d(ud);
return 1;
}
デフォルトのフレーバーは、参照の「一時的な」アスペクトへのバインド定数参照を使用します。
このプログラムは、質問の答えを理解するのに役立つかもしれません。これは、参照 "j" と変数 "x" を指すポインター "ptr" の単純なプログラムです。
#include<iostream>
using namespace std;
int main()
{
int *ptr=0, x=9; // pointer and variable declaration
ptr=&x; // pointer to variable "x"
int & j=x; // reference declaration; reference to variable "x"
cout << "x=" << x << endl;
cout << "&x=" << &x << endl;
cout << "j=" << j << endl;
cout << "&j=" << &j << endl;
cout << "*ptr=" << *ptr << endl;
cout << "ptr=" << ptr << endl;
cout << "&ptr=" << &ptr << endl;
getch();
}
プログラムを実行して、出力を見れば理解できます。
また、10 分の時間を割いて、次のビデオをご覧ください: https://www.youtube.com/watch?v=rlJrrGV0iOg
いくつかの比喩が役立つかもしれません。デスクトップ画面スペースのコンテキストで -