C言語/中級者向けの話題

出典: フリー教科書『ウィキブックス(Wikibooks)』

配列の文法についての制限[編集]

配列は一括代入できません[編集]

C言語では、FortranPerl のような配列同士の一括代入はできません。

#include <stdio.h>

int main() {
  int a[] = {2, 3, 5, 7, 11},
      b[sizeof a / sizeof *a];
  for (int i = 0; i < sizeof a / sizeof *a; i++)
    b[i] = a[i];
  printf("b[3] = %d\n", b[3]);
}

のように、要素ごとに代入する必要があります。

    int a[] = { 2, 3, 5, 7, 11 },

は、一見すると配列に代入しているように見えますが、これは初期化の構文です。

また、

        b[sizeof a / sizeof *a];
    for (int i = 0; i < sizeof a / sizeof *a; i++)

sizeof a / sizeof *a は、「配列の要素数」を得る時のイディオムです。

配列の一括代入
この節で触れたように、C言語では配列同士の代入は出来ません。が、構造体同士の代入はできます。

この事と、キャストを組み合わせると。

強引な配列の一括代入
#include <stdio.h>

int main() {
  int a[] = {2, 3, 5, 7, 11},
      b[sizeof a / sizeof *a];
      
  struct Dummy { int dmy[sizeof a / sizeof *a] };
  
  *(struct Dummy *)b = *(struct Dummy *)a;
  printf("b[3] = %d\n", b[3]);
}

の様に要素1つの構造体の代入として実現できます。


main関数[編集]

プログラム起動時に呼び出される関数はmainと名付けられています。実装では,この関数のプロトタイプを宣言しない。この関数は,戻り値の型がintで,パラメータがない状態で定義されなければならない。

int main(void) { /* ... */ }

または2つのパラメータ(ここではargcとargvとしていますが、宣言された関数のローカルなものなので、どのような名前を使っても構いません)を持つものです。

int main(int argc, char *argv[]) { /* ... */ }

またはそれと同等の方法で、またはその他の実装で定義された方法による[1]

  • argc の値は非負でなければならない。
  • argv[argc]はヌルポインターとする。
  • argcの値が0より大きい場合、配列メンバargv[0]~argv[argc-1]には、プログラムの起動前にホスト環境によって実装定義された値を持つ文字列へのポインタが格納される。この目的は、ホスト環境の他の場所からプログラム起動前に決定された情報をプログラムに供給することである。ホスト環境が大文字と小文字の両方の文字を持つ文字列を供給できない場合、実装は文字列が小文字で受信されるようにしなければならない。
  • argc の値が 0 より大きい場合、argv[0] が指す文字列はプログラム名を表す。argv[0][0]には、ホスト環境からプログラム名が得られない場合は、ヌル文字を指定する。argv[0][0]はヌル文字とする。argc の値が 1 より大きい場合は、argv[1]~argv[argc-1]が指す文字列がプログラム仮引数(program parameters; 一般にコマンドライン引数)を表す。
  • パラメータ argc、argv および argv 配列が指す文字列は、プログラムによって変更可能であり、プログラムの起動から終了までの間、最後に保存された値を保持する。

プログラム仮引数[編集]

プログラム仮引数とは何か[編集]

コマンドラインから実行ファイル名などを入力してプログラムを実行する際、実行ファイルに文字列データを渡すことができます。

方法は、コンパイル後に、単にコマンド入力で、たとえば

$ 実行ファイル名 moji

のように、ファイル名の後ろに、渡したい文字列を書けばいい。(上記コマンドの場合なら文字列「moji」が渡される)

このように、実行ファイル自体に対して渡される情報を、プログラム仮引数と呼ぶ。(なお「プログラム仮引数」自体の意味は、単になんらかの関数の定義側で書かれた引数のことであり、実行ファイルとは関係ない。)

例えばコマンドプロンプトにおいて、

C:\example.exe ABC DEF GHI

のようにプログラムを実行した場合、example.exeプログラムのmain関数に対して、「example.exe」、「ABC」、「DEF」、「GHI」という4個の文字列が渡される。

プログラム仮引数を利用するには、ソースコードのmain関数に

void main(int argc, char *argv[]) {
// 以下略

のようにmain関数に引数 int argc, char *argv[] をこの順番で書けばいい。

この順番を守ること。

第一引数の整数型の引数は、コマンドライン入力時の単語の数です。(実行ファイル名も単語1個ぶんとして数える。)

たとえばコマンド入力が

C:\example.exe ABC DEF GHI

なら、argcは4になる。

上記の例の場合

argv[0]は実行ファイル名「example.exe」であり、
argv[1]は 「ABC」 であり、
argv[2]は 「DEF」 であり、
argv[3]は 「GHI」 です。


コード例
#include "stdio.h"
#include "string.h"

void main(int argc, char *argv[]) {

  if (argc == 1) {
    printf("hello");
  }

  if (argc >= 2) {

    // strcmpは文字列比較して同じなら0を返す関数

    if ( strcmp(argv[1], "en")==0 ) {
      printf("hello");
    }

    if ( strcmp(argv[1], "ja")==0 ) {
      printf("こんにちは");
    }
  }
}

コマンド入力は、ファイル名が example.exe なら、コンパイル後にたとえば

example.exe ja

のように入力すれば、日本語で「こんにちは」と答える。

プログラムの実行環境[編集]

フリースタンディング環境[編集]

フリースタンディング環境でプログラムが利用できるライブラリ機能は、<float.h>, <iso646.h>, <limits.h>, <stdarg.h>,<stdbool.h>, <stddef.h>, <stdint.h> および <stdnoreturn.h>[2]で、これ以外は実装で定義されます[2]

ホスト環境[編集]

プログラムの開始[編集]
プログラムの実行[編集]
プログラム終了処理[編集]

関数指定子[編集]

inline関数指定子[編集]

inline関数指定子は、関数の宣言だけで使用できます。 inline関数指定子は、その関数の呼び出しを可能な限り速くすることを示唆します。 この示唆が効果をもつ程度は、処理系定義とします。 [3]

inline関数は、その関数を呼び出した部分に展開して直接埋め込む。 関数呼び出しにかかる処理を短縮することができるが、コードを複数の部分に展開するためファイルサイズが大きくなる。

inline関数の使用例
#include <stdio.h>

inline int function(int a, int b)
{
        return a + b;
}

int main(void){
        int r = function(1,2);
}

_Noreturn関数指定子[編集]

_Noreturn関数指定子は、関数が呼び出し元に戻らないことを示します。 _Noreturn関数指定子と<stdnoreturn.h>ヘッダファイルは、C11で追加されました[4]

_Noreturn void f (void)
{
    abort(); 
}

_Noreturn関数指定子は、関数mainには適用できません。

関数の応用[編集]

関数へのポインタ[編集]

関数へのポインタとは、ある関数のメモリアドレスを格納し、その関数に間接的にアクセスする方法です。 関数へのポインタは、次のような場合に使われる。 ひとつは、関数を呼び出す際に、関数へのポインタを引数として渡し、呼び出した関数の内部で、関数へのポインタが指す関数を実行する場合。 もうひとつは、関数へのポインタの配列をつくり、if文や、switch文で関数を呼び出すのをやめ、コードを単純にする場合。

関数へのポインタの宣言の記述は次のようになっている。

返却値のデータ型 (*関数へのポインタ名)(引数のリスト):

この宣言では、代入する関数と同じ返却値のデータ型と引数のリストを指定する必要がある。 また、演算子の優先順位のため、「*関数へのポインタ名」を囲う「()」を省略はできない。

関数のメモリアドレスを関数へのポインタに格納する記述は次のようになっている。

関数へのポインタ名 = 関数名;

関数名の後ろに、「()」はいらないことに注意。

関数へのポインタを使って、間接的に関数を呼び出すには、次のように記述します。

(*関数へのポインタ名)(引数のリスト)
//例 関数へのポインタの引数
int main(void)
{
        func2(func1);
}
#include <stdio.h>

void func1() { printf("func1()が呼び出されました。\n"); }

void func2(void (*func)()) {
  printf("func2()が呼び出されました。\n");
  (*func)();
}

int main(void) {
  func2(func1);
}
関数へのポインタの配列
#include <stdio.h>

int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int mul(int a, int b) { return a * b; }
int div(int a, int b) {
  if (b == 0) {
    printf("0で割ることはできません。\n");
    return 0;
  } else {
    return a / b;
  }
}

int main(void) {
  int i, j;
  int arithmetic;
  int (*func[])(int a, int b) = {add, sub, mul, div};
  printf("2つの整数をスペースで区切って入力してください。:");
  scanf("%d %d", &i, &j);
  printf("計算方法を入力してください(0=加法、1=減法、2=乗法、3=除法)。:");
  scanf("%d", &arithmetic);
  if (0 <= arithmetic && arithmetic <= 3)
    printf("答えは%d。\n", (*func[arithmetic])(i, j));
}

再帰的呼出し[編集]

再帰的呼出しとは、ある関数がその関数自身を呼び出すことです。 典型的な用途として、数学の階乗(5×4×3×2×1のような計算)などが、よくあげられる。

再帰的呼出しに向いた計算に再帰的呼出しを使うと、ソースコードを簡潔に書ける場合がある。

ただし、再帰的呼出しではハードウェア資源的には弱点があり、スタックを多く占有するので負担になるという弱点もある。 そのため、再帰的呼出しで桁数の多すぎる計算などをすると、与えられたスタック領域を使いはたてしまうと異常終了などのエラーを引きおこす場合もある。 これは、いわゆる「スタック・オーバーフロー」(stack overflow)というエラーの一種です。

再帰を使った階乗の計算
#include <stdio.h>

int factorial(int n) {
  if (n == 0)
    return 1;
  return factorial(n - 1) * n;
}

int main(void) {
  int i;
  printf("整数を入力してください:");
  scanf("%d", &i);
  printf("%dの階乗は%dです。", i, factorial(i));
}

この例では再帰的呼出しを使って、入力された整数の階乗を計算している。 3を入力した場合、処理の流れは以下のようになる。

factorial(3)が呼ばれ
factorial(3)が実行されfactorial(2)が呼ばれ
factorial(2)が実行されfactorial(1)が呼ばれ
factorial(1)が実行されfactorial(0)が呼ばれ
factorial(0)が実行され1が返され
factorial(1)に戻り1*1が返され
factorial(2)に戻り1*1*2が返され
factorial(3)に戻り1*1*2*3が返される
再帰的呼出しを使って2進数表示[編集]
再帰的呼出しを使って2進数表示
#include <stdio.h>

void print_binary(int n) {
  if (n < 0) {
    puts("-");
    print_binary(-n);
    return;
  }
  int x = n / 2;
  if (x)
    print_binary(x);
  putchar("01"[x % 2]);
}

int main(void) {
  print_binary(0xbadbeef);
}
実行結果
0101110101101101111101110111

6行目と11行目で print_binary 自身を呼び出し再帰呼び出しとなっています。 print_binary は、リエントラントでない関数 puts と putchar を呼び出しているのでリエントラントでは有りませんが、問題なく再帰呼び出しが行なえます。 このように、再帰可能性(Recursivity)と再入可能性(Reentrancy)は別個の概念で、再入可能性(Reentrancy)はマルチスレッドやシグナルハンドラーで問題になります。

構造体のビットフィールド[編集]

ビットフィールドとは、 1ビット以上のビットからなる、 構造体のメンバーの一種です。 ビットフィールドを用いると、 1ビット以上のビットに名前をつけ、 その名前でアクセスできます。

ビットフィールドは、構造体を宣言する時メンバーとして次のように記述します。

データ型 メンバー名:サイズ;

データ型は、(signed) intまたはunsigned intを指定します。 (signed) intを指定すると、上位ビットが符号として扱われる。 メンバー名とサイズとを「:(コロン)」で区切る。 サイズはそのメンバーを何ビットで表すかを指定します。

ビットフィールド
#include <stdio.h>

int main(void) {
  struct sbits {
    unsigned char bit8 : 1;
    unsigned char bit7 : 1;
    unsigned char bit6 : 1;
    unsigned char bit5 : 1;
    unsigned char bit4 : 1;
    unsigned char bit3 : 1;
    unsigned char bit2 : 1;
    unsigned char bit1 : 1;
  } bits;

  bits.bit1 = 1;
  bits.bit2 = 0;
  bits.bit3 = 1;
  bits.bit4 = 0;
  bits.bit5 = 1;
  bits.bit6 = 0;
  bits.bit7 = 1;
  bits.bit8 = 0;

  printf("bitsのメンバーの値は、%d %d %d %d %d %d %d %dです。\n", bits.bit1,
         bits.bit2, bits.bit3, bits.bit4, bits.bit5, bits.bit6, bits.bit7,
         bits.bit8);
  printf("bitsのサイズは、%dです。\n", sizeof(bits));
}
結果
bitsのメンバーの値は、1 0 1 0 1 0 1 0です。
bitsのサイズは、1です。

上の例では、0か1かの値を持つ1ビットの8つのビットフィールドをもつbits構造体を宣言し各ビットフィールドに値を代入しその値を表示している。 なお、unsigned char型をビットフィールドに使えるかはコンパイラによる。

オブジェクト指向プログラミング[編集]

C言語は一般にオブジェクト指向プログラミング言語だとは考えられていませんが、従前の構造体やマクロを使うことでクラスの継承に類似した実装を行うことができます。

構造体やマクロを使った継承の例
#include <stdio.h>

struct Shape {
    double x, y;
    void (* print)(struct Shape *);
    double (* area)(struct Shape *);
};

struct Square {
    struct Shape shape;
    double wh;
};
static void printSquare(struct Shape* sp) {
    struct Square *p = (struct Square *)sp;
    printf("Square(x:%f, y:%f, wh:%f)", sp->x, sp->y, p->wh);
}
static double areaSquare(struct Shape* sp) {
    struct Square *p = (struct Square *)sp;
    return p->wh * p->wh;
}
#define Square(x, y, wh) (struct Shape*)&((struct Square){ {x, y, printSquare, areaSquare }, wh })

struct Rectangle {
    struct Shape shape;
    double w, h;
};
static void printRectangle(struct Shape* sp) {
    struct Rectangle *p = (struct Rectangle *)sp;
    printf("Rectangle(x:%f, y:%f, w:%f, h:%f)", sp->x, sp->y, p->w, p->h);
}
static double areaRectangle(struct Shape* sp) {
    struct Rectangle *p = (struct Rectangle *)sp;
    return p->w * p->h;
}
#define Rectangle(x, y, w, h) (struct Shape*)&((struct Rectangle){ {x, y, printRectangle, areaRectangle }, w, h })

struct Circle {
    struct Shape shape;
    double r;
};
static void printCircle(struct Shape* sp) {
    struct Circle *p = (struct Circle *)sp;
    printf("Circle(x:%f, y:%f, r:%f)", sp->x, sp->y, p->r);
}
static double areaCircle(struct Shape* sp) {
    struct Circle *p = (struct Circle *)sp;
    return 3.1415926536 * p->r * p->r;
}
#define Circle(x, y, r) (struct Shape*)&((struct Circle){ {x, y, printCircle, areaCircle }, r })

void print(struct Shape *p ) {
    p->print(p);
}
double area(struct Shape *p ) {
    return p->area(p);
}
int main(void){
    struct Shape* shapes[] = {
        Square(10.0, 20.0, 15.0),
        Rectangle(20.0, 10.0, 12.0, 11.0),
        Circle(15.0, 10.0, 10.0),
    };
    for (int i = 0; i < sizeof shapes / sizeof *shapes; i++) {
        print(shapes[i]);
        printf(", area = %f\n", area(shapes[i]));
    }
}
実行結果
Square(x:10.000000, y:20.000000, wh:15.000000), area = 225.000000
Rectangle(x:20.000000, y:10.000000, w:12.000000, h:11.000000), area = 132.000000 
Circle(x:15.000000, y:10.000000, r:10.000000), area = 314.159265

この例は、継承というより合成の例になっているという批判はありえます。

脚註[編集]

  1. ^ N2176 C17 ballot ISO/IEC 9899:2017. ISO/IEC JTC1/SC22/WG14. p. 10, §5.1.2.2.1 Program startup. オリジナルの2018-12-30時点によるアーカイブ。. https://web.archive.org/web/20181230041359/http://www.open-std.org/jtc1/sc22/wg14/www/abq/c17_updated_proposed_fdis.pdf. 
  2. ^ 2.0 2.1 N2176 C17 ballot ISO/IEC 9899:2017. ISO/IEC JTC1/SC22/WG14. p. 8, §4. Conformance. オリジナルの2018-12-30時点によるアーカイブ。. https://web.archive.org/web/20181230041359/http://www.open-std.org/jtc1/sc22/wg14/www/abq/c17_updated_proposed_fdis.pdf. 
  3. ^ 『JISX3010:2003』p.83「6.7.4 関数指定子」
  4. ^ 『ISO/IEC 9899:2011』p.91「6.7.4 Function specifiers」