第1章 配列とポインタの基礎
まず、簡単なデータ構造を持つ配列について説明します。配列は同じデータ型のデータが要素となっている集合データであり、個々のデータである配列の要素は順番を示す添字(そえじ)で区別されます。添字には式を用いることができるため、個々の要素を実行時に指定することができ、繰り返し処理が可能となります。つまり、複数個のデータを1個の配列として定義できると、繰り返し処理が可能となってプログラムを簡潔に記述できると同時に、変更や拡張にも強いプログラムを作成できるようになります。
一方、C言語の大きな特徴の一つであるポインタは、考え方において配列と強い関連を持っています。関数などを利用して配列を活用するには、ポインタを正しく使用できなければなりません。
そこで、この章では、配列とポインタに関しての要点を述べることにします。特に大切な部分と間違いやすい部分を詳しく説明します。以降、正しいプログラムを作成できるようになるための導入部となります。
なお、配列の定義方法や個々の要素の指定方法、ならびに、ポインタの定義方法や間接指定などについては十分に知っているものと仮定します。
1 配列に関する注意点
C言語では、配列も通常の変数と同様に定義あるいは宣言してから使用します(データ用の記憶領域を実際に確保する場合を定義といい、実際に記憶領域の確保は行わないが、他の場所で定義されていることをコンパイラに知らせることを宣言ということにします)。
配列は集合データですから、実際にデータを取り扱うときは、配列を構成している個々のデータをまえもって定義しておかなければなりません(配列を構成している個々のデータを要素といいます)。要素の指定には、データが並んでいる順序を意味する番号が使用されます(この番号を添字といいます)。
配列を定義するときは、要素の個数を指定しなければなりません。たとえば、
int a[5];
によって定義された配列は、5個の整数型データを要素として持つ配列です。なお、定義において指定できる要素の個数には式を記述してもかまいませんが、コンパイルの時に確定できる値でなければなりません。たとえば、
#define LENGTH 10
char string[LENGTH + 1];
は、記号定数LENGTHがコンパイル時に確定した値(この場合は10)であるため、誤りではありません。しかし、
int length;
scanf("%d", &length);
char string[length];
では、個数を表す変数lengthの値が、実行時に入力された値として確定し、コンパイルの時には個数が確定しないので誤りになります。
配列を定義するときは要素の個数を用いますが、個々のデータを指定する際には、0番目から要素数-1番目までの順番を表す番号が用いられます(配列の要素は必ず0番目から始まります)。たとえば、4番目の要素はa[4]と記述されます。このように、各要素は配列名の後に何番目の要素であるかを表す定数や式を[と]で囲った形式で指定されます。この[と]で囲まれた、個々の要素を指定する式を添字といいます。添字には結果が整数型のデータとなる式が使用されます(定数も式を構成するもののひとつです)。このとき、添字の値の範囲チェックは行われません。たとえば、
int a[3];
のとき、a[0]、a[1]、a[2]は実際に記憶領域が確保された要素ですから、エラーも発生せず安心して使用できますが、変数kの値が3や4や-1などのときのa[k]は、コンパイル時にエラーが発生しないにもかかわらず、実際に記憶領域が確保されていない要素であるため、このような要素を用いると誤動作の原因となります(このような範囲外にデータを書き込むと、他の変数やプログラムの一部にデータが書き込まれ、場合によってはプログラムの暴走の原因となります)。添字の範囲を正しく管理することや、範囲のチェックを行うかどうかはプログラマの責任になります。
配列を定義するときは、コンパイル時に確定する要素の個数を指定しなければなりません。しかし、要素の個数と初期値の個数が同じであるときは、要素の個数を省略してもかまいません。たとえば
int b[] = {2, 5, 6, 4};
は初期値が4個指定されていますから、4個の要素を持つ配列とみなされます。
配列は添字の個数によって区別されることがあります。たとえば、添字が1個の配列を1次元配列といい、2個の配列を2次元配列といいます。添字の個数が3個以上の場合も同様です。また、添字の個数が2個以上の配列を多次元配列といいます。
配列はデータの集まりですから、一種の表を表しています。添字の個数によって表の種類が異なります。たとえば、1次元配列の場合のイメージは1列に並んだ表となります。また、配列においては、各要素がどのような順序で記憶領域に配置されるか(格納されるか)にも注意を払わなければなりません。1次元配列の場合のイメージと記憶領域での配置は次のようになります。
また、2次元配列の場合、配列は添字を2個持ちますから、一方の添字が横の変化を表し、他方の添字が縦の変化を表すとみなすと、縦横の表のイメージとなります。なお、記憶領域での配置は、後の添字が先に変化するように一列に並びます。
記憶領域においては、たとえば図1-2の場合、要素a[0][3]の直後に要素a[1][0]が配置されます。すなわち、2番目の添字の大きさが要素の個数4に等しくなったとき、その添字の大きさを0とし、左隣の添字の大きさを1だけ大きくしなければなりません。このようなコンパイラが実行する添字の計算においては、2番目の添字の最大値(すなわち要素の個数)がコンパイル時にわかっていなければなりません。一般に、多次元配列においては後の添字が先に変化するように配置されますから、2番目以降の添字の大きさを省略することはできません。
図1-2の場合、a[k][j]は記憶領域の3*k+j番目に配置される要素となります。このとき、この式で使用される定数3は2番目の添字の要素数です。1番目の添字の要素数は使用されていません。このことから、多次元配列の場合、1番目の添字の要素数を省略してもかまわない場合があります。多次元配列が記憶領域にどのように配置されるかを知ると、2番目以降の添字の大きさを省略できないことを理解できます。
配列を宣言する場合は、すなわち、他の場所で定義されている配列をコンパイラに知らせる場合は、最初の添字の大きさだけを省略することができます。たとえば、
extern a[], b[][4], c[][4][5];
のようになります。また、初期値を持つ定義の場合も同様です。たとえば、
int a[][2] = {{2, 4}, {5, 1}, {9, 3}};
の場合、2個の要素を持つ1次元配列を3個要素として持つ配列を定義しています。
配列の添字の大きさのチェックが行われませんから、図1-2の場合、a[0][5]はa[1][1]に等しくなります。また、a[1][6]はa[2][2]に等しくなります。したがって、a[0][k]やa[1][k]のように1番目の添字を固定値にすることによって、2次元配列を1次元配列のように取り扱うことができます。
C言語では、どのような配列も1次元配列として考えることができます。たとえば、2次元配列は1次元配列を要素とする1次元配列です。また、3次元配列は2次元配列を要素とする1次元配列です。具体的な例としては、
int a[3][4];
のとき、2次元配列aは、4個の要素を持つ1次元配列を3個だけ要素として持つ1次元配列です。

簡単な配列の使用例として、全部のデータを入力した後合計を求める例を次に示します。これは基本的なプログラム例で、ぜひ覚えてほしいものです。なお、合計を求めるだけであればデータが入力されるごとに累計を求めればよく、配列を使用する必要はありません。
【例1-2】配列を用いて合計を求める例
#include <stdio.h>
#define MAX 50
int main() {
int a[MAX], k, max;
long sum = 0;
printf("データの個数を入力:");
scanf("%d", &max);
if(max
printf("%d 個のデータを入力\n", max);
for(k = 0; k < max; k++)
scanf("%d", &a[k]);
for(k = 0; k < max; k++)
sum += a[k];
printf("合計は %ld\n", sum);
}
else
printf("データの個数は %d 個までです\n", MAX);
return 0;
}
配列を利用する場合、要素の個数を明確に指定して配列を定義しなければなりません。そこで、
#define MAX 50
によって個数を指定しています。この場合、50個までのデータを取り扱うことができます。もっと多い個数のデータを処理したいときは
#define MAX 500
のように変更します。配列の要素に記号定数を用いると、要素数の変更が非常に簡単になります。
まず50以下の整数値をデータの個数として入力し、次にその個数だけ整数型データをまとめて入力します。その後入力されたデータの合計を求めています。配列の添字には変数を用いることができますから、この例のようにループにすることができます。特に配列の場合添字は0から個数-1までですから、maxが個数を表しているとき、
for(k = 0; k < max; k++)
のような記述がしばしば用いられます。 [例終わり]
2 ポインタに関する注意点
プログラムで処理されるデータは記憶領域に格納されます。図1-2に示されているように、記憶領域のイメージは1次元配列と同じで、各データの存在位置を表す情報を用いて個々のデータを指定できます。このようなデータの存在位置を表す情報をアドレスといいます。アドレスは順番を意味する整数値ですから、処理データそのものとは異なったデータとして取り扱われます。
アドレスは、個々のデータの格納場所ごとに割り当てられた整数値の番号ということができます。この番号によって記憶領域が区別されます。通常プログラムを作成する際には、格納場所に対応付けられた変数名が用いられ、格納場所を直接指定するアドレスが利用されることはありません。実際、ほとんどのコンピュータ言語でアドレスを用いることはできません。しかし、高度なプログラムを作成するために作られたC言語では、アドレスを利用することが可能です。このため、高速に処理を行うプログラムや汎用的なプログラムを容易に作成することができます。しかし、アドレスの概念が理解しにくいため、C言語をマスターすることは必ずしも容易ではありません。なお、プログラムの実行のための命令もデータと同じ格納場所が用いられるため、アドレスの間違った使い方が原因で命令が書き換えられてプログラムが暴走することも珍しいことではありません。
計算機の内部の記憶領域にはデータの格納場所が大量に存在しています。通常8ビットの集まりであるバイトごとにアドレスが定められており、データの大きさに対応した1個または複数個のバイトを1個の変数に対応付け、さらにそのような格納場所の集まりを配列に対応付けています。なお、通常プログラムで変数や配列を定義したとき、各データがどのような大きさの、どこにある格納場所に割り当てられるかはコンパイラや実行時のシステムプログラムによって決められます。ユーザが変数などのアドレスを自由に指定することはできません。
変数や配列の要素にデータを代入することは、割り当てられた格納場所にデータを書き込むことを意味しています。また、変数や配列の要素を参照する(利用する)ことは、割り当てられた格納場所に書き込まれているデータを読み出して使用することを意味しています。
変数に対応付けられたデータの格納場所のアドレスを求めるには、変数名の直前に演算子&を付けます。たとえば
int a;
のとき
&a
によって変数aのアドレスを求めることができます。この値はコンパイラが処理したときに確定する場合もあれば、プログラムを実行するまで未定で、実行のためにプログラムが計算機の記憶領域に読み込まれたときに確定する場合もあります。したがって、一定の値となる場合も、一定の値とはならず、実行時の環境によって実行するごとに異なった値となる場合もあります。このような理由から、特殊な場合を除き、アドレスが実際にいくつの値なのかを問題にすることはありません。
整数値で表されるアドレスはデータの存在場所に関する情報です。したがって、アドレスはデータとして取り扱うことができます。このようなデータを格納するための変数をポインタ変数といいます。なお、ポインタ変数やアドレスを簡単にポインタということもあります。
ポインタ変数はデータの存在場所を表すアドレスをデータとして持つことができますので、ポインタ変数によって間接的にデータを指定することができます。間接的にデータを指定することをデータをポイントする(指し示す)といいます。たとえば、
int a = 3, *ap;
ap = &a;
のとき、変数apはint型データをポイントするポインタ変数として定義され、int型変数aのアドレスが代入されます。以降、ポインタ変数apを用いた間接指定*apはaと同じことを意味し、ポインタ変数apは変数aをポイントする(指し示す)といいます。なお、ポインタ変数を用いて間接指定を利用するときは、必ずポインタ変数にアドレスを代入してから使用します。そうでないと、偶然ポインタ変数の値が命令の存在する位置のアドレスに等しい場合、命令が書き換えられてプログラムが暴走する可能性があります。
ポインタ変数apに変数aのアドレス(&a)が代入されているとき、間接指定*apがaに等しく、ポインタ変数apの値は&aですから、結局*(&a)はaと同じことになります。すなわち、あるデータのアドレスを間接指定に直接用いると、そのデータそのものを表していることになります。また同様に、&(*ap)はapに等しくなります。
ポインタ変数はポイントするデータの種類によって区別されます。文字型データをポイントするポインタ変数pは、たとえば
char *p;
のように定義されます。同様に、単精度実数型データをポイントするポインタ変数qは
float *q;
のように定義されます。このとき、ポインタ変数pはchar*型であるといわれ、ポインタ変数qはfloat*型であるといわれます。このようなポインタ変数pとqは互いにデータの型が異なりますから、
p = q;
のように直接代入することはできません。どうしても代入しなければならないときは、型キャストによる強制的な型変換を行わなければなりません。
通常ポインタ変数にはデータの存在場所を示す情報であるアドレスが代入されて間接指定によって利用されますが、ポインタ変数も記憶領域を割り当てられた変数の一種ですから、さらにポインタ変数のアドレスを考えることもでき、たとえば
int *q;
と定義されているとき
&q
によってポインタ変数qのアドレスを求めることができます。この値のデータ型はint**型となりますから、この値をポインタ変数へ代入したい場合は、たとえば
int **q_p;
のように2個の*を用いて定義されたポインタ変数q_p(変数名はq_pでなくてもよい)を定義しなければなりません。ポインタ変数q_pは整数型データをポイントするポインタ変数へのポインタ変数ですから、定義において2個の*を必要とします。
【例1-2】間接指定の例
#include <stdio.h>
int main() {
int a = 5, *p, **q;
p = &a; /* データ変数のアドレス */
q = &p; /* ポインタ変数のアドレス */
printf("a = %d\n", a);
printf("p = %p\t*p = %d\n", p, *p);
printf("q = %p\t*q = %p\t**q = %d\n", q, *q, **q);
return 0;
}
ポインタ変数を利用した間接指定を確認するプログラムです。単純な間接指定をポインタ変数pで示し、2重の間接指定をポインタ変数qで示しています。なお、この例では参考としてアドレスの値を表示していますが、通常はアドレスの値がいくつであるかを問題とすることはありません。
なお、標準関数printfを用いてアドレスを表示したいときは、変換文字列として%pを利用することができます。このとき、アドレスは16進数で表示されます。[例終わり]
3 配列とポインタ変数の関係
個々の記憶領域を区別するために用いられるアドレスは整数と同じ2進数の値をとるデータです。したがって、アドレスを整数のように計算の対象とすることができます。しかし、アドレスは個々のデータの位置を表わしていますので、アドレスの計算結果がデータの境界からずれた値になっては困ります。たとえば、整数型データのアドレスを持つポインタ変数の値が整数型データを半分ずれた位置のアドレスを持ってしまうと、正しいデータを取り扱うことができなくなります。このため、アドレスを計算に用いるときは、通常の整数演算とは異なる計算が行われます。
配列においては、添字はデータの順番を意味しています。個々のデータ要素の大きさには関係なく、何番目のデータであるかを対象としています。たとえば、a[0]は配列aの0番目(最初)の要素を表し、a[1]はa[0]の直後の要素を意味しています。
一方、配列名はその配列の先頭アドレス(0番目の要素のアドレス)と解釈することになっています。たとえば、
int a[5];
と定義された配列aにおいては、配列名aはa[0]のアドレス、すなわち、&a[0]に等しいと解釈されます。アドレスは間接指定に利用できますから、*aはa[0]と同じことになります。
添字は配列の要素の順番を意味しており、配列における要素の位置を示しています。一方、アドレスは記憶領域の位置を表しています。位置を表すという意味では、よく似た性質を持っています。このため、アドレスも添字と同じように処理されるように決められています。たとえば、1次元の配列aにおいて、*aとa[0]は同じ要素であり、*(a+1)とa[1]は同じ要素を表しています。aはアドレスですから、a+1はアドレスの計算となります。アドレスを1増加することは、直後のデータのアドレスを求めることを意味しています。このような性質を一般的に表現すると、a+kはk個後のデータのアドレスを求めることを意味していますから、k番目の要素は*(a+k)やa[k]で表すことができます。
このようにデータ単位でアドレスの計算を行うものと決めているため、アドレス計算の結果がデータの途中のアドレスとなることがなく、同時に、ポインタと配列との関連性が明確になります。たとえば、aがポインタ(ポインタ変数か配列名)であるとき、間接指定の*(a+k)と配列指定のa[k]は同じものを意味しています(kは整数の式や定数)。
【例1-3】アドレス計算の例
#include <stdio.h>
int main() {
int a[] = {5, 7, 1, 4, 6};
int *a1p, *a2p;
a1p = &a[0];
a2p = a1p + 3;
printf("a[0] のアドレス %p\n", a1p);
printf("a[1] のアドレス %p %p\n", a1p+1, &a[1]);
printf("a[3] のアドレス %p %p\n", a2p, &a[3]);
printf("a[1] と a[0] のアドレスの差 %d\n",
(char*)(a1p+1) - (char*)a1p);
printf("a[1] と a[0] の 要素数の差 %d\n", (a1p+1) - a1p);
printf("a[3] と a[0] のアドレスの差 %d\n",
(char*)a2p - (char*)a1p);
printf("a[3] と a[0] の 要素数の差 %d\n", a2p - a1p);
return 0;
}
ポインタ変数に関する演算(アドレス計算)はポイントしているデータの大きさに対応して行われます。したがって、
a1p = &a[0];
a2p = a1p + 3;
の加算においては、a1pの値、すなわちa[0]のアドレスに3個後のint型データのアドレス、すなわちa[3]の値が結果として求められ、この値がポインタ変数a2pへ代入されます。このポインタ変数a2pの値と&a[3]が同じ値であることを確認できます。また、a1p+1の値、すなわち&a[0]+1と&a[1]が同じであることも確認できます。
ポインタに関する引き算の結果は何個離れているかを表すデータの個数になります。実際のアドレスの値の差を求めるには、この例のように型キャスト(char*)によって強制的な型変換を行って文字型データへのアドレスとして計算する必要があります。なぜなら、文字型データは大きさが1バイトですから、データの個数の差に等しい文字型データのアドレスの差は実際のアドレスの値の差を与えてくれるからです。 [例終わり]
ポインタ変数が指し示す位置には、データが1個だけ存在する場合もあれば、何個も並んでいる場合もあります。たとえば、
int a, *p = &a;
と定義されたとき、ポインタ変数pが指し示す位置には、変数aのデータのみが存在します。一方、
int a[5], *p = a;
と定義されたときは、ポインタ変数pが指し示す位置には、5個の要素を持つ配列aのデータが存在しています。あるポインタ変数がどちらの状況となっているのかわからない場合がありますが、プログラムを作る人がどちらにも解釈してもかまいません。
配列名はその配列の先頭アドレスですから、配列名をポインタとして間接指定に利用してかまいません。一方、ポインタ変数を配列名のように使用してもかまいません。たとえば、ポインタ変数pを用いて、*(p+5)の代わりにp[5]と記述することができます。このように、配列名とポインタ変数は使用上ほとんど差がありません。しかし、配列名は代入したり変更することができない定数として扱われますが、ポインタ変数は代入や変更が可能な変数であるという、重要な違いがあります。
関数の引数にポインタ変数を定義するときは、配列のように記述してもかまいません。たとえば、
void disp(int *p) { ... }
と
void disp(int p[]) { ... }
は同じことを意味しており、どちらも引数pはポインタ変数であることを示しています。なお、引数に渡される実際のデータ領域は別のところで定義されていますから、引数を配列で表して定義したとき、最初の添字の要素数を省略することができます。
void disp(int p[5]) { ... }
のように、要素数を記述したとしても無視されます。
1次元配列とポインタ変数の関係は簡単ですが、多次元配列の場合はやや複雑になります。たとえば、
int a[4][5];
と定義されているとき、**aはa[0][0]と同じであるため(**a→*(a[0])→a[0][0])、ポインタである2次元配列の配列名aをしばしばint **型のデータ型と間違います。2次元配列は要素として1次元配列を持ちますから(図1-3を参照)、配列名は1次元配列へのポインタ、この場合は、5個のint型データの集まりへのポインタであり、データの型としてはint (*)[5]型となります。すなわち、
int (*p)[5];
と定義されているポインタ変数pは、配列名aと同じデータ型となります。なお、a[0]やa[1]も配列名となり、この場合、ともにint *型となります。
同様に、3次元配列の配列名は2次元配列を要素とする配列の先頭アドレスを意味しています。したがって、たとえば
int b[4][5][6], (*q)[5][6];
によって定義された配列名bとポインタ変数qは同じデータ型となります。
関数の引数が配列として定義されたときも同様です。たとえば、
void disp(int a[4][5][6]) { ... }
として引数が定義されたとき、これは
void disp(int (*a)[5][6]) { ... }
と定義したものと同じになります。もちろん、最初の添字の要素数は省略可能ですから、
void disp(int a[][5][6]) { ... }
と定義しても同じです。
引数を利用して配列を関数に渡すとき、いくつかの注意が必要です。まず、添字の範囲のチェックが行われませんから、関数側で渡された配列の要素数を暗黙的に知る方法がありません。そこで、通常は要素の個数も引数として渡します。なお、文字列データのように終了記号を持つ配列などの場合は必ずしも要素数を渡す必要はありません。一方、配列が渡される引数の定義においては、最初の添字に関する要素数を指定しても無視されます。
【例1-4】配列が渡される関数の例
#include <stdio.h>
#define MAX 100
#undef DEBUG
#define DEBUG
#ifdef DEBUG
#define D(f) f
#else
#define D(f)
#endif
void disp(int a[], int max) {
int k;
for(k = 0; k < max; k++)
printf("%d%c", a[k], (k % 10 == 9)? '\n': '\t');
printf("%s", (k % 10 == 0)? "": "\n");
}
void sort(int a[], int max) {
int i, j, k, gap;
D(disp(a, max));
for(gap = max/2; gap > 0; gap /= 2)
for(i = gap; i < max; i++)
for(j = i-gap; j >= 0 && a[j] > a[k = j+gap];
j -= gap) {
int temp = a[j];
a[j] = a[k];
a[k] = temp;
D(disp(a, max));
}
}
void input(int a[], int max) {
int k;
for(k = 0; k < max; k++) {
printf("(%d) ", k+1);
scanf("%d", &a[k]);
}
}
int main() {
int a[MAX], max;
printf("データの数(%d個以下)を入力:", MAX);
scanf("%d", &max);
if(max <= MAX) {
input(a, max);
sort(a, max);
printf("\nソートの結果は\n");
disp(a, max);
}
else
printf("要素数は %d 個以下です\n", MAX);
return 0;
}
引数を利用して配列を関数に渡す例として、ソートのプログラムを示します。ソートとはデータを大きさの順に並べ変えることで、小さい方から大きい方へ並べる昇順ソートと、大きい方から小さい方へ並べる降順ソートがあります。ソートの種類は数多くありますが、この例に用いられたシェルソートは比較的簡潔な手順のわりに高速な処理が行われ、データ量が多くても実用的な方法です。
このプログラム例では機能ごとに関数を用いています。配列へデータを入力する関数、処理としてソートを行う関数、結果を表示する関数の3種類の関数が利用されています。このように機能ごとに関数を作成すると、プログラム全体が簡潔になって理解しやすくなり、誤りが発生しにくくなります。なお、関数への引数としては
void input(int a[], int max) {
のように、配列とその要素の個数が用いられています。第1引数は配列として定義されていますが、実際はポインタ変数です。 [例終わり]
4 文字列データ
C言語に文字型データは存在しますが、文字列型データは存在しません。文字列データは文字型データの集まりですから、文字列データは文字型データの配列として取り扱われます。ただし通常の配列と異なり、文字列データには終わりを表す特別な文字が最後の文字データの直後に付加されています。この文字列データの終了を意味する文字は、'\0'で表される空文字(値としては0)で、文字列の長さには数えられません。
このように文字列データは終了記号で管理されますから、長さについては制限がありません。すなわち、文字列の長さはハードウェアなどからの制約があるものの基本的には任意の長さとなります。
入力した文字列データを取り扱うときなど、文字型データをどこかに代入しなければならないときは、文字型データの配列を定義しなければなりません。たとえば、標準関数scanfで文字列データを入力するときは、次のようにします。
char buffer[100];
scanf("%s", buffer);
しかし、scanfの第2引数にはポインタを渡せばよいため、しばしば次のように間違います。
char *p;
scanf("%s", p);
配列名bufferもポインタ変数pもともにchar *型であるため、この間違いは文法的な間違いではありません。論理的な間違いです。入力された文字列データがどこに代入されるかを考えると、代入先の記憶領域を準備しなければならないことに気が付くはずです。しかし、しばしば見受けられる誤りです。
処理データとして文字列型データは存在しませんが、プログラム内には文字列データを文字列定数として記述できます。文字列定数を用いたときは、文字列定数の値について理解しておかなければなりません。
文字列定数を記述すると、通常コンパイラはその文字列を記憶領域のどこかに確保し、先頭アドレスをその文字列の値とします。したがって、たとえば、
char *p;
p = "abcd";
のとき、ポインタ変数pには文字列"abcd"の先頭アドレス、すなわち、先頭の文字'a'のアドレスが代入されます。決して文字列データが代入されるのではありません。
関数の引数にchar *型のデータを渡すときには、文字列定数を指定することができる場合があります。また、通常引数として文字列定数が記述される場所に配列名を指定できることがあります。たとえば、
printf("%d", &a);
は
char *format = "%d";
printf(format, &a);
と記述しても同じ結果となります。
文字列定数が初期化に用いられるときは、2種類の解釈があります。たとえば、
char *p = "abc";
char a[] = "xyz";
のとき、ポインタ変数pには文字列"abc"の先頭アドレスが初期値として与えられますが、配列aには文字列"xyz"そのものが初期値として与えられます。すなわち、配列aは終了記号も含めて4個の要素を持つ配列として定義されます。
文字列データを複数個配列として取り扱う場合は、文字型データの2次元配列を用いるか、char*型ポインタの1次元配列を用います。対象としているデータの型が異なりますから、同じ機能の関数を作成する場合、それぞれで関数の引数が異なります。このような場合のプログラム例を次に示します。
【例1-5】複数個の文字列データの例
#include <stdio.h>
#include <string.h>
#define MAX_LENGTH 10
/* 文字列を指定した長さだけ表示する */
void disp_line(char a[], int max) {
int k;
for(k = 0; k < max; k++) {
if(a[k] == '\0')
break;
putchar(a[k]);
}
}
/* 文字列データの配列を表示する */
void disp_all_a(char a[][MAX_LENGTH], int max, int length) {
int k;
for(k = 0; k < max; k++) {
/* 文字列をひとまとめに表示する */
printf("%s\t", a[k]);
/* 文字列を一文字ずつ表示する */
disp_line(a[k], length);
printf("\n");
}
printf("\n");
}
/* ポインタの配列を表示する */
void disp_all_p(char *p[], int max, int length) {
int k;
for(k = 0; k < max; k++) {
/* 文字列をひとまとめに表示する */
printf("%s\t", p[k]);
/* 文字列を一文字ずつ表示する */
disp_line(p[k], length);
printf("\n");
}
printf("\n");
}
int main() {
/* 文字型データの配列として先頭の5個を初期化 */
char a[7][MAX_LENGTH] = { "Sunday", "Monday", "Tuesday",
"Wednesday", "Thursday" };
/* ポインタの配列として先頭の5個を初期化 */
char *p[7] = { "Sunday", "Monday", "Tuesday", "Wednesday",
"Thursday" };
/* 文字型データの配列の残り2個を代入(文字列データ自体を代入) */
strcpy(a[5], "Friday");
strcpy(a[6], "Saturday");
/* ポインタの配列の残り2個を代入(文字列の先頭アドレスを代入) */
p[5] = "Friday";
p[6] = "Saturday";
/* 文字列全体を表示する */
printf("文字型データの配列を表示する\n");
disp_all_a(a, 7, MAX_LENGTH);
/* 文字列全体と先頭から3文字だけを表示する */
disp_all_a(a, 7, 3);
/* 文字列全体を表示する */
printf("ポインタの配列を表示する\n");
disp_all_p(p, 7, MAX_LENGTH);
/* 文字列全体と先頭から3文字だけを表示する */
disp_all_p(p, 7, 3);
return 0;
}
配列aは、文字列を取り扱うための文字型データの配列となっています。したがって、もっとも長い文字列データの長さ(この例の場合、"Wednesday"の長さである9に終了記号を加算した長さ10で、記号定数MAX_LENGTHで表されている)以上の文字型配列を要素とする配列になります。この例では、長さ10の文字型配列を要素とする2次元配列になっています。
一方、配列pは文字列データの先頭アドレスを要素とする1次元配列です。配列pに関する文字列定数は、コンパイラによって記憶領域のどこか適当な場所に確保され、その先頭アドレスがデータとして取り扱われます。
この例から理解できるように、関数disp_all_aとdisp_all_pは同じ結果を表示する関数ですが、対象とする配列の型が異なるため、引数の定義方法が異なります。一般に、文字列データを取り扱うとき、文字列データの格納領域を配列として定義するか、格納領域はコンパイラに任せ、先頭アドレスだけを管理するかによって、異なった取り扱いになります。 [例終わり]
C言語では配列全体に対する演算は定義されていません。したがって、文字列データを代入したり、比較したりするための演算子は定義されていません。このような機能を実現したいときは、ループを用いて1文字づつ代入したり比較したりするか、コンパイラに用意されている標準関数を利用します。
5 配列と関数の関連付け
集合データは、個々のデータである要素が集まった、全体がひとまとまりなデータになっています。すなわち、複数のデータが集められて全体が1個のデータとなっているという意味で、データが階層化しています。
一般に、階層化した集合データを取り扱うときは、データの階層に対応して処理も階層化するように配慮すると、よいプログラムを作成することができます。複数の機能を集めてひとかたまりの機能とすることは、C言語では関数を作成することですから、プログラムの階層化は関数の利用によって実現されます。
配列の処理を階層化するときは、基本データを要素とする1次元の配列を基本単位とします。すなわち、1次元配列を引数とする関数を作成し、その関数を利用して2次元配列を処理する関数を作成し、さらにこの関数を利用して3次元配列を処理する関数を作成していくように階層化します。なお、要素が集合データである場合は、その要素の処理を基本単位とします。
【例1-6】配列処理の階層化の例
#include <stdio.h>
#define MAX1 3
#define MAX2 2
void input_1(int array[], int max) {
int k;
for(k = 0; k < max; k++) {
printf("%d 番目 : ", k+1);
scanf("%d", &array[k]);
}
}
void input_all(int array[][MAX2], int max1, int max2) {
int k;
for(k = 0; k < max1; k++) {
printf("%d 番目\n", k+1);
input_1(array[k], max2);
}
}
void print_1(int *array, int max) {
int k;
for(k = 0; k < max; k++)
printf("%6d ", *(array + k));
printf("\n");
}
void print_all(int (*array)[MAX2], int max1, int max2) {
int k;
for(k = 0; k < max1; k++)
print_1(*(array + k), max2);
}
int main() {
int x[MAX1][MAX2];
input_all(x, MAX1, MAX2);
print_all(x, MAX1, MAX2);
return 0;
}
この例では2次元の配列を取り扱っています。そこで、1次元の配列を処理する関数を基本単位とし、その関数を利用して2次元配列を処理する関数を作成します。たとえば、入力のための関数として、まず、1次元配列への入力関数を
void input_1(int array[], int max) {
として作成しています。次に、この関数を利用して2次元配列への入力関数を
void input_all(int array[][MAX2], int max1, int max2) {
として作成しています。
同様に、1次元配列を表示するための関数として
void print_1(int *array, int max) {
を作成し、この関数を利用して2次元配列を表示するための関数として
void print_all(int (*array)[MAX2], int max1, int max2) {
を作成しています。
このように、処理を関数によって階層化すると、プログラムの記述が簡潔になり、全体の可読性が向上します。 [例終わり]
6 関数へのポインタ
ポインタはデータを間接的に操作する機能を持ち、C言語で重要な役割を果たしています。また、ポインタによって関数の開始アドレスを取り扱うことにより、関数を間接指定で呼び出すことができるという、ポインタにはさらに強力な機能があります。関数をポインタによって取り扱うことができることは、関数をデータとして取り扱うことができることを意味しています。すなわち、プログラムを作成する際、単にデータへのポインタだけでなく、関数へのポインタを利用することができます。
関数へのポインタは通常のデータと同じに取り扱うことができるので、これを関数の引数に利用することができます。すなわち、関数の中で用いる関数を引数で間接的に与えることができます。
次に示される例はクイックソートの例です。ここでは、クイックソートの関数の使い方に着目し、クイックソートの理論的な考え方を対象としてません。クイックソートを利用する際の参考例として下さい。
【例1-7】関数へのポインタを関数の引数に用いる例
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* 文字列データの昇順比較 */
int comp_s(const void *a, const void *b) {
return strcmp(*((char**)a), *((char**)b));
}
/* 文字列データの降順比較 */
int r_comp_s(const void *a, const void *b) {
return -comp_s(a, b);
}
/* 整数データの昇順比較 */
int comp_i(const void *a, const void *b) {
return *((int*)a) - *((int*)b);
}
/* 整数データの降順比較 */
int r_comp_i(const void *a, const void *b) {
return -comp_i(a, b);
}
char *s[5] = { "cde", "defg", "abc", "aabc", "bcdef" };
int x[10] = { 5, 3, 9, 2, 1, 0, 7, 6, 4, 8};
/* 文字列データの表示 */
void disp_str(char *s[], int max) {
int k;
printf("文字列データは\n");
for(k = 0; k < max; k++)
printf("%s\n", s[k]);
}
/* 整数データの表示 */
void disp_int(int x[], int max) {
int k;
printf("整数値データは\n");
for(k = 0; k < max; k++)
printf("%d\t", x[k]);
printf("\n");
}
int main() {
printf("昇順に並べると\n");
qsort(s, 5, sizeof(s[0]), comp_s);
disp_str(s, 5);
qsort(x, 10, sizeof(x[0]), comp_i);
disp_int(x, 10);
printf("\n降順に並べると\n");
qsort(s, 5, sizeof(s[0]), r_comp_s);
disp_str(s, 5);
qsort(x, 10, sizeof(x[0]), r_comp_i);
disp_int(x, 10);
return 0;
}
標準関数qsortはクイックソート法による高速なソートを行う関数で、第4引数に比較のための関数のポインタを与えなければなりません。このように、引数でアドレスとして与えられる関数は関数qsortの中で間接的に呼び出され、並べ換えのためにデータを比較するために用いられます。このとき、比較される2個のデータを直接用いるのではなく、処理データの汎用性を高めるために、比較される2個のデータのアドレスのみが関数の引数に与えられます。このような前提で、比較のための関数を処理データに応じて定義し、この関数のアドレスを関数qsortの引数に指定しなければなりません。
この例では、文字列の比較を行うcopm_sと整数値の比較を行うcomp_iを定義して利用しています。このプログラムで使用されているvoid*型のデータは汎用ポインタといわれ、ポイントしているデータの種類を問題にすることなく、ただ単にアドレスであればどのようなものでも構わないデータです。したがって、void*型のポインタ変数にはどのような種類のアドレスも代入できますが、ポイントしているデータの型を無視していますので、間接指定に直接使用することはできません。汎用ポインタを間接指定に用いるときは、このプログラムにも示されているように、必ず型キャストによって処理データのデータ型に型変換を行わなければなりません。このような不便さがあるものの、この例に示されているように、さまざまな種類のデータに対処するためには、汎用ポインタを利用しなければなりません。
この例では、キーになるデータが小さい方から大きい方に並ぶ昇順ソートを行っています。これとは反対の順序に並ぶ降順ソートにするには、比較を行うための関数を変更すれば簡単です。たとえば、
int comp_s(const void *a, const void *b) {
return -strcmp(*((char**)a), *((char**)b));
}
int comp_i(const void *a, const void *b) {
return *((int*)b) - *((int*)a);
}
のように、比較の結果の符号を反転するだけで十分です。しかし、この例では、
int r_comp_s(const void *a, const void *b) {
return -comp_s(a, b);
}
int r_comp_i(const void *a, const void *b) {
return -comp_i(a, b);
}
のように、表現は異なるものの同じ意味と機能を持った別の関数を定義しています。なお、標準関数qsortに関する詳細はマニュアルを参照してください。 [例終わり]
関数へのポインタを利用して、データとそのデータに付随する関数を集合データとして定義し、データと処理とを密接に関連付けることもできます。これについては構造体の応用として説明します。
7 データ領域の動的な管理
通常のデータ領域の通用範囲(参照できる範囲)と寿命はプログラムの作成時に決定されます。多くの場合、このような性質だけを利用できれば十分ですが、場合によっては効率的でないこともあります。たとえば、配列を利用したプログラムを作成する場合、大きめのサイズの配列を準備しておくと小さいサイズから大きいサイズまでの多くの場合に対処できるため、汎用性が大きいプログラムとなります。しかし、実行の際データ領域がすべて使用されることは少なく、ほとんどの場合記憶領域の使用効率は小さいものとなってしまいます。ユニックスなど複数個のプログラムが同時に実行されるシステムにおいては、このような記憶領域の無駄使いは他のプログラムにとって迷惑となり、システム全体にとって非効率的となります。また、プログラムファイルが大きくなって、ディスクの使用効率が低下することも考えられます。
このようなことから、自由領域あるいはヒープ領域といわれる記憶領域から、データのための領域をプログラムの実行時に動的に確保できるようになっています。このようなデータ領域の動的な確保は、mallocやcallocなどの標準関数を使用することによって実現されます。また、動的に確保された領域は標準関数freeを利用していつでも解放することが可能です。解放された領域は再度動的に確保し直すことによって、他の用途に再利用することが可能となります。このように、動的に確保された領域の通用範囲と寿命はプログラムを作る人の管理に任されています。なお、動的なデータ領域を実際に管理するのは、ユニックスやMS-DOSなどのOS(基本ソフト)です。プログラムの実行中にデータ領域が必要になったとき、必要な大きさを確保するようにOSに依頼し、確保に成功した場合は確保した領域の先頭アドレスをOSから受け取ります。解放をする場合は、OSに依頼して返却します。すなわち、動的なメモリ領域は、プログラマの管理のもと、必要になったときにOSから受け取り、不必要になったときにOSへ返却することになります。
このようなデータ領域の動的な確保と解放はコンパイル時でなく実行時に行われますから、実行時に配列の要素の数などのデータ数が定まるような場合に向いており、汎用的なプログラムをコンパクトに作成することが可能となります。
【例1-8】データ領域を動的に管理する例
#include <stdio.h>
#include <stdlib.h>
#undef DEBUG
#define DEBUG
#ifdef DEBUG
#define D(f) f
#else
#define D(f)
#endif
void disp(int a[], int max) {
int k;
for(k = 0; k < max; k++)
printf("%d%c", a[k], (k % 10 == 9)? '\n': '\t');
printf("%s", (k % 10 == 0)? "": "\n");
}
void sort(int a[], int max) {
int i, j, k, gap;
D(disp(a, max));
for(gap = max/2; gap > 0; gap /= 2)
for(i = gap; i < max; i++)
for(j = i-gap; j >= 0 && a[j] > a[k = j+gap];
j -= gap) {
int temp = a[j];
a[j] = a[k];
a[k] = temp;
D(disp(a, max));
}
}
void input(int a[], int max) {
int k;
for(k = 0; k < max; k++) {
printf("(%d) ", k+1);
scanf("%d", &a[k]);
}
}
int main() {
int *ap, max, f;
for( ; ; ) {
printf("データの数を入力:");
if((f = scanf("%d", &max)) < 0) {
printf("個数のデータがありません\n");
return 1;
}
if(f == 1 && max > 0)
break;
printf("整数値を入力しなさい\n\n");
rewind(stdin);
}
ap = (int *)calloc(max, sizeof(int));
if(ap == NULL) {
printf("記憶領域が足りません\n");
return 2;
}
input(ap, max);
sort(ap, max);
printf("\nソートの結果は\n");
disp(ap, max);
free(ap);
return 0;
}
動的な記憶領域の確保と解放の機能を利用して、例1-4を一般化したプログラムです。まず、記憶領域を動的に確保するための関数callocを利用し、
ap = (int *)calloc(max, sizeof(int));
によって、sizeof(int)の大きさを持つデータをmax個確保し、その先頭アドレスをポインタ変数apへ代入しています。領域が確保できたときはその先頭アドレスが、確保できなかったときは無効ポインタNULLがポインタ変数apへ代入されますので、
if(ap == NULL) {
printf("記憶領域が足りません\n");
return 2;
}
によって領域が確保できたかどうかをチェックしています。なお、記憶領域を動的に確保するための標準関数mallocやcallocを用いるときは
#include <stdlib.h>
が必要です。
ポインタ変数apがポイントしている場所のデータは*apとして間接指定でき、このデータのk個後のデータは*(ap+k)として間接指定できます。一般にポインタを利用した間接指定*(ap+k)はap[k]の配列表現と同じですから、以上のような動的な領域の確保によって、配列を確保できたことになります。 [例終わり]
それでは、例1-8を参考にして多次元配列を動的に確保する例を示します。この例では多次元配列の基本として2次元の配列を対象としています。
【例1-9】2次元配列を動的に確保する例
#include <stdio.h>
#include <stdlib.h>
#define MAX1 4
#define MAX2 3
void disp_1(int *p, int max2) {
int k;
for(k = 0; k < max2; k++)
printf("%d\t", p[k]);
printf("\n");
}
void disp_all(int **p, int max1, int max2) {
int k;
for(k = 0; k < max1; k++)
disp_1(p[k], max2);
}
void input_1(int *p, int max2) {
int k;
for(k = 0; k < max2; k++) {
printf("(%d) ", k+1);
scanf("%d", &p[k]);
}
}
void input_all(int **p, int max1, int max2) {
int k;
for(k = 0; k < max1; k++) {
printf("--- %d ---\n", k+1);
input_1(p[k], max2);
printf("\n");
}
}
void error(void) {
printf("メモリ領域を確保できません\n");
exit(1);
}
void *allocate(int size, int max, void (*error)(void)) {
void *p;
if((p = calloc(max, size)) == NULL)
(*error)();
return p;
}
int **get_array(int max1, int max2) {
int **p, k;
p = (int **)allocate(sizeof(int*), max1, &error);
for(k = 0; k < max1; k++)
p[k] = (int *)allocate(sizeof(int), max2, &error);
return p;
}
void free_all(int **p, int max1) {
int k;
for(k = 0; k < max1; k++)
free(p[k]);
free(p);
}
int main() {
int **p;
p = get_array(MAX1, MAX2);
input_all(p, MAX1, MAX2);
disp_all(p, MAX1, MAX2);
free_all(p, MAX1);
return 0;
}
2次元配列の動的な確保は1次元配列を複数個動的に確保することによって実現されます。これは関数get_arrayで実行されています。まず、
p = (int **)allocate(sizeof(int*), max1, &error);
によって、後で確保する1次元配列の先頭アドレスを代入するための、要素の数がmax1個であるポインタの配列を確保しています。正常に確保された場合、
for(k = 0; k < max1; k++)
p[k] = (int *)allocate(sizeof(int), max2, &error);
によって、要素数がmax2個の1次元配列を順に確保しながら、直前に確保したポインタの配列に各1次元配列の先頭アドレスを代入しています。領域の動的な確保が正常に実行されたときは、ポインタの配列の先頭アドレスが
return p;
によって関数値として返されます。なお、メモリ領域の動的な確保は、void *型の汎用ポインタを関数値とする関数allocateで行っています。また、メモリ領域の動的な確保に失敗したときは、関数allocateの第3引数にポインタで指定されている関数を、間接指定(*error)()によって呼び出しています。この例では、単にエラーメッセージを表示してプログラムの実行を終了しています。
関数allocateの関数値は汎用ポインタとなっています。このため、どのような型のデーも動的に確保することが可能です。しかし、この関数値をポインタ変数などに代入する場合は、多くの場合、この例のように型キャストが必要です。
動的に確保した領域はプログラマが責任を持って解放しなければなりません。この例では関数free_allで領域の解放を実行しています。解放の順序は確保のときと逆でなければなりません。この場合、まず、
for(k = 0; k < max1; k++)
free(p[k]);
によって、後で確保した各1次元配列を先に解放し、最後に
free(p);
によって、最初に確保した、各1次元配列の先頭アドレスを代入するためのポインタ配列を解放しています。なお、関数free_allでは第1添字の大きさしか使用しませんので、第2添字を関数の引数に使用していません。
このプログラム例のように、複数の1次元配列の先頭アドレスを管理するために、ポインタの配列を用いている場合は、
input_all(p, MAX1, MAX2);
のように指定して、2次元の配列に相当するデータを関数の引数とすることができます。pは2次元配列ではありませんから
input_all(p[][MAX2], MAX1, MAX2);
のように記述すると誤りになります。 [例終わり]
2次元配列を動的に確保する例を示しました。この考えを利用すると、3次元配列や4次元配列の動的な確保も同様に実現できます。たとえば、3次元配列の場合、2次元配列の先頭アドレスを代入するためのポインタ配列を第1添字の大きさだけ確保し、その各要素に動的に確保した2次元配列の先頭アドレスを順に代入すればよいことになります。このとき、2次元配列を確保する方法は例9-9に示されています。
配列の要素の個数を実行時に決定できるようにすると、データの融通性が高まります。また同時に、関連する処理と一体で用いると、プログラムも融通性が向上します。オブジェクト指向プログラミングにおいては、記憶領域の動的な管理は基本的な機能と位置付けられており、確保と開放のための演算子が定義されています。
第1章の問題
【問題1-1】次の記述は正しいですか。
(1)同じ配列に、異なるデータを要素として定義することができる。
(2)配列を定義するときは、原則として要素の個数を指定する。
(3)各要素は添字を用いて区別する。
(4)添字の範囲は1から要素数までである。
(5)添字には式を記述することができる。
(6)添字の範囲はチェックされる。
(7)複数の添字を持つ配列を多次元配列という。
(8)多次元配列では、a(5,6)のように記述する。
(9)すべての要素を初期化して定義するときは、要素の個数をすべて省略できる。
(10)配列名はその要素の先頭アドレスである。
(11)配列名はポインタ変数のように、間接指定に用いることができる。
(12)aを1次元配列とするとき、a[k]と*(a+k)は同じである。
(13)aを配列名とするとき、++aのように配列名aを用いることができる。
(14)int a[4][5]; と定義された配列aはint**型である。
(15)関数の引数として定義された配列は、実はポインタ変数である。
(16)文字列データは文字型データの配列である。
(17)文字列データは終了の印として'\0'を利用している。
(18)文字列データは長さで管理されているので、長さに制限がある。
(19)文字列定数の値は、その文字列の先頭アドレスである。
【問題1-2】要素数が5の整数型配列aを定義しなさい。
【問題1-3】要素数が5の符号無し整数型配列bを定義しなさい。
【問題1-4】要素数が5の単精度実数型配列cを定義しなさい。
【問題1-5】要素数が50の文字型配列dを定義しなさい。
【問題1-6】文字列データは文字型データの配列として取り扱われますか。
【問題1-7】100文字までの文字列を書き込めるデータ領域を定義しなさい。ただし、データ領域の名前はeとします。
【問題1-8】要素数が5の整数型配列fを定義しなさい。ただし、先頭から3個の要素を5,8,30に初期化するものとします。
【問題1-9】整数型配列gを先頭から順に0,5,7,8,19に初期化して定義しなさい。
【問題1-10】文字列定数"abcdef"を配列hとして定義しなさい。
【問題1-11】整数型配列aの2番目の要素に5を代入する文を示しなさい。ただし、要素は0番から数えるものとします。
【問題1-12】要素数が100の整数型配列bを定義し、for文を利用して配列の要素をすべてゼロにする文を示しなさい。
【問題1-13】要素数が50の整数型配列aにおいて、a[k+1]をa[k]へ転送する文を作成しなさい。ただし、k=0,1,2,... とします。
【問題1-14】要素数が50の整数型配列aにおいて、a[k]をa[k+1]へ転送する文を作成しなさい。ただし、k=0,1,2,... とします。
【問題1-15】次の記述は正しいですか。
(1)文字列定数を文字型配列eへ代入するために e = "ecec"; を用いてよい。
(2)文字型配列eが文字列"abcd"に等しいかどうかは e == "abcd" で調べることができる。
(3)b = a; によって、配列aのすべての要素を配列bへ代入することができる。
【問題1-16】それぞれ要素数が50の整数型配列c,dにおいて、配列cのすべての要素を配列dの対応する要素へ代入する文を示しなさい。
【問題1-17】それぞれ5個の要素を持つ整数型配列が10個集まった二次元の配列を定義しなさい。以降このような配列を10×5の配列ということにします。
【問題1-18】10×5の整数型配列aのすべての要素をゼロにするfor文を示しなさい。
【問題1-19】整数型配列bにおいてk番目の要素に値kを代入するために、次の文を実行したところ正しい結果が得られないことがあります。その理由を説明しなさい。
int b[10], k = 0;
while(k < 10)
b[k] = k++;
【問題1-20】5×4の整数型配列aを次の図のように初期化して定義しなさい。

【問題1-21】次に示される定義を用いると、b[1][2]の値はいくつになりますか。
int b[][3] = {{1,2,3},{5,6,7},{4,6,8},{9,7,5}};
【問題1-22】次の記述は正しいですか。
(1)整数型データをポイントするポインタ変数pに関して、*(p+1)は整数型データを意味するが、そこに実際に整数型データが存在するかどうかはわからない。
(2)配列名は、その配列の先頭アドレスを意味している。
(3)pをポインタ変数とするとき、*(p+k)はkバイト後ろのデータを意味している。
(4)ポインタ変数は配列名のように引数を持つ表現に使用できる。
(5)引数にポインタ変数を定義するとき、配列のように記述してよい。
(6)ポインタ変数は、どのようなデータをポイントするかによって区別される。
(7)どのようなアドレスも代入できるポインタ変数がある。
(8)ポインタ変数が何もデータをポイントしていないことを表すことはできない。
(9)void*型ポインタ変数には、どのようなアドレスもキャストなしに代入できる。
(10)void*型ポインタ変数の値をint*型ポインタ変数にキャストなしで代入できる。
(11)void*型ポインタ変数を間接指定に用いるときは、キャストが必要である。
【問題1-23】変数aのアドレスをポインタ変数pに求める式を、定義とともに示しなさい。
【問題1-24】一次元の配列bの3番目の要素のアドレスを求める式を示しなさい。
【問題1-25】二次元の配列cにおいて&c[0][0]とc[0]は同じ意味ですか。
【問題1-26】二次元の整数型配列の先頭アドレスを代入できるポインタ変数pを定義しなさい。
【問題1-27】次の文を実行すると*pntの値はいくつになりますか。ただし、変数 a,b,pnt のアドレスはそれぞれ100,150,200であるとします。
int a = 2, b = 3, *pnt;
pnt = &a;
【問題1-28】次の文を実行すると変数aの値はいくつになりますか。ただし、変数 a,b,c,p,q のアドレスはそれぞれ100,200,300,400,500であるとします。
int a = 2, b = 3, c = 5, *p, *q;
p = &b;
q = &c;
a = *p + *q;
【問題1-29】次の文を実行すると変数aの値はいくつになりますか。
int a = 2, *p;
p = &a;
*p = 5;
【問題1-30】次の文は正しく動作しますか。
int *p;
*p = 10;
【問題1-31】メモリ内のデータが図1のようになっているとき、
次の文を実行すると*pの値はいくつになりますか。
int *p;
p = 100;
【問題1-32】メモリ内のデータが図1のようになっているとき、
次の文を実行すると*qの値はいくつになりますか。
int **q;
q = 100;
【問題1-33】メモリ内のデータが図のようになっているとき、次の文を実行すると**qの値はいくつになりますか。
int **q;
q = 100;
【問題1-34】整数値3をポイントするポインタ変数pを、次のように定義することは正しいですか。
int *p = 3;
【問題1-35】次に示される定義はどのような意味となりますか。
int *b[5][4];
int (*c)[5][4];
【問題1-36】次に示される定義を用いると、*a,*(a+3),*a+3,*a+*(a+3)の値はいくつになりますか。
int a[5] = {10,20,40,50,30};
【問題1-37】次に示される文を実行するとポインタ変数pで与えられる文字列はどのようになりますか。
char *str = "abcdefg", *p;
p = str + 3;
【問題1-38】次に示される定義を用いると、*b[2],*(b[2]+2),*b[2]+2,**b,*(*b+3),**b+6,*(b[1]+2),**(b+2)の値はいくつになりますか。
int b[][3] = {{1,2,3},{5,6,7},{4,6,8},{9,7,5}};
【問題1-39】次の文を実行したところ図のようにメモリ内にデータが
設定されたものとします。このときp,*p,*(p+2)の値はいくつにな
りますか。
char *p;
p = "abc";
【問題1-40】要素数が50の整数型配列cとdにおいて、c[k]の値を4倍
してその結果をd[k]へ転送する文を作成しなさい。ただし、k=0,1,2,
...とし、添え字の代わりにポインタ変数を用いるものとします。
【問題1-41】キーボードより整数型データを配列に入力し、大きさの順に並べ替えて結果を表示するプログラムを作成しなさい。ただし、配列の大きさは10とし、ポインタ変数を用いるものとします。
【問題1-42】次の文を実行すると*a,*p,*qの値はいくつになりまか。ただし、変数a,p,qのアドレスはそれぞれ100,200,300とします。
char a[] = "abcd";
char *p, *q;
p = &a[0];
q = a;
【問題1-43】次の文を実行すると*p,*(a+2),*a+2の値はいくつになりますか。ただし、変数a,pのアドレスはそれぞれ100,200とします。
char a[] = "abcd";
char *p;
p = &a[2];
【問題1-44】次の文を実行するとpがポイントする文字列はどのようになりますか。
char *p;
p = "abcd";
*(p + 1) = 'x';
【問題1-45】次の文を実行すると変数bの値はいくつになりますか。
int b;
char *p;
p = "abcd";
if(p == "abcd")
b = 3;
else
b = 8;
【問題1-46】次のプログラムはキーボードより文字型配列にデータ(文字列)を入力し、文字コードを1だけ増加して表示する例です。
#define MAX 500
void main() {
char a[MAX], *p;
printf("文字列を入力:");
scanf("%s", a);
for(p = a; *p; ++p)
printf("%c", *p + 1);
}
このプログラムを利用し、入力文字列に英小文字があればそれを大文字へ変更して表示するプログラムを作成しなさい。
【問題1-47】入力された文字列の長さを表示するプログラムを作成しなさい。ただし、ポインタ変数を用いるものとします。
【問題1-48】次の文をポインタの代わりに配列を用いるように変更しなさい。
char a[MAX], *p;
for(p = a; *p; ++p)
*p += 1;
【問題1-49】次のようにポインタ変数を定義したときの意味を説明しなさい。
char *p = "abcd";
【問題1-50】次のようにポインタ変数を定義したとき、p[2]の値はいくつですか。
char *p = "abcd";
【問題1-51】次のようにポインタ変数を定義したとき、*q[2],q[3][2],*(q[2]+2),*(*(q+3)+2),**(q+1)の値はいくつですか。
char *q[] = { "abcd", "12345", "ABCDEFG", "987" };
【問題1-52】次の文を実行するとポインタ変数pとqの値はいくつになりますか。ただし、配列cの先頭アドレスは1000であるものとします。
int *p, *q;
char c[100];
p = (int *)c+3;
q = (int *)(c+3);
【問題1-53】整数型データへのポインタ変数qの値を文字型データへのポインタ変数pcへ代入するにはどうすればよいでしょうか。
【問題1-54】キーボードより文字型配列に文字列データを入力し、そのまま入力データを表示するプログラムを作成しなさい。ただし、入力と表示は1文字単位で行い、入力の際は改行を文字列の終了とします。
【問題1-55】引数として一次元の整数型配列とそのデータの個数とが与えられるとき、配列の中の最小値を関数値として返す関数minを作成し、確認用のプログラムを作成しなさい。
【問題1-56】引数として一次元の単精度実数型配列とそのデータの個数とが与えられるとき、配列内の全データの合計値を関数値として返す関数sumを作成し、確認用のプログラムを作成しなさい。
【問題1-57】キーボードより数字を順に入力し、それぞれの数字(0から9)の頻度を求めて表示するプログラムを作成しなさい。ただし、数字でない文字が入力されたときに終了するものとします。なお、キーボードより英大文字を順に入力し、それぞれの文字(AからZ)の頻度を求めて表示するプログラムへ、容易に変更できるように配慮して作成しなさい。
【問題1-58】キーボードから入力された文字列を、入力の順序とは逆順で表示するプログラムを、再帰関数を用いない場合と用いる場合とを作成しなさい。
【問題1-59】キーボードより整数値(倍長整数)を入力してそれを金額と見なし、その金額に必要な金種とその数を表示するプログラムを作成しなさい。たとえば、入力データが128000の場合、1万円が12枚、5千円が1枚、千円が3枚となります。このとき、10000円や5000円などの金額のデータは配列に持つものとし、結果も配列に求めるものとします。また、金種の変更があったとき容易に変更できるように配慮しなさい。(ヒント)金種のデータは正の整数だけです。金種のデータを持つ配列の最後に余分に0や負の値を設定することによって、配列の大きさの変化に融通性を持たせることができます。
【問題1-60】キーボードから入力された整数が、次に入力された整数値で示される桁数で表示されるプログラムを標準関数sprintfを利用して作成しなさい。たとえば、25と4が入力されたとき、4桁で25が表示されます。
【問題1-61】縦横に広がる平面状の数値データのイメージで表される整数型データに関して、データを入力し、表示を行うプログラムを作成しなさい。ただし、最も下の段に縦の合計を、右端に横の合計を、右下隅に総合計を表示するものとします。また、配列全体を入力するための関数、行単位の入力を行う関数、配列全体を表示する関数、行単位の表示を行う関数などを作成して利用しなさい。
【問題1-62】正方行列の積を計算するためのプログラムを作成しなさい。(ヒント)行列aと行列bの積として求められる行列cに関する配列の要素c[j][k]は次のように計算されます。
c[j][k]=a[j][0]・b[0][k]+a[j][1]・b[1][k]+a[j][2]・b[2][k]+・・・
+a[j][n-1]・b[n-1][k]
【問題1-63】2個の100桁以内の整数を加算するプログラムを作成しなさい。なお、入力や表示のための関数も作成しなさい。ただし、負の値も入力できるものとします。(ヒント)100個の要素を持つ整数型配列を考え、1個の要素で1桁を意味するように取り扱うとよいでしょう。
【問題1-64】整数型変数mを分母、nを分子とするとき、この分数を小数点以下50桁以内の小数で表示するプログラムを作成しなさい。ただし、循環小数になる場合は繰り返される部分を'で囲んで表示しなさい。また、mとnにはキーボードからデータを入力するものとします。
【問題1-65】anxn+an−1xn−1+・・・+a2x2+a1x+a0で表される多項式は次のようにして計算できます(Honorの方法)。
f0 = an
f1 = f0 * x + an−1
f2 = f1 * x + an−2
:
fk = fk−1 * x + an−k
:
fn = fn−1 * x + a0
すなわち、
an (k = 0 のとき)
fk =
fk−1 * x + an−k (k > 0 のとき)
の漸化式を用いて計算できます。この漸化式を利用して、5次(nが5のとき)の多項式の係数を配列に入力し、入力データとして与えられたxの値に対する多項式の値を表示するプログラムを作成しなさい。なお、多項式の値はxの値を引数に持つ関数を作成して計算しなさい。計算においては、再帰関数を利用しない場合と、再帰関数を利用した場合とを考えなさい。
【問題1-66】整数型配列を利用して、合計を求めるプログラムを作成しなさい。ただし、再帰関数を利用しない場合と、再帰関数を利用する場合とを考えなさい。なお、n個の数値をa0、a1、・・・、an-1とするとき、n個のデータの合計snは
0 (n = 0 のとき)
sn =
sn-1 + an-1 (n > 0 のとき)
【問題1-67】1001個の要素を持つint型の配列を定義し、すべてを0に初期化しておきます。次の方法(エラトステネスのふるい)で、1000以下の素数を求めて表示するプログラムを作成しなさい。なお、配列名をaとします。
(1)kを2から初めて1ずつ増加させながら1000以下の間(2)以降を繰り返す
(2)a[k] が 0 ならば(3)を実行する
(3)j を k+k から初めて k ずつ増加させなが 1000以下の間
a[j] に 1 を代入する
以上の処理が終了したとき、a[k]が0ならばkは素数となっています。
【問題1-68】入力された個数の大きさの整数型配列を動的に確保し、その配列にデータを入力して最大値を求めるプログラムを作成しなさい。ただし、配列へ入力を行う関数と最大値を求める関数を作成して利用しなさい。
【問題1-69】入力によって与えられた行数と列数を持つ正方行列を実行時に動的に確保し、データを入力して表示するプログラムを作成しなさい。ただし、行列のデータは整数型とします。
【問題1-70】2個の正方行列の積を求め、結果を表示するプログラムを作成しなさい。ただし、行列の大きさは実行時に指定するものとし、動的に領域を確保する関数と、そのような領域を開放する関数と、行列の積を求める関数を作成して利用しなさい。
【問題1-71】関数へのアドレスを要素とする配列を定義し、順に実行するプログラムをつくりなさい。なお、実行する関数は4個で、関数値も引数も持たないものとします。
「補遺」メインページへ/
この章のトップへ/
桐山先生のページのトップへ