Cでポインター比較はどのように機能しますか?同じ配列を指さないポインターを比較しても大丈夫ですか?


33

K&R(Cプログラミング言語第2版)の第5章で以下を読みました。

まず、特定の状況下でポインタを比較できます。もしpq同じ配列のメンバーへのポイント、その後のような関係==!=<>=、などの作業を適切に。

これは、同じ配列を指すポインターのみを比較できることを意味しているようです。

しかし、私がこのコードを試したとき

    char t = 't';
    char *pt = &t;
    char x = 'x';
    char *px = &x;

    printf("%d\n", pt > px);

1 画面に出力されます。

まず第一に、私はので、私は、未定義またはいくつかのタイプやエラーになるだろうと思ったptし、px(少なくとも私の理解では)同じ配列を指していません。

またpt > px、両方のポインタがスタックに格納されている変数を指しているため、スタックtが大きくなり、メモリアドレスがx?どちらがpt > px本当ですか?

mallocが導入されると、さらに混乱します。また、8.7章のK&Rには、次のように書かれています。

ただし、によって返されるさまざまなブロックへのポインターをsbrk有意義に比較できるという前提はまだ1つあります。これは、配列内でのみポインタ比較を許可する標準では保証されていません。したがって、このバージョンのmallocは、一般的なポインタ比較が意味のあるマシン間でのみ移植可能です。

ヒープでmallocされたスペースを指すポインターとスタック変数を指すポインターを比較しても問題はありませんでした。

たとえば、次のコードは正常に機能し1、印刷されました。

    char t = 't';
    char *pt = &t;
    char *px = malloc(10);
    strcpy(px, pt);
    printf("%d\n", pt > px);

私のコンパイラーでの実験に基づいて、ポインターが個別にどこを指しているかに関係なく、ポインターは他のポインターと比較できると考えられています。さらに、2つのポインター間のポインター演算は、ポインターが格納するメモリアドレスを使用するだけなので、ポインターが個別にどこを指すかに関係なく、問題ないと思います。

それでも、K&Rで何を読んでいるのか混乱しています。

私が尋ねている理由は私の教授だからです。実際に試験問題にしました。彼は次のコードを与えました:

struct A {
    char *p0;
    char *p1;
};

int main(int argc, char **argv) {
    char a = 0;
    char *b = "W";
    char c[] = [ 'L', 'O', 'L', 0 ];

   struct A p[3];
    p[0].p0 = &a;
    p[1].p0 = b;
    p[2].p0 = c;

   for(int i = 0; i < 3; i++) {
        p[i].p1 = malloc(10);
        strcpy(p[i].p1, p[i].p0);
    }
}

これらは何を評価しますか:

  1. p[0].p0 < p[0].p1
  2. p[1].p0 < p[1].p1
  3. p[2].p0 < p[2].p1

答えは010

(私の教授は、質問がUbuntu Linux 16.04、64ビットバージョンのプログラミング環境に関するものであるという免責事項を試験に含めています)

(編集者注:SOがより多くのタグを許可した場合、その最後の部分は、そしておそらくを保証します。質問/クラスのポイントがポータブルCではなく、具体的に低レベルのOS実装の詳細であった場合。)


17
あなたは多分何であるか混乱している有効Cあるものと安全C同じ型への 2つのポインタの比較は常に可能です(たとえば、等価性のチェック)。ただし、ポインタ演算と比較>を使用して、特定の配列(またはメモリブロック)内で使用する場合に<のみ安全です。
Adrian Mole

13
余談ですが、K&RからCを学ぶべきでありませ。初めに、言語はそれ以来多くの変更を経てきました。そして、正直に言うと、そこにあるサンプルコードは、読みやすさよりも簡潔さが重視されていた時代のものです。
paxdiablo

5
いいえ、動作することは保証されていません。セグメント化されたメモリモデルを持つマシンでは実際には失敗する可能性があります。参照んCは、STDと同等のものを持っている:: C ++から少ないですか?最近のほとんどのマシンでは、UBに関係なく動作します。
Peter Cordes

6
@Adam:近いですが、これは実際にはUBです(OPが使用していたコンパイラGCCがそれを定義することを選択しない限り、それは可能性があります)。しかし、UBは「確実に爆発する」という意味ではありません。UBの可能な動作の1つは、期待どおりに動作しています!! これがUBの厄介な原因です。デバッグビルドで正しく動作し、最適化を有効にして失敗するか、その逆、または周囲のコードに応じて中断することができます。 他のポインタを比較しても答えは得られますが、言語はその答えの意味を定義していません(もしあれば)。 いいえ、クラッシュは許可されています。まさにUBです。
Peter Cordes

3
@アダム:はい、コメントの最初の部分は気にしないでください。しかし、他のポインタを比較しても答えが得られると主張しています。それは真実ではない。完全なUBではなく、詳細不明です。UBははるかに悪く、実行がそれらの入力でそのステートメントに到達すると(実際に発生する前または後の任意の時点で)、プログラムがsegfaultまたはSIGILLする可能性があります。(UBがコンパイル時に表示される場合にのみx86-64でもっともらしいですが、一般的には何でも起こります。)UBのポイントの一部は、asmの生成中にコンパイラーに「安全でない」仮定をさせることです。
Peter Cordes

回答:


33

よるC11標準、関係演算子<<=>、及び>=、同じ配列または構造体オブジェクトの要素へのポインタで使用されてもよいです。これはセクション6.5.8p5で詳しく説明されています。

2つのポインターを比較する場合、結果は、指し示されるオブジェクトのアドレス空間内の相対位置に依存します。オブジェクト型への2つのポインタが両方とも同じオブジェクトを指している場合、または両方が同じ配列オブジェクトの最後の要素の1つ先を指している場合、それらは等しいと比較します。指し示されたオブジェクトが同じ集合オブジェクトのメンバーである場合、後で宣言された構造体メンバーへのポインターは、構造体の最初に宣言されたメンバーへのポインターよりも大きいと比較し、添字値が大きい配列要素へのポインターは、同じ配列の要素へのポインターよりも大きい添え字の値が小さい。同じユニオンオブジェクトのメンバーへのすべてのポインターは等しいと比較します。

この要件を満たさない比較は、未定義の動作を呼び出すことに注意してください。これは、(とりわけ)結果を繰り返し可能であるかどうかに依存できないことを意味します。

特定のケースでは、2つのローカル変数のアドレス間の比較と、ローカルアドレスと動的アドレスのアドレスの比較の両方で、操作は「機能している」ように見えましたが、コードに一見無関係な変更を加えることで結果が変わる可能性がありますまたは、異なる最適化設定で同じコードをコンパイルすることもできます。未定義の動作では、コードクラッシュしたりエラーを生成したりする可能性があるからといって、そうなるとは限りません。

例として、8086リアルモードで実行されているx86プロセッサには、16ビットセグメントと16ビットオフセットを使用して20ビットアドレスを構築するセグメントメモリモデルがあります。したがって、この場合、アドレスは正確に整数に変換されません。

等価演算子==!=ただし、この制限はありません。これらは、互換性のある型への任意の2つのポインターまたはNULLポインター間で使用できます。したがって、==または!=両方の例でまたはを使用すると、有効なCコードが生成されます。

ただし、それを使用==!=ても、予期しない結果がまだ明確に定義されている結果が得られる可能性があります。関連のないポインタの等価比較はtrueと評価できるか?を参照してくださいこの詳細については。

あなたの教授から出題された試験問題に関して、それは多くの欠陥のある仮定をします:

  • フラットメモリモデルは、アドレスと整数値の間に1対1の対応がある場合に存在します。
  • 変換されたポインター値が整数型の内部に収まること。
  • 実装は、未定義の動作によって与えられる自由を利用せずに比較を実行するときに、ポインタを整数として扱うだけです。
  • スタックが使用され、ローカル変数がそこに格納されていること。
  • 割り当てられたメモリをプルするためにヒープが使用されること。
  • スタック(したがってローカル変数)がヒープ(したがって割り当てられたオブジェクト)よりも高いアドレスに表示されること。
  • その文字列定数は、ヒープよりも低いアドレスに表示されます。

このコードをアーキテクチャー上で実行したり、これらの前提を満たさないコンパイラーを使用して実行したりすると、非常に異なる結果が得られる可能性があります。

また、どちらの例も、を呼び出したときに未定義の動作を示しますstrcpy。これは、右側のオペランドが(場合によっては)nullで終了する文字列ではなく単一の文字を指すため、関数が指定された変数の境界を超えて読み取るためです。


3
@酒々井それでも、結果に依存するべきではありません。コンパイラーは最適化に関して非常に積極的になり、未定義の動作をその機会として使用します。異なるコンパイラーや異なる最適化設定を使用すると、異なる出力が生成される可能性があります。
dbush

2
@酒々井:通常、x86-64などのフラットメモリモデルのマシンで動作します。そのようなシステムの一部のコンパイラは、動作をドキュメントで定義している場合もあります。しかし、そうでない場合、コンパイル時に表示されるUBが原因で「異常な」動作が発生する可能性があります。(実際には、誰もそれを望んでいないと思うので、主流のコンパイラーが探して「壊そうとする」ものではありません。)
Peter Cordes

1
コンパイラーが実行の1つのパスが結果とローカル変数(自動ストレージ、つまりスタック)の<間につながると認識した場合と同様にmalloc、実行パスが決して取られないと仮定して、関数全体をud2命令にコンパイルするだけです(不正を発生させます) -カーネルがプロセスにSIGILLを配信することによって処理する命令例外)。GCC / clangは、他の種類のUBに対して実際にこれを行いvoidます。たとえば、非関数の終わりから落ちるようなものです。 godbolt.orgは現在ダウンしているようですが、コピー/貼り付けint foo(){int x=2;}を試してみてくださいret
Peter Cordes

4
@Shisui:TL:DR:x86-64 Linuxではたまたまうまく機能するという事実にもかかわらず、移植可能なCではありません。ただし、比較の結果について仮定を立てるのはおかしいだけです。メインスレッドにいない場合malloc、OSからより多くのメモリを取得するために使用するのと同じメカニズムを使用してスレッドスタックが動的に割り当てられるため、ローカル変数(スレッドスタック)がmalloc動的に割り当てられていると想定する理由はありませんストレージ。
Peter Cordes

2
@PeterCordes:必要に応じて、動作のさまざまな側面を「オプションで定義された」ものとして認識し、実装が自由に定義できるかどうかを判断できるようにしますが、そうでない場合はテスト可能な方法(事前定義されたマクロなど)で示す必要があります。さらに、最適化の効果が「未定義の動作」として観察可能であるという状況を特徴付ける代わりに、オプティマイザが特定の動作を「観察不能」と見なす場合、オプティマイザがそれらを示す場合、それを「非観察可能」と見なすことがはるかに有用です。そうする。たとえば、指定int x,y;された実装...
スーパーキャット

12

同じ型の2つの異なる配列へのポインターを比較する際の主な問題は、配列自体を特定の相対位置に配置する必要がないことです。一方が他方の前後に配置される可能性があります。

まず、ptとpxが同じ配列を指していないため(少なくとも私の理解では)、未定義または何らかのタイプまたはエラーが発生すると思いました。

いいえ、結果は実装やその他の予測不可能な要因に依存します。

また、両方のポインターがスタックに格納されている変数を指しているため、pt> pxであり、スタックが大きくなるため、tのメモリアドレスはxのメモリアドレスよりも大きくなりますか?pt> pxがtrueである理由はどれですか?

必ずしもスタックがあるとは限りません。それが存在する場合、成長する必要はありません。大きくなる可能性があります。奇妙な方法で隣接していない可能性があります。

さらに、2つのポインター間のポインター演算は、ポインターが格納するメモリアドレスを使用するだけなので、ポインターが個別にどこを指すかに関係なく、問題ないと思います。

関係演算子(つまり、使用している比較演算子)について説明しているC仕様、85ページの6.5.8を見てみましょう。これは直接!=または==比較には適用されないことに注意してください。

2つのポインターを比較する場合、結果は、指し示されるオブジェクトのアドレス空間内の相対位置によって異なります。...指し示されたオブジェクトが同じ集合オブジェクトのメンバーである場合、...添え字値が大きい配列要素へのポインターは、添え字値が小さい同じ配列の要素へのポインターよりも大きくなります。

他のすべての場合、動作は未定義です。

最後の文は重要です。スペースを節約するためにいくつかの無関係なケースを削減しましたが、私たちにとって重要なケースが1つあります。同じ構造体/集計オブジェクト1の一部ではない2つの配列で、これら2つの配列へのポインターを比較しています。これは未定義の動作です。

コンパイラーがポインターを数値的に比較するある種のCMP(比較)マシン命令を挿入しただけで、ここで運が良ければ、UBはかなり危険な獣です。文字通り、何でも起こりえます-あなたのコンパイラは目に見える副作用を含む関数全体を最適化することができます。鼻の悪魔を生み出す可能性があります。

1 2つの配列が同じ集合オブジェクト(構造体)の一部であるという節に該当するため、同じ構造体の一部である2つの異なる配列へのポインターを比較できます。


1
でさらに重要なこと、tおよびx同じ機能で定義されている、コンパイラのターゲットx86-64では、この関数のスタックフレームに地元の人々をレイアウトする方法について何も仮定するゼロ理由があります。スタックが下向きに成長することは、1つの関数内の変数の宣言順序とは関係ありません。別の関数でも、1つが他の関数にインライン化できる場合、「子」関数のローカルが親と混在する可能性があります。
Peter Cordes

1
コンパイラは目に見える副作用を含めて関数全体を最適化できます 誇張ではありません:他の種類のUB(非void関数の終わりから落ちるような)の場合、g ++とclang ++は実際にそれを実際に行います: godbolt.org/z/g5vesBそれら実行パスがUBにつながるために使用されないと仮定し、そのような基本ブロックをすべて不正な命令にコンパイルします。または、まったく何の指示もなく、その関数が呼び出された場合、次のasmに黙ってフォールスルーします。(何らかの理由でgccこれを行わないのはだけですg++)。
Peter Cordes

6

次に何を尋ねた

p[0].p0 < p[0].p1
p[1].p0 < p[1].p1
p[2].p0 < p[2].p1

評価する。答えは0、1、0です。

これらの質問は次のようになります。

  1. スタックの上または下のヒープです。
  2. プログラムの文字列リテラルセクションの上または下のヒープです。
  3. [1]と同じ。

そして、3つすべてに対する答えは、「実装定義」です。あなたの教授の質問は偽です。彼らはそれを伝統的なunixレイアウトに基づいています:

<empty>
text
rodata
rwdata
bss
< empty, used for heap >
...
stack
kernel

しかし、いくつかの近代的なユニックス(および代替システム)は、これらの伝統に準拠していません。彼らが質問の前に「1992年現在」を付けない限り; 評価では必ず-1を指定してください。


3
実装定義ではなく、未定義です!このように考えると、前者は実装間で異なる可能性がありますが、実装は動作がどのように決定されるかを文書化する必要があります。後者は、動作がどのように変化しても、実装がスクワットを指示する必要がないことを意味します:-)
paxdiablo

1
@paxdiablo:標準の作成者による根拠によれば、「未定義の動作...は、準拠する言語拡張の可能性のある領域も識別します。実装者は、公式に未定義の動作の定義を提供することにより、言語を拡張できます。」理論的根拠は、「目標は、移植性がない、したがって副詞が厳密にない完全に有用なCプログラムを非難するように思われることなく、プログラマーに移植性の高い強力なCプログラムを作成するための戦いの機会を与えることです。」商用のコンパイラライターはこれを理解していますが、他のコンパイラライターは理解していません。
スーパーキャット

別の実装定義された側面があります。ポインター比較は署名されているため、マシン/ os /コンパイラーによっては、一部のアドレスが負として解釈される場合があります。たとえば、スタックを0xc << 28に配置した32ビットマシンでは、ヒープまたはrodataよりも小さいアドレスに自動変数が表示される可能性があります。
mevets

1
@mevets:規格では、比較におけるポインタの符号性が観察可能な状況を指定していますか?16ビットプラットフォームで32768バイトを超えるオブジェクトが許可されていてarr[]、そのようなオブジェクトである場合、標準では、符号付きポインターの比較で報告される場合arr+32768よりarrも多くの値を比較することが義務付けられます。
スーパーキャット

知りません; C規格は、ダンテの第9サークルを周回し、安楽死を祈っています。OPは特にK&Rと試験問題に言及しました。#UBは怠惰なワーキンググループからの破片です。
mevets

1

ほとんどすべてのリモートモダンプラットフォームでは、ポインターと整数には同型の順序関係があり、互いに素なオ​​ブジェクトへのポインターはインターリーブされません。ほとんどのコンパイラは、最適化が無効になっている場合にプログラマにこの順序を公開しますが、標準では、そのような順序を持つプラットフォームとそうでないプラットフォームを区別せず、実装がプログラマにこのような順序を公開する必要があるプラットフォームでも、それを定義します。その結果、一部のコンパイラ作成者は、コードが異なるオブジェクトへのポインタの使用関係演算子を決して比較しないという仮定に基づいて、さまざまな種類の最適化および「最適化」を実行します。

公開された根拠によれば、標準の作成者は、標準が「未定義の動作」として特徴付けられる状況(つまり、標準が要件を課さない場合)での動作を指定することにより、実装が言語を拡張することを意図しました場合)でしかし、一部のコンパイラ作成者は、プログラムが標準で義務付けられている範囲を超えて恩恵を受けようとはせず、プラットフォームがサポートできる動作を追加費用なしで有効に利用できるようにすることを想定しています。

私はポインター比較で奇妙なことをする商業的に設計されたコンパイラーについては知りませんが、コンパイラーがバックエンドの非商用LLVMに移動すると、以前に指定された動作の無意味なコードを処理する可能性が高まります彼らのプラットフォームのためのコンパイラ。このような動作は、関係演算子に限定されませんが、等価/不等価に影響を与えることさえあります。たとえば、標準では、1つのオブジェクトへのポインターと直前のオブジェクトへの「直前」のポインターとの比較は等しいと比較するように指定されていますが、gccおよびLLVMベースのコンパイラーは、プログラムがこのようなコードを実行すると、無意味なコードを生成する傾向があります比較。

等価比較でさえgccとclangで無意味に動作する状況の例として、次のことを考慮してください。

extern int x[],y[];
int test(int i)
{
    int *p = y+i;
    y[0] = 4;
    if (p == x+10)
        *p = 1;
    return y[0];
}

clangとgccはどちらも、x10要素であっても常に4を返し、yその直後にあり、i0であるコードを生成します。その結果、比較はtrueになりp[0]、値1で書き込まれます。最適化の1回のパスで書き換えが行われると思います関数*p = 1;がに置き換えられたかのようにx[10] = 1;。コンパイラがと同等と解釈*(x+10)した場合、後者のコードは同等になります*(y+i)が、残念ながら、下流の最適化ステージでは、少なくとも11個の要素があるx[10]場合にのみアクセスが定義されると認識されるxため、そのアクセスがに影響することはありませんy

コンパイラーが標準で記述されているポインター等価シナリオでその「クリエイティブ」を取得できる場合、標準が要件を課していない場合、コンパイラーがさらにクリエイティブになるのを控えることはありません。


0

それは単純です。オブジェクトのメモリ位置が宣言したのと同じ順序になるとは限らないため、ポインタを比較しても意味がありません。例外は配列です。&array [0]は&array [1]よりも小さいです。それがK&Rが指摘していることです。実際には、構造体メンバーのアドレスも、私の経験で宣言した順序になっています。保証はありません...もう1つの例外は、ポインタを等しいかどうか比較する場合です。あるポインターが別のポインターと等しい場合、同じオブジェクトを指していることがわかります。それが何であれ。あなたが私に尋ねる場合、悪い試験問題。Ubuntu Linux 16.04、試験問題の64ビットバージョンのプログラミング環境に依存しますか?本当に ?


技術的には、配列本当にあなたが宣言していないので、例外arr[0]arr[1]別途、など。arr全体として宣言するので、個々の配列要素の順序は、この質問で説明されているものとは別の問題です。
paxdiablo

1
構造要素は正しい順序であることが保証されています。これmemcpyにより、構造の隣接部分をコピーし、その中のすべての要素に影響を与え、他の要素には影響を与えないことが保証されます。標準は、構造体でどのような種類のポインタ演算を実行できるか、または、malloc()割り当てられたストレージをです。offsetofマクロは、1つの可能性でない場合と同様に、構造体のバイトのポインタ演算の同じ種類にかなり役に立たないであろうchar[]が、標準明示構造体のバイトである(またはとして使用することができる)ことを言っていません配列オブジェクト。
スーパーキャット

-4

なんて挑発的な質問だ!

このスレッドでの応答とコメントの大まかなスキャンでも、一見単純で単純なクエリがいかに感動的かがわかります。

それは驚くべきことではありません。

間違いなく、の概念と使用に関する誤解ポインタのプログラミング全般における重大な障害の主な原因を表しています。

この現実の認識は、対処するために、そして好ましくはポインターが完全にもたらす課題を回避するために特別に設計された言語の遍在性において容易に明らかです。C ++とその他のC、Javaとその関係、Pythonとその他のスクリプトの派生物を考えてみてください。単に、より目立って普及しているものであり、問​​題への対処の厳しさは多かれ少なかれ順序付けられています。

根本的な原則をより深く理解することは、 、関連することを熱望するすべての個々の卓越したプログラミングで-特にシステムレベルで

これはまさにあなたの先生が示すことを意味していると思います。

また、Cの性質により、Cはこの探索に便利な手段になります。アセンブリほど明確ではありませんが、おそらくより容易に理解できますが、実行環境のより深い抽象化に基づく言語よりもはるかに明確です。

決定論を促進するように設計Cは、プログラマーの意図を機械が理解できる命令に変換おり、システムレベルの言語です。高レベルに分類されていますが、実際には「中」のカテゴリに属しています。しかし、そのようなものが存在しないため、「システム」の指定で十分です。

この特性は、デバイスドライバーオペレーティングシステムコード、および組み込み実装で、この言語を最適な言語にする責任があります。さらに、最適な効率が最優先であるアプリケーションでは当然のことながら好ましい代替手段です。ここでそれは生存と絶滅の違いを意味するため、贅沢ではなく必需品です。そのような場合、携帯性の魅力的な利便は、その魅力をすべて失い、光沢のないパフォーマンスのパフォーマンスを選択します。最小公分母のは、考えられないほど有害なオプションになります。

Cとその派生物のいくつかを非常に特別なものにしているの、ユーザーが望んでいるときに、関連する責任をユーザーに課すことなく、ユーザーが完全に制御できることです。それにもかかわらず、それは機械からの最も薄い断熱材以上のものを決して提供しないので、適切な使用はポインターの概念の厳密な理解要求します

本質的に、あなたの質問に対する答えは、あなたの疑いを確認する上で、非常にシンプルで満足できるほど甘いです。ただし、必要な重要性このステートメントのすべての概念にします。

  • ポインタを、調査し比較し、操作の行為は常にしている必然の結果から得られた結論は、このように必要性を数値の妥当性に依存含まれており、一方で、有効なないこと。

前者は、両方で常に 安全かつ潜在的に 適切な後者の缶がしかなりながら、適切なそれがされたときに確立安全。驚くべきこと-にいくつか-そう、後者の有効性を確立することに依存して要求元を。

もちろん、混乱の一部は、ポインターの原則内に本質的に存在する再帰の影響と、アドレスとコンテンツを区別することで生じる課題から生じます。

あなたはかなり正確に推測しました、

個々のポインタがどこを指しているかに関係なく、どのポインタも他のポインタと比較できると私は思わされています。さらに、2つのポインター間のポインター演算は、ポインターが格納するメモリアドレスを使用するだけなので、ポインターが個別にどこを指すかに関係なく、問題ないと思います。

そして、いくつかの貢献者は断言しました:ポインタは単なる数字です。時に近いもの複素数にが、それでも数に過ぎないもの。

この論争がここで受け取られた愉快な犯罪は、プログラミングよりも人間の本質についてより多くを明らかにしますが、注目に値し、詳述する価値があります。おそらく後でそうするでしょう...

1つのコメントが示唆し始めると、このすべての混乱と驚きは、有効なものと安全ものを区別する必要性から生じますが、それは単純化しすぎです。また、機能的信頼できるもの、実用的適切なもの、さらには特定の状況で適切なものと、より一般的な意味で適切なものとを区別する必要もあります。言うまでもなく; 適合性妥当の違い。

そのためには、まずポインタ何であるかを正確に理解する必要があります

  • あなたはコンセプトをしっかりと把握してきました。他の人と同じように、これらの図は非常に単純化されていると思うかもしれませんが、ここで明らかな混乱のレベルは、説明を簡潔にすることを要求しています。

いくつか指摘しているように、ポインタという用語は単にインデックスであるものの単なる特別な名前であり、したがって、他のどの番号にもすぎません。

これは、既にあるべき自明すべての現代的な主流のコンピュータがあるという事実を考慮して、バイナリのマシン必ずしも動作し、排他的にし、上の数字。量子コンピューティング変えるませんが、それは非常にありそうにありません、そしてそれは成熟していません。

技術的には、既に述べたように、ポインタはより正確なアドレスです。それらを家の「住所」または通りの区画と相関させることのやりがいのある類推を自然に導入する明白な洞察。

  • フラットなメモリモデル:同じ道路上の都市うそのすべての家を、そしてすべての家が独自にその番号のみで識別されています全体のシステムメモリは、単一の、線状配列で構成されています。楽しくシンプル。

  • セグメント化されたスキーム:番道路の階層構造は、複合アドレスが必要とされているように番号を付け家のそれの上に導入されます。

    • いくつかの実装はさらに複雑で、個別の「道路」の合計が連続したシーケンスになる必要ありませんが、基礎となるものを変更するものはありません
    • 私たちは必然的に、そのようなすべての階層リンクをフラットな組織に分解して戻すことができます。組織が複雑になるほど、そのために必要なフープも増えますが、それ可能でなければなりません。実際、これはx86の「リアルモード」にも適用されます。
    • それ以外の場合は、信頼性の高い実行(システムレベルで)が必須である必要があるため、場所へのリンクのマッピングは全単射ありません。
      • 複数のアドレスが単一のメモリ位置にマップしてはならない。
      • 単一のアドレスは、複数のメモリ位置にマップしてはなりませ

難問をこのような魅力的に複雑なもつれに変えるさらなるねじれに私たちを連れて来てください。上記では、単純さと明快さのために、ポインタアドレスであると示唆するのが好都合でした。もちろん、これは正しくありません。ポインタアドレスではありません。ポインタがある参照アドレスには、それが含まれているアドレスを。封筒スポーツのように家への参照。これを熟考すると、概念に含まれている再帰の提案で何が意味されているのかを垣間見る可能性があります。それでも; 私たちはあまりにも多くの単語を持っています、そしてアドレスへの参照アドレスについて話しますなど、無効なオペコード例外でほとんどの頭脳をすぐに失速させます。そして、ほとんどの場合、意図はコンテキストからすぐに獲得されるので、私たちは通りに戻りましょう。

私たちのこの架空の都市の郵便局員は、「現実の」世界で私たちが見つけるものとよく似ています。無効なアドレスについて話したり問い合わせたりしても、脳卒中になる可能性はほとんどありませんが、その情報に基づいて行動するように依頼すると、最後の人は誰もが頭を痛めます。

私たちの特異な通りに20軒の家しかないと仮定します。さらに、いくつかの誤った、または失読症の魂は今、数71に、手紙、非常に重要なものを指示したことをふり、我々はそのようなアドレスがあるかどうか、私たちのキャリアフランクを求めることができ、彼は簡単かつ冷静になりますレポート:なし。私たちも、彼はそれがあれば、この場所はうそだろうか遠く外の通りを推定することを期待することができなかったが存在する:およそ2.5倍、さらに端部よりも。これによって彼に憤慨が生じることはありません。しかし、この手紙を届けるように、またはその場所からアイテムを受け取るように頼んだ場合、彼は不快感について率直に言って従うことを拒否するでしょう。

ポインタは単なるアドレスであり、アドレスは単なる数値です。

次の出力を確認します。

void foo( void *p ) {
   printf(“%p\t%zu\t%d\n”, p, (size_t)p, p == (size_t)p);
}

有効かどうかに関係なく、好きなだけポインタを呼び出してください。してください、それはあなたのプラットフォーム上で失敗した場合、またはお使いの場合は、あなたの調査結果を投稿(現代)コンパイラが文句を言います。

現在、ポインタ単なる数値なので、それらを比較することは必然的に有効です。ある意味で、これはまさにあなたの教師が示していることです。以下のステートメントはすべて完全に有効であり、適切です!- C、およびコンパイル済みの問題に遭遇することなく実行されますどちらのポインタを初期化する必要が、彼らが含まれている値は、したがってすることができるにもかかわらず、未定義

  • 私たちは、計算されresult 、明示的にのために明快さ、および印刷することはして強制的にそうでない場合は冗長、デッドコードがどうなるかを計算するようにコンパイラに。
void foo( size_t *a, size_t *b ) {
   size_t result;
   result = (size_t)a;
   printf(“%zu\n”, result);
   result = a == b;
   printf(“%zu\n”, result);
   result = a < b;
   printf(“%zu\n”, result);
   result = a - b;
   printf(“%zu\n”, result);
}

もちろん、テストの時点でaまたはbのいずれかが未定義(読み取り:正しく初期化されていない)である場合、プログラムの形式は正しくありませんが、これは、ここでの説明のこの部分とはまったく関係ありませ。これらのスニペットは、あまりにも次の文のように、されている保証 - 「標準」で-するコンパイル実行完璧に、かかわらずのIN関わるあらゆるポインタの-validity。

問題は、無効なポインタが逆参照されたときにのみ発生します。フランクに無効な存在しない住所への集荷または配達を依頼した場合。

任意のポインタが与えられた場合:

int *p;

このステートメントはコンパイルして実行する必要がありますが、

printf(“%p”, p);

...これが必要です:

size_t foo( int *p ) { return (size_t)p; }

...次の2つは対照的に、コンパイルは簡単ですが、ポインタ有効でない限り実行に失敗します。これにより、ここでは、現在のアプリケーションがアクセスを許可されているアドレスを参照していることを意味します

printf(“%p”, *p);
size_t foo( int *p ) { return *p; }

変化はどれほど微妙ですか?-ポインタの値との差が区別嘘その番号で家の:アドレス、および内容の値。ポインタが逆参照されるまで問題は発生しません。リンク先のアドレスへのアクセスが試行されるまで。道路を越えて荷物を配達または受け取りしようとするとき...

拡大すると、同じ原則が必然的に前述の必要な有効性を確立する必要性を含む、より複雑な例にも適用されます。

int* validate( int *p, int *head, int *tail ) { 
    return p >= head && p <= tail ? p : NULL; 
}

関係比較と算術は、等価性のテストと同じユーティリティを提供し、原則として同等に有効です。しかし、そのような計算の結果が何を意味するかは、まったく別の問題であり、正確には、あなたが含めた引用によって対処される問題です。

Cでは、配列は連続したバッファーであり、連続した線形の一連のメモリ位置です。そのような特異なシリーズ内の位置を参照するポインターに適用される比較と算術は、当然ながら、相互に、およびこの「配列」(単純にベースで識別される)の両方に関して明らかに意味があります。まったく同じことがmalloc、またはを介して割り当てられたすべてのブロックに適用されますsbrkので、これらの関係がある暗黙の、コンパイラは、それらの間の有効な関係を確立することができる、ことができるので自信を持って計算が予想される答えを提供すること。

参照ポインタに類似の体操を行う別個のブロックまたはアレイは、任意のそのような提供していない固有の、及び見かけのユーティリティを。なぜなら、ある時点で存在する関係は、その後に続く再割り当てによって無効にされる可能性があるためです。この再割り当ては、変更される可能性が非常に高いため、反転されます。このような場合、コンパイラは、以前の状況での信頼を確立するために必要な情報を取得できません。

ただし、プログラマーとして、あなたはそのような知識を持っているかもしれません!そして、場合によってはそれを利用する義務があります。

そこAREような状況のため、これさえ完全でVALIDと完璧PROPERは。

実際、まさにmallocそれは、再利用されたブロックをマージしようとする時が来たとき、それ自体が内部的にしなければならないことです-大部分のアーキテクチャで。オペレーティングシステムアロケータについても同様ですsbrk。もし、より明らかに頻繁に、上の複数の異なる複数のエンティティ、批判的にも、関連する、これはプラットフォーム上で- mallocないかもしれません。また、Cで書かれていないものはどれくらいありますか?

アクションの有効性、安全性、および成功は、必然的にそれが前提とされ、適用される洞察のレベルの結果です。

あなたが提供した引用の中で、カーニハンとリッチーは密接に関連しているが、それでも別個の問題に取り組んでいます。それらは言語制限定義し、コンパイラーの機能を利用して、少なくとも誤った構造を検出することによってユーザーを保護する方法を説明しています。それらは、プログラミングタスクを支援するために、メカニズムが可能な長さ、つまり設計された長さを説明しています。コンパイラはあなたのしもべであり、あなたマスターです。ただし、賢明なマスターは、彼のさまざまな使用人の能力に精通しているマスターです。

この文脈では、未定義の動作は潜在的な危険と危害の可能性を示すのに役立ちます。私たちが知っているように、差し迫った、不可逆的な運命、または世界の終わりを意味するものではありません。それは単にことを意味し、私たち「コンパイラの意味は、」 - - このことはあるかもしれないものについての推測をする、または表すことができないと、このような理由のために、我々は問題の私たちの手を洗うことを選択します。この施設の使用または誤用に起因する可能性のあるいかなる冒険についても、当社は責任を負いません

実際、それは単に「このポイントを超えて、カウボーイ:あなたはあなた自身である...」と単に言います。

あなたの教授はあなたにもっと細かいニュアンスを示すことを求めています。

彼らが彼らの例を作る際にどれほど細心の注意払ったかに注目してください。そしてどのようにもろく、それはまだです。のアドレスを取ることによりa

p[0].p0 = &a;

コンパイラーは、変数をレジスターに配置するのではなく、変数に実際のストレージを割り振るように強制されます。これは自動変数なので、プログラマーはそれがどこに割り当てられているかを制御できません。そのため、それに続くものについて有効な推測をすることができません。これが、コードが期待どおりに機能するためにゼロに設定する必要がある理由です。a

この行を変更するだけです:

char a = 0;

これに:

char a = 1;  // or ANY other value than 0

プログラムの動作が未定義になります。少なくとも、最初の答えは1になります。しかし、問題ははるかに不吉です。

現在、コードは災害を招いています。

それでもながら完全に有効としても、標準に準拠し、それは今ある病気-形成し、コンパイルしてくださいが、様々な理由で実行に失敗することがあります。今のところ存在しない複数の問題- どれもそのコンパイラがあることして認識しています。

strcpyのアドレスから開始し、aこれを超えて、nullが検出されるまで、バイトごとに消費し、転送します。

p1ポインタは、まさにのブロックに初期化されている10バイト。

  • 場合a、ブロックの端部に配置されるように起こり、プロセスは次のものへのアクセス何を持っていない、非常に次のリード- P0の[1] -セグメンテーション違反を誘発します。このシナリオはx86アーキテクチャではありそうもありませんが、可能です。

  • のアドレスを超えた領域a アクセス可能であれば、読み取りエラーは発生しませんが、プログラムは依然として不幸から救われていません。

  • 場合はゼロバイトが起こるのアドレスで始まる10内で発生しa、それがあり、その後のために、まだ、生き残るstrcpy停止し、少なくとも我々は、書き込み違反を被ることはありません。

  • ミスによる読み取りに障害はないが、この10のスパンでゼロバイトが発生しない場合strcpyは続行し、によって割り当てられたブロックを超えて書き込みを試みmallocます。

    • この領域がプロセスによって所有されていない場合は、segfaultがすぐにトリガーされます。

    • さらに悲惨な-そして微妙な ---状況は、次のブロックプロセスによって所有されている場合に発生します。そのため、エラー検出できず、シグナルを発生させることができずそれでも「動作」するように見えます。実際には、他のデータ、アロケータの管理構造、またはコード(特定の動作環境)を上書きします。

これがポインタに関連するバグの追跡が非常に困難になる理由です。これらの行が、他の誰かが書いた数千行の複雑に関連するコードの行の奥深くに埋め込まれていて、あなたが徹底的に調査するように指示されていると想像してください。

それにもかかわらず完全に有効標準に準拠した Cであり続けるため、プログラムコンパイルする必要があります。

エラーこれらの種類は、何の標準およびコンパイラは不用心に対してを守ることはできません。それがまさに彼らがあなたに教えようとしていることだと思います。

偏執狂的な人々は絶えずC の性質変えてこれらの問題のある可能性を捨て、私たちを私たち自身から救おうと努めています。しかし、それは不誠実です。これは、を追求し、機械をより直接的かつ包括的に制御することで得られる自由を得ることを選択した場合に受け入れる義務がある責任です。パフォーマンスの完璧さを追求するプロモーターと追求者は、これ以上何も受け入れません。

移植性とそれが表す一般は根本的に別の考慮事項であり、規格が対処しようといるすべてのことです。

このドキュメントは、フォームを指定し、プログラミング言語Cで表現されたプログラムの解釈を確立します。 目的さまざまなコンピューティングシステムでのC言語プログラムの移植性、信頼性、保守性、および効率的な実行促進することです

これが、定義区別しておくことが完全に適切な理由であり、技術仕様言語そのものの。多くの人が信じているように見えることに反して一般がある対極異例模範的な

結論として:

  • ポインタ自体の検査と操作は常に有効であり、多くの場合実りの多いものです。結果の解釈は、意味がある場合とない場合がありますが、ポインタが逆参照されるまで、災難は招かれません。リンクされたアドレスへのアクセスが試行されるまで。

これが真実でなければ、私たちが知っている(そしてそれを愛する)プログラミングは不可能だったでしょう。


3
残念ながら、この答えは本質的に無効です。未定義の動作については何も推論できません。比較はマシンレベルで行う必要はありません。
Antti Haapala

6
Ghii、実際には違います。C11 Annex Jと6.5.8を見ると、比較自体はUBです。逆参照は別の問題です。
paxdiablo

6
いいえ、ポインターが逆参照される前でも、UBは有害である可能性があります。コンパイラーは、UBを使用する関数を1つのNOPに完全に最適化できますが、これにより目に見える動作が明らかに変わります。
nanofarad

2
@ Ghii、Annex J(私が言及したビット)は未定義の動作のリストであるため、それが引数をどのようにサポートするかはわかりません:-) 6.5.8は比較をUBとして明示的に呼び出します。supercatへのコメントのために、あなたはときに起こって何の比較がない印刷あなたはおそらく正しい、それがクラッシュしていないということですので、ポインタを。しかし、それはOPが求めていたものではありません。3.4.3また、注目すべきセクションです。UBは「この国際標準が要件を課さない動作」として定義されてます。
paxdiablo

3
@GhiiVelte、あなたは指摘されているもかかわらず、明らかに間違っていることを述べ続けます。はい、投稿したスニペットはコンパイルする必要がありますが、問題なく実行されるという主張は正しくありません。特に(この場合)は、標準を実際に読むことをお勧めします。C11 6.5.6/9「shall」という単語は要件を示すことに注意してください配列オブジェクトの要素」。
paxdiablo

-5

ポインターは、コンピューターの他のすべてのものと同様に、単なる整数です。あなたはそれらを絶対に比較することができます<し、>そしてクラッシュにプログラムを発生させずに結果を生成します。とは言うものの、標準では、これらの結果が配列比較以外の意味を持つことを保証していません。

スタックに割り当てられた変数の例では、コンパイラーはそれらの変数をレジスターまたはスタックメモリアドレスに自由に割り当てることができます。などの比較<>従って、コンパイラやアーキテクチャ間で一貫できません。しかし、==および!=その限定されず、ポインタの比較平等は、有効かつ便利な操作です。


2
ワードスタックは、C11標準では正確に0回表示されます。また、未定義の動作は、何かが起こる可能性があることを意味ます(プログラムのクラッシュを含む)。
paxdiablo

1
@paxdiablo言った?
Nickpro

2
スタック割り当て変数について言及しました。標準にはスタックはありません。これは単なる実装の詳細です。この回答のより深刻な問題は、クラッシュの可能性なしにポインターを比較できる競合です-それは間違っています。
paxdiablo

1
@nickelpro:オプティマイザと互換性のあるコードをgccとclangで記述したい場合は、多くのばかげたループにジャンプする必要があります。両方のオプティマイザは、標準がそれらを正当化するためにねじれる方法があるときはいつでも(そして時にはない場合でも)、ポインタによって何がアクセスされるかについて推論する機会を積極的に探します。与えられたint x[10],y[10],*p;場合、コードが評価される場合、中間で変更せずにy[0]評価p>(x+5)および書き込み*pp行い、最終的にy[0]再度評価します...
supercat

1
ニッケルプロ、同意しないことに同意しますが、あなたの答えは根本的に間違っています。私はあなたのアプローチを、(ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z')代わりに使用する人々のアプローチに似ています。isalpha()なぜなら、どのような正気な実装でも、それらの文字が不連続になるからです。つまり、移植性を重視する場合は、実装に問題がないことがわかっていても、できるだけ標準に準拠してコーディングする必要があります。「規格の達人」というラベルには感謝しています。私は私のCVに入れてもよい:-)
paxdiablo
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.