web-dev-qa-db-ja.com

間接参照されたポインターのポストインクリメント?

Cでのポインターの動作を理解しようとすると、次のことに少し驚きました(以下のサンプルコード)。

#include <stdio.h>

void add_one_v1(int *our_var_ptr)
{
    *our_var_ptr = *our_var_ptr +1;
}

void add_one_v2(int *our_var_ptr)
{
    *our_var_ptr++;
}

int main()
{
    int testvar;

    testvar = 63;
    add_one_v1(&(testvar));         /* Try first version of the function */
    printf("%d\n", testvar);        /* Prints out 64                     */
    printf("@ %p\n\n", &(testvar));

    testvar = 63;
    add_one_v2(&(testvar));         /* Try first version of the function */
    printf("%d\n", testvar);        /* Prints 63 ?                       */
    printf("@ %p\n", &(testvar));   /* Address remains identical         */
}

出力:

64
@ 0xbf84c6b0

63
@ 0xbf84c6b0

2番目の関数(*our_var_ptr++)のadd_one_v2ステートメントは、明らかに*our_var_ptr = *our_var_ptr +1と同じではないため、正確に何をしますか?

51
ChristopheD

演算子の優先規則と_++_が後置演算子であるという事実により、add_one_v2()はポインターを逆参照しますが、_++_は実際に適用されますポインター自体に。ただし、Cは常に値渡しを使用することに注意してください。add_one_v2()はポインターのlocal copyをインクリメントします。

テストとして、add_one_v2()を次のコードに置き換えて、出力がどのように影響を受けるかを確認します。

_void add_one_v2(int *our_var_ptr)
{
    (*our_var_ptr)++;  // Now stores 64
}

void add_one_v2(int *our_var_ptr)
{
    *(our_var_ptr++);  // Increments the pointer, but this is a local
                       // copy of the pointer, so it doesn't do anything.
}
_
40
hbw

これは、CとC++を非常に楽しいものにする小さな落とし穴の1つです。脳を曲げたいなら、これを考えてください:

while (*dst++ = *src++) ;

文字列のコピーです。ポインターは、値がゼロの文字がコピーされるまで増分され続けます。このトリックが機能する理由がわかれば、++がポインターで再び機能することを決して忘れないでしょう。

追伸括弧で演算子の順序をいつでもオーバーライドできます。以下は、ポインター自体ではなく、ポイントされた値を増分します。

(*our_var_ptr)++;
61
Mark Ransom

OK、

*our_var_ptr++;

それはこのように動作します:

  1. 最初に逆参照が行われ、our_var_ptr(63を含む)で示されるメモリの場所が提供されます。
  2. その後、式が評価され、63の結果はまだ63です。
  3. 結果は破棄されます(あなたはそれで何もしていません)。
  4. our_var_ptrは、評価後にインクリメントされます。ポインタが指しているものではなく、ポインタが指している場所が変わっています。

これは事実上、これを行うことと同じです。

*our_var_ptr;
our_var_ptr = our_var_ptr + 1; 

理にかなっていますか? Mark Ransomの答えは、彼が実際に結果を使用することを除いて、これの良い例です。

33
BIBD

ここで多くの混乱があるので、ここで何が起こるかを明確にするために修正されたテストプログラムがあります(または少なくとも明確にer):

#include <stdio.h>

void add_one_v1(int *p){
  printf("v1: pre:   p = %p\n",p);
  printf("v1: pre:  *p = %d\n",*p);
    *p = *p + 1;
  printf("v1: post:  p = %p\n",p);
  printf("v1: post: *p = %d\n",*p);
}

void add_one_v2(int *p)
{
  printf("v2: pre:   p = %p\n",p);
  printf("v2: pre:  *p = %d\n",*p);
    int q = *p++;
  printf("v2: post:   p = %p\n",p);
  printf("v2: post:  *p = %d\n",*p);
  printf("v2: post:   q = %d\n",q);
}

int main()
{
  int ary[2] = {63, -63};
  int *ptr = ary;

    add_one_v1(ptr);         
    printf("@ %p\n", ptr);
    printf("%d\n", *(ptr));  
    printf("%d\n\n", *(ptr+1)); 

    add_one_v2(ptr);
    printf("@ %p\n", ptr);
    printf("%d\n", *ptr);
    printf("%d\n", *(ptr+1)); 
}

結果の出力:

v1: pre:   p = 0xbfffecb4
v1: pre:  *p = 63
v1: post:  p = 0xbfffecb4
v1: post: *p = 64
@ 0xbfffecb4
64
-63

v2: pre:   p = 0xbfffecb4
v2: pre:  *p = 64
v2: post:  p = 0xbfffecb8
v2: post: *p = -63
v2: post:  q = 64

@ 0xbfffecb4
64
-63

注意すべき4つのこと:

  1. ポインターのローカルコピーへの変更は、not呼び出しポインターに反映されます。
  2. ローカルポインターのターゲットへの変更は、呼び出しポインターのターゲットに影響します(少なくともターゲットポインターが更新されるまで)
  3. add_one_v2でポイントされている値はnotインクリメントされ、どちらも次の値ではありませんが、ポインターは
  4. add_one_v2のポインタの増分が発生します参照解除

どうして?

  • ++*よりも緊密にバインドするため(逆参照または乗算として)、add_one_v2の増分がポインターに適用され、ポインターが指すものではありません。
  • post増分が発生しますafter項の評価。したがって、逆参照は配列の最初の値を取得します(要素0)。
7
dmckee

他の人が指摘したように、演算子の優先順位により、v2関数の式は*(our_var_ptr++)として認識されます。

ただし、これはポストインクリメント演算子であるため、ポインターをインクリメントしてから逆参照すると言うのはあまり真実ではありません。これが本当なら、次のメモリ位置に値を返すため、出力として63を取得するとは思わないでしょう。実際、操作の論理的な順序は次のとおりです。

  1. ポインターの現在の値を保存します
  2. ポインターをインクリメントする
  3. 手順1で保存されたポインター値の逆参照

Htwが説明したように、ポインターは値によって関数に渡されるため、ポインターの値の変更は表示されません。

6
Dave Costa

括弧を使用して操作の順序を指定しない場合、プレフィックスとポストフィックスの両方の増分が参照と間接参照よりも優先されます。ただし、プレフィックス増分とポストフィックス増分は異なる操作です。 ++ xでは、演算子は変数への参照を取得し、それに変数を追加して、値で返します。 x ++では、演算子は変数をインクリメントしますが、古い値を返します。これらは次のような動作をします(クラス内でメソッドとして宣言されていると想像してください):

//prefix increment (++x)
auto operator++()
{
    (*this) = (*this) + 1;
    return (*this);
}

//postfix increment (x++)
auto operator++(int) //unfortunatelly, the "int" is how they differentiate
{
    auto temp = (*this);
    (*this) = (*this) + 1; //same as ++(*this);
    return temp;
}

(後置インクリメントに関与するコピーがあるため、効率が低下することに注意してください。これが、ほとんどのコンパイラーがi ++ではなく++ iを優先する理由です。最近あなたのために自動的に。)

ご覧のとおり、接尾辞の増分が最初に処理されますが、その振る舞いにより、ポインタの以前の値を逆参照します。

以下に例を示します。

char * x = {'a', 'c'};
char   y = *x++; //same as *(x++);
char   z = *x;

2行目では、ポインターxは逆参照の前に増分されますが、逆参照はxの古い値(後置増分によって返されるaddress)で発生します。したがって、yは「a」で初期化され、zは「c」で初期化されます。しかし、次のようにすると:

char * x = {'a', 'c'};
char   y = (*x)++;
char   z = *x;

ここで、xは逆参照され、valueが指す( 'a')は( 'b'に)インクリメントされます。後置インクリメントは古い値を返すため、yは引き続き「a」で初期化されます。また、ポインターは変更されなかったため、zは新しい値「b」で初期化されます。

プレフィックスのケースを確認しましょう:

char * x = {'a', 'c'};
char   y = *++x; //same as *(++x)
char   z = *x;

ここでは、xの増分値(接頭辞増分演算子によって直ちに返される)で逆参照が発生するため、yとzの両方が 'c'で初期化されます。別の動作を取得するには、演算子の順序を変更できます。

char * x = {'a', 'c'};
char   y = ++*x; //same as ++(*x)
char   z = *x;

ここでは、xの内容を最初にインクリメントし、ポインターの値が変更されないようにします。したがって、yとzには「b」が割り当てられます。 strcpy関数(他の回答で言及)では、増分も最初に行われます:

char * strcpy(char * dst, char * src)
{
    char * aux = dst;
    while(*dst++ = *src++);
    return aux;
}

各反復で、src ++が最初に処理され、後置インクリメントであるため、srcの古い値を返します。次に、srcの古い値(ポインター)が逆参照され、代入演算子の左側にあるものに代入されます。次に、dstがインクリメントされ、その古い値が逆参照されて左辺値になり、古いsrc値を受け取ります。これが、dst [0] = src [0]、dst [1] = src [1]など、* dstに0が割り当てられるまでループを中断する理由です。

補遺:

この回答のすべてのコードは、C言語でテストされています。 C++では、おそらくポインターをリスト初期化することはできません。したがって、C++で例をテストする場合は、最初に配列を初期化してからポインターに分解する必要があります。

char w[] = {'a', 'c'};
char * x = w;
char   y = *x++; //or the other cases
char   z = *x;
3
Jango

our_var_ptrはメモリへのポインタです。すなわち、データが保存されるメモリセルです。 (nこの場合、intのバイナリ形式で4バイト)。

* our_var_ptrは間接参照されたポインターです。ポインターが「指す」位置に移動します。

++は値をインクリメントします。

そう。 *our_var_ptr = *our_var_ptr+1はポインターを逆参照し、その位置の値に1を追加します。

演算子の優先順位を追加します-(*our_var_ptr) = (*our_var_ptr)+1そして、逆参照が最初に発生することがわかるので、値を取得して値を増やします。

他の例では、++演算子は*よりも優先順位が低いため、渡されたポインターを取得し、ポインターを追加して(今はゴミを指すようにして)から戻ります。 (値は常にCの値で渡されることを忘れないでください。関数が元のtestvarポインターを返す場合、ポインターは同じままで、関数内のポインターのみを変更します)。

私のアドバイスでは、デリファレンス(または他の何か)を使用する場合は、括弧を使用して決定を明確にします。優先順位のルールを覚えようとしないでください。1日のうちにわずかに異なる言語しか使用せず、混乱してしまいます。または、古い優先順位の高いものを忘れてしまいます(*や->の場合と同様)。

3
gbjbaanb
    uint32_t* test;
    test = &__STACK_TOP;


    for (i = 0; i < 10; i++) {
        *test++ = 0x5A5A5A5A;
    }

    //same as above

    for (i = 0; i < 10; i++) {
        *test = 0x5A5A5A5A;
        test++;
    }

Testはポインターであるため、test ++(これは間接参照されません)はポインターをインクリメントします(ポインターの指す(宛先)アドレスであるtestの値をインクリメントします)。宛先はuint32_t型であるため、test ++は4バイトずつ増加し、宛先がこの型の配列の場合、testは次の要素を指すようになります。これらの種類の操作を行う場合、必要なメモリオフセットを取得するために、最初にポインターをキャストする必要がある場合があります。

        ((unsigned char*) test)++;

これにより、アドレスが1バイトだけ増加します;)

1
pgibbons

「++」演算子は「*」演算子よりも優先順位が高いため、ポインタアドレスは逆参照される前にインクリメントされます。

ただし、「+」演算子の優先順位は「*」よりも低くなります。

1
Martin Cote

これを少し異なる角度から答えようとします...ステップ1演算子とオペランドを見てみましょう。この場合はオペランドであり、2つの演算子があります。インクリメント用。優先度の高いステップ2 ++の優先度が高い*ステップ3 ++の場合、右側にあるのは[〜#〜] post [〜#〜] Increment 、コンパイラはインクリメントを実行するために「メンタルノート」を取ります[〜#〜] after [〜#〜]それは他のすべての演算子で行われます... * ++ pであったかどうかに注意してくださいそれは前にそれを行うので、この場合、プロセッサのレジスタを2つ取るのと同じです.1つは逆参照された* pの値を保持し、もう1つはインクリメントされたp ++の値を保持します、この場合の理由POST activity ...があります。これはこの場合、トリッキーであり、矛盾のように見えます。++が*より優先されることを期待します。これは、POSTが、他のすべてのオペランドの後、次の ';'トークンの前にのみ適用されることを意味するということだけです...

1
G. Simsic

K&R、105ページから:「* t ++の値は、tがインクリメントされる前にtがポイントした文字です」。

0
ItM

写真は約1000語の価値があります(付与または100万語程度)...そしてシンボルは写真になり得ます(逆も同様です)。

だから、tl;dr (最適化されたデータ消費)、それでも「(ほとんど)ロスレス」エンコーディングが必要です。ベクター画像/画像/イラスト/デモが最重要です。

つまり、最後の2つのステートメントを無視して、以下を参照してください。

Valid forms:


* a ++ ≣*(a ++)
     ≣(a ++)[0]≣a ++ [0]
     ≣0 [a ++] //これをあえて使用しないでください(「教育目的のみ」)
     //これらは同等の(副作用)効果を生み出します。
       val = * a、++ a、val
       ≡ptr = a、++ a、* ptr
       ≡*(ptr = a、++ a、ptr)

* ++ a ≣*(++ a)
     ≣*(a + = 1)≣*(a = a + 1)
     ++(++ a)[0]≣(a + = 1)[0]≣(a = a + 1)[0] //()は 必要
                [0 [++ a] // 0 [a + = 1]など...「教育目的のみ」
     //これらは同等の(副)効果を生み出します:
       ++ ++ a、* a
       ≡a + = 1、* a
       ≡a = a + 1、* a

++ * a ++ ++(* a)
     ≣* a + = 1
     ≣* a = * a + 1
     ++ ++ a [0]≣++(a [0])
              ++ ++ 0 [a] //そのまま

(* a)++  //これはポインタを返さないことに注意してください。
        //「a」が指す場所は変更されません
        //(つまり、「a」の「値」は変更されません)
        val = * a、++ * a、val

Notes

/ *間接/遅延演算子は ̲ターゲット識別子を譲ります:* /
 a ++ *; 
 a * ++; 
 ++ a *; 

0
YenForYang