C言語復習

書籍でC言語を学習した人は多いでしょう。C言語について多くの書籍が市販されています。しかし、それらは主にプログラムを書く、という視点で記述されています。ここでは読む、という視点でC言語のいくつかの機能を見直してみます。

以降の説明で「痕跡文字列」という言葉を使います。その意味と位置付けの詳細はコードリーディングのモデルにて説明します。コードリーディングを開始するソースコード上の場所(「コードポイント」)をみつけるために使う最初の検索文字列、と考えて下さい。

空白

区切り文字としてスペースやタブが使われますが、複数個連続していても、一つとみなされます。凝った検索を行うときに考慮する必要があります。詳細は最低限必要となるツールにて説明します。

インデントスタイル

インデントスタイルはソフトウェアによって様々です。慣れないスタイルもあるかもしれませんが、読む側の立場にあっては静かに受け入れる必要があります。どうしても受け付けられないひどいスタイルに遭遇したら indent コマンドで再インデントしても良いでしょう。

筆者の経験ではたいていの場合1つのソフトウェアのなかではインデントスタイルは統一されています。スタイルを把握しておけば、凝った検索を組み立てるときの助けとなります。詳細は最低限必要となるツールにて説明します。

文字列定数の連結

ダブルクォートで囲まれた文字列を文字列リテラル、文字列定数あるいは固定文字列と呼びます。空白で区切られた隣接する2つ以上の文字列はCコンパイラによって連結され1つの文字列定数となります。

/* concat.c */
const char* str = "abc" "def" "ghi";
printf("=>%s\nb", str);
$ gcc concat.c
$ ./a.out
=>abcdefghi

abcdefghiを痕跡文字列 とみたてて ソースコードツリー中でabcdefghiを文字列検索しても、該当するコードポイントがみつかりません。

プリプロセッサ(cpp)

Cコンパイラは入力を処理する前にプリプロセッサを呼び出します。プリプロセッサにはプログラムを書く人にやさしく、読む人に厳しい側面があります。

プリプロセッサはインクルードファイルを読み込むこと、マクロを展開することでコンパイラへの入力を変形します。痕跡文字列がこの変形された入力に由来している場合、変形前のソースコードツリーを検索してもみつかりません。

プリプロセスだけの実行

gccの-Eオプションを使うと引数で与えたcファイルについてプリプロセスだけを実行して結果を標準出力に表示できます。

$ cat /tmp/foo.c
#define x "abc"
x

$ gcc -E /tmp/foo.c
# 1 "/tmp/foo.c"
# 1 "<コマンドライン>"
# 1 "/tmp/foo.c"

"abc"

名前の合成

マクロ定義中で、‘##’が使われると、文字列を連結して、変数名、関数名、型名など識別子を合成します。合成された名前はマクロ展開された結果に含まれます。

/* /tmp/foo.c */
#define def_inc(A) \
  static int A;    \
  inc##A (void) { A++; }

def_inc(x)  /* incx関数を定義される。*/
def_inc(y)

int main(void)
{
  incx();   /* ソースコードを呼んでいてincxやincy関数を発見して */
  incx();   /* その定義を探してみつからない。*/
  incy();
  return x + y;
}
$ gcc -E /tmp/foo.c
...省略
static int x; incx (void) { x++; }
static int y; incy (void) { y++; }

int main(void)
{
  incx();
  incx();
  incy();
  return x + y;
}

$ gcc /tmp/foo.c

$ ./a.out

$ echo $?
3

マクロによる文字列化

マクロ定義中で、 マクロ変数の前に‘#’があるとマクロ変数の値が(ダブルクォートで括られた)定数文字列へ展開されます。

#include <stdio.h>

typedef enum pref_type {
  PREF_UINT,
  PREF_BOOL,
  PREF_ENUM,
  PREF_STRING,
  PREF_RANGE,
  PREF_STATIC_TEXT,
  PREF_UAT,
  PREF_OBSOLETE,
} pref_type_t;

/* Taken from
   http://anonsvn.wireshark.org/viewvc/trunk/ui/gtk/prefs_dlg.c?r1=46545&r2=46544&pathrev=46545 */

static const char*
get_pref_type_string(pref_type_t t)
{
  switch (t)
    {
#define CASE(T) case PREF_##T: return #T
      CASE(UINT);
      CASE(BOOL);
      CASE(ENUM);
      CASE(STRING);
      CASE(RANGE);
      CASE(STATIC_TEXT);
      CASE(UAT);
      CASE(OBSOLETE);
    default:
      return "UNKNOWN";
    }
}


int
main(int argc)
{
  printf("=>%s\n", get_pref_type_string(argc));
  return 0;
}

実行してみます

$ gcc /tmp/bar.c
gcc /tmp/bar.c
$ ./a.out
./a.out
=>BOOL
$ ./a.out a
./a.out a
=>ENUM
$ ./a.out a b
./a.out a b
=>STRING

get_pref_type_string関数だけを抜粋してマクロ展開してみると次のようになります。

static const char*
get_pref_type_string(pref_type_t t)
{
  switch (t)
    {

      case PREF_UINT: return "UINT";
      case PREF_BOOL: return "BOOL";
      case PREF_ENUM: return "ENUM";
      case PREF_STRING: return "STRING";
      case PREF_RANGE: return "RANGE";
      case PREF_STATIC_TEXT: return "STATIC_TEXT";
      case PREF_UAT: return "UAT";
      case PREF_OBSOLETE: return "OBSOLETE";
    default:
      return "UNKNOWN";
    }
}

goto文

通常「スパゲティコード化する」という理由でgoto文の使用はあまり、推奨されていません。しかし現実には多くのソースコードで利用されています。

良く見るのは段階的に複数のリソースを確保して行くという処理において、途中で失敗した場合のクリーンアップです。全てのリソースを確保できなかったので、処理の続行をあきらめて、確保してしまったリソースを順次開放します。

static int __net_init netdev_init(struct net *net)
{
        INIT_LIST_HEAD(&net->dev_base_head);

        net->dev_name_head = netdev_create_hash();
        if (net->dev_name_head == NULL)
                goto err_name;

        net->dev_index_head = netdev_create_hash();
        if (net->dev_index_head == NULL)
                goto err_idx;

        return 0;

err_idx:
        kfree(net->dev_name_head);
err_name:
        return -ENOMEM;
}


/* Taken from
   kernel-2.6.32-131.0.15.el6/linux-2.6.32-131.0.15.fc13.x86_64/net/core/dev.c */

三項演算子

三項演算子を使った式

D = A? B: C;

はAが真のときBを、偽(== 0)のときCを返すという意味になります。if文を使って次のようにも書けます。

if (A)
        D = B;
else
        D = C;

returnの値、if/whileなどの条件を記述する箇所で良く使われます。

ビット演算

ビット演算群を使って正数型(unsigned int)の変数を、フラッグ群として扱うことがあります。またよりサイズの小さな正数を格納するのに使われることがあります。

フラッグ

フラッグ群として用いる場合、フラッグを操作すための定数(マスク値)を各フラッグの種類毎に用意されているはずです。定数名をenumあるいは#defineを使って用意して。定数値を<<演算子で用意しています。

enum {
        FLAG_X  = 1 << 0,        /* 0番目のビット */
        FLAG_Y  = 1 << 1,        /* 1番目のビット */
        ...
        FLAG_Z  = 1 << n,        /* n番目のビット */
        FLAG_0N = FLAG_X|FLAG_Z, /* 0番目とn番目のビット */
};

変数からフラッグのの値を取り出す箇所で、&演算子が使われます。

unsigned int var;
...
if (var & FLAG_X) {
        /* FLAG_Xで指定したビットが立っていれば、ここへ
           制御がうつる。*/
        ...
}

if (var & FLAG_0N) {
        /* FLAG_0Nで指定したいずれかのビットが立っていれば、
           ここへ制御がうつる。*/
        ...
}

if ((var & FLAG_0N) == FLAG_0N) {
        /* FLAG_0Nで指定した全てのビットが立っていれば、
           ここへ制御がうつる。*/
        ...
}

変数中の特定のフラッグを立てるのに|=が使われます。

unsigned int var;
...
var |= FLAG_X;

逆に特定のフラッグを下げるのに~&=が使われます。~で 全ビットを反転した値を得ます。

unsigned int var;
...
var &= ~FLAG_X;

サイズの小さな正数

正数変数の中の特定にビット範囲を小さなサイズの正数を格納するのに使われることがあります。例として4, 5, 6番目に3ビット長の正数が格納されているケースを想定します。

3ビット長の正数が何番目のビットから格納されているかを示すオフセット定数と、3ビット長の正数部分を抜きとるためのマスク定数が定義されているでしょう。

#define SUBINT_OFFSET 4
#define SUBINT_MASK (1<<SUBINT_OFFSET|1<<(SUBINT_OFFSET + 1)|1<<(SUBINT_OFFSET + 2))
/* あるいは */
#define SUBINT_RANGE 3
#define SUBINT_MASK ((1 << (SUBINT_RANGE + 1)) - 1) << SUBINT_OFFSET

正数変数varから3ビット長の正数を取り出す処理は次のように記述されているかもしれません。

r = (var & SUBINT_MASK) >> SUBINT_OFFSET

&演算子で3ビット長の正数が格納された箇所以外の全てのビット群をクリアした値を生成します。この値をそのまま正数変数の値としてみると、格納した値よりも16倍( 1 << 4 )大きいままです。>>演算子で調整します。

逆に値をセットする処理は次のように記述されているかもしれません。

var = (var & ~SUBINT_MASK) | (r << SUBINT_OFFSET);

&~を使って 正数変数の 3ビット長の正数を格納する領域のビット群をクリアします。一方、正数変数r中に3ビット長の正数の値が格納されています。<<演算子で値の格納箇所をずらした値を作ります。最後に 格納箇所をずらした3ビット長の正数の値と、格納する領域だけをクリアした値とを|演算子で 重ねあわせた値を作ります。

否定の否定

b = !!var

と否定の演算子を2つ重ねた式が出てくることがあります。これは

b = !(var == 0)

と同じ意味になります。非0の値を1に、0をそのまま0に変換するのに使われます。

スコープ

スコープとはある名前が参照されうる範囲のことです。ここでは型の名前について扱いません。変数定義あるいは関数定義を指し示すものとしての名前を扱います。

詳細はコードリーディングのモデルにて説明しますが、ソースコードを読むこととは、制御フローとデータフローを追跡する、という行為の繰り返しです。多くの場面でそれは関数や変数を指し示す名前がどのように利用されているかを追跡することになります。もし名前のスコープがわかれば、追跡しないといけない範囲特定できます。

C言語には狭い順番に文内、.cファイル内、プログラム全域の3つのスコープがあります。以降では、各スコープについて説明します。ただし説明のために順番を入れかえています。

文内

文中で定義された変数をローカル変数と呼びます。

int main(void)
{
  for (int i = 0; i++; i < 10)
      ;
  return i;
}

このプログラムをコンパイルすると、return iの箇所で文法エラーで失敗します。for文で定義された変数iが定義されていますが、それはfor文の範囲からだけ参照されうる名前です。for文の外にあるreturn文からは参照できません。リーディングの立場から言うと、for文に定義されたiのデータフローを追う必要があっても、それはfor文の範囲だけに着目すれば十分、ということになります。

{}を使うと複数の文をまとめた複文(ブロック)を作れます。ブロック内で定義された変数はブロック内からしか参照できません。関数定義もブロックとみなすことができます。関数の仮引数は関数定義のブロックをスコープとします。

int foo(int o)
{
        int p;
        /* o, pが参照可能 */
        ...
        {
                int q;
                /* o, p, qが参照可能 */
                ...
        }
        /* o, pが参照可能 */
        ...
}

規格化されたCの範囲では文中に関数を定義できないので、ローカル関数というものは存在しません。しかし文中で関数を宣言することはできます。

int foo(void)
{
        extern int f(void);
        int g(void);

        return f() + g();
}

プログラム全域をスコープに持つfとgについて、その存在を宣言することでfooはその中でfとgを参照可能としています。このようなコードが出現することはあまりありません。簡便な記述でソースコードツリーの様々な箇所で参照できるようプログラム全域をスコープに持つ名前については、通常その宣言をヘッダファイルに含めます。参照する側の.cファイルでは、そのヘッダファイルをインクルードすることで、.cファイル内全体でその名前を参照できます。

ブロック内で独自に宣言を記載して参照するのは理由ありと考えて良いでしょう。プログラム全域をスコープに持つ関数を定義しているが、それはソースコードツリー中の極一部のソースコード中からだけ参照することを想定しており、他のソースコードから参照してしまうことが無いようにヘッダファイルに宣言を記載していない、という場合です。

/*
 * Do not modify this file.
 *
 * It is created automatically by Makefile or Makefile.nmake.
 */

#include "config.h"

#include <gmodule.h>

#include "moduleinfo.h"

#ifndef ENABLE_STATIC
G_MODULE_EXPORT const gchar version[] = VERSION;

/* Start the functions we need for the plugin stuff */

G_MODULE_EXPORT void
plugin_register (void)
{
  {extern void proto_register_wimaxmacphy (void); proto_register_wimaxmacphy ();}
}

G_MODULE_EXPORT void
plugin_reg_handoff(void)
{
  {extern void proto_reg_handoff_wimaxmacphy (void); proto_reg_handoff_wimaxmacphy ();}
}
#endif

/* Taken from
   wireshark/plugins/wimaxmacphy/plugin.c */

プログラム全域

ある.cファイル内にあって関数定義の外(トップレベルとも言う)でstatic修飾子無しで定義されている関数あるいは変数の名前はプログラム全域から参照できます。このようなスコープを持つ変数を特に大域変数と言います。ライブラリを構成する.cファイル内でプログラム全域のスコープを持つ変数や関数の名前をリンク先のプログラムでも、そのプログラム全域から参照可能です。

ただし参照する側で宣言が必要となります。関数についてはその定義部分が無ければ、宣言とみなされます。変数についてはextern予約語がついていれば宣言とみなされます。通常、宣言はヘッダファイルに記載されています。利用する.cファイルで、そのヘッダファイルがインクルードされています。

リーディングにあたり、プログラム全域のスコープを持つ名前、特に変数名には注意が必要です。データフロー追跡の範囲が格段に増します。

.cファイル内

ある.cファイル内にあって関数定義の外(トップレベルとも言う)でstatic修飾子付きで定義されている関数あるいは変数の名前はその.cファイル内からだけ参照されます。ファイルのトップレベルにあってstatic修飾子付きの変数や関数をファイルローカル変数、あるいはファイルローカル関数と呼ぶことがあります。もし同じ名前がソースコードツリー中の別のファイルで使われていても、その実体(定義)は別です。

static int counter;
static void func (void);
_images/scope.svg

あるプログラムについて各.cファイルは、それぞれ役割があり、他の.cファイルに何らかの機能を提供しています。

ある.cファイルにはファイルローカルの変数や関数もあれば、static修飾子無し(=プログラム全域スコープ)の関数もあるはずです。このうちプログラム全域スコープを持つ関数が、その.cファイルで実現する機能を起動するための窓口となります。ある.cファイルについてプログラム全体に対してどんな機能を提供しているのか、という疑問が湧いたら、まずはそのファイルで定義されているプログラム全域スコープの関数だけを調べれば良いでしょう。

ファイルローカルの関数や変数は、その窓口の下請けと位置付けることができます。窓口が直接ファイルローカル変数にアクセスしないように、ファイルローカル変数へアクセスするためのファイルローカル関数群が用意されている場合もあります。窓口となる関数はファイルローカル関数を経由してファイルローカル変数を利用しまう。

スコープとは関係ありませんが、staticとついた変数の値はプログラム起動時に0に初期化されます。

参照によるスコープの崩壊

ここまでで話が終れば良いのですが、残念ながらスコープの説明には続きがあります。スコープとは名前に関するものでした。名前によって参照される定義は、ポインタ変数を介して、名前に由来するスコープを越えて参照されてしまいます。

/* bar.h */
void func(int * p);
/* bar.c */
static int * pointer;

void func(int * p)
{
        pointer = p;
}
/* foo.c */
#include "bar.h"
static int file_private;

...
{
        ...
        func(&file_private);
        ...
}

このコード例でfile_privateはfoo.c内でstatic修飾子付きで定義されています。file_privateという名前の利用範囲については foo.c だけを調べれば良いということになります。ところがfile_privateという名前で指す変数は、func関数にアドレス参照演算子付きで渡したところで、bar.cから見えるようになります。bar.cで定義された関数funcを経由してfile_privateという名前で指す変数のアドレスがbar.cに知れてしまいます。bar.cではpointerポインタ変数経由でその変数の値を参照、操作できてしまいます。

static修飾子付き変数については、名前ではなくアドレス経由で変数がファイルの外へ漏れてしまっていないか注意する必要があります。static修飾子付き関数の参照を活用するケースについては、コードパターン: 関数ポインタとコールバックにて仔細に検討します。

初期値が明記されていないstatic変数の値

初期値が明記されていないstatic変数の値は、実行時に実行環境が0で塗り潰します。

static boolean b;
static int i;
static void* p;

static boolean b = false;static int i = 0;static void* p = NULL;

と読み替えることができます。

コマンドライン引数

プログラムの起動引数はmain関数に渡されます。int型の第一引数がその個数を、文字列(char*)の配列型の第二引数にその内容が渡されます。通常個数を示す変数にargc、内容を格納する変数にargvの名前が付けられています。本当に適当な痕跡文字列が見当らないとき、制御フローの追跡をmainから、データフローについてはこのargc, argvから開始することになります。