質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

87.77%

二次元配列での添字演算子の動きについて教えてください

解決済

回答 5

投稿 編集

  • 評価
  • クリップ 0
  • VIEW 2,972

score 37

お世話になります

添字演算子について
以下教えて頂けますでしょうか

■質問
下記コードでp[2][1]とすればintの6が得られるのですが
この結果を得られるまでの流れを詳細に教えて頂けますか
出来ましたら私の思考過程に突っ込みを入れながら。。。

■自分の思考過程
まず、p[2][1]は(p[2])[1]と考えて
p[2]は何を示すのだろうと考えました

pはint2個分の要素を指すポインタで
aで初期化されている

このため、p[2]はaの先頭から要素2つ
つまりint4つ分進んだ先にあるモノを示すはずなので
a[2]のアドレスこれは&a[2][0]と同じものを指す

ここまででp[2][1]は
(&a[1][0])[1]と同じなのではと推測

ですが、ここから先が進めません

p[1]を求めるにはpの参照先の型がint2つ分という情報が宣言の文にあるので
aの先頭からint4つ分進められたのですが、次はありません

仮に(&a[1][0])が具体的なアドレスを返してきたとしても
そこから[1]をどう解決していいかわからないと思うのです。

こういう動きを具体的に捉えるには
コンパイラとかリンカがどう動いているか追及しないといけないのでしょうかね。。。

■私が理解していること(正しいかどうかは)
・ポインタ変数は「参照先の型」と「アドレス」を保持している
・ポインタを1進めると「参照先の型」のサイズ分「アドレス」が増える
・ptr[i]は*(ptr+i)のシンタックスシュガーである
・ptr[i]はptrが指しているアドレスから
i要素後ろの位置にあるアドレスに格納されているモノを示す
・3次元の配列aにおいてa、a[0]、a[0][0]、&a[0][0][0]は
いずれも評価すると配列の先頭アドレスを得られる

■コード

include <stdio.h>

int main() {
int a[3][2] = { { 1, 2 }, { 3, 4 }, { 5, 6 } };
int (*p)[2] = a;

printf("\n%d", p[2][1]);
printf("\n%d", (p[2])[1]);

return 0;
}

  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 過去に投稿した質問と同じ内容の質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

回答 5

checkベストアンサー

+1

・ポインタ変数は「参照先の型」と「アドレス」を保持している
・ポインタを1進めると「参照先の型」のサイズ分「アドレス」が増える
・ptr[i]は*(ptr+i)のシンタックスシュガーである
・ptr[i]はptrが指しているアドレスからi要素後ろの位置にあるアドレスに格納されているモノを示す

上記はあなたの解釈で正しいです。

・3次元の配列aにおいてa、a[0]、a[0][0]、&a[0][0][0]はいずれも評価すると配列の先頭アドレスを得られる 

これは厳密には少々誤りを含んでいます。int a[1][2][3]のような3次元配列があるとき、それぞれの式の評価結果は 型(type) が異なっています:

  • aは「3次元配列型int[1][2][3]の値」ですが、「2次元配列型int[2][3]を要素とする配列の先頭要素をさすポインタ値」へと変換され、評価結果はint(*)[2][3]型となります。
  • a[0]は「2次元配列型int[2][3]の値」ですが、「1次元配列型int[3]を要素とする配列の先頭要素をさすポインタ値」へと変換され、評価結果はint(*)[3]型となります。
  • a[0][0]は「配列型int[3]の値」ですが、「int型を要素とする配列の先頭要素をさすポインタ値」へと変換され、評価結果はint*型となります。
  • &a[0][0][0]は「要素がint型配列の先頭要素をさすポインタ値」です。この式はint*型です。(最終結果は1つ上のa[0][0]と同じ)

C言語のポインタと配列に関するルールのうち、「"配列型の値"は"配列の先頭要素をさすポインタ値"へと暗黙に変換される」というものがあります。(この分かりにくいルールのせいで、配列とポインタの混同がよく見られます)

簡単に型Tの1次元配列T a[N]を考えたとき、式a自身の型はT[N]という「N個のT型要素からなる配列型」であり、この式の評価結果は「先頭要素a[0]をさすポインタ値」つまり ポインタ値&a[0]int*型 へと暗黙変換されます。

また厳格な仕様解釈ではC言語に「多次元配列型」は存在せず、俗にいう3次元配列は「配列型の配列型の配列型」としてCコンパイラに解釈されています。上記のルールを再帰的に適用していけば、当初の疑問に答えられるかと思います。

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2016/06/16 22:48

    yohhoyさんご回答ありがとうございます
    返答が遅くなってしまい申し訳ありません

    ヒントがたくさんありそうなので
    熟読していますが、まだ理解できません><
    何が理解できていないのか理解できていないかもしれません

    もう少し拝読させていただきます。

    キャンセル

  • 2016/06/16 23:59

    yohhoyさん

    数パターン考えてみましたところ
    「"配列型の値"は"配列の先頭要素をさすポインタ値"へと暗黙に変換される」というルールは理解できたように思います

    あとは、「モノ」と表現しているところが抽象的なので
    もう少し具体的に捉えたいと思うので、考えてみます。

    キャンセル

  • 2016/06/17 19:47 編集

    配列型からポインタ型への暗黙変換の部分を詳細化してみました。

    『「モノ」と表現しているところが抽象的』とのことですが、「モノ」=「ある型の値」という解釈が最も正確だと思います。

    C言語の仕様上も、まさにこのような「モノ」を「オブジェクト(object)」と呼びます。ただし、オブジェクト指向で登場するようなオブジェクトの意味ではなく、例えばint型の値100もオブジェクト(=モノ)です。同様に"3要素のint型からなる配列型(int[3])の値"もまた、一つのオブジェクト(=モノ)となります。

    キャンセル

  • 2016/06/19 17:40

    yohhoyさん ご返答ありがとうございます。

    返事が遅くなってしまい申し訳ありません

    モノとはつまりメモリ領域の事なのかなと思いました
    でも、突き詰めていけばそれはビットであり電子であり、、、
    そこまで考えるのはナンセンスなので
    メモリ領域とかオブジェクトと理解することにしました。

    キャンセル

+1

まず、C言語でのa[i]は、*(a + i)と全く同じです(そんなことをしても読みにくいコードで嫌がらせする程度の用途しかなさそうですが、i[a]と書いてもまったく同じ動作をします)。また、添字演算子は左結合なので、a[b][c](a[b])[c]と解釈するということで間違いありません。

ということで、「int[2]を指すポインタ」であるpについて、p[2]とすると、*(p + 2)、つまり、intが2 * 2 = 4つ分だけ進んだ場所を指して、ポインタを1つ外してint[2]型の値ということになります。そして、(宣言そのものやsizeofのオペランドとなる場合は別として)、配列型の値が式中に現れた時には要素へのポインタと解されるので、p[2][1]は、「先ほどのp[2]からint1つ分だけ進めた場所にある、int型の値」を指します。

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2016/06/16 21:49

    maisumakunさん ご回答ありがとうございます

    仰っている「配列型の値が式中に現れた時には要素へのポインタと解される」という部分が核心をついているような気がするのですが

    正直なところ、いまだに理解できません
    何が理解できていないかもわかっていないような気がしてきました。

    もう少しご回答拝読させていただきます><

    キャンセル

+1

実際のアドレスがどうなっているか見てみるとイメージが湧くかもしれません。
N次元配列でメモリを確保した場合は、連続したメモリ領域を確保できます。

int main() {
  int a[3][2] = { { 1, 2 }, { 3, 4 }, { 5, 6 } };
  int (*p)[2] = a;

  //printf("\n%d", p[2][1]); //イ
  //printf("\n%d", (p[2])[1]); //ロ

  for (int i = 0; i < 3; i++) {
    for (int j = 0; j < 2; j++) {
      printf("%p: %d\n", &a[i][j], a[i][j]);
    }
  }
  return 0;
}
$ gcc -std=c99 test.c
$ ./a.out
0x7ffe63903730: 1
0x7ffe63903734: 2
0x7ffe63903738: 3
0x7ffe6390373c: 4
0x7ffe63903740: 5
0x7ffe63903744: 6

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2016/06/17 00:03

    moonphaseさん ご回答ありがとうございます

    アセンブラは自分には恐らくわからないので(判りたいですが)
    手出しできそうにありません><

    折角お返事頂いたのにすみません。

    キャンセル

  • 2016/06/19 17:42

    アセンブラについて少し学んでみました、
    スタックに実引数を積んでいる様子とか
    BSS領域の取られ方とか見えてきて、大変興味が沸きました。

    C言語の勉強が一通り終わったら、勉強してみます。
    きっかけを与えて下さり、ありがとうございます!

    キャンセル

  • 2016/06/19 18:24

    多次元配列の実装は処理系依存みたいですね.

    http://ja.stackoverflow.com/questions/5022/%EF%BC%92%E6%AC%A1%E5%85%83%E9%85%8D%E5%88%97%E3%81%AF%E4%B8%8D%E9%80%A3%E7%B6%9A%E3%81%8B

    但し,連続的にしたほうが実装もラクで動作上も有利なため,そうしないコンパイラは現実的には存在しない,と見るべきなんでしょうか.

    (私も仕様に関して論争起こすの嫌いなので,十中八九それならそれでいいじゃん,って感じの思考回路ではありますが)

    キャンセル

0

こんにちは。

ここまででp[1][2]は(&a[1][0])[2]と同じなのではと推測

その通りです。そして、a[1][0]がint型ですから、&a[1][0]はint型へのポインタですね。

・ptr[i]はptrが指しているアドレスからi要素後ろの位置にあるアドレスに格納されているモノを示す 

このルールに従い、(&a[1][0])[2]はint型へのポインタ(&a[1][0])が指しているアドレスから、2つの要素(int型)後ろの位置にあるアドレスに格納されているモノを示します。

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2016/06/17 00:07

    Chironianさん ご返答ありがとうございます

    お蔭さまで少し見えてきた気がします
    でもまだ抽象的でふわっとしているので
    「モノ」とか表現してしまっている部分を具体的に数値等で考えてみたいと思います

    int a[4] = {1,2,3,4};のとき
    式aを評価すれば「要素1へのアドレスと型はintである」と言葉にできるのですが

    これが多次元となるとなんだか。。。

    もう少し悩んでみます。

    キャンセル

  • 2016/06/17 00:22

    C/C++は、下記の点で混乱しやすいです。
      型と変数名を分離して定義できないものがある(配列と関数のシグニチャ)
      配列の添字定義は意味的には逆順になっている
    その混乱に巻き込まれているのかも知れません。

    C/C++の文法的には間違いですが、
      int a[4];を、int[4] a;と表現し、
      int a[3][2];を、int[2][3] a;と表現して
    みると分かりやすいかも知れません。後者は「int型2個の配列」が3個並んでいるのです。

    キャンセル

  • 2016/06/19 17:54

    Chironian さん ご返答ありがとうございます

    仰るように宣言順序がややこしいと思います。
    最近やっと関数へのポインタ宣言等読めるようになってきましたが、
    複雑なものになるとまだまだ大変です。

    今回の件はおかげ様でイメージできるようになりましたので、
    もう少し学び、落ち着いたらコンパイラの動きを学習してみようと思います。
    ありがとうございました。

    キャンセル

0

p[2][1]を(p[2])[1]として二段階で考える

まず、p[2]を考える

pは「配列先頭のアドレス」と「int2つ分」という情報を持つ。
p[2]は「配列先頭のアドレス」から「int2つ分を2つ」進めたアドレスから「int2つ分」のモノを指す
これは、実質は配列({5,6})である

つまり、p[2]は配列{5,6}である

式中に配列が現れた場合、その評価値は内包する先頭要素のアドレスと型となる、
つまり、p[2]は「5のアドレス」と「5の型(int基本型)」を示す

結果的に、(p[2])[1]は
(「5のアドレス」と「int基本型)」)[1]となるので
{5,6}の6を得られる

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 87.77%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

関連した質問

同じタグがついた質問を見る