間接演算子「*(アスタリスク)」とインクリメント演算子「++」の優先順位

C言語でポインタを使うと間接演算子「*(アスタリスク)」を使うことになりますが、この間接演算子にインクリメント演算子「++」が加わると、どちらを先に演算するのか混乱してしまうときがあります。以下の例について、どういう演算かわかりますか?
p はポインタ変数です。

1. ++*p;
2. *++p;
3. *p++;
4. (*p)++;

1. は、pが指す値自体をインクリメントして参照します。
2. は、pのポインタ位置をインクリメントしてpが指す値を参照をします。
3. は、pが指す値を参照してから、pのポインタ位置をインクリメントします。
4. は、pが指す値を参照してから、pが指す値自体をインクリメントします。

間違いやすいのは、たぶん 3.じゃないかと思います。後置演算の「++」が「*p」にかかるのか「p」にかかるのか、というところです。3. の場合は「p」にかかります。

実際にこれらの演算を行って結果を表示するプログラムを載せておきます。結果を推測してみてください。

コピペするならこちらをお使いください。


#include <stdio.h>

int main( void )
{
	char *p;
	char a[] = { 1, 3, 5, 7 }; 

	p = a;

	/* pの指す値 */
	printf( "0: %d\n", *p );

	/* pの指す値をインクリメント */
	printf( "1: %d\n", ++*p );
	
	/* ポインタをインクリメントしてからpの指す値を参照 */
	printf( "2: %d\n", *++p );

	/* pの指す値を参照後にポインタをインクリメント */
	printf( "3: %d\n", *p++ );
	printf( "4: %d\n", *p );

	/* pの指す値を参照後にpの指す値をインクリメント */
	printf( "5: %d\n", (*p)++ );
	printf( "6: %d\n", *p );

	return 0;
}

結果は以下となります。

$ ./a.out
0: 1
1: 2
2: 3
3: 3
4: 5
5: 5
6: 6

2020/11/8 追記
配列の添字がついたときの演算子の優先順位も試してみました。ほとんど使うことはないだろうし、混乱するので読み飛ばしてもらっても構いません。。以下の違いです。

5. *++argv[0]
6. (*++argv)[0]

やりたいことは以下のプログラムと同じです。


#include <stdio.h>

int main( int argc, char *argv[] )
{
	while( *++argv )
		printf( "%s\n", *argv );

	return 0;
}

実行時の引数を画面に表示(印字)するだけのプログラムです。引数に「apple」と「orange」をつけて実行すると

$ ./a.out apple orange
apple
orange

という結果が画面に表示されます。これをわざわざ配列を使って処理します。*++argv[0] と (*++argv)[0] が登場します。ソースコードを先に載せておきます。


#include <stdio.h>

int main( int argc, char *argv[] )
{
	while( --argc )
	{
		printf( "%c", (*++argv)[0] );

		while( *++argv[0] )
			printf( "%c", *argv[0] );

		printf( "\n" );
	}

	return 0;
}

whileで2重ループにしています。文字列(ここでは文字配列と書いておきます)へのポインタ配列を1つづつ進ませつつ、その中で文字配列を1つづつ印字しています。イメージ図を書いておきます。[ ] は *(アスタリスク)や ++(インクリメント演算子)より結合が強いです。

配列を使わずにポインタで処理する場合も載せておきます。

(*++argv)[0] は **++argv に変更
*++argv[0] は *++*argv に変更

としていますが、ここまでくるとよくわからなくなります・・


#include <stdio.h>

int main( int argc, char *argv[] )
{
	while( --argc )
	{
		printf( "%c", **++argv );

		while( *++*argv )
			printf( "%c", **argv );

		printf( "\n" );
	}

	return 0;
}

実行結果は同じになります。

文字列を指すポインタとchar型配列との違い

C言語では文字列をポインタを使って扱うこともあれば、charの配列として扱う場合もあります。

char *p = “Orange”;
char a[ ] = “Apple”;

上の例では、p は「Orange」という文字列を指すポインタであり、a[ ] は「Apple」を格納している配列です。OrangeもAppleもヌル文字「\0」が終端文字として付加されます。

文字列を扱う方法が違うので以下の差が出てきます。

まず、ポインタの「p」も配列の「a」も文字列の先頭を指し示していますが、「p++」はできても「a++」はできません。a は配列の名前であり配列の名前はその先頭の要素の位置と同義ですが、ポインタの加減算とは異なるものであるため a++ のような使い方は出来ず、コンパイルエラーになります。また a に別のポインタアドレスを代入することも出来ません。「a = p」は間違いです。「p = a」は出来ます。

また、「Apple」という文字列は配列で確保しているため書き換えることができますが「Orange」という文字列の書き換えはできません。「Orange」を書き換えた場合の動きは不定です。僕の環境(ubuntu)で、*pに値を代入(*p = ‘A’)をして実行してみたところコアダンプしてしまいました。

ちなみに、ポインタ「p」は「Orange」の文字列を指し示していますが、ポインタの向き先を「Apple」に変えることは可能です。

これらを確認したプログラムソースを載せておきます。

コピペするなら以下を使ってください。


#include <stdio.h>

int main( void )
{
	char	*p = "Orange";
	char	a[] = "Apple";
	char	*ap;

	printf( "p: %s\n", p );
	printf( "a: %s\n\n", a );

	/* Orangeを指し示すポンタの位置をずらす */
	p++;

	/* Appleを指し示すポンタの位置をずらす。注意: a++ はできません */
	ap = a;
	ap++;

	printf( "p++:  %s\n", p );
	printf( "ap++: %s\n\n", ap );

	/* Orangeの値は書き換えられません( *p に代入はできません)
	   Orangeを指すポインタの向き先を変えることはできます */
	p = a;
	printf( "p: %s\n", p );

	/* Appleの値は書き換えられます */
	*p = 'a';
	*ap = 'P';
	a[4] = 'E';
	printf( "a: %s\n", a );

	return 0;
}

実行結果は以下となります。

$ ./a.out
p: Orange
a: Apple

p++: range
ap++: pple

p: Apple
a: aPplE