プロジェクト

全般

プロフィール

C言語で2次元配列を渡せなくて困った話 » 履歴 » バージョン 5

sylow castle, 2019/03/15 21:12

1 1 sylow castle
# C言語の2次元配列を渡せなくて困った話
2
3
タイトルの通りです。正確に言うと、C言語で文字列の配列を引数に取る関数でドハマりしたっていう話です。
4
ポインタをちゃんと勉強したら「よくできてるな」って納得した感じ。
5
6
## 前提
7
8
* intは4バイトとします。
9
10
## 詰まったところ
11
12
二次元配列を受け取れるような関数を定義したかった。
13
とりあえず話をするために名前を付ける。
14 3 sylow castle
15 1 sylow castle
```
16
int caller_matrix[3][5]
17
```
18 2 sylow castle
19
caller_matrixを実引数にして渡そうとしています。
20 1 sylow castle
で受け取る側ですが以下の、1番のシグネチャじゃダメってコンパイラから怒られた(警告だったかもしれない)っていうのでわけわかんなくなって詰まったんですね。
21
1番:
22
23
```
24
void some_function(int *matrix[5])
25
```
26
27
※ちなみに正しい渡し方。
28
2番:
29
30
```
31
void some_function(int arr[][5])
32
```
33
34
or
35
3番:
36
37
```
38
void some_function(int (*arr)[5]) 
39
```
40
41
2番は理解するけど、3番と1番の違いって何よ?何でダメ?っていうお話。
42
「2番、3番はバッファオーバーランの原因になるだろ。その書き方は認めない」っていう突っ込みはとりあえずやめてください。
43
44
### 配列の名前
45
46
以下のような宣言があったとする。
47 3 sylow castle
48 1 sylow castle
```
49
int arr[5];
50
int* p_arr;
51
52
p_arr = arr;
53
```
54 3 sylow castle
55 1 sylow castle
これは正しいですね。なぜなら「arr」は評価(?)されたときに、intへのポインタ型であるアドレスを返します。これがまず第一の面白ポイント。
56
で、配列を引数に取る関数は
57
58
```
59 4 sylow castle
void some_function(int* argument) 
60 1 sylow castle
```
61
62
って書いても
63
64
```
65 4 sylow castle
void some_function(int argument[])
66 1 sylow castle
```
67
68
って書いても大丈夫。これを踏まえて
69
70
```
71
void some_function(int *matrix[5])
72
```
73
74
って書いた。結果は上で言った通りむっちゃ怒られた。~~C言語マジ意味わかんねぇとか呟いた~~
75
76
### 整理
77
78
さて、疑問を整理してみると一番は
79
80
```
81
int *matrix[5]
82
```
83
84
の型ってなんだ?っていう話になります。
85
86
### 型の計算
87
88
ちょっとJavaやC#の総称型っぽく書きます。
89
型Tに対してArr[T]で「Tの配列」型、Ptr[T]で「Tへのポインタ」型を意味するとします。
90
さて、問題の型を見てみましょう。これは以下のように二通りの解釈ができます。C言語の初心者にとっては。
91
これがよく分からない原因です。
92
93
1. Arr[Ptr[int]]
94
1. Ptr[Arr[int]]
95
96
前者は分かります。実際のデータを考えるとアドレス(intへのポインタ型)がメモリにびっしり詰まっているイメージです。
97
さて後者は何でしょう?配列へのポインタ?なんだそれ?
98
そこで「配列へのポインタとはそういえば聞いたことがない気がする」と気づきます。でも、「配列が確保したメモリの先頭アドレスだろ。常識的に考えて…」とか思ったりしました。
99
100
後者は何だと頭を悩ませていたときに「そもそもPtr<int>ってなんだ?」とか思い始めて来ます。これは理解できます。
101
メモリをイメージすればいいのです。以下の図はメモリのイメージです。
102
図形一個で1バイトです。四角がint型変数が確保した領域を指します。(ちゃんとintは4バイトと仮定しましたよ)
103
○○○ ■□□□ ○○○○
104
105
Ptr<int>は■を指します。確保したメモリ領域の先頭を指します。
106
107
数学っぽく考えましょう。以下のように一般化します。
108
「Ptr<T>はT型変数が確保した連続したメモリ領域の先頭を指す」
109
と考えます。こう考えると全ての表記と辻褄があってきそうです。
110
111
さて、次に以下のコードに戻りましょう。さっき面白ポイントといったところです。
112
113
```
114
int arr[5];
115
int *p_arr;
116
117
p_arr = arr;
118
```
119
120
p_arrの型はPtr[int]です。arrの型はArr[int]です。ですが、ある意味で区別しなくてよい表記ができています。
121
※厳密にはポインタと配列は区別しなければならないものです。arrにint型へのポインタを代入することはできないですしね。
122
123
### ポインタ記法と配列記法
124
125
この解釈をつきつめていくとポインタ記法と配列記法の整合性もちゃんと取れているのが面白ポイントその2です。
126
127
```
128
#include <stdio.h>
129
130
int main() {
131
    int arr[5] = {1,2,3,4,5};
132
    int *p_arr;
133
134
    p_arr = arr;
135
136
    printf("arr[2]:     %d\n", arr[2]);
137
    printf("*(arr + 2): %d\n", *(arr+2));
138
    printf("p_arr[2]:   %d\n", p_arr[2]);
139
    printf("*(p_arr+2): %d\n", *(p_arr+2));
140
}
141
```
142
143
実行結果:
144
145
```
146
arr[2]:     3
147
*(arr + 2): 3
148
p_arr[2]:   3
149
*(p_arr+2): 3
150
```
151
152
どれも同じ値として評価されますね。メモリの状態を頭の中で描くとこんな感じになります。
153
○○○ ■□□□ ■□□□ ■□□□ ■□□□ ■□□□ ○○○○○
154
四角系はarrが占めている領域です。色塗りが各要素の先頭アドレスの指す領域です。
155
arrの評価結果は最初の■を表し、
156
arr[1]は2番目の4ブロックを表します。2ブロック(1ブロック=sizeof(int))の分だけ進んだ値の実態です
157
ポインタ演算p_arr+2の演算結果は2ブロック分進んだ三つ目の■のアドレスを指します。
158
素晴らしいですね。
159
ややこしいですね。
160
161
ということでPtr<T>と解釈を広げてあげることでPtr[Atr[T]]が意味づけられ、今までのものとちゃんと一貫してるかのような表記方法が得られるわけですね。
162
ただ素直に考えた配列のアドレス「&arr」というのは通用しなくなってしまいます。ここが惜しい感じがします。
163
164
### 再び二次元
165
166
さて、新しい記法を手に入れたので戻ってみます。
167
int型の二次元配列(int matrix[3][5]、厳密には『「int型の配列」の配列』、はArr[Arr[T]]ですね。メモリのイメージは
168
169
○○○ ■□□□ ■□□□ ■□□□ ■□□□ ■□□□
170
    ■□□□ ■□□□ ■□□□ ■□□□ ■□□□
171
    ■□□□ ■□□□ ■□□□ ■□□□ ■□□□ ○○○○○
172
173
といった具合です。
174
この確保の仕方は二次元配列の初期化子{{1,2,3,4,5},{6,7,8,9,10},{11,12,13,14,15}}とも一貫しています。
175
また、matrix[1]で2列目の先頭の黒のアドレスを指すことも整合性が取れています。matrix[1]はPtr[Arr[int]ではないはずですがね…。
176
これらはPtr[T]がArr[T]と同じように考えたられることになるかと思います
177
TをArr[int]で置き換えてみましょう(int型の配列も立派な型です)。
178
それぞれ、Ptr[Arr[int]]、Arr[Arr[int]]となります。
179
180
そして今、戻りに戻って当初の問題となった「2次元配列を受け取る関数」シグネチャを見てみましょう。
181
182
```
183
void some_function(int arr[][5])
184
```
185
186
この書き方はArr[Arr[int]]を想起させます。
187
188
とすると
189
190
```
191
void some_function(int (*arr)[5]) 
192
```
193
194
はPtr[Arr[int]]に相当するのでしょう。\*を括弧内に入れることで「仮引数arrはPtr型」を優先的に示していると。
195
196
そして、ダメなシグネチャ
197
198
```
199
void some_function(int *matrix[5])
200
```
201
202
この引数の宣言はArr[Ptr[int]]と解釈されます。C言語的には。
203
これはよろしくないですね。
204
205
### 結論
206
207
「ハゲの山田氏のカツラ」という名詞句は「ハゲの山田氏」なのか「ハゲのカツラ」なのかという2通りの解釈ができますね。「ハゲの」を受けるのが山田氏なのかカツラなのかどちらかが優先されるかで意味合いが変わってきます。
208
本質的にはこれと同じく、int \*arr[5]と書いたときの\*と[5]のどちらの解釈が優先されるのかということが問題だったんですね。
209
左から修飾するもの(今回の\*)と右から修飾するもの(今回の[5])を括弧なしに並べて書いたときは同様に解釈の優先順位の問題がでてくるんじゃないかと思います。
210
211
212
### 余談
213
214
* Arr[int]とPtr[int]が同じように解釈できるならダメなのもできなきゃおかしくねっていう気も若干してきましたが、
215
Arr[Ptr[int]]は、Ptr[int]が確保するサイズを1ブロックとしてメモリを連続して確保するの意味合いが変わってきてしまいますね。やっぱダメですね。
216 5 sylow castle
一番外側のArrだけPtrと交換可能なんですかね。
217
    * メモリ確保で考えると交換できないけど、そこだけ気を付ければなんかうまくいく気もするぞ…
218 1 sylow castle
219
* 例えば、JavaScriptの式typeof 'string' === 'bool'ということを考えるとtypeof ('string' === 'bool')と解釈すれば'bool'ですし、(typeof 'string' ) === 'bool'ならtrue(だよな?)という評価になるかと思います。前者の表記はとても不自然ですが、一つの読み方としてはあり得ない話ではないかと思います。
220
ちゃんと括弧つけてあげると安心。
221
222
* 今度は3次元を引数に取ろうとして迷う気がする。
223
224
* C言語の二次元配列のメモリの確保の仕方はCOBOLの配列の宣言と同じです。なんてこったい、つながったぞ。まぁ一次元を以下に二次元にするかだから自然な発想ですよね。
225
226
## 参考
227
228
オライリーさんの詳説Cポインタ:https://www.oreilly.co.jp/books/9784873116563/
229
230
こんなことを考えて配列とポインタとなんとなく分かったような気がしたのです。長い長い言い訳でした。
231
というか段々何言ってるか分からなくなってきた。