14. C、C++に関する知識 I

シラバス: 

1. 科目の概要

 主要なオープンソースソフトウェアの記述言語であるC言語の基本的な知識を解説する。C言語プログラムの構造、型、演算子、制御構文や標準的な関数、ポインタや構造体、ユーザや外部データとの入出力に求められるプログラミングなどについて説明する。

2. 習得ポイント

 本科目の学習により習得することが期待されるポイントは以下の通り。

3. IT知識体系との対応関係

「14. C、C++に関する知識Ⅰ」とIT知識体系との対応関係は以下の通り。

[シラバス:http://www.ipa.go.jp/software/open/ossc/download/Model_Curriculum_05_14.pdf]

<IT知識体系上の関連部分>

4. OSSモデルカリキュラム固有の知識

OSSモデルカリキュラム固有の知識として、Linux上での標準入出力、ファイル入出力を扱うための知識がある。また、OSSであるviエディタ、gccコンパイラ(GNU Compiler Collection)を用いた開発の流れを学ぶ。

(網掛け部分はIT知識体系で学習できる知識を示し、それ以外はOSSモデルカリキュラム固有の知識を示している)

I-14-1. C言語の歴史と特徴、開発事例と開発手順

Cプログラミング学習の入り口として、C言語の歴史や特徴、他の言語との比較、Cによる開発事例を解説する。また、プログラムを開発する手順として、エディタによるプログラム作成からコンパイル、プログラム実行までの流れを説明する。

【学習の要点】

* CはUNIXなどのシステムソフトからアプリケーションまで、非常に幅広く使われている言語である。

* Cは強力なポインタ機能を有し、OSやドライバの記述に必要な低レベル処理が記述できる。

* Cは他の言語に比べるオーバーヘッドが少なく、一般に高速である。

* Cのソースコードはテキストエディタなどを使って書き、コンパイルすることでプログラム開発の流れがつかめる。

図I-14-1. Cプログラミングの流れ

【解説】

1) Cの歴史

Cは1970年代前半に開発されたプログラミング言語である。

Cの歴史はUNIXの歴史と切り離すことができない。UNIXはCで開発され、UNIXの普及がCを支え、Cの普及がUNIXを支えてきた。他の様々なプログラミング言語が誕生し普及した現在においても、最も重要なプログラミング言語の一つとして位置づけられている。

2) Cの特徴

Cの一番の特徴といえるのが、強力なポインタ機能である。この強力なポインタ機能により、アセンブリで開発するような低レベル処理(ハードウェア制御などの処理)も可能にし、多くのOS、周辺機器のドライバソフトウェア、他のプログラミング言語の処理系などがCで開発されている。ソースコードの記述の自由度も高く、高度なプログラミングができる反面、プログラムの保守性やセキュリティなどの面でリスクを抱えている。

3) 他の言語との比較

アセンブリと比較すると、Cは人間が使う言葉に非常に近い記述ができ、高級言語と呼ばれるが、低レベル処理が可能であることから、他の高級言語とアセンブリとの中間に位置づけられることが多い。

C++はCにオブジェクト指向を取り込んだ言語である。その他のCの後発言語の多くは、Cのポインタ機能などの一部をあえて隠蔽しており、プログラムの保守性やセキュリティ面、学習の容易性の利点がある反面、ハードウェア制御といった低レベル処理には適さないものとなっている。これらの言語で書かれたプログラムでは、上記利点を得るために、ソースコードに記述されないさまざまな処理を内部で行うが、Cではそのような処理がほとんどなく、オーバヘッドが小さい。よって、Cで書かれたプログラムは、他の言語で書かれたものよりも一般的に高速に動作する。

4) Cのプログラム作成と実行の流れ

Cのプログラミングの流れは、図Ⅰ-14-1に示したような手順になる。

* テキストエディタを用いてソースコードを記述する。

* gccなどのコンパイラを用いてコンパイルを実行する。gccの場合、a.outという実行ファイルができる。

* 実行ファイルを実行する。

I-14-2. C言語の基本的な構造、型、演算子、制御構文

Cの基本的なしくみ、プログラムの構成、基本的な文法を説明する。基本的なプログラム記述の例を示し、変数、データ型、演算子、制御構造といったCプログラムの基本的な要素について説明を加える。

【学習の要点】

* 制御構文によって、繰り返しや分岐処理など、プログラムの実行順を制御することができる。

* Cでプログラムを書く時は、データ型に気を付ける。

図I-14-2. Cの基本的な演算子と変数型

【解説】

1) Cの基本的な仕組み

Cは手続き型言語と呼ばれている。プログラムの実行が、上から記述した順に行われていくからである。文の終わりには必ずセミコロン(;)を付ける。そうすることで、命令文が一文終わったことを示す。また、Cではソース内にコメントを書くことができる。 /* と */ とで囲まれた部分や、 // から改行までがコメントになる。

2) Cの基本的な制御構文

Cは制御構文によってプログラムの挙動を制御する。制御構文は、プログラムの状態と指定された条件を比べて、その条件が当てはまったとき、または当てはまらなかったときの挙動を変えるために指定するものである。この制御構文を用いることで、繰り返しや分岐処理ができる。以下に代表的な制御構文を紹介する。

* if

ある条件に当てはまった場合の挙動を制御する。

* for

指示した回数だけ挙動を繰り返す。

* return

関数を終了する時点を指示する。

3) Cの基本的なデータ型

Cで変数を宣言するには、次の通りに書く。

型名 変数名;

基本的なデータ型と格納されるデータサイズは一般には以下の通りである。

* 整数型

- int : 32bit short : 16bit long : 32bit char : 8bit (文字型)

* 浮動小数型

- float : 32bit double : 64bit

4) Cの基本的な演算子

* 算術演算子

+ 加算 -減算 *乗算 /除算 %剰余

* 比較演算子

> より大きい < 未満 >=以上 <=以下 ==等価

!= 等しくない

* ビット演算子

& 積 |和 ~補数 <<左シフト >>右シフト

I-14-3. 関数の定義、標準関数の利用方法

関数とは何かを説明し、関数を活用したプログラム部品化の概念を示す。関数定義の方法と関数の利用方法、変数のスコープなど、関数の利用時に気をつけるべき点を列挙する。さらに代表的な標準関数を紹介し、標準で提供される関数で様々なことを実現できることを例示する。

【学習の要点】

* 関数とは、ある手続きをひとまとめにしたものである。

* 関数を活用するとプログラムを構造的に書くことができ、読みやすく、保守性の高いプログラムを作ることができる。

図I-14-3. 関数を使ったプログラム

【解説】

1) 関数とは何か

関数とは、ある手続きをひとまとめにしたものである。関数には引数と返り値というものがあり、関数実行時に関数に渡す値を引数、そして計算結果として出てきた値として呼び出し元に返す値を返り値と呼ぶ。頻繁に使う手順などを関数化することで、コードの削減と可読性を高めることができる。また、汎用性の高い関数を作り、一つのプログラムだけではなく複数のプログラムで活用することで、開発時間の短縮を図れる。こうした汎用性の高い関数を作り、活用していくことを、プログラム部品化と呼んでいる。

2) 関数の宣言

関数は、次のように宣言することで使うことができるようになる。

返り値の型 関数名(引数の型 引数の名前, ...){

関数で行いたい手続きの記述

return 返り値;

}

例) 引数の2乗を返す関数

float square(float a){

float s;

s = a * a;

return s;

}

3) 関数の利用

関数を利用する場合は、「関数名(引数)」のように記述する。

例)

r = square(1.1);

この場合は 1.1 が引数となり、返り値は 1.21 となるので、rには1.21が代入される。

4) 変数のスコープ

変数には、グローバル変数とローカル変数の二種類がある。グローバル変数はどこからでも利用でき、ローカル変数は使える場所が制限されている。ローカル変数が使える場所は、そのローカル変数が宣言された関数の中である。ある変数を参照できるプログラム中の範囲のことを、その変数のスコープという。

5) 標準関数

Cの規格として定められている、標準ライブラリというファイルに定義されている関数。利用するときにはプログラムの頭に #include <標準ライブラリ名> と書いて、ライブラリを読み込んで利用する。代表的なものは以下の通り。

* printf() 文字列などを出力する関数 (定義されているライブラリ:stdio.h)

* fgets() 文字列を読み込む関数 (定義されているライブラリ:stdio.h)

* strcpy() 文字列をコピーする関数 (定義されているライブラリ:string.h)

I-14-4. ポインタを利用した効果的なプログラミング

メモリ上に配置されているデータの抽象化とアドレスの考え方を示し、ポインタの概念、特徴、利用方法について解説する。また配列や文字列、関数を利用する際にポインタを上手に利用する技法を示し、関数ポインタの概念についても言及する。

【学習の要点】

* すべての変数はアドレスを割り当てられ、そのアドレスが指すメモリに保存されている。

* ポインタとは、あるデータが格納されているメモリのアドレスを保存したものである。

図I-14-4. ポインタの概念

【解説】

1) データ格納アドレスの抽象化

メモリ上のデータは、本来ならばアクセスするのにメモリのアドレスを指定する必要がある。しかし、データにアクセスするためにいちいち人が手でメモリのアドレスを指定するのは、非常に困難である。そのため、データに変数名をつけて、その名前を指定することでデータが入っているメモリにアクセスできるようにした。こうすることで、データをメモリの実アドレスを指定することなく、抽象的に扱うことができるようになる。

2) ポインタの概念、特徴、利用方法

変数はそれぞれコンパイラによって自動的にアドレスを割り当てられる。通常はこのアドレスを意識する必要はないが、ポインタを使うことでアドレスを利用することができる。ポインタは、メモリのアドレスを保存しておくためのものである。ポインタ型の変数を利用するには、変数名の前に「*」をつけて宣言をする必要がある。また、ポインタに関する演算子として、アドレスを表す「&」、逆参照を表す「*」がある。例えば、「&var」は変数varのアドレスを指すポインタを示し、「*ptr」はポインタ変数ptrが指すアドレスに格納されている値を示す。

3) 関数引数としてのポインタ

Cでは引数は値呼び出しで関数に渡される。変数を引数として関数に渡しても、その変数の値を関数内から変更することはできない。。しかし、ポインタを引数として関数を宣言し、関数呼び出し時にアドレスを渡すと、関数内でポインタが差す値(実際に引数として使用される変数の値)を変更することができるようになる。

4) ポインタと配列

Cでの配列の宣言の仕方は、次のとおりである。

型名 配列名[確保したい配列の要素数];

配列の要素を指定するときは、「 配列名[要素番号] 」のように記述する。

例)

int arr[100];

arr[3] = 21;

この例の場合、Cでは単に「 arr 」と書くことで、配列arrの先頭要素を指すポインタを表すことになる。つまり、「 arr 」は「 &arr[0] 」と等価である。

5) ポインタと文字列

Cには文字列型がなく、文字列は文字を配列にしたものであらわされる。配列で表せるということは、ポインタでも表せるということである。

6) 関数ポインタについて

関数も変数と同じくメモリ上にアドレスを持っている。これは関数のポインタということができ、ポインタ変数で扱うことができる。これを使用することで、動的に呼び出す関数を変えることができる。

I-14-5. 構造体の概念と構造体を使用したプログラミング

構造体の概要、定義と使い方を解説し、変数や配列などデータをまとめて扱う方法について説明する。また構造体を配列で利用する方法、構造体メンバへのポインタによるアクセス、関数への構造体データの受け渡し方法など、実際にプログラムで構造体を利用する際に必要となる技術を解説する。

【学習の要点】

* 構造体とは、複数のデータ型をひとまとめに扱えるようにしたものである。

* 構造体は配列とは違い、柔軟性に富んだデータの扱いができる。

図I-14-5. 構造体の定義

【解説】

1) 構造体とその定義

構造体とは、異なるデータ型をグループとして扱うために用いる機能である。同じデータ型をグループとして用いる配列とは違い、柔軟性に富んだデータの扱いができる。

構造体を定義するためには、以下のような文法を用いる。

struct 構造体名 {

フィールドのデータ型 フィールドの名前;

フィールドのデータ型 フィールドの名前;

......

};

フィールドとは、構造体を構成する要素のこと。構造体では、フィールドをそれぞれ違うデータ型にできる。

2) 構造体の使い方

構造体の各フィールドにアクセスするには、次のような構文を利用する。

変数名.フィールド名

3) 構造体と配列

構造体は配列にしても用いることができる。配列として用いる場合の宣言方法と、メンバへのアクセス方法は下記のようにする。

* 宣言例

struct elem{

int f1;

int f2;

};

struct elem a[10];

* アクセス例

a[0].f1 = 0;

4) 構造体のポインタ

ポインタを用いて構造体のフィールドにアクセスするには、演算子「 -> 」を利用する。

変数名->フィールド名

I-14-6. マクロの利用(プリプロセッサ機能)

Cコンパイラにおけるプリプロセッサの位置づけと、プリプロセッサが備えている機能を説明する。また、マクロによるプログラミング例を紹介し、マクロの効果的な使い方とマクロ利用時の留意点について解説する。

【学習の要点】

* プリプロセッサとは、コンパイルの前処理をするプログラムである。

* プリプロセッサを用いれば、文字列の置換や条件付きコンパイル、外部ファイルの挿入などができる。

* プリプロセッサ用の構文はCの構文とは別物なので、その扱いには注意しなければならない。

図I-14-6. プリプロセッサに通す前と後

【解説】

1) Cコンパイラにおけるプリプロセッサの位置づけ

プリプロセッサとは、コンパイルの前に実行し、ソースコードに変更を加えるプログラムである。Cでは、#から始まる行がプリプロセッサへの指示となっている。

2) プリプロセッサが備える機能

プリプロセッサが備える機能の一部を紹介する。

* マクロ

「#define」命令により、マクロを定義できる。例えば、次のコマンドにより、プリプロセッサはソースコード中のすべての「PI」という文字列を「3.14」に置き換える。

例) #define PI 3.14

* 条件付きコンパイル

デバッグ用のコードをプログラムに埋め込み、出荷時には取り除きたい場合には、条件付きコンパイル #ifdef/#endif を用いる。

例)

#define DEBUG

#ifdef DEBUG

printf ("ステータスは%d\n", status);

#endif

デバッグ中はプログラム先頭にマクロDEBUGを定義しておくと、変数statusが表示される。

#undef DEBUG

#ifdef DEBUG

printf ("ステータスは%d\n", status);

#endif

とマクロDEBUGを未定義に変えると、#ifdef~#endifの間はコンパイル対象から外れる。

* インクルードファイル

#include命令を使用すると、プログラム中で別のファイルのソースコードを挿入できる。マクロ定義を複数のファイルで共通に使いたい場合などに用いる。

3) マクロ利用時の留意点

プリプロセッサとコンパイラとはソースコードの解釈が異なるので、プリプロセッサ命令中でC言語の構文は使えないことに注意する必要がある。また、プリプロセッサは適切なCの構文であるかどうかをチェックしないため、特にマクロで書き換えたため予期せぬ問題が生じることがあることにも注意が必要である。

I-14-7. 標準入出力を利用したプログラミング

Cプログラムにおける入出力の概念を説明し、入出力ストリームに対するプログラミング手法を紹介する。ここでは特に標準入出力を対象とした文字や文字列の受け渡しについて解説する。

【学習の要点】

* Cでは標準入出力もファイルとして扱われるので、ファイルの入出力と同様の方法で標準入出力にアクセスできる。

* fgets()関数やfputs()関数の引数に標準ファイルを指定することで、標準入出力を対象とした文字列の受け渡しができる。

図I-14-7. 入出力ストリーム

【解説】

1) Cプログラムにおける入出力

Cプログラムにおける入出力は、すべてファイルに対して行われる。キー入力と画面出力(標準入出力と呼ばれる)もファイルとして扱われる。

* stdin 標準入力(通常はキー入力)

* stdout 標準出力(通常は画面表示)

* stderr 標準エラー出力(通常は画面表示)

これらのファイルはCでは常にオープン状態となっているため、明示的にオープンする命令は不要である。

2) 標準入力からの文字列の読み込み

標準関数fgets()を使用すると、標準入力から文字列を読み込むことができる。fgets()呼び出しの一般的な形は以下のとおりである。

fgets (name, sizeof(name), stdin);

ここで、nameは文字列変数を表す。引数は以下のようになる。

* name

文字配列の名前を指定する。1行(文字列終端文字を含む)が、この配列に読み込まれる。

* sizeof(name)

読み込む文字の最大数(文字列終端文字の1文字分を加えた数)を指定する。関数sizeof()を使用すると、読み込む文字数を変数が保持可能な数に制限できる。

* stdin

読み込むべきファイルを指定する。ここでは、このファイルは標準入力となる。

3) 標準出力への文字列の出力

文字列はfputs()を使って標準出力に書きだすことができる。

例)

fputs(name, stdout);

4) 標準エラー出力への文字列の出力

標準出力と同様である。

例)

fputs(name, stderr);

I-14-8. ファイル入出力とファイル/ディレクトリ操作

ファイル入出力、ファイル操作、ディレクトリ操作といったCプログラムからファイルにアクセスするファイル管理について説明する。またファイルに対する高水準ファイル入出力関数と低水準ファイル入出力があることを示し、具体的なデータ入出力方法を解説する。

【学習の要点】

* ファイルの入出力には、ファイル変数を使う。

* ディレクトリ操作をするには、direct.hに入っている標準関数を使う。

* 通常はシステムの頻繁な呼び出しを防ぐためバッファリングの仕組みを備える高水準ファイル入出力を用いる。

図I-14-8. ファイルアクセスの例

【解説】

1) ファイルの入出力

ファイルの入出力を行うには、以下のような手順が必要となる。

* ファイル変数を宣言する。

FILE *infile、*outfile;

* ファイルをオープンにする。

infile = fopen("infile.dat", "r"); //読み取りモードでオープン

outfile = fopen("outfile.dat", "w"); //書き込みモードでオープン

* ファイルを読み書きする。

fgets(str, sizeof(str), infile); //文字列変数strに1行読み込む場合

fputs(str, outfile); //文字列変数strの内容を書き込む場合

* ファイルをクローズする。

fclose(outfile);

fclose(infile);

2) ディレクトリ操作の方法

UNIX系OSでディレクトリを操作する関数には、次のようなものがある。

* mkdir() ディレクトリの作成

* rmdir() ディレクトリの削除

* opendir() ディレクトリのオープン

* readdir() オープンしたディレクトリのファイル一覧の取得

* closedir() ディレクトリのクローズ

3) 高水準ファイル入出力と低水準ファイル入出力の使い分け

一時的にデータをため込む領域のことを、バッファという。fopen()、fclose()などの高水準ファイル入出力関数はバッファを使用した入出力を行う。open()、close()などの低水準ファイル入出力関数ではバッファを使用しない。低水準ファイル入出力関数を利用すると、システムコールが頻繁に発生するので、通常は高水準ファイル入出力関数を利用する。

4) 具体的なファイル入力方法

ファイル入力例を図I-14-8に示す。

I-14-9. データ構造

データ構造とは何か説明し、線形リスト、ツリー、スタック、キューといった一般的なデータ構造を紹介する。またメモリ管理とデータ構造の関係に触れ、Cプログラムによるデータ構造の実装例を紹介する。

【学習の要点】

* データ構造はアルゴリズムと深く関わる、プログラムの根幹をなすものである。

* 線形リスト、ツリー、スタック、キューといったデータ構造は、ポインタを用いることで実現できる。

図I-14-9. 各データ構造のイメージ図

【解説】

1) データ構造とは

データ構造とは、複数のデータの関連性を表現するモデルである。用いるアルゴリズムによって適切なデータ構造は異なり、その逆でデータ構造によって適切なアルゴリズムも異なる。プログラミングをする上でデータ構造は非常に重要な要素である。

2) 代表的なデータ構造

* 線形リスト

各ノードが次のノードへのポインタを持ち、データが直線状に並んだデータ構造。

* ツリー

各ノードがポインタを二つ以上持ち、それぞれのポインタが下位のノードを指す、データが木のように枝分かれするデータ構造。

* スタック

各ノードが積み重なっており、最後に追加されたデータが最初に取り出される、重ね餅のようなデータ構造。

* キュー

各ノードが積み重なっており、最初に追加されたデータが最初に読みだされる、パイプのようなデータ構造。

3) メモリ管理とデータ構造

各種のデータ構造に適したメモリ割り当てをすることにより、必要最小限のメモリで実装できる。データ構造がメモリ上でどう割り当てられているかを把握することが重要である。

4) データ構造の実装例

ここでは例として、線形リストの実装例を紹介する。

* 構造体のフィールドで、同じ構造体へのポインタを定義

struct linearelem {

char data[100]; // 実際のデータ

struct linearelem *next; // 次の構造体へのポインタを格納

};

* 構造体を3つと構造体へのポインタを宣言

struct linearelem first, second, third, *curr;

* ポインタでつないで線形リストを作成

first.next = &second;

second.next = &third;

* 最初の構造体を指し示す場合

curr = &first;

* 次の構造体を指し示す場合

curr = curr->next;

I-14-10. アルゴリズム

用途に応じたデータ構造の使い方を説明し、それぞれのデータ構造を使用した代表的なアルゴリズムや効果的な利用方法を紹介する。

【学習の要点】

* データ構造は、用途によって使い分けることで、効率的なアルゴリズムを実行することができる。

図I-14-10. 二分探索木のデータ構造

【解説】

1) 用途に応じたデータ構造の使い方

* テスト結果のリストを点数順にして作りたい時など、リストに挿入、削除を頻繁に行いたい場合

線形リストが使える。もしこのリストに配列を使ったとしたら、リストの途中に挿入を行うと、その挿入を行った場所より後のデータも並びなおさなければならないが、線形リストだとポインタのつながり方を直すだけで済み、挿入した場所よりも後ろのデータの並び替えをする必要がない。これにより、データを作る時点で点数順に並べたリストを作ることができる。

* 辞書を作ってそれを活用したい時など、データの検索を頻繁に行いたい場合

二分探索木が使える。各ノードに一つ単語を割り当て、ノードを追加する際に後述する規則を守ることで、検索する際にその規則を利用して検索することができるので、線形検索よりも早く検索することができる。

2) 代表的なアルゴリズム

ここでは、例として二分探索木を用いて辞書を作るアルゴリズムを紹介する。

* 二分探索木とは

二分探索木とは、ツリー状のデータ構造の一種である。ツリー状のデータ構造では、ルートノードから枝分かれしていく構造をとり、あるノードから枝分かれした先のノードをそのノードの子ノードと呼び、そのノードは子ノードからみた親ノードという。ツリー構造では、ルートを除く各ノードにおいて親ノードは1つのみ存在する。二分木では、各ノードが最大2つまで子ノードを持つことができる。その2つの子は、右の子、左の子と呼ぶ。この二分探索木では、「左の子ノード≦自ノード<右の子ノード」という条件を満たしている必要がある。

* 二分探索木にデータを追加するアルゴリズム

二分探索木にデータを追加する場合は、まず対象ノードをルートノードに位置づけ、以下の処理を繰り返す。

- 対象ノードが存在しなければ、そこにノードを追加し、そのノードに追加すべき値を設定して終了する。

- 対象ノードの値と追加する値とを比較する。追加する値のほうが大きければ、対象ノードを右の子ノードに変更し、そうでなければ、対象ノードを左の子ノードに変更する。

* 二分探索木からデータを検索するアルゴリズム

二分探索木からデータを検索する場合は、まず対象ノードをルートノードに位置づけ、以下の処理を繰り返す。

- 対象ノードが存在しなければ、検索されなかったものとして終了する。

- 対象ノードの値と検索する値とを比較する。検索する値と一致すれば、検索されたものとして終了し、検索する値のほうが大きければ、対象ノードを右の子ノードに変更し、そうでなければ、対象ノードを左の子ノードに変更する。