プログラミング 08 - Soft Forum P.C. Club

ちょっと時間がかかってきたので, ここからペースを上げていくかもしれません.

また, 情報の授業では配列とポインタを分割するようですが, 個人的にはこの2つは不可分なものだと思っているので同時に解説します.

この資料は関数です.

関数

関数とは, 引数に値を受け取り, 何らかの値つまり返り値を返すもののことです.

int add(int a, int b) {
  return a + b;
}

例えば, この関数addは引数にint型aとint型bを受け取り, int型の値を返しています(return)

これをadd(10, 20)という風にすることで呼び出すことができます. 数学の記法と(f(10))とかと同じです.

関数の利点

関数を使うと, さまざまな利点があります

例えば, 私たちが使っているprintfは関数です. しかし, printfの具体的な実装をどれだけの人が知っているでしょうか?

printfの引数と戻り値, 副作用を知っていれば中身の実装は私たちは気にしなくてよくなっているのです. また, printfは環境によって実装が異なる場合があったり, 知らない間により速い実装に置き換えられているかもしれませんが, 私たちはそれを気にする必要がありません.

実際に

加算関数addを定義してみます.

#include <stdio.h>
int add(int, int);

int main(int argc, const char *argv[]) {
  printf("10 + 10 = %d\n", add(10, 10));
  return 0;
}

int add(int a, int b) {
  return a + b;
}

まず

int add(int, int);

で関数プロトタイプの宣言をしています. これは, 「intを二つとってintを返す関数addっていうものが存在するよ!!」というのをコンパイラに伝えています. この情報がなければ, main関数中でaddを使ったときに「addってなんだよ?」となります. もちろん,

#include <stdio.h>
int add(int a, int b) {
  return a + b;
}

int main(int argc, const char *argv[]) {
  printf("10 + 10 = %d\n", add(10, 10));
  return 0;
}

という風にmainより先に具体的な中身を書いてしまっても動くのですが, これではmain関数がどんどん後ろに行ってわかりにくくなります. そこで, addという関数があるということを関数プロトタイプで伝えて, 具体的な中身は後で書くという手段を取ることができます.

ここで, なぜ引数型と返り値の型, あと名前が必要なのでしょうか?

上で示したとおり, 呼び出す側は関数の中身を知る必要はありません. 呼び出す側が必要なのは,

だけです. なので, 関数プロトタイプはその情報だけ先に伝えておいてくれるわけです.

関数についてのetc

int add(int, int);

のようなものを関数プロトタイプといい,

int add(int a, int b) {
  return a + b;
}

のような実際の実装を関数定義といいます. またこのようなint a, int bといったものを仮引数(parameter)といいます.

対して,

add(10, 20);

のように実際に渡している10, 20といった値を実引数(argument)といいます.

値渡し

これは非常に大事なことなのですが,

C言語では関数の引数は常に値渡しです

つまり,

int add(int a, int b) {
  return a + b;
}
add(10, 20);

となっていると, まず10, 20という値はa, bという名前の付いたメモリ領域にコピーされます. これは後にpointerのところで大変重要な意味を持つので覚えておいてください.

変数寿命とローカル変数(局所変数)

いままで変数を使ってきましたが, これは当然メモリの中に作られているわけです. でも, もちろんメモリは有限であるので, 使ったまま放置ではメモリがあふれてしまいます. ではどうなっているのでしょうか?

実は変数には寿命があり, その寿命が切れると自動的に回収されるようになっています.

#include <stdio.h>
int fact(int);

int main(int argc, const char *argv[]) {
  printf("5! = %d\n", fact(5));
  return 0;
}

int fact(int n) {
  int i, res = 1;
  for (i = 1; i <= n; ++i) {
    res *= i;
  }
  return res;
}

factorialを計算する関数factを定義してみました. このとき, int i, resを変数として使っていますが, この変数の寿命はどれだけなのでしょうか.

i, resは関数が終了するともう必要ないですね. そのため, これらのメモリ領域は関数fact終了時に回収されます. つまり寿命はfact実行からfact終了までというわけです.

一般にこういった範囲のことをスコープといい, このように関数内で作られ, 関数終了時に回収される変数のことを局所変数, もしくはローカル変数といいます. 関数の中だけ, つまり局所的に有効になる変数というわけです.

では局所じゃない変数も見てみましょう.

#include <stdio.h>
int my_age = 19;

int main(int argc, const char *argv[]) {
  printf("my age = %d\n", my_age);
  return 0;
}

変数が関数の中にありません. この場合の寿命はどれだけになるのでしょうか?

この場合はプログラム実行開始から終了までが寿命になります. そして, 関数の中からmy_ageを見ることができていますね. このような変数を大域変数, もしくはグローバル変数といいます. 大域変数は他の関数の中からも見れています.

静的変数

そしてもう一つ, 静的変数の話をします.

もしあなたが関数の呼ばれた数を数えたいと思うとしましょう. その場合どうすればよいのでしょうか?

#include <stdio.h>
void counter(void);

int main(int argc, const char *argv[]) {
  counter();
  counter();
  counter();
  counter();
  return 0;
}

void counter() {
  int i = 0;
  printf("%d times\n", ++i);
}

実行すると,

1 times
1 times
1 times
1 times

となります. 増えていませんね. 当然です. なぜならローカル変数のiは関数に入るたびに0になって, ++iされて1となり, そして回収されて消えてしまうのですから.

じゃあどうするのか, グローバル変数を使うとある意味解決します(これはグローバル変数が寿命範囲が大きいというものを示すためのもので, 決してよいcodeとはいえないので注意)

#include <stdio.h>
void counter(void);
int count = 0;

int main(int argc, const char *argv[]) {
  counter();
  counter();
  counter();
  counter();
  return 0;
}

void counter() {
  printf("%d times\n", ++count);
}

今度は増えていますね. よかったよかった.

しかしこれには強烈な脆弱性があります. つまり, 誰かがこっそりこんなことをするかもしれないのです.

#include <stdio.h>
void counter(void);
int count = 0;

int main(int argc, const char *argv[]) {
  counter();
  counter();
  counter();
  count = -200;
  // あれれ?
  counter();
  return 0;
}

void counter() {
  printf("%d times\n", ++count);
}

countがグローバル変数であるために, 外に漏れてしまうので誰かがうっかりいじるかもしれないのです.

実はこのような場合に静的変数が使えます.

#include <stdio.h>
void counter(void);

int main(int argc, const char *argv[]) {
  counter();
  counter();
  counter();
  counter();
  return 0;
}

void counter() {
  static int count = 0;
  printf("%d times\n", ++count);
}

counterの局所変数の定義が

static int count = 0;

とstaticが増えていますね. これはstatic(静的)であるという宣言です.

静的変数は, 初めてその変数の宣言がなされたときにメモリを確保し, 以後プログラム終了まで持ち続けます. つまり, counter一回目でcount = 0;が入り, 関数が終了してもcount変数のメモリは回収されません. そして次にcounterが呼ばれたときにはcount = 1;のメモリがそのまま残っているのです.

このようにメモリが回収されないで残る変数を静的変数といい, 自動で回収される変数を自動変数といいます.

マクロ

ここらでmacroをやらねばならぬということでやります.

例えば一般に円周率を使って円周を求める関数を作りたいとしましょう.

#include <stdio.h>
double circle(double);

int main(int argc, const char *argv[]) {
  printf("%f\n", circle(2));
  return 0;
}

double circle(double r) {
  return 2 * r * 3.14159;
}

2πrによってこんな簡単な関数を作って求めることができました. さて, この数値3.14159, わかりやすい数値なので案外わかるかもしれませんがこれをまったく知らない人が見たとき「なんじゃ唐突に個の数字は!!」となるかもしれません. また, このように直接書いていると, もし3.14159265358という精度にしようと思ったときに, このように書いている部分をすべて書き直さなければいけません!!

C言語ではdefineマクロを使ってこのようなことができます.

#include <stdio.h>

#define PI 3.14159

double circle(double);

int main(int argc, const char *argv[]) {
  printf("%f\n", circle(2));
  return 0;
}

double circle(double r) {
  return 2 * r * PI;
}
#define PI 3.14159

でPIに3.14159を当てています. こうするとプリプロセッサが事前にPIをこれに置き換えておいてくれるので, コンパイラには,

double circle(double r) {
  return 2 * r * 3.14159;
}

となっているように見えます. これでまったく知らない人にも「PIかー」とわかりやすいし, 変更するときもdefine macro部分を変更するだけですみます!!

知らない人が見たら何を意味しているのかわからない数字のことをmagic numberといい, それはプログラムを難解にしbug発生率をうなぎ上りにします. よってdefine macroで名前をつけてあげてわかりやすくするという方法がC言語では取られます.

マクロは私たちがコンパイルの時点ですでにわかっている, 意味のあるデータ(数値や文字列など)に使うと非常に効果的です.

とりあえず

ではせっかくなので, 円の面積を求める関数を作ってみてください. PIはdefine macroで3.14159と定義してもらえれば.

なんか上のをちょっと変えればできちゃうかもなので, ちょっと簡単ですね...


解答


せっかくなので!!

ちょっと難しそうなのをやりましょう. stepを踏めば簡単です.


まず 1,

ファイルを読み込んで, アルファベットが出た数を数えるプログラムを書いてください.

ファイルは,

./a.out < test.txt

とするので, 標準入力から読み込むようにすれば大丈夫です.

つかうtest.txtは,

there's more than one way to do it

という文章にします.(Perlでやたらと有名)

26というカウントができれば正解です.


解答


さて, 2

さあ, 上ができればもう一息です. シーザー暗号を作りましょう.

シーザー暗号とは, アルファベットをずらすことによって作られる暗号です.



シーザー暗号の古典にしたがって, 3文字ずらすことにします. また, zなどは前に持ってくることにします. 大文字小文字はすべて小文字に変換してから考えることにします. つまり,

a  d
b  e
c  f
.
.
.
x  a
y  b
z  c

です. アルファベット以外は除外します. 先ほどの文章が,

wkhuh'v pruh wkdq rqh zdb wr gr lw

となれば正解です. caesar変換部分だけ関数にしてみてもいいと思います.

hints

  1. まずアルファベットかどうかを調べる
    1. これはさっきのやったやつを使う
  2. 大文字を小文字にする
    1. 'a' - 'A'が大文字と小文字の差. これを大文字に足してあげれば, 'A' + ('a' - 'A') => 'a'になる
  3. 3大きくすると'z'を超えるかも
    1. 超えたらアルファベット分(26文字)巻き戻せばいい


解答

Soft Forum P.C. Club