C言語の理解度を高めるための課題の案

配列シャッフルをC言語(ISO C89)で書くと以下のようになります:

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

static void shuffle(const char ** const array);
static void swap(const char ** const array, int i, int j);

static const char * names[] = {
  "taro",
  "jiro",
  "saburo",
  "shiro",
  "goro",
  0};

extern int main(void) {
  const char * const * cursor;
  shuffle(names);
  for (cursor = names; *cursor; ++cursor) puts(*cursor);
  return 0;
}

static void shuffle(const char ** const array) {
  const char ** cursor = 0;
  int i = 0;
  for (cursor = array; *cursor; ++cursor) {
    int dst = (int) floor((rand() / (RAND_MAX + 1.0)) * (i + 1));
    swap(array, i , dst);
    i += 1;
  }
}

static void swap(const char ** const array, int i, int j) {
  const char * const tmp = array[i];
  array[i] = array[j];
  array[j] = tmp;
}

これはvalidなコードで、無警告でコンパイル可能です。

考察ポイントは

  • C言語の型の構文
    • なるべくconstを厳密に使用するには
    • namesの型を「const char ** names = ...」にするとコンパイルが通らないこと
  • static, externの意味
  • ポインタ配列のラストを0(NULL)にするイディオム、その使い方
  • 標準ライブラリを調べること
    • rand()の使い方

です。リファレンスマニュアルで調べましょう。

選択ソートもC(ISO C89)で書いておきます

extern int puts(const char *);
extern int strcmp(const char *, const char *);

static void selection_sort(const char ** const array);
static void swap(const char ** const array, int i, int j);
static int array_count(const char * const * array);

static const char * names[] = {
  "taro",
  "jiro",
  "saburo",
  "shiro",
  "goro",
  0};

extern int main(void) {
  const char * const * cursor;
  selection_sort(names);
  for (cursor = names; *cursor; ++cursor) puts(*cursor);
  return 0;
}

static void selection_sort(const char ** const array) {
  const int count = array_count(array);
  int i = 0;
  for (; i < count; i++) {
    int min = i;
    int j = i + 1;
    for (; j < count; j++) {
      if (strcmp(array[j], array[min]) < 0) min = j;
    }
    swap(array, i, min);
  }
}

static int array_count(const char * const * array) {
  int count = 0;
  for (; *array; ++array) count++;
  return count;
}

考察ポイントはプリプロセッサincludeを使用していないことです。
なぜ#includeするのか、を調べてください。

プリプロセッサマクロで書き直す

上は、関数でシャッフルや選択ソートを書いたのですが、それをプリプロセッサマクロで書き直したものです。ポインタ配列の型に依存しないようにしています。

シャッフル

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

#define shuffle(ElemType, array) do { \
  ElemType * cursor = 0; \
  int i = 0; \
  for (cursor = array; *cursor; ++cursor) { \
    int dst = (int) floor((rand() / (RAND_MAX + 1.0)) * (i + 1)); \
    swap(ElemType, array, i , dst); \
    i += 1; \
  } \
} while (0)

#define swap(ElemType, array, i, j) do { \
  ElemType tmp = array[i]; \
  array[i] = array[j]; \
  array[j] = tmp; \
} while (0)

static const char * names[] = {
  "taro",
  "jiro",
  "saburo",
  "shiro",
  "goro",
  0};

extern int main(void) {
  const char * const * cursor;
  shuffle(const char *, names);
  for (cursor = names; *cursor; ++cursor) puts(*cursor);
  return 0;
}

考察ポイントは、マクロをdo while(0)でくくることです。
なぜただのブロックにしていないかは、以下のコンパイルが通らないコードを考えてください

/* parse error code */

#define twoup(c) { \
  c++; \
  c++; \
}

int main() {
  int count = 0, flag = 1;
  
  if (flag) twoup(count);
  else count += 10;
  
  return 0;
}

(これはともかく、コーディングスタイルとしては、else節のつくif文では単文でもブロックの{}をつけるべきでしょう)。

同様に選択ソートも書き換えました:

extern int puts(const char *);
extern int strcmp(const char *, const char *);

#define selection_sort(ElemType, array, compare_to) do { \
  int count; \
  int i = 0; \
  array_count(ElemType, array, count); \
  for (; i < count; i++) { \
    int min = i; \
    int j = i + 1; \
    for (; j < count; j++) { \
      if (compare_to(array[j], array[min]) < 0) min = j; \
    } \
    swap(ElemType, array, i, min); \
  } \
} while (0)

#define swap(ElemType, array, i, j) do { \
  ElemType tmp = array[i]; \
  array[i] = array[j]; \
  array[j] = tmp; \
} while (0)

#define array_count(ElemType, array, count) do { \
  ElemType * cursor; \
  count = 0; \
  for (cursor = array; *cursor; ++cursor) count++; \
} while (0)

static const char * names[] = {
  "taro",
  "jiro",
  "saburo",
  "shiro",
  "goro",
  0};

extern int main(void) {
  const char * const * cursor;
  selection_sort(const char *, names, strcmp);
  for (cursor = names; *cursor; ++cursor) puts(*cursor);
  return 0;
}

マクロを使わず関数をvoid**化することでも一般化可能です。記述してみましょう。

ポインタ配列を一般化する場合、void**になります。
ただし、void**はvoid*ではないので、const char **等を直接代入するにはキャストが必要です(gccではキャストしないと警告が出る)。