コードの信頼性を落とさないために守るべきこと | C言語

ソフトウェアの信頼性とは、狙い通り正確に動作すること、誤動作しても速やかに復帰して周囲のシステムに影響を及ぼさないことなどが求められます。テストにより不具合を減らすことはできます。しかしテストにできるのは、落ちきった品質を少し持ち上げる程度でしかないということです。品質を上げるためには、仕様書の完成度を上げるしかないのです。

ここではそもそも品質を落とさないようにするために、ソースコードレベルで守るべきことをまとめていきます。

浮動小数点式は等価または非等価の比較をしない

浮動小数点数の演算には誤差が発生します。このため等価ではなく「限りなく近い」という判定を行うべきです。

[不適合例]

void func(double d1, double d2) {
  if (d1 == d2) {
    ...
  }
}

[適合例]

void func(double d1, double d2) {
  double diff = d1 - d2;
  double eps  = 1e-7;
  if ((-eps <= diff) && (diff <= eps)) {
    ...
  }
}
浮動小数点型変数をループカウンターとして使用しない

動小数点数の演算を繰り返すと誤差が累積するため、意図した結果が得られない恐れがあります。

[不適合例]

double d;

// ループが10回まわるとは限らない
for (d = 0.0; d < 1.0; d += 0.1) {
  ...
}

[適合例]

int i;
double d;

for (i = 0; i < 10; i++) {
  d = i / 10.0;
  ...
}
構造体や共用体の比較にmemcmpを使用しない

構造体や共用体はアライメント調整により、未使用の領域が含まれる可能性があります。その領域にどのような値が入っているかわからないので、メンバー同士で比較してください。

[不適合例]

typedef struct {
  char c;
  long w;
} HOGE;
HOGE var1, var2;

void funct(void) {
  if (memcmp(&var1, &var2, sizeof(var1))
      == 0) {
    ...
  }
  ...
}

[適合例]

typedef struct {
  char c;
  long w;
} HOGE;
HOGE var1, var2;

void funct(void) {
  if ((var1.c == var2.c) &&
      (var1.w == var2.w)) {
    ...
  }
  ...
}
符号付きと符号なしの混在した演算・比較は期待する型にキャストする

[不適合例]

int i;
unsigned int ui;

void func(void) {
  i = i / ui;

  // iがunsigned int型に型変換される
  // "i < 0"の場合は期待した結果にならない
  if (i < ui) {
    ...
  }
}

[適合例]

int i;
unsigned int ui;

void func(void) {
  i = i / (int)ui;

  if (i < (int)ui) {
    ...
  }
}
ループカウンターと継続条件式に使用する変数は同じ型にする

[不適合例]

void func(int num) {
  signed char i;

  // numがsigned char型で表せられない値の場合
  // 無限ループに入る
  for (i = 0; i < num; i++) {
    ...
  }
}

[適合例]

void func(int num) {
  int i;
  for (i = 0; i < num; i++) {
    ...
  }
}
演算の型と演算結果の代入先の型が異なる場合はキャストする

[不適合例]

int i1, i2;

void func(void) {
  long   w;
  double d;

  // int型が2バイトでlong型が4バイトの場合
  // "i1 + i2"がint型の範囲を超えると
  // 上位バイトが切り捨てられる
  w = i1 + i2;

  // 演算結果の小数点以下が切り捨てられる
  d = i1 / i2;
}

[適合例]

int i1, i2;

void func(void) {
  long   w;
  double d;

  w = (long)i1 + i2;
  d = (double)i1 / i2;
}
ビット演算は符号なし整数に対してのみ行う

符号付き整数のビット演算は処理系によって異なるため、期待した演算結果にならない恐れがあります。

ビット反転または左シフトをする場合は結果の型を明示的にキャストする

int型より小さな整数型は、演算時に整数拡張されます。これにより期待した演算結果にならない恐れがあります。

[不適合例]

unsigned char port = 0x5a;
unsigned char result;

// int型が2バイトだとすると
// "~port"は0xffa5となり"result = 0xfa"となる
result = (~port) >> 4;

[適合例]

unsigned char port = 0x5a;
unsigned char result;

// result = 0x0a
result = (unsigned char)(~port) >> 4;
ローカル変数にサイズの大きい配列を宣言しない

ローカル変数は、関数呼び出し時にスタックメモリーに配置されます。これにより次のような問題が発生します。

  1. スタックオーバーフローの発生する恐れが高まります。
  2. スタックメモリーへの確保と解放に時間を取られます。

[不適合例]

void func(void) {
  char buf[1000];
  ...
}

[適合例]

// ①か②のどちらかの対応をとる
static char m_buf[1000];  // ①
void func(void) {
  static char buf[1000];  // ②
  ...
}

コメント

コメントする

CAPTCHA


目次