C言語の復習

スクールの教科書でC言語について復習したのでメモしておきます。

EclipseでのC言語のプログラムの作成方法

  • こちらからC/C++用のEclipseをダウンロードします。
  • Eclipseを起動後、パースペクティブがC/C++になっていることを確認します。
  • ファイルタブから新規->Cプロジェクトを選択し、プロジェクトを作成します。プロジェクト名は「Sample」とします。
  • プロジェクトを右クリック->新規->ソースファイルを選択し、ソースファイルを作成します。ソースファイル名は「Sample.c」とします。
  • ソースファイルに以下のソースコードを記述します。
#include <stdio.h>

int main(void){
    printf("Hello World!\n");

    return 0;
}
  • ソースファイルを上書き保存してからプロジェクトを右クリック->プロジェクトのビルドを選択し、プロジェクトをビルド(コンパイル)します。
  • 実行タブから実行->1 ローカルC/C++アプリケーション(1)を選択し、プログラムの実行を行います。
  • コンソールに「Hello World!」と表示されます。

※上手く行かない場合は、ビルドに失敗していることが考えられます。ソースコードを見直したうえで、プロジェクトを右クリック->構成のビルド->すべてビルドを選択し、強制的に再ビルドを行います。
更にこの再ビルドが失敗する場合、Eclipseを一旦終了し、エクスプローラで対象プロジェクトのフォルダを開き、DebugフォルダとReleaseフォルダを手動で削除してから再度ビルドを行ってみてください。

コンパイラの動作

コンパイラは3段階に分けてソースコードを機械語のプログラムに翻訳します。

  • プリプロセッサ:ソースコードをソースコードに書き込まれた指示に従って整形します。
  • コンパイラ:整形されたソースコードを機械語に翻訳します。機械語のオブジェクトファイルが生成されます。
  • リンカ:複数のソースコードから生成された複数のオブジェクトファイルやライブラリファイルを結合(リンク)してプログラムを完成します。

プリプロセッサは、例えばソースコードに書き込まれた複数の「SYOUHIZEI」という文字列を
指示に従って「0.08」と書き換えるような形で働きます。
将来消費税率が上がれば、「0.1」と書き換えるように指示を書き直すと、すべての「SYOUHIZEI」が「0.1」と書き換えられることになります。

コンパイラは、プリプロセッサが整形したソースコードを解釈して機械語に翻訳します。
その際、ソースコードが言語の文法にしたがっているかどうかはチェックし、チェックを通らなければエラーメッセージを表示して停止します。
コンパイラは文法のチェックを行うだけで動作そのもののチェックは行いません。
解釈、翻訳を行う際に、より高速に動作するよう最適化が行われます。
同じソースコードをコンパイルしても、コンパイラの最適化の性能によって動作は速くも遅くもなります。

大きなプログラムは、機能ごとに別々のソースコードに記述されることがあります。
それらのソースコードはそれぞれの機械語のオブジェクトファイルとしてコンパイルされます。
また、標準入出力の処理などは、共通の処理用のプログラム部品(ライブラリ)を前提としています。
ライブラリは、コンパイラがオブジェクトファイルとして持っています。
また、組み込みシステム開発では、プログラムの一部をアセンブリ言語で記述することもあります。
その部分もアセンブルすればオブジェクトファイルとなります。
これらのプログラムを構成するプログラム部品としてのオブジェクトファイルを結合するのがリンカです。
リンカによってリンクされたものは実行可能ファイルとなります。
Windows環境では、実行可能ファイルの拡張子は.exeとなります。

広い意味でのコンパイラの多くはこの3段階の操作を見た目は一つの動作として行いますので、
通常、これらの段階は意識されることはありません。
しかし、プログラミングを行うに当たっては、十分にこの段階を意識して行うことが必要となります。

ディレクティブ

ソースプログラムをコンパイルする前に、ソースプログラムに対して行われる前処理をプリプロセスと言います。このプリプロセスを行なうプログラムのことをプリプロセッサと呼びます。通常はコンパイラがプリプロセッサの機能を兼ね備えています。

プリプロセッサに対する指令を「ディレクティブ」と呼びます。C言語プリプロセッサのディレクティブは、先頭にシャープ(#)が付きます。

ディレクティブには以下の種類があります。

ディレクティブ 説明
#define マクロを定義する。
#ifdef シンボルが定義されているときに実行する。
#if 式が真のときに実行する。
#include ヘッダファイルをインクルードする。
#error コンパイラにエラーを発生させる。
#warning コンパイラに警告を発生させる。
#pragma マシンやOS固有の機能をサポートする。

これらのディレクティブによるプリプロセッサへの命令はコンパイルを行う前の処理で行われることを覚えておいてください。

ヘッダファイルの役割

ソースコードに#include <stdio.h>と書かれていると、
コンパイラのプリプロセッサ段階でその部分に、「stdio.h」ファイルの内容が読み込まれます。

ヘッダファイルには、関数プロトタイプ宣言のほか、グローバル変数や独自に作成した型などを記述できます。

標準のヘッダファイルをインクルードするときには<>でファイル名を囲みますが、
自分で作成したヘッダファイルをインクルードするときには""でファイル名を囲みます。

例えば、stdio.hには標準入出力に関する多くの関数のプロトタイプ宣言が含まれており、
開発環境には、それらの関数のソースコードをコンパイルしたオブジェクトファイルが含まれています。
そこで#include <stdio.h>と記述すると、printf()関数やscanf()関数を使うことができるようになります。

変数の型

C言語で扱うことができる変数の型には以下があります。

種類 名前 サイズ(例) 値の範囲(例)
文字型 char 1バイト 英数字1文字 -128~127
文字型 unsigned char 1バイト 英数字1文字 0~255(符号なし)
整数型 short int 2バイト 整数 -32768~32767
整数型 unsigned short int 2バイト 整数 0~65535(符号なし)
整数型 int 4バイト 整数 -2147483648~2147483647
整数型 unsigned int 4バイト 整数 0~4294967295(符号なし)
整数型 long int 4バイト 整数 -2147483648~2147483647
整数型 unsigned long int 4バイ 整数 0~4294967295(符号なし)
浮動小数点型 float 4バイト 単精度浮動小数点数 3.4E-38~3.4E+38
浮動小数点型 double 8バイト 倍精度浮動小数点数 1.7E-308~1.7E+308
浮動小数点型 long double 8バイト 拡張倍精度浮動小数点数 1.7E-308~1.7E+308

一覧には文字列型(JavaでいうString)がありませんが、C言語では、char型の配列がStringに相当しているためです。

printfで文字列に何かを埋め込む

C言語でprintf関数を使って数値や文字を文字列に埋め込むには、以下のようにします。

#include <stdio.h>

/*
 * 数値や文字や文字列を文字列に埋め込む
 */
int main(void){
    printf("整数:%d\n", 321);
    printf("実数:%f\n", 32.1);
    printf("実数(小数点以下2桁):%.2f\n", 3.14);
    printf("文字:%c\n", 'C');
    printf("文字列:%s\n", "あいうえお");
    printf("数式:%d\n", 3 * 2);
    printf("複数埋め込み:%dと%sと%.2f\n", 100, "あいうえお", 3.14);

    return 0;
}

実行結果

整数:321
実数:32.100000
実数(小数点以下2桁):3.14
文字:C
文字列:あいうえお
数式:6
複数埋め込み:100とあいうえおと3.14

ここで注意してほしいのは、文字型('')と文字列型("")は明確に区別されていることです。
文字型は一文字のみです。対して、文字列型には複数字入れることができます。

関数

関数は、下のような姿で定義されます。

戻り値の型 関数名(引数リスト){
    文;
    ・・・
    
    return 式;
}

例えば、キーボードから入力された整数の自乗を表示する関数は以下のように定義できます。

void square(void){

    int num;
    int square;	

    printf("自乗する整数を入力:");
    scanf("%d", &num);

    square = num * num;

    printf("%d の自乗は %d です。\n", num, square);
}

return文で何かしらの値を呼び出し元に返したいときは、その戻り値の型を記述しますが、戻り値が無いときはvoidを記述します。

引数が無いときはvoidを記述します。

変数の記憶寿命(スコープ)

変数が宣言されると、値を格納するためにメモリの領域が確保されます。(グローバル変数はプログラムの開始時に確保されます)

変数の値が初期化された段階で、確保されたメモリに値が書き込まれます。
変数に値を代入すると、確保されたメモリの値が上書きされます。
最終的には、確保されたメモリは開放されるわけですが、そのタイミングが

  • グローバル変数はプログラム全体の終了時
  • ローカル変数は宣言されたブロックを出るとき

の2つで違っています。

ローカル変数でも static という記憶クラス指定子をつけるとプログラムの終了までの記憶寿命を得ることが出来ます。

下記のソースコードを記述、実行してみましょう。

#include <stdio.h>

int a = 0;

void increment(void){

    static int b = 0;
    int c = 0;
    printf("a:%d  b:%d  c:%d\n", a, b, c);

    a++;
    b++;
    c++;
}

int main(void){
    while(a < 5){
        increment();
    }
    return 0;
}

実行結果は以下のようになります。

a:0  b:0  c:0
a:1  b:1  c:0
a:2  b:2  c:0
a:3  b:3  c:0
a:4  b:4  c:0

変数aはグローバル変数なので、プログラムが終了するまで値を保持し続けます。
変数bはローカル変数ですが、staticが付いているので、値を保持し続けます。
変数cはローカル変数なので、関数内の処理が終了すると値が破棄されます。

呼び出す関数と呼び出される関数

これまで、呼び出される関数を呼び出す関数の前に記述してきました。逆にするとどうなるでしょうか?

次のソースコードを記述、実行してみましょう。

#include <stdio.h>

int main(void){

    involution(1, 5);

    return 0;
}

int involution(int a, int b){

    int involution = 1;
    int i;

    for(i = 0; i < b; i++){
        involution *= a;
    }

    return involution;
}

コンパイルしようとすると、下のような警告が表示されます。

implicit declaration of function 'involution'

これはmain()関数でinvolution()という関数を呼び出しているがそのような関数は定義されてないので暗黙的(implicit)に定義したという意味です。

呼び出される関数が呼び出す関数より後に記述されていると、呼び出す関数内で、呼び出される関数名を処理する際に「そんな関数知らない」ということになってしまいます。
コンパイラによっては知らない関数についてもなんとか処理してくれることもありますが意図しない動きになります。

それを防ぐために、下記のような記述を追加します。

#include <stdio.h>

/*involution関数の宣言*/
int involution(int a, int b);

int main(void){

    involution(1, 5);

    return 0;
}

int involution(int a, int b){

    int involution = 1;
    int i;

    for(i = 0; i < b; i++){
        involution *= a;
    }

    return involution;
}

追加した行を関数プロトタイプ宣言と呼びます。この宣言があると、コンパイラはこのような関数がどこかで定義されているという前提の下にコンパイルを進めます。
そこで、関数の定義は、ソースコードのどこでしてもかまわないということになります。

関数プロトタイプは上記のように関数定義の{...}を書かず、代わりに;を記述し、その関数の名前、引数の型と数、戻り値の型をコンパイラに伝えます。

別ファイルの関数を呼び出す

大きなプログラムを作成するときには、ソースコードを関数ごとに分割できれば共同作業が簡単に出来ることになります。
関数プロトタイプ宣言は、ソースコードを複数ファイルに分割する際に活躍します。

次のソースコードをを作成して記述、実行しましょう。
その際、「involution.h」は新規->ヘッダー・ファイルを選択し、作成してください。

involution.h

#ifndef INVOLUTION_H_
#define INVOLUTION_H_

/*involution関数の宣言*/
int involution(int a, int b);

#endif /* INVOLUTION_H_ */

involution.c

int involution(int a, int b){

    int involution = 1;
    int i;

    for(i = 0; i < b; i++){
        involution *= a;
    }

    return involution;
}

Sample.c

#include <stdio.h>
#include "involution.h"

int main(void){

    int integer1;
    int integer2;
    int integer3;

    setvbuf(stdout, NULL, _IONBF, 0);

    printf("累乗される整数を入力:");
    scanf("%d", &integer1);

    printf("何乗しますか?:");
    scanf("%d", &integer2);

    integer3 = involution(integer1, integer2);

    printf("%d の %d 乗は %d です。\n", integer1, integer2, integer3);

    return 0;
}

ここでは次の3つのファイルにソースコードを分割しています。

  1. Sample08_07.c:#includemain()関数(プログラム本体)
  2. involution.c:involution()関数の定義
  3. involution.h:involution()関数の関数プロトタイプ宣言(ヘッダファイル)

1と2が個別にコンパイルされオブジェクトファイルが作成された後、
オブジェクトファイル同士がリンクされ、実行ファイルが生成されます。

static

プログラムの規模が大きくなってくると、関数を別のファイルに定義する、また、その関数の補助関数として関数を定義するが外部からは使わせたくないということがよく起こります。

C言語では明確に別のファイルから呼び出せないようにすることができます。
そのためには関数にstaticを付加します。

下記のソースコードを記述、コンパイルしてみましょう。

add.h

#ifndef ADD_H_
#define ADD_H_

extern int sum;

void add(int a);

#endif /* ADD_H_ */

add.c

int sum = 0;

static void add_internal(int a){
    sum += a;
}

void add(int a){
    add_internal(a);
}

Sample.c

#include <stdio.h>
#include "add.h"

void add_internal(int a);

int main(void){

    int i;

    for (i = 1; i <= 10; i++) {
        add_internal(i);
    }

    printf("加算結果は %d です。\n", sum);

    return 0;
}

Sample.cで関数プロトタイプ宣言を行い、add.cに記述されている補助関数add_internal()を無理やり呼び出そうとしています。しかしこのプロジェクトはコンパイルできず以下のようなリンクエラーになります。

undefined reference to `add_internal'

これはadd.c内でadd_internal()関数にstatic指定子を付けているためです。この場合、コンパイラはadd_internalという名前を外部から参照できないようにします。
Sample.cのコンパイルはvoid add_internal(int a);で「どこかにある」としているので成功しますが、リンクの段階で「add_internalがどこにもない(staticが付けられているのでadd.cにあるadd_internalはリンカからは参照できない)」ことが分かりエラーになります。
このように別のファイルから呼ばれたくない関数についてはstaticを付け、「隠蔽」を行います。

なお、staticはグローバル変数に付けることもでき、グローバル変数に付けた場合も関数に付けた場合と同様に別のファイルから変数を参照することができなくなります。

ポインタ

変数を宣言するとその変数のためにメモリの領域が確保され
変数を初期化すると、そのメモリの領域に値が書き込まれることになります。
変数に値を代入すると、確保されたメモリの領域に値が上書きされます。

ところで、メモリはアドレスで管理されています。
アドレスとは、住所のようなもので、メモリ内での位置を一義的に表します。

32bitCPUの場合は、メモリのアドレスは32bit、
つまり2進数の32桁で表され、それぞれのアドレスには1バイトの情報を記憶させることが出来ます。
32bitすなわち4GB(ギガバイト)ですから、32bitCPUは4GBまでのメモリを管理することが出来るわけです。

32桁の2進数では桁が多すぎて扱いに困るので、C言語では通常、8桁の16進数でメモリのアドレスを扱います。
C言語で変数のアドレスは、アドレス演算子&を使って、&変数名として取得できます。

次のソースコードを記述、実行してみましょう。

#include <stdio.h>

int main(void){

    int a = 10;

    printf("変数aの値:\t\t%d\n", a);
    printf("変数aのアドレス:\t%p\n", &a);

    return 0;
}

実行結果は以下のようになります。(アドレスは異なっているかもしれませんが、問題ありません)

変数aの値:		10
変数aのアドレス:	0028FF24

変換仕様の「%p」はメモリのアドレス(ポインタ)を表示するためのもので、16進数で表示されます。

変数のメモリのアドレスはアドレス演算子&を変数名につけることで知ることができましたが、この値を、何かに代入して使うことができると便利そうです。メモリのアドレスを格納し使うようにできる仕組みがポインタです。

ポインタは型名 *ポインタ名;で宣言することができます。
型名で指定した型を持つ変数のアドレスを代入することができます。

次のソースコードを記述、実行してみましょう。

#include <stdio.h>

int main(void){

    int a;
    int b = 20;
    int *Pa;   /*ポインタの宣言*/

    Pa = &a;   /*変数aのアドレスをポインタに代入*/

    printf("変数aのアドレスをポインタPaに代入しました\n");
    printf("変数aのアドレス:\t%p\n", &a);
    printf("ポインタPaの値:\t%p\n", Pa);

    *Pa = 10;  /*ポインタPaの指す値に値を代入*/

    printf("ポインタPaの指す値に値10を代入しました\n");
    printf("変数aの値:\t\t\t%d\n", a);
    printf("ポインタPaの指す値:\t%d\n", *Pa);

    Pa = &b;   /*変数bのアドレスをポインタに代入*/

    printf("変数bのアドレスをポインタPaに代入しました\n");
    printf("変数aのアドレス:\t%p\n", &a);
    printf("変数bのアドレス:\t%p\n", &b);
    printf("ポインタPaの値:\t%p\n", Pa);

    printf("変数aの値:\t\t\t%d\n", a);
    printf("変数bの値:\t\t\t%d\n", b);
    printf("ポインタPaの指す値:\t%d\n", *Pa);

    return 0;
}

実行結果は以下のようになります。

変数aのアドレスをポインタPaに代入しました
変数aのアドレス:    0028FF24
ポインタPaの値:    0028FF24
ポインタPaの指す値に値10を代入しました
変数aの値:          10
ポインタPaの指す値: 10
変数bのアドレスをポインタPaに代入しました
変数aのアドレス:    0028FF24
変数bのアドレス:    0028FF20
ポインタPaの値:    0028FF20
変数aの値:          10
変数bの値:          20
ポインタPaの指す値: 20

ポインタ変数に他の変数のアドレスを代入することで
間接的にその変数を参照すること、変更することなどが可能となります。
間接的に参照する際には、ポインタ変数の前に間接演算子*をつけます。

ポインタに他の変数のアドレスを代入すると、他の変数を間接的に参照することができるようになります。
つまり、ポインタPaに変数aのアドレスを&aとして代入すると、*Paaと意味的に等しくなり、さらに、ポインタPaに変数bのアドレスを&bとして代入すると、*Pabと意味的に等しくなるということです。

ポインタを作ったが、何も指し示していない状態にしたいというときはNULLを指定します。

ポインタの利用シーン

プログラミングでは、メモリのアドレスをコピーすることをシャローコピー(浅いコピー)、メモリの中にある実体をコピーすることをディープコピー(深いコピー)と呼びます。

プログラムを書いていると、ディープコピーを行いたいシーンというのは実はそれほど多くはありません。メモリの節約や利便性を考えて、普段はシャローコピー(ポインタ)を使用し、どうしても必要な時だけディープコピーを使用するというのは良い考えです。

関数ポインタ

定義された関数もメモリに格納され、そこから読み出されて実行されます。
つまり、関数もメモリのアドレスを持っており、ポインタとして記述、実行することができます。

次のソースコードを記述、実行してみましょう。

#include <stdio.h>

int max(int number, int *numbers){

    int max;
    int i;

    max = numbers[0];
    for (i = 1; i < number; i++){
        if(max < numbers[i]){
            max = numbers[i];
        }
    }
    return max;
}

int main(void){

    int (*pMax)(int number, int *numbers);
	
    int nums[100];
    int number = 0;
    int maxnumber;
    int i;

    pMax = max;

    setvbuf(stdout, NULL, _IONBF, 0);

    printf("数値の数を入力!\n");
    scanf("%d", &number);

    for(i = 0; i < number; i++){
        printf("%d番目の数値:", i + 1);
        scanf("%d", &nums[i]);
    }

    maxnumber = (*pMax)(number, nums);

    printf("\n最大値は %d です。\n", maxnumber);

    return 0;
}

実行結果は以下のようになります。

数値の数を入力!
5
1番目の数値:12
2番目の数値:-21
3番目の数値:35
4番目の数値:-42
5番目の数値:28

最大値は 35 です。

int (*pMax)(int number, int *numbers);でint型の戻り値を持ち、リストのような引数を持つ関数のポインタを定義しています。

pMax = max;でポインタに関数max()のアドレスを代入しています。

maxnumber = (*pMax)(number, nums);でポインタから関数を呼び出しています。

ただ、ポインタを使うメリットは、

関数ポインタの配列

関数ポインタを配列として扱うことができます。

次のソースコードを記述、実行してみましょう。

#include <stdio.h>

void sayDog(char str[]){
    printf("%sDog\n", str);
}

void sayCat(char str[]) {
	printf("%sCat\n", str);
}

void sayPenguin(char str[]) {
	printf("%sPenguin\n", str);
}

int main(void) {

	void (*say[3])();
	say[0] = sayDog;
	say[1] = sayCat;
	say[2] = sayPenguin;

	(*say[0])("One");
	(*say[1])("Two");
	(*say[2])("Three");

	return 0;
}

実行結果は以下のようになります。

OneDog
TwoCat
ThreePenguin

ただ、C言語ではポインタの配列から要素数を取得する方法が用意されていないので、sizeof()で要素数を取得することができないため、for文で回すことができず、使用できるシーンは限られてくると思います。

参考資料
スクールの教科書
C言語 プリプロセッサ