C言語編 第30章 理解の定着・小休止B

このエントリーをはてなブックマークに追加

この章の概要

この章の概要です。

理解の定着・小休止B

さて、この章ではこれまでに見てきた内容の理解を再確認しましょう。 また、1章丸ごとを割くほどでも無い細かい部分について、少し触れていきます。

今回は、以下の範囲が対象です。 型やスコープ、プリプロセッサの理解がテーマとなります。

2進数の表現

コンピュータは情報を、2進数の形で表現しています。 2進数は、0 と 1 の 2つの数字だけで数を表現する形式です。
2進数1桁分の情報をビットという単位で呼びます。 一般に、8ビットは 1バイトにあたります

2進数以外にも、人が一般に使っている 10進数や、C言語のプログラムで登場する 8進数や 16進数なども存在します。 これらをまとめて、一般化して考えるときには、n進数と呼びます。 また、n進数の n に当たる数を基数またはと呼びます。

10進数から n進数への変換

10進数を n進数に変換するには、次の手順を踏みます。

  1. 元の数を n で割り、その余りを書き出す。
  2. 元の数が 0 になるまで@を繰り返す。
  3. 書き出しておいた余りを、逆順に読み取ると、それが n進数に変換した結果になっている。

例えば、10進数の 93 を 2進数に変換するには、次のようにします。

2)93
2)46・・・1
2)23・・・0
2)11・・・1
2) 5・・・1
2) 2・・・1
2) 1・・・0
   0・・・1

n進数から 10進数への変換

n進数を 10進数に変換するには、各桁に重み付けをして、それぞれの合計を計算します。 重みは、下位の桁から順に、基数を 0乗、1乗、2乗…した数です。

例えば、2進数の 1011101 を 10進数に変換するには、次のようにします。

まず、各桁の重みを計算します。 基数は 2 ですから、下位の桁から順に、20、21、22、23、24、25、26、となります。 これはそれぞれ、1、2、4、8、16、32、64 です。

これを元の数 1011101 の各桁と乗算します。 つまり、「1*64 + 0*32 + 1*16 + 1*8 + 1*4 + 0*2 + 1*1」となります。 この結果は 93 であり、これが 10進数に変換した結果になっています。

16進数

C言語では、16進数の整数を扱えます。 例えば、「100」と書けば、10進数の 100 ですが、「0x100」と書けば、16進数の 100 を表します。 このように、数値の頭に「0x」を付ければ、その数は 16進数として扱われます

16進数は、「0〜9」と「A〜F」の合計 16種類の数字・文字を使って表現されます。 10〜15 までの数を 1桁として扱うために、アルファベットを導入する訳で、A が 10、B が 11 … そして、F が 15 を表します。 なお、「0x77E」を「0x77e」と書いても構いません(アルファベットの大文字・小文字は問いません)。

また、printf関数(⇒リファレンス) では "%x" や "%X" というフォーマット指定子が使えます(両者は、A〜F を大文字で出力するか、小文字で出力するかの違いです)。 なお、これらのフォーマット指定を使う場合、負数は使えません
scanf関数(⇒リファレンス)や sscanf関数(⇒リファレンス) でも、同様のフォーマット指定子が使用できますが、こちらは両者に違いはありません。

8進数

8進数は、「0〜7」の数字だけで表現されます。 C言語においては、「0100」のように、先頭に「0」を付けて表現します

printf関数(⇒リファレンス)や scanf関数(⇒リファレンス)、 sscanf関数(⇒リファレンス)では、"%o"フォーマット指定子で 8進数を扱えます。

short と long

int型より小さい型や、大きい型を作るために、shortlong というキーワードが使えます。 ただし、本当に int型より小さい(大きい)型になる保証はなく、この辺りは環境依存となります。

これらのキーワードは、次のように使います。

short int num;
long int num;

また、long型の定数を記述する場合は、数値の末尾に「L」か「l」を付けます

12345678L;

printf関数(⇒リファレンス)や scanf関数(⇒リファレンス)、 sscanf関数(⇒リファレンス)で、 short型の 10進整数を出力には "%h"フォーマット指定子を使い、long型なら "%l"フォーマット指定子を使います。

符号

int、short、long、char といった各種整数型には、signed または unsigned というキーワードを付加でき、 これによって、符号の有無を指示します。
signedキーワードを付けると符号付き整数に、unsignedキーワードを付けると符号無し整数になります。

int num;
signed int num;
unsigned int num;

int、short、long型に関しては、signed や unsigned を付けなければ、signed つまり符号付き整数であるものとみなされます。 しかし、char型に関しては、いずれも付けなかった場合の扱いは環境依存です

なお、signed int型および、unsigned int型は、「int」を省略して、次のように宣言することもできます。

signed num;
unsigned num;

次の例のように、符号無し整数の定数を記述する場合は、数値の末尾に「U」か「u」を付けます

1234U;

printf関数(⇒リファレンス)や scanf関数(⇒リファレンス)、 sscanf関数(⇒リファレンス)で符号無しの 10進整数を扱うには、 "%u"フォーマット指定子を使います。

型の大きさ

C言語では、型の大きさは、最小サイズだけが定められています。 また、それに加えて、他の取り決めが存在する箇所もあります。
具体的には、以下のようになっています。 なお、signed と unsigned で、サイズに関する違いはありません。

最小サイズ 備考
int 16bit
short 16bit int型より大きいということはない
long 32bit int型より小さいということはない
char 8bit 必ず 1Byte。符号の有無は環境依存。
float 既定されていないが、多くの環境で 4Byte
double 既定されていないが、多くの環境で 8Byte
long double 既定されていないが、多くの環境で 10Byte

本当のサイズを調べるには、sizeof演算子を使う必要があります。 sizeof演算子の結果は、符号無しの整数です。

#include <stdio.h>

int main(void)
{
	printf( "int型の大きさは %uByte\n", sizeof(int) );
	printf( "unsigned int型の大きさは %uByte\n", sizeof(unsigned int) );
	printf( "short int型の大きさは %uByte\n", sizeof(short int) );
	printf( "long int型の大きさは %uByte\n", sizeof(long int) );
	printf( "char型の大きさは %uByte\n", sizeof(char) );
	printf( "float型の大きさは %uByte\n", sizeof(float) );
	printf( "double型の大きさは %uByte\n", sizeof(double) );

	return 0;
}

実行結果:

int型の大きさは 4Byte
unsigned int型の大きさは 4Byte
short int型の大きさは 2Byte
long int型の大きさは 4Byte
char型の大きさは 1Byte
float型の大きさは 4Byte
double型の大きさは 8Byte

浮動小数点数

浮動小数点数は、float型、double型、long double型のいずれかで表現します。
これらの型の具体的な大きさに関しては、何も既定されていませんが、 現在の多くの環境では、float型が 4Byte、double型が 8Byte、long double型が 10Byte です。 なお、float型で表現できる範囲は、必ず double型で表現でき、 double型で表現できる範囲は、必ず long double型で表現できることになっています

float型の定数には、末尾に「F」か「f」を付け、long double型の場合は「L」か「l」を付けます。 これらを付けなければ、double型として扱われます。 これらの文字は、浮動小数点接尾語と呼ばれます。

printf関数(⇒リファレンス)で、 浮動小数点数を扱うには、"%f"フォーマット指定子か、"%e"フォーマット指定子を使います。 後者は、科学的記数法という方法で小数を表現します。

scanf関数(⇒リファレンス)、 sscanf関数(⇒リファレンス)で、 浮動小数点数を扱うには、float型ならば "%f"フォーマット指定子、double型ならば "%lf"フォーマット指定子、 long double型ならば "%Lf"フォーマット指定子を使います。

科学的記数法は、3.402823 を 1038 した値を、3.402823e+038 と表現するような記法です。 表記の途中にある「+」は、指数が正であることを表しており、省略可能です。 指数が負なら「-」にします。「e」は単なる区切りと考えて構いません

限界値

ある型が扱える値の範囲は、limits.hfloat.h に定義されているマクロを利用して調べることができます。

#include <stdio.h>
#include <limits.h>
#include <float.h>

int main(void)
{
	printf( "          char型の最小値は %d、最大値は %d\n", CHAR_MIN, CHAR_MAX );
	printf( "   signed char型の最小値は %d、最大値は %d\n", SCHAR_MIN, SCHAR_MAX );
	printf( " unsigned char型の最小値は %u、最大値は %u\n", 0, UCHAR_MAX );
	printf( "\n" );
	printf( "           int型の最小値は %d、最大値は %d\n", INT_MIN, INT_MAX );
	printf( "  unsigned int型の最小値は %u、最大値は %u\n", 0, UINT_MAX );
	printf( "\n" );
	printf( "         short型の最小値は %d、最大値は %d\n", SHRT_MIN, SHRT_MAX );
	printf( "unsigned short型の最小値は %u、最大値は %u\n", 0, USHRT_MAX );
	printf( "\n" );
	printf( "          long型の最小値は %ld、最大値は %ld\n", LONG_MIN, LONG_MAX );
	printf( " unsigned long型の最小値は %lu、最大値は %lu\n", 0, ULONG_MAX );

	printf( "\n" );
	printf( "         float型の最小値は %f、最大値は %f\n", -FLT_MAX, FLT_MAX );
	printf( "\n" );
	printf( "        double型の最小値は %f、最大値は %f\n", -DBL_MAX, DBL_MAX );
	printf( "\n" );
	printf( "   long double型の最小値は %f、最大値は %f\n", -LDBL_MAX, LDBL_MAX );

	return 0;
}

実行結果:

          char型の最小値は -128、最大値は 127
   signed char型の最小値は -128、最大値は 127
 unsigned char型の最小値は 0、最大値は 255

           int型の最小値は -2147483648、最大値は 2147483647
  unsigned int型の最小値は 0、最大値は 4294967295

         short型の最小値は -32768、最大値は 32767
unsigned short型の最小値は 0、最大値は 65535

          long型の最小値は -2147483648、最大値は 2147483647
 unsigned long型の最小値は 0、最大値は 4294967295

         float型の最小値は -340282346638528860000000000000000000000.000000、最大
値は 340282346638528860000000000000000000000.000000

        double型の最小値は -1797693134862315700000000000000000000000000000000000
00000000000000000000000000000000000000000000000000000000000000000000000000000000
00000000000000000000000000000000000000000000000000000000000000000000000000000000
00000000000000000000000000000000000000000000000000000000000000000000000000000000
00000000000000000.000000、最大値は 179769313486231570000000000000000000000000000
00000000000000000000000000000000000000000000000000000000000000000000000000000000
00000000000000000000000000000000000000000000000000000000000000000000000000000000
00000000000000000000000000000000000000000000000000000000000000000000000000000000
000000000000000000000000.000000

   long double型の最小値は -1797693134862315700000000000000000000000000000000000
00000000000000000000000000000000000000000000000000000000000000000000000000000000
00000000000000000000000000000000000000000000000000000000000000000000000000000000
00000000000000000000000000000000000000000000000000000000000000000000000000000000
00000000000000000.000000、最大値は 179769313486231570000000000000000000000000000
00000000000000000000000000000000000000000000000000000000000000000000000000000000
00000000000000000000000000000000000000000000000000000000000000000000000000000000
00000000000000000000000000000000000000000000000000000000000000000000000000000000
000000000000000000000000.000000

汎整数拡張と通常の算術型変換

式の中に登場する整数型の大きさが int型以下の場合、int型や unsigned int型に拡張されます。 これを、汎整数拡張(インテグラルプロモーション)と呼びます。
int型に拡張されるのか、unsigned int型に拡張されるのかは、値が int型で表現できるかどうかに依ります。 int型で表現できない場合には、unsigned int型に拡張されます。

また、2つのオペランドを持つ演算子によって計算が行われる際にも、型の変換が起こります。 これを通常の算術型変換と呼びます。

まとめると、次の規則によって変換が起こります。

キャスト

明示的に型変換を行うことをキャストと呼びます。 キャストは次の構文で行います。

(型名)値

このような構文で、値を強制的に任意の型に変換します。 キャストの効力はその場限りであって、以降ずっと型変換されたままになるという訳ではありません。

例えば、int型の変数num があるとき、「(double)num / 3」は、キャストの効力によって「double型 / int型」になります。 更に、通常の算術型変換によって、これは「double型 / double型」として計算されることとなり、小数点以下も正しく計算されます。

型の使い分け

まず、整数が必要な場面では基本的に int型を優先的に使います

データ量を削減しなければならない正当な理由がある場合は、short型や char型のような小さな型が使えますが、可能であれば避けるべきです。 int型よりも小さい型を使っても、汎整数拡張によって、計算処理は int型以上の大きさで行われます。 その計算結果を再び short型などの小さな型に格納すると、情報が切り詰められることになり、注意していないと、うっかり情報を失うことになります。

unsigned についても、必要がない限りは避けるべきです。 signed と unsigned とで、一方でしか表現できないような値が登場すると、他方の型に変換したときに問題が起こる可能性があります。

浮動小数点数を使う際は、double型を基本として考えます。 データ量の削減のために float型を使うことは考えられますが、それ以外の理由では double型で統一すべきです。 「float型の方が高速になる」という話は、まず疑ってかかりましょう(もちろん、実測すべきです)。
なお、long double型を使う機会はありません。

構文糖

プログラムを、短く簡潔に記述できるようにするために用意された構文を、構文糖(シンタックスシュガー)と呼びます。

「+=」のように、演算と代入を合体させた演算子を複合代入演算子と呼びます。

次のように、代入を連続的に行うことが出来ます。 また、型が同じ変数は、カンマで区切ってまとめて宣言できます。

int a, b, c;
a = b = c = 100;

変数をまとめて宣言する場合に、それぞれに初期値を与えることもできます。

int a, b = 10, c = 20;

カンマ演算子を使うと、複数の式を連結できます。

#include <stdio.h>

int main(void)
{
	int i, j;

	for( i = 0, j = 100; i < j; ++i, --j ){
		printf( "%d %d\n", i, j );
	}
	
	return 0;
}

実行結果:

0 20
1 19
2 18
3 17
4 16
5 15
6 14
7 13
8 12
9 11

カンマ演算子(,) で区切られた 2つの式は、左側から処理されます

条件演算子は、if文の変形のようなもので、演算子を使って分岐構造が実現できます。

条件式 ? 真のときの式 : 偽のときの式

文字列定数を続けて記述すると、それぞれが連結して書かれたことになります。

#include <stdio.h>

int main(void)
{
	char str[] = "abcde"
	             "fghij";

	puts( str );
	
	return 0;
}

実行結果:

abcdefghij

プリプロセス

プログラムを記述し、それを実行するまでの流れは、「プリプロセス⇒コンパイル⇒リンク⇒実行」となります。 このように、プリプロセス(前処理)は、コンパイル作業よりも前にあります。

プリプロセスで処理する部分は、先頭が「#」で始まる行で、#include が代表的です。 このような、プリプロセスで処理させる命令を、プリプロセッサディレクティブ(前処理命令)などと言います。

マクロ

#defineディレクティブを使うと、マクロ置換が実現できます。 マクロ置換とは、ソースコード上の文字の並びを、別の文字の並びに置き換えることを言います。 次のような形式で記述します。

#define マクロ名 置換後の文字の並び
#define マクロ名(引数のリスト) 置換後の文字の並び

前者の形式を、オブジェクト形式マクロといい、後者を関数形式マクロといいます。 なお一般的に、マクロ名はすべて大文字で書くことが多いです。

#define の効果は、その定義が記述された場所よりも後方であれば、どこまでも適用されます。 要するに、スコープの概念がありません。 ただし、#undefディレクティブを使えば、#define の効果を無効化することができます。

関数形式マクロを使う際には、

#define MAX(a,b) ((a) > (b) ? (a) : (b))

のように、与えられた引数を ( ) で囲むようにし、更に全体を ( ) で囲むように記述すべきです。 こうすることで、使用時の安全性が増します。 ただし、次のようなインクリメント及びデクリメントの際に起こる問題は解決できません。

int ans = MAX(++a, 5);

プリプロセスによる分岐処理

#if#ifdef#ifndef といったディレクティブを使うと、 プリプロセスの段階での分岐処理が実現できます。 これらのディレクティブは全て、#endif で終端を表します。

#if DEBUG == 1
#endif

#ifdef DEBUG
#endif

#ifndef DEBUG
#endif

また、#else および #elif を使って、多方向の分岐も表現できます。

#if DEBUG == 1
#elif RELEASE == 1
#else
#endif

#ifdef と #ifndef は、#define でマクロを定義されているかどうかで分岐を行うものです。 これは、definedキーワードを使えば #if や #elif でも表現可能です。

#if defined(DEBUG)
#endif

#if !defined(DEBUG)
#endif

事前定義マクロ

何らかのヘッダファイルをインクルードしなくても使える、あらかじめ定義されているマクロを、事前定義マクロといいます。

__FILE__は、このマクロを記述しているファイルの名前に置換されます。
__LINE__は、このマクロを記述している行の行番号に置換されます。
__DATE__は、コンパイルを行ったときの日付に置換されます。
__TIME__は、コンパイルを行ったときの時刻に置換されます。
__STDC__は、コンパイラが標準規格に準拠しているかどうかを調べます。

__FILE__ と __LINE__ に関しては、#line指令を使うと、置換結果を変更できます。

ローカル変数

特定の関数内だけで使用できる変数を、ローカル変数(局所変数)と呼びます。 ローカル変数は、ブロックの始まるところで宣言しなければなりません

ブロック内で宣言された変数は、そのブロックか、それよりも内側のブロックからしかアクセスできません。 このような、アクセスが可能な範囲という考え方をスコープと呼びます。 ブロックでスコープが定まるような場合、これをブロックスコープと呼びます。
ここでのブロックは、{ と } で囲まれた範囲ということなので、if文や for文、関数の中身全体を取り囲む { } もブロックです。

ブロックスコープの考え方の下では、異なるブロックで宣言された変数は、名前が同じであっても、別の変数であるとみなされます。 内側のブロックからは、外側のブロックで宣言された変数にはアクセスできません。 このような性質を隠蔽(いんぺい)と呼びます。

#include <stdio.h>

int main(void)
{
	int num = 10;
	
	printf( "%d\n", num );
	{
		int num = 20;  /* ここもブロックの先頭である */

		printf( "%d\n", num );  /* 内側のブロックの num が見えている */
	}
	
	return 0;
}

実行結果:

10
20

また、ローカル変数の宣言時に、static というキーワードを付けると、静的変数(静的ローカル変数)になります。

static int num;

静的変数は、どこに宣言していようとも関係なく、プログラムの実行開始時にメモリ上に作られ、初期値も与えられます。 そして、プログラムの実行が終了する直前まで、消えることなくメモリ上に存在し続けます。 なお、静的変数は、具体的な初期値を与えなくても、必ず 0 で初期化されることが保証されていますが、明示的に初期値を与えることをお勧めします

#include <stdio.h>

void myprint(void);

int main(void)
{
	int i;
	
	for( i = 0; i < 5; ++i ){
		myprint();
	}
	
	return 0;
}


void myprint(void)
{
	static int num = 0;   /* プログラム開始時に作られて、ずっとそのまま */
	
	num += 10;            /* num はずっと記憶されるので、呼び出すたびに値は増える */
	printf( "%d\n", num );
}

実行結果:

10
20
30
40
50

静的変数のように、プログラムの実行の開始から終了まで存在し続ける場合、この存在期間を、静的記憶域期間と呼びます。 一方、静的変数でないローカル変数は、宣言されている関数内でだけ存在でき、これを自動記憶域期間と呼びます。

グローバル変数

ローカル変数に対し、関数の外側で宣言される変数を、グローバル変数(大域変数)と言います。

グローバル変数は、staticキーワードを付けたローカル変数同様、静的記憶域期間を持ちます。 従って、プログラムの実行が開始されたときに作られ、実行が終了するときまで、ずっと存在し続けています。 明示的に初期値を与えなくても、0 で初期化されるという点も同様です。

また、グローバル変数のスコープはプログラム全体に及び、これを、グローバルスコープ(大域スコープ)と呼びます。
複数のソースファイルを使う場合に、このように広大なスコープを持たせると、プログラムを難解で再利用しづらいものにしてしまいがちです。 そこで、グローバル変数の宣言時に staticキーワードを用いることで、スコープを、単体のソースファイルだけに限定することができます。 staticキーワード付きのグローバル変数を、静的グローバル変数と呼び、この場合のスコープを、ファイルスコープと呼びます。

複数ファイルの連携

1つのプログラムに、複数のソースファイルが含まれる場合があります。 例えば、次のようになります。

/* main.c */
#include <stdio.h>
#include "print.h"


int main(void)
{
	printNum( 100 );
	printNum( 200 );
	printNum( 300 );
	
	printf( "%d\n", gLastPrintNum );

	return 0;
}
/* print.c */
#include <stdio.h>
#include "print.h"


int gLastPrintNum = 0;

void printNum(int num)
{
	printf( "[[ %d ]]\n", num );
	gLastPrintNum = num;
}
/* print.h */
#ifndef PRINT_H
#define PRINT_H

extern int gLastPrintNum;


void printNum(int num);

#endif

実行結果:

[[ 100 ]]
[[ 200 ]]
[[ 300 ]]
300

それぞれ、main.c、print.c、print.h という名前のファイルで、このうち print.h はヘッダファイルと呼ばれます。 ヘッダファイルの中身は、#include を使って、他のファイルに取り込んで使用します。

ヘッダファイルを作成する際には、必ずインクルードガードを行っておきましょう。 これは、

#ifndef MY_HEADER_H
#define MY_HEADER_H

/* ヘッダの中身はここに書く */

#endif

このように #ifndef、#define、#endif を使って、ヘッダの中身を取り囲むことで、このヘッダファイルを2度以上インクルードしても、 定義の重複が起こらないようにするものです。 これをしなくても問題がないとしても、必ず行うクセを付けておくべきです。

print.h には、externキーワードの付いたグローバル変数が存在します。 externキーワードを付けて変数を宣言すると、その変数の本物の実体は、別のところに存在していることを表現できます。
この場合、print.h にある gLastPrintNum は、実はそこにはなく、別のところ(print.c)にある gLastPrintNum が本物です。 従って、extern を使うのなら、extern が付かない本物の実体がどこかに必要になります

このように、グローバルスコープを持つ変数を使う際には、ヘッダファイルに externキーワード付きで宣言し、 ソースファイルに実体を定義するようにします。 しかし、スコープは狭くした方が良いという原則に従って、そもそもグローバルスコープを避けるべきです
そこで、静的グローバル変数が登場する訳です。

/* main.c */
#include <stdio.h>
#include "print.h"

int main(void)
{
	printNum( 100 );
	printNum( 200 );
	printNum( 300 );
	
	printf( "%d\n", getLastPrintNum() );

	return 0;
}

/* print.c */
#include <stdio.h>
#include "print.h"

static int gLastPrintNum = 0;

void printNum(int num)
{
	printf( "[[ %d ]]\n", num );
	gLastPrintNum = num;
}


int getLastPrintNum(void)
{
	return gLastPrintNum;
}
/* print.h */
#ifndef PRINT_H
#define PRINT_H

void printNum(int num);


int getLastPrintNum(void)

#endif

実行結果:

[[ 100 ]]
[[ 200 ]]
[[ 300 ]]
300

このように、ソースファイル側に静的グローバル変数を定義し、ヘッダファイルには、その変数をアクセスするための関数を宣言します。

また、静的グローバル変数は内部結合という結合規則を持ちます。 これは、外部のファイルには名前を公開しないというものです。 つまり、同じ名前の変数が、別のソースファイルにあっても構わない訳です。
staticキーワードを付けない、通常のグローバル変数の場合は、外部結合となり、これだと同じ名前の変数があると衝突してしまいエラーになります。
要するに、静的グローバル変数の方が、他のファイルのことを気にしなくて済むということです。

staticキーワードは、関数に対して使うこともでき、この場合は静的関数と呼ばれるものになります。 静的関数は、そのソースファイル内でしか呼び出せない関数です。

/* main.c */
#include "score.h"

int main(void)
{
	printScore( 70 );
	printScore( 90 );
	printScore( 50 );
	printScore( 91 );

	return 0;
}
/* score.c */
#include <stdio.h>
#include "score.h"

static void printRank(int score);

void printScore(int score)
{
	printf( "SCORE: %d  ", score );
	printRank( score );
	printf( "\n" );
}


static void printRank(int score)
{
	printf( "RANK: " );

	if( score > 90 ){
		printf( "S" );
	}
	else if( score > 70 ){
		printf( "A" );
	}
	else if( score > 50 ){
		printf( "B" );
	}
	else{
		printf( "C" );
	}
}


/* score.h */

void printScore(int score);

実行結果:

SCORE: 70  RANK: B
SCORE: 90  RANK: A
SCORE: 50  RANK: C
SCORE: 91  RANK: S

#演算子と ##演算子

#演算子は、マクロの置換後の文字の並びの中でのみ使用できます。 この演算子は、マクロの引数を、"" で囲んだ文字列リテラルに置き換える効果があります

#include <stdio.h>

#define LOG_INT(var)	printf(#var ": %d\n", var)

int main(void)
{
	int num1 = 123;
	int num2 = -350;

	LOG_INT( num1 );
	LOG_INT( num2 );
	
	return 0;
}

実行結果:

num1: 123
num2: -350

##演算子も、マクロの置換後の文字の並びの中でのみ使用できます。 この演算子は、## の前後にある字句を連結します

#include <stdio.h>

#define CAT(first,second)	first ## second

int main(void)
{
	int num1 = 10;
	int num2 = 20;
	int num3 = 30;

	printf( "%d\n", CAT(num, 1) );
	printf( "%d\n", CAT(num, 2) );
	printf( "%d\n", CAT(num, 3) );
	
	return 0;
}

実行結果:

10
20
30

アサート

assert (⇒リファレンス)というマクロは、 デバッグの助けとして非常に有益です。 assert は「アサート」と読み、「表明する」という意味です。

このマクロは、プログラム内の任意の場所で、「この時点でこうなっていなければならない」という予定を表明するものです。 プログラムを実行したとき、もし、その予定通りの状態になっていなければ、プログラムをその場で停止させます。

assertマクロを使用するには、assert.h を include する必要があります。 また、assertマクロは、NDEBUG (⇒リファレンス)というマクロ(記号定数)が定義されていない場合にだけ有効になります

配列

配列とは、変数が複数連結されたものです。 配列の宣言は次のように書きます。

データ型 配列名[要素数];
データ型 配列名[要素数] = { 0番目の初期値, 1番目の初期値, … };
データ型 配列名[] = { 0番目の初期値, 1番目の初期値, … };

配列に含まれる1つ1つの領域を要素と言います。 初期値を与える場合には、2つ目や3つ目の形式のように、{ } を使って、各要素ごとの初期値を指定します。 初期値を与える場合に限って、要素数の指定を省略することができ、その場合は、与えた初期値の個数に応じて、要素数が決定されます。
なお、2つ目の形式において、要素数よりも、与えた初期値の個数の方が少ない場合、不足分には 0 が補われます。 逆に、与えた初期値の個数の方が多い場合は、コンパイルエラーになります。

配列の要素には、添字(そえじ)という整数を使ってアクセスします。 添字は、0以上で要素数未満の整数です。 例えば、要素数 5 の配列であれば、添字の範囲は 0〜4 です。 この範囲外へのアクセスすることは不正な行為です。

最後に、使い方の例を挙げます。

#include <stdio.h>

#define SIZE_OF_ARRAY(array)	(sizeof(array)/sizeof(array[0]))

int main(void)
{
	int array[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
	int i;

	for( i = 0; i < SIZE_OF_ARRAY(array); ++i ){
		printf( "%d ", array[i] );
	}
	printf( "\n" );
	
	return 0;
}

実行結果:

0 1 2 3 4 5 6 7 8 9

ここで、SIZE_OF_ARRAYマクロは、配列の要素数を計算的に求める関数形式マクロです。 sizeof演算子で配列全体のサイズと、要素1つ分のサイズを求めて、除算すれば要素数が分かるという仕組みです。

文字列

文字列というのは、文字列定数(文字列リテラル)のことです。 要するに、"" で囲まれた 0文字以上の文字の並びですが、定数でない文字の並びであっても、文字列と呼ぶことが多いです。

文字列に対して、何らかの処理を加えたいときには、文字の配列(char型の配列) を利用することになります。

データ型 配列名[要素数] = "文字列";

このような初期化の構文が許可されるのは、char型の配列のときだけです。 配列のところで確認したように、通常、配列の初期値は { } で囲んで与えるものです。

文字列の末尾には、'\0' という特殊な終端文字が存在することを忘れないで下さい。 先ほどのように初期化を行った場合も同様で、自動的に '\0' が付加されています。

なお、文字の配列に、後から代入する場合には、

strcpy( str, "abcde" );

というように、strcpy関数(⇒リファレンス)を使います。 単なる代入ではダメです。

構造体

構造体は、複数の変数を1つにまとめたものです。 配列とは違い、含まれる要素(構造体の場合はメンバフィールドなどと呼びます)の型は異なっても構いません。

struct タグ名 {
	型 メンバ名;
	型 メンバ名;
	  :
};

構造体を表すキーワード struct を使います。 ここで注意してほしいことは、上のような形式で書いたとき、これはまだ「構造体を変数として宣言していない」という点です。 上の記述は、「新しい型を作った(型を定義した)」だけです。

タグ(構造体タグ)は、新しく構造体として定義した型を識別するための名前です(タグとは名札のことです)。 構造体の変数を宣言する際には、structキーワードとタグ名をセットにして、型を表現します。

struct タグ名 構造体変数名;

構造体のメンバにアクセスするには、ドット演算子を使います。

構造体変数名.メンバ名;

構造体変数を宣言したときに、同時に初期値を与えて初期化することも可能です。

struct タグ名 構造体変数名 = { 1つ目のメンバの初期値, 2つ目のメンバの初期値, … };

構造体変数は、同じ型同士であれば、代入が可能です。 ただし、==演算子などによる比較は、たとえ同じ構造体型であっても不可能です


構造体のタグは省略することもできます。 ただしその場合は、構造体を定義するときに、構造体変数の宣言も同時に行う必要があります。
また、タグ名がないため、プログラム内の他の箇所で、この構造体の型の名前が表現できなくなってしまいます。 これは、他の箇所では、この構造体の変数を宣言できないことを意味します。

また、typedef を使うという方法もあります。

typedef struct タグ名(省略可) {
	型 メンバ名;
	型 メンバ名;
	  :
} 型名;

上の構文にあるように、typedef を使った場合には、タグ名の省略が許されます。 typedef を使わずにタグを省略する場合と異なり、typedef を使うと、型名の記述を必要としますが、 そのため、プログラム内の他の箇所でも、この構造体の型を表現できます。
またこの場合、タグ名と型名がまったく同じになることも許されますが、分かりにくくなるので、やめた方が無難です

#include <stdio.h>

#include <string.h>

#define STUDENT_NAME_LEN 32         /* 生徒の名前データの最大長 */

/* 生徒のデータ */
typedef struct {
	char  name[STUDENT_NAME_LEN];   /* 名前 */

	int   grade;                    /* 学年 */
	int   class;                    /* 所属クラス */
	int   score;                    /* 得点 */

} Student;


void printStudentData(Student student);

int main(void)
{
	Student student;

	strcpy( student.name, "Saitou Takashi" );
	student.grade = 2;
	student.class = 3;
	student.score = 80;

	printStudentData( student );
	
	return 0;
}

/*
	生徒のデータを出力する。
	引数:
		student: 出力するデータを集めた構造体変数。
*/
void printStudentData(Student student)
{
	printf( "name: %s\n", student.name );
	printf( "grade: %d\n", student.grade );
	printf( "class: %d\n", student.class );
	printf( "score: %d\n", student.score );
}

実行結果:

name: Saitou Takashi
grade: 2
class: 3
score: 80

typedef を使って構造体を定義すると、型名が必要な場面で「struct」が省略できます。


なお、typedef はそもそも、既存の型に新しい名前を付けるためのキーワードです。

typedef 既存の型名 新しい型名;
既存の型名 typedef 新しい型名;


練習問題

まとめとして、多めに練習問題を用意しました。★の数は難易度を表します。

問題@ 2進数の 0111101 を 10進数と 16進数に変換して下さい。[★]

問題A 次の条件式が偽になる理由を説明して下さい。[★★]

if( -1 < 1UL ){
}

問題B 自分の使っているコンパイラが提供している、limits.h などの標準ヘッダの内容を確認してみて下さい。[★]

問題C 次のプログラムの実行結果を答えて下さい。[★]

#include <stdio.h>

int num = 0;

void func1(void);
void func2(int num);

int main(void)
{
	int num = 1;

	func1();
	func2( num );
	{
		int num = 2;
		func1();
		func2( num );
	}
	
	return 0;
}

void func1(void)
{
	printf( "%d\n", num );
}

void func2(int num)
{
	printf( "%d\n", num );
}

問題D 次のような配列があります。[★★]

#define ARRAY_SIZE 5
int values[ARRAY_SIZE] = { 13, 27, 75, 27, 48 };

この配列の中に、同じ値が重複して含まれているかどうかを調べるプログラムを作成して下さい。

問題E 次のような配列があります。[★★]

#define ARRAY_SIZE 5
int values1[ARRAY_SIZE] = { -17, 8, 29, -5, 13 };
int values2[ARRAY_SIZE] = { 64, -5, 17, -22, -38 };

2つの配列の両方に同じ値が含まれているかどうかを調べるプログラムを作成して下さい。

問題F 次のような配列があります。[★★★]

#define ARRAY_SIZE 5
double values[ARRAY_SIZE] = { 6.2, 9.71, 3.05, 8.6, 4.19 };
double result[ARRAY_SIZE];

values の中身を昇順(小さい方→大きい方に並んだ状態)に並び変えた結果を、 result に格納するプログラムを作成して下さい。

問題G 生徒の名前と得点を保持できるような構造体を定義して下さい。 その構造体の配列を要素数5 で宣言し、適当な内容で初期化を行って下さい。 [★]

問題H 問題Gで作成したデータを、各生徒を得点の順番で順位付けするプログラムを作成して下さい。 出力結果には、順位・生徒の名前・得点を含むようにして下さい。 同じ得点の場合の順位の扱いは任意とします。[★★★]

問題I 次のプログラム片を見て下さい。[★]

signed char c1 = 120;
signed char c2 = 60;
signed char c3 = -100;
signed char result = c1 + c2 + c3;

signed char型で表現できる最大値は 127 であり、c1 + c2 の段階で溢れ出してしまうように思えます。 実際には、最終的な result の値は、80 となり正しく計算できます。 問題が起こらない理由を説明して下さい。

問題J ビルドを行った日付と時間を、標準出力に出力するプログラムを作成して下さい。 [★]

問題K 次のような文字の配列があります。[★★★]

char str1[] = "abcdef";
char str2[] = "abcdef";

str1 と str2 の内容が一致しているかどうかを調べるプログラムを、strcmp関数を使わずに自作して下さい。

問題L デバッグ作業中にだけ有効になるような puts関数を作成して下さい。 また、追加情報として、整数を1つだけ渡せるような拡張版も作成して下さい。 [★★]

問題M 標準入力から受け取った 10進数の正の整数を、2進数で標準出力に出力するプログラムを作成して下さい。 [★★★]

問題N 標準入力から受け取った 2進数の文字列を、10進数で標準出力に出力するプログラムを作成して下さい。 [★★★]


解答ページはこちら

参考リンク

更新履歴

'2015/12/27 SIZE_OF_ARRAYマクロの定義を修正。

'2015/8/24 ソースコード内の色変え位置が間違っている箇所があったのを修正。

'2009/10/21 新規作成。



前の章へ

次の章へ

C言語編のトップページへ

Programming Place Plus のトップページへ