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

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

新規登録して質問してみよう
ただいま回答率
85.50%
C

C言語は、1972年にAT&Tベル研究所の、デニス・リッチーが主体となって作成したプログラミング言語です。 B言語の後継言語として開発されたことからC言語と命名。そのため、表記法などはB言語やALGOLに近いとされています。 Cの拡張版であるC++言語とともに、現在世界中でもっとも普及されているプログラミング言語です。

ポインタ

ポインタはアドレスを用いてメモリに格納された値を"参照する"変数です。

Q&A

解決済

5回答

4270閲覧

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

777

総合スコア34

C

C言語は、1972年にAT&Tベル研究所の、デニス・リッチーが主体となって作成したプログラミング言語です。 B言語の後継言語として開発されたことからC言語と命名。そのため、表記法などはB言語やALGOLに近いとされています。 Cの拡張版であるC++言語とともに、現在世界中でもっとも普及されているプログラミング言語です。

ポインタ

ポインタはアドレスを用いてメモリに格納された値を"参照する"変数です。

1グッド

0クリップ

投稿2016/06/16 11:10

編集2016/06/16 11:46

お世話になります

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

■質問
下記コードで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;

}

maisumakun👍を押しています

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

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

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

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答5

0

ベストアンサー

・ポインタ変数は「参照先の型」と「アドレス」を保持している
・ポインタを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 12:31

編集2016/06/17 10:41
yohhoy

総合スコア6189

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

777

2016/06/16 13:48

yohhoyさんご回答ありがとうございます 返答が遅くなってしまい申し訳ありません ヒントがたくさんありそうなので 熟読していますが、まだ理解できません>< 何が理解できていないのか理解できていないかもしれません もう少し拝読させていただきます。
777

2016/06/16 14:59

yohhoyさん 数パターン考えてみましたところ 「"配列型の値"は"配列の先頭要素をさすポインタ値"へと暗黙に変換される」というルールは理解できたように思います あとは、「モノ」と表現しているところが抽象的なので もう少し具体的に捉えたいと思うので、考えてみます。
yohhoy

2016/06/17 10:48 編集

配列型からポインタ型への暗黙変換の部分を詳細化してみました。 『「モノ」と表現しているところが抽象的』とのことですが、「モノ」=「ある型の値」という解釈が最も正確だと思います。 C言語の仕様上も、まさにこのような「モノ」を「オブジェクト(object)」と呼びます。ただし、オブジェクト指向で登場するようなオブジェクトの意味ではなく、例えばint型の値100もオブジェクト(=モノ)です。同様に"3要素のint型からなる配列型(int[3])の値"もまた、一つのオブジェクト(=モノ)となります。
777

2016/06/19 08:40

yohhoyさん ご返答ありがとうございます。 返事が遅くなってしまい申し訳ありません モノとはつまりメモリ領域の事なのかなと思いました でも、突き詰めていけばそれはビットであり電子であり、、、 そこまで考えるのはナンセンスなので メモリ領域とかオブジェクトと理解することにしました。
guest

0

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

c

1int main() { 2 int a[3][2] = { { 1, 2 }, { 3, 4 }, { 5, 6 } }; 3 int (*p)[2] = a; 4 5 //printf("\n%d", p[2][1]); //イ 6 //printf("\n%d", (p[2])[1]); //ロ 7 8 for (int i = 0; i < 3; i++) { 9 for (int j = 0; j < 2; j++) { 10 printf("%p: %d\n", &a[i][j], a[i][j]); 11 } 12 } 13 return 0; 14}

bash

1$ gcc -std=c99 test.c 2$ ./a.out 30x7ffe63903730: 1 40x7ffe63903734: 2 50x7ffe63903738: 3 60x7ffe6390373c: 4 70x7ffe63903740: 5 80x7ffe63903744: 6

投稿2016/06/16 12:05

moonphase

総合スコア6621

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

777

2016/06/16 12:55

moonphaseさん ご回答ありがとうございます 実行してみました 0060FEF0: 1 0060FEF4: 2 0060FEF8: 3 0060FEFC: 4 0060FF00: 5 0060FF04: 6 拝見する限りアドレスが連続して取られるという事や ポインタの進み方は理解できるのですが(1段階目までは) 2段階目のポインタを進めるための型がどう扱われているのかわからない状態です、 もし何かヒントがございましたら頂けませんでしょうか。。
moonphase

2016/06/16 14:12

a[3][2]で a[0]のoffsetは0、a[1]のoffsetはsizeof(int[2]) * 1、a[2]のoffsetはsizeof(int[2]) * 2 そのoffsetに2段目の添字 * 4を加算しているだけです。 a[0][0] = 0 + 0 * 4 a[0][1] = 0 + 1 * 4 a[1][0] = 8 + 0 * 4 a[1][1] = 8 + 1 * 4 a[2][0] = 16 + 0 * 4 a[2][1] = 16 + 1 * 4 アセンブラのコードを吐き出してみるといいかもしれません。 gccの場合 gcc -S -g test.c
777

2016/06/16 15:03

moonphaseさん ご回答ありがとうございます アセンブラは自分には恐らくわからないので(判りたいですが) 手出しできそうにありません>< 折角お返事頂いたのにすみません。
777

2016/06/19 08:42

アセンブラについて少し学んでみました、 スタックに実引数を積んでいる様子とか BSS領域の取られ方とか見えてきて、大変興味が沸きました。 C言語の勉強が一通り終わったら、勉強してみます。 きっかけを与えて下さり、ありがとうございます!
mpyw

2016/06/19 09: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 但し,連続的にしたほうが実装もラクで動作上も有利なため,そうしないコンパイラは現実的には存在しない,と見るべきなんでしょうか. (私も仕様に関して論争起こすの嫌いなので,十中八九それならそれでいいじゃん,って感じの思考回路ではありますが)
guest

0

まず、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 11:57

maisumakun

総合スコア145123

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

777

2016/06/16 12:49

maisumakunさん ご回答ありがとうございます 仰っている「配列型の値が式中に現れた時には要素へのポインタと解される」という部分が核心をついているような気がするのですが 正直なところ、いまだに理解できません 何が理解できていないかもわかっていないような気がしてきました。 もう少しご回答拝読させていただきます><
guest

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を得られる

投稿2016/06/19 09:08

777

総合スコア34

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

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/16 11:50

Chironian

総合スコア23272

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

777

2016/06/16 12:19

Chironianさんご回答ありがとうございます。 自分で書いておいて何なのですが、 「ここまででp[1][2]は(&a[1][0])[2]と同じなのではと推測」 →&a[1][0]の部分はa[1]でもよいのかなと思いました Chironianさんが仰るように &a[1][0] が帰ってくるのなら、それはintの基本型を参照し得るアドレスだと思うのですが つまり int a[3][2] = { { 1, 2 }, { 3, 4 }, { 5, 6 } }; int (*p)[2] = a; であるときに p[2] を評価すると帰ってくる「アドレス」は想像がつくのですが 「型」がわからないみたいです、私は。 ※ double d; と宣言しておき &dを評価すると dの「アドレス」と「ダブルの基本型」へのポインタという事は理解しております 申し訳ございませんが、 もう少しだけかみ砕いて頂けませんでしょうか。。
Chironian

2016/06/16 12:41

> →&a[1][0]の部分はa[1]でもよいのかなと思いました 微妙な差はありますが、現在の議論では同じと考えて良いですよ。 > p[2]を評価すると帰ってくる「アドレス」は想像がつくのですが >「型」がわからないみたいです、私は。 確かにややこしいです。 pはint[2]型へのポインタですね。なので、p[2]はint[2]型2つ分後ろのint[2]型を返します。 p[0] = {1, 2} p[1] = {3, 4} p[2] = {5, 6} です。 念のため確認です。 int *p[2];とint (*p)[2];の相違は理解されていると思いますが、その通りですよね? int*[2] p; int[2] *p;みたいな記述ができると分かりやすいのですが、C/C++はできないので残念です。
777

2016/06/16 13:37

Chironianさん 再度のご回答ありがとうございます。 返答遅くなってしまって申し訳ありません int *p[2] pはint型のポインタ2つを格納できる配列 int (*p)[2] pはint2つを要素とする配列へのポインタ でしょうか 絵を書いて、もう少し考えてみます。
Chironian

2016/06/16 13:59

その通りです。全て理解されているように見えますので、何かちょっとしたことに引っかかっているだけだろうと思います。
777

2016/06/16 15:07

Chironianさん ご返答ありがとうございます お蔭さまで少し見えてきた気がします でもまだ抽象的でふわっとしているので 「モノ」とか表現してしまっている部分を具体的に数値等で考えてみたいと思います int a[4] = {1,2,3,4};のとき 式aを評価すれば「要素1へのアドレスと型はintである」と言葉にできるのですが これが多次元となるとなんだか。。。 もう少し悩んでみます。
Chironian

2016/06/16 15:22

C/C++は、下記の点で混乱しやすいです。   型と変数名を分離して定義できないものがある(配列と関数のシグニチャ)   配列の添字定義は意味的には逆順になっている その混乱に巻き込まれているのかも知れません。 C/C++の文法的には間違いですが、   int a[4];を、int[4] a;と表現し、   int a[3][2];を、int[2][3] a;と表現して みると分かりやすいかも知れません。後者は「int型2個の配列」が3個並んでいるのです。
777

2016/06/19 08:54

Chironian さん ご返答ありがとうございます 仰るように宣言順序がややこしいと思います。 最近やっと関数へのポインタ宣言等読めるようになってきましたが、 複雑なものになるとまだまだ大変です。 今回の件はおかげ様でイメージできるようになりましたので、 もう少し学び、落ち着いたらコンパイラの動きを学習してみようと思います。 ありがとうございました。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

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

ただいまの回答率
85.50%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問