プログラミング序論 page12(update:2017/07/06)
[ index | prev | next ] まとめ C言語の配列

12. 配列(array)

教科書6章配列
沢山のデータを扱うプログラムを作るために、配列が用意されています。1次元配列は使えるようになってください。

C言語の配列は,同じ型のデータをメモリアドレス順に連続して記憶します。配列の要素を参照するために,添え字演算子を用意しています。添え字演算子は先頭のメモリアドレスから添え字の要素数だけ移動したメモリを要素型の値を記憶したメモリとして参照します。

12-0. 構造を持つデータ型

基本のデータ型

基本のデータ型(base type)はこれ以上分解できないような1個のデータを記憶する型でした。しかし、実用的なプログラムでは大量のデータを扱う必要があったり、幾つかのデータをまとめて初めて意味のあるデータがあったり 。基本のデータ型だけでは不便です。

 そこでC言語では複数のデータをまとめた、構造を持つデータ型を作る機能が用意されています。

構造を持つデータ型


 

12-1. 配列の定義とメモリー割り当て

C言語には連続したメモリー領域を(その先頭アドレス添え字演算子を使って)配列として利用する仕組みが在ります。

12-1-1. 1次元配列(1-dimensional array)

配列は同じ型のデータをメモリーのアドレス順に一列に並べたもので、個々のデータを要素(element)、データの数を要素数 (number of elements)といいます。

配列の定義

配列の定義は要素のデータ型、配列名、要素数を合わせて

要素の型 配列名[要素数];

のように定義します。この定義の結果として、配列の全要素を記憶するための連続したメモリー領域が割り当てられます

※C90では要素数はコンパイルの時点で値が確定している定数や定数式でなければいけません。C99からは要素数に変数が使える可変長配列が使えるようになりましたが、対応していない処理系も珍しくありません。可変長配列はここでは扱いません。

全要素を記憶するために必要なバイト数は

要素1個のバイト数×要素数

となります。例えばint型の要素数1000個の配列 aを定義する場合

int a[1000];

の様に記述すると、int型は4バイト使うので4000バイトの連続したメモリーが配列 aに割り当てられます。

※配列の各要素の参照には次に示す添え字によるアドレス計算を行うため、隙間なく連続したメモリ領域を割り当てる。

※配列aに対してsizeof演算子を用いたsizeof(a)のような記述を行うと、コンパイルの時点で配列領域のバイト数に置き換えてくれる。

要素の参照(値の代入や読み出し)

個々の要素は配列の先頭から順番に付けた番号(添え字:subscript)で区別されます。

要素の参照(値の代入や読み出し)は、次のように記述します。

a[123]=10;
printf("%d",a[123]);

注意:
ここで、要素はa[123]みたいに書くのかと、安易に納得しないでください。[ ]は実はメモリーアドレスをもとに、メモリーの値を読み書きするための演算子です。aや123などは演算子のオペランドなのです。

[ ] :添字演算子(subscript operator)

[ ]添字演算子といいます。この演算子は以下のように先頭アドレス と 添え字 を基にメモリーの参照を行います。

先頭アドレス [ 添え字 ] 

先頭アドレスから要素のバイト数を考慮して、次の計算式で算出されたアドレスのメモリーを参照します

参照するメモリーのアドレス = 先頭アドレス要素1個のバイト数×添え字

このような仕組みの関係で配列要素の参照に使う場合、添え字の範囲は 0〜要素数-1 までが正しい範囲です。例えば、先の配列 a[1000] の場合の各要素に対する添え字は次のようになる

 int a[1000]; 整数1000個の配列
a[0]
a[1]
a[2]
a[3]
a[4]
a[5]
..
..
..
a[998]
a[999]
1個目 2個目 3個目 4個目 5個目 6個目 999個目 1000個目

配列に割り当てられた連続したメモリー 
  低<=アドレス=>高

※アドレスが与えられれば添え字演算子で自由にメモリー参照が可能です

#include <stdio.h>
int main(void)
{
    int a=10;
    (&a)[0]=20;/*変数aのアドレスに20を代入*/
    printf("%d",a)
    return 0;
}
/*
20
*/

のようなプログラムも書けてしまいます。

※添え字の値は自由なので、配列 int a[1000]に対してa[-1]とかa[2000]とかのメモリーを読み書きすることもできます。でも、このメモリーはほかのことに使われていたりするので、書き換えると何が起きるかわかりません。

配列名は配列の先頭アドレスを示す定数

添え字演算子の説明からわかるように、コンパイラは配列名を配列の先頭アドレスの値を取る定数(リテラル)として扱います。

int a[3];の様に定義された配列が在るとき、定数aの値は配列の先頭アドレスです。そこから添え字で参照するa[1]の様な要素はメモリーが割り当てられた変数です。変数であるa[0]には値をa[0]=10の様に代入できますが、定数であるaには値を代入できません。a=10のような代入はコンパイルエラーとなります。

配列とその要素のアドレスと値を書き出すサンプルプログラムを次に示す。
ここで aの値はアドレス&a[0] と同じであることに注意。

#include<stdio.h>
int main(void)
{
    int a[3];
    a[0]=10;
    a[1]=20;
    a[2]=30;
    /*a=10; はコンパイルエラーになる*/
    printf("配列aの番地は%X\n",a);
    printf("要素a[0]の番地は%Xで値は%d\n",&a[0],a[0]);
    printf("要素a[1]の番地は%Xで値は%d\n",&a[1],a[1]);
    printf("要素a[2]の番地は%Xで値は%d\n",&a[2],a[2]);
    return 0;
}
/* 実行例
配列aの番地は18FA98
要素a[0]の番地は18FA98で値は10
要素a[1]の番地は18FA9Cで値は20
要素a[2]の番地は18FAA0で値は30 <<a[0]〜a[2]の番地が4バイトずつ異なることも確認できる
*/

※C言語ではアドレスを記憶する変数も作れます。この変数のデータ型をポインターといいます。

配列の利用で注意すること

添え字の下限は 0

a[1]は先頭ではなくて2番目の要素です

#include<stdio.h>
int main(void)
{
    int a[3]={1,2,3};

    printf("a[0]=%d\n",a[0]); 
    printf("a[1]=%d\n",a[1]); 
    printf("a[2]=%d\n",a[2]);

    return 0;
}

添え字の上限は 要素数-1

C言語の初心者は1000個の要素を持つ配列 int a[1000]; を利用するときにa[1000]=10;の様な代入を行いがちです。しかし、これは配列用に割り当てた範囲の外のメモリへの代入とな ります。PascalやJava言語では実行時に添え字の範囲をチェックするのですが、C言語ではチェックが省かれているので配列の外のメモリーに書き込みます。この間違いは重大なエラーになりがち です。
 int a[1000]; 整数1000個の配列
a[0]
a[1]
a[2]
a[3]
a[4]
a[5]
..
..
..
a[998]
a[999]
a[1000]
1個目 2個目 3個目 4個目 5個目 6個目 999個目 1000個目 1001個目
配列に割り当てられた連続したメモリー
範囲外の
メモリー

暴走するプログラム例
#include<stdio.h>
int main(void)
{
    int a[3]={1,2,3};

    printf("a[3]=%d\n",a[3]); 
    printf("a[-1]=%d\n",a[-1]);
    a[3]=10;/*範囲外に書き込んだ結果 returnで戻る場所を書き換えてしまう*/
    printf("a[3]=%d\n",a[3]);
    fflush(stdout);/*ここまでは実行されるが*/

    return 0;/*正常にリターンできない*/
}


次の図のようにスタック上に割り当てた配列aの高位番地側にはreturnで戻る番地が書かれている。この場所をa[3]=10で書き換えてしまうとreturnで10番地に制御が移動し、そこに書かれたデータを機械語と見なし実行する。この為、なにが起こるか解らない重大な実行時エラーとなる。


要素数の多い配列は注意

要素数が多い配列はメモリーを大量に必要とする。大きな配列(要素数が百万程度)を自動変数で作るとスタックが溢れる場合があります。大きな配列は静的変数として静的領域のメモリーに割り当てるか、メモリ確保を行う関数でヒープ領域に割り当てるかすること。

#include<stdio.h>

int main(void)
{
    static int a[1000000];/*メモリーを4MB使う、要素数100万の整数配列、静的変数なら実行可能*/
    /*int a[1000000]; 自動変数にすると実行時エラーとなることがある*/
    int i;
    for(i=0;i<1000000;i++)a[i]=i;
    printf("END");
    return 0;
}

12-1-2  2次元配列 (2-dimensional array)

配列の配列も作れる。

2次元配列の定義

次のような形で定義する。

要素の型 配列名行数][列数

この場合も「要素1個のバイト数×要素数」の大きさの連続したメモリ領域を割り当てるのは1次元配列と同じ。ただし、この要素数は行数×列数となる。例えば2次元配列mを

int m[2][4]={
    {10、20、30、40},
    {50、60、70、80}
};/*8個の要素を持つ2次元配列で、このように要素を並べて初期化できる*/

と定義すると、各要素はメモリーアドレス順に次のように1列に並べられる。

m は1次元配列2個で構成された2次元配列
m[0] は整数4個の1次元配列 m[1] は整数4個の1次元配列
m[0][0]
m[0][1]
m[0][2]
m[0][3]
m[1][0]
m[1][1]
m[1][2]
m[1][3]
1個目 2個目 3個目 4個目 5個目 6個目 7個目 8個目
10
20
30
40
50
60
70
80

※メモリーはアドレス順に1次元に並んでいるので2次元のデータを1次元に展開してメモリーと対応付けている。

このように展開するので、mは「整数4個の1次元配列」が2個の配列になっている。

要素の参照

要素の参照は、添え字が2つ在るので次のように記述します。

m[0][0]=10;
printf("%d",m[1][2]);

2次元配列の場合も要素の参照は添え字演算子で行います。

添え字演算子の結合規則は左から右で m[ i ][ j ]  (m[ i ]) [ j ] と等価です。

先に計算される。m[ i ] 1次元配列の先頭アドレスです

 m[ i ] =mの先頭アドレス +   1次元配列のバイト数   × i( i 行目 )
        
=mの先頭アドレス +  要素のバイト数×列数  × i( i 行目 )

さらに後半の添え字のアドレスを加算して、

m[ i ][ j ]で参照するアドレスは、

 参照アドレス
mの先頭アドレス +  要素のバイト数×列数× i 要素のバイト数× j(j 列目)
=mの先頭アドレス +  要素のバイト数×( 列数×i  + j ) 

となります。
従って、int m[2][2];の様な2次元配列ではmとm[0]の値はアドレス&m[0][0]に等しくなります。

※2次元配列の要素を参照するには、列数(1次元配列の要素数)が解っていることが必要です。このことは配列の初期化や関数の引数として配列を渡すときに注意しなければいけません。

※mは定数です。m[ i ]のようなアドレスは実行中に算出される値です。従って、mやm[ i ] は変数ではないので値を代入することはできません。

※コンパイラはmは2次元配列の先頭アドレス、m[0]は1次元配列の先頭アドレス、&m[0][0]は整数データの先頭アドレスとして区別して扱います。添え字演算子はこのような違いを考慮して要素を参照する。

メモ: 2次元配列類似のデータ構造

ポインターを使って2次元配列と類似したデータ構造を作れます。要素を参照する記述は同じ形になるのですが、ここで説明した2次元配列とは別物です。以下に例を示します。

#include<stdio.h>
#include<stdlib.h>
int main(void)
{
    /*ポインターのポインター型変数mを定義*/
    int **m;

    /*int型変数のアドレス2個を記憶するメモリーをヒープから確保しアドレスをmに代入*/
    m=(int**)malloc(2*sizeof(int*));

    /*ヒープから整数4個分の連続したメモリーを確保し、
      上記のアドレスの記憶場所にそのアドレスを記憶する*/
    m[0]=(int*)malloc(4*sizeof(int));
    m[1]=(int*)malloc(4*sizeof(int));
    
    /*以降は2次元配列と同じ添え字演算子で要素を参照できる。
      しかし、メモリーの割り当て方は2次元配列とは異なる*/
    m[0][0]=10;m[0][1]=20;m[0][2]=30;m[0][3]=40;
    m[1][0]=50;m[1][1]=60;m[1][2]=70;m[1][3]=80;
    
    printf("%d",m[1][2]);
    return 0;
}

※上記プログラムの、mはアドレスを記憶するポインタ変数です。この変数の値の番地には、さらにint型の変数のメモリー番地が記憶されます。2段階で番地を指すポインターなので「*」が2個付いています。この変数mに、malloc関数を呼びだしてアドレスを2個記憶できるメモリー領域をヒープ領域から割り当て、その先頭アドレスを代入します。

続いて、この2個の要素m[0]とm[1]に、malloc関数を呼びだしてさらにint型を4個記憶できるメモリー領域をヒープ領域から割り当て、それぞれ代入します。

これで2次元配列類似の構造は完成です。以後は添え字演算子を使って2次元配列と同じように要素を参照できます。

 

参照するときの添え字演算子を2個使った記述は同じ形ですが,mはint型の値を記憶する番地を記憶した番地を記憶したポインター型の変数,aは2次元配列の先頭番地を示す定数として添え字演算子が区別して扱うので,どちらも正しい参照を行えます。

12-1-3 多次元配列(multi-dimensional array)

3次元以上の配列も同様に作ることができて、要素はメモリー上に一列に並んだ形で割り当てられる。

char t[2][2][2]={{{'A','B'}{'1','2'}},{{'C','D'}{'3','4'}}};

3次元配列なら2次元配列を要素とする配列なので以下のようになります

t[0] t[1]
t[0][0] t[0][1] t[1][0] t[1][1]
t[0][0][0]
t[0][0][1]
t[0][1][0]
t[0][1][1]
t[1][0][0]
t[1][0][1]
t[1][1][0]
t[1][1][1]
1個目 2個目 3個目 4個目 5個目 6個目 7個目 8個目
'A'
'B'
'1'
'2'
'C'
'D'
'3'
'4'

 

12-2. 配列の初期化

静的変数は自動的に0で初期化されるが自動変数は自動的には初期化されない。下記の様な書式を用いて初期化を指示できるが、自動変数の場合は処理系依存で初期化できない場合もある。

1次元配列の初期化

要素の型 配列名要素数 = { 要素の値, 要素の値, 要素の値 }

ここで、値は最初の要素から順に初期化されるが、指定した数が足りない場合は残りを0で初期化する。

例えば

int a[10]={100,80,50,200,30} 

の様な初期化が指定された配列の場合
 
a[0]
a[1]
a[2]:
a[3]
a[4]
a[5]
a[6]
a[7]
a[8]
a[9]
100 80 50 200 30

のように初期化されます

要素数を省いた初期化も可能です

要素の型 配列名[ ]= { 要素の値, 要素の値, 要素の値, 要素の値 }

この場合は初期化で示された値の個数が要素数となる。

例えば

int a[]={1,2,3};

の様な初期化が行われた場合
a[0]
a[1]
a[2]
2 3

のように要素数3の配列が作られます。

2次元配列の初期化 

省略できるのは最初の要素数のみ。添え字からのアドレスの計算のため2番目以降の要素数は省略できない。

例:

int n1[2][3]={{1,2,3},{4,5,6}};
int n2[ ][4]={{1,2,3,4},{5,6,7,8}};

/*コンパイル・エラーとなる記述 
 多次元配列の初期化において2つ目以降の要素数は省略できない 
int e1[ ][ ]={{1,2,3,4},{5,6,7,8}};
int e2[2][ ]={{1,2,3,4},{5,6,7,8}};
*/ 

12-3. 配列を使うプログラム例

配列を使うことで多数のデータを処理するプログラムが記述可能になる。このような多数のデータを処理する簡単な例を以下に示す。

12-3-1. 1次元配列を使うプログラム例

要素の並べ替え

例は単純な並べ替え手法をプログラムにしたものです。要素数が少ない場合はこれでも構いませんが、計算時間が要素数の2乗に比例するので、要素数が多い場合は「アルゴリズムとデータ構造」で学ぶもっと効率的な手法を使ってください。

/*配列要素の並べ替え*/
#include<stdio.h>
int num=10;
int list[10]={ 1, 5, 2, 4, 9 ,6, 3, 7, 8};/*初期値を9個しか示さなかったので10個目は0で初期化される*/

void exchenge(int i,int j)/*配列の添え字iとjの要素値を入れ換える*/
{
int tmp;
tmp=list[i];
list[i]=list[j];
list[j]=tmp;
}

int main(void)
{
int i,j;
printf("sort前 \n");
for(i=0;i<num;i++) {
printf("%d ",list[i]);
}
printf("\n"); /*並べ替え*/
for(i=0;i<(num-1);i++) {
for(j=i+1;j<num;j++) {
if(list[i]>list[j]) {
exchenge(i,j);
}
}
}
printf("sort済み\n");
for(i=0;i<num;i++) {
printf("%d ",list[i]);
}
printf("\n");
return 0;
}
/*実行結果
sort前 
1 5 2 4 9 6 3 7 8 0
sort済み
0 1 2 3 4 5 6 7 8 9
*/

ベクトルの内積

簡単な例としてベクトルの内積計算をプログラムにしてみました。下記は3次元のベクトルですが、工学では数千、数万次元のベクトルを扱うことも必要になります。

※2次元や3次元のベクトルは配列ではなくて構造体を用いる方が適当かもしれません

/*ベクトルの内積*/
#include<stdio.h>
double u[3]={ 1.0, 1.5,-2.0},
v[3]={-2.5, 0.5,-3.0};
int main(void)
{
int i;
double sum;
printf("u=(%f, %f, %f)\n",u[0],u[1],u[2]);
printf("v=(%f, %f, %f)\n",v[0],v[1],v[2]); /*内積*/ sum=0.0;
for(i=0;i<3;i++)
sum+=u[i]*v[i];
printf("u・v=%f\n",sum); /**/
return 0;
}
/*実行結果
u=(1.000000, 1.500000, -2.000000)
v=(-2.500000, 0.500000, -3.000000)
u・v=4.250000
*/

12-3-2. 2次元配列を使うプログラム例

行列計算

行列を2次元配列で実装し、行列とベクトルの積を計算してみました。

/*ベクトルと行列の積*/
#include<stdio.h>
double u[3]={ 1.0, 1.5,-2.0};
double v[3];
double m[3][3]={
{1,2,3},
{2,1,4},
{3,4,1}
};
int main(void)
{
int i,j;
printf("u=(%f, %f, %f)\n",u[0],u[1],u[2]);
printf(" |%f, %f, %f|\n",m[0][0],m[0][1],m[0][2]);
printf("M=|%f, %f, %f|\n",m[1][0],m[1][1],m[1][2]);
printf(" |%f, %f, %f|\n",m[2][0],m[2][1],m[2][2]); /*v=u*mを計算してみた*/
for(i=0;i<3;i++) {
v[i]=0;
for(j=0;j<3;j++) {
v[i]+=u[j]*m[j][i];
}
}
printf("uM=(%f, %f, %f)\n",v[0],v[1],v[2]);
return 0;
}
/*実行結果
u=(1.000000, 1.500000, -2.000000)
|1.000000, 2.000000, 3.000000|
M=|2.000000, 1.000000, 4.000000|
|3.000000, 4.000000, 1.000000|
uM=(-2.000000, -4.500000, 7.000000)
*/

連立方程式

連立一次方程式を解くプログラム例。手法は単純なガウス・ジョルダン法。

※連立方程式の未知数の数をNとすると、メモリーは係数行列aを格納するためにNの2乗個の要素が記憶できる量が必要です。計算時間はNの3乗に比例します。この結果、未知数が1000個程度までは演習室のパソコンでも解けますが1万個になると難しいでしょう。

#include<stdio.h>
#include<math.h>

/*テストデータ*/
int length=4;
double a[4][4]={
    {1.0,  1.0,  1.0,  1.0},
    {1.0,  2.0,  3.0,  4.0},
    {1.0,  4.0,  9.0, 16.0},
    {1.0,  8.0, 27.0, 64.0}
};
double b[4]={1.0, 2.0, 3.0, 4.0};

/*プロトタイプ宣言*/
void Gauss_Jordan(int);

/*テスト用main関数*/
int main(void){
    int i,j;
    Gauss_Jordan(length);
    for(i=0;i<length;i++){
        for(j=0;j<length;j++)
            printf(" %lf*x[%d]",a[i][j],j);
        printf("= %lf",b[i]);
        printf("\n");
    }
    return 0;
}

/*テストプログラムの実行結果
1.000000*x[0] 0.000000*x[1] 0.000000*x[2] 0.000000*x[3]= -0.833333
0.000000*x[0] 1.000000*x[1] 0.000000*x[2] 0.000000*x[3]= 3.000000
-0.000000*x[0] -0.000000*x[1] 1.000000*x[2] 0.000000*x[3]= -1.500000
0.000000*x[0] 0.000000*x[1] 0.000000*x[2] 1.000000*x[3]= 0.333333
*/



/*連立方程式 ax=b を解く
* 手法は部分ピボット選択付きGauss-Jordan法
*
*前提条件:
* aは2次元配列、bは1次元配列として
* 関数外に定義されているものとする。
* 方程式の行数は引数numで渡される。
*実行結果:
* bに解xが代入される。
* 副作用として、aは単位行列に書き換えられる。
*/
void Gauss_Jordan(int num)
{
    int i,j,k,max;
    double p,tmp; 
    for(i=0;i<num;i++){

        /*部分ピボット選択*/
        /*1) i列の値が最大の行をi+1行目以降から探し、何行目かをmaxに記憶*/
        max=i;
        for(j=i+1;j<num;j++){
            if( fabs(a[j][i]) > fabs(a[max][i]) )
            max=j;
        }

        /*2) i行とmax行が異なる場合はi行とmax行を入れ替える*/
        if(i!=max){
            for(k=i;k<num;k++){
                tmp=a[i][k];
                a[i][k]=a[max][k];
                a[max][k]=tmp;
            }
            tmp=b[i];b[i]=b[max];b[max]=tmp;
        }

        /*消去法*/
        /*1)i行目のピボットa[i][i]を1にするため、方程式のi行目をa[i][i]で割る*/
        p=a[i][i];
        for(k=0;k<num;k++){a[i][k]/=p;}
        b[i]/=p;

        /*2)i行目の何倍かを他の行から引いて、他の行のi列の係数を全てゼロにする*/
        for(j=0;j<num;j++){
            if(i!=j){
                p=a[j][i];
                for(k=0;k<num;k++){a[j][k]-=p*a[i][k];}
                b[j]-=p*b[i];
            }
        }
    }
    return;
}

12-4. 文字列の扱い

文字列を配列に格納する方法と注意点について以下に述べる。

9-4-1.文字列

C言語の文字列はメモリ上の表現が下の様に文字コードの並びと文字列の終端を示す0で構成されている。  "ABC"の場合

XXX0番地 XXX1番地 XXX2番地 XXX3番地
'A'
65
'B'
66
'C'
67
'\0'
0

C言語では文字列の終端を0で示すルールが使われるので、文字列中にコード0を含むことはできない。他の言語 では専用のデータ型を用意することが多い。例えば、BASIC言語の文字列では文字数をデータとして持つ表現方法が使われることがある。

12-4-2 文字配列

文字配列を利用して文字列と同じデータ構造をメモリー上に作ることが可能である。このとき、文字列の終端を0で示すため文字配列の要素数は文字コード分のバイト数+1個が最低でも必要になる

書式

 char 配列名[要素数];

文字列で初期化する場合は要素数を省いて次の様にすると簡単である。
 
 char 配列名[ ]="文字列";

この場合配列の要素数は文字列の終端を示す\0を含むため、文字 コード分+1となる。
要素数を指定することも可能で

 char 配列名[要素数]="文字列";

のような記述もできる。ただし、1文字に使われるバイト数は文字コード依存なので単純に文字数ではないことに注意しよう。


漢字などの文字コードは色々有る、コードにより必要なバイト数が異なる。文字コードがSJISなら”文字列”は漢字1文字2バイトで6バイトと終端の0を入れる1バイトが必要で計7バイトで記憶される。1バイト使う文字と2バイト使う文字が混在する場合に、その境目を示すコードを挟む形式の文字コードもある。この様に,様々な仕組みの文字コードが存在している。

文字コード分のバイト数を求める標準関数がある。標準関数のところで説明する。
size_t strlen(char *s) /*利用にはstdlib.hのインクルードが必要*/

文字を1個づつ指定して初期化することも、もちろんできる。

char 配列名[ ]={ 'A', 'B', 'C', 0 };

この場合は文字列として使うためには、最後を0(あるいは'\0')にすることを忘れてはいけない。

文字列の入力:

文字配列にscanfを使って標準入力から文字列を取り込める。しかし、文字列を書き込む配列の大きさに注意しないとバッファーオーバーランを起こしてプログラムが暴走する原因となる。

#include<stdio.h>
....

    char buffer[128];/*文字列を記憶する場所を用意する*/

    scanf("%s",buffer);
    /*
    この関数はbufferに文字列を読み込むが、
    bufferの大きさを超える読み込みをチェックしないので127文字以上を読み込んで
    bufferの外のメモリーまで書き換えるバッファーオーバーランの危険性がある。
    */

    scanf("%127s",buffer);
    /*
    書式指定子で読み込む文字数を制限した書き方。
    文字列終端の'\0'を書き込むので取り込む文字数は最大で127文字まで
    */
....

 

12-4-3. 文字列の配列

2次元の文字配列を用いて複数の文字列を格納可能である。しかし、文字列は長さがまちまちで2次元の文字配列に格納すると次の例のように2番目の要素数は最も長い文字列 に合わせなければならない。これは非常に無駄が多い。

※例に示すような、ポインタの配列を使った効率の良い格納法が在るが、それは後期の序論演習で説明予定

例 

char st[][13]={"one","two","three","Hello World!"};//2次元文字配列
char* list[] ={"one","two","three","Hello World!"};//文字型ポインタの配列   

12-5. 関数の呼び出しにおける配列データの渡し方

C言語では配列を関数へ引数で渡すことはできないし、戻り値としても使えない決まりです

配列自体は仮引数にできません。しかし、呼び出す関数に配列データを教える方法は在ります。それが配列の先頭アドレスを仮引数で渡す方法です。

配列のアドレスを引数で渡す

引数にアドレスを記憶するポインター変数を使いますが、ポインターは後期に説明予定なので、ここでは使いかただけを示すことにします。

例題:1次元配列の要素の最大値を戻す関数

配列のデータを関数へ渡す次の例題ではmax関数の仮引数bが配列のように記述されています。実はこの仮引数bはポインター変数と言ってアドレスを記憶する変数です。 仮引数bの値がアドレスなので、例題のように呼ばれた側では添え字演算子が配列と同じように使えます。

#include<stdio.h>

/*
*1次元配列の要素の最大値を戻す関数
*仮引数は b:配列の先頭アドレス length:配列の要素数 *アドレスが渡されるので仮引数bに対して添え字演算子が使える */
int max(int b[],int length)/*このように書いてもbは配列ではない*/
{
int i,m;
m=b[0];
for(i=1;i<length;i++)
if(m<b[i]) m=b[i];
return m;
}
int main(void)
{
int a[]={1,2,3};
printf("int a[]={1,2,3};\n");
printf("max=%d\n",max(a,3));/*配列aの先頭アドレスと配列の要素数を引数で渡す*/
return 0;
}

アドレスを渡す場合の注意点

中身を知らないと配列を仮引数で渡すように見えるのですが、本当は違うのです。

本当に、配列を仮引数で渡すとすれば、配列の中身を全部コピーして仮引数を作ることになります。このようなコピーはメモリーが沢山必要なだけでなくコピーにも時間がかかります。C言語では、この重い処理を避けるために配列を仮引数で渡すことはしません。

代わりとして、int b[ ]のような仮引数の記述で、int型のデータを格納したアドレスを記憶する変数とし、関数を呼ぶときに仮引数をbに配列の先頭アドレスを代入できるようにしました。bはアドレスなので添え字演算子を使ってのメモリー参照が可能になります。例えば、a[0]とb[0]は同じメモリーを参照するので配列aの中身a[0]を関数max側でb[0]として読み取ることができます。

ここで注意が必要なのは、関数maxでb[0]=10のような代入を行うとa[0]の値も10に変わることです。仮引数をbを作るときに配列aの中身を全てコピーしていればa[0]とb[0]は別のメモリーでしたが、先頭アドレスだけを仮引数で渡しているのでa[0]とb[0]は同じメモリーなのです。

次の図のように、a[0]b[0]は同じメモリーを参照します。

呼ばれた側で要素の書き換えが可能
#include<stdio.h>
void F(int a[])
{
    a[1]=10;/*これはn[1]=10;と同じ結果になる*/
    return;
}
int main(void)
{
    int n[]={0,1,2,3};
    F(n);/*関数Fを呼んだことでn[1]の値が1から10に変わる*/
    printf("%d",n[1]);
    return 0;
}

このような書き換えを予防したいときは,仮引数に定数であることを指定するconstを付ける。constを付けることでa[1]=10;の様な記述をコンパイラがエラーにしてくれる。しかし,それを迂回して書き換えることもできる。

void F(const int a[])
{
    a[1]=10;<<ここでコンパイラがエラーを出してくれる。
    return;
}

悪意が有る場合,以下のように書き換えてエラーを迂回できます

void F(const int a[])
{
    int *p;
    p=(int *)a;
    p[1]=10;/*VisualStadioのコンパイラはここまでチェックできない*/
    return;
}

 

多次元配列の場合

多次元配列では2番目以降の要素数は重要で省略できない。省略できるのは最初の添え字の要素数のみです。 なぜなら、a[i][j]のような参照をコンパイラが機械語に翻訳するときに2番目以降の要素数が必要だからです。

※ 2次元配列の要素の参照を見てもらえば分かります

/*
*2次元配列の要素を出力する関数
*ただし2番目の要素数が2の場合に限られる
*ポインタを使うともっとうまい方法が可能になる
*/
#include<stdio.h> /*  2次元配列の先頭アドレスを代入した仮引数aが渡される。  添え字演算子でint型要素のアドレスを求めるのに2番目の要素数が必要になる */
void print(int a[][2],int row)/* 仮引数aは配列int[2]のアドレスを示すポインタ変数 */
{
int i,j;
for(i=0;i<row;i++) {
for(j=0;j<2;j++) {
if(j!=0)printf(", ");
printf("%d",a[i][j]);
}
printf("\n");/*改行*/
}
}
int main(void)
{
int m[][2]={{1,2},{2,3},{3,4}};
print(m,3);
return 0;
}
/*実行結果
1, 2
2, 3
3, 4
*/

※教科書の例は131pリスト6_12

課題 11 (初期ファイルp11.c)

関数sumは1次元配列の全要素の値を合計して戻す関数です。プロトタイプ宣言に記述されているように引数の1番目は整数型の一次元配列の先頭アドレス、2番目は配列の要素数です。main関数にはsumの動作をテストするプログラムが書かれています。

初期ファイルにあるようにsumの関数定義を書き足してください。プログラムを実行したときの合計値がコメントに記述した値と同じになること。

#include<stdio.h>
int sum(int[], int); /*第一引数は配列 第二引数は配列の要素数*/
int main(void)
{
    int table[10]={95,80,90,80,90,75,90,70,90,65};
    int table2[5]={99,98,97,96,95};
    printf("合計値=%d\n",sum(table,10));
    printf("合計値=%d\n",sum(table2,5));
    return 0;
}
/*実行結果
合計値=825
合計値=485
*/

/*以下に関数sumの定義を記述して欲しい*/
/* Write the code of function sum*/

[ index | prev | next ]