【C言語】配列を引数として渡すことの考察（２次元配列まで）

## はじめに
C言語の関数で、配列を引数として渡す方法については、数多のサイトで紹介されています。
ただし、２次元配列については方法が複数あり、うまく使い分けることが必要となりますので、そのあたりの考察を含めて書いておきます。

わかりやすいように図表をつけて、１次元配列から２次元配列まで順を追って書いていきます。
使用している環境はmacで、コンパイラはgccです。

## １次元配列（数値型）を引数として渡す
まずは、基本形として、数値型（int）の１次元配列についてです。
C言語では、配列そのものを引数として渡せないので、ポインタを引数として渡します。
具体的には、次のソースコード中、main関数4行目にある`num_arr(num, numlen);`のところとなります。

__●ソースコード__

```num_arr.c
#include <stdio.h>

void num_arr(int *num, int numlen) {
  for (int i = 0; i < numlen; i++) {
    printf("%d ", num[i]);
  }
}

int main(void) {
  int num[] = { 13, 5, 33, 69, 37, 14, 98, 23 };
  int numlen = sizeof(num) / sizeof(int);
  num_arr(num, numlen);
  return 0;
}
```
__●ターミナルで実行__

```
$ gcc num_arr.c 
$ ./a.out
13 5 33 69 37 14 98 23
```

ここで注意を要するのは、引数として渡すのは、配列の先頭ポインタを示す`num`だけではなく、配列の要素数`numlen`も渡していることです。
これは、ポインタ`num`が、次のイメージ図のように配列`num[]`の先頭アドレスの情報（`1500`）しか持っていないためです。

__●イメージ図__
<img width="542" alt="スクリーンショット 2020-06-21 17.52.00.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/551412/4af4ab4d-423b-642c-9e94-b4b3f30da50f.png">

ソースコード中の`num_arr関数`側では、配列の先頭アドレスの情報（num = `1500`）だけを受け取っても、それだけでは配列としての要素数がわからないということになります。
これを補うために、配列の要素数`numlen`が引数として必要になります。

なお、配列`num[]`はint型で指定しているため、配列のアドレスは、1500、1504、1508というように4バイトごとに確保されています（環境によっては、int型が4バイトでない場合もあります。）。
ポインタを1つインクリメントするたびに、4バイトずつアドレスが進むことになります。

## １次元配列（文字型）を引数として渡す
次に、char型の配列（文字列）を引数として渡す場合です。
C言語における文字列は、基本的には`char型の配列`になるので、これも文字列（配列）そのものを渡すことはできず、文字列の先頭ポインタを引数として渡すことになります。
具体的には、次のソースコード中、main関数3行目にある`str_arr(str);`のところとなります。

__●ソースコード__

```chr_arr.c
#include <stdio.h>

void str_arr(char *str) {
  printf("%s\n", str);
}

int main(void) {
  char *str = "HELLO";
  str_arr(str);
  return 0;
}
```
__●ターミナルで実行__

```
$ gcc chr_arr.c 
$ ./a.out
HELLO
```

数値型と異なり、引数として渡すのは、配列の先頭ポインタを示す`str`のみで足ります。
これは、文字列（char型配列）の末尾には、NULL文字`'\0'`があるため、受け取り側（str_arr関数）でも配列数（要素数）が簡単にわかるからです。

__●イメージ図__
<img width="540" alt="スクリーンショット 2020-06-21 17.53.20.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/551412/3d78f0c9-0b76-6da2-b887-5c170bcfe994.png">

ソースコード中の`str_arr関数`側では、配列の先頭アドレスの情報（str = 3300）だけを受け取れば、そこから末端NULL文字までを文字型配列として認識すればよいことになります。

## ２次元配列（数値型）を引数として渡す
#### (1) ２次元配列（数値型）の一般的な方法
次に、数値型（int型）の２次元配列についてです。
一般的には、次のようにすれば、引数に渡すことができます。

__●ソースコード__

```num_arr2-1.c
#include <stdio.h>

void num_arr2(int num[][5], int numline) {
  for (int i = 0; i < numline; i++) {
    for (int j = 0; j < 5; j ++) {
      printf("%d ", num[i][j]);
    }
    printf("\n");
  }
}

int main(void) {
  int num[][5] = {
    { 32, 4, 78, 34, 64 },
    { 74, 5, 66, 36, 42 },
    { 56, 13, 55, 3, 81 },
    { 7, 56, 33, 83, 4 },
    { 32, 85, 50, 24, 39 },
    { 16, 24, 56, 43, 6 },
    { 75, 35, 27, 34, 83 },
    { 69, 41, 62, 2, 88 }
  };

  num_arr2(num, 8);

  return 0;
}
```
__●ターミナルで実行__

```
$ gcc num_arr2-1.c 
$ ./a.out
32 4 78 34 64 
74 5 66 36 42 
56 13 55 3 81 
7 56 33 83 4 
32 85 50 24 39 
16 24 56 43 6 
75 35 27 34 83 
69 41 62 2 88 
```

ソースコード中、引数の受け取り側である`num_arr2関数`では、次のように第１引数で、`int num[][5]`という形式で、受け取る配列を指定しています。

```sample.c
void num_arr2(int num[][5], int numline)
```
これは、各行ごとの要素数（列数）を指定しないと、プログラム上、２次元配列として認識されないためです。
２次元配列`num[][]`のデータは、次のような形で、格納されています。

__● イメージ__
<img width="805" alt="スクリーンショット 2020-06-21 16.16.12.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/551412/188920ad-ad18-6af7-2fb7-ee589d30eec3.png">
上記のように、各行のデータが全て繋がっており、データ上は２次元配列でも構造的には１次元配列と同じ形になっています。
そのため、`num_arr2関数`第１引数の`int num[][5]`は、見かけ上は、`int **num`のようなダブルポインタが渡されているように見えますが、実際は、`int *num`と同様のシングルポインタが渡されていることになります。

なお、１次元配列と同様の理由で、２次元配列では、行数`int numline`と列数`int numlen`を、別途に引数として渡す必要があります。
しかし、この形式の場合は`int num[][5]`という形で列数は固定されているため、列数を引数として渡す必要はありません。
#### (2) ２次元配列（数値型）をシングルポインタで渡す方法
一般的な方法で配列を渡すと、あらかじめ要素数が固定されてしまい汎用性に乏しくなります。
そこで、あえてシングルポインタで配列を渡すと、次のとおりとなります。

__●ソースコード__

```num_arr2-2.c
#include <stdio.h>

void num_arr2(int *num, int numline, int numlen) {
  for (int i = 0; i < numline; i++) {
    for (int j = 0; j < numlen; j ++) {
      printf("%d ", num[i * numlen + j]);
    }
    printf("\n");
  }
}

int main(void) {
  int num[][5] = {
    { 32, 4, 78, 34, 64 }, 
    { 74, 5, 66, 36, 42 }, 
    { 56, 13, 55, 3, 81 }, 
    { 7, 56, 33, 83, 4 }, 
    { 32, 85, 50, 24, 39 }, 
    { 16, 24, 56, 43, 6 }, 
    { 75, 35, 27, 34, 83 }, 
    { 69, 41, 62, 2, 88 }
  };

  int numlen = 5;
  int numline = sizeof(num) / sizeof(int) / numlen;
  int *np = (int *)num;
  num_arr2(np, numline, numlen);
  
  return 0;
}
```
__●ターミナルで実行__

```
$ gcc num_arr2-2.c 
$ ./a.out
32 4 78 34 64 
74 5 66 36 42 
56 13 55 3 81 
7 56 33 83 4 
32 85 50 24 39 
16 24 56 43 6 
75 35 27 34 83 
69 41 62 2 88 
```

以上のようなソースコードを記載することで、シングルポインタで配列を渡すことができます。
ただし、`int num[][5]`というような、規則に則った記載をしていないため、プログラム上では２次元配列として認識されず、１次元配列として認識されます。
そのため、配列の取り出しに`num[i][j]`という形は使えないので、次のような回りくどい方法で取り出しをしています（上のイメージ図を見れば、式の意味はわかると思います。）。

```sample.c
printf("%d ", num[i * numlen + j]);
```
少々、問題は残りますが、受け取り側の`num_arr2関数`で列数（要素数）を固定することはないので、汎用的に使用することができるようになります。

なお、このシングルポインタで渡す形式の場合は、列数は固定されていないことから、行数`int numline`と併せて列数`int numlen`を引数として渡します。
#### (3) ２次元配列（数値型）をダブルポインタで渡す方法
さて、次に、配列をダブルポインタで渡す方法です。
これをするには、配列の構造を、次のイメージ図のように変えてあげる必要があります。
<img width="701" alt="スクリーンショット 2020-06-21 16.47.24.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/551412/c644b074-7ab6-caf1-b3c5-dbb719c0d5a3.png">
ダブルポインタが示すアドレス`10`は、シングルポインタの先頭アドレスとなります。
そして、このシングルポインタが示すアドレス`100`は、目的となるデータが格納されているアドレスとなります。
これに準じてソースコードを書き直すと、次のようになります。

__●ソースコード__

```num_arr2-3.c
#include <stdio.h>
#include <stdlib.h>

void num_arr2(int **num, int numline, int numlen) {
  for (int i = 0; i < numline; i++) {
    for (int j = 0; j < numlen; j ++) {
      printf("%d ", num[i][j]);
    }
    printf("\n");
  }
}

int main(void) {
  int input_num[][5] = {
    { 32, 4, 78, 34, 64 }, 
    { 74, 5, 66, 36, 42 }, 
    { 56, 13, 55, 3, 81 }, 
    { 7, 56, 33, 83, 4 }, 
    { 32, 85, 50, 24, 39 }, 
    { 16, 24, 56, 43, 6 }, 
    { 75, 35, 27, 34, 83 }, 
    { 69, 41, 62, 2, 88 }
  };

  // ポインタを使用して２次元の構造にする
  int numlen = 5;
  int numline = sizeof(input_num) / sizeof(int) / numlen;
  int **num = malloc(numline * sizeof(int *));
  for (int i = 0; i < numline; i++) {
    num[i] = malloc(numlen * sizeof(int));
    for (int j = 0; j < numlen; j++) {
      num[i][j] = input_num[i][j];
    }
  }

  // num_arr2関数の実行
  num_arr2(num, numline, numlen);

  // メモリの解放
	for (int i = 0; i < numline; i++) {
		free(num[i]); //各行のメモリを解放
	}
	free(num);

  return 0;
}
```
__●ターミナルで実行__

```
$ gcc num_arr2-3.c 
$ ./a.out
32 4 78 34 64 
74 5 66 36 42 
56 13 55 3 81 
7 56 33 83 4 
32 85 50 24 39 
16 24 56 43 6 
75 35 27 34 83 
69 41 62 2 88 
```
多少、無駄が生じてしまいますので、ここまでやる必要があるかは、目的によると思います。
ただ、こうすることで、（※個人的に）違和感なく汎用的に、２次元配列の受け渡しができるようになりました。
#### (4) ２次元配列（数値型）を簡単に渡す方法（C99に準拠している場合のみ）
２次元配列（数値型）の最後として、C99で使用可能な方法を書いておきます（※ご指摘を受けて一部修正しました）。
これは、「[C言語の引数に多次元配列を渡す](https://qiita.com/Hiraku/items/babed27bc1d750c2e12d)」という記事を元とさせていただきました。

__●ソースコード__

```num_arr2-c99.c
#include <stdio.h>

void num_arr2(int numline, int numlen, int num[numline][numlen]) {
  for (int i = 0; i < numline; i++) {
    for (int j = 0; j < numlen; j ++) {
      printf("%d ", num[i][j]);
    }
    printf("\n");
  }
}

int main(void) {
  int num[][5] = {
    { 32, 4, 78, 34, 64 },
    { 74, 5, 66, 36, 42 },
    { 56, 13, 55, 3, 81 },
    { 7, 56, 33, 83, 4 },
    { 32, 85, 50, 24, 39 },
    { 16, 24, 56, 43, 6 },
    { 75, 35, 27, 34, 83 },
    { 69, 41, 62, 2, 88 }
  };

  int numlen = 5;
  int numline = sizeof(num) / sizeof(int) / numlen;
  num_arr2(numline, numlen, num);
  
  return 0;
}
```
（※実行結果は同じなので省略）

C99に準拠している環境であれば、これを使えば便利だと思います（私の環境では、gccでは動きますが、Visual Studioでは動きませんでした）。
なお、配列のポインタを渡す引数`int num[numline][numlen]`は最後（第３引数）にしないと読み込みができずエラーが起きるので注意してください。

## ２次元配列（文字型）を引数として渡す【参考】
文字配列（char型配列）についても、基本的には、数値型と同じ考え方で対応ができると思います。

参考として、実験的に各行のバイト数を可変長で取得した場合のソースコードを、下記に紹介しておきます。
細かいことは、こちらの記事「[Ｃ言語におけるファイル情報の読み取りと文字型配列への格納](https://yama-3.net/c/file)」に書いてあります。
この元記事を書いた頃はまだビギナーだったので、輪を掛けて拙い記述となっている点はご容赦ください。

__●ソースコード__

```chr_arr2.c
#include <stdio.h>
#include <stdlib.h>

int get_ftext(char **str, const char *fname, int lines, int *len);
int get_ftext_lines(const char *fname);
int get_ftext_len(int *len, const char *fname);

int main(void) {
  // ファイルの読み出し（get_ftext関数の実行まで）
  const char *fname = "file01.txt";  // ファイル名を設定（ソースコードと同じフォルダに）
  int lines = get_ftext_lines(fname);  // get_ftext_lines関数を用いて、ファイルの行数を取得
  int *len = (int *)malloc(lines * sizeof(int));  // 変数lenは、各行のバイト数を格納するためのもの
  get_ftext_len(len, fname);  // get_ftext_len関数を用いて、変数lenに各行のバイト数を入れる
  char **str = (char **)malloc(lines * sizeof(char *));
  for (int i = 0; i < lines; i++) {
    str[i] = (char *)malloc((len[i] + 1) * sizeof(char)); // 各行に文字列を格納するメモリを確保
  }
  get_ftext(str, fname, lines, len); // get_ftext関数を用いて、strの用意したメモリにファイルの文字列データを格納
  
  // 実行結果の確認
  printf("＜strの格納データは以下のとおり＞\n");
  for (int i = 0; i < lines; i++) {
    printf("%s", str[i]);
  }
  
  // メモリの解放
  for (int i = 0; i < lines; i++) {
    free(str[i]); // 各行のメモリを解放
  }
  free(str);
  free(len);
  
  return 0;
}

// get_ftext関数（二次元配列を使ってファイル内の文字列データを行ごとに格納する関数）
int get_ftext(char **str, const char *fname, int lines, int *len) {
  FILE *fp = fopen(fname, "rb");
  if (fp == NULL) {
    printf("file open error!\n");
    return -1;
  }
  for (int i = 0; i < lines && fgets(str[i], len[i] + 1, fp) != NULL; i++) {}
  fclose(fp);
  return 0;
}

// get_ftext_lines関数（ファイルの行数を取得する関数）
int get_ftext_lines(const char *fname) {
  FILE *fp = fopen(fname, "rb");
  int c;
  int lines = 0; // 行をカウントする変数
  if (fp == NULL) {
    printf("file open error!\n");
    return -1;
  }
  
  while (1) {
    c = fgetc(fp);
    if (c == '\n') { // 改行があるたびに行をカウント（なお、fgetsの場合'\n'を改行として認識する）
      lines++;
    } else if (c == EOF) { // 最終行に改行がされていない場合も1行として拾うための処置
      lines++;
      break;
    }
  }
  
  fclose(fp);
  return lines;
}

// get_ftext_len関数（ポインタ[len]にファイルの行ごとのバイト長を格納する）
int get_ftext_len(int *len, const char *fname)
{
  FILE *fp = fopen(fname, "rb");
  int c;
  int i = 0;
  int byt = 0; //バイト数のカウント用
  int byt_tmp = 0; //変数bytの一時コピー用
  if (fp == NULL) {
    printf("file open error!\n");
    return -1;
  }
  
  while (1) {
    c = fgetc(fp);
    byt++;
    if (c == '\n') {
      len[i] = byt - byt_tmp;
      i++;
      byt_tmp = byt; //bytの数値をbyt_tmpにコピー
    } else if (c == EOF) {
      if ((byt - byt_tmp) <= 1) { //最後が改行で終わっている場合
        len[i] = 0;
      } else {
        len[i] = byt - byt_tmp; //最後が改行で終わっていない場合
      }
      break;
    }
  }
  
  fclose(fp);
  return 0;
}
```
２次元配列を渡しているのは、次の関数です。

```sample.c
int get_ftext(char **str, const char *fname, int lines, int *len);
```

このダブルポインタの`str`に渡されるのは、ファイル内のテキストが入るだけのメモリを確保した空の領域（のポインタ）となります。

## さいごに
数か月ぶりに、C言語を扱いました。
基本的なことを忘れがちなので、良いテーマがあれば少しずつ記事にまとめていこうと思っています。

＜2020年10月9日追記＞
「**２次元配列（数値型）の一般的な方法**」につき一部修正しました。
修正前は、`void num_arr2(int num[][5], int numline, int numlen)`という形で、列数も引数として渡していましたが、`numlen = 5`であることは自明であるため列数は引数として渡さない形に修正しました。