M.Hiroi's Home Page

Linux Programming

お気楽C言語プログラミング超入門

[ PrevPage | Clang | NextPage]

C言語の基礎知識 (1)

プログラミング言語を手続き型言語、関数型言語、論理型言語の 3 つに分類するならば、C言語は手続き型のプログラミング言語になります。プログラムの実行を制御する「文」、データを格納する「変数」、決められた処理を行う「関数」があります。

C言語のプログラムで使用される変数や関数などの名前には、英数字とアンダースコア _ が使えます。英大文字と英小文字は区別されるので、FOO と Foo と foo は異なる名前と判断されます。コメントは /* と */ で囲む伝統的な方法と、最近の規格 (C99) [*1] では // から行末までがコメントになります。

プログラミング言語で扱うデータの種類のことを「データ型 (data type)」といいます。C言語は「数」や「配列」といった基本的なデータ型のほかに、複数のデータをまとめて一つのデータとして扱う「構造体」という機能があります。また、C言語では typedef を使ってユーザーが新しいデータ型の名前を定義することもできます。

このほかに、C言語には悪名高い「ポインタ (pointer)」があります。プログラミング言語の学習には、どの言語にもいくつかの難関があります。C言語ではポインタが最大の難関と言われていますが、コンピュータの基本 (CPU やメモリの概念) を正しく理解していれば、けっして難しい話ではありません。ポインタはあとで詳しく説明する予定です。

-- note --------
[*1] C99 は 1999 年に制定された規格の略称です。2011 年に制定された規格は C11 と呼ばれています。

●こんにちはC言語

それでは皆さんお馴染みの hello, world を表示するプログラムを作ってみましょう。次のリストを見てください。

リスト : hello.c

#include <stdio.h>

int main(void)
{
  printf("hello, world\n");
  return 0;
}

stdio.h のように拡張子が .h のファイルを「ヘッダファイル」といいます。stdio は「標準入出力 (standard input output)」の意味で、shdio.h にはC言語の標準入出力ライブラリで定義されているデータ型や関数の仕様 (関数名や引数と返り値のデータ型) などが記述されています。C言語でライブラリ関数を使用するときはヘッダファイルを #include 命令 [*2] で読み込んでください。ヘッダファイル名は <...> の中に記述します。

次の int main(void){ ... } で main という名前の関数を定義しています。関数はあとで詳しく説明しますが、先頭の int は関数が出力する値のデータ型 (返り値の型)、main が関数の名前、後ろのカッコ ( ) には関数の引数を記述します。引数がない場合は void と書きます。そして、{ ... } の中で実行する処理を記述します。C言語の場合、プログラムの実行は main 関数から開始されます。main 関数が定義されていないとコンパイルエラーになります。

このプログラムでは関数 printf を呼び出します。printf は画面にデータを出力するライブラリ関数です。関数呼び出しは、関数名の後ろにカッコ ( ) を付けます。カッコの中に関数に渡すデータを記述します。ここでは文字列 "hello, world\n" を渡して、それを画面に出力します。最後にセミコロン ( ; ) を付けます。C言語の場合、文の終わりにはセミコロンを付けることに注意してください。

関数は計算した値を出力することができます。これを「返り値」といいます。関数の返り値は return で指定します。main 関数の返り値は、そのプログラムが OS に返す終了コードになります。Windows や Unix 系 OS の場合、終了コードは整数で、0 はそのコマンドが正常に終了したことを表し、0 以外の値は異常終了したことを表します。

プログラムのコンパイルは clang で行います。clang がインストールされていなければ gcc でもかまいません。端末で次のコマンドを入力すると hello という実行ファイルが作成されます。

$ clang -o hello hello.c
$ ls
hello  hello.c
$ ./hello
hello, world

-o は実行ファイル名を指定するオプションです。-o を省略すると実行ファイル名は a.out になります。

-- note --------
[*2] C言語はコンパイルする前にファイルの読み込みや文字列の置換などの前処理を行うことができます。前処理を行うプログラムを「プリプロセッサ」といい、標準的なC言語では cpp が担当します。#include は cpp の命令です。

●数と四則演算

まず最初に、C言語で扱うことができる「数」から説明しましょう。C言語は標準で「整数 (integer)」と「浮動小数点数 (floating point number)」を使うことができます。下表に基本的な数を示します。

表 : 基本的な数 (32 ビット処理系の場合)
型名範囲
char-128 ~ 127 (1 バイト)
unsigned char0 ~ 255 (1 バイト)
short int-32768 ~ 32767 (2 バイト)
unsigned short int0 ~ 65535 (2 バイト)
int-2147483648 ~ 2147483647 (4 バイト)
unsigned int0 ~ 4294967295 (4 バイト)
long int-2147483648 ~ 2147483647 (4 バイト)
unsigned long int0 ~ 4294967295 (4 バイト)
long long int-9223372036854775808 ~ 9223372036854775807 (8 バイト)
unsigned long long int0 ~ 18446744073709551615 (8 バイト)
float±1.1754944E-38 ~ 3.4028235E+38 (32 bit 単精度)
double±2.22507E-308 ~ 1.79769E+308 (64 bit 倍精度)

char は文字 (character) を表すデータ型ですが、文字コードを整数として扱うことができるので、ここでは整数型に含めています。unsigned は無符号整数を表します。short int, long int, long long int の int は省略してもかまいません。C言語では整数値に 0x を付けると 16 進数、0 から始まる整数値は 8 進数となります。

C言語の場合、数値の範囲は処理系によってかわります。規格 (C99) で定められているのは最低限の条件だけです。一般に、32 bit 処理系では int, long が 4 バイトで、64 bit 処理系では int が 4 バイト、long が 4 バイトか 8 バイトになるようです。

さっそく簡単な例を示しましょう。次のリストを見てください。

リスト : 数と四則演算 (sample10.c)

#include <stdio.h>

int a = 10;
int b = 20;
double c = 1.234;
double d = 5.678;

int main(void)
{
  printf("%d\n", a + b);
  printf("%d\n", a - b);
  printf("%f\n", c * d);
  printf("%f\n", c / d);
  return 0;
}
$ clang sample10.c
$ ./a.out
30
-10
7.006652
0.217330

C言語では、あらかじめ使用する変数とそのデータ型を宣言する必要があります。変数の定義は次のように行います。

データ型 変数名1, 変数名2, ..., 変数名N;
データ型 変数名1 = 初期値1, 変数名2 = 初期値2, ..., 変数名N = 初期値N;

まず最初にデータ型を指定して、その後ろに変数を表す名前を書きます。同じデータ型であれば、カンマ ( , ) で区切って複数の変数を定義することができます。このとき、同時に初期値をセットすることができます。これを変数の「初期化」といいます。また、変数に値をセットすることを「代入」といい、代入には記号 = を使います。これを「代入演算子」といいます。初期値は式を与えてもかまいません。式の評価結果が変数の初期値になります。

変数 a, b には整数を、変数 c, d には浮動小数点数を代入します。そして、演算結果を printf で表示します。printf は第 1 引数の文字列 (書式文字列) を画面に表示します。その中に % があると、次の文字を変換指定子と解釈し、その指示に従って残りの引数を文字列に変換して表示します。"%d\n" の d は整数を表示するための指定子で、"%f\n" の f は float, double を表示するための指定子です。\n は改行を表します。

C言語の場合、関数の引数が式や関数呼び出しであれば、それを先に実行して、その結果が関数に渡されます。printf("%d\n", a + b) は引数 a + b が式なので、printf を呼び出す前に a + b を実行して 30 という結果を得ます。したがって、printf が受け取るデータは "%d\n" と 30 になります。printf は変換指定子に従って、引数の値 30 を画面に表示します。

主な算術演算子を下表に示します。

表 : 算術演算子
演算子操作
-x x の符号を反転
x + y x と y の和
x - y x と y の差
x * y x と y の積
x / y x と y の商
x % y x と y の剰余

異なるデータ型が混在する算術式を「混在式 (mixed expression)」といいます。混在式を計算するとき、C言語は情報が失われないようにデータ型を自動的に変換 (型変換) します。たとえば、整数と浮動小数点数を演算する場合、C言語は整数を浮動小数点数に変換してから演算を行います。整数同士でもデータ型が異なる場合、たとえば int と long long int の計算は int を long long int に変換してから行われます。

なお、C言語の代入演算子は代入した値が演算結果になります。たとえば、a = 5; の結果は 5 になります。また、b = (a = 5); は a = 5 の結果である 5 が変数 b に代入されます。代入演算子は他の演算子と違って「右結合」なので、b = (a = 5); は b = a = 5; と書くことができます。

●局所変数と外部変数

変数は関数の中でも定義することができます。これを「ローカル変数 (local variable)」もしくは「局所変数」といいます。C言語では「自動変数」と呼ばれることもあります。これに対し、関数の外で定義されている変数を「グローバル変数 (golbal variable)」もしくは「大域変数」といい、C言語では「外部変数」といいます。局所変数は定義されている関数の中だけしかアクセスできませんが、外部変数は同じファイル内で定義されている関数であればどこからでもアクセスすることができます。詳細は関数の回で詳しく説明します。

変数 a, b, c, d を関数内で定義すると次のようになります。

リスト : 局所変数の定義

#include <stdio.h>

int main(void)
{
  int a = 10;
  int b = 20;
  double c = 1.234;
  double d = 5.678;

  printf("%d\n", a + b);
  printf("%d\n", a - b);
  printf("%f\n", c * d);
  printf("%f\n", c / d);
  return 0;
}

実行結果は同じです。

大域変数の場合、変数の初期値を指定しないと、整数型は 0 に、float 型や double 型は 0.0 に初期化されます。局所変数の場合、初期値を指定しないと初期化は行われないので、変数の値は不定になります。

実際に試してみましょう。

リスト : 大域変数と局所変数の初期化 (sample11.c)

#include <stdio.h>

int a;
double b;

int main(void)
{
  int c;
  double d;
  printf("%d\n", a);
  printf("%f\n", b);
  printf("%d\n", c);
  printf("%f\n", d);
  return 0;
}
$ clang sample11.c
$ ./a.out
0
0.000000
-1632161712
0.000000

変数 a は 0 に、変数 b は 0.0 に初期化されます。変数 c と d は適当な値になりますが、今回は d の値がたまたま 0.0 になりました。

●文字と文字列

文字 (char) は クォート ' で囲んで表します。

'A'
'a'
'0' 

ただし、扱うことができる文字コードは ASCII コードだけです。文字列 (character string) はダブルクォート " で囲んで表します。C言語の場合、文字列は char 型の「配列」です。他のプログラミング言語のように文字列というデータ型があるわけではありません。配列は次回で詳しく説明します。

'...' や "..." の中では \ (バックスラッシュ) は特別な働きをします。\ とその直後の文字で 1 文字を表します。これを「エスケープ・シーケンス (escape sequence)」といいます。画面に表示しにくい文字やキーボードからの入力が難しい制御文字などを表すために使います。よく使用されるものには、改行を表す \n、タブを表す \t、引用符を表す \' や \"、バックスラッシュ自身を表す \\ などがあります。

●if 文

if 文は「条件分岐」を行います。「条件分岐」と書くとなにやら難しい言葉のようにみえますが、プログラムにとっては最も基本的な動作のひとつです。簡単にいうと「もしも~~ならば○○をせよ」という動作です。下図を見てください。


               図 : if 文の動作

図 (1) では、「もしも条件を満たすならば、処理 A を実行する」となります。この場合、条件が成立しないときは処理を実行しませんが、図 (2) のように、条件が成立しないときでも処理を実行させることができます。(2) の場合では、「もしも条件を満たすならば処理 A を実行し、そうでなければ処理 B を実行する」となります。すなわち、条件によって処理 A か処理 B のどちらかが実行されることになります。

一般に、プログラミング言語では、条件が成立することを「真 (true)」といい、条件が不成立のことを「偽 (false)」といいます。実際のプログラムでは真偽を表すデータ型と値が必要になります。C言語の場合、整数型の 0 で偽を表して、それ以外の整数値を真とみなします。なお、最近の規格 (C99) では真偽値を表すデータ型 _Bool が定義され、ヘッダファイル stdbool.h を読み込むと、真偽値を表すデータ型 bool と、真偽値 (true, false) を使用することができます。

_Bool 型は整数型の一種です。したがって、true と false は整数値になります。実際、stdbool.h では true を 1 に、false を 0 に定義しています。true は真の代表と考えてください。本ページでは true と false を使うことにしましょう。

下図に if 文の構文を示します。

if (test) {
  処理A;
  処理B;
  処理C;
} else {
  処理D;
  処理E;
  処理F;
}

図 : if の構文 (1)

条件部 test を実行し、その結果が真であれば、処理A から処理C を実行します。{ } で囲まれた部分を「ブロック」と呼び、ここに複数の処理を書くことができます。then ブロックのことを then 節と呼ぶことがあります。test の結果が偽であれば、else から始まるブロックで書かれている処理D から処理F を実行します。 else ブロック (else 節) は省略することができます。また、ブロック内の処理が一つしかない場合は { } を省略することができます。

もう少し複雑な使い方を紹介しましょう。

if (test_a) {
  処理A;
} else {
  if (test_b) {
    処理B;
  } else {
    処理C;
  }
}

図  : if 文の入れ子

           図 : if 文の入れ子の動作

test_a が偽の場合は else 節を実行します。else 節は if 文なので、条件 test_b を実行します。この結果が真であれば処理 B を実行します。そうでなければ、else 節の処理 C を実行します。この処理は下図のように書き換えることができます。

if (test_a) {
  処理A;
} else if (test_b) {
  処理B;
} else {
  処理C;
}

図 : if の構文 (2)

if 文は eles if を使って複数の if 文を連結することができます。test_a が偽の場合は、次の else if の条件 test_b を実行します。この結果が真であれば処理 B を実行します。そうでなければ、else 節の処理 C を実行します。なお、else if はいくつでも繋げることができます。

●比較演算子と論理演算子

C言語には下表に示す比較演算子と論理演算子が用意されています。

表 : 比較演算子
演算子意味
== 等しい
!= 等しくない
< より小さい
> より大きい
<= より小さいか等しい
>= より大きいか等しい

表 : 論理演算子
操作意味
!x x の否定(真偽の反転)
x && y x が真かつ y が真ならば真
x || y x が真まはた y が真ならば真

&& は左項が偽ならば右項を評価せずに偽を返します。|| は左項が真ならば右項を評価せずに左項の値を返します。このため、&& と || は「短絡演算子」と呼ばれることもあります。

●三項演算子

C言語は三項演算子 ? : を使うことができます。三項演算子の構文を示します。

条件式 ? 真の場合の式 : 偽の場合の式

簡単な例を示しましょう。

// if 文
if (条件式) {
  a = 式1;
} else {
  a = 式2;
}

// 三項演算子
a = 条件式 ? 式1 : 式2;

このように三項演算子を使うとプログラムを簡潔に記述できる場合があります。

●while 文による繰り返し

繰り返しは同じ処理を何度も実行することです。まずは簡単な繰り返しから紹介しましょう。while 文は条件が真のあいだ、ブロックに書かれている処理を繰り返し実行します。

while (test) {
  処理A;
  処理B;
  処理C;
}

図 : while 文の構文

       図 : while 文の動作

上図を見ればおわかりのように、while 文はいたって単純です。なお、処理が一つしかない場合は { } を省略することができます。

簡単な例を示しましょう。Hello, World! を 10 回表示するプログラムを次のリストに示します。

リスト : hello. wolrd の表示 (sample12.c)

#include <stdio.h>

int main(void)
{
  int i = 0;
  while (i < 10) {
    printf("hello, world\n");
    i += 1;
  }
  return 0;
}
$ clang sample12.c
$ ./a.out
hello, world
hello, world
hello, world
hello, world
hello, world
hello, world
hello, world
hello, world
hello, world
hello, world

変数 i を 0 に初期化し、i の値が 10 よりも小さいあいだ処理を繰り返します。i += 1 は i = i + 1 と同じ意味です。このほかに、-=, *=, /=, %= などがあります。i の値はブロックを実行するたびに +1 されていくので、i が 10 になった時点で繰り返しを終了します。

while 文には do { ... } while (test); という形式もあります。これはブロックの処理を実行してから条件部をチェックします。したがって、ブロックの処理は最低でも 1 回は必ず実行されます。do - while 文でプログラムを書き直すと、次のようになります。

リスト : hello, world の表示 (sample13.c)

#include <stdio.h>

int main(void)
{
  int i = 0;
  do {
    printf("hello, world\n");
    i += 1;
  } while (i < 10);
  return 0;
}

これでも hello, world を 10 回表示することができます。

●インクリメント演算子とデクリメント演算子

C言語には値を 1 増やすインクリメント演算子 ++ と値を 1 減らすデクリメント演算子 -- があります。演算子を前に置くと、値を更新してから式を評価します。後ろに置いた場合は式を評価してから値を更新します。次の例を見てください。

リスト : インクリメント演算子 (sample14.c)

#include <stdio.h>

int main(void)
{
  int i = 1, j = 10;
  int x = ++i;
  int y = j++;
  printf("%d\n", i);
  printf("%d\n", j);
  printf("%d\n", x);
  printf("%d\n", y);
  return 0;
}
$ clang sample14.c
$ ./a.out
2
11
2
10

変数 x, y に値をセットするとき、変数 i, j の値を +1 しています。このため、i, j の値は 2 と 11 になります。x の値は演算子 ++ が i の前についているので、i の値を +1 してから、i の値を x に代入します。このため、x の値は 2 になります。逆に、y の値は演算子 ++ が j の後ろに付いているので、j の値を y に代入してから j の値を +1 します。したがって、y の値は 10 になるのです。

簡単な例として、1 から 1000 までの総和を求めてみましょう。

リスト : 1 から 1000 までの総和 (sample15.c)

#include <stdio.h>

int main(void)
{
  int i = 1, sum = 0;
  while(i <= 1000) sum += i++;
  printf("%d\n", sum);
  return 0;
}
$ clang sample15.c
$ ./a.out
500500

変数 i を 1 に、総和を表す変数 sum を 0 に初期化します。while 文で i の値を sum に加えてから、i の値を +1 します。この処理は sum += i++; と表すことができます。++ を i の後ろにつけることで、i の値を sum に加えてから、i の値を +1 することができます。++i とすると正しい値にはなりません。このように、++ と -- はとても便利な演算子ですが、使うときには十分に注意してください。

●FizzBuzz 問題

もう一つ簡単な例題として FizzBuzz 問題をC言語で解いてみましょう。FizzBuzz 問題は 1 から 100 までの値を表示するとき、3 の倍数のときは Fizz を、5 の倍数ときは Buzz を表示するというものです。FizzBuzz 問題の詳細については Fizz Buzz - Wikipedia をお読みください。

プログラムは次のようになります。

リスト : FizzBuzz 問題 (fizzbuzz.c)

#include <stdio.h>

int main(void)
{
  int i = 1;
  while (i <= 100) {
    if (i % 3 == 0 && i % 5 == 0) {
      // 15 の倍数
      printf("FizzBuzz");
    } else if (i % 3 == 0) {
      // 3 の倍数
      printf("Fizz");
    } else if(i % 5 == 0){
      // 5 の倍数
      printf("Buzz");
    } else {
      // 数字をそのまま表示
      printf("%d", i);
    }
    if(i % 20 == 0){
      printf("\n");
    } else {
      printf(" ");
    }
    i++;
  }
  return 0;
}
$ clang fizzbuff.c
$ ./a.out
1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 FizzBuzz 16 17 Fizz 19 Buzz
Fizz 22 23 Fizz Buzz 26 Fizz 28 29 FizzBuzz 31 32 Fizz 34 Buzz Fizz 37 38 Fizz Buzz
41 Fizz 43 44 FizzBuzz 46 47 Fizz 49 Buzz Fizz 52 53 Fizz Buzz 56 Fizz 58 59 FizzBuzz
61 62 Fizz 64 Buzz Fizz 67 68 Fizz Buzz 71 Fizz 73 74 FizzBuzz 76 77 Fizz 79 Buzz
Fizz 82 83 Fizz Buzz 86 Fizz 88 89 FizzBuzz 91 92 Fizz 94 Buzz Fizz 97 98 Fizz Buzz

変数 i を 1 に初期化し、while 文の中で 1 ずつ増やしていきます。最初の if 文で、i が 3 の倍数でかつ 5 の倍数かチェックします。この処理は 15 の倍数をチェックすることと同じなので、条件文を i % 15 == 0 としてもかまいません。そうでなければ、次の else if で i が 3 の倍数かチェックし、次の else if で i が 5 の倍数かチェックします。どの条件も該当しない場合は最後の else 節で i をそのまま出力します。

●数値積分

最後に数値積分で円周率 \(\pi\) を求めてみましょう。区間 [a, b] の定積分 \(\int_a^b f(x)\,dx\) を数値的に求めるには、区間を細分して小区間の面積を求めて足し上げます。小区間の面積を求める一番簡単な方法は長方形で近似することです。この場合、3 つの方法が考えられます。

  1. (b - a) * f(a)
  2. (b - a) * f(b)
  3. (b - a) * f((a + b) / 2)

1 は左端の値 f(a) を、2 は右端の値 f(b) を、3 は中間点の値 f((a + b) / 2) を使って長方形の面積を計算します。この中で 3 番目の方法が一番精度が高く、これを「中点則」といいます。このほかに、台形で近似する「台形則」や、2 次近似で精度を上げる「シンプソン則」という方法があります。

それでは実際に、中点則で \(\pi\) の値を求めてみましょう。\(\pi\) は次の式で求めることができます。

\( \pi = \displaystyle \int_0^1 \dfrac{4}{1 + x^2}\,dx \)

プログラムは次のようになります。

リスト : 数値積分で円周率を求める (midpoint.c)

#include <stdio.h>

int main(void)
{
  int n = 10000;
  double w = 1.0 / n;
  double s = 0.0;
  int i = 1;
  while (i <= n) {
    double x = (i - 0.5) * w;
    s += 4.0 / (1.0 + x * x);
    i++;
  }
  printf("%.14f\n", s * w);
  return 0;
}

変数 n が分割数です。最初に小区間の幅を求めて変数 w にセットします。面積は変数 s にセットします。次の while ループで区間 [0, 1] を n 個に分割して面積を求めます。

最初に x 座標を計算します。中間点を求めるため、変数 i を 1 から始めて、x 座標を次の式で求めます。

x = (i - 0.5) * w

たとえば、変数 i が 1 の場合は 0.5 になるので、x は区間 [0 * w, 1 * w] の中間点になります。あとは、4 / (1 + x * x) を計算して s に加算します。最後に s に w を掛け算して全体の面積を求めます。

printf の % と変換指定子の間には、出力変換書式 (フラグ、最小フィールド幅、精度、変換修飾子) を指定することができます。.14 は精度の指定で、f 変換指定子の場合は小数点以下の桁数を指定します。

実行結果を示します。

$ clang midpoint.c
$ ./a.out
3.14159265442313

今回はここまでです。次回は配列を中心にC言語の基本的な機能について説明します。


初版 2015 年 2 月 1 日
改訂 2023 年 4 月 2 日

Copyright (C) 2015-2023 Makoto Hiroi
All rights reserved.

[ PrevPage | Clang | NextPage ]