それではいつものように次のC++ソースコードを見てみましょう。
#include <iostream>
int main()
{
const std::string ch0 = "Typical: I don't know anything about programming!\n";
const std::string ch ("Simplified form: I don't know anything about programming!\n");
const std::string* ch1 = new std::string ("Pointer form: I don't know anything about programming!\n");
std::cout << "Popular: I don't know anything about programming!" << std::endl;
std::printf(ch0.c_str());
std::cout << ch.data();
std::cout << ch1->data();
std::cout << ch.c_str();
std::cout << ch1->c_str();
std::cout.write(ch.c_str(), ch.size());
std::cout.write(ch1->c_str(), ch1->size());
delete ch1;
return int(0);
}
私たちはこのC++ソースコードから次のような印象を受けます。すでに説明済みの項目は、打ち消されています。
印象1:とにかく難しい!
印象2:Cのソースコードとどこか似ている
印象3:#includeに続くiosteamという意味が分からない
印象4:stdio.hがないにもかかわらず、printf関数が呼び出されていることが不思議だ!
印象5:std::coutのstdの意味が分からない
印象6:std::coutの「::」の意味が分からない
印象7:std::coutのcoutの意味が分からない
印象8:「<<」の意味が分からない
印象9:std::endlのendlの意味が分からない
印象10:「std::string ch」のstringの意味が分からない
印象11:「std::string ch」のchの意味が分からない
印象12:ch.data()の意味が分からない
印象13:ch.c_str()の意味が分からない
印象14:std::cout.writeの意味が分からない
印象15:ch.size()の意味が分からない
印象16:std::string*の「*」の意味が分からない
印象17:std::string* ch1の「ch1」の意味が分からない
印象18:ch1->data()の意味が分からない
印象19:ch1->c_str()の意味が分からない
印象20:ch1->size()の意味が分からない
今回は、前回コードと深い関係にある、「印象18:ch1->data()の意味が分からない」の意味を考えてみたいと思います。次のVC++2003コンパイラ出力情報をご覧いただきましょう。
12: std::cout << ch.data();
00401121 8B F4 mov esi,esp
00401123 8D 4D AC lea ecx,[ch] (0x0012ff14)
00401126 FF 15 78 92 40 00 call dword ptr [__imp_std::basic_string<char,std::char_traits<char>,std::allocator<char> >::data (409278h)]
0040112C 3B F4 cmp esi,esp
0040112E E8 6D 03 00 00 call _RTC_CheckEsp (4014A0h)
00401133 8B F4 mov esi,esp
00401135 50 push eax
00401136 8B 0D 68 92 40 00 mov ecx,dword ptr [__imp_std::cout (409268h)]
0040113C 51 push ecx
0040113D FF 15 6C 92 40 00 call dword ptr [__imp_std::operator<<<std::char_traits<char> > (40926Ch)]
00401143 83 C4 08 add esp,8
00401146 3B F4 cmp esi,esp
00401148 E8 53 03 00 00 call _RTC_CheckEsp (4014A0h)
13: std::cout << ch1->data();
0040114D 8B F4 mov esi,esp
0040114F 8B 4D A4 mov ecx,dword ptr [ch1](0x00376048)
00401152 FF 15 78 92 40 00 call dword ptr [__imp_std::basic_string<char,std::char_traits<char>,std::allocator<char> >::data (409278h)]
00401158 3B F4 cmp esi,esp
0040115A E8 41 03 00 00 call _RTC_CheckEsp (4014A0h)
0040115F 8B F4 mov esi,esp
00401161 50 push eax
00401162 8B 15 68 92 40 00 mov edx,dword ptr [__imp_std::cout (409268h)]
00401168 52 push edx
00401169 FF 15 6C 92 40 00 call dword ptr [__imp_std::operator<<<std::char_traits<char> > (40926Ch)]
0040116F 83 C4 08 add esp,8
00401172 3B F4 cmp esi,esp
00401174 E8 27 03 00 00 call _RTC_CheckEsp (4014A0h)
赤字部分を見ると、筆者の環境では、「ch」はアドレス0x0012ff14、「ch1」はアドレス0x00376048を、それぞれ指していることが分かります。「ch」の場合、そのアドレスはLEA(Load Effective Address)という命令で取り出され、「ch1」の場合には、MOV(転送)命令が使われています。一般論で言えば、この場合のMOV命令はメモリとレジスタ間のデータ転送を発生させますから、パフォーマンスが若干低下するといえます。筆者が調べた結論(つまり、簡単なステップインデバッグ分析の結果)だけをお伝えすると、(VS.NET 2003)コンパイラライタは「ch.data()」と「ch1->data()」の相違を次のように表現しています。
ch.c_str(this*, ch); // ch.data()。
ch1->c_str(this*, ch1*); // ch1->data()。
ご覧のように、「data()」メソッドは、内部で「c_str()」メソッドを再利用しています。しかし、受け取る第2引数の「型」が異なっています。前者は有効アドレス、後者は、強いて言えば、次のコードが示しているように、"別のアドレスを指すアドレス(つまり、そのままでは有効なアドレスとして使えない!)"、ということになります。
12: printf("%d %d", a1, *d1);
00401143 8B F4 mov esi,esp
00401145 8B 45 E8 mov eax,dword ptr [d1]eaxはそのまま使えない!
00401148 8B 08 mov ecx,dword ptr [eax]
0040114A 51 push ecx
0040114B 8B 55 EC mov edx,dword ptr [a1]
0040114E 52 push edx edxはそのまま使える!
0040114F 68 4C 66 40 00 push offset ___xi_z+138h (40664Ch)
00401154 FF 15 0C 94 40 00 call dword ptr [__imp__printf (40940Ch)]
0040115A 83 C4 0C add esp,0Ch
0040115D 3B F4 cmp esi,esp
0040115F E8 DC 09 00 00 call _RTC_CheckEsp (401B40h)
なお、先のソースコード内の「ch」は不要になれば、「自然に」、あるいは、「自動的に」解放されるところから、「自然変数」や「自動変数」と呼ばれています。一方、「ch1」は、「new」という演算子で私たちが明示的に確保するメモリブロック(文字列を格納している)の先頭アドレスを指すポインタです。このため、厳密には、不要になった時点で、「delete ch1;」というコードを私たちが明示的に実行させ、解放する必要があります。これもよくよく考えてみれば当たり前のことで、決して難しいことではありません。MOV命令はデータを転送する命令であり、メモリとレジスタ間の、一種の「データ通信」です。データ通信が終了すれば、通信の両端を解放するのが流儀です。