] > Basic Data Structure

第 3 回 単純なデータ構造

本日の内容


このドキュメントは http://edu.net.c.dendai.ac.jp/ 上で公開されています。

3-1. ビルダの利用

Borland C++ Builder、Visual C++、Code Warrior、gcc developer station 2000 など、統合環境といわれるソフトウェアは(裏で make と同じ処理をして)、 プログラムのソースファイルを管理します。 これらのソフトでは、プログラムの管理の単位を「プロジェクト」と呼んでい ます。 プログラムを作る際には「プロジェクト」にソースコードを登録します。 この時、 #include で呼ばれるヘッダファイルは自動的に解釈されます。 そして、プログラムが終了したら、ビルドを指示すると、コンパイル、リンク を自動的に行います。

なお、これらの統合環境では Windows 用のアプリケーションの作成支援も行っ ているので、文字だけの画面を前提としたプログラム(コンソールアプリ)と Windows の GUI 環境用のプログラムでプロジェクトの設定が異なります。 この区別は、プロジェクトを新規作成する時に選択します。 選択を間違えるとプログラムが正しく実行できなくなりますので注意して下さ い。

前回の、 factor.c combination.c main.c testf.c testc.c testm.c に対し ては、次の 4 つのプロジェクトを作ると、テストなども全て管理できます。

  1. factor.c combination.c main.c
  2. factor.c testf.c
  3. factor.c combination.c testc.c
  4. testm.c main.c

なお、 Borland C++ Builder でコンソールアプリケーションを実行させると、 プログラムが終了した瞬間に画面が消えてしまいます。 その結果、そのままではプログラムの実行結果を見ることができません。 これを防ぐためプログラムの最後に getchar(); など、意味のない入力待ちを させる必要があります。

3-2. ローカル変数、グローバル変数

C 言語では関数の内部で宣言された変数は、その関数の外部(他の関数など)か らはアクセスできません。 これをローカル変数と言います。 一方、関数の外側でも変数を宣言できます。 これをグローバル変数と言います。 グローバル変数へはどんな関数でもアクセスできます。 但し、別のファイルの中にあるグローバル変数にアクセスするには関数のプロ トタイプのように宣言をする必要があります。 ある変数名がグローバル変数を指すことを宣言するのがextern 宣言 です。構文は次の通りです。


extern 変数宣言

これは、グローバル変数であることを宣言しているだけで、変数そのものの宣 言になってません。そのため、一つのソースファイルでグローバル変数の宣言 をする必要があります。 一般に、このような extern 宣言はヘッダファイルに格納します。

なお、グローバル変数と同じ名前の変数をローカル変数として宣言可能です。 おなじ名前になった時は、グローバル変数にはアクセスできず、ローカル変数 にだけアクセスできます。

但し、グローバル変数の利用には注意が必要です。 グローバル変数は全ての関数からアクセスが可能なので、プログラムミ スなどによりグローバル変数の値がおかしくなった時など、原因の追求が大変 です。プログラム分割をシンプルにしたい立場からも、グローバル変数という 全関数が知らなければならないような情報はない方がプログラム開発が楽です。 C++ 言語、Java 言語などの、オブジェクト指向言語では、特定の変数とそれ にアクセスする関数をまとめてクラスとして扱えるので、グローバル変数を使 わないようにすべきです。 変数など、不必要な情報へのアクセスを制限することをオブジェクト指向では カプセル化と言います。

また、関数の宣言の時に宣言する引数は、値を読み出すことはできますが、そ れは値が与えられたローカル変数なので、書き換えた値は呼び出した側には影 響しません。

演習3-1

次のプログラムがどのように動くか予想し、また、実際に動かして動作を確か めなさい。


#include <stdio.h>
int i;
int a(int i){
   i++;
   return i;
}
int b(int j){
   i++;
   return i;
}
int c(int k){
  int i;
  i=0;
  return i;
}
main(){
  i=1;
  printf("%d\n",i);
  printf("%d ",a(i));
  printf("%d\n",i);
  printf("%d ",b(i));
  printf("%d\n",i);
  printf("%d ",c(i));
  printf("%d\n",i);
}  

3-3. キャスト

ある型を別の型として扱いたい時に、型を指定する方法があります。 例えば、整数(int)の値 1 と 2 を考えた場合、個数や合計は整数(int)になり ますが、平均値は実数(float)になります。 C 言語では(整数)/(整数)の結果は整数になってしまうため、割算をする時、 float だと指定する必要があります。そこで、「(型) 式」と書くと小数点以 下を切り捨てられず、結果が float 型になります。


#include <stdio.h>
main(){
  int a[]={1,2,-1}
  int i;
  int goukei, kosuu;
  float heikin;

  kosuu=0;
  goukei=0;
  for(i=0;a[i]!=-1;i++){
    kosuu++;
    goukei+=a[i];
  }
  heikin=(float)goukei / kosuu;
  printf("個数=%d, 合計=%d, 平均=%f\n",
          kosuu, goukei, heikin);
}

3-4. ポインタ

C 言語では、変数の値の格納しているメモリの番地を取り扱うことができます。 変数 x に対して、その変数の値のメモリの番地は &x で表します。 また、この番地を変数に代入して処理することもできます。 int 型の変数の番地を扱う変数の型は int * で表します。 int *x,y; と宣言すると x は int の変数の番地を取り扱うこと ができます。 x = &y=1で y の番地を代入できます。 また、 *x とするとこれは x に格納されているメモリの番地に入っている値 を表します。*x はこの場合 1 になります。 このメモリの番地を扱う変数を ポインタ と言います。 このメモリの番地やポインタには様々な利用法があります。

関数とのデータのやりとり

C 言語では関数の中で宣言された変数はローカル変数なので、 何の工夫もなく別の関数からその変数の値を変更することはできません。 一つの変数の値だけなら、 a=f(a) のように関数の戻り値を代 入することで f から a の値を変更できますが、二つ以上の変数に対しては行 えません。 例えば、変数 a と変数 b の値を交換することを考えます。 関数さえ呼び出さなければ次のようにすればできます。


int a=4,b=1;
int c;

c=a;
a=b;
b=c;

この処理はしばしば出てくるので、これを関数としたいところです。 ところが次のようにしても思うように動きません。


#include <stdio.h>
swap(int x, int y){
  int z;
  z=x;
  x=y;
  y=z;
  return;
}
main(){
  int a,b;
  a=2; b=3;
  swap(a,b);
  printf("a=%d, b=%d\n",a,b);
}

関数の引数の宣言での x, y はローカル変数です。 従って、呼び出す側の変数と値は同じですが、既に別の変数ですので、代入し ても呼出側の変数の値は変わりません。

そこで、ポインタを使います。 呼び出し側の変数の番地をサブルーチンに渡し、関数では、その番地に値を書 き込むのです。 次のようにすると思った通りの動きになります。


#include <stdio.h>
swap(int *x, int *y){
  int z;
  z=*x;
  *x=*y;
  *y=z;
  return;
}
main(){
  int a,b;
  a=2; b=3;
  swap(&a,&b);
  printf("a=%d, b=%d\n",a,b);
}

配列の要素を指す

C 言語での配列名は実はポインタです。 配列 a の 0 番目の要素が格納されているメモリ上の番地が a になっていま す。つまり、 &a[0] と a は等しいです。ですから、a[0] と *a は等し いです。


#include <stdio.h>
main(){
  double a[]={1.0,2.0,3.0,-1.0};
  printf("&a[0]=%d, a=%d\n",(int)&a[0],(int)a);
  printf("a[0]=%f, *a=%f\n",a[0],*a);
}

では a[1] と a の関係はどうでしょうか? double のように一文字では表せないようなデータは、複数の番地に渡って格 納されているため、 a の隣の番地はまだ a[0] の内容の続きが入っています。 sizeof 演算子を使うと、その型が何バイトの記憶領域を使うかが 分かります。 筆者の環境では sizeof (double) の値は 8 になっています。 つまり、一つの double 型の変数を格納するのに 8 バイトの記憶領域が必要 になってきます。 ですから、 a[1] の番地は a[0] の番地から 8 バイト先の位置に記憶されて います。 結局、 C 言語の配列というのは次のようにして計算された値になります。

a[i] = *( a+i *sizeof(a[0] ))

ところが、ポインタの計算では、何の型のポインタかが明らかなため、 +1 を すると、次の要素を指すように値が増えます。つまり、その型のサイズ分だけ 増えます。 結局 a[1]=*(a+1) となります。


#include <stdio.h>
main(){
  double a[]={1.0,2.0,3.0,-1.0};
  printf("&a[1]=%d, a=%d, sizeof double = %d\n",
	 (int)&a[1],(int)a,sizeof (double));
  printf("a + 1 * sizeof a[0] = %d, a + 1 =%d \n",
	 (int)a + 1*sizeof a[0], (int)(a+1)); 
  printf("a[1]=%f, *(a+1)=%f\n",a[1],*(a+1));
}

これを使うと、配列の値を次々指し示す場合に ++ などのインクリメントが使えま す。 またループ変数を無くすこともできます。


#include <stdio.h>
main(){
  double a[]={1.0,2.0,3.0,-1.0};
  double *x;
  for(x=a; *x != -1; x++){
    printf("%f\n",*x);
  }
}

3-5. 構造体

座標のように、複数の値を対に用いたい時に使うのが構造体です。 座標の構造体の型は次のように宣言されます(最後のセミコロン(;)は必須です)。


struct point {
  double x;
  double y;
};

そして、この後、座標の変数を宣言するには次のようにします。


struct point p;
struct point *q;
struct point r={1.0,2.0};

プログラムの中では、まず、構造体から構造体へは代入が可能です。 p=rq=&p*q=r な どできます。 また、構造体の内部の値はピリオド(.)の後にフィールド名を書きます。 p.x=3(*q).y=4 などが可能です。 また、特に、(*q).yq->yと書くこ とができます。

3-6. 文字列

C 言語では文字列の関数が提供されています。 利用するには string.h ヘッダファイルを読み込む必要があります。 その時、次を行うプログラムが以下に示すように簡単に書けます。 なお、文字列の最後には必ず '\0' を入れておく必要があります。

  1. 文字列の長さを求める
  2. 文字列中で最初に特定の文字が出現する位置
  3. 文字列中に含まれる特定の文字の数
  4. 文字列の結合

関数を使わない場合

  1. 
    #include <stdio.h>
    main(){
      char x[]="This is a pen.";
      int i,k;
      k=0;
      for(i=0;x[i]!='\0';i++){
        k++;
      }
      printf("%d\n",k);
    }
    
  2. 
    #include <stdio.h>
    main(){
      char x[]="This is a pen.";
      char y='p';
      int i,k;
      k=0;
      for(i=0;(x[i]!='\0')&&(x[i]!=y);i++){
        k++;
      }
      if(x[k]!='\0'){
        printf("%d\n",k);
      }else{
        printf("Not found.\n");
      }
    }
    
  3. 
    #include <stdio.h>
    main(){
      char x[]="This is a pen.";
      char y='p';
      int i,k;
      k=0;
      for(i=0;x[i]!='\0';i++){
        if(x[i]==y){
          k++;
        }
      }
      printf("%d\n",k);
    }
    
  4. 
    #include <stdio.h>
    main(){
      char x[]="This is a pen.";
      char y[]="That is a book.";
      char z[50];
      char *p,*q;
      for(p=x,q=z;*p!='\0';*(q++)=*(p++));
      for(p=y;*p!='\0';*(q++)=*(p++));
      *q='\0';
      printf("%s\n",z);
    }
    

関数を使った場合

  1. 
    #include <stdio.h>
    #include <string.h>
    main(){
      char x[]="This is a pen.";
      printf("%d\n",strlen(x));
    }
    
  2. 
    #include <stdio.h>
    #include <string.h>
    main(){
      char x[]="This is a pen.";
      char y='p';
      char *z;
      z=strchr(x,y);
      if(z!=NULL){
        printf("%d\n",(int)(z-x));
      }else{
        printf("Not found.\n");
      }
    }
    
  3. 
    #include <stdio.h>
    #include <string.h>
    main(){
      char x[]="This is a pen.";
      char y='p';
      char *z;
      int k;
      k=0;
      for(z=strchr(x,y);z!=NULL;z=strchr(z+1,y)){
        k++;
      }
      printf("%d\n",k);
    }
    
  4. 
    #include <stdio.h>
    #include <string.h>
    main(){
      char x[]="This is a pen.";
      char y[]="That is a book.";
      char z[50];
      strcpy(z,x);
      strcat(z,y);
      printf("%s\n",z);
    }
    

3-7. C++言語特有の話

C++ 言語は C 言語に Simula67 で開発されたオブジェクト指向の考え方を導入 したものです。 これはプログラミングの方法論を変えてしまう仕組みです。 C++ 言語を使う以上、C++ の流儀に基づいてプログラミングをすべきです。

コメント

C++ 言語ではコメントとして/* と */ の他に、 // から行末までもコメント として取り扱います。 /* */ は入れ子に出来ませんが、 // でコメントを付けた部分は、後で /* と */ で囲めるので便利です。

変数宣言の自由さ

C++ では C 言語と違い、変数宣言を自由な位置に置くことができます。 従って、条件に適合する時だけ巨大な配列を作って処理するようなことができ ます。 また、変数宣言はブロック内のみ有効で、ブロックの外で廃棄されます。


if(a==0){
  int b=1;
}    // この部分で b は廃棄される
a=b; // b は既に廃棄されているのでエラーになる

オーバーロード

C++ 言語では引数の型だけが違う同じ名前の関数が許されています。 これをオーバーロードと言います。 これは、C 言語では禁止されています。

クラス

C++ では C 言語とのしがらみがない限りは、構造体を使いません。 その代わり、構造体の拡張のような形で class が導入されています。 class の内部では変数だけではなく、関数のプロトタイプを書くことができま す。

class はオブジェクト指向を実現するために導入されました。 オブジェクト指向とは、メッセージのやりとりを行うオブジェクトを用いてプ ログラムを作成する手法です。 公開すべきメッセージ仕様(インターフェイス)のみを共有して、 オブジェクトの仕様であるクラスを作成することでプログラムを作ります。 すると、クラス毎に共有すべき情報が最小限に済みます。 プログラミング言語では、メッセージとして、関数(メソッド)と 引数の組合せを指定します。 インターフェイス以外のオブジェクトの持つ変数や、関数を外部からアクセス できないように宣言するため、private というキーワードが用意 されています。 その反対に外部に公開するメソッド(インターフェイス)のために public というキーワードも用意されています。

クラスを作ると、新しい型としてオブジェクトの宣言が可能です。

クラス名 オブジェクト名;

また、構造体と同様に、変数名やメソッド(関数)はピリオドの後に書きます。 メソッド(関数)はメソッド名の後に丸カッコ()を付けます。

オブジェクト名.メソッド(引数)

テンプレート

テンプレートは特定の関数やオブジェクトクラスを任意の型に対して生成した い時に使います。数の二乗をする関数を次のようにテンプレートで作成します。


template <typename T>
T square(T a){
  return a*a;
}

このように用意してヘッダファイルに入れておくと、次のようにプロトタイプ を書くだけで、C++ コンパイラは自動的にその型に応じた関数を生成します。


#include <iostream>
#include "square.h"
using namespace std;
template int square(int);
template double square(double);
main(){
  cout << square(2) << endl
       << square(3.1) << endl;
}

STL

STL(Standard Template Library) は良く使われるデータ構造と関 数が集められたテンプレートです。 なお、STL の関数はアルゴリズムと呼ばれています。 STL に含まれるデータ構造や関数は、この講義でもしばしば取り上げます。

さようなら配列

STL では要素を自由に追加、削除できる配列のテンプレートである vector が 用意されています。 vector は C の配列と違い、領域外にアクセスすると暴走せずにエラーを発生 します。 また、アルゴリズムを使うと高速に検索などのアクセスができます。 以下は配列の合計と平均を求めるプログラムです。


#include <iostream>
#include <numeric> //accumulate に必要
#include <vector>
using namespace std;
main(){
  int a[5]={ 3, 5, 2, 1, 4 };
  vector<int> v(a, a+5);
  int kosuu=v.size();
  int goukei=accumulate(v.begin(),v.end(),0);
  cout << "個数: " << kosuu << endl
       << "合計: " << goukei << endl
       << "平均: " << (double) goukei/kosuu << endl;
}

さようなら文字配列

C++ の STL では string テンプレートが用意されています。 これを用いて C 言語で示した文字列の処理を書くと次のようになります。

  1. 文字列の長さを求める
    
    #include <iostream>
    #include <string>
    using namespace std;
    main(){
      string x="This is a pen.";
      cout << x.size() << endl;
    }
    
  2. 文字列中で最初に特定の文字が出現する位置 (但しこの場合、文字ではなく文字列でも可能)
    
    #include <iostream>
    #include <string>
    using namespace std;
    main(){
      string x="This is a pen.";
      string y="p";
      int z=x.find(y);
      if(z<=x.size()){
        cout << z << endl;
      }else{
        cout << "Not found." << endl;
      }
    }
    
  3. 文字列中に含まれる特定の文字の数
    
    #include <iostream>
    #include <string>
    #include <algorithm>
    using namespace std;
    bool match(const char &c){
      return (c == 'p');
    }
    main(){
      string x="This is a pen.";
      string::iterator p;
      int count=0;
      for(p=find_if(x.begin(), x.end(), match);
          p!=x.end();
          p=find_if(p+1, x.end(), match)){
        count++;
      }
      cout << count << endl;
    }
    
  4. 文字列の結合
    
    #include <iostream>
    #include <string>
    using namespace std;
    main(){
      string x="This is a pen.";
      string y="That is a book.";
      string z=x+y;
      cout << z << endl;
    }
    

3-8. Java 言語特有の話

Java にはテンプレートはありませんが、同様に java.util.ArrayList (java.util.Vector もありますが、パフォーマンスが悪い) と java.lang.String(変更不可), java.lang.StringBuffer(変更可) があります。


import java.lang.*;
import java.util.*;
class Main{
    public static void main(String[] arg){
	int[] a = new int[] {3, 5, 0, 2, 4 };
	List l = new ArrayList();
	for(int i=0; i< a.length;i++){
	    l.add(new Integer(a[i]));
	}
	int kosuu = l.size();
	int goukei=0;
	for(Iterator i = l.iterator(); i.hasNext();){
	    goukei += ((Integer) i.next()).intValue();
	}
	System.out.println("個数: "+kosuu);
	System.out.println("合計: "+goukei);
	System.out.println("平均: "+(double)goukei/kosuu);
    }
}

  1. 文字列の長さを求める
    
    class Test1 {
        public static void main(String[] args){
    	String x="This is a pen.";
    	System.out.println(x.length());
        }
    }
    
  2. 文字列中で最初に特定の文字が出現する位置 (但しこの場合、文字ではなく文字列でも可能)
    
    class Test2 {
        public static void main(String[] args){
    	String x="This is a pen.";
    	String y="p";
    	int k=x.indexOf(y);
    	if(k>=0){
    	    System.out.println(k);
    	}else{
    	    System.out.println("Not found.");
    	}
        }
    }
    
  3. 文字列中に含まれる特定の文字の数
    
    class Test3 {
        public static void main(String[] args){
    	String x="This is a pen.";
    	String y="i";
    	int k=0;
    	for(int i=x.indexOf(y); i>=0; i=x.indexOf(y,i+1)){
    	    k++;
    	}
    	System.out.println(k);
        }
    }
    
  4. 文字列の結合
    
    class Test4 {
        public static void main(String[] args){
    	String x="This is a pen.";
    	String y="That is a book.";
    	System.out.println(x+y);
        }
    }
    

坂本直志 <sakamoto@c.dendai.ac.jp>
東京電機大学工学部情報通信工学科