ポインターのインデックス付け


11

現在、「Numerical Recipes in C」というタイトルの本を読んでいます。この本では、著者は、1で始まるインデックスがある場合に特定のアルゴリズムが本質的にどのように機能するかを詳しく説明しています(私は彼の議論に完全には従わず、この投稿のポイントではありません)が、Cは常に0で始まる配列にインデックスを付けますこれを回避するために、彼は単に割り当て後にポインタをデクリメントすることを提案します、例えば:

float *a = malloc(size);
a--;

これは、1で始まるインデックスを持つポインターを効果的に提供し、その後で解放されると彼は言います:

free(a + 1);

ただし、私が知る限り、これはC標準による未定義の動作です。これは明らかにHPCコミュニティ内で非常に評判の良い本なので、彼の言っていることを単に無視したくはありませんが、割り当てられた範囲外のポインターを単にデクリメントすることは私には非常に大雑把です。これはCの「許可された」動作ですか?私はgccとiccの両方を使用してテストしましたが、これらの結果はどちらも私が何も心配していないことを示しているように見えますが、絶対にポジティブになりたいです。


3
どのC標準を参照していますか?私の回想、「Cにおける数値のレシピ」あたりのK&Rと、おそらくANSI Cの古代に、1990年代に公開されているので、私は尋ねる
ブヨ


3
「私はgccとiccの両方を使用してテストしましたが、これらの結果は両方とも、何も心配していないことを示しているように見えますが、絶対にポジティブになりたいです。」コンパイラが許可しているため、C言語が許可していると思い込まないでください。もちろん、将来コードが壊れても大丈夫でない限り。
ドーバル

5
陰気になりたくはないが、「数値レシピ」は一般に、ソフトウェア開発や数値解析のパラダイムではなく、便利で迅速かつ汚い本と考えられている。批判の概要については、「数値レシピ」に関するウィキペディアの記事をご覧ください。
チャールズE.グラント14

1
:余談として、ここではなぜ我々にインデックスはゼロからだcs.utexas.edu/~EWD/ewd08xx/EWD831.PDF
ラッセルBorogove

回答:


16

そのようなコードは正しい

float a = malloc(size);
a--;

ANSI C規格のセクション3.3.6に従って、未定義の動作が生じます。

ポインターオペランドと結果の両方が同じ配列オブジェクトのメンバー、または配列オブジェクトの最後のメンバーの1つを指している場合を除き、動作は未定義です

このようなコードの場合、本のCコードの品質(1990年代後半に使用したとき)はそれほど高いとは見なされませんでした。

未定義の動作に関する問題は、コンパイラがどのような結果を生成するかに関係なく、その結果は定義によって正しいことです(たとえそれが非常に破壊的で予測不能であっても)。
幸いなことに、このような場合に予期しない動作を実際に引き起こす努力をするコンパイラは非常に少なくmalloc、HPCに使用されるマシンの一般的な実装には、アドレスが返される直前に簿記データがあります。そこに書くのは得策ではありませんが、それらのシステムではポインタを作成するだけで無害です。

ランタイム環境が変更されるか、コードが別の環境に移植されると、コード破損する可能性があることに注意してください。


4
正確には、マルチバンクアーキテクチャでは、mallocがバンクの0番目のアドレスを提供し、それをデクリメントすると、アンダーフローのあるCPUトラップが発生する可能性があります。
バリティ14

1
私はそれが「不幸」だとは思わない。未定義の動作を呼び出すたびにコンパイラがすぐにクラッシュするコードをコンパイラが出力した方がはるかに良いと思います。
デビッドコンラッド14

4
@DavidConrad:それでは、Cはあなたの言語ではありません。Cで定義されていない動作の多くは、簡単に検出できないか、重大なパフォーマンスヒットのみがあります。
バートヴァンインゲンシェナウ14

「コンパイラスイッチ付き」を追加することを考えていました。明らかに、最適化されたコードにはそれは望ましくありません。しかし、あなたは正しいです、そしてそれは私が10年前にCを書くのをあきらめた理由です。
デビッドコンラッド14

@BartvanIngenSchenauは、「深刻なパフォーマンスヒット」の意味に応じて、Cのシンボリック実行(clang + kleeなど)と、デバッグに非常に役立つ傾向があるサナタイザー(asan、tsan、ubsan、valgrindなど)があります。
マチェイピエチョトカ14

10

公式には、それは、(エンド過去1を除く)アレイの外側にポインタポイントを持っている未定義の動作です、それが間接参照決していたとしても

実際には、場合お使いのプロセッサは、(のような奇妙なものとは対照的に、フラットなメモリモデルを持っているx86-16)、および場合は無効なポインタを作成する場合、コンパイラはあなたにランタイムエラーや不正な最適化を与えるものではありません、コードは動作します結構です


1
それは理にかなっている。残念ながら、それは私の好みのために2つ多すぎます。
wolfPack88 14

3
最後のポイントは私見で最も問題が多いです。これらの時間のコンパイラは、UBの場合にプラットフォームが「自然に」行うことを何も起こさないので、オプティマイザーは積極的にそれを悪用しているので、私はそれをそれほど軽く遊んではいません。
マッテオイタリア14

3

まず、未定義の動作です。最近、一部の最適化コンパイラは、未定義の動作について非常に積極的になります。たとえば、a--この場合は未定義の動作であるため、コンパイラはaをデクリメントせずに、命令とプロセッササイクルを保存することを決定できます。これは公式に正しい法律です。

それを無視して、1、2、または1980を引きます。たとえば、1980年から2013年までの財務データがある場合、1980を引きます。確かに存在するいくつかの Kヌルポインタである-ように大きな定数kは。その場合、何かがうまくいかないことを本当に期待しています。

さて、メガバイトのサイズの大きな構造体を取ります。2つの構造体を指すポインターpを割り当てます。p-1はNULLポインターの場合があります。p-1はラップアラウンドする場合があります(構造体がメガバイトで、mallocブロックがアドレス空間の先頭から900 KBの場合)。したがって、p-1> pというコンパイラーの悪意はありません。面白くなるかもしれません。


1

...割り当てられた範囲外のポインタを単にデクリメントすることは、私にとって非常に大ざっぱなようです。これはCの「許可された」動作ですか?

許可された?はい。良いアイデア?普通ではありません。

Cはアセンブリ言語の省略形であり、アセンブリ言語にはポインタはなく、メモリアドレスのみがあります。Cのポインターは、算術を受けたときにポイントするサイズによって増加または減少するという副次的な動作を持つメモリアドレスです。これにより、構文の観点から次のことがうまくなります。

double *p = (double *)0xdeadbeef;
--p;  // p == 0xdeadbee7, assuming sizeof(double) == 8.
double d = p[0];

配列は、実際にはCのものではありません。それらは、配列のように動作する連続したメモリ範囲への単なるポインタです。[]オペレータはそう、ポインタ演算を行うと、逆参照の省略形ですa[x]実際の手段*(a + x)

上記を実行する正当な理由があります。たとえば、いくつかのI / Oデバイスがとにdoubleマップされます。それを行う必要のあるプログラムはほとんどありません。0xdeadbee70xdeadbeef

&演算子の使用やの呼び出しなどによって何かのアドレスを作成する場合malloc()、元のポインターをそのままにしておき、そのポインターが実際に有効なものであることがわかるようにします。ポインターのデクリメントは、誤ったコードの一部がそれを逆参照しようとし、誤った結果を得たり、何かを破壊したり、環境によってはセグメンテーション違反を犯したりすることを意味します。これは、で特に当てはまりmalloc()ます。なぜならfree()、元の値を渡すことを覚えている人に負担をかけているのであり、変更されたバージョンではなく、すべての問題が解消されるからです。

Cで1ベースの配列が必要な場合は、使用されない追加要素を1つ割り当てることを犠牲にして安全に行うことができます。

double *array_create(size_t size) {
    // Wasting one element, so don't allow it to be full-sized
    assert(size < SIZE_MAX);
    return malloc((size+1) * sizeof(double));
}

inline double array_index(double *array, size_t index) {
    assert(array != NULL);
    assert(index >= 1);  // This is a 1-based array
    return array[index];
}

これは上限を超えないように保護するものではないことに注意してください。ただし、これは簡単に処理できます。


補遺:

C99ドラフトのいくつかの章と詩(申し訳ありませんが、リンクできるのはそれだけです):

§6.5.2.1.1は、添字演算子で使用される2番目の(「その他」)式は整数型であると述べています。 -1は整数であり、これによりp[-1]有効になり、したがってポインターも有効になり&(p[-1])ます。これは、その場所でメモリにアクセスすると定義された動作が生成されることを意味しませんが、ポインターは依然として有効なポインターです。

§6.5.2.2従って、ポインタに要素番号を追加の相当する配列添字演算子評価することを言うp[-1]と等価です*(p + (-1))。まだ有効ですが、望ましい動作が得られない場合があります。

§6.5.6.8によると(強調鉱山):

整数型の式がポインターに追加またはポインターから減算されると、結果はポインターオペランドの型になります。

...式が配列オブジェクトの-th要素をP指す場合、式(同等に)および (where の値は)は、存在する場合、それぞれ配列オブジェクトの-thおよび -th要素を指しますi(P)+NN+(P)(P)-NNni+ni−n

これは、ポインター演算の結果が配列内の要素を指している必要があることを意味します。算術演算を一度に行う必要があるとは言いません。したがって:

double a[20];

// This points to element 9 of a; behavior is defined.
double d = a[-1 + 10];

double *p = a - 1;  // This is just a pointer.  No dereferencing.

double e = p[0];   // Does not point at any element of a; behavior is undefined.
double f = p[1];   // Points at element 0 of a; behavior is defined.

このようにすることをお勧めしますか?私はしません、そして私の答えはその理由を説明します。


8
-1未定義の結果を生成することは有用ではないとC標準が宣言するコードを含む「許可」の定義。
ピートKirkham

他の人は、それは未定義の振る舞いであると指摘しているので、「許可された」と言うべきではありません。ただし、余分な未使用の要素0を割り当てることをお勧めします。
200_success

これは本当に正しくありません。少なくともC標準では禁止されていることに注意してください。
バリティ14

@PeteKirkham:私は同意しません。私の答えの補遺をご覧ください。
Blrfl 14

4
ISO C11標準の@Blrfl 6.5.6は、ポインターに整数を追加する場合の状態を示しています。 、評価によってオーバーフローが発生することはありません。それ以外の場合、動作は未定義です。」
バリティ14
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.