回答編集履歴

2 サンプルのミスを修正

Eki

Eki score 391

2018/07/18 14:56  投稿

Chironian さんのコメント欄でお話していましたが、あまりにも長くなってきたので、まとめる意味を込めてこちらに回答します。
しばらく、既にコメント欄で進めて解決した部分も含めてまとめていきます。
説明の都合上変更したところがあります。特に `pimage_type` を廃止したので注意です (別の型を導入しました) 。 `pimage_type` のことは一旦忘れてください。
## malloc() と free()
まず最初の問題は、グローバル変数として確保しようとしている多次元配列が非常に大きかったため、コンパイラの限界を越えてしまったことでした。そのような大量のメモリ確保を行うときは、残念ながら **ヒープ領域** に頼る他ありません。ヒープ領域は `malloc()` 関数を使って確保するメモリ領域です。ここでは一般により多くのメモリを確保することができますが、若干速度が遅くなったり、使い終えた領域を忘れずに `free()` しなければいけなかったりと少し不便な領域です。
さて、例えば `malloc()` を使って int 型の要素数 10 の配列を確保するには、次のようにします。
```c
// malloc() は取得したい領域のサイズをバイト単位で受け取ります。
// sizeof(int) は int 1 つ分のバイト数を表すので、それ×10 をしてやれば 10 個分になりますね。
int *p = (int *)malloc(sizeof(int) * 10);
// なんかいろいろ。 int のポインタは厳密には一次元配列ではありませんが、
// 似たような扱いができます。 p[0] のように添字でアクセスできます。
p を使う処理1;
p を使う処理2;
p を使う処理3;
// 終わったら、必ず解放しましょう。
free(p);
```
`malloc()` は言われた通りのサイズの領域をガバッと持ってくることしかしませんから、確保したメモリ領域がどういう型を格納するためのものかなどは気にしません。上の例では、わたしたちは確保した領域を int の配列として使いたかったのですよね?ということで、確保した領域を int のポインタにつっこんでいるのです。
## 多次元配列の malloc()
さて、多次元配列のときはどうしましょうか。実は、C言語には多次元配列というものはなく **配列の配列** しかないのです。ということは配列の型が分かれば、上の例と同様に、その配列の型へのポインタにつっこめば配列の配列として使えるのではないでしょうか?
はい、実は配列にも型があります。ただし表記が複雑なので混乱します。これはC言語の悪いところの一つですが、興味があれば聞いてください。説明します。が、もう少し後でもよいかもしれません。
とりあえず次の一文で、 `image_type` という名前が、とある配列へのポインタ型になります。
```c
typedef unsigned char image_type[MAX_IMAGESIZE][MAX_IMAGESIZE][3];
```
**注** さきほどまでコメント欄では `pimage_type` としていましたが、 **名前も内容も** 僅かに変わっています。混乱させてすみません。
この `image_type` がどういう型かですが、
```c
unsigned char image[MAX_NUM_OF_IMAGES][MAX_IMAGESIZE][MAX_IMAGESIZE][3];
image_type image[MAX_NUM_OF_IMAGES];
```
上の二つの宣言が全く同じものになるような型です。よく見ると上の配列は最初にグローバル変数で配列を用意しようとしていたときのアレですね。
感覚的に言えば、上の配列の 3 次元配列部分 `[MAX_IMAGESIZE][MAX_IMAGESIZE][3]` をひとまとめにしたような型です。
欲しい型が判明してしまえば、やることは int の配列を用意したときと同様です。int だったところを `image_type` にすればよいです。
```c
image_type *pimage = (image_type *)malloc(sizeof(image_type) * MAX_NUM_OF_IMAGES);
```
**注** 実はコメントにおける `pimage_type` というのは、 `image_type` のポインタ型でした。つまり `pimage_type == (image_type *)` です。 `image_type` を定義したほうが説明しやすく、分かりやすいと思ったので変更しました。
その後、 `malloc()` を **直接グローバル変数の初期値としてつっこむことはできない** という話をしましたね。
必ず何か関数を用意して (main() 関数でもよいですが) **実行時の処理** として malloc() しないといけません。同様に free() もどこかしらで行う必要があります。
---
(イマココ) ここまでコメント欄でお話しました。
---
## いろいろな場所から使えるようにする
そういうわけで実装してくださった `memory()` 関数ですが、今度は他の場所で `pimage` が見つからなくなったということでした。
`memory()` 関数内のローカル変数として宣言されたものは当然、他の関数からは見えません。
別の関数で `malloc()` するなら、確保したメモリ領域を `memory()` 関数の戻り値として返してあげるか、 `pimage` をグローバル変数にするか、どちらかの方法で外に出してやらないといけません。
グローバル変数にするということなので、単純に `pimage` をグローバルで宣言してやればよいです。 `pimage` はよくわからない `image_type *` などという型をしていますが、ただのポインタです。 `int *` とかと同じでいいんです。
```c
image_type *pimage;
// (中略)
void memory(void) {
 // memory() 側ではローカル変数 pimage を宣言するのをやめ、グローバル変数につっこむようにする。
 pimage = malloc(...);
}
```
## まとめ
結局、次のような感じのコードになります。
```c
typedef unsigned char image_type[MAX_IMAGESIZE][MAX_IMAGESIZE][3];
image_type *pimage;
// (中略)
void memory(void){
   pimage = (image_type *)malloc(sizeof(image_type) * MAX_IMAGESIZE);
   pimage = (image_type *)malloc(sizeof(image_type) * MAX_NUM_OF_IMAGES);
}
```
---
## おまけ
本題とは逸れますが、
1. `printf()` や `FILE` 構造体などは `stdio.h` というヘッダファイルに入っています。できれば `#include <stdio.h>` しておいたほうがよいと思います。
1. `ReadImage()` 関数の定義のところで、その引数のところが
       void ReadImage(set) {
         // ...
       }
   となっていますが、きちんと `set` の型を明示するべきです。コンパイルエラーにはならないのですが、C言語では何も書かないと勝手に int と思われるという恐ろしいルールがあります。今回は `set` の型は使われ方から見ても int なので結果的に問題はなかったのですが、思わぬバグの温床になります。見た目にもわかりづらいので、例え int を受け取るのであろうと、きちんと書くべきです。
1 おまけを追加

Eki

Eki score 391

2018/07/18 01:57  投稿

Chironian さんのコメント欄でお話していましたが、あまりにも長くなってきたので、まとめる意味を込めてこちらに回答します。
しばらく、既にコメント欄で進めて解決した部分も含めてまとめていきます。
説明の都合上変更したところがあります。特に `pimage_type` を廃止したので注意です (別の型を導入しました) 。 `pimage_type` のことは一旦忘れてください。
## malloc() と free()
まず最初の問題は、グローバル変数として確保しようとしている多次元配列が非常に大きかったため、コンパイラの限界を越えてしまったことでした。そのような大量のメモリ確保を行うときは、残念ながら **ヒープ領域** に頼る他ありません。ヒープ領域は `malloc()` 関数を使って確保するメモリ領域です。ここでは一般により多くのメモリを確保することができますが、若干速度が遅くなったり、使い終えた領域を忘れずに `free()` しなければいけなかったりと少し不便な領域です。
さて、例えば `malloc()` を使って int 型の要素数 10 の配列を確保するには、次のようにします。
```c
// malloc() は取得したい領域のサイズをバイト単位で受け取ります。
// sizeof(int) は int 1 つ分のバイト数を表すので、それ×10 をしてやれば 10 個分になりますね。
int *p = (int *)malloc(sizeof(int) * 10);
// なんかいろいろ。 int のポインタは厳密には一次元配列ではありませんが、
// 似たような扱いができます。 p[0] のように添字でアクセスできます。
p を使う処理1;
p を使う処理2;
p を使う処理3;
// 終わったら、必ず解放しましょう。
free(p);
```
`malloc()` は言われた通りのサイズの領域をガバッと持ってくることしかしませんから、確保したメモリ領域がどういう型を格納するためのものかなどは気にしません。上の例では、わたしたちは確保した領域を int の配列として使いたかったのですよね?ということで、確保した領域を int のポインタにつっこんでいるのです。
## 多次元配列の malloc()
さて、多次元配列のときはどうしましょうか。実は、C言語には多次元配列というものはなく **配列の配列** しかないのです。ということは配列の型が分かれば、上の例と同様に、その配列の型へのポインタにつっこめば配列の配列として使えるのではないでしょうか?
はい、実は配列にも型があります。ただし表記が複雑なので混乱します。これはC言語の悪いところの一つですが、興味があれば聞いてください。説明します。が、もう少し後でもよいかもしれません。
とりあえず次の一文で、 `image_type` という名前が、とある配列へのポインタ型になります。
```c
typedef unsigned char image_type[MAX_IMAGESIZE][MAX_IMAGESIZE][3];
```
**注** さきほどまでコメント欄では `pimage_type` としていましたが、 **名前も内容も** 僅かに変わっています。混乱させてすみません。
この `image_type` がどういう型かですが、
```c
unsigned char image[MAX_NUM_OF_IMAGES][MAX_IMAGESIZE][MAX_IMAGESIZE][3];
image_type image[MAX_NUM_OF_IMAGES];
```
上の二つの宣言が全く同じものになるような型です。よく見ると上の配列は最初にグローバル変数で配列を用意しようとしていたときのアレですね。
感覚的に言えば、上の配列の 3 次元配列部分 `[MAX_IMAGESIZE][MAX_IMAGESIZE][3]` をひとまとめにしたような型です。
欲しい型が判明してしまえば、やることは int の配列を用意したときと同様です。int だったところを `image_type` にすればよいです。
```c
image_type *pimage = (image_type *)malloc(sizeof(image_type) * MAX_NUM_OF_IMAGES);
```
**注** 実はコメントにおける `pimage_type` というのは、 `image_type` のポインタ型でした。つまり `pimage_type == (image_type *)` です。 `image_type` を定義したほうが説明しやすく、分かりやすいと思ったので変更しました。
その後、 `malloc()` を **直接グローバル変数の初期値としてつっこむことはできない** という話をしましたね。
必ず何か関数を用意して (main() 関数でもよいですが) **実行時の処理** として malloc() しないといけません。同様に free() もどこかしらで行う必要があります。
---
(イマココ) ここまでコメント欄でお話しました。
---
## いろいろな場所から使えるようにする
そういうわけで実装してくださった `memory()` 関数ですが、今度は他の場所で `pimage` が見つからなくなったということでした。
`memory()` 関数内のローカル変数として宣言されたものは当然、他の関数からは見えません。
別の関数で `malloc()` するなら、確保したメモリ領域を `memory()` 関数の戻り値として返してあげるか、 `pimage` をグローバル変数にするか、どちらかの方法で外に出してやらないといけません。
グローバル変数にするということなので、単純に `pimage` をグローバルで宣言してやればよいです。 `pimage` はよくわからない `image_type *` などという型をしていますが、ただのポインタです。 `int *` とかと同じでいいんです。
```c
image_type *pimage;
// (中略)
void memory(void) {
 // memory() 側ではローカル変数 pimage を宣言するのをやめ、グローバル変数につっこむようにする。
 pimage = malloc(...);
}
```
## まとめ
結局、次のような感じのコードになります。
```c
typedef unsigned char image_type[MAX_IMAGESIZE][MAX_IMAGESIZE][3];
image_type *pimage;
// (中略)
void memory(void){
   pimage = (image_type *)malloc(sizeof(image_type) * MAX_IMAGESIZE);
}
```
---
## おまけ
本題とは逸れますが、 `printf()` や `FILE` 構造体などは `stdio.h` というヘッダファイルに入っています。できれば `#include <stdio.h>` しておいたほうがよいと思います。
本題とは逸れますが、
1. `printf()` や `FILE` 構造体などは `stdio.h` というヘッダファイルに入っています。できれば `#include <stdio.h>` しておいたほうがよいと思います。
1. `ReadImage()` 関数の定義のところで、その引数のところが
       void ReadImage(set) {
         // ...
       }
   となっていますが、きちんと `set` の型を明示するべきです。コンパイルエラーにはならないのですが、C言語では何も書かないと勝手に int と思われるという恐ろしいルールがあります。今回は `set` の型は使われ方から見ても int なので結果的に問題はなかったのですが、思わぬバグの温床になります。見た目にもわかりづらいので、例え int を受け取るのであろうと、きちんと書くべきです。

思考するエンジニアのためのQ&Aサイト「teratail」について詳しく知る