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

先頭へ戻る

この章の概要

この章の概要です。

理解の定着・小休止③

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

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

2進数の表現

コンピュータは情報を、2進数の形で表現しています。 2進数は、2進法で表現された数で、0 と 1 の2つの数字だけで数を表現します。

2進数で 1桁分の情報を、ビットという単位で呼びます。 一般に、8ビットは 1バイトにあたります

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

また、n進数の n に当たる数を、基数と呼びます。

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

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

  1. 元の数を n で割り、その余りを書き出す
  2. 元の数が 0 になるまで (1) を繰り返す
  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進数の整数を扱えます。

16進法では、「…7、8、9」と増えていった数は、次に「A、B、C、D、E、F」と増えます。 10進数でいえば、A が 10、B が 11、F が 15 を意味しています。 F の次で桁が増えて、16進数の「10」になります。

C言語では、「100」と書けば 10進数の 100 ですが、「0x100」と書くと 16進数の 100 を表します。 このように、数値の頭に「0x」または「0X」を付けると、その数は 16進数として扱われます
なお、「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" 変換修飾子を使って、"%hd" や "%ld" のように表記します。8進数なら "%ho" や "%lo"、16進数なら "%hx"、"%lx" といったようになります。

符号

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" 変換指定子を使います。8進数は "%o"、16進数は "%x" です。

浮動小数点数

浮動小数点数は、float型、double型、long double型のいずれかで表現します。

これらの型の具体的な大きさに関しては、何も既定されていませんが、 現在の多くの環境では、float型が 4バイト、double型が 8バイトです。 long double型については、VisualStudio では 8バイト、clang では 16バイトです。 なお、float型で表現できる範囲は、必ず double型で表現でき、 double型で表現できる範囲は、必ず long double型で表現できることになっています

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

printf関数で、浮動小数点数を扱うには、"%f" 変換指定子か、"%e" 変換指定子を使います。後者は、科学的記数法という方法で浮動小数点数を表現します。
long double型を扱う場合には、"%Lf" や "%Le" のようにします。

scanf関数sscanf関数で、浮動小数点数を扱うには、"%f" 変換指定子を使います。
"%f" と指定した場合は、float型になります。 double型を扱うのであれば、"%l" 変換修飾子、long double型ならば "%L" 変換修飾子を使って、"%lf" や "%Lf" のように指定します。

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

型の大きさ

C言語では、型の大きさについては、最小の大きさだけが定められています。 また、それに加えて、他の取り決めが存在する箇所もあります。

具体的には、以下のようになっています。 なお、signed と unsigned で、型の大きさに違いはありません。

最小の大きさ 備考
int 16ビット
short 16ビット int型より大きいということはない
long 32ビット int型より小さいということはない
char 8ビット 必ず 1バイト。符号の有無は環境依存。
float 既定されていないが、多くの環境で 4バイト
double 既定されていないが、多くの環境で 8バイト
long double 既定されていない(VisualStudio では 8バイト、clang では 16バイト)

本当の大きさを調べるには、sizeof演算子を使う必要があります。 sizeof演算子の結果は、size_t型で、これは符号無しの整数型です。

#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

限界値

ある型が扱える値の範囲は、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型に変換します。 これを、汎整数拡張(インテグラルプロモーション)と呼びます。 変換順位は、次の順序です。

  1. long、unsigned long
  2. int、unsigned int
  3. short、unsigned short
  4. char、signed char、unsigned char

汎整数拡張は、元の型で表現できるすべての値が、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 が代表的です。 このような、プリプロセスで処理させる命令を、プリプロセッサディレクティブ(前処理命令)などと言います。

マクロ

#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__ に関しては、#line指令を使うと、置換結果を変更できます。

ローカル変数

ブロックスコープを持つ変数のことをよく、ローカル変数(局所変数)と呼びます。 ブロックスコープとは、ブロック内で宣言した変数がもつスコープのことで、宣言位置からブロックの末尾までの間でのみ、 その変数を使用することができます。

(C95 までのルールでは)関数の定義内で、ブロックスコープを持つ変数を宣言する際には、ブロックの先頭で宣言しなければなりません。

C99 でこのルールは撤廃されています。古い慣習は無意味なので、従うべきではありません。

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

#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キーワードを付けずとも、静的記憶域期間を持ちます。 むしろ、グローバル変数に対する 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キーワード付きで宣言し、 ソースファイルに実体を定義するようにします。 しかし、スコープは狭くした方が良いという原則に従って、そもそもファイルスコープを避けるべきです。 代わりの方法として、static付きのグローバル変数があります。

/* 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 付きのグローバル変数は、内部結合という結合規則を持ちます。 内部結合では、1つの有効範囲内にあるすべての宣言が、たった1つの同じ定義を指します。 また、定義を行ったソースファイル以外からは、その定義を使うことができなくなります。

staticキーワードを付けない、通常のグローバル変数の場合は、外部結合となります。 外部結合の場合は、複数ある宣言のすべてが1つの同じ定義を指します。 定義が1つなければなりませんし、1つでなければなりません。

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 をインクルードする必要があります。また、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' という終端を表す文字(ヌル終端文字)で終わる文字の並びです。文字列リテラルは、"" で囲まれた 0文字以上の文字の並びです。 "" であっても '\0' は存在しています。

文字列を変数で扱う場合には、文字型の配列を使います。

文字型 配列名[要素数] = 文字列リテラル;

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

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

なお、文字型の配列に、後から代入する場合には、strcpy関数を使います。

strcpy( str, "abcde" );

構造体

構造体は、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進数に変換して下さい。[★]

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

if( -1 < 1UL ){
}

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

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

#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 );
}

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

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

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

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

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

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

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

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

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

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

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

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

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 となり正しく計算できます。 問題が起こらない理由を説明して下さい。

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

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

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

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

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

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

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


解答ページはこちら

参考リンク

更新履歴

'2018/5/11 用語を統一(文字列定数 -> 文字列リテラル)

'2018/4/2 「VisualC++」という表現を「VisualStudio」に統一。

'2018/3/6 全面的に文章を見直し、修正を行った。

'2018/2/26 「静的グローバル変数」という表現を「static 付きのグローバル変数」に改めた。
「静的関数」という表現を「static 付きの関数」に改めた。

'2018/2/22 「サイズ」という表記について表現を統一。 型のサイズ(バイト数)を表しているところは「大きさ」、要素数を表しているところは「要素数」。

≪更に古い更新履歴を展開する≫





前の章へ(第29章 事前定義マクロとプラグマ)

次の章へ(第31章 ポインタ①(概要))

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

Programming Place Plus のトップページへ


このエントリーをはてなブックマークに追加
rss1.0 取得ボタン RSS