プロジェクト

全般

プロフィール

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

sylow castle, 2019/03/15 21:09

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