Quantcast
Channel: 配列タグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 757

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

$
0
0

はじめに

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

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

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

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

●ソースコード

num_arr.c
#include <stdio.h>
voidnum_arr(int*num,intnumlen){for(inti=0;i<numlen;i++){printf("%d ",num[i]);}}intmain(void){intnum[]={13,5,33,69,37,14,98,23};intnumlen=sizeof(num)/sizeof(int);num_arr(num,numlen);return0;}

●ターミナルで実行

$ 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>
voidstr_arr(char*str){printf("%s\n",str);}intmain(void){char*str="HELLO";str_arr(str);return0;}

●ターミナルで実行

$ 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>
voidnum_arr2(intnum[][5],intnumline,intnumlen){for(inti=0;i<numline;i++){for(intj=0;j<numlen;j++){printf("%d ",num[i][j]);}printf("\n");}}intmain(void){intnum[][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}};intnumlen=5;intnumline=sizeof(num)/sizeof(int)/numlen;// つまり numline = 8 となるnum_arr2(num,numline,numlen);return0;}

●ターミナルで実行

$ 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
voidnum_arr2(intnum[][5],intnumline,intnumlen)

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

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

なお、1次元配列と同様の理由で、行数int numlineと列数int numlenは、別途に引数として渡す必要があります。

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

一般的な方法で配列を渡すと、あらかじめ要素数が固定されてしまい汎用性に乏しくなります。
そこで、あえてシングルポインタで配列を渡すと、次のとおりとなります。

●ソースコード

num_arr2-2.c
#include <stdio.h>
voidnum_arr2(int*num,intnumline,intnumlen){for(inti=0;i<numline;i++){for(intj=0;j<numlen;j++){printf("%d ",num[i*numlen+j]);}printf("\n");}}intmain(void){intnum[][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}};intnumlen=5;intnumline=sizeof(num)/sizeof(int)/numlen;int*np=(int*)num;num_arr2(np,numline,numlen);return0;}

●ターミナルで実行

$ 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関数で列数(要素数)を固定することはないので、汎用的に使用することができるようになります。

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

さて、次に、配列をダブルポインタで渡す方法です。
これをするには、配列の構造を、次のイメージ図のように変えてあげる必要があります(たぶん)。
スクリーンショット 2020-06-21 16.47.24.png
ダブルポインタが示すアドレス10は、シングルポインタの先頭アドレスとなりますす。
そして、このシングルポインタが示すアドレス100は、目的となるデータが格納されているアドレスとなります。
これに準じてソースコードを書き直すと、次のようになります。

●ソースコード

num_arr2-3.c
#include <stdio.h>
#include <stdlib.h>
voidnum_arr2(int**num,intnumline,intnumlen){for(inti=0;i<numline;i++){for(intj=0;j<numlen;j++){printf("%d ",num[i][j]);}printf("\n");}}intmain(void){intinput_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次元の構造にするintnumlen=5;intnumline=sizeof(input_num)/sizeof(int)/numlen;int**num=malloc(numline*sizeof(int*));for(inti=0;i<numline;i++){num[i]=malloc(numlen*sizeof(int));for(intj=0;j<numlen;j++){num[i][j]=input_num[i][j];}}// num_arr2関数の実行num_arr2(num,numline,numlen);// メモリの解放for(inti=0;i<numline;i++){free(num[i]);//各行のメモリを解放}free(num);return0;}

●ターミナルで実行

$ 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>
voidnum_arr2(intnumline,intnumlen,intnum[numline][numlen]){for(inti=0;i<numline;i++){for(intj=0;j<numlen;j++){printf("%d ",num[i][j]);}printf("\n");}}intmain(void){intnum[][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}};intnumlen=5;intnumline=sizeof(num)/sizeof(int)/numlen;num_arr2(numline,numlen,num);return0;}

(※実行結果は同じなので省略)

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

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

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

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

●ソースコード

chr_arr2.c
#include <stdio.h>
#include <stdlib.h>
intget_ftext(char**str,constchar*fname,intlines,int*len);intget_ftext_lines(constchar*fname);intget_ftext_len(int*len,constchar*fname);intmain(void){// ファイルの読み出し(get_ftext関数の実行まで)constchar*fname="file01.txt";// ファイル名を設定(ソースコードと同じフォルダに)intlines=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(inti=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(inti=0;i<lines;i++){printf("%s",str[i]);}// メモリの解放for(inti=0;i<lines;i++){free(str[i]);// 各行のメモリを解放}free(str);free(len);return0;}// get_ftext関数(二次元配列を使ってファイル内の文字列データを行ごとに格納する関数)intget_ftext(char**str,constchar*fname,intlines,int*len){FILE*fp=fopen(fname,"rb");if(fp==NULL){printf("file open error!\n");return-1;}for(inti=0;i<lines&&fgets(str[i],len[i]+1,fp)!=NULL;i++){}fclose(fp);return0;}// get_ftext_lines関数(ファイルの行数を取得する関数)intget_ftext_lines(constchar*fname){FILE*fp=fopen(fname,"rb");intc;intlines=0;// 行をカウントする変数if(fp==NULL){printf("file open error!\n");return-1;}while(1){c=fgetc(fp);if(c=='\n'){// 改行があるたびに行をカウント(なお、fgetsの場合'\n'を改行として認識する)lines++;}elseif(c==EOF){// 最終行に改行がされていない場合も1行として拾うための処置lines++;break;}}fclose(fp);returnlines;}// get_ftext_len関数(ポインタ[len]にファイルの行ごとのバイト長を格納する)intget_ftext_len(int*len,constchar*fname){FILE*fp=fopen(fname,"rb");intc;inti=0;intbyt=0;//バイト数のカウント用intbyt_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にコピー}elseif(c==EOF){if((byt-byt_tmp)<=1){//最後が改行で終わっている場合len[i]=0;}else{len[i]=byt-byt_tmp;//最後が改行で終わっていない場合}break;}}fclose(fp);return0;}

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

sample.c
intget_ftext(char**str,constchar*fname,intlines,int*len);

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

さいごに

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


Viewing all articles
Browse latest Browse all 757

Trending Articles