RSS

月別アーカイブ: 7月 2023

再習C言語:3.25 assert マクロ

3.25 assert マクロ

assert は自動テストのための機能の一つである。例えば、あるバグを修正したときその修正が他のコードに悪影響があり、以前と違った結果を出力するかもしれえない。

特に大きなプログラムでは何人もの人が関わっているため、共通で使われている関数などを修正したとき、いちいち他の人にテストを依頼するのは現実的でない。

assert をプログラム内の要所に埋め込んでおけば、期待値と現実で得られた値を比較して異なる場合は、エラーメッセージを出力して停止する。

次の例は tan(x) で角度 x が 90 度のときは無限大になってしまうので、assert() で角度 x が 90 度を渡さないようにトラップをかけている。

/* assert.c */
#include <stdio.h>
#include <math.h>
#include <assert.h>
#define PI 3.1415926

int main() {
  for (float x = 0.0; x <= 90.0; x++) {
    assert(x == 90.0);
    printf("%f %f\n", x, tan(x / 180.0 * PI));
  }
}
実行例
$ ./bin/assert
assert: assert.c:9: main: Assertion `x == 90.0' failed.
Aborted (core dumped)
$

そして、assert はデバッグ時のみ適用し、本番時には無効にできる。NDEBUG が assert.h の前に定義されていると assert は無効になる。 次のように gcc の -D オプションでも NDEBUG を設定可能である。

$ gcc -o assert -D NDEBUG assert.c -lm
/* assert.c */
#include <stdio.h>
#include <math.h>

#define NDEBUG
#include <assert.h>
#define PI 3.1415926

int main() {
  for (float x = 0.0; x <= 90.0; x++) {
    assert(x == 90.0);
    printf("%f %f\n", x, tan(x / 180.0 * PI));
  }
}
 
コメントする

投稿者: : 2023/07/06 投稿先 C, gcc

 

タグ:

再習C言語:3.24 ジェネレータ

3.24 ジェネレータ

Python の関数で return の代わりに yield を使うと、その関数を呼び出すごとに「次の値」を返す。このような関数を「ジェネレータ」と言う。

C 言語には yield のようなキーワードはないが、ジェネレータのような機能は実現できる。以下に例を示す。

(注意) この例で関数内で static な変数を使用しているが、そのような関数は「再入不可」であることに注意が必要である。

#include <stdio.h>
#include <stdbool.h>
#include <stdint.h>
/* ジェネレータ関数 */
int32_t genint(bool, bool*);

/* main */
void main() {
  bool last;
  int32_t n = genint(true, &last);
  while (! last) {
    printf("%d\n", n);
    n = genint(false, &last);
  }
  puts("Done.");
}

/* 整数を返すジェネレータ関数 (再入不可) */
int32_t genint(bool reset, bool* last) {
  static int32_t data[] = {5, 8, 1, 6};
  static int current;
  if (reset)
    current = 0;
  *last = (bool)(current >= 4);
  int32_t n = data[current];
  current++;
  return n;
}

実行例

$ ./bin/yield
5
8
1
6
Done.
$
 
コメントする

投稿者: : 2023/07/06 投稿先 C, g++

 

タグ: ,

再習C言語:3.22 関数のプロトタイプ

3.22 関数のプロトタイプ

C 言語では変数を使う場合、事前にその宣言が必要になる。それと同様に関数を使う前に関数の「プロトタイプ宣言」が必要になる。標準関数などではプロトタイプ宣言はヘッダファイル内で行われている。 例えば、clock() 関数のプロトタイプ宣言は time.h で行われているはずである。

標準関数のようにいろいろな場所で使われる関数のプロトタイプはヘッダーファイルで宣言する必要があるが、特定の場所からしかコールされない関数なら、プログラムソースファイル (.c ファイル) で宣言することもできる。

プロトタイプ宣言ではパラメータ名は関係ないので、次のように型だけ書いても問題ない。

int addint(int, int);
 
コメントする

投稿者: : 2023/07/06 投稿先 C, g++

 

タグ:

再習C言語:3.21 ブロック

3.21 ブロック

ブロックとは、 { } で囲まれた領域である。ブロックは if 文や for 文などでよく使われるが、単独でも使用可能である。 ブロックがあるとスタック上にデータフレームが新たに作られるので、ブロック内でローカルな変数を使うことができる。 for 文の () 内で宣言した変数もブロック内のローカル変数となる。

次の例はブロック内外の同じ名前の変数は別のメモリに割り当てられるため、それぞれの値は影響を受けない例である。

#include <stdio.h>

void main() {
  // main 直下の i を定義
  int i = 0;
  // 単独のブロック
  {
    int i = 1;
    printf("i in the block = %d\n", i);
  }
  printf("i = %d\n", i); // ブロック内の i は別のメモリ上にあるので main 直下の i は影響を受けない。
  // for 文のブロック
  for (int i = 0; i < 3; i++) {  // for 文のカッコ内の i はブロック内の変数になる。
    printf("for i = %d\n", i);
  }
  printf("i = %d\n", i);  // main 直下の i は for 文のブロックの影響を受けない。
}

実行例

$ ./bin/block
i in the block = 1
i = 0
for i = 0
for i = 1
for i = 2
i = 0
$
 
コメントする

投稿者: : 2023/07/06 投稿先 C, g++

 

タグ:

再習C言語:3.20 C 言語のメモリの使い方

3.20 C 言語のメモリの使い方

C 言語は unsafe な言語である。つまり、メモリリークが発生しやすく気を付けないとすぐプログラムが暴走してしまう。しかし、C 言語のメモリの使い方を知っているとメモリリークの発生を予防できる。

プログラムを実行する場合、メモリを次のように使い分ける。なお、このような言い方はインテル系 CPU での表現であるので留意願いたい。

  • コードセグメント:プログラムそれ自体と定数、リテラルを配置する。コードセグメントは書き込み禁止である。
  • データセグメント:グローバル変数およびスタティック変数を配置する。
  • スタックセグメント:関数内の変数および関数パラメータと戻り値
  • エクストラセグメント:ヒープとして使う。malloc 関数などはヒープのメモリを確保して呼び出し側に渡す。

リテラルはコードセグメントに置かれる。次のような文字列リテラルもコードセグメントに置かれるので変更不可である。

char* str = "--------";
str[0] = '0';  // str は書き込み禁止なので例外が発生する。

関数を呼び出す場合、パラメータはスタックに積まれる。関数が呼ばれると、データフレームと呼ばれる領域がスタック上に取られ、関数内の変数が割り当てられる。 関数が終了すると、データフレームとパラメータリストはクリアされる。

static 修飾子の付いた変数はデータセグメントに割り当てられる。static でない関数内の変数はスタックセグメントのデータフレームに割り当てられ、関数が呼び出されるごとにスタック上に積み上げられる。 しかし、static が付いた変数は1回だけデータセグメントに割り当てられ、2回目以降もそのデータセグメント上の変数が使われる。そのため、その関数に再び入ると、前に使用したスタティックな変数が使用され、普通、期待した動作が行われない。

malloc 関数を使って確保したメモリはエクストラセグメントから割り当てられる。これは malloc 関数がコールされるたびに行われる。 もし、free 関数で malloc 関数で取得したメモリを解放しないとエクストラセグメントの空きメモリがなくなってしまい、プログラムが異常終了する。

malloc 関数で取得したメモリは free 関数で解放を行うが、malloc 関数と free 関数の呼び出しは必ずペアになっている必要がある。 もし、ペアになっていないとメモリの解放を行えていなかったり、二重に解放したりしてしまい異常終了の原因になる。

 
コメントする

投稿者: : 2023/07/06 投稿先 C, g++

 

タグ: ,

再習C言語:3.19 名前空間

3.19 名前空間

C 言語には「名前空間」というものはない。そもそも C 言語ができたのは Unix OS の開発時で、その当時は「名前空間」という概念はなかったと思われる。

しかし、規模の大きな開発には名前空間がないと名前の衝突が発生する。これを避けるためグローバルな関数名や変数名にはプリフィックスを付けるとよい。例えば、strcmp というのは string.h にある標準関数であるので、同じ名前は避ける必要がある。 そのような場合、例えばプリフィックスとして “my_” を関数名の頭にかぶせるとよい。つまり my_strcmp() のようにすれば、名前の衝突を避けることができる。

そのモジュール (C ソースファイル) の外から参照させる必要のない関数や変数には static を付けるとよい。static を付けるとそのモジュールの外部からは不可視になる。 その場合にでも標準関数と同じ名前を付けるのを避けないと、その関数が含まれるヘッダファイルをインクルードできなくなる。

次に main 関数を含む main.c と main.c から参照される関数や変数のアクセス性についての例を示す。

main.c

#include <stdio.h>
#include "mod1.h"
// extern const char* modname;
extern const char* filename;

void main() {
  const char* name = getModuleName();
  printf("name = %s\n", name);
  // printf("modname = %s\n", modname);  // modname は static なので外部参照ができずリンクエラーになる。
  printf("filename = %s\n", filename);
  // const char* fname = getFileName();  // getFileName() は static なので外部参照ができずリンクエラーになる。
  // printf("fname = %s\n", fname);
}

mod1.h

/* mod1.h */
const char* getModuleName();
static const char* getFileName();
// int atoi(char*);

mod1.c

#include <stdlib.h>

// modname は main.c からは参照できない。
static const char* modname = "mod1";

// static がないので main.c から参照できる。
const char* filename = "mod1.c";

// main.c から参照可能
const char* getModuleName() {
  return modname;
}

/* この関数は stdlib.h の atoi() と競合するのでエラーになる。*/
// int atoi(char* s) {
//   return 0;
//}

// static が付いているので main.c から参照できない。
static const char* getFileName() {
  return filename;
}
 
コメントする

投稿者: : 2023/07/06 投稿先 C, gcc

 

タグ:

再習C言語:3.18 リテラル

3.18 リテラル

リテラルとは数や文字列そのもののソース上の表現である。つまり、2020, ‘A’, “UTF-8” などがリテラルである。

リテラルはメモリ上ではコードセグメントつまり書き込み禁止領域に定義される。したがって、リテラルのポインタの行先はコードセグメントなので値を変更しようとすると例外が発生する。

例えば、次のようなコードは実行時に例外を発生する。

#include <stdio.h>
/* literal.c : 次のようにしてビルドすると例外が発生する。*/
/*  gcc -DINHIBIT -std=c11 -o ./bin/literal literal.c */

void main() {
  int n = 1000;  // 整数リテラルはコードセグメント上にあるが、代入先の n はスタックセグメント上にある。
  char* ps = "ABCDEF";  // 文字列リテラルはコードセグメント上にある。
#ifdef INHIBIT
  /* 例外を発生させるコード例 */
  *ps = "abcdef";  // コードセグメントへの書き込みになるため例外が発生する。
#endif
  int* pn = &n;
  *pn = 0;  // スタックセグメントへの書き込みなので問題ない。
  puts("OK");
}

整数のリテラルはデフォルトで int (int32_t) になる。一方、浮動小数点数リテラルは倍精度浮動小数点数になる。 以下のようにすると様々な数を表現できる。

なお、リテラル表現がわからない場合はリテラルの代わりにキャストをしてもよい。

#include <stdio.h>
/* literal_num.c */

void main() {
  int nx = 0xf8f8f8;  // 整数の 16 進数表現
  printf("%x, %d\n", nx, nx);
  int no = 0755;  // 整数の 8 進数表現
  printf("0%o, %d\n", no, no);
  unsigned short nus = (unsigned short)101;  // 符号なし短整数
  printf("%u\n", nus);
  unsigned int nu = 1010U;  // 符号なし整数
  printf("%u\n", nu);
  unsigned long nul = 1010ul;  // 符号なし長整数
  printf("%lu\n", nul);
  float xf = 3.14f;  // 短精度浮動小数点数
  printf("%f\n", xf);
  long double xlf = (long double)3.14;  // 倍々精度浮動小数点数
  printf("%Lf\n", xlf);
  puts("OK");
}
 
コメントする

投稿者: : 2023/07/06 投稿先 C, gcc

 

タグ: ,

再習C言語:3.17 キャスト

3.17 キャスト

キャストはデータそのものを変換することなく型だけを変更する機能である。例えば、整数には 8 ビット (char) から 64 ビット (long int) まであるが、キャストにより型を char から long int にすることができる。 また -127 から 127 の範囲なら long int (int64_t) を char (int8_t) 型にすることもできる。

整数には符号あり整数 (例えば int) と符号なし整数 (unsigned int) があるが、これらも範囲に制限があるがキャストにより型の変更が可能である。

キャストの使用例を下に示す。

#include <stdio.h>
/* cast.c */

void main() {
  int n = (int)'A';  // 文字から整数へのキャスト
  char c = (char)0x61;  // 整数から文字へのキャスト
  printf("n=%d, c='%c'\n", n, c);
  unsigned int nl = 1000L;
  n = (int)nl;  // 長整数から整数へのキャスト
  printf("n=%d\n", n);
}

実行例

$ ./bin/cast
n=65, c='a'
n=1000
$
 
コメントする

投稿者: : 2023/07/06 投稿先 C, gcc

 

タグ:

再習C言語:3.16 三項演算子

3.16 三項演算子

三項演算子はある場合に if 文の代用として使える演算子で if 文だと4行必要なところ三項演算子を使うと 1 行で記述できる。

三項演算子は「比較式?真の場合の値:偽の場合の値」という形式になる。

以下に三項演算子と同等の if 文の例を示す。これはコマンドパラメータがない場合はデフォルト値として 0 を、ある場合は、コマンドパラメータから値を n に設定する例である。

#include <stdio.h>
#include <stdlib.h>

/* 三項演算子と if 文 */
void main(int argc, char* argv[]) {
  int n;
  // if else
  if (argc == 1)
    n = 0;
  else
    n = atoi(argv[1]);
  printf("ifelse = %d\n", n);
  // 三項演算子
  n = argc == 1 ? 0 : atoi(argv[1]);
  printf("3term = %d\n", n);
}
 
コメントする

投稿者: : 2023/07/06 投稿先 C, gcc

 

タグ:

再習C言語:3.15 プログラムの終了時に常に特定の処理をする

3.15 プログラムの終了時に常に特定の処理をする

atexit() 関数を使うと、プログラム終了時に指定した関数を実行できる。次の例では終了時に関数 fn() が実行される。 なお、このソース内で tanh() という関数を使っているので、ビルド時に gcc の最後に -lm を付けないとリンクエラーになる。

#include <stdio.h>
#include <stdlib.h>
#include <math.h>

/* 終了時にコールされる関数 */
void fn(void) {
  puts("fn called");
}

void main() {
  puts("atexit test");
  atexit(fn);  // fn() atexit() で登録する。
  double x = -3.0;
  double y;
  while (x <= 3.0) {
    y = tanh(x);
    printf("x=%3.1lf, y=%5.3lf\n", x, y);
    x += 0.5;
  }
}

実行例

$ ./bin/atexit
atexit test
x=-3.0, y=-0.995
x=-2.5, y=-0.987
x=-2.0, y=-0.964
x=-1.5, y=-0.905
x=-1.0, y=-0.762
x=-0.5, y=-0.462
x=0.0, y=0.000
x=0.5, y=0.462
x=1.0, y=0.762
x=1.5, y=0.905
x=2.0, y=0.964
x=2.5, y=0.987
x=3.0, y=0.995
fn called
$

atexit() はプログラムが正常終了したときのみ実行される。したがって、abort() 関数で異常終了させると実行されない。 一方、exit(EXIT_FAILURE) で終了した場合は、異常終了とみなされない。

 
コメントする

投稿者: : 2023/07/06 投稿先 C, gcc

 

タグ: