Aritalab:Lecture/Programming/Cpp
m |
m (→Garbage Collection) |
||
(24 intermediate revisions by one user not shown) | |||
Line 1: | Line 1: | ||
{{Lecture/Header}} | {{Lecture/Header}} | ||
+ | |||
+ | <big>"C makes it easy to shoot yourself in the foot; C++ makes it harder, but when you do it blows your whole leg off." | ||
+ | <div Align="right">Bjarne Stroustrup</div></big> | ||
+ | |||
==C++プログラミング== | ==C++プログラミング== | ||
− | C++ | + | C++はC言語の完全な上位互換ですが、オブジェクト指向型という点でC言語とは全く違うプログラミング思想に基づいています。簡単に言うと、JavaはC++の(思想の)利点を活かせるように新開発された言語です。 |
− | + | ||
− | === | + | このページでは、C++の基本原則を書いています。より細かいプログラミングテクニックやサンプルプログラムが[[Aritalab:Lecture/Programming/Cpp/Tips|Tips]]のページにもあります。 |
− | + | ||
+ | ===マクロ, inlineの利用=== | ||
+ | C言語でマクロが果たしていた役割を、C++ではinline関数を用いて実現できます。 | ||
+ | 関数定義を | ||
+ | <pre> | ||
+ | inline int cmp(const char& x, const char& y) | ||
+ | { if (x < y) return -1; else if (x > y) return 1; else return 0; } | ||
+ | </pre> | ||
+ | と書いておくと、<tt>cmp(x,y)</tt>という呼び出しはコンパイル時に全てソース中に展開されます。 | ||
+ | 実行ファイルが大きくなるのは欠点ですが、処理速度を犠牲にせずに可読性が上がります。 | ||
===ヘッダーファイル=== | ===ヘッダーファイル=== | ||
− | 利用する関数の型情報は<tt>.h</tt>や<tt>.hh</tt>という拡張子を持つヘッダーファイルに記述し、それをプログラムの先頭で読み込みます。一番有名なヘッダーは<tt>stdio.h</tt>と<tt><iostream></tt>でしょう。 | + | 利用する関数の型情報は<tt>.h</tt>や<tt>.hh</tt>という拡張子を持つヘッダーファイルに記述し、それをプログラムの先頭で読み込みます。一番有名なヘッダーは<tt><stdio.h></tt>と<tt><iostream></tt>でしょう。 |
<pre> | <pre> | ||
#include <stdio.h> | #include <stdio.h> | ||
Line 18: | Line 30: | ||
</pre> | </pre> | ||
二行目のusing以下は、<tt>iostream</tt>で定義される標準入出力 std::cin, std::cout を、std::というネームスペース名無しで利用するために記述します。 | 二行目のusing以下は、<tt>iostream</tt>で定義される標準入出力 std::cin, std::cout を、std::というネームスペース名無しで利用するために記述します。 | ||
+ | C++でCの入出力を使うには | ||
+ | <pre> | ||
+ | #include <cstdio> | ||
+ | </pre> | ||
+ | としてください。 | ||
ヘッダーファイルはクラスを定義するソースプログラム毎に用意します。ディレクトリ内に<tt>.c</tt>や<tt>.cpp</tt>等のファイルと混在してくると面倒です。例えばinclというディレクトリを作り、その中にヘッダーを集めておくと便利でしょう。(コンパイル方法は後述。) | ヘッダーファイルはクラスを定義するソースプログラム毎に用意します。ディレクトリ内に<tt>.c</tt>や<tt>.cpp</tt>等のファイルと混在してくると面倒です。例えばinclというディレクトリを作り、その中にヘッダーを集めておくと便利でしょう。(コンパイル方法は後述。) | ||
Line 23: | Line 40: | ||
各ヘッダーファイルは、プログラム中で複数回読み込むとエラーになります。ヘッダーファイルの先頭に | 各ヘッダーファイルは、プログラム中で複数回読み込むとエラーになります。ヘッダーファイルの先頭に | ||
<pre> | <pre> | ||
− | #ifndef | + | #ifndef LIST_H |
− | #define | + | #define LIST_H |
− | #include "iterator. | + | #include "iterator.h" |
: | : | ||
#endif | #endif | ||
</pre> | </pre> | ||
のように、ifndefのおまじないを入れておきましょう。 | のように、ifndefのおまじないを入れておきましょう。 | ||
− | + | <!---- | |
===コンパイルの仕方=== | ===コンパイルの仕方=== | ||
統合環境を使わない場合は、Makefileを作りましょう。Makeではタブが意味を持つので注意します。下の例では、ヘッダーをinclディレクトリに記述する場合です。(タブが重要なので、下の内容を単にcopy&pasteしても動きません。) | 統合環境を使わない場合は、Makefileを作りましょう。Makeではタブが意味を持つので注意します。下の例では、ヘッダーをinclディレクトリに記述する場合です。(タブが重要なので、下の内容を単にcopy&pasteしても動きません。) | ||
Line 41: | Line 58: | ||
OBJS = $(wildcard *.o) | OBJS = $(wildcard *.o) | ||
− | + | clean: | |
rm -f main | rm -f main | ||
Line 48: | Line 65: | ||
</pre> | </pre> | ||
これを用意しておくと、<tt>make main</tt> や <tt>make clean</tt>と打つだけでコンパイルや片づけができます。 | これを用意しておくと、<tt>make main</tt> や <tt>make clean</tt>と打つだけでコンパイルや片づけができます。 | ||
+ | ---> | ||
+ | |||
+ | ===ファイルの入出力=== | ||
+ | C++では入出力にオペレータ<tt><<, >></tt>を使います。 | ||
+ | ;与えられたファイルを全て結合して表示する cat.cpp | ||
+ | <pre> | ||
+ | #include <iostream> | ||
+ | #include <fstream> | ||
+ | #include <string> | ||
+ | #include <vector> | ||
+ | using namespace std; | ||
+ | |||
+ | int main(int argc, char *argv[]) | ||
+ | { | ||
+ | vector<string> lines; | ||
+ | string line; | ||
+ | for(int i=1; i < argc; i++) { | ||
+ | //argv[0]には実行ファイル名が入っている | ||
+ | ifstream fin(argv[i], ios::in); | ||
+ | if(!fin) {// 必ずエラーチェックを入れる | ||
+ | cout << "Error: cannot open file(" << argv[i] << ")" << endl; | ||
+ | exit(1); | ||
+ | } | ||
+ | while(getline(fin, line)) | ||
+ | lines.push_back(line); | ||
+ | // fin.close(); ファイルはクローズしなくて良い。 | ||
+ | } | ||
+ | for(vector<string>::iterator itr=lines.begin(); | ||
+ | itr != lines.end(); ++itr) | ||
+ | cout << *itr << "\n"; | ||
+ | } | ||
+ | </pre> | ||
+ | これは STL を使うプログラム例なのでファイルの中身をすべて vector の中にコピーしています。 | ||
+ | (ファイルの内容を出力するためだけなら、こんなことをしてはいけません。) | ||
==クラス定義== | ==クラス定義== | ||
+ | クラスは以下のように定義します。 | ||
<pre> | <pre> | ||
class class名 { | class class名 { | ||
Line 58: | Line 110: | ||
}; | }; | ||
</pre> | </pre> | ||
− | + | C言語における構造体との主な違いはアクセス制限です。 | |
− | + | ||
+ | ===クラスと構造体の違い=== | ||
+ | 以下のようなC構造体を考えましょう。 | ||
+ | <pre> | ||
+ | struct list { | ||
+ | char* name; | ||
+ | struct list* next; | ||
+ | struct list* prev; | ||
+ | }; | ||
+ | </pre> | ||
+ | 構造体を初期化するには、構造体用のメモリと名前のメモリをそれぞれ malloc しなくてはなりません。 | ||
+ | <pre> | ||
+ | ... | ||
+ | struct list *p; | ||
+ | char* sampleName; | ||
+ | ... | ||
+ | if ((p = (struct list *) malloc(sizeof(struct list))) == NULL) { | ||
+ | printf("malloc error\n"); exit(EXIT_FAILURE); | ||
+ | } | ||
+ | if (p->name = (char*) malloc(sizeof(char)*20)) == NULL { | ||
+ | printf("malloc error\n"); exit(EXIT_FAILURE); | ||
+ | } | ||
+ | strcpy(p->name, sampleName); | ||
+ | ... | ||
+ | </pre> | ||
+ | これでは全てのユーザが構造体の中身を熟知していないと使えません。 | ||
+ | とりわけ、nameに malloc が必要なことや、20文字で十分かという判断は面倒です。 | ||
+ | そこでC++ではこれをクラスにします。 | ||
+ | <pre> | ||
+ | public class Node { | ||
+ | string name; | ||
+ | Node* next; | ||
+ | Node* prev; | ||
+ | |||
+ | public: | ||
+ | Node(string str) : name(str) { prev=next=NULL; } //コンストラクタ | ||
+ | ~Node() {} //デストラクタ | ||
+ | ... | ||
+ | string getName() { return name; } | ||
+ | }; | ||
+ | </pre> | ||
+ | コンストラクタが、構造体の malloc に対応します。 | ||
+ | Cにおける char* 型はできる限り STLの string で置き換えましょう。こうすると、文字列の長さを気にしなくてよくなります。 | ||
+ | 実際、上のクラスでも 20 という定数や malloc が消えています。 | ||
+ | |||
+ | ===自分自身を参照する<tt>this</tt>=== | ||
+ | クラスの中で、自分自身を参照するには this というポインタを使います。 | ||
+ | つまり実体を参照する場合は *this になります。 | ||
+ | このポインタはコピーコンストラクタを定義する際に出てきます。 | ||
+ | <pre> | ||
+ | public class Node { | ||
+ | ... | ||
+ | Node& Operator=(const Node& x) | ||
+ | { name = x.name; next = x.next; prev = x.prev; return *this; } | ||
+ | ... | ||
+ | } | ||
+ | </pre> | ||
+ | 最後に <tt>return *this;</tt> としている点に注意してください。 | ||
+ | 自分自身を戻り値として返し、その際に参照渡し(&)をしています。 | ||
+ | メンバー変数をたとえば <tt>this->name</tt> と参照することもできます。 | ||
+ | (ちなみに、上のオペレータ定義はポインタの値をNodeクラスから持ち出す「不適切なコード」です。実際にはこのようなコードをかくことは無いでしょう。) | ||
+ | |||
+ | |||
+ | ==STL== | ||
+ | STLとは Standard Template Library の略でC++が標準でサポートするデータ構造ライブラリです。 | ||
+ | 使えるデータ構造がある場合は積極的に使いましょう。逆に、標準でサポートされないライブラリはあまり使わないようにしましょう。 | ||
+ | |||
+ | * [http://www.cplusplus.com/reference/algorithm/ C++.comのSTL解説] | ||
+ | |||
+ | ===できる限りstringクラスを使う=== | ||
+ | 単純な文字列でも、できる限りstringクラスを使いましょう。自分で<tt>char*</tt>を使うのを避けましょう。以下は、それでも<tt>char*</tt>を使いたい人に向けたヒントです。 | ||
+ | |||
+ | * 文字列は文字数+1の長さを宣言する。最後のnull文字分を忘れないように。 | ||
+ | * 文字列のメモリはcallocで確保する。callocはmallocした後に0で初期化するのと同じです。 | ||
+ | * strncpy, strncmpを利用する。strcpyやstrcmpはnull文字が出てくるまで処理を続けます。できる限り、処理する長さを自分で管理してください。 | ||
+ | |||
+ | ===listはリスト、vectorは配列=== | ||
+ | STLでは、vectorクラスでも push_back 等の動的なサイズ変更が可能ですが、これははじめから余分にメモリを確保してあるためで、むやみに挿入や削除をすると効率が悪くなります。 | ||
+ | |||
+ | ===イテレータを使う=== | ||
+ | STLにおいて配列、リスト、集合などのクラスはイテレータでスキャンすることができます。ポインタを用いて実装されています。 | ||
+ | <pre> | ||
+ | vector<int> V; | ||
+ | ... | ||
+ | for(vector<int>::iterator I = V.begin(); I != V.end(); I++) | ||
+ | cout << *I << ' '; | ||
+ | </pre> | ||
+ | ここで終了条件が <tt>V.end()</tt>になっていることに注意してください。beginは最初の要素を返すメソッドですが、endは最後の要素の次を返すメソッドです。 | ||
+ | |||
==メモリ管理== | ==メモリ管理== | ||
===コンストラクタとデストラクタ=== | ===コンストラクタとデストラクタ=== | ||
− | + | C++のクラス定義では、例えば以下のような記述をします。 | |
<pre> | <pre> | ||
class list_node; | class list_node; | ||
Line 101: | Line 241: | ||
基本データ型のnew/deleteは、基本的にmalloc/freeと同じですが、void*しか返さないmallocに比較して型キャストの必要がありません。 | 基本データ型のnew/deleteは、基本的にmalloc/freeと同じですが、void*しか返さないmallocに比較して型キャストの必要がありません。 | ||
− | + | ですから、C++らしいプログラムを心がけるには、常にnew/deleteを使うと良いでしょう。 | |
; 働きの全く同じ二つの例 | ; 働きの全く同じ二つの例 | ||
Line 108: | Line 248: | ||
delete[] cArray; | delete[] cArray; | ||
− | char** cArray2 = (char**) malloc(sizeof(char)*10); | + | char** cArray2 = (char**) malloc(sizeof(char*)*10); |
free(cArray2); | free(cArray2); | ||
</pre> | </pre> | ||
===Garbage Collection=== | ===Garbage Collection=== | ||
− | Javaとの最大の違いは、メモリ管理を自分で行う点です。これは単に「遅れている」という訳ではなく、実時間プログラミングのようなGarbage collector(GC) | + | Javaとの最大の違いは、メモリ管理を自分で行う点です。これは単に「遅れている」という訳ではなく、実時間プログラミングのようなGarbage collector(GC)が致命的となる場合にも使える「汎用性」と捉えましょう。C++の人が GC を使う際には [http://www.boost.org/ Boost Library] にある boost::shared_ptr を使うのが良いでしょう。 |
− | + | ||
+ | * [http://www.boost.org/doc/libs/1_47_0/libs/smart_ptr/shared_ptr.htm Shared_ptr] @Boost | ||
+ | |||
+ | Shared_ptr の仕組については、Scott Myersの教科書"Effective C++"を読んでください。私自身が昔Cppプログラマだった頃は [http://www.hpl.hp.com/personal/Hans_Boehm/gc/ Boehm Collector]をよく使いました(笑)。 |
Latest revision as of 14:22, 4 October 2011
Wiki Top | Up one level | レポートの書き方 | Arita Laboratory |
|
"C makes it easy to shoot yourself in the foot; C++ makes it harder, but when you do it blows your whole leg off."
[edit] C++プログラミング
C++はC言語の完全な上位互換ですが、オブジェクト指向型という点でC言語とは全く違うプログラミング思想に基づいています。簡単に言うと、JavaはC++の(思想の)利点を活かせるように新開発された言語です。
このページでは、C++の基本原則を書いています。より細かいプログラミングテクニックやサンプルプログラムがTipsのページにもあります。
[edit] マクロ, inlineの利用
C言語でマクロが果たしていた役割を、C++ではinline関数を用いて実現できます。 関数定義を
inline int cmp(const char& x, const char& y) { if (x < y) return -1; else if (x > y) return 1; else return 0; }
と書いておくと、cmp(x,y)という呼び出しはコンパイル時に全てソース中に展開されます。 実行ファイルが大きくなるのは欠点ですが、処理速度を犠牲にせずに可読性が上がります。
[edit] ヘッダーファイル
利用する関数の型情報は.hや.hhという拡張子を持つヘッダーファイルに記述し、それをプログラムの先頭で読み込みます。一番有名なヘッダーは<stdio.h>と<iostream>でしょう。
#include <stdio.h>
iostreamも以前は拡張子(.h)を付けてインクルードしていたのですが、標準化の際に付けないことが決まりました。なので以下のように書いてください。
#include <iostream> using namespace std;
二行目のusing以下は、iostreamで定義される標準入出力 std::cin, std::cout を、std::というネームスペース名無しで利用するために記述します。 C++でCの入出力を使うには
#include <cstdio>
としてください。
ヘッダーファイルはクラスを定義するソースプログラム毎に用意します。ディレクトリ内に.cや.cpp等のファイルと混在してくると面倒です。例えばinclというディレクトリを作り、その中にヘッダーを集めておくと便利でしょう。(コンパイル方法は後述。)
各ヘッダーファイルは、プログラム中で複数回読み込むとエラーになります。ヘッダーファイルの先頭に
#ifndef LIST_H #define LIST_H #include "iterator.h" : #endif
のように、ifndefのおまじないを入れておきましょう。
[edit] ファイルの入出力
C++では入出力にオペレータ<<, >>を使います。
- 与えられたファイルを全て結合して表示する cat.cpp
#include <iostream> #include <fstream> #include <string> #include <vector> using namespace std; int main(int argc, char *argv[]) { vector<string> lines; string line; for(int i=1; i < argc; i++) { //argv[0]には実行ファイル名が入っている ifstream fin(argv[i], ios::in); if(!fin) {// 必ずエラーチェックを入れる cout << "Error: cannot open file(" << argv[i] << ")" << endl; exit(1); } while(getline(fin, line)) lines.push_back(line); // fin.close(); ファイルはクローズしなくて良い。 } for(vector<string>::iterator itr=lines.begin(); itr != lines.end(); ++itr) cout << *itr << "\n"; }
これは STL を使うプログラム例なのでファイルの中身をすべて vector の中にコピーしています。 (ファイルの内容を出力するためだけなら、こんなことをしてはいけません。)
[edit] クラス定義
クラスは以下のように定義します。
class class名 { private: // 外からアクセスできない変数やメンバー関数 public: // 外からアクセス可能な変数やメンバー関数 };
C言語における構造体との主な違いはアクセス制限です。
[edit] クラスと構造体の違い
以下のようなC構造体を考えましょう。
struct list { char* name; struct list* next; struct list* prev; };
構造体を初期化するには、構造体用のメモリと名前のメモリをそれぞれ malloc しなくてはなりません。
... struct list *p; char* sampleName; ... if ((p = (struct list *) malloc(sizeof(struct list))) == NULL) { printf("malloc error\n"); exit(EXIT_FAILURE); } if (p->name = (char*) malloc(sizeof(char)*20)) == NULL { printf("malloc error\n"); exit(EXIT_FAILURE); } strcpy(p->name, sampleName); ...
これでは全てのユーザが構造体の中身を熟知していないと使えません。 とりわけ、nameに malloc が必要なことや、20文字で十分かという判断は面倒です。 そこでC++ではこれをクラスにします。
public class Node { string name; Node* next; Node* prev; public: Node(string str) : name(str) { prev=next=NULL; } //コンストラクタ ~Node() {} //デストラクタ ... string getName() { return name; } };
コンストラクタが、構造体の malloc に対応します。 Cにおける char* 型はできる限り STLの string で置き換えましょう。こうすると、文字列の長さを気にしなくてよくなります。 実際、上のクラスでも 20 という定数や malloc が消えています。
[edit] 自分自身を参照するthis
クラスの中で、自分自身を参照するには this というポインタを使います。 つまり実体を参照する場合は *this になります。 このポインタはコピーコンストラクタを定義する際に出てきます。
public class Node { ... Node& Operator=(const Node& x) { name = x.name; next = x.next; prev = x.prev; return *this; } ... }
最後に return *this; としている点に注意してください。 自分自身を戻り値として返し、その際に参照渡し(&)をしています。 メンバー変数をたとえば this->name と参照することもできます。 (ちなみに、上のオペレータ定義はポインタの値をNodeクラスから持ち出す「不適切なコード」です。実際にはこのようなコードをかくことは無いでしょう。)
[edit] STL
STLとは Standard Template Library の略でC++が標準でサポートするデータ構造ライブラリです。 使えるデータ構造がある場合は積極的に使いましょう。逆に、標準でサポートされないライブラリはあまり使わないようにしましょう。
[edit] できる限りstringクラスを使う
単純な文字列でも、できる限りstringクラスを使いましょう。自分でchar*を使うのを避けましょう。以下は、それでもchar*を使いたい人に向けたヒントです。
- 文字列は文字数+1の長さを宣言する。最後のnull文字分を忘れないように。
- 文字列のメモリはcallocで確保する。callocはmallocした後に0で初期化するのと同じです。
- strncpy, strncmpを利用する。strcpyやstrcmpはnull文字が出てくるまで処理を続けます。できる限り、処理する長さを自分で管理してください。
[edit] listはリスト、vectorは配列
STLでは、vectorクラスでも push_back 等の動的なサイズ変更が可能ですが、これははじめから余分にメモリを確保してあるためで、むやみに挿入や削除をすると効率が悪くなります。
[edit] イテレータを使う
STLにおいて配列、リスト、集合などのクラスはイテレータでスキャンすることができます。ポインタを用いて実装されています。
vector<int> V; ... for(vector<int>::iterator I = V.begin(); I != V.end(); I++) cout << *I << ' ';
ここで終了条件が V.end()になっていることに注意してください。beginは最初の要素を返すメソッドですが、endは最後の要素の次を返すメソッドです。
[edit] メモリ管理
[edit] コンストラクタとデストラクタ
C++のクラス定義では、例えば以下のような記述をします。
class list_node; typedef list_node* lnode; class list_node { private: list_node(const list_node&); list_node& operator=(const list_node&); public: void* key; lnode list_pred; lnode list_succ; list_node(GenPtr x=0x0) : key(x), list_pred(0), list_succ(0) {} ~list_node() {} };
この中でlist_node(GenPtr x=0x0)とあるのがコンストラクタ、~list_node()がデストラクタで、それぞれクラスのインスタンス生成、消去時に呼び出されます。コンストラクタ内ではクラス変数の初期化、デストラクタ内では必要なくなったポインタのnull化などをしておくと良いでしょう。(後者は無駄に思えるかもしれませんが、バグをなくすのに役立ちます。)
[edit] コピーコンストラクタと演算子のオーバーロード
上の例でprivate指定でアクセス制限されているのが、コピーコンストラクタlist_node(const list_node&)と演算子=の定義list_node& operator=(const list_node&)になります。 コピーコンストラクタは、
list_node x = 既に定義されているlist_nodeクラス;
と=をつけて初期化する場合と、関数にクラスを(参照渡しではなく)値渡しするときに実行されます。 また、演算子=のほうは、
list_node x,y; x = y = 既に定義されているlist_nodeクラス;
と書かれたときに実行されます。上の例では、list_nodeクラスがユーザに無闇にコピーされることを防ぐためにprivate指定にしています。
[edit] new と delete
Cにおけるmalloc/freeと、new/deleteは同じではありません。 クラスのnew/deleteでは、コンストラクタとデストラクタが呼ばれることに注意しましょう。 基本データ型のnew/deleteは、基本的にmalloc/freeと同じですが、void*しか返さないmallocに比較して型キャストの必要がありません。
ですから、C++らしいプログラムを心がけるには、常にnew/deleteを使うと良いでしょう。
- 働きの全く同じ二つの例
char** cArray = new char*[10]; delete[] cArray; char** cArray2 = (char**) malloc(sizeof(char*)*10); free(cArray2);
[edit] Garbage Collection
Javaとの最大の違いは、メモリ管理を自分で行う点です。これは単に「遅れている」という訳ではなく、実時間プログラミングのようなGarbage collector(GC)が致命的となる場合にも使える「汎用性」と捉えましょう。C++の人が GC を使う際には Boost Library にある boost::shared_ptr を使うのが良いでしょう。
- Shared_ptr @Boost
Shared_ptr の仕組については、Scott Myersの教科書"Effective C++"を読んでください。私自身が昔Cppプログラマだった頃は Boehm Collectorをよく使いました(笑)。