2023 年度「数学特論」 2023-12-27

§10.1 構造体

「計算数学b」では、 「構造体」は、時間の余った人のために提供しただけだった。 学んでいない者は 220112.html で学べ。最後の発展練習はしなくてもよい。

そこでは説明し忘れたが、構造体の成分のことを「メンバー」と呼ぶ。

§10.2 構造体と関数

構造体は、関数に引数として渡せる。 また、関数から返り値として受け取ることもできる。

次は、やたらに大きい構造体を関数の引数として渡し、 また返り値として受け取るプログラムである。 そしてそれを一億回繰り返す。

実行すると、数秒かかる。 それは、引数として渡すときに時間がかかり、 また、返り値として受け取るときに時間がかかるからである。

#include <stdio.h>

struct Misc {
  int a;
  int b[256];           /* やたらに大きい構造体 */
};

struct Misc f(struct Misc x);

int main() {
  int i;
  struct Misc x;

  x.a = 0;
  for (i = 0; i < 10000*10000; i++) {
    x = f(x);
  }
  printf("%d\n", x.a);
}

/* メンバー a を 1 だけ増すだけの関数 */
struct Misc f(struct Misc x) {
  x.a++;

  return x;
}

次は、同じことを、構造体のアドレスを関数に渡すように書いてみたもの。 こちらは一瞬で終わる。 渡すものが 4 バイトだからである。

ここで p->a++; は新しい。 「->」は二文字で一つの演算子である。 p->a は、 その前に置かれたポインタ p の指す構造体のメンバー a を意味する。 そして ++ が続いているからそれを 1 だけ増す、という意味になる。

p->a(*p).a と同じである。 この小かっこは省略できない。 よって、p->a という書き方がよく用いられる。

#include <stdio.h>

struct Misc {
  int a;
  int b[256];           /* やたらに大きい構造体 */
};

void f(struct Misc *p);

int main() {
  int i;
  struct Misc x;

  x.a = 0;
  for (i = 0; i < 10000*10000; i++) {
    f(&x);
  }
  printf("%d\n", x.a);
}

/* メンバー a を 1 だけ増すだけの関数 */
void f(struct Misc *p) {
  p->a++;
}

§10.3 構造体のサイズはメンバーのサイズの和とは限らない

次のプログラムで、構造体 Aaa のメンバーは charint が一つずつなので、サイズは 1 + 4 = 5 バイトかと思うかもしれないが、 ここの gcc では 8 バイトである。 それは、int 型は「mod 4 で 0 である番地からの 4 バイト」 として置くことで実行時の効率が上がるから、であろうと思われる。

#include <stdio.h>

struct Aaa {
  char c;
  int n;
};

int main() {
  struct Aaa a;

  printf("%p.\n", &a);
  printf("%d バイト.\n", sizeof(struct Aaa));
}

§10.4 自己参照的構造体

構造体 struct Aaa 型が struct Aaa 型のメンバーを持つのは、 集合論で「x ∈ x」が成り立つ集合 x を考えるようなもので、不合理である。

#include <stdio.h>

struct Aaa {        /* 間違い */
  int n;
  struct Aaa a;
};

しかし、 構造体 struct Aaa 型が struct Aaa 型へのポインタをメンバーとして持つのは合法である。

#include <stdio.h>

struct Aaa {        /* 正しい */
  int n;
  struct Aaa *a;
};

§10.5 双方向循環リストの例

次のような構造体 struct Lnode を定義する。 (Lnode は line と node に由来すると思う。 prev は英単語 previous に由来する。)

struct Lnode {
  char *text;           /* テキスト */
  struct Lnode *prev;   /* 前の行の struct Lnode のアドレス */
  struct Lnode *next;   /* 次の行の struct Lnode のアドレス */
};

入力行を一行ごとに分け、その数だけこの構造体を用意し、 行はメンバー text に置く。 そして、 prev は前の行の Lnode を、 next は次の行の Lnode を、指す。

すると一列につながった Lnode の列ができる。 最初の行の prev と最後の行の next は特別な Lnode を指すことにし、その Lnode をもってこの一連の文字列の代表とする。 そこから順に next をたどれば、文字列全体にアクセスできる。

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

struct Lnode {
  char *text;           /* テキスト */
  struct Lnode *prev;   /* 前の行の struct Lnode のアドレス */
  struct Lnode *next;   /* 次の行の struct Lnode のアドレス */
};

#define MAXLINE 1024    /* 一行の長さの限度 */

int main() {
  int len, n;
  char line[MAXLINE];       /* とりあえず一行を入れるところ */
  struct Lnode textlines;   /* 最初にして最後となる、特別な Lnode */
  struct Lnode *pivot, *lp, *lpnew;

  pivot = &textlines;
  lpnew = pivot;                                /* lpnew をこうおくのは入力が空の場合のため */
  
  for (lp = pivot; gets(line) != NULL; lp = lpnew) {        /* gets() は【お勧めしません!】*/
    len = strlen(line);
    lpnew = malloc(sizeof(struct Lnode));       /* 厳密にはメモリ不足のときの処理が必要 */
    lpnew->text = malloc(len + 1);              /* 厳密にはメモリ不足のときの処理が必要 */
    strcpy(lpnew->text, line);
    lp->next = lpnew;
    lpnew->prev = lp;
  }
  lpnew->next = pivot;          /* 最後と最初とをつなぐ */
  pivot->prev = lpnew;

  n = 1;
  for (lp = pivot->next; lp != pivot; lp = lp->next) {       /* 行番号をつけて出力してみる */
    printf("%6d: %s\n", n, lp->text);
    n++;
  }
}

§10.6 課題3

前の節のプログラムを改変し、 最後、ユーザーが自然数 n を入力すると第 n 行だけを出力するようにせよ。

レポートの本文冒頭に、氏名を、なるべく大学に届けるある通りの文字で書け。 次に、プログラムソースファイルを、添付ファイルではなく、本文に貼りつけよ。 それから、このプログラムを使った実験結果を貼れ。

件名は「??? kadai3」(←全て半角文字、小文字、 ??? は自分の履修者番号、その後ろに半角スペース一つ、 kadai3 の間にはスペースを入れない)とせよ。


岩瀬順一