ポインターを逆参照せずに範囲外に保つことは安全ですか?[複製]

Jan 10 2021

Cでは、さらに演算を行うためにポインターを(逆参照せずに)範囲外に保つのは安全ですか?

void f(int *array)
{
    int *i = array - 1;  // OOB

    while(...) {
        ++i;
        ...
    }
}

void g(int *array, int *end /* past-the-end pointer: OOB */)
{
    while(array != end) {
        ...
        ++array;
    }
}

アドレスがメモリの最初または最後の場合、いくつかの極端なケースを想像します...

回答

9 MikeCAT Jan 10 2021 at 07:31

最後の要素を超えて1つの要素にポインタを移動することは許可されていますが、最初の要素の前にさらに移動したり移動したりすることは許可されていません。

N1570からの引用6.5.6加法演算子(ポイント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つ過ぎを指している場合、評価される単項*演算子のオペランドとして使用されないものとします。

3 dbush Jan 10 2021 at 07:32

ポインタは、配列の最後の要素を過ぎた1つの要素を指す場合があり、ポインタ演算は、そのポインタと配列の要素へのポインタの間で実行される場合があります。

このようなポインターは逆参照できませんが、ポインター演算で使用できます。たとえば、以下が有効です。

char arr[10];
char *p1, *p2;
p1 = arr + 10;
p2 = arr + 5;
int diff = p1 - p2;
printf("diff=%d\n", diff);   // prints 5

ポインタは最初の要素の前を指すことはできません

これは、C標準のセクション6.5.6p8で詳しく説明されています。

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

配列の終わりを過ぎた1つの要素を指すようにポインターを作成できることを示す太字の部分であり、配列の開始前の任意の点を指すことを許可するものは何もないことに注意してください。

klutt Jan 10 2021 at 08:19

他の人が指摘しているように、あなたは過去を指すことが許されてます。ただし、最初の要素のに1つの要素を指すことは許可されていないことに注意してください。したがって、配列を逆方向にトラバースするアルゴリズムを作成する場合は注意が必要な場合があります。このスニペットは無効であるため:

void foo(int *arr, int *end) {
    while(end-- != arr) { // Ouch, bad idea...
        // Code
    }
    // Here, end has the value arr[-1]
}

これendは、と同じ要素を指す場合arr、条件はfalseになりますが、その後endもう一度デクリメントされ、配列のの1つの要素を指すため、未定義の動作が呼び出されるためです。

それとは別に、コードは正常に機能することに注意してください。バグを修正するには、代わりにこれを行うことができます。

void foo(int *arr, int *end) {
    while(end != arr) { 
        end--; // Move end-- to inside the loop, in the very beginning

        // Code
    }

    // And here, end is equal to arr, which is perfectly fine
}

ループ内のコードは以前とまったく同じように機能します。唯一の違いはend、前回デクリメントされないことです。