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

出典: フリー教科書『ウィキブックス(Wikibooks)』
ナビゲーションに移動 検索に移動

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

宣言時にしか、配列に一括で代入できない[編集]

C言語では、ある配列で、その全要素に一括して数値を代入する行為は、宣言のときにしか、出来ない。

宣言以外のときに代入したい場合には、面倒だが、各要素に一個ずつ中身を代入していくしかない。

関数[編集]

main関数には大まかに分けてプログラム仮引数(コマンドライン引数)を持たない関数と持つ関数との2種類の記述方法がある。

仮引数[編集]

仮引数とは何か[編集]

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

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

$ 実行ファイル名 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.ex」であり、
argv[1]は 「ABC」 であり、
argv[2]は 「DEF」 であり、
argv[3]は 「GHI」 である。


コード例

下記コードは、実行時に「ja」と追記することで、日本語で「こんにちは」とアイサツをするプログラムである。単に実行ファイル名だけを入力した場合、英語で「hello」と答える。

#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

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

詳細[編集]

プログラム仮引数を持つ関数[編集]

プログラム仮引数を使用する場合、次のように2つの仮引数をもつ関数を記述する。

int main(int argc, char *argv[])
{
	/*いくつかの文*/
}
argcはプログラム仮引数の個数である。
argvはプログラム仮引数を表す文字列の配列へのポインタである。
argv[0]が指す文字列はプログラム名である。
argcにはこのプログラム名も個数として数えられる。

ただし、argcが0の場合、argv[0]は空ポインタである。 またargc>0であっても、ホスト環境からプログラム名が得られない場合、 argv[0]は空ポインタである。 [1] argv[1]からargv[argc-1]までが指す文字列は、プログラム仮引数である。 argv[argc]は空ポインタである。 [2]

//例 プログラム仮引数
#include <stdio.h>

int main(int argc, char *argv[])
{
	for (int i = 0; i < argc; ++i)
	{
		printf("argv[%d]の値 : %s\n", i, argv[i]);
	}
}


プログラム仮引数を持たせない場合[編集]

プログラム仮引数を使用したくない場合には、単に下記のように main 関数の引数を void あるいは空白にすればいいだけである。こうすれば、仮引数をもたないとして処理される。

int main(void)
{
	/*いくつかの文*/
}


関数の実行環境[編集]

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

オペレーティングシステムのいかなる支援もなしにCプログラムを実行する環境を、 フリースタンディング環境という。 フリースタンディング環境では、 プログラムの開始時に呼び出される関数の名前および型は処理系定義である。 フリースタンディング環境では、複素数型を使ってはいけない。 フリースタンディング環境でプログラムが利用できるライブラリ機能は、 <float.h>, <iso646.h>, <limits.h>, <stdarg.h>, <stdbool.h>, <stddef.h>, <stdint.h> であり、[3] これ以外は処理系定義である。 [4]

ホスト環境[編集]

オペレーティングシステムの支援ありでCプログラムを実行する環境を、 ホスト環境という。[5] ホスト環境では、プログラム開始処理において呼び出される関数の名前はmainである(エントリポイント)。 main関数とはプログラムの開始処理において、 いろいろな初期化処理の後、最初に呼び出される関数である。 たいていのプログラムはこのmain関数から始まると考えてよい。


ホスト環境では、プログラムは、全てのライブラリ機能を使用してよい。 [6]

ホスト環境では、main関数の返却値が、このプログラムを実行するオペレーティングシステムに対して渡され、0はプログラムの成功終了状態を表している。 main関数を終了する}に到達した場合、main関数は値0を返す。 [7]

関数の応用[編集]

inline[編集]

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

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

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

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

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

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

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

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

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

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

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

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

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

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

(*関数へのポインタ名)(引数のリスト)
//例 関数へのポインタの引数
#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)というエラーの一種である。

再帰では、 ある関数がその関数自身を呼び出すたびに、 引数と制御が戻るべきアドレス(リターンアドレス)が、 スタックと呼ばれる領域に格納される(プッシュ)。

また、ローカル変数もスタックと呼ばれる領域に格納されている。 関数の処理が終わるたびに、 スタックに最後に格納したデータを取り出して(ポップ)、 関数を呼び出したときの状態を復元し、 処理を続ける。[9]


再帰を行うには、その関数がリエントラントである必要がある。 リエントラントであるためには、使用する変数を共有しないように気を付ける必要がある。[10]

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

int factorial(int n);

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

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

この例では再帰を使って、入力された整数の階乗を計算している。 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が返される。

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

ビットフィールドとは、 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));
}

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

脚注[編集]

  1. ^ 『[迷信] argv[0] はプログラム名 | 株式会社きじねこ』http://www.kijineko.co.jp/tech/superstitions/argv0-is-program-name.html
  2. ^ 『JISX3010:2003』p.8「5.1.2.2.1 プログラム開始処理」
  3. ^ 『JISX3010:2003』p.5「4 規格合致性」
  4. ^ 『JISX3010:2003』p.8「5.1.2.1 フリースタンディング環境」
  5. ^ 『JISX3010:2003』p.8「5.1.2.2 ホスト環境」
  6. ^ 『JISX3010:2003』p.9「5.1.2.2.2 プログラムの実行」
  7. ^ 『JISX3010:2003』p.9「5.1.2.2.3 プログラム終了処理」
  8. ^ 『JISX3010:2003』p.83「6.7.4 関数指定子」
  9. ^ 矢沢久雄、原田英生『日経 BP パソコンベストムック C言語とC++がわかる本』日経ソフトウェア、2013年5月15日発行、112項
  10. ^ 『再入可能とは 「リエントラント」 (reentrant) さいにゅうかのう: - IT用語辞典バイナリ』http://www.sophia-it.com/content/%E5%86%8D%E5%85%A5%E5%8F%AF%E8%83%BD