RSS

月別アーカイブ: 7月 2023

再習C言語:3.14 プログラムの終了

3.14 プログラムの終了

プログラムは main 関数が終了すると同時に終了する。その際、return で終了コードを返すことができる。終了コードは stdlib.h で定義されている EXIT_SUCCESS (0) または EXIT_FAILURE (1) を返すのが普通である。 しかし、アプリケーションによってはその他のコードを返すことがある。このコードはそのプログラムを起動したシェルで変数 $? に保存され、シェルスクリプトで利用できる。

main 以外の関数で終了する場合は、処理を main に戻す他、void exit(int code) 関数でもそのプログラムを終了させることができる。 また、致命的なエラーを検出した場合は、void abort() で異常終了させることもできる。この場合、終了コードは不定になる。

exit() 関数の使用例

/* exit.c */
#include <stdio.h>
#include <stdlib.h>

void func();

/* main */
void main() {
    puts("main start.");
    func();
    puts("main end.");  // ここへは戻らない。
    exit(EXIT_SUCCESS);
}

void func() {
    puts("func start");
    exit(EXIT_FAILURE); // ここで終了する。
    puts("func end.");
}

実行例

$ ./bin/exit
main start.
func start
$ echo $?
1
$

abort() 関数の使用例

/* abort.c */
#include <stdio.h>
#include <stdlib.h>

void func();

/* main */
void main() {
    puts("main start.");
    func();
    puts("main end."); // ここには戻らない。
    exit(EXIT_SUCCESS);
}

void func() {
    puts("func start");
    abort();  // ここで異常終了
    puts("func end.");
}

実行例

$ ./bin/abort
main start.
func start
Aborted (core dumped)
$ echo $?
134
$
 
コメントする

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

 

再習C言語:3.13 関数の再帰呼び出し

3.13 関数の再帰呼び出し

他の言語でもたいてい可能ですが、関数内で自関数を呼び出すことができる。この機能を再帰呼び出しと言う。 再帰呼び出しはツリー構造のデータを検索するときなどに便利だが、大量にスタック領域を消費するので深い再帰になる場合は注意が必要である。

次の例は再帰呼び出しにより階乗 n! を計算するものである。

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

int64_t factorial(int64_t);

void main(int argc, char* argv[]) {
  int64_t n = 0;
  if (argc > 1)
    n = atol(argv[1]);
  printf("%ld\n", factorial(n));
}

/* 階乗の計算 */
int64_t factorial(int64_t n) {
  if (n == 0)
    return 1L;
  else {
    return n * factorial(n - 1); // ここで自関数を呼び出す。
  }
}

実行例

$ ./bin/factorial 4
24
$ ./bin/factorial 5
120
$
 
コメントする

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

 

タグ:

再習C言語: 3.12 インクリメント演算子

3.12 インクリメント演算子

3.12.1 ++ 演算子の位置の違い

++ 演算子等には前付け (++a) と後付け (a++) がある。これらの違いは、値を読んだとき演算後の値を得るか、演算前の値を得るかである。

#include <stdio.h>

/* 前付け ++ 後付け ++ の違い */
void main() {
  int i = 0;
  printf("%d\n", ++i);  // ++ 後の値になる。(つまり 1)
  printf("%d\n", i++);  // ++ 前の値になる。(つまり 1)
  printf("%d\n", i);
}

実行例

$ ./bin/inca
1
1
2
$

++ 演算子は対象の変数を + 1 するが += 演算子を使うと 2 以上増加できる。また、減少させる場合は、– や -= を使う。

  (例) i += 2; j -= 4;

コマンドライン引数は main 関数の第二引数で取得できるが、++ 演算子を使うと簡単に取得できる。

int main(int argc; char** argv) {
if (argc < 3) {
  ....
  return EXIT_FAILURE;
}
const char* param1 = *++argv;  // argv[0] はコマンド名であるため ++ を argv の後に置くとコマンド名が返される。
const char* param2 = *++argv;
const char* param3 = *++argv;

  ....

}

3.12.2 ポインタのインクリメント

ポインタに対して ++, — や +=, -= を適用すると、そのポインタが指すデータの幅だけポインタが進んだり、戻ったりする。 例えば、int 型なら sizeof(int) だけ進んだり戻ったりする。つまり、配列の次の要素、前の要素を指す。

次の例は uint8_t, uint32_t のポインタの例である。

#include <stdio.h>
#include <stdint.h>

void main() {
  uint8_t buff8[] = "ABCDEF";
  uint32_t buff32[] = {1, 2, 3, 4, 5};
  uint8_t* p8 = buff8;
  uint32_t* p32 = buff32;
  printf("%c, %u\n", *p8, *p32);
  p8 += 1;  // uint8_t ポインタの加算は 1 バイト分だけ進む。
  p32 += 1;  // uint32_t ポインタの加算は 4 バイト分だけ進む。
  printf("%c, %u\n", *p8, *p32);
}

実行例

$ ./bin/incb
A, 1
B, 2
$
 
コメントする

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

 

タグ:

再習C言語: 3.11 ビット演算

3.11 ビット演算

ビット演算は他の言語でもたいてい可能だが、C 言語は「システム記述言語」(OS やコンパイラ、ファームウェアの開発に使われる) なので使われる頻度が高い。

ビット演算子には次のようなものがある。シフトには「算術シフト」と「論理シフト」の違いがあり、普通の整数の場合、算術シフトは符号を含めてシフトする。
正負は 2 の補数として表現されるため、最上位ビットにより判別される。 よって、シフト対象が符号なし整数なら論理シフト(符号が付かない) になり、そうでなければ算術シフト(符号が付く) になる。

  • NOT: ~
  • AND: &
  • OR: |
  • XOR: ^
  • Shift Left: <<
  • Shift Right: >> (符号なし整数なら論理シフト、普通の整数なら算術シフト)

サンプル1 (算術演算とビット演算)

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

/* 算術演算とビット演算 */
int main(int argc, char* argv[]) {
  if (argc == 1) {
    puts("Usage: bitop1 uint8");
    return EXIT_FAILURE;
  }
  uint8_t a = atoi(argv[1]);
  // 足し算で文字コードを変える。
  printf("%02xH\n", a);
  printf("%c\n", a);
  uint8_t b = a + 1;
  printf("%02xH\n", b);
  printf("%c\n", b);
  // x 2 は左シフトと同じになる。
  b = a * 2;
  printf("%08xH\n", b);
  b = a << 1;
  printf("%08xH\n", b);

  return EXIT_SUCCESS;
}

実行例

$ ./bin/bitop1 51
33H
3
34H
4
00000066H
00000066H

サンプル2 (1の補数と2の補数)

次のサンプルは整数の1の補数と2の補数を求めて表示する。1の補数は単純にビットの値を反転させたもので、2の補数は元の数に加算するとキャリーが1ですべてのビットが 0 となるような値である。

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

/* 補数を求める。*/
int main(int argc, char* argv[]) {
  if (argc < 2) {
    puts("Usage: bitop2 hex");
    return EXIT_FAILURE;
  }
  uint32_t a;
  sscanf(argv[1], "%x", &a);

  // 1の補数
  printf("%08x\n", ~a);
  //  あるいは
  printf("%08x\n", 0xffffffff ^ a);
  // 2の補数
  printf("%08x\n", 0 - a);
  //  あるいは
  printf("%08x\n", -a);
}

実行例

$ ./bin/bitop2 1
fffffffe
fffffffe
ffffffff
ffffffff
$

サンプル3 (パリティ計算)

このサンプルは32ビット符号なし整数のパリティを計算する。パリティとは、その整数に含まれる各ビットの1の数が偶数なら 0 、奇数なら 1 となるようなビットで整数値の誤り検出に使用される。

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

uint32_t parity(uint32_t, bool);

/* 32ビット整数のパリティを計算する。*/
int32_t main(int argc, char* argv[]) {
  if (argc == 1) {
    puts("Usage: parity uint32 fast");
    return EXIT_FAILURE;
  }
  uint32_t u = atol(argv[1]);
  bool fast = atoi(argv[2]) ? true : false;
  uint32_t par = parity(u, fast);
  printf("PARITY=%u\n", par);
}

/* パリティ計算 */
uint32_t parity(uint32_t u, bool fast) {
  if (fast) {
    /* XOR 演算を用いて高速演算 */
    u ^= u >> 16;
    u ^= u >> 8;
    u ^= u >> 4;
    u ^= u >> 2;
    u ^= u >> 1;
    return u & 0x00000001;
  }
  else {
    /* 定義に基づいて計算 */
    int count = 0;
    for(int i = 0; i < 32; i++) {
      if(u & 0x00000001)
       count++;
     u >>= 1;
    }
    return count & 0x00000001;
  }
}

実行例

$ ./bin/parity 269504512 0
PARITY=0
$ ./bin/parity 269504512 1
PARITY=0
$ ./bin/parity 269504513 0
PARITY=1
$ ./bin/parity 269504513 1
PARITY=1

サンプル4 (st_mode のマスク処理)

このサンプルは stat 関数でファイル情報を取得し、ファイルモード st_mode からビット演算でユーザ、グループ、誰でものモードを取得して表示するものである。

#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <sys/stat.h>

/* フラグの操作 */
int main(int argc, char* argv[]) {
  if (argc == 1) {
    puts("Usage: bitop3 file_path");
    return EXIT_FAILURE;
  }
  struct stat stat_buff;
  if (stat(argv[1], &stat_buff) != 0) {
    puts("Error. Is the file_path wrong?");
    return EXIT_FAILURE;
  }
  char access[11];
  access[10] = '\0';
  access[0] = S_ISDIR(stat_buff.st_mode) ? 'd' : '-';
  access[1] = stat_buff.st_mode & S_IRUSR ? 'r' : '-';
  access[2] = stat_buff.st_mode & S_IWUSR ? 'w' : '-';
  access[3] = stat_buff.st_mode & S_IXUSR ? 'x' : '-';
  access[4] = stat_buff.st_mode & S_IRGRP ? 'r' : '-';
  access[5] = stat_buff.st_mode & S_IWGRP ? 'w' : '-';
  access[6] = stat_buff.st_mode & S_IXGRP ? 'x' : '-';
  access[7] = stat_buff.st_mode & S_IROTH ? 'r' : '-';
  access[8] = stat_buff.st_mode & S_IWOTH ? 'w' : '-';
  access[9] = stat_buff.st_mode & S_IXOTH ? 'x' : '-';
  printf("%s %s\n", access, argv[1]);
  return EXIT_SUCCESS;
}

実行例

$ ./bin/statmode ./bin/bitop1
-rwxrwxr-x ./bin/bitop1
$ ./bin/statmode ../
drwxrwxr-x ../
 
コメントする

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

 

タグ: ,

再習C言語:3.10 関数値の返し方

3.10 関数値の返し方

関数値としてスカラー以外も返すことができるが、関数内の変数は関数が終了したときその内容が保証されないし、スタック上にその値を積んで返す場合、スタックを圧迫したり、 スタックから値を引き取るのに時間がかかり、特にループないとかではスピード上の問題もある。 スカラーの場合は、CPU 内にある「レジスタ」に関数値を入れて呼び出し側に渡すためスピードも速くてスタックも圧迫しない。

よって、スカラー以外のオブジェクトを返す場合、そのポインタを返すか、パラメータとして返す場所のポインタを受け取り、そこへオブジェクトを格納する。

ポインタを関数値として返す場合、関数内の変数のポインタを返すことはできない。と言うのは、 関数が終了するとその変数もなくなるため (実際には正しいデータが返される場合もあるが、それは常には保証されない。) である。

static を付けた関数内の変数は消えないが、場合によっては正しく動かないこともある。 例えば、再帰呼び出しなどを行うとおかしな結果になることがありうる。

関数内で malloc 関数でメモリを確保し、そのポインタを返す場合はそういう心配はない。ただし、その確保したメモリを適切なタイミングで解放しないとメモリリークの原因になる。

3.10.1 関数内の static 変数のポインタを返す例

#include <stdio.h>
/* 関数内の static 変数のポインタを返す例。*/

char* func(void);

void main() {
  char* data = func();
  puts(data);
}

char* func() {
  static char data[] = "ABCDEF";
  return data;
}

関数内で確保した領域のポインタを返す例

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* 関数内で確保した領域のポインタを返す例。*/

char* func(void);

void main() {
  char* data = func();
  puts(data);
  free(data);
}

char* func() {
  char* data = malloc(10);
  strcpy(data, "abcdef");
  return data;
}

パラメータで関数値を格納する領域のポインタを受け取り、そこに関数値を格納する例

#include <stdio.h>
#include <string.h>
/* パラメータで関数値を格納する領域のポインタを受け取り、そこに関数値を格納する例 */
void func(char*);

void main() {
  char buff[10];
  func(buff);
  puts(buff);
}

void func(char* result) {
  strcpy(result, "01234");
}

3.10.4 構造体変数を関数値として返す例

#include <stdio.h>

struct Point {
  double x;
  double y;
};

struct Point clear_point(struct Point p);

int main() {
  struct Point p1;
  struct Point p2 = clear_point(p1);
  printf("%lf, %lf\n", p2.x, p2.y);
}

struct Point clear_point(struct Point p) {
  p.x = 0.0;
  p.y = 0.0;
  return p;
}

ポインタを含む構造体を返す場合は、ポインタが指す先の変数の値が関数終了後も保証されるのかよく考える必要がある。 次の例はポインタの指す先が関数内の配列なので関数終了後には値が保証されない。関数終了直後は値は残っているが、時間が経つとどうなるかわからない。

/* ポインタを含む構造体変数を関数値として返す。 */
#include <stdio.h>
#include <string.h>

struct Points {
  double **x;
  double **y;
};

struct Points clear_points(struct Points p, int n);

/* main */
int main() {
  struct Points p1;
  struct Points p2 = clear_points(p1, 5);
  double* px;
  double* py;
  for (int i = 0, px = p1.x, py = p1.y; i < 5; i++) {
    printf("%lf, %lf\n", p2.x++, p2.y++);
  }
}

/* 長さ n のクリアされた Points を返す関数 */
struct Points clear_points(struct Points p, int n) {
  double a[n];
  memset(a, 0, n);
  double b[n];
  memset(b, 0, n);
  p.x = a;  // 配列 a はスタック上にあるので、関数が終わると内容が保証されない。(正しく動作しないかもしれない)
  p.y = b;  //  b についても a と同様。
  return p;
}
 
コメントする

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

 

タグ: ,

再習C言語: 3.9 関数の引数

3.9 関数の引数

3.9.1 配列の引数

配列を関数に渡す場合、そのまま配列を渡す方法と配列のポインタを渡す方法がある。配列をそのまま渡すと、配列をスタックにコピーするので巨大な配列だと時間がかかるし、スタックを消費しエラーが発生するかもしれない。

配列をそのまま渡す例
#include <stdio.h>
#include <stdint.h>

/* 配列を直接関数へ渡す例 */
uint8_t checksum(uint8_t a[], int16_t n);

void main() {
  uint8_t a[6] = {'0', 'A', '1', 'B', '2', '3'};
  uint8_t s = checksum(a, sizeof(a));
  printf("%02x\n", s);
}

uint8_t checksum(uint8_t a[], int16_t n) {
  uint8_t cs = 0;
  for (int16_t i = 0; i < n; i++) {
    cs += (cs + a[i]) % 256;
  }
  return cs;
}

3.9.2 配列のポインタ渡す例

#include <stdio.h>
#include <stdint.h>

/* 配列へのポインタを関数へ渡す例 */
uint8_t checksum(uint8_t* a, int16_t n);

void main() {
  uint8_t a[6] = {'0', 'A', '1', 'B', '2', '3'};
  uint8_t s = checksum(a, sizeof(a));
  printf("%02x\n", s);
}

uint8_t checksum(uint8_t* a, int16_t n) {
  uint8_t cs = 0;
  uint8_t* p = a;
  for (int16_t i = 0; i < n; i++) {
    cs += (cs + *p) % 256;
    p++;
  }
  return cs;
}

3.9.3 可変数パラメータを渡す例

stdarg.h に含まれる va_list 等を利用すると可変数パラメータを関数に渡すことができる。可変数パラメータを関数内で受け取る手順は次の通りである。

  1. va_list 型の変数を宣言する。
  2. va_list 型の変数と可変パラメータの終了値を引数として va_start() を実行する。(初期化)
  3. va_arg() で最初のパラメータを受け取る。
  4. パラメータが終了値でなければ、そのパラメータを保存またはその処理を行う。
  5. va_arg() で次のパラメータを受け取り、処理4を繰り返す。
  6. パラメータが終了値ならループを抜けて、va_list 型の変数をパラメータとして va_end() を実行する。

C 言語にはパラメータのデフォルト値を与える機能はないが、可変数パラメータを渡すことにより疑似的にパラメータのデフォルト値を与えることも可能である。

可変数パラメータの型は全部同じである必要があるが、文字列型のパラメータにすれば、内部で整数などに変換できるので、いろいろな型のパラメータを渡すことができる。

次に可変数パラメータの使用例を示す。

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

int func(char* nil, ...);

void main() {
  int n = func(NULL, "12", "0.52", "char");
  printf("n = %d\n", n);
}

int func(char* nil, ...) {
  char* arg;
  va_list vl;
  va_start(vl, nil);
  int i = 0;
  while ((arg = va_arg(vl, char*)) != nil) {
    switch (i) {
      case 0:
        int a = atoi(arg);
        printf("%d\n", a);
        break;
      case 1:
        float f = atof(arg);
        printf("%f\n", f);
        break;
      case 2:
        printf("%s\n", arg);
        break;
      default:
        break;
    }
    i++;
  }
  va_end(vl);
  return i;
}

このプログラムの実行例を下に示す。

$ ./bin/overload
12
0.520000
char
n = 3
$

終了値によりパラメータの数を判別する代わりに直接、パラメータの数を指定することもできる。前の例において func(int count, …) としてパラメータ数を渡すことが可能である。 具体的な例は、「5.1.6 可変長パラメータの入出力」のサンプルを参照。

3.9.4 構造体パラメータ

構造体を関数のパラメータにする場合、構造体そのものを渡す方法とポインタを渡す方法がある。構造体のサイズが大きくループ内の関数コールや再帰コールの場合は直接、構造体を渡すのは効率がよくない。

関数パラメータは一般にはスタックに積まれてから関数に渡される。そのため、関数呼び出しごとにそのオーバーヘッドが発生する。構造体の中に長い配列がいくつもあるような場合はそのオーバーヘッドは特に大きくなる。

一般に他の言語では構造体のような変数はポインタで渡される。その方がオーバーヘッドがなくしかもスタックに積まずに直接、CPU のレジスタで渡すことができるためである。

小さな構造体の場合は次のコードのようにしてもオーバーヘッドは小さいので問題ない。

/* 構造体変数を関数パラメータとして渡し、構造体を関数値として返す。 */
#include <stdio.h>

struct Point {
  double x;
  double y;
};

struct Point clear_point(struct Point p);

int main() {
  struct Point p1;
  struct Point p2 = clear_point(p1);
  printf("%lf, %lf\n", p2.x, p2.y);
}

struct Point clear_point(struct Point p) {
  p.x = 0.0;
  p.y = 0.0;
  return p;
}

長い配列を含む構造体を関数に渡す場合は、配列そのものを渡すのではなく次の例のようにポインタとして渡す方がよい。

/* ポインタを含む構造体変数を関数値として返す。 */
#include <stdio.h>
#include <string.h>

struct Points {
  double **x;
  double **y;
};

struct Points clear_points(struct Points p, int n);

/* main */
int main() {
  struct Points p1;
  struct Points p2 = clear_points(p1, 5);
  double* px;
  double* py;
  for (int i = 0, px = p1.x, py = p1.y; i < 5; i++) {
    printf("%lf, %lf\n", p2.x++, p2.y++);
  }
}

/* 長さ n のクリアされた Points を返す関数 */
struct Points clear_points(struct Points p, int n) {
  double a[n];
  memset(a, 0, n);
  double b[n];
  memset(b, 0, n);
  p.x = a;  // 配列 a はスタック上にあるので、関数が終わると内容が保証されない。(正しく動作しないかもしれない)
  p.y = b;  //  b についても a と同様。
  return p;
}
 
コメントする

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

 

タグ: ,

再習C言語: 3.8 例外

3.8 例外

残念なことに C 言語には “try .. catch .. finally” のような構文はない。従って例外が発生する可能性のある場所で、 次の処理を行うと例外が発生しないかの確認を行う必要がある。

特にポインタを使う個所は要注意である。よく発生するのがポインタが指す領域が実在しない場合である。例えば、配列の長さを超えた領域をアクセスしようとすることが起こり得る。 他の言語ではポインタという機能がないのでこういうことは起こらない。C# のようにポインタが使える場合も unsafe キーワードが必要で、unsafe な部分では同様のことが起こり得る。

Linux (Unix) では致命的エラーが発生したとき、シグナルが発生するのでそれを処理するコードを書けば、例外処理を行うことができる。Windows でも同様なことは可能だがメカニズムが異なるので、別のコードが必要になる。

参照 SIGNAL

次にメモリアクセスエラーのシグナルを受け取るコード例を示す。

fatal.c

/* fatal.c */
#include <unistd.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

static void handlerSIGBUS(int);
static void handlerSIGABRT(int);
static void handlerSIGSEGV(int);

/* main */
int main() {
  // ハンドラを設定する。
  signal(SIGBUS, handlerSIGBUS);   // Core バスエラー
  signal(SIGABRT, handlerSIGABRT); // Core abort(3) からの中断 (Abort) シグナル
  signal(SIGSEGV, handlerSIGSEGV); // Core 不正なメモリー参照

  // SIGSEGV を発生させる。
  char *src = "ABC";
  char *dest = NULL;
  strcpy(dest, src);  // dest が NULL なので例外が発生する。
}

/* シグナルハンドラ */
static void handlerSIGBUS(int) {
  puts("SIGBUS received (FATAL Error)");
  exit(EXIT_FAILURE);
}

static void handlerSIGABRT(int) {
  puts("SIGABRT received (FATAL Error)");
  exit(EXIT_FAILURE);
}

static void handlerSIGSEGV(int) {
  puts("SIGSEGV received (FATAL Error)");
  exit(EXIT_FAILURE);
}

recover.c

Long Jump を併用すると、例外が発生した場所へ戻ることもできる。

/* recover.c */
#include <unistd.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <setjmp.h>

static void handlerSIGSEGV(int);

// ジャンプ情報を保持するバッファ
jmp_buf jmpbuf;

/* main */
int main(int argc, char* argv[]) {
  // SIGNAL ハンドラを設定する。
  signal(SIGSEGV, handlerSIGSEGV); // Core 不正なメモリー参照

  if (setjmp(jmpbuf) == 0) {
    // ロングジャンプを行う関数を呼び出す。
    handlerSIGSEGV(0);
  }
  else {
    // ここにジャンプしてくる。
    puts("SIGSEGV received (FATAL Error)");
    return EXIT_FAILURE;
  }

  // SIGSEGV を発生させる。
  char* src = "ABC";
  char* dest = NULL;
  if (argc > 1)  // 何らかのコマンドパラメータがあれば例外を発生させない。
    dest = malloc(100);
  // *dest が NULL の場合は例外が発生する。
  strcpy(dest, src);  // *dest が NULL なので例外が発生する。
  printf("%s\n", dest);
  return EXIT_SUCCESS;
}

/* SIGSEGV シグナルハンドラ */
static void handlerSIGSEGV(int n) {
  longjmp(jmpbuf, SIGSEGV);  // jmpbuf の内容に基づいてロングジャンプする。
}

signal.h に含まれる int raise(int signal) 関数 を使うとシグナルを発生させることができる。関数コール階層の深い部分でエラーが発生したときなどに raise() を呼び出すと、他の言語の throw 文のような動作を行える。

その際には、シグナルにはユーザが自由に使える SIGUSER1, SIGUSER2 がある。

 
コメントする

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

 

タグ: , , , ,

再習C言語: 3.6 グローバル変数とスタティック変数、3.7 const, volatile, restrict 変数

3.6 グローバル変数とスタティック変数

関数の外部で宣言した変数は、他のモジュールの関数から参照可能である。その際、他のモジュールの変数であることを示すために extern キーワードを付ける必要がある。

次の例は、g1.c というモジュールのグローバル変数 comint を main 関数を含む main_g1.c で参照する例である。

main_g1.c

#include <stdio.h>
extern int comint;

void init_g1(void);

void main() {
  init_g1();
  printf("%d\n", comint);
}

g1.c

/* g1.c */
int comint;

void init_g1() {
  comint = 123;
}

ビルドと実行例を示す。

$ gcc -std=c11 -o bin/g1 main_g1.c g1.c
$ ./bin/g1
123

このように他のモジュールから変数が参照できる場合、予期せず変数の値が変更されてしまうことがある。他のモジュールから見えないようにするには static を付ければよい。

次の例は static を付けたので main 関数から参照できずにリンクできずにエラーになる。

main_g2.c

#include <stdio.h>
extern int comint2;  // 参照不可

void init_g2(void);

// このプログラムはリンクエラーになる。
void main() {
  init_g2();
  printf("%d\n", comint2);
}

g2.c

/* g2.c */
static int comint2;

void init_g2() {
  comint2 = 321;
}

static 変数は関数内でも使うことができる。関数内の static な変数は関数が終了しても値を保持するので、関数を呼び出すごとに static 変数を変化させることにより、他言語の yield のような機能を持たせることができる。

注意すべき点として static 変数を含む関数は「再入不可」になる。つまり、複数のスレッドから同時に呼び出したり、再帰呼び出しを行うと正しく動作しない。 これは static 変数が静的データ領域に作られるため、物理的に1つだけしなかないためである。static でない変数は関数がコールされるたびにスタック上に作られるため、関数に再入があっても正しく動作できる。

以下に関数内の static 変数の使用例を示す。

main_s1.c

#include <stdio.h>

int f(int);

void main() {
  printf("%d\n", f(1));
  printf("%d\n", f(0));
  printf("%d\n", f(0));
  printf("%d\n", f(0));
}

int f(int init) {
  static int n;
  if (init > 0) {
    n = init;
  }
  else {
    n = n * 2;
  }
  return n;
}

次にビルドと実行例を示す。

$ gcc -std=c11 -o bin/s1 main_s1.c
$ ./bin/s1
1
2
4
8

3.7 const, volatile, restrict 変数

const が付いた変数の値を変更することはできない。ただし、const が付いたポインタは変更できる。

次の例は const が付いたポインタが指す値を変更しようとしているのでコンパイルエラーになる。

#include <stdio.h>

int f(const char* p);

void main() {
  char buff[] = "01234";
  f(buff);
}

int f(const char* p) {
  p++;  // これは OK
  *p = 'A';  // コンパイルエラーになる。
  return 0;
}

restrict 修飾子の付いたポインタどうしはお互いに重なり合わないものとする。 ただし、これはヒントであってシステムが保証しているわけではない。 つまり、ポインタどうしが重ならないようにするのはプログラマの責任になる。なお、restrict は現在 C++ にはないので、 C++ でも使う関数には使えない。

void f(char* restrict a, char* restrict b);

volatile 修飾子は他のスレッドなどが書き換える可能性のある変数に付ける。これもヒントであり、書き込んだ値が後で読みだしたとき変化している可能性があることを示す。

volatile char xbuf[1000];
 
コメントする

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

 

タグ: , , , , ,

再習C言語:3.3 switch 文での制限、3.4 ブール値、3.5 ループ

3.3 switch 文での制限

C 以外の言語にも switch 文やそれに相当する文がある。しかし、普通、条件となる変数は整数以外でも大丈夫でよく文字列なども使われる。 一方、C では条件となる変数は整数型でなければならない。(列挙型も実質整数型なのでよく使用される)
あるいは “#define A 0” のようにシンボルを定義しておけば、case A: のように使えるのでプログラムがわかりやすくなる。 従って、次のような switch 文はエラーとなる。

/* エラーとなる switch 文 */
switch (argv[1]) {
  case "A":
    ...
    break;
  case "B":
    ...
    break;
  ....
}

この例で文字列でなく文字、例えば “A” でなく ‘A’ にすれば問題ない。つまり文字型は8ビット整数型であるためだ。

3.4 ブール値

古い規格 (C99 より前) の C では bool 型というのはなくて、整数型を bool 型として使っていた。 新しい規格ではヘッダファイル stdbool.h をインクルードすることで、 bool, true, false が使える。ただ、これも実際は整数で true=1, false=0 である。

新しい規格でも整数型をブール値の代わりに使うことができる。その際、0 が false, 非 0 が true とみなされる。

次に stdbool.h の使用例を示す。このソースをビルドするには、-std=c11 オプションを付ける。

さらに新しい規格 C23 では bool, true, false がキーワードとなったため stdbool.h をインクルードする必要がなくなった。(その代わり gcc では -std=c2x オプションが必要)。

#include <stdbool.h>
#include <stdio.h>

void main() {
    bool b = false;
    printf("%d, %d\n", b, !b);
}

実行例
$ bin/stdbool
0, 1

3.5 ループ

C ではループを作るのに、for, while, do .. while がある。残念なことに foreach はない。

3.5.1 無限ループ

for 文のカッコ内は3つの部分に分かれているが、真ん中の部分がループを続けるかどうかを判断する式である。 この式の値が偽 (= 0) ならループを抜けるが、この部分を省略するとループを抜ける条件がないことになり、 無限ループを実現できる。その場合、他の部分も空欄で構わないので、次の例のようにすると無限ループを実現できる。

無限ループからの脱出には、ある条件が成り立ったとき、break 文を実行する。

#include <stdio.h>

void main() {
  int i = 10;
  for (;;) {
    if (--i) {
      printf("%d\n", i);
    }
    else {
      break;
    }
  }
}

while 文で無限ループを実現した方が直感的である。while 文のカッコ内の式が偽になるとループを脱出するので、この部分を true (あるいは非ゼロ) にすると、偽になることがないので無限ループになる。

#include <stdio.h>
#include <stdbool.h>

void main() {
  int i = 10;
  while (true) {
    if (--i) {
      printf("%d\n", i);
    }
    else {
      break;
    }
  }
}

3.5.2 変則型 for 文

for 文のカッコ内は3つの部分からなるが、真ん中の比較式を除いて複数の文を書くことができる。また、各部の内容は省略できるし、for 文の外に書くこともできる。

次の例は、初期化部と更新部に複数の文を置いた例である。

#include <stdio.h>

void main() {
  int i, j;
  for (i = 0, j = -5; i < 6; i++, j++) {
    printf("i=%d, j=%d\n", i, j);
  }
}

次の例は初期化部を省略し、その代わりに for 文の外で変数を初期化している例である。

#include <stdio.h>

void main() {
  int i = 0;
  int j = -5;
  for (; j < 3; i++) {
    printf("i=%d, j=%d\n", i, j);
    j++;
  }
}

下の例は、for 文としての意味はないが、繰り返しを行わない for 文である。

#include <stdio.h>

void main() {
  int i = 0;
  int j = -5;
  for (i = 1, j = 2; 0;);
  printf("i=%d, j=%d\n", i, j);
}

3.5.3 深いループからの脱出

ループからの脱出には普通 break 文を使うが、深いループ内でエラーが発生したときのようにループの一番外へ脱出したい場合には goto 文を使用する。

#include <stdio.h>

void main() {
  int i, j;
  for (i = 0; i < 5; i++) {
    for (j = 0; j < 10; j++) {
      if (i > 2) {
        goto LABEL;
      }
    }
  }
LABEL: printf("i=%d, j=%d\n", i, j);
}

下はこのプログラムの実行例である。

$ ./bin/goto
i=3, j=0

longjmp() 関数をコールして setjmp() 関数で設定した位置へロングジャンプする方法もある。この方法だと、ループ外だけでなくメイン関数などへも戻れる。

次のコードは関数 test() 内の二重ループから直接、メイン関数へ戻る例である。

/* setjmp / longjmp */
#include <stdio.h>
#include <setjmp.h>
#include <stdbool.h>

void test();
jmp_buf jumpenv;
volatile int flag;

/* main */
int main() {
    if (setjmp(jumpenv) == 0) {
        // 普通に setjmp 関数が実行された後
        flag = false;
        printf("setjmp: flag=%d\n", flag);
        test();
    }
    else {
        // longjmp 関数が実行された後
        printf("longjmp: flag=%d\n", flag);
    }
}

/* 二重ループから脱出し、main へもどる。 */
void test() {
    for (int i = 0; i < 100; i++) {
        for (int j = 0; j < 100; j++) {
            if (i == 50 && j == 50) {
                // setjmp() がコールされた状態を復元し、setjmp() の戻り値として 1 を設定する。
                flag = true;
                longjmp(jumpenv, 1);
            }
        }
    }
}

実行例

$ ./bin/longjmp
setjmp: flag=0
longjmp: flag=1
$
 
コメントする

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

 

タグ: , ,

再習C言語:3.2 プリプロセッサ

3.2 プリプロセッサ

ソースプログラムをビルドする過程で最初に実行されるのがプリプロセッサで、先頭に # が付いた行を処理する。これらは「ディレクティブ」と呼ばれる。

よく使われるものを以下に示す。

#include

  • #include は「ヘッダファイル」を読み込むのに使う。ヘッダファイルには様々な定義が含まれる。機能的に Python の import 文に近い。
  • #include でヘッダファイルを指定する場合、#include <stdio.h> のように <, > で囲む場合と、#include “foo.h” のように ” で囲む場合がある。
  • 前者は標準関数などのシステム領域にインストールされているヘッダファイルを指定する場合に使用し、後者は独自に定義したヘッダファイルを指定する場合に使用する。

#define

  • #define は定数シンボルを定義したり、マクロを定義するのに使用する。
  • 定数シンボルの例#define PI 3.1415926
  • マクロの例#define RADIAN(deg) (deg / 180.0 * PI)マクロは関数のように使えるが、関数と違って呼び出しは行われずプログラムに埋め込まれる。#include <stdio.h> #include <math.h> #define PI 3.1415926 #define RADIAN(deg) (deg / 180.0 * PI) void main() { double deg = 0.0; double y; while (deg <= 360.0) { y = sin(RADIAN(deg)); printf("deg=%5.1lf y=%5.3lf\n", deg, y); deg += 10.0; } }

#pragma

  • #pragma はコンパイラ (gcc) に様々な指示を出すのに使用する。次の例は “-Wformat-security” という警告を出さないようにコンパイラに指示する。#pragma GCC diagnostic ignored “-Wformat-security”

#ifdef

  • #ifdef はシンボルが定義されているかを判別してコンパイルを行う指示をするディレクティブである。シンボルは #define で定義することも gcc の -D オプションで定義することもできる。
  • 次の例は1つのソースファイルで2つのパターン main 関数を定義している。#include <stdio.h> #include <stdlib.h> #ifdef RETURN int main() { puts("Return exitcode."); #else void main() { puts("Return no exitcode."); #endif #ifdef RETURN return EXIT_SUCCESS; #endif }
  • このソースをビルドするとき、gcc のオプションに “-D RETURN” があると “#ifdef RETURN” の中のコードがコンパイルされ、ないと “#else” の中のコードがコンパイルされる。
  • 次の例は “-D RETURN” を付けてビルドしたときとそうでないときの実行例である。$ gcc -std=c11 -o bin/main4n main4.c $ gcc -std=c11 -D RETURN -o bin/main4r main4.c $ ./bin/main4r Return exitcode. $ ./bin/main4n Return no exitcode.
 
コメントする

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

 

タグ: