web-dev-qa-db-ja.com

C標準では、任意の値をポインターに割り当ててインクリメントすることを許可していますか?

このコードの動作は適切に定義されていますか?

#include <stdio.h>
#include <stdint.h>

int main(void)
{
    void *ptr = (char *)0x01;
    size_t val;

    ptr = (char *)ptr + 1;
    val = (size_t)(uintptr_t)ptr;

    printf("%zu\n", val);
    return 0;
}

つまり、ポインタに何らかの固定番号を割り当てて、それが何らかのランダムなアドレスを指している場合でも、それをインクリメントすることはできますか? (あなたはそれを間接参照できないことを知っています)

52
David Ranieri

割り当て:

void *ptr = (char *)0x01;

整数をポインタに変換しているため、実装定義の動作です。これは、ポインターに関する C標準 のセクション6.3.2.3で詳しく説明されています。

5整数は任意のポインター型に変換できます。前に指定した場合を除き、結果は実装定義であり、正しく位置合わせされていない可能性があり、参照された型のエンティティを指していない可能性があり、トラップ表現である可能性があります。

後続のポインター演算に関して:

ptr = (char *)ptr + 1;

これはいくつかのことに依存しています。

まず、ptrmayの現在の値は、上記の6.3.2.3によるトラップ表現です。そうである場合、動作はundefinedです。

次は、0x1が有効なオブジェクトを指しているかどうかの質問です。ポインターと整数の追加は、ポインターオペランドと結果の両方が配列オブジェクトの要素(サイズ1の配列としてカウントされる)または配列オブジェクトの1つの要素を指す場合にのみ有効です。これについては、セクション6.5.6で詳しく説明します。

7これらの演算子の目的上、配列の要素ではないオブジェクトへのポインタは、オブジェクトの型を持つ長さ1の配列の最初の要素へのポインタと同じように動作します要素タイプとして

8整数型の式がポインターに加算またはポインターから減算される場合、結果にはポインターオペランドの型が含まれます。ポインターオペランドが配列オブジェクトの要素を指し、配列が十分に大きい場合、結果は、結果の配列要素と元の配列要素の添え字の差が整数式と等しくなるように、元の要素からオフセットした要素を指します。つまり、式Pが配列オブジェクトのi番目の要素を指している場合、式(P)+ N(同様に、N +(P))および(P)-N(ここでNの値はn)iをそれぞれ指す配列オブジェクトの+ n番目とi-n番目の要素(存在する場合)。さらに、式Pが配列オブジェクトの最後の要素を指す場合、式(P)+1は配列の最後の要素の1つ後を指しますオブジェクト、および式Qが配列オブジェクトの最後の要素の1つ先を指す場合、式( Q)-1は、配列オブジェクトの最後の要素を指します。 ポインタオペランドと結果の両方が同じ配列オブジェクトの要素、または配列オブジェクトの最後の要素の1つを指す場合、評価はオーバーフローを生成しません。それ以外の場合、動作は未定義です。結果が配列オブジェクトの最後の要素の1つを指す場合、評価される単項*演算子のオペランドとして使用されません。

ホストされた実装では、値0x1はほぼ確実にnot有効なオブジェクトを指します。その場合、追加はundefined。ただし、組み込み実装で​​は、特定の値へのポインターの設定をサポートできます。その場合、0x1が実際に有効なオブジェクトを指している可能性があります。そうである場合、動作はwell defined、そうでない場合はundefinedです。

68
dbush

いいえ、このプログラムの動作は未定義です。プログラム内で未定義の構造に到達すると、将来の動作は未定義になります。逆説的に、過去の行動も未定義です。

charがトラップ表現を持つことができるという事実に一部起因して、void *ptr = (char*)0x01;の結果は実装定義です。

ただし、ステートメントptr = (char *)ptr + 1;内の後続のポインター演算の動作はndefinedです。これは、ポインター演算が、配列の末尾を過ぎた配列を含む配列内でのみ有効だからです。このため、オブジェクトは長さ1の配列です。

18
Bathsheba

未定義の動作です。

N1570から(強調を追加):

整数は、任意のポインター型に変換できます。前に指定した場合を除き、結果は実装定義であり、正しく位置合わせされていない可能性があり、参照された型のエンティティを指していない可能性があり、トラップ表現の可能性があります

値がトラップ表現の場合、それを読み取ることは未定義の動作です。

特定のオブジェクト表現は、オブジェクトタイプの値を表す必要はありません。オブジェクトの保存された値にそのような表現があり、文字型を持たない左辺値式によって読み取られる場合、動作は未定義ですこのような表現がすべてを変更する副作用によって生成される場合または、文字型を持たない左辺値式によるオブジェクトの任意の部分動作は未定義です。)このような表現は、トラップ表現と呼ばれます。

そして

識別子は、オブジェクト(この場合はそれは左辺値)または関数(この場合は関数指定子)を指定するものとして宣言されていれば、プライマリ式です。

したがって、(char*)0x01または(void*)(char*)0x01がトラップ表現である実装では、void *ptr = (char *)0x01;行は既に未定義の動作である可能性があります。左側は、文字型を持たない左辺値式であり、トラップ表現を読み取ります。

一部のハードウェアでは、無効なポインタをマシンレジスタにロードするとプログラムがクラッシュする可能性があるため、これは標準化委員会による強制的な動きでした。

8
Davislor

はい、コードは実装定義として明確に定義されています。未定義ではありません。 ISO/IEC 9899:2011 [6.3.2.3]/5および注67を参照してください。

C言語は、もともとシステムプログラミング言語として作成されました。システムプログラミングでは、メモリにマップされたハードウェアを操作する必要があり、ハードコードされたアドレスをポインターに詰め、時にはそれらのポインターをインクリメントし、結果のアドレスとの間でデータを読み書きしなければなりません。そのために、ポインターに整数を割り当て、算術を使用してそのポインターを操作することは、言語によって適切に定義されています。実装定義にすることで、言語が許可することは、あらゆる種類のことが起こりうることです:古典的な停止とキャッチファイアから、奇数アドレスを逆参照しようとするときにバスエラーを上げることです。

未定義の動作と実装定義の動作の違いは、基本的に未定義の動作は「それをしないで、何が起こるかわからない」という意味です。何が起こるかを知ってください。」

8
Stephen M. Webb

標準では、特定の整数値、またはNullポインター定数以外の可能な整数値に対しても、実装が整数からポインターへの変換を意味のある方法で処理することを要求していません。そのような変換について保証する唯一のことは、そのような変換の結果を適切なポインター型のオブジェクトに直接保存し、そのオブジェクトのバイトを調べること以外は何もしないプログラムは、最悪の場合、指定されていない値を見るということです。整数をポインターに変換する動作は実装定義ですが、any実装(実際にそのような変換で何が行われようとも!)の一部(またはすべて)を指定することは禁止されません。指定されていない値を持つ表現のバイト、および一部(またはすべて)の整数値がトラップ表現を生成するかのように動作することを指定します。

標準が整数からポインターへの変換について何も述べていない唯一の理由は、次のとおりです。

  1. いくつかの実装では、構造は意味があり、それらの実装の一部のプログラムはそれを必要とします。

  2. 標準の作成者は、一部の実装で使用される構造が他の実装の制約違反を表すという考えを好まなかった。

  3. 規格が構造を記述するのは奇妙でしたが、すべての場合に未定義の動作があることを指定していました。

個人的には、コンパイラが意味のないコードを受け入れることを要求するのではなく、有用な状況を定義しない場合、標準は実装が整数からポインタへの変換を制約違反として扱うことを許可すべきだったと思いますが、そうではありませんでした当時の哲学。

ポインタから整数への変換から受け取ったintptr_tまたはuintptr_tの値以外の整数からポインタへの変換を伴う操作は、未定義の動作を呼び出すと単純に言うのが最も簡単だと思いますが、それは意図された品質の実装では一般的であることに注意してください「環境に特有の文書化された方法で」未定義の動作を処理する低レベルのプログラミング規格は、実装がその方法でUBを呼び出すプログラムをいつ処理するかを指定せず、代わりに実装の品質の問題として扱います。

実装が、整数からポインターへの変換が、次の動作を定義する方法で動作することを指定している場合

char *p = (char*)1;
p++;

「char p =(char)2;」と同等の場合、実装はそのように動作することが期待されます。一方、実装では、次のような方法で整数からポインターへの変換の動作を定義できます。

char *p = (char*)1;
char *q = p;  // Not doing any arithmetic here--just a simple assignment

鼻の悪魔を解放します。ほとんどのプラットフォームでは、整数からポインターへの変換によって生成されるポインターの算術演算が奇妙に動作するコンパイラーは、低レベルのプログラミングに適した高品質の実装とは見なされません。そのため、他の種類の実装を対象としないプログラマーは、標準では要求されていなくても、コードが適したコンパイラーでこのような構造が有効に動作することを期待できます。

3
supercat