こんにちは。田原です。
C言語は学習しやすく、かつ、コンピュータの基本的な仕組みを学ぶことができる優れた言語ですが、いざ実用プログラムを作ろうとするとメモリ・リーク防止やバッファオーバー・フロー攻撃対策で頭が痛いです。
そのたいへんな部分について少しC++の機能を導入するとかなり楽になります。
その中で特に便利と思うものをTheolizer®のサンプル・ソースで使わせて頂きましたので、それらについて解説します。
1.まずは定番のstd::cout
std::coutはC言語のstdoutに相当します。#include <iostream>で使えるようになります。
使い方は簡単です。基本はstd::cout << foo;だけです。std::coutが対応している型であれば型を気にしないで良しなに出力してくれます。(数値型や文字列型など多くの型に対応しています。ただし、ユーザ定義クラスはユーザが対応させる必要があります。)
改行したい時は、“¥n”;を出力します。複数のデータを連続して出力できます。
std::cout << “foo=” << foo << “bar=” << bar << “¥n”;
さて、実は私はstd::coutではなくprintf()を長い間使ってました。<<が冗長に思えることと書式制御が難しいことでつい避けていたのです。しかし、C++を本格的に使い始めてからstd::coutを使うように心がけてみたところ、意外に具合が良く、今ではprintf()系の関数を使わなくなってしまいました。(なお、std::coutよりprintf()系の方が高速ですので、ログを超高速で出力する必要があるような時はprintf()系関数を使った方が良いかも知れません。)
1-1.std::coutは変数の型の指定が不要です
printf()は書式を指定する際に変数の型も適切に指定する必要があります。そして、その指定を間違ったら化けて出ます。printf()はデバッグ中に多数使うのでどうしても時々ミスしてしまい、その度にやり直しでした。しかし、std::coutを使えば同様なミスはまず発生しないので、このストレスから開放されました。
ただし、std::coutも1つ落とし穴があります。char型変数は文字として出力されますので、char x=48; std::cout << x;とすると”48″ではなくて”0″(’0’のASCIIコードは0x30=48)が出力されます。デバッグ用ならstd::cout << (int)x;などとして逃げてます。
1-2.printf(), fprintf(), sprintf(), vsnprintf()等の使い分けが不要です
文字列として取り出したい時は、std::coutの代わりにstd::stringstream ss;が使えます。ss << foo;などと書きます。
また、ファイルへの出力も同様です。std::ofstream(ファイル名) ofs;です。ofs << foo;などと書きます。
最初の宣言だけ異なりますが、他は同じですので非常に楽です。
1-3.書式指定について
std::coutも書式指定できます。#include <iomanip>で書式指定が使えるようになります。
例えば、std::setw(n)はそれ以降に出力されるフィールドの幅を指定します。
ここにstd::setwの解説があります。このページのexampleの右肩に歯車マークがあり、サンプル・ソースを修正して実行できるサイトにリンクしてます。その場でリファレンスとサンプル・ソースが見れて、更にサンプル・ソースを修正して実行してみることができるので理解が進みます。
1-4.Theolizer®のおまけ機能
std::coutはstd::setw以外にも多数の書式指定子があり複雑な書式指定もできるのですが、また1から書式指定子を覚えないといけないのが辛くて実はほとんど使っていません。しかし、boost::formatを使えば楽にできます。
これを使うとC言語のprintf()っぽい書式指定でフォーマットでき、しかも、型指定を間違っても概ね適切に出力されます。
そして、これにラップをかぶせてもう少しprintf()に近づけたのがtheolizer::print()です。
boost::formatはoperator%で出力するのですが、それがどうにも分かりにくかったので、print(format, 式, 式, …)の書式で書けるようにしたものです。
例えば、家計簿サンプル・ソースでは下記のように使っています。
std::cout << theolizer::print(u8" 残高は%6d%2s\n", iItem->mAmount, iItem->mUnit);
C++erの方へ
可変長引数ではなくVariadic Templatesを使って実装しているので型情報もハンドリングしていますから、適切に出力されます。
2.次にファイル操作のstd::ofstream, std::ifstream
std::ofstream, std::ifstreamはC言語のFILE*とfprintf()関数群、fscanf()関数群に相当します。
つまり、std::ofstreamはstd::coutのファイル出力版です。使い方はstd::coutとほぼ同じです。
テキスト・ファイルのオープンとクローズは、下記です。
#include <fstream> int main() { { std::ofstream ofs("sample.txt"); // ファイルを出力モードでオープン ofs << "output sample\n"; } // ・・・① return 0; }
このソースの①がポイントです。
C言語ではブロック内で定義された変数はそのブロックの終わりでなくなります。
C++の場合も同様ですが、更にその変数が削除される時に実行される関数(デストラクタと呼ばれます)を定義できます。std::fstream型には「ファイルがオープンされていたらクローズする」と言うデストラクタが定義されていますので、①のタイミングで”sample.txt”ファイルが自動的にクローズされます。これを使えば、ファイルのクローズ漏れを心配しなくて良くなるので便利です。
std::ifstreamはその入力版です。>>で入力します。
下記では、std::string(後述)を使って文字列を入力してみました。std::stringはバッファのサイズを気にせず使えます。
#include <fstream> int main() { { std::ifstream ifs("sample.txt"); // ファイルを入力モードでオープン std::string temp; ifs >> temp; // ファイルから文字列を入力 std::cout << "Data of sample.txt is " << temp << "¥n"; } return 0; }
3.そして本命のstd::string
C言語を使っていて何が一番辛いかというと文字列処理と感じてます。
例えば「安全に」文字列を読み込む処理はどう作ろうか悩ましいです。
文字列を読み込むまで、必要なバッファ・サイズが分かりませんから1バイトづつ読み込む度にrealloc()で1バイトづつ拡張していけば簡単ですが効率があまりに悪いです。そこでバッファの拡張を例えば128バイト毎に行うなどの工夫が必要となり手間がかかります。
しかし、std::string型はそのようなバッファの拡張処理も内部に隠蔽しているためお手軽に使えます。下記のように一発です。(std::cinはstd::coutの標準入力版です。)
std::string temp; std::cin >> temp;
そして、C言語の標準ライブラリの文字列操作関数のほとんどの機能をカバーしています。
C標準ライブラリ(string.h) | C++標準ライブラリ(string) | C++での書き方 |
---|---|---|
strcpy / strncpy |
operator= |
std::string foo; foo=”dummy”; // “dummy” |
strcat / strncat |
operator+= |
foo += “+str1”; // “dummy+str1” |
append | foo.append(“+str2”); // “dummy+str1+str2” | |
strcmp / strncmp | compare | foo.compare(“dummy”); // false foo.compare(“dummy+str1+str2”); // true |
比較演算子 | (foo == “dummy”) // false (foo == “dummy+str1+str2”) // true 他多数 |
|
strchr | find | foo.find(‘m’); // 2 |
strstr | foo.find(“str”); // 6 | |
strcspn | find_first_of | foo.find_first_of(“0123”); // 9 |
strpbrk | find_first_of | foo.c_str() + foo.find_first_of(“0123”); |
strrchr | rfind | foo.rfind(“str”); // 11 |
strspn | find_first_not_of | foo.find_first_not_of(“dmuy”) // 5 |
strlen | size | foo.size(); // 15 |
C言語文字列形式へ変換 | c_str | foo.c_str(); // char*型で出力される |
strtok | 無し | 代替案 std::istringstream iss(foo); std::string token; while (std::getline(iss, token, ‘+’)) { std::cout << token << ” “; } // dummy str1 str2 |
strcoll | 無し | 地域(locale)を考慮した比較 |
strxfrm | 無し | 地域(locale)を考慮したコピー |
strerror | 無し | C言語のエラー処理用 |
4.ついでにstd::vector<>とstd::list<>
4-1.std::vector<T>
C言語で文字列の次に苦労するものとして配列があると思います。C言語の配列は一度要素数を決めたら原則として変更できません。変更したい場合はrealloc/freeを使うことになります。mallocやrealloc等でメモリを獲得した場合、適切にfreeで開放しないとメモリ・リークや多重開放が発生するのでその制御が難しいです。
そして、C++には動的に要素数を変更でき配列のように使える標準コンテナが幾つか有ります。その代表的なものがstd::vector<T>です。Tにはintやcharや構造体名等の型名を指定します。
// Example of std::vector<> #include <iostream> #include <vector> #include <string> int main() { std::vector<int> aIntArray; // int型の配列です aIntArray.push_back(1); // 配列に1を追加 aIntArray.push_back(2); // 配列に2を追加 aIntArray.push_back(3); // 配列に3を追加 for (size_t i=0; i < aIntArray.size(); ++i) std::cout << aIntArray[i] << "\n"; std::vector<std::string> aStringArray; // 文字列の配列です aStringArray.push_back("Alice"); // 配列に"Alice"を追加 aStringArray.push_back("Bob"); // 配列に"Bob"を追加 aStringArray.push_back("Eve"); // 配列に"Eve"を追加 for (size_t i=0; i < aStringArray.size(); ++i) std::cout << aStringArray[i] << "\n"; return 0; }
なお、std::vector<T>を使う際、注意事項があります。これは配列領域を獲得する際realloc()を使っているような振る舞いをします。push_back()等で要素を追加した時、realloc()が発生して記憶領域が別の場所へ変わることがあります。ですので、特定の要素へのポインタやイテレータ(後述)はpush_back()等の操作後、あらぬ場所を指すようになることがありますので要注意です。
4-2.std::list<T>
C言語で各種データ構造を学習する際、リスト構造と呼ばれるデータ構造が出てくることが多いと思います。線形リスト、連結リストなど多数の呼び方が有りますが「リスト」を含む呼び方がほとんどです。
そのようなstd::list<T>も用意されています。std::vector同様、Tには型名を指定します。
std::listはstd::vectorと異なり、配列のように[i]でアクセスできません。C言語で実装した時の線形リストと同様、「イテレータ」と呼ばれる一種のポインタを使って枚挙することが多いです。イテレータは通常のポインタのように振る舞いますが、もう少し便利になっています。C言語でリストを追跡する時はp=p->next;のように記述することが多いですが、イテレータは++pで「次」の要素へ移動できます。また、イテレータ名をitとすると*itでイテレータが指している要素の内容(T型の変数)を取り出せます。
// Example of std::list<> #include <iostream> #include <list> #include <string> int main() { std::list<std::string> aStringList; // 文字列のリストです typedef std::list<std::string>::iterator Iterator; aStringList.push_front("Alice"); // 配列先頭に"Alice"を追加 aStringList.push_front("Bob"); // 配列先頭に"Bob"を追加 aStringList.push_front("Eve"); // 配列先頭に"Eve"を追加 for (Iterator it=aStringList.begin(); it != aStringList.end(); ++it) std::cout << *it << "\n"; return 0; }