ポインター演算に関する優れた記事や説明 (ブログ、例) はありますか? 聴衆は、C および C++ を学習している Java プログラマーの集まりであると考えてください。
7 に答える
ここでポインターを学びました: http://www.cplusplus.com/doc/tutorial/pointers.html
ポインターを理解すれば、ポインター演算は簡単です。それと通常の算術演算の唯一の違いは、ポインターに追加する数値が、ポインターが指している型のサイズで乗算されることです。たとえば、 an へのポインタがありint
、 anint
のサイズが 4 バイトの場合、(pointer_to_int + 4)
は 16 バイト (4 int) 先のメモリ アドレスに評価されます。
だからあなたが書くとき
(a_pointer + a_number)
ポインター演算では、実際に起こっていることは
(a_pointer + (a_number * sizeof(*a_pointer)))
通常の算数で。
まず、ビンキービデオが役立つかもしれません。ポインタについての素晴らしいビデオです。算術の例を次に示します。
int * pa = NULL;
int * pb = NULL;
pa += 1; // pa++. behind the scenes, add sizeof(int) bytes
assert((pa - pb) == 1);
print_out(pa); // possibly outputs 0x4
print_out(pb); // possibly outputs 0x0 (if NULL is actually bit-wise 0x0)
(null ポインター値を含むポインターのインクリメントは、厳密には未定義の動作であることに注意してください。ポインターの値にのみ関心があるため、NULL を使用しました。通常、配列の要素を指す場合は、インクリメント/デクリメントのみを使用します)。
以下に、2 つの重要な概念を示します。
- ポインターへの整数の加算/減算は、ポインターを N 要素だけ前後に移動することを意味します。したがって、int が 4 バイトの大きさの場合、1 ずつインクリメントした後、pa はプラットフォーム上で 0x4 を含む可能性があります。
- 別のポインターによるポインターの減算は、要素によって測定された距離を取得することを意味します。したがって、pa から pb を引くと 1 になります。これは、要素距離が 1 つであるためです。
実際の例について。あなたが関数を書き、人々が開始ポインタと終了ポインタを提供するとします (C++ では非常に一般的なことです):
void mutate_them(int *begin, int *end) {
// get the amount of elements
ptrdiff_t n = end - begin;
// allocate space for n elements to do something...
// then iterate. increment begin until it hits end
while(begin != end) {
// do something
begin++;
}
}
ptrdiff_t
(end - begin) の型です。一部のコンパイラでは「int」の同義語である可能性がありますが、別のコンパイラでは別の型である可能性があります。知ることはできないので、一般的な typedef を選択しますptrdiff_t
。
NLP を適用すると、アドレス演算と呼ばれます。「指針」が恐れられ、誤解される主な理由は、間違った人によって教えられたり、間違った段階で間違った例を間違った方法で教えられたりするためです。誰もそれを「理解」していないのも不思議ではありません。
ポインターを教えるとき、教員は「p は a へのポインターであり、p の値は a のアドレスです」などについて説明します。それはうまくいきません。これがあなたが構築するための原材料です。それを使って練習すれば、生徒はそれを習得します。
'int a'、a は整数で、整数型の値を格納します。'int* p'、p は 'int star' で、'int star' タイプの値を格納します。
'a' は、a に格納されている 'what' 整数を取得する方法です ('a の値' を使用しないようにしてください) '&a' は、a 自体が格納されている 'where' を取得する方法です ('address' と言ってみてください)。
'b = a' が機能するには、両側が同じ型でなければなりません。a が int の場合、b は int を格納できる必要があります。(だから______ b、空白は「int」で埋められます)
'p = &a' これが機能するには、両側が同じタイプでなければなりません。a が整数で &a がアドレスの場合、p は整数のアドレスを格納できなければなりません。(だから______ p、空白は「int *」で埋められます)
int *p を別の方法で記述して、型情報を引き出します。
int* | p
「ぴ」とは?答え: 'int *' です。したがって、「p」は整数のアドレスです。
整数 | *p
'*p' とは何ですか? ans: それは「int」です。したがって、「*p」は整数です。
次に、アドレス演算に進みます。
int a; a=1; a=a+1;
「a=a+1」で何をしているのか? 「次」と考えてください。a は数字なので、これは「次の数字」と言っているようなものです。a は 1 なので、「次へ」と言うと 2 になります。
// 誤った例。あなたは警告されました!!! int *p int a; p = &a; p=p+1;
「p=p+1」で何をしているのか? それはまだ「次」と言っています。今度は p は数字ではなくアドレスです。つまり、私たちが言っているのは「次のアドレス」です。次のアドレスは、データ型、より具体的にはデータ型のサイズに依存します。
printf("%d %d %d", sizeof(char), sizeof(int), sizeof(float));
そのため、アドレスの「next」は sizeof(data type) の前に移動します。
これは私と私が教えていたすべての人々にとってうまくいきました。
ポインター演算の良い例として、次の文字列長関数を考えます。
int length(char *s)
{
char *str = s;
while(*str++);
return str - s;
}
それに取り組むにはいくつかの方法があります。
ほとんどの C/C++ プログラマーが考えている直感的なアプローチは、ポインターがメモリ アドレスであるということです。litb の例では、このアプローチを採用しています。ヌル ポインター (ほとんどのマシンではアドレス 0 に対応) があり、int のサイズを追加すると、アドレス 4 が得られます。これは、ポインターが基本的にファンシーな整数であることを意味します。
残念ながら、これにはいくつかの問題があります。そもそも、うまくいかないかもしれません。ヌル ポインターが実際にアドレス 0 を使用することは保証されていません (ただし、定数 0 をポインターに割り当てるとヌル ポインターが生成されます)。
さらに、null ポインターをインクリメントすることは許可されていません。より一般的には、ポインターは常に割り当てられたメモリ (または 1 つ前の要素)、または特別な null ポインター定数 0 を指す必要があります。
したがって、より正しい考え方は、ポインタは単なるイテレータであり、割り当てられたメモリを反復処理できるということです。これは、STL イテレータの背後にある重要なアイデアの 1 つです。それらはポインタとして非常によく振る舞うようにモデル化されており、適切な反復子として機能するように生のポインタを修正する特殊化を提供します。
これについてのより詳細な説明は、たとえばここにあります。
しかし、この後者の見方は、STL イテレーターについて実際に説明し、ポインターはこれらの特殊なケースであると簡単に説明する必要があることを意味します。のように、ポインターをインクリメントしてバッファー内の次の要素を指すことができますstd::vector<int>::iterator
。他のコンテナの終了イテレータと同様に、配列の末尾を超えて 1 つの要素を指すことができます。同じバッファーを指している2 つのポインターを減算して、それらの間の要素の数を取得することができます。これは、イテレーターの場合と同様です。ポインターが別々のバッファーを指している場合、イテレーターの場合と同様に、意味のある比較はできません。(そうしない理由の実際的な例として、セグメント化されたメモリ空間で何が起こるかを考えてみましょう。別々のセグメントを指している 2 つのポインタ間の距離はどれくらいですか?)
もちろん、実際には、CPU アドレスと C/C++ ポインターの間には非常に密接な相関関係があります。しかし、それらはまったく同じものではありません。ポインターには、CPU で厳密に必要ではないかもしれないいくつかの制限があります。
もちろん、ほとんどの C++ プログラマーは、技術的には正しくないとはいえ、最初の理解で混乱します。通常、コードが最終的にどのように動作するかは、人々がそれを理解して先に進むと考えるのに十分近いものです。
しかし、Java から来て、ポインターについてゼロから学んでいる人にとっては、後者の説明は簡単に理解できるかもしれません。
したがって、覚えておくべき重要なことは、ポインターは逆参照用に型指定された単なるワード サイズの変数であるということです。つまり、それが void *、int *、long long ** のいずれであっても、それはワード サイズの変数にすぎません。これらの型の違いは、コンパイラが逆参照された型と見なすものです。明確にするために、ワードサイズとは仮想アドレスの幅を意味します。これが何を意味するのかわからない場合は、64 ビット マシンではポインターが 8 バイトであり、32 ビット マシンではポインターが 4 バイトであることを思い出してください。アドレスの概念は、ポインターを理解する上で非常に重要です。アドレスは、メモリ内の特定の場所を一意に識別することができる番号です。メモリ内のすべてにアドレスがあります。ここでは、すべての変数にアドレスがあると言えます。これは必ずしも真であるとは限りませんが、コンパイラはこれを想定しています。アドレス自体はバイト粒度です。つまり、0x0000000 はメモリの先頭を指定し、0x00000001 はメモリへの 1 バイトです。これは、ポインタに 1 を追加することで、1 バイト前方にメモリに移動していることを意味します。では、配列を取りましょう。32 要素の大きさの quux 型の配列を作成すると、配列の各セルが sizeof(quux) 大きいため、割り当ての先頭から、割り当ての先頭に 32*sizeof(quux) を加えたものまでの範囲になります。したがって、配列の要素を array[n] で指定する場合、それは *(array+sizeof(quux)*n) の単なる構文糖衣 (省略形) です。ポインター演算は、実際には参照しているアドレスを変更するだけです。これが、 strlen を次のように実装できる理由です。0x00000001 はメモリへの 1 バイトです。これは、ポインタに 1 を追加することで、1 バイト前方にメモリに移動していることを意味します。では、配列を取りましょう。32 要素の大きさの quux 型の配列を作成すると、配列の各セルが sizeof(quux) 大きいため、割り当ての先頭から、割り当ての先頭に 32*sizeof(quux) を加えたものまでの範囲になります。したがって、配列の要素を array[n] で指定するとき、それは *(array+sizeof(quux)*n) の単なる構文糖衣 (省略形) です。ポインター演算は、実際には参照しているアドレスを変更するだけです。これが、 strlen を次のように実装できる理由です。0x00000001 はメモリへの 1 バイトです。これは、ポインタに 1 を追加することで、1 バイト前方にメモリに移動していることを意味します。では、配列を取りましょう。32 要素の大きさの quux 型の配列を作成すると、配列の各セルが sizeof(quux) 大きいため、割り当ての先頭から、割り当ての先頭に 32*sizeof(quux) を加えたものまでの範囲になります。したがって、配列の要素を array[n] で指定する場合、それは *(array+sizeof(quux)*n) の単なる構文糖衣 (省略形) です。ポインター演算は、実際には参照しているアドレスを変更するだけです。これが、 strlen を次のように実装できる理由です。配列の各セルは sizeof(quux) 大きいため、割り当ての先頭から、割り当ての先頭に 32*sizeof(quux) を加えた範囲になります。したがって、配列の要素を array[n] で指定する場合、それは *(array+sizeof(quux)*n) の単なる構文糖衣 (省略形) です。ポインター演算は、実際には参照しているアドレスを変更するだけです。これが、 strlen を次のように実装できる理由です。配列の各セルは sizeof(quux) 大きいため、割り当ての先頭から、割り当ての先頭に 32*sizeof(quux) を加えた範囲になります。したがって、配列の要素を array[n] で指定する場合、それは *(array+sizeof(quux)*n) の単なる構文糖衣 (省略形) です。ポインター演算は、実際には参照しているアドレスを変更するだけです。これが、 strlen を次のように実装できる理由です。
while(*n++ != '\0'){
len++;
}
ゼロに到達するまでバイトごとにスキャンしているだけなので。それが役立つことを願っています!