私は C クラスの入門用にいくつかのスライドを準備しており、配列添字よりもポインター演算を使用するための良い例 (および動機) を提示しようとしています。
私が本で目にする多くの例は、かなり同等です。たとえば、多くの書籍では、文字列内のすべての値の大文字と小文字を逆にする方法が示されていますが、a[i] を *p に置き換えることを除いて、コードは同じです。
ポインター演算がはるかに洗練されたコードを生成できる、1 次元配列の良い (そして短い) 例を探しています。何か案は?
私は C クラスの入門用にいくつかのスライドを準備しており、配列添字よりもポインター演算を使用するための良い例 (および動機) を提示しようとしています。
私が本で目にする多くの例は、かなり同等です。たとえば、多くの書籍では、文字列内のすべての値の大文字と小文字を逆にする方法が示されていますが、a[i] を *p に置き換えることを除いて、コードは同じです。
ポインター演算がはるかに洗練されたコードを生成できる、1 次元配列の良い (そして短い) 例を探しています。何か案は?
値の代わりにポインターを再度取得します。
通常、ポインターを再度取得したい場合は、ポインター演算を使用します。配列インデックスを使用しているときにポインターを取得するには、1) ポインターのオフセットを計算し、次に 2) そのメモリ位置で値を取得し、次に 3) & を使用してアドレスを再度取得する必要があります。これは、タイピングが多くなり、構文がクリーンではなくなります。
例 1:バッファーの 512 番目のバイトへのポインターが必要だとします。
char buffer[1024]
char *p = buffer + 512;
以下よりもきれいです:
char buffer[1024];
char *p = &buffer[512];
例 2:より効率的な strcat
char buffer[1024];
strcpy(buffer, "hello ");
strcpy(buffer + 6, "world!");
これは以下よりもきれいです:
char buffer[1024];
strcpy(buffer, "hello ");
strcpy(&buffer[6], "world!");
ポインター演算 ++ を反復子として使用する:
++ でポインターをインクリメントし、- でデクリメントすると、要素の配列内の各要素を反復処理するときに便利です。オフセットを追跡するために使用される別の変数を使用するよりもクリーンです。
ポインタ減算:
ポインター演算でポインター減算を使用できます。これは、指している要素の前に要素を取得する場合に役立ちます。配列の添え字でも実行できますが、非常に見栄えが悪く、混乱を招きます。特に、リストの最後から何かをインデックス化するために負の添え字が与えられる python プログラマーに。
char *my_strcpy(const char *s, char *t) {
char *u = t;
while (*t++ = *s++);
return u;
}
そんな美しさをインデックスで台無しにしたいのはなぜですか?(K&R と、それらがこのスタイルにどのように構築されているかを参照してください。)上記の署名をそのまま使用したのには理由があります。最初に説明を求めずに編集をやめてください。知っていると思う人は、現在の署名を調べてください。いくつかのrestrict
資格を逃しています。
構造のアラインメント テストとoffsetof
マクロの実装。
ポインター演算は派手で「ハッカー的」に見えるかもしれませんが、標準のインデックス作成よりも高速なケースに遭遇したことはありません。正反対に、コードの速度が大幅に低下するケースによく遭遇しました。
たとえば、ポインターを使用した配列の典型的なシーケンシャル ループは、SSE 拡張機能をサポートする最新のプロセッサで従来のインデックスを使用したループよりも効率が悪い場合があります。ループ内のポインター演算は、コンパイラーがループのベクトル化を実行するのを十分にブロックし、通常の 2 倍から 4 倍のパフォーマンス向上を実現できます。さらに、単純な整数変数の代わりにポインターを使用すると、ポインターのエイリアシングが原因で不要なメモリ ストア操作が発生する可能性があります。
したがって、一般に、標準のインデックス アクセスの代わりにポインター演算を使用することは決して推奨されません。
ポインターを使用しない場合、データムの位置が実際には問題にならない 2 次元配列を反復処理する
場合、ポインターを使用して 2 つの添字を追跡する必要
があり、配列の先頭を指すことができ、単一のループを使用できます。 、すべてを圧縮します
古いコンパイラ、またはある種の特殊な組み込みシステム コンパイラを使用している場合、パフォーマンスにわずかな違いがある可能性がありますが、最新のコンパイラのほとんどは、おそらくこれらの (小さな) 違いを最適化します。
次の記事は参考になるかもしれません - 生徒のレベルによって異なります:
具体的にはCについて質問していますが、C++もこれに基づいて構築されています。
ほとんどのポインター演算は、自然に Forward Iterator の概念に一般化されます。メモリのウォークスルー*p++
は、演算子のオーバーロードのおかげで、任意のシーケンス コンテナー (リンク リスト、スキップ リスト、ベクター、バイナリ ツリー、B ツリーなど) に使用できます。
対処する必要がないことを願っています。ポインターはエイリアスを作成できますが、配列はエイリアスを作成できません。エイリアシングは、あらゆる種類の非理想的なコード生成を引き起こす可能性があります。最も一般的なのは、ポインターを別の関数への出力パラメーターとして使用することです。基本的に、コンパイラは、関数によって使用されるポインターがそのスタック フレーム内でそれ自体または他の何かにエイリアスされていないと想定できないため、使用されるたびにポインターから値をリロードする必要があります。というか、安全のためにそうします。
#include ctype.h
void skip_spaces( const char **ppsz )
{
const char *psz = *ppsz;
while( isspace(*psz) )
psz++;
*ppsz = psz;
}
void fn(void)
{
char a[]=" Hello World!";
const char *psz = a;
skip_spaces( &psz );
printf("\n%s", psz);
}
多くの場合、選択はスタイルの 1 つにすぎません。特定のケースでは、一方が他方よりも自然に見えたり感じたりします。
インデックスを使用すると、コンパイラがループ内でオフセットを繰り返し再計算する必要があるという議論もあります-最適化されていないビルドを除いて、これがどのくらいの頻度で発生するかはわかりませんが、発生すると思いますが、おそらくめったに問題にはなりません。
長い目で見れば重要だと私が考える分野の 1 つは (入門的な C クラスには当てはまらないかもしれませんが、早い段階で習得してください)、ポインター演算の使用は C++ STL で使用されるイディオムに適用されるということです。ポインター演算を理解して使用できるようになれば、STL に移行したときに、イテレーターを適切に使用する方法について理解を深めることができます。