C++

出典: フリー教科書『ウィキブックス(Wikibooks)』
移動: 案内検索

情報技術 > プログラミング > C++

大学の学習 > C++


Wikipedia
ウィキペディアC++の記事があります。

はじめに[編集]

C++は、Cを拡張してより使いやすい言語にすることを目指して作成されました。実際には、現在でもCは非常に人気のある言語であり続けています。このことには、様々な理由が考えられますが、例としては、

  1. Cはよく用いられる言語なので、他人のコードを読むためには結局Cを勉強する必要がある。
  2. C++は、機能が多く勉強するのに時間がかかる。
  3. C++が提供する機能は、ある程度大きいコード以外ではあまり用いる機会がない。

等があげられます。

C++が提供するCの拡張として以下の機能があげられます。

  1. constの使用
  2. インライン関数
  3. テンプレート
  4. 名前空間
  5. クラスの定義
  6. STL

これらは、Cを用いる上でたびたび現れる問題に対応するために導入されました。ここではそれぞれの機能がどのような目的を持って導入されたかを述べます。

const, インライン関数, テンプレートはCにおけるマクロの代替物として導入されました。マクロは便利な機能ですが、状況によってはプログラムの読み手に混乱を引き起こすことがあるため、これらの機能が導入されました。

名前空間はC言語で名前の重複が起こることを念頭に導入されました。名前の重複については後に詳しく述べますが、複数人でプログラムを作成した時同じ名前の関数があると、 用いる関数を把握できなくなり、エラーが出るという問題です。名前空間はこれを回避するために導入されました。

クラスは、オブジェクト指向をサポートするために導入されました。オブジェクト指向はプログラミング手法の一種で、データ自身に動作を定義することで、プログラムの内容をわかりやすくするという手法です。オブジェクト指向は現在でも特に、大きなプログラムを作成する場合によく用いられています。逆に、それほど大きくないプログラムを書く時には、オブジェクト指向はあまり役に立ちません。 具体的にどれくらいの大きさのプログラムが大きいと思うかは、書き手の好みにもよります。オープンソースからの具体例では、Mozilla Firefoxや、Thunderbirdは、C++で書かれています。また、いかにも巨大なプログラムであるOpenOfficeも、C++で書かれています。一方、巨大プログラムと呼ばれてもよい大きさのプログラムでも、Cで書かれているプログラムは存在します。例えば、Linuxや、X Window Systemは、Cで書かれています。(これらのソフトウェアにCが採用された理由には、プロジェクト立ち上げ当時のC++の普及度や安定度、あるいは対象となるソフトウェアの扱う領域や目的等の要因があると思われ、今まさに同様のプロジェクトをはじめた場合にもCが選ばれるかは分かりません)

最後に、STLは、言語を問わずあらゆる場面でよく用いられるデータ構造等を提供することを目的として導入されました。STLの中には、連想配列リストなどのデータ構造が定義されています。これらは、Perlや、Pythonなどの言語では標準のデータ構造として言語に組み込まれています。これらのデータ構造は、Cのプログラムでもよく用いられます。しかし、Cはこのようなデータ構造を言語自身の機能として持ってはいなかったため、Cを用いるプログラマは、プログラムを作成するときに、まず用いるデータ構造を作成する必要がありました。例えば、Apache httpdや、glibでは、STLで定義されるようなデータ構造が作成されています。 STLは作業の重複を省く意味でも積極的に用いるのがよいでしょう。ただし、これらのデータ構造を用いることだけが目的なら、PerlPythonなどを試してみることを勧めます。これらの言語はC++ほど速く実行されはしません。しかし、C++と比べて比較的簡潔にこれらを用いることが出来ます。そのため、STLはC++を用いる上でこれらのデータ構造が必要になった時に使われます。

次章からはC++の機能についてまとめていきます。

Hello World[編集]

はじめてコードを書くときは、まず、世界へ挨拶をします。

C++はCを拡張して作られているため、 Cと似た部分が多くあります。例えば、

 #include<stdio.h>
 int main(void)
 {
     printf("Hello World.\n");
     return 0;
 }

は正しいC++プログラムです。しかし、C++では、入出力はより直観的な方法で書き直されています。以下は同じ内容をC++の機能を用いて書き直したものです。

 #include <iostream>
 using namespace std;
 
 int main()
 {
     cout<<"Hello World."<<endl;
     return 0;
 }

この例はクラスと演算子オーバーロードを用いています。これらの機能は後に紹介されます。

  • 注意

using namespace の部分は名前空間の機能を用いています。これについては後に述べます。

クラスを用いたプログラムをオブジェクト指向プログラミングと呼びます。そのため、上の例はオブジェクト指向プログラムの例です。ここで用いられているオブジェクト指向について簡単にまとめます。オブジェクト指向に詳しくない人は先にオブジェクト指向について調べる方が無難でしょう。

この例では、オブジェクト指向を導入した結果として、

"Hello World.\n"という文字列をprintfする

という考えから、

coutと呼ばれるデータに"Hello World."という文字列を与えて、'cout自身にそれを解釈させる'

という考えへの移行が行われています。ここで、coutのようにある演算に対してどう振る舞えばよいか知っているデータのことを、オブジェクト指向の用語でオブジェクトと呼びます。上の例では、coutと呼ばれるオブジェクトに対して定義された演算<<を用いました。この例は短いのでわかり辛いのですが、cout自身が文字列の扱い方を知っていることはプログラムを簡単にします。例えば、C言語では標準出力に出力する時には、

 printf("aaa")

関数を用いました。一方、ファイルに出力する時には、

 fprintf(f,"aaa") //fはファイルへのポインタ

という異なる関数を用いる必要があります。これは、扱う対象が異なっている以上当然ですが、出力する対象の数が増えて来るとわかりづらくなります。一方、C++を用いると、標準出力に対しては、

 std::cout.operator<<("aaa");

ファイルに対しても、

 f.operator<<("aaa"); //fはファイルストリーム

となり、同じ名前の関数operator<<を用いることが出来ます。ここで、operator<<は、それが所属するオブジェクト(実際にはオブジェクトが属するクラス)によって動作が変化していることに注意が必要です。このように、データ自身が実際の動作を知っていることで様々なデータを統一的に扱うことが出来ます。もちろん、オブジェクト自身を設計する時にはデータを統一的に扱える'ように'クラスを設計する必要があるため、C言語の場合より必ずしも仕事が簡単になる訳ではありません。しかし、これらを使う側に取っては、C++の用い方の方がわかりやすいといえるでしょう。

  • 注意

実際に<<演算子を用いた演算を行う時には、

 cout << "aaa"

の部分は、

 cout.operator<<("aaa")

というメソッドの呼び出しに書き換えられて実行されます。上の説明ではこのことを用いています。

上の例では、printf等の関数よりcin, cout等のメソッドを用いた方が簡単に入出力が出来ることがわかりました。加えて、C++では、stringと呼ばれるオブジェクトが導入されたため、<string.h>内の関数(strlenなど)が用いられることは減りました。しかし、C言語で導入された関数のほとんどは、そのまま用いられます。これらの関数はC言語と同じヘッダファイルをインクルードして用いることが出来ます。しかし、C++では、名前空間の観点から、

 #include <???.h>

よりも、

 #include <c???>

を用いることを推奨しています。(???は同じ文字列)この時には、C言語で定義された関数はstd名前空間内に導入されます。そのため、不注意で同じ名前の関数を導入することを防ぐことが出来ます。

 #include <stdlib.h>
 char *a = getenv("HOME");

は、

 #include <cstdlib>
 char *a = std::getenv("HOME");

と同じ結果になります。

各機能の解説[編集]

const修飾[編集]

constは様々な場面で用いられます。しかし、最も基本的な用い方は、プログラム内で変更されない定数を導入することです。

 const double PI = 3.14;

この例では、円周率をconstをつけて定義しています。

C言語では、定数を用いるためにマクロを用いることが通例でした。上の例では、

 #define PI 3.14

とします。この方法はC++でも用いることが出来ます。しかし、この例ではPIの型を定めることが出来ないことが問題になります。マクロはコンパイルの前にプリプロセッサによって処理されるため、うっかりPIをdouble以外の型に代入しても気づかない危険があります。一方、constを用いた例では、PIは doubleで宣言した変数であるため、異なる型に対して用いると警告かエラーが出されます。また、constは見ての通り変数としての宣言の為、変数のスコープがそのまま使えるという長所も持ち合わせています。このため、出来る限りconstを用いた方がよいでしょう。


インライン関数[編集]

関数を呼び出すためには、いったん変数の中身を退避する作業が必要になります。この作業には時間がかかり、小さい関数が多数回呼び出される時にはプログラムの実行が遅くなります。そのため、関数を呼び出すのではなく、関数の内容が直接ソースファイル内に書き込まれたかのようにして、コンパイルを行うと、関数呼び出しをさけることが出来ます。このような関数をインライン関数と呼びます。

インライン関数を用いる時には、関数の前にinlineとつけます。

 inline void aaa () { ... }

inline関数はよく用いられる機能で、C言語の最新版であるC99にも導入されています。一方、関数呼び出しをさけるためには、マクロを用いることも出来ます。

 #define aaa ...

この例では、上のインライン関数の例をマクロで置き換えています。しかし、この例でも返り値の型が正しく扱われているかが調べられません。そのため、出来る限りマクロよりインライン関数を使うとよいでしょう。

テンプレート[編集]

テンプレートはクラスと組み合わせて使うことが多い考え方です。しかし、ここでは関数に対するテンプレートを用いて、テンプレートを導入します。

テンプレートに近い機能は最近のJavaでも導入されています。Javaでは、この機能はジェネリックスと呼ばれます。

テンプレートはパラメータとして型を与える機能です。例えば、同じ機能をint型、double型などに対して作成したい場合があります。この時には、型をパラメータとして与えることが出来ると便利です。テンプレートは、

template <typename T, ...> Tを用いた関数定義

という表記を用いることで定義することが出来ます。ここで、Tは区別するために用いられる型名で、どのような文字列を用いてもかまいません。テンプレートを用いて定義された関数を用いるには、

関数名 <型名, ...> (引数)

として呼び出すことが出来ます。型名は曖昧でなければ省略することが出来ます。

 template <typename T>
 T max (T a, T b){ return (a > b)?a :b ;}

これは、2つの数を与えて大きい方を取り出すための関数です。実際にこの関数を用いる時には、

 max (1,2);
 max (1.0, 3.0);
 max<double>(1,3.0); // 明示的に型を指定

などとします。

C言語では、これらの機能を実現する際にマクロを用いて、

 #define max(a, b) ((a > b)?(a) :(b))

とすることが通例でした。しかし、ここではaとbが異なる型を用いているときの扱いがテンプレートを用いている場合と比べてわかりづらくなります。そのため、C++では出来る限りテンプレートを使う方がよいでしょう。

クラス[編集]

クラスはオブジェクト指向の中核となる概念で、様々な言語でクラスを定義することが出来ます。C++以外ではJava,Python,Rubyなどがオブジェクト指向言語として有名です。

C言語のクラス定義は他の言語と比べても非常に汎用的で出来ることが多く、それだけにわかりにくい面もあります。単純にオブジェクト指向に興味があるのなら、PythonやRubyのオブジェクト指向の方が簡潔かも知れないので、そちらも検討してみるとよいでしょう。

オブジェクト指向[編集]

オブジェクト指向はデータ自体に動作を設定するプログラミング手法です。いくつかのデータに対して似た動作をさせる場合には同じような命令をそれぞれに対して作成する必要が出てきます。そのため、それぞれのデータに対して自分がなすべきことを設定しておいて、実際にそれを呼び出す時には共通のインタフェースで呼び出せるようにしておく方が便利です。オブジェクト指向はこのような場合に用いられます。

オブジェクト指向が用いられる例として、GUIウィジェットがあります。ウィジェットはウィンドウやボタンなど、グラフィックを作成するために用いられるデータです。これらは共通の操作として、最大化、最小化などを持っています。これらの操作に対応する関数をウィジェットごとに作成するのは大変な作業なので、これらは多くの場合オブジェクト自体に操作を与えるプログラム手法がとられます。

例えば、Gtkでは、Containerクラスとそれを継承したクラスに属するウィジェットには、

 gtk_container_add(GtkContainer, GtkWidget)

という関数を用いてウィジェットを入れ込む作業が行われます。このように、多くの似通ったデータを扱う際には、オブジェクト指向プログラミングがよく用いられます。

  • 注意

Gtk+はC言語を用いてウィジェットを作成しているので、ここでのオブジェクト指向は変則的です。

継承[編集]

実際には上の例ではオブジェクト指向でいう継承という考えが用いられています。継承とは、似通った性質をもつクラスを複数作成する時に、それらの差分だけを新たに書き加え、共通の部分は既存のものを流用するという考え方です。 上の例では、GtkWindowクラスは、GtkContainerクラスを継承しているので、GtkWindowクラスに対してもgtk_container_addを用いることが出来ます。これは、GtkWindowだけでなく、GtkContainerを継承した全てのクラスの特徴です。 実際に継承を用いる時には、どの部分をどのクラスで導入するかが問題になりますが、ある程度試行錯誤を繰り返すしかなさそうです。 C++では、クラスの継承を用いることが出来ます。これについては後述します。

カプセル化[編集]

カプセル化とはオブジェクト指向のクラスにおいて、オブジェクトが与える手法以外の手法で、オブジェクトの持つデータに影響を与えてはいけないとする考え方です。これはオブジェクト自身が自分のデータを扱う方法を全て提供すべきで、それ以外の方法に頼ることはクラスの設計の問題であるとする考え方にもとづいています。 Pythonなどの言語では、カプセル化は(あまり)サポートされていません。Javaではサポートされています。 C++では、public, private, protectedの3つがアクセス制御として与えられています。privateは、同じクラスに属するメソッドからしか値が呼び出すことができなくなります。publicはプログラムの他の部分からも呼び出せるようになります。protectedは、クラスを継承した先から用いることが出来るようになります。しかし、プログラムの他の部分から呼び出すことは出来ません。

クラスの定義[編集]

C++では、次の様にしてクラスを定義します。

class クラス名 {
アクセス制御:
メソッドもしくはデータの宣言
};

最後の;はCとの互換性のためにつけられています。 また、メソッドが大きい場合にはここのメソッドを次のように定義することが出来ます。

返り値の型 クラス名::メソッド名 (引数) {}
 class aaa {
   private:
     int a;
   public:
    int getA() const {return a;}
    int setA(int a){return this->a = a;}
 };
  • 例2
 class aaa {
   private:
     int a;
   public:
    int getA() const;
    int setA(int a);
 };
 
 int aaa::getA() const
 {
   return a;
 }
 
 int aaa::setA(int a)
 {
   return this->a = a;
 }

上の例ではgetA,setAというメソッドとaというintのデータを持ったクラスを定義しています。getAは、aの中身を得るためのメソッドでsetAは、aを設定するためのメソッドです。これらはaがprivateであることから必要になります。setAでは、thisが用いられていますが、これは与えられたクラスの性質を持つオブジェクトへのポインタを表しています。ポインタなので、それが表すデータにアクセスする時には、->を用います。これは(*this).aと書いても同じ意味になります。

実際には、クラス名の後から最初のアクセス制御までにおかれた関数やデータは、privateとして導入されます。そのため、上の例では、privateをはずしても同じ意味になります。

string[編集]

Cではchar型の配列を用いる事で文字列を扱いましたが、 C++で標準ライブラリにあるstring型で文字列を扱います。

Cの機能を使った文字列操作には、 次のようなものがあります。

 #include <cstdio>
 #include <cstring>
 
 int main()
 {
   char s1[] = "mojiretu";
   char s2[256];
 
   std::strcpy(s2, s1);    /* 文字列のコピー */
   std::printf("%s\n", s2);
   std::strcat(s2, s1);    /* 文字列の追加 */
   std::printf("%s\n", s2);
   std::printf("%zu\n", std::strlen(s2));    /* 文字列のサイズ */
 
   return 0;
 }

これをC++のstring型を使って書き直すと、 次のようになります。

 #include <iostream>
 #include <string>
 
 int main()
 {
   std::string s1 = "mojiretu";
   std::string s2;
 
   s2 = s1;    /* 文字列のコピー */
   std::cout << s2 << std::endl;
   s2 += s1;    /* 文字列の追加 */
   std::cout << s2 << std::endl;
   std::cout << s2.size() << std::endl;    /* 文字列のサイズ */
 
   return 0;
 }

string型にはchar型の配列を使うのにくらべて、 次のようなポイントがあります。

  1. 事前に文字列の最大数を指定する必要がなく、適宜メモリサイズが拡張される。
  2. コピーや追加等の操作を演算子により行える。
  3. 現在の文字列のサイズを、オブジェクトの中に持っている。

上記例では同一関数内ですのであまりメリットが見えないかもしれませんが、 ソフトウェアの規模が大きくなるにつれて利便性が見えてきます。

Cバージョンを関数分割すると、 次のようになります。

 #include <cstdio>
 #include <cstring>
 
 void func(const char* s1)
 {
   char s2[256];
 
   std::strcpy(s2, s1);    /* 文字列のコピー */
   std::printf("%s\n", s2);
   std::strcat(s2, s1);    /* 文字列の追加 */
   std::printf("%s\n", s2);
   std::printf("%zu\n", std::strlen(s2));    /* 文字列のサイズ */
 }
 
 int main()
 {
   char s1[] = "mojiretu";
 
   func(s1);
 
   return 0;
 }

この例ではmain()からしかfunc()は呼ばれないので、 事実上問題はありませんが、 func()に渡された文字列サイズをチェックしないと、 s2に対して範囲外へのアクセスを行う可能性があるため危険です。

これに対してC++バージョンを関数分割すると、 次のようになります。

 #include <iostream>
 #include <string>
 
 void func(const std::string& s1)
 {
   std::string s2;
 
   s2 = s1;    /* 文字列のコピー */
   std::cout << s2 << std::endl;
   s2 += s1;    /* 文字列の追加 */
   std::cout << s2 << std::endl;
   std::cout << s2.size() << std::endl;    /* 文字列のサイズ */
 }
 
 int main()
 {
   std::string s1 = "mojiretu";
 
   func(s1);
 
   return 0;
 }

string型への操作は必要に応じて内部のメモリサイズが増えるので、 引数で渡された文字列のサイズを気にせずコピーや追加を行うことが出来ます。


このページ「C++」は、書きかけです。加筆・訂正など、協力いただける皆様の編集を心からお待ちしております。また、ご意見などがありましたら、お気軽にノートへどうぞ。