メモリの動的確保
前回、前々回に作ったプログラムは、読み込めるデータの上限をはじめから決めていました(接触原子の計算では5000原子まで、hydropathy plotでは2000アミノ酸残基まで)。扱っているデータがある程度決まっている場合はこのままでもいいのですが、PDBに登録されているデータ全部やゲノムにコードされているタンパク質全部を扱うような場合、この上限を超えてしまう可能性があります。
対策の1つとして始めから上限を大きくしておくという方法があります(接触原子の計算プログラムを例とすると、#define行のMAXATOMで指定した5000という数字を10000にすれば、10000原子まで読み込めるようになります)。ただこの方法だと、さらに大きなデータが出てきた場合にまたプログラムを変更する必要がでてくるし、プログラムを動かすたびに無駄に大きなメモリが使われてしまうので、あまり効率的ではありません。
そこで、今回は読み込めるデータの大きさを動的に(読み込むデータにあわせて)変える方法を紹介します。今回紹介する方法を使えば、建前上はコンピュータが持っているメモリを上限としてデータを読み込むことができます。
メモリを動的に確保するには、すでに名前は出てきているmalloc()関数を使います。以下にmalloc()関数を使った簡単なプログラム例を示します。プログラムの機能としては、1から10を順番に表示するというものです。
#include <stdio.h> #include <stdlib.h> int main() { int *a; int i; a = (int *)malloc(10 * sizeof(int)); for (i = 0; i < 10; i++) { a[i] = i+1; } for (i = 0; i < 10; i++) { printf("%d\n", a[i]); } }
ソースコードのファイル名をex_malloc.cとでもして、コンパイルしましょう。
cc -o ex_malloc ex_malloc.c
で大丈夫です。
まず、malloc()関数を使うために、2行目にあるようにstdlib.hをincludeしておきましょう。
6行目でint型のポインタ変数aを宣言しています。ポインタ変数なので、あくまでもメモリの番地が入る変数であり、この宣言だけでは実際にデータを入れることができる場所(メモリ)は確保されていません(この辺は第3回で出てきた話と微妙に違うので注意)。
9行目が問題のmalloc()関数を使っているところです。このmalloc()関数を使うことで、データを入れる場所が確保されたメモリの番地がポインタ変数aに代入されます。ポインタ変数aがint型のポインタなので、malloc()関数の前に、(int *)をつける必要があります。また、malloc()関数に渡す引数は、確保する領域の大きさになります。この場合、10 * sizeof(int)となっているので、”int型のデータ”が”10個分”入る大きさという意味になります。もしポインタ変数aがchar型であり、1000文字分の領域を確保するのであれば、
a = (char *)malloc(1000 * sizeof(char))
となります。
10行目以降は単に、確保した領域に1から10の数字を代入して、それを出力しているだけです。
今のプログラム例では、確保するメモリの量をあらかじめ決めていたので、ある意味これまでに作ってきたプログラムと変わっていません。実際にメモリを動的に確保する場合は、プログラムの中で必要なメモリの大きさを求めるというプロセスが入ります。
実践編
PDBのデータを使って、メモリを動的に確保するプログラムを実際に作ってみましょう。プログラムの機能は、タンパク質主鎖の配列上隣り合うα炭素間の距離を求めて表示するというものです。
プログラムの中で必要な手順は、
- PDBのファイルを開く。
- PDBのファイル中にあるα炭素の数を数える。
- α炭素の原子座標を格納するメモリを確保する。
- α炭素の原子座標を読み込む。
- α炭素間の距離を順番に計算し、出力する。
といったところでしょうか。これらの手順をできるだけ関数化してプログラム化すると以下のように書けます。
#include <stdio.h> #include <string.h> #include <stdlib.h> #include <math.h> #define MAXCHR 128 int GetAtomNum(FILE *fp) { int atomNum; char line[MAXCHR]; while (fgets(line, MAXCHR, fp) != NULL) { if (strncmp(line, "ATOM ", 6) == 0 && strncmp(&line[12], " CA ", 4) == 0) { atomNum++; } } return atomNum; } float **Make_e(int atomNum) { int i; float **e; e = (float **)malloc((atomNum+1) * sizeof(float *)); if (e == NULL) { fprintf(stderr, "Make_e(): Cannot allocate memory(1)\n"); exit(1); } for (i = 0; i < atomNum+1; i++) { e[i] = (float *)malloc(3 * sizeof(float)); if (e[i] == NULL) { fprintf(stderr, "Make_e(): Cannot allocate memory(2)\n"); exit(1); } } return e; } void Read_e(FILE *fp, int atomNum, float **e) { int i = 0; char line[MAXCHR]; while (fgets(line, MAXCHR, fp) != NULL) { if (strncmp(line, "ATOM ", 6) == 0 && strncmp(&line[12], " CA ", 4) == 0) { e[i][0] = atof(&line[30]); e[i][1] = atof(&line[38]); e[i][2] = atof(&line[46]); i++; } } } float Distance(float e1[3], float e2[3]) { float dis; dis = pow(e1[0] - e2[0], 2.0) + pow(e1[1] - e2[1], 2.0) + pow(e1[2] - e2[2], 2.0); return sqrt(dis); } void PrintDis(int atomNum, float **e) { int i; for (i = 0; i < atomNum-1; i++) { printf("%4d %5.2f\n", i+1, Distance(e[i], e[i+1])); } } int main(int argc, char *argv[]) { int atomNum; float **e; FILE *fp; if (argc != 2) { fprintf(stderr, "Usage: %s PDB_FILE\n", argv[0]); exit(1); } if ((fp = fopen(argv[1], "r")) == NULL) { fprintf(stderr, "Cannot open %s\n", argv[1]); exit(1); } atomNum = GetAtomNum(fp); e = Make_e(atomNum); rewind(fp); Read_e(fp, atomNum, e); PrintDis(atomNum, e); return 0; }
自作の関数が5つあり、少し長ったらしくなってしまいましたが、がんばって入力してください。入力が終わったら、disCa.cとでも名前をつけて保存し、コンパイルしましょう。コンパイルの仕方は、
cc -o disCa disCa.c -lm
です。
最初にGetAtomNum()関数を作っています。この関数は、ファイルポインタを渡すと、ファイルの中から行頭が”ATOM “であり、かつ、行の12列目から” CA “がある行の数を数えてそれを返してくれる関数です。これにより、malloc()関数で確保すべきメモリの大きさを知ることができます。
次に作っているのがMake_e()関数です。これが今回の肝になる関数です。役割は、α炭素の数を受け取って、その分のα炭素のxyz座標を記録するためのメモリを確保する、というものです。先ほどのプログラム例に比べると、同じmalloc()関数を使っていても複雑に見えます。複雑になっている原因の一つが、2次元のメモリ(1次元目が各α炭素、2次元目がx座標、y座標、z座標)を確保しているため、もう一つがmalloc()関数によりメモリを確保できなかった場合のエラー処理をしているためです。28行目から32行目が1次元目のメモリ(α炭素の数の分)の確保に該当し、33行目から39行目が2次元目のメモリ(それぞれのα炭素についてのxyz座標の分)の確保に該当します。関数の最後で、メモリを確保した二次元ポインタ変数eをreturnで返しています。これにより、この関数の返り値をmain()関数で受けると、必要な分のメモリが確保された変数がつくれます。なお、このMake_e()関数は二次元ポインタを返すので、関数の先頭(23行目)において、*が2つついています。この関数でやっていることは、メモリを動的に確保する場合によく使われるパターンだと思います。最初は慣れないと思いますが、こういうものだと思って形式だけでも覚えてください。
Read_e()関数、Distance()関数については新しい要素はありません。前回の説明のところを参考にしてください。
最後に、PrintDis()関数ですが、配列上隣り合ったα炭素間の距離を計算するので、i番目のα炭素とi+1番目のα炭素の座標をDistance()関数に渡すようにしています。
練習問題
- 第9回のhydropathy plotを求めるプログラムについて、アミノ酸配列のデータを格納するchar型変数のメモリを、malloc()関数を使って確保するようプログラムを変更しなさい。
- 第10回の接触原子の計算を行うプログラムについて、原子座標のデータを格納する構造体のメモリを、malloc()関数を使って確保するようプログラムを変更しなさい。
2つの練習問題共に、配列で宣言しているところをポインタに変更する必要があります。特に関数の引数のところを変更することを忘れないようにしてください。