プロジェクト

全般

プロフィール

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

sylow castle, 2019/03/15 21:10

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