組込みソフト向けC言語コーディング規約|関数の定義と宣言

シェアする

どうも、ながやすです。

この記事では関数の定義と宣言について注意すべきことを記載します。「定義」とは実際にメモリ上に配置されることです。「宣言」とはどこかに定義があることを示すだけのものです。例えばプロトタイプ宣言をしても、これは「宣言」なので実際に関数のコードがメモリに配置されるわけではありません。「どこかにこういう関数があるよ」と言っているだけです。一方、「関数の定義」を書けば、メモリのどこかに関数が配置されます。変数の場合も同様です。「extern宣言」しても、メモリ上には配置されません。

また、「定義」は各関数に必ず1つしか書けませんが、宣言はいくつあっても(文法上は)かまいません。例えばロトタイプ宣言をいろんなファイルに書き散らしても文法上はOKですが、関数の定義を複数個かく(多重定義)とNGです。これは、定義は実際にどこかのメモリに配置されるもの、ということを把握していれば理解できます。
関数を定義するときは名前空間についての配慮も必要です。関数定義の場合は、「cファイル内だけから見える」「どこからでも見える」の2パタンあります。モジュール化の観点からも、また、名前の重複を避けるためにも、名前空間はできるだけ小さくなるように、つまりできるだけ他から見えないようにしてください。そうしておけば、後の変更するときにも影響範囲が小さくなり、メンテナンスが楽になります。

各ルールごとにその理由とルール違反のソースコード例&ルール適合のソースコード例を書いています。お使いのパソコンやスマホの画面サイズに寄っては、ソースコードが横にはみ出てしまうことがあります。その場合、環境によってはスライドバーは表示されませんがソースコードのところをドラッグして横にスライドすることができます

スポンサーリンク

関数コール時にプロトタイプ宣言を参照する

プロトタイプ宣言を参照しないとコンパイラによる引数チェックが行われません。コンパイル時にバグを見つけ出すために、必ずプロトタイプ宣言を参照してください。公開されているヘッダファイルをincludeしてプロトタイプ宣言を参照してください、自前で勝手にプロトタイプ宣言を書かないでください。

違反コード

void func_NG_0(void)
{
    char *ptr;
    int  ch;
    size_t size;

    // 略


    // NG. プロトタイプ宣言を参照していない
    memset(ptr1, ptr2); 

}

void func_NG_1(void)
{
    char *ptr;
    int  ch;
    size_t size;

    // NG. 自前で勝手にプロトタイプ宣言をしてはだめ.
    void *memset(void *s, int c, size_t n);


    // 略.
		
    memset(ptr1, ptr2); // NG. 

}

適合コード

#include <string.h> // プロトタイプ宣言のあるヘッダをincludeする.

void func_OK(void)
{
    char *ptr;
    int  ch;
    size_t size;
	
    // 略
	
    memset(ptr, ch, size);
}

可変引数は使わない

可変引数はプロトタイプ宣言の型チェックが行われないため、バグにつながりやすいので使用しないでください(printf系の関数を除く)。どうしても必要な場合は、可変引数でなく、argc, argv方式を使用してください。main関数と同様に、argcに配列の要素数、argvに配列のポインタを渡します。

違反コード

// 可変引数なのでNG.
int func_NG(int *per, ...)
{
    // 略
}

適合コード

// argcに配列の要素数、argvに配列へのポインタを渡す.
// argcを調整することにより、引数の個数を変えられる.
// 何番目の要素が何のパラメータなのかは、ヘッダに明記すること.
// ex.
// #define PARAM_HOGE 0
// #define PARAM_FUGA 1
// #define PARAM_MAX  2 // 最大要素数.

int func_OK(int argc, int *argv)
{
    int param_0, param_2;
	
    if (argc >= PARAM_MAX)
        return -1;

    param_0 = argv[PARAM_HOGE];
    param_1 = argv[PARAM_FUGA];

    // 略

}

到達しないコードを書かない

バグなのかどうかまぎらわしいので、実行されないコードは書かないでください。また、致命的なエラーの処理などでどうしても到達しないコードを書く必要がある場合には、その到達しないところに「/* NOTREACHED */」とコメントを入れておいてください。多くのコンパイラや静的解析ツールはこのコメントがあるとそこまで制御が実行されないことを理解し、警告などを抑止します。

違反コード

int func_NG(int tmp)
{
    if (tmp)
        return 0;
    else
        return -1;
	
	
    func_b();// ここには絶対こない.
}

int func_critical_err_handler(void)
{

    // 略

    /* 致命的なエラー発生、しかたなくここで無限ループ */
    for (;;)
        ;

    return 0; // NG. ここには絶対到達しないので、コンパイラが警告を吐く。
}

適合コード

int func_OK(void)
{
    if (tmp)
	return 0;
    else
        return -1;

    // func_b();// ここには絶対こない.削除.
}

int func_critical_err_handler(void)
{
    // 略

    /* 致命的なエラー発生、しかたなくここで無限ループ */
    for (;;)
        ;

    /* NOTREACHED */  // NOTREACHED コメントで、コンパイラ,解析ツールに到達しないコードだとわからせる.
}

引数を持たない関数は、明示的に引数をvoidとして宣言する

プロトタイプ宣言のコンパイラチェックを有効にするため、引数がない関数はvoidとしてください。

違反コード

int func_NG()
{
    return 0;
}

適合コード

int func_OK(void)
{
    return 0;
}

同一ファイル内で定義された関数からしかアクセスされない関数はstaticをつける

staticをつけない関数は、グローバル関数になるので他の.cファイルからもコールできます。もし、同じ名前のグローバル関数が複数あった場合、リンクエラーにならないことが多く、また、リンク順によってどの関数がコールされるかが変わります。他への影響を少なくするために、他の.cファイルからもコールしない関数にはstaticをつけてください。

違反コード

// == file0.c ==

// 本当はfile0.cからしかコールしない関数.
void func_tmp(void)
{
	printf("File 0.\n")
}

// == file1.c ==

// グローバルな公開関数.
void func_tmp(void)
{
	printf("File 1.\n")
}

// == file2.c ==

void func_NG(void)
{
	// NG.file0.cのfuncか、file1.cのfuncか,
	// どちらがコールされるか分からない.
	func_tmp(); 
}

適合コード

// == file0.c ==

// file0.cからしかコールしない関数なら,
// staticをつける.
static void func_tmp(void)
{
	printf("File 0.\n")
}

// == file1.c ==

// グローバルな公開関数.
void func_tmp(void)
{
	printf("File 1.\n")
}

// == file2.c ==

void func_OK(void)
{
	// OK.file0.cのfunc_tmpはstaticなので,
	// file2.cからはみえない.
	// file1.coのfunc_tmpがコールされる.
	func_tmp(); 
}

1つの関数からしかアクセスしない変数は関数内で定義する

関数の外で定義された変数は、他の関数からも参照可能です。もし同じ名前のグローバル変数があった場合、変数名が重複してしまいます。意図しない重複を避けるために、1つの関数内でしか使用しない変数はその関数内で定義してください。

違反コード

int variable; //func_NGからしか参照しない.

void func_NG(void)
{
	// 略
	printf("%d\n", variable);
}

適合コード

void func_OK(void)
{
	int variable; // func_OKからしか参照しない.

	// 略
	printf("%d\n", variable);
}

関数定義内では関数宣言をしない

関数定義内での、グローバルな関数以外の宣言はC言語規格上、NGです。簡単のため、グローバル関数も含めてすべての関数宣言は行わないようにしてください。関数の外(ファイルスコープ)にて関数宣言を行ってください。

C言語の定石として通常、
・公開用グローバル関数のプロトタイプ宣言は公開ヘッダファイルにかく
・内部用グローバル関数のプロトタイプ宣言はプライベートヘッダファイルにかく
・内部用static関数は関数定義と同じ.cファイルの先頭にかく
とします。

違反コード

void func_NG(void)
{
	extern void   func_global(void);  // 規格上はOkだが、ルール上NG.
	static void   func_satic(void);   // 規格上はNG.

	return;
}

適合コード

/**
 * @file header_OK.h
 * 公開ヘッダファイル
 */
#ifndef HEDADER_OK_H_INCLUDED
#define HEDADER_OK_H_INCLUDED

extern void func_global(void); // 公開グローバル関数の宣言は公開ヘッダで.
#endif //HEDADER_OK_H_INCLUDED

// hoge.c
#include <hoge.h>

static func_satic(void);   // 静的関数の宣言も、関数の外で行う.

void func_OK(void)
{
	return;
}

ヘッダファイルで変数や関数の定義をしない、参照のみにする

変数や関数の定義が書かれたヘッダファイルを、複数の.cからincludeすると多重定義になります。また、大抵の場合、この多重定義はコンパイルエラーにならず、発見しにくい不具合につながります。変数定義や関数定義は.cに書き、そのextern参照だけをヘッダファイルに書いてください。

違反コード

// hoge.h
#ifndef HOGE_H_INCLUDED
#define HOGE_H_INCLUDED

int hoge_array[] = {0, 1, 2, 3, 4, 5}; // NG. 定義は書かない.

#endif

適合コード

// hoge.h
#ifndef HOGE_H_INCLUDED
#define HOGE_H_INCLUDED

extern int hoge_array[]; //Ok. 参照だけにする。定義は特定の.cファイルに書く.

#endif

以上、関数の定義と宣言のコーディング規約でした!

他のコーディング規約は下記のページからたどれます。ぜひ見てみてくださいね

組込みソフトウェア向けC言語コーディング規約
どうも。ながやすです。 組込みソフト(ファームウェア)向けのc言語コーディング規約を書いています。これをみておけばcの文法的なミス減らせるかなと思います。また、C言語のベテランの方もひとつくらい...