Entry

プログラミングメモ - C言語の定義と宣言の話

2009年03月02日

先日書いた,「qune: よりそいプログラミングのすすめ」が『WEB+DB PRESS Vol.49』に紹介されていてびっくり。「濃縮還元オレンジニュース」で紹介していただいたのは知っていたんですけど,まさか本誌に載るとは……。記念に一冊買ってしまいました。

WEB+DB PRESS Vol.49
WEB+DB PRESS Vol.49
posted with amazlet at 09.03.02

技術評論社
売り上げランキング: 159

で,今日は本誌を同僚とつらつら読んでたんですけど,C の話でちと盛り上がっていました。

今回の特集は DRY(Don't Repeat Yourself)の特集で,要するに,繰り返しや重複するコードをなくしましょうという話。WEB+DB PRESS では,基本的に C/C++ を取り扱わないんですけれど,今回はマクロスイッチを使って宣言の数を減らす参考例が挙げられていました(p46)。次のようなもの。

#ifdef MAIN
#define EXTERN
#define INITVAL(x) = x
#else
#define EXTERN extern
#define INITVAL(x)
#endif

どうやって読むかというと,「MAIN がマクロ定義されているときは,EXTERN に空をマクロ定義して,INITVAL(x) に "= x" をマクロ定義する」と読みます。MAIN がマクロ定義されていないとき(#else)はその反対で,「EXTERN に "extern" を定義して,INITVAL(x) には空を定義する」ことになります。

これをすると何が嬉しいのかというと,common.h のような共通ヘッダで,EXTERN char* foobar INITVAL("10") とかしておくことで,MAIN が定義されているソースだけ変数定義にすることができて,その他のソースでは変数宣言にすることができるんだとか。各ソースでは,common.h を include するだけでいいので,変数の宣言と定義を意識しないで,大域変数を使うことができるというわけです。よく考えるなあ。

ただ,今日話していた同僚曰く,「メリットが分からん」とのこと。うちのお作法とはかなり違うやり方なので,まぁ,そうなのかもなぁ,とも。

こゆのはお作法の問題なので,どっちがいいとかいった話でもないんでしょうけれど,あたしも同僚と同じく,定義と宣言の区別をあいまいにするような方法には,あまり同意できなかったりします。簡単におさらいしておくと,C/C++ の「定義」と「宣言」つのは,こういう意味を持っています。

  • 宣言: 識別子の名前(とか型名とか)を解決するために使う(メモリは確保されない)
  • 定義: その識別子で領域を確保して指定した値を代入するのに使う(メモリが確保される)

つまるところ,宣言は「このソースでメモリを確保する必要はないけれど,コンパイルするのに名前(とか型名とか)が必要なんです」という意味で,定義は「このソースでメモリを確保しておかないと,コンパイルできませんよ」という意味。プロトタイプ宣言なんかで,引数の型を書くだけだったら,名前を解決できればいいから宣言するだけで十分だけど,具体的に使用されるメモリの大きさが分からないといけない場面では,定義されていないとコンパイルできないわけです。ものすごく初歩的な話なんですけど,そゆこと。

で,こゆ区別がなんで必要なのかというと,デバッグするときに,どのソース(オブジェクトファイル)でメモリを確保しているのか,調べなくちゃいけないときがあるからです。マクロ定義でラップしていると,どこで定義しているのか分からなくて,結局マクロの中身を解読しなくちゃいけなくなります。

もうひとつ,この方法だと定義するソースが変わるたびに,MAIN に代わるマクロ定義を用意しなくちゃいけなくなります。小規模な開発ならまだなんとかなるんですけれど,ちょっと規模が大きくなると MAIN とか CONF とかいった謎なマクロ定義が増えることになってしまいます。ファイル名とか関数名が変わった時のメンテナンスもしんどそうです(そんなでもないか)。

一方,うちのお作法はというと,そんなに大したことはしてなくて,共通ファイルと共通ソースを用意しています。例えば,common.h で,

extern char* foo;

と宣言しておいて,common.c とかで,

#include "common.h"
char* foo = "foo";

とかするだけ。common.h を各ソースで include しておけば,各ソースの extern 宣言になるし,common.c をコンパイルすれば,定義を作ることができます。各ソースとこの定義はリンク時に解決されることになります。実際,これで足りることが多い。MSVC を使っているなら stdafx.h や stdafx.cpp のようなファイルに便乗しちゃってもいいのかも。ま,あまりグローバル変数は使わない方がいいので,const をつけるときは,こんな風にする(まとめて書いちゃいます)。

/* common.h */
extern const char* foo;

/* common.c */
#include "cmn.h"
const char* foo = "bar";

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

int
main(int argc, char* argv[]) {
    printf("%s\n", foo);
    return 0;
}

C じゃなくて C++ を使っていると,ここら辺の区別をちゃんとつけていないと,コンパイル時間が無駄に長くなったり,コンパイルできてもリンクできないとかいったことが続出したりします。実際,上の例だと,main.c がいくら変わっても,common.c は1回コンパイルするだけで済みます(リンクし直すだけ)。名前だけ解決できりゃいいのに,ものすごく依存関係のあるソースをコンパイルしちゃったりしてたら,時間の無駄ですもんね。

本誌の話に戻ると,ここではマクロ定義を工夫して,DRY を実現できる場合があるよ,ってな話だったわけで,頭ごなしに批判するもんでもないんだと思います。ちと例が悪かったか。

Trackback
Trackback URL:
Ads
About
Search This Site
Ads
Categories
Recent Entries
Log Archive
Syndicate This Site
Info.
クリエイティブ・コモンズ・ライセンス
Movable Type 3.36
Valid XHTML 1.1!
Valid CSS!
ブログタイムズ

© 2003-2012 AIAN