80
59

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

【C言語】配列を引数として渡すことの考察(2次元配列まで)

Last updated at Posted at 2020-06-21

はじめに

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

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

1次元配列(数値型)を引数として渡す

まずは、基本形として、数値型(int)の1次元配列についてです。
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)しか持っていないためです。

●イメージ図
スクリーンショット 2020-06-21 17.52.00.png

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

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

1次元配列(文字型)を引数として渡す

次に、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関数)でも配列数(要素数)が簡単にわかるからです。

●イメージ図
スクリーンショット 2020-06-21 17.53.20.png

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

2次元配列(数値型)を引数として渡す

(1) 2次元配列(数値型)の一般的な方法

次に、数値型(int型)の2次元配列についてです。
一般的には、次のようにすれば、引数に渡すことができます。

●ソースコード

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関数では、次のように第1引数で、int num[][5]という形式で、受け取る配列を指定しています。

sample.c
void num_arr2(int num[][5], int numline)

これは、各行ごとの要素数(列数)を指定しないと、プログラム上、2次元配列として認識されないためです。
2次元配列num[][]のデータは、次のような形で、格納されています。

● イメージ
スクリーンショット 2020-06-21 16.16.12.png
上記のように、各行のデータが全て繋がっており、データ上は2次元配列でも構造的には1次元配列と同じ形になっています。
そのため、num_arr2関数第1引数のint num[][5]は、見かけ上は、int **numのようなダブルポインタが渡されているように見えますが、実際は、int *numと同様のシングルポインタが渡されていることになります。

なお、1次元配列と同様の理由で、2次元配列では、行数int numlineと列数int numlenを、別途に引数として渡す必要があります。
しかし、この形式の場合はint num[][5]という形で列数は固定されているため、列数を引数として渡す必要はありません。

(2) 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]というような、規則に則った記載をしていないため、プログラム上では2次元配列として認識されず、1次元配列として認識されます。
そのため、配列の取り出しにnum[i][j]という形は使えないので、次のような回りくどい方法で取り出しをしています(上のイメージ図を見れば、式の意味はわかると思います。)。

sample.c
printf("%d ", num[i * numlen + j]);

少々、問題は残りますが、受け取り側のnum_arr2関数で列数(要素数)を固定することはないので、汎用的に使用することができるようになります。

なお、このシングルポインタで渡す形式の場合は、列数は固定されていないことから、行数int numlineと併せて列数int numlenを引数として渡します。

(3) 2次元配列(数値型)をダブルポインタで渡す方法

さて、次に、配列をダブルポインタで渡す方法です。
これをするには、配列の構造を、次のイメージ図のように変えてあげる必要があります。
スクリーンショット 2020-06-21 16.47.24.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 }
  };

  // ポインタを使用して2次元の構造にする
  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 

多少、無駄が生じてしまいますので、ここまでやる必要があるかは、目的によると思います。
ただ、こうすることで、(※個人的に)違和感なく汎用的に、2次元配列の受け渡しができるようになりました。

(4) 2次元配列(数値型)を簡単に渡す方法(C99に準拠している場合のみ)

2次元配列(数値型)の最後として、C99で使用可能な方法を書いておきます(※ご指摘を受けて一部修正しました)。
これは、「C言語の引数に多次元配列を渡す」という記事を元とさせていただきました。

●ソースコード

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]は最後(第3引数)にしないと読み込みができずエラーが起きるので注意してください。

2次元配列(文字型)を引数として渡す【参考】

文字配列(char型配列)についても、基本的には、数値型と同じ考え方で対応ができると思います。

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

●ソースコード

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;
}

2次元配列を渡しているのは、次の関数です。

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

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

さいごに

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

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

80
59
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
80
59

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?