Cコンパイラ作成入門①〜トークナイザの導入まで
低レイヤを知りたい人のためのCコンパイラ作成入門は、最初に電卓程度の機能しか無いコンパイラを作り、徐々に機能を付け足していくという内容である。今回はステップ1〜ステップ3までを自分用にまとめてみる。
すでに知っていたことや、まとめるのが面倒になってしまったこと、結局よくわからないまま読み飛ばしたことは省略した。逆に、わからなかったけれど調べたらなにかわかったような部分については説明を補足している。
開発環境については上記リンク先の「本書の想定する開発環境」参照。なお私の環境は Ubuntu 18.04.4 LTS である。
はじめに〜ステップ1:整数1個をコンパイルする言語の作成
上記リンク先で使用するコマンドをインストールする。Ubuntuの場合は端末で以下のコマンドを実行すればよい。
$ sudo apt update $ sudo apt install -y gcc make git binutils libc6-dev
オプション-y
をつけると、すべてのプロンプトに自動的に"yes"と答え、非対話的に実行する(但し、不適切な処理がなされるときは実行を中断する)。
コンパイラ本体の作成
gcc などのコンパイラでは、CプログラムファイルX.c
を以下のようにコンパイルすると、$ cc -o X X.c
前処理→コンパイル→アセンブル→リンク
という処理を行う。
我々が目指すのは、Cプログラムファイルをアセンブリ言語で書かれたファイルに変換することだ。アセンブリ言語から機械語への翻訳はアセンブラに任せる。
さて、ここではコンパイラ名を「9cc」としよう(コンパイラ名は各自、自由に名付ければよい)。ステップ1では「整数1個をコンパイルする言語」を作成する。
まずは作業ディレクトリ9cc
を作成する。今後ファイルを新規作成するときは、断りのない限りこのディレクトリで作成するものとする。さて、以下のCプログラムファイル9cc.c
を作成する。
#include <stdio.h> #include <stdlib.h> int main(int argc, char **argv) { if (argc != 2) { fprintf(stderr, "引数の個数が正しくありません\n"); return 1; } printf(".intel_syntax noprefix\n"); printf(".globl main\n"); printf("main:\n"); printf(" mov rax, %d\n", atoi(argv[1])); printf(" ret\n"); return 0; }
ちなみにコマンドライン引数argc
,argv
の中身は、例えば
$ cc -o 9cc 9cc.c $ ./9cc 123
とコンパイル&実行したとき以下のようになる。
コマンドライン引数 | 中身 | 説明 |
---|---|---|
argc | 2 | 配列argvの要素数 |
argv[0] | "./9cc" | プログラム名 |
argv[1] | "123" | 第1引数 |
次にatoi
関数は文字列を渡すと、int
型整数に変換して返してくれる。atoi( "123ABC" )
の値は123
、atoi( "456" )
の値は456
となる。
さて、9cc.c
を以下のようにコンパイル&実行してみよう。
$ cc -o 9cc 9cc.c $ ./9cc 357 .intel_syntax noprefix .globl main main: mov rax, 357 ret
思い通りの出力結果が得られた。この結果をファイルtmp.s
に出力してみよう。
$ ./9cc 357 > tmp.s
ファイルtmp.s
の中身は、以下のCプログラムをアセンブリ言語に変換したものと等しい。
int main() { return 357: }
ちなみにtmp.s
の中身
.intel_syntax noprefix .globl main main: mov rax, 357 ret
の一行目は、複数あるアセンブラの書き方の中でIntel記法というものを選ぶためのアセンブラコマンドである。お約束として書いておく。main:
内のmov rax, 357
とはレジスタrax
に値357
を書き込むという意味だ。
さらにtmp.s
をアセンブルして、作成された実行ファイルtmp
を実行しよう。
$ cc -o tmp tmp.s $ ./tmp $ echo $? 357
すると終了コードは最初にわたした引数357
と等しくなる。
自動テスト
次に、テストを行うためのプログラムを作成する。以下のシェルスクリプトtest.sh
を作成しよう。
#!/bin/bash assert() { expected="$1" input="$2" ./9cc "$input" > tmp.s cc -o tmp tmp.s ./tmp actual="$?" ## 終了コード=$expectedになるはず… if [ "$actual" = "$expected" ]; then echo "$input => $actual" else echo "$expected expected, but got $actual" exit 1 fi } assert 0 0 assert 42 42 echo OK
実行結果:
0 => 0 42 => 42 OK
例えばassert 42 42
では、第2引数の値42を自作コンパイラでコンパイルしたときの終了コードが、第1引数の値42に等しいかどうか確かめている。
makeによるビルド
毎回コマンドを打ち込むのは面倒なのでMakefile
を作成する。Makefile
という名前のファイルを以下の内容で作成する。
CFLAGS=-std=c11 -g -static 9cc: 9cc.c test: 9cc ./test.sh clean: rm -f 9cc *.o *~ tmp* .PHONY: test clean
ターゲット9cc
,test
,clean
について説明する。
まずはターゲット9cc
について。$ make
は$ make 9cc
と同じである。コマンド$ make
を実行するだけで、9cc.c
をコンパイルして実行ファイル9cc
を生成する。
$ make cc -std=c11 -g -static 9cc.c -o 9cc`
正確には9cc.c
がコンパイル済み&実行ファイル9cc
生成済みのときは何もしない。
$ make make: '9cc' は更新済みです. $ touch 9cc.c $ make cc -std=c11 -g -static 9cc.c -o 9cc`
touch
コマンドで最終更新日時を今の日時に変えると、再びコンパイルが行われる。勿論9cc.c
の内容に変更を加えても、再びコンパイルが行われる。
次はターゲットtest
について。$ make test
を行うとどうなるか。
$ make test ./test.sh 0 => 0 42 => 42 25+2-6 => 21 OK
テストを行ってくれる。$make
のときと同様に、必要があればターゲット9cc
の処理も行ってくれる。
$ touch 9cc.c $ make test cc -std=c11 -g -static 9cc.c -o 9cc ./test.sh 0 => 0 42 => 42 OK
最後にターゲットclean
について。$ make clean
でテンポラリファイルを削除する。手動で消すと間違える可能性があるため書かれた。
これで最初のCコンパイラは完成。使ってみよう。
$ ./9cc 123 > tmp.s $ ./tmp $ echo $? 123
最後にGitHubへのアップロードをする。
gitによるバージョン管理
GitHubへのユーザ登録などの準備は済んでいるものとする。
手で書いたファイル(9cc.c
,test.sh
,Makefile
等)しかバージョン管理しない。コマンドを実行することで生成されるようなファイル(9cc
,tmp.s
,tmp
等)は、バージョン管理から外す。
gitでは.gitignore
というファイルに、バージョン管理から外すファイルのパターンを書いておくことができる。今回はテンポラリファイルやエディタのバックアップファイルなどを外す。そのために、以下の内容でファイル.gitignore
を作成しよう。
*~ *.o tmp* a.out 9cc
さて、現在のディレクトリ9cc
で以下のコマンドを実行し、ローカルリポジトリを作成する。
$ git init
次にバージョン管理したいファイルをローカルリポジトリのインデックスに追加する。
$ git add 9cc.c test.sh Makefile .gitignore
次にそれらのファイルをローカルリポジトリにcommitする。
$ git commit -m "整数1つをコンパイルするコンパイラを作成"
次にgit remote
でアドレスgit@github.com:naomeo/9cc.git
の短縮名をoriginと設定する。originと名付けるのは慣習なので、自分で違う名前を設定してもよい。masterはデフォルトのブランチ名である。naomeo部分はgitのアカウント名、9cc部分はリポジトリ名である。
$ git remote add origin git@github.com:naomeo/9cc.git
最後にローカルリポジトリの中身を、originという名のリモートサーバの、masterブランチに追加する。
$ git push origin master
プラウザでGitHubの自分のページを見ると、新しい更新が確認できるはずだ。
ステップ2:加減算のできるコンパイラの作成
このステップでは、以下のような挙動を目指す。
$ ./9cc "10+2-3" > tmp.s $ cc -o tmp tmp.s $ ./tmp $ echo $? 9
計算はアセンブラに行わせたい。10+2-3
ならこんなアセンブリコードになるはず。
.intel_syntax noprefix .globl main main: mov rax, 10 add rax, 2 sub rax, 3 ret
ファイル9cc.c
の内容は以下のようになる。
#include <stdio.h> #include <stdlib.h> int main(int argc, char **argv) { if (argc != 2) { fprintf(stderr, "引数の個数が正しくありません\n"); return 1; } char *p = argv[1]; printf(".intel_syntax noprefix\n"); printf(".globl main\n"); printf("main:\n"); // 最初の数字 printf(" mov rax, %ld\n", strtol(p, &p, 10)); while(*p) { if(*p == '+') { p++; printf(" add %ld\n", strtol(p, &p, 10)); continue; } if(*p == '-') { p++; printf(" sub %ld\n", strtol(p, &p, 10)); continue; } fprintf(stderr, "予期しない文字です : '%c'\n", *p); return 1; } printf(" ret\n"); return 0; }
strtol
関数は文字列をlong int
型整数に変換する。ヘッダstdlib.h
で定義されており、形式はlong int strtol(const char* s, char** endptr, int radix)
である。radix
には2
から32
の数字を入れる。radix
を基数にして文字列s
をlong int
型に変換し、変換できなかった最初の文字から始まる文字列をendptr
に格納する。返り値は読み取った文字。以下に例を示した。
#include <stdio.h> #include <stdlib.h> int main() { char *p; p = NULL; printf("返り値: %ld, ", strtol("17", &p, 10)); printf("p: %s\n", p); printf("返り値: %ld, ", strtol("17", &p, 16)); printf("p: %s\n", p); printf("返り値: %ld, ", strtol("17abc", &p, 10)); printf("p: %s\n", p); printf("返り値: %ld, ", strtol("abc17", &p, 10)); printf("p: %s\n", p);xx return 0; }
実行結果:
返り値: 17, p: 返り値: 23, p: 返り値: 17, p: abc 返り値: 0, p: abc17
テストファイルtest.sh
にテストを書き足しておく。
assert 10 '2+11-3' assert 10 `24-7-7'
ここまでの変更をgitに記録しておこう。
$ git add 9cc.c test.sh $ git commit
$ git commit
と入力するとエディタが開く。そこに「足し算と引き算を追加」なるコメントを書き込もう。最後にpush
でGitHubに更新をプッシュすればステップ2は完了だ。
$ git push origin master
ステップ3:トークナイザの導入
上のコンパイラでは、記号'+'や'-'
の直前に空白を入れるとエラーになる。
$ ./9cc '3 +1' > tmp.s 予期しない文字です : ' '
今回はそれを解決したい。
ちなみに数字の直前に空白を入れてもエラーにはならない。
$ ./9cc ' 3+ 1' > tmp.s
なぜならstrtol関数が勝手に空白を無視してくれているからだ。
char *p; strtol(" 123abc", &p, 10); // 123 strtol(" 123+", &p, 10); // 123
さて、この問題を解決するにはどうすればいいだろうか。入力された文字列を読み取るときに空白文字を飛ばしても解決できる。しかしここでは違う方法をとる。
例えば'2 + 3 - 1'
は'2'
, '+'
, '3'
, '-'
, '1'
と5つの単語(token)に分解できる。このトークン列に対して処理を施せばよい。入力された文字列を単語に分解することを「トークナイズする」と言う。
コードは以下のようになる。
#include <ctype.h> #include <stdarg.h> #include <stdbool.h> #include <stdio.h> #include <string.h> #include <stdlib.h> // トークンの種類 typedef enum { TK_RESERVED, // 記号 TK_NUM, // 整数 TK_EOF // 入力の終わりを表す }TokenKind; // トークンの型 typedef struct TokenDummy Token; struct TokenDummy { TokenKind kind; Token *next; // 次のトークン long int num; // 数値(整数トークンであれば) char *str; // トークン文字列 }; // 現在着目しているトークン Token* token; // エラーを報告するための関数 // printf と同じ引数を取る void error( char *fmt, ... ) { va_list args; va_start( args, fmt ); vfprintf( stderr, fmt, args ); fprintf( stderr, "\n" ); exit(1); } // 今のトークンが期待している記号のときには、トークンを1つ読み進めて // 真を返す。それ以外の場合には偽を返す。 bool consume(char op) { if( token->kind != TK_RESERVED || token->str[0] != op ) return false; token = token->next; return true; } // 今のトークンが期待している記号のときには、トークンを1つ読み進める。 // それ以外の場合にはエラーを報告する。 void expect(char op) { if( token->kind != TK_RESERVED || token->str[0] != op ) error( "'%c'ではありません", op ); token = token->next; } // 今のトークンが数値の場合、トークンを1つ読み進めてその数値を返す。 // それ以外の場合にはエラーを報告する。 long int expect_number() { if( token->kind != TK_NUM ) error("数ではありません\n"); long int val = token->num; token = token->next; return val; } // token->kind が TK_EOF なら 1 を、それ以外なら 0 を返す bool at_eof() { return token->kind == TK_EOF; } // 新しいトークンを作成してcurに繋げる Token *new_token(TokenKind kind, Token *cur, char *str) { Token* tok = calloc( 1, sizeof(Token) ); // callocについては後述する tok->kind = kind; tok->str = str; cur->next = tok; return tok; } // 入力文字列pをトークナイズしてそれを返す Token *tokenize(char *p) { Token head; head.next = NULL; Token* cur = &head; while( *p ) { if( isspace( *p ) ) // isspace 関数はヘッダ ctype.h に入っている // 引数が空白文字なら0以外を、空白文字以外なら0を返す { p++; continue; } if( *p == '+' || *p == '-' ) { // cur->nextを設定して、それをcurに代入している cur = new_token( TK_RESERVED, cur, p ); p++; continue; } if( isdigit(*p) ) // isdigit 関数はヘッダ ctype.h に入っている // 引数が数字なら0以外を、数字以外なら0を返す { // cur->nextを設定して、それをcurに代入している cur = new_token( TK_NUM, cur, p ); cur->num = strtol( p, &p, 10 ); continue; } error("トークナイズできません"); } // cur->nextを設定 new_token(TK_EOF, cur, p); return head.next; } int main(int argc, char **argv) { if (argc != 2) { fprintf(stderr, "引数の個数が正しくありません\n"); return 1; } printf(".intel_syntax noprefix\n"); printf(".globl main\n"); printf("main:\n"); token = tokenize(argv[1]); // expect_number() について↓ // 今のトークンが数値の場合、トークンを1つ読み進めてその数値を返す。 // それ以外の場合にはエラーを報告する。 printf(" mov rax, %ld\n", expect_number() ); while(!at_eof()) // token->kind が TK_EOF でない限り { if(consume('+')) // bool consume(char op) について↓ // そのトークンが op を表すものでなければ false を返す // token->str[0]が op の時はtokenを次に進める { printf(" add rax, %ld\n", expect_number()); continue; } expect('-'); // void expect(char op) について↓ // そのトークンが op を表すものでなければエラーを出力 // token->str[0]が op の時はtokenを次に進める printf(" sub rax, %ld\n", expect_number()); } printf( " ret\n"); return 0; }
トークンを直接触る関数は consume や expect といったもののみにした。
テストファイルtest.shに以下の行を書き加えて、GitHubにアップロードしてこのステップは完了。
assert 41 " 12 + 34 - 5 "
Unixのプロセスの終了コードは0〜255ということになっているので、テストを書く時は式全体の結果が0〜255に収まるようにする。
$ git add test.sh 9cc.c $ git commit -m "トークナイザを導入" $ git push
calloc関数とは
malloc関数とfree関数の紹介も行う。
malloc関数とcalloc関数は<stdlib.h>に宣言されている。これらの関数はメモリを動的に確保する。
確保したメモリはfree関数で解放しなければならない。
形式 | 戻り値 | 備考 |
---|---|---|
void* malloc(size_t size) | 確保したメモリ領域の先頭アドレス。確保に失敗した場合(その原因はたいていメモリ不足)、ヌルポインタを返す。 | 引数には確保したい領域の大きさを入れる。 |
void* calloc(size_t n, size_t size) | 同上 | 引数nには確保したい領域の要素数を、引数sizeには確保したい領域の1要素あたりの大きさを入れる。確保した領域のメモリは0で埋めてくれる。 |
void free(void *ptr) | malloc, calloc, reallocで確保されたポインタを開放する。また、ヌルポインタを渡しても何も起こらないことが保証されている。それら以外のポインタを渡してはいけない。解放済みのメモリポインタを渡してもいけない。 |
calloc関数ではメモリ領域のポインタの指す先がゼロクリアされる。malloc関数を実行してからmemset関数で埋めるのと同じことが起きる。int型であれば数値が0になるがそれ以外の型では意味が違ってくる。以下は使用例。
#include <stdlib.h> #include <stdio.h> // free関数の代わりにこのマクロでメモリ領域を解放する // 解放後のポインタをヌルポインタにすることによって、 // 誤って2回連続で解放してしまっても何も起こらないことが保証される // (なぜならfreeはヌルポインタを渡しても何も起こらないことが保証されているから) // とはいえまたfree関数を使うと非効率的なので // if文の条件式でヌルポインタのときは何も処理しないことにしている // ただし同じ領域を指すポインタが2つ以上ある場合は、これだけで確実とはいえない #define SAFE_FREE(ptr) if(ptr != NULL){ free(ptr); ptr = NULL; } // malloc関数やcalloc関数でメモリ領域を確保した後は、 // 確保に失敗していないか(即ち戻り値がヌルポインタでないか) // 確かめる必要がある。もし失敗しているのならプログラムを終了する // 十中八九メモリ不足が原因なのでプログラムの続行が困難だろうし、 // それにこのまま続行したら「ヌルポインタの間接参照」をしてしまうだろうから // これは「未定義動作」なので絶対にしてはいけない void* xmalloc(size_t size) { void* p = malloc(size); if(p == NULL) exit(EXIT_FAILURE); return p; } void* xcalloc(size_t n, size_t size) { void* p = calloc(n, size); if(p == NULL) exit(EXIT_FAILURE); return p; } int main() { int n = 10; char *pc = (char *)xmalloc(sizeof(char) * (n+1)); // n文字分(終端のヌル文字の分の確保が必要) int *pn = (int *)xcalloc(n, sizeof(int)); // n個分 double *pd = (double *)xcalloc(n, sizeof(int)); // n個分 for(int i = 0; i < n; i++) { *(pc+i) = i + 65; } for(int i = 0; i < n; i++) { printf("%d番目:文字は%c、整数は%d、浮動小数点数は%lf\n", i+1, *(pc+i), *(pn+i), *(pd+i)); } SAFE_FREE(pc); SAFE_FREE(pn); SAFE_FREE(pd); }
実行結果:
1番目:文字はA、整数は0、浮動小数点数は0.000000 2番目:文字はB、整数は0、浮動小数点数は0.000000 3番目:文字はC、整数は0、浮動小数点数は0.000000 4番目:文字はD、整数は0、浮動小数点数は0.000000 5番目:文字はE、整数は0、浮動小数点数は0.000000 6番目:文字はF、整数は0、浮動小数点数は0.000000 7番目:文字はG、整数は0、浮動小数点数は-92814515680449404682762993892231767752418376643893800353030666635977673095686505197065389597837902323793617910687840191447592198249733150802535531061322482099385922528301143450509381301005901833674907101457935353760197414790103040.000000 8番目:文字はH、整数は0、浮動小数点数は-0.000000 9番目:文字はI、整数は0、浮動小数点数は-594992427546604190142431982475201895362482393844132071030483778462117173325880190427941624155802662644620859256088401385866208754181687271744662862583640563132807611373800234328854102016.000000 10番目:文字はJ、整数は0、浮動小数点数は-0.000000
疑問: なぜ 低レイヤを知りたい人のためのCコンパイラ作成入門のコードには free 関数でメモリを解放する記述がなかったのか?
ロベールのC++教室・第1部を読んだ
「ロベールのC++教室」第1部を読んだ。if文、for文、ポインタの定義の仕方など、自分にとって既知のことは説明を端折りつつ、未知の情報を整理してみる。
目次:
文字列について
string.h をインクルードすれば、文字列を比較したり書き換えたりする関数が使える。
書式 | 説明 |
---|---|
size_t strlen( const char *s ); | 文字列*sの長さ(バイト数)を取得する。終端の'\0'は含まない。 |
char strcpy( char s1, const char *s2); | 文字列s1に文字列s2を'\0'までコピーする。文字列*s1のサイズに注意。 |
char strcat( char s1, const char *s2 ); | 文字列配列s1のうしろにs2を連結する。文字列*s1のサイズに注意 |
int strcmp( const char s1, const char s2 ); | 文字列s1と文字列s2を比較する。等しければ0を返す。文字列を直接==で比較することはできないので注意 |
char strchr( const char s, int c ); | 文字列sの先頭から文字cを探し、最初に見つかった位置をポインタで返す。見つからなければNULLを返す。'\0'も検索可能 |
以下は使用例。半角英数は1文字あたり1バイトである。ひらがなの1文字あたりバイト数は環境により異なるが、つねに2バイト以上である。
// String.cpp #include <iostream> #include <string.h> using namespace std; int main() { char szHello[50] = "こんにちは"; const char* pszCat = "Koneko"; cout << (void *)pszCat << endl; cout << (void *)strchr( pszCat, 'o' ) << endl; cout << strchr( pszCat, 'o' ) << endl; cout << strlen( szHello ) << endl; cout << strcpy( szHello, pszCat ) << endl; cout << szHello << endl; return 0; }
実行結果:
0x5648549f6d25 0x5648549f6d26 oneko 15 Koneko Koneko
ポインタの演算
ポインタに整数を足したり引いたりできる。
// #includeなどは省略 int main(){ char szHello[] = "こんにちは"; int nOdd[] = { 1, 3, 5, }; long int lnPrime[] = { 2, 3, 5, }; cout << szHello << " " << szHello+1 << endl; cout << nOdd << " " << nOdd+1 << endl; cout << lnPrime << " " << lnPrime+1 << endl; }
実行結果
こんにちは ��んにちは 0x7fff70cdbfc4 0x7fff70cdbfc8 0x7fff70cdbfd0 0x7fff70cdbfd8
ポインタp(=&p[0])
に1足すと&p[1]
になる。*(p + 5)
は (p+5)[0]
や p[5]
や (p + 2)[3]
に等しい。
さらにポインタの加減は以下のようにも使える。
// Pointer_plus_minus.cpp #include <iostream> using namespace std; int strlen( const char* s ) { int i; for( i = 0; *(s + i); i++); return i; } void DispLength( const char* s ) { cout << s << "の長さは" << strlen( s ) << "Byteです。" << endl; } int main() { DispLength("やっほー"); DispLength("good morning"); return 0; }
実行結果:
やっほーの長さは12Byteです。 good morningの長さは12Byteです。
for文中の*(s + i)
はs[i]
と同じである。文字列終端のヌルターミネータに着いたらfor文を抜ける。
マクロ
マクロは以下のように定義する。#define マクロ名 差し込むテキスト
マクロを定義しておけば、それ以降のソース中に書いた「マクロ名」は、コンパイル前に「差し込むテキスト」へと置き換えられる。
下のコード Macro1.cpp でマクロの使用例を示した。
// Macro1.cpp #include <iostream> using namespace std; #define NUM 5 #define REP( i, time ) for( i=0; i<time; i++ ) #define FUNC( name ) void name( int x, int y ) FUNC(func); int main() { int i; REPP( i, NUM ) { func( 1, i ); } return 0; } FUNC(func) { cout << x + y << endl; }
実行結果:
1 2 3 4 5
「マクロ名」と「差し込むテキスト」を分けるのは「空白」と「カッコの終わり」である。例えば#define FUNC ( num ) num+1
はマクロ名がFUNCだとみなされるのでエラー。ただしカッコ内の空白は許される。#define FUNC( num ) num+1
次に以下のマクロは何が問題だろうか。#define CAT(name); strcat( name, "さん" );
例えば
if( something == 0 ) CAT( a );
のマクロ展開後のコードは
if( something == 0 ) ; strcat( a );
これでは、if文の条件を満たせばセミコロンだけの白文が実行され、if文とは関係なくstrcat( a )
が実行されてしまう。マクロ名が「カッコの終わり」までとみなされ、「差し込むテキスト」は白文と strcat( a )
の2つであると判断されたのである。
解決するにはセミコロンを消せばよい。#define CAT(name) strcat( name, "さん" )
次に、以下のマクロは何が問題だろうか。#define MUL( n1, n2 ) n1*n2
例えば、このマクロを以下のように使用したとする。
cout << MUL( 1+2, 3+4 );
展開後はこのようになる。
cout << 1+2*3+4;
演算子+
よりも演算子*
が優先される。そのため、値21がほしかったのに値11が出てきてしまう。解決するには「差し込むテキスト」内の変数部分をカッコでくくってあげればよい。#define MUL( n1, n2 ) ((n1)*(n2))
最後に「差し込むテキスト」が複数行にわたる場合。
#define SWAP( n1, n2 ) tmp = n1; \
n1 = n2; \
n2 = tmp;
行末にバックスラッシュを入れると改行ができる。それはよいとして、上のマクロ定義の何が問題だろうか。以下のマクロ使用例を見てみよう。
if( something == 0 ) SWAP( a,b );
これをマクロ展開する。
if( something == 0 ) tmp = a; a = b; b = tmp;
このようになってしまう。では差し込むテキストを波括弧でくくるとどうなるだろうか。
#define SWAP( n1, n2 ) {tmp = n1; \
n1 = n2; \
n2 = tmp;}
この場合、
if( something == 0 ) SWAP( a, b ); else // 何らかの処理
をマクロ展開すると
if( something == 0 ) { tmp = a; a = b; b = tmp; }; else // 何らかの処理
これではセミコロンの手前でif文が終わってしまい、else
の部分でエラーが出てしまう。この問題を解決するには差し込むテキストを do〜while 文でくくればよい。
#define SWAP( n1, n2 ) do{tmp = n1; \
n1 = n2; \
n2 = tmp;}while(1)
するとマクロ展開後のコードは以下のようになる。
if( something == 0 ) do{ tmp = a; a = b; b = tmp; }while(0); else // 何らかの処理
このdo〜while文の中身は1回だけ実行される。
フラグ処理〜bit演算の使用例〜
以下のコードはbit演算を使ってフラグ処理する例。文字列をコピーする関数strcpy_ex
を作った。フラグが
- 00ならそのままコピー
- 01なら1文字毎にスペースを入れてコピー
- 10なら「x」と「X」以外をコピー
- 11なら「x」と「X」以外を、1文字毎にスペースを入れてコピー
#include <iostream> using namespace std; #define BIT(num) ((unsigned int)1<<(num)) #define SCEX_COPY 0 #define SCEX_SPACE BIT(0) #define SCEX_TRIM_X BIT(1) void strcpy_ex( char* pszDest, const char* pszSource, unsigned int flags) { int i,j; for( i=0, j=0; *(pszSource+j); i++, j++ ) { // x,X をトリミングするなら、今指している文字が x,X の時は飛ばして次のループに行く if( (flags & SCEX_TRIM_X) && (*(pszSource+j) == 'x' || *(pszSource+j) == 'X') ) { i--; continue; } // スペース処理をするならスペースを入れて、しないならスペースを入れないでコピーする if( flags & SCEX_SPACE ) { *(pszDest+2*i) = *(pszSource+j); *(pszDest+2*i+1) = ' '; }else *(pszDest+i) = *( pszSource + j ); } } void Disp( char* pszDest, const char* pszSource, unsigned int flags ) { strcpy_ex( pszDest, pszSource, flags ); cout << pszDest << endl; } int main() { const char pszSource1[512] = "hello"; const char pszSource2[512] = "xxXxxXGoXxxodxxMxxoXrning.XX"; char pszDest1[512], pszDest2[512]; Disp( pszDest1, pszSource1, SCEX_SPACE ); Disp( pszDest2, pszSource2, SCEX_TRIM_X | SCEX_SPACE ); return 0; }
実行結果
h e l l o G o o d M o r n i n g .
ファイル
ファイルを開いて読み込んだり、書き込んだりできる。関数fopenでファイルを開いたら、その情報をFILE構造体のポインタに入れておく。ファイルは開いたら関数fcloseで閉じなければならない。
// FileSample.cpp #include <stdio.h> int main() { FILE *pFile; char buffer[128]; // FileSample.cpp と同じディレクトリにあるファイルtest.txtを // 書き込み専用で開く pFile = fopen( "test.txt", "w" ); fputs( "ファイルに\n書き込むよ\n", pFile ); fclose( pFile ); // 読み取り専用で開く pFile = fopen( "test.txt", "r" ); // 1行読み出し fgets( buffer, 128, pFile ); // putsは最後のヌル文字を勝手に改行文字に変えて表示する puts( buffer ); fgets( buffer, 128, pFile ); puts( buffer ); fclose( pFile ); return 0; }
実行結果:
ファイルに 書き込むよ
ファイルを開く時、関数fopenの第二引数にはファイルをどのモードで開くかを指定する。
モード | ファイルがある場合 | ファイルがない場合 |
---|---|---|
r | 読み取りモードで開く。 | エラー。関数fopenはNULLを返す。 |
w | ファイルの内容がなくなり、書き込みモードで開く。 | 新しくファイルを作成して書き込みモードで開く。 |
a | ファイルの終端に書き込むモード(追加モード)で開く。 | 新しくファイルを作成して書き込みモードで開く。 |
r+ | 読み取りと書き込み両方のモードで開く。 | エラー。関数fopenはNULLを返す。 |
w+ | ファイルの内容がなくなり、読み取りと書き込み両方のモードで開く。 | 新しくファイルを作成して読み取りと書き込み両方のモードで開く。 |
a+ | 読み取りと追加両方のモードで開く。データを書き込む前にEOFマーカー(0X1A)が削除され、書き込みが完了すると終端にEOFマーカーが追加される。 | 新しくファイルを作成して読み取りと追加両方のモードで開く。 |
モードの最後にbをつけるとバイナリモードで開く。bをつけないとテキストモードで開く。その違いは、改行文字の扱い方である。改行文字は、Windowsでは'\r\n'
、Macでは'\r'
、UNIX系OSでは'\n'
である。テキストモードでは、それぞれの環境でファイルを扱うために、勝手に改行文字を変換してくれる。例えばfputs( "aaa\n", pFile )
だとWindows環境ならば"aaa\r\n"
と書き込んでくれる。しかし、バイナリモードでは何環境であろうと、そのまま"aaa\n"
と書き込む。
さて、ファイルの入出力に使う関数をまとめてみよう。
〜出力編〜
定義 | 動作 | 返り値 |
---|---|---|
int fputc( int c, FILE* pFile ) | 文字cをunsigned char 型に変換してファイルに書き込む。 |
正常に終了すれば書き込まれた文字を返す。エラーの時はEOFを返す。 |
int fputs( const char str, FILE pFile) | 文字列strをファイルに出力する。関数puts に似ているけれど違う点がある。それは関数puts が最後に改行文字を出力するのに対し、関数fputs はそうしないことだ。 |
出力エラーがあればEOF、正常に終了すれば0以上の値。 |
int fprintf( FILE pFile, const char format, ... ) | 関数printf のようにして、文字列をファイルに書き込む。 |
正常に終了すれば、出力した文字数を返す。エラーの時は負の値を返す。 |
size_t fwrite( const void pArray, size_t size, size_t n, FILE pFile) | ポインタpArrayの指す文字列をsize*nバイトだけファイルに書き込む。 | 正常終了すれば、実際に書き込まれた要素数。エラーの時は引数nよりも小さい値を返す。 |
〜入力編〜
定義 | 動作 | 返り値 |
---|---|---|
int fgetc( int c, FILE* pFile ) | ファイルpFileから1文字読み取る。その際、ファイル位置指示子を進める。getcマクロの関数バージョンである。 | 読み込んだ文字をunsigned charからintに変換して返却する。 |
char fgets( char str, int n FILE* pFile ) | ファイルpFileから最大n-1文字読み取って文字列strに格納する。途中でファイル終端に達するか改行が見つかれば、読み取りを終了する。改行が見つかった場合は、改行文字を文字列strに格納する。n文字目に'\0'を格納する。 | 読み取った文字列strを返す。読み取りエラーや一文字も読み取れなかった時はNULLを返す。 |
int fscanf( FILE pFile, const char format, ... ) | ファイルpFileからscanf 関数のように文字列を読み取る。後述の例を参照。 |
変換を1つも行えないまま入力エラーが起きた場合は、EOFを返す。その他の場合には、正常に代入を行えた個数を返す。 |
size_t fread( void pArray, size_t size, size_t n, FILE pFile) | ファイルpFileからsize*nバイト分の文字列を読み込んで、配列pArrayに格納する。 | 実際に読み込まれたバイト数÷第2引数size。エラーが発生したり、ファイルの末尾まで到達したりした際には、それよりも小さい値が返される。 |
ではいくつか例をやってみよう。
例1:ファイル"animals.txt"から特定の位置にある文字列を読み込んで出力する。
// fscanf.cpp #include <stdio.h> #include <stdlib.h> int main() { FILE* pFile; pFile = fopen( "animals.txt", "r" ); if( pFile == NULL ) return EXIT_FAILURE; char str[128]; while(1) { // %*sとはその文字列を無視するという意味 if( EOF == fscanf( pFile, "%*s %*s %s", str ) ) break; printf( "%s\n", str ); } return 0; }
animals.txtの内容:
I like cat You like dog She likes rabbit He likes bird
出力結果:
cat dog rabbit bird
例2:fread関数で読み出して別ファイルにfwrite関数で書き込む。
// fwrite_and_fread.cpp #include <stdio.h> #include <stdlib.h> int main() { FILE *pFile1, *pFile2; pFile1 = fopen( "sample1.txt", "rb" ); pFile2 = fopen( "sample2.txt", "wb" ); if( pFile1 == NULL ) return EXIT_FAILURE; if( pFile2 == NULL ) return EXIT_FAILURE; char buffer[512]; int nDivisor = 8; // freadが返すのは、読み取った全バイト数(終端のヌル文字含む)を第2引数の数で割ったもの // 掛け算した値が配列サイズを越えさえしなければ第二、第三引数の値はメチャクチャでも良い int nLength = nDivisor * (int)fread( buffer, nDivisor, 512/nDivisor, pFile1 ); fprintf( pFile2, "終端のヌルを入れた文字数は「%d」です\n", nLength ); if( nLength > 512 ) nLength = 512; fwrite( buffer, 1, nLength, pFile2 ); fclose(pFile1); fclose(pFile2); return 0; }
sample1.txt
の内容はあらかじめ用意しておこう。sample1.txt
の内容:
12345 678 9
プログラム実行後のsample2.txt
の内容:
終端のヌルを入れた文字数は「12」です 12345 678 9
sizeof演算子
sizeof 変数
(またはsizeof(型名)
)でその変数が確保しているメモリサイズを、sizeof(型名)
でその型のサイズを
バイト単位で返す。
// うちの環境では、ひらがな1文字3バイト // さらに終端のヌル文字ぶんの1バイトも含めて数字を返す cout << sizeof "あいうえお" << endl; // 16 // sizeof( aiu ) はconst char型ポインタのサイズ。 const char* aiu = "あいうえお"; cout << sizeof aiu << endl; // うちでは8
さて、引数に配列を渡すと、配列の要素数を返す関数がほしいと思ったとする。配列のサイズを型のサイズで割ればよいだろう。しかし、△型配列arrayを関数の引数に渡すと、sizeof(array)は△*型ポインタのサイズになってしまう 。なので配列のサイズが取得できなくなる。よって上記で望んだ関数は作れない。その代わりに、配列の要素数を取得するマクロを定義することはできる。
#include <iostream> using namespace std; // 配列の要素数を取得するマクロ #define ELEM(array) (sizeof(array) / sizeof *(array) ) void Func( long int* array ) { // 8 8 1 // 関数を通してしまうと正しい結果が得られなくなる例 cout << sizeof *(array) << " " << sizeof array << " " << ELEM(array) << endl; // 配列を引数として渡さなければ望んだ結果が得られる例 long int array2[7]; // 8 56 7 cout << sizeof *(array2) << " " << sizeof array2 << " " << ELEM(array2) << endl; } int main() { long int test[7]; // 8 56 7 cout << sizeof *(test) << " " << sizeof test << " " << ELEM(test) << endl; Func(test); return 0; }
構造体
構造体の基本的な使い方を以下のコードで示した。
#include <iostream> using namespace std; #define ELEM(array) (sizeof (array) / sizeof *(array)) struct SHuman { char szName[16]; int nAge; char szHobby[16]; }; /* 構造体は参照渡しやポインタ渡しをする 値渡しすると、どんなに構造体のサイズが巨大であっても メモリ上にコピーされてしまうので注意 */ void DispHuman( const SHuman* phuman ) { // phuman->szNameと(*phuman).szNameは同じ // *phuman.szName とすると優先順位上 *(phuman.szName)になってしまう cout << "私の名前は" << phuman->szName << "です。" "年齢は" << phuman->nAge << "歳で" "趣味は" << phuman->szHobby << "です。"<<endl; } void LastYear( const SHuman& human ) { cout << human.szName << "さんは1年前" << human.nAge - 1 << "歳でした。" << endl; } int main() { SHuman friends[] = { { "Taro", 30, "釣り" }, { "John", 20, "読書" } }; SHuman me = { "naomeo", 1, "散歩" }; int i; for( i = 0; i < ELEM( friends ); i++ ) DispHuman( &friends[i] ); DispHuman( &me ); LastYear( me ); return 0; }
実行結果:
私の名前はTaroです。年齢は30歳で趣味は釣りです。 私の名前はJohnです。年齢は20歳で趣味は読書です。 私の名前はnaomeoです。年齢は1歳で趣味は散歩です。 naomeoさんは1年前0歳でした。
オーバーロード
以下の関数プロトタイプ宣言を見てみよう。
void func(int x, int y, int z); void func(int x, int y); int func(char a);
こんな風に同じ名前の違う関数を作ることをオーバーロードと呼ぶ。
また、仮引数によく使う値を代入しておくこともできる(その引数をデフォルト引数と呼ぶ)。オーバーロードやデフォルト引数にはルールがいくつかある。
1 : オーバーロードを行うには、引数の型が異なっていなければならない。戻り値の型が違うだけではエラーになる。
// エラー void func( int x ); int func( int x );
2 : デフォルト引数はこんな風に使う。
// ここではプロトタイプ宣言しか書いていない // 関数定義するときも同様に書く void func(int x, int y = 0, int z = 0); int func(char a = 'X');
途中の引数を省略することはできない:
// インクルードファイル・関数定義省略 int main() { func( x,, z = 3); // エラー! return 0; }
3 : オーバーロードとデフォルト引数を併用する時は、どの関数を呼び出しているかを区別ができるか確かめよう。
void func( int x, int y = 0, int z = 0 ); void func( int x, int y );
例えば上のような関数があったとして、
int main() { func( 1, 0 ); // どちらのfuncか区別できないのでエラー! func( 3 ); // 区別できるので問題なし func( 1, 0, 0 ); // 区別できるので問題なし return 0; }
呼び出す時の引数のとり方によって、どちらのfunc関数から呼び出したのか分からないことがある。しかもfunc( int x, int y )
の方は、呼び出すことができない。このようなことが起きないように気をつけよう。
4 : デフォルト引数には静的なデータしか使えない。静的なデータとは、定数、もしくはプログラム実行前に位置が決まっているデータのことである。つまり定数、外部変数、関数のことである。
void func( int x, int y = x ); // エラー!
内部変数はデフォルト引数には使えない。
#include <stdio.h> int main() { int a = 10; } // エラー! void func( int x, int y = a ){ }
ただし内部変数の定義にstaticをつけて静的にすれば大丈夫である。
静的内部変数
静的な内部変数を定義してみよう。
static int n; static char c;
普通の内部変数と何が違うのだろうか? 静的変数の特徴を挙げてみる。
- 一度しか初期化されない
- 初期値が与えられなければ0で初期化される
- 関数を抜けても値がそのアドレス上に保持される
- アドレスは常に一定である
それらを確かめてみよう。
// static_variable.cpp #include <iostream> using namespace std; #include <memory.h> // 1.一度しか初期化されない void OnceInit( int x ) { // 変数numが初期化されるのは最初だけ。2回目以降は値を渡しても何もしない static int num = x; num++; cout << num << endl; } // 2. 初期値が与えられなければ0で初期化される void InitZero() { static int num1, num2, num3, num4; /* ここで「代入」をしてしまうと関数が実行される度にnum0は5になる。 なので静的変数であることによる利点がなくなる。 なぜなら静的変数は「オブジェクトを参照のように扱いたいけど、 関数の外からは変更できないようにしたい」時に使うからである。 */ static int num0; num0 = 5; // 代入 cout << "num0 = " << num0 << endl; cout << "num1 = " << num1 << endl; cout << "num2 = " << num2 << endl; cout << "num3 = " << num3 << endl; cout << "num4 = " << num4 << endl; } // 3.関数を抜けても値がそのアドレス上に保持される void HoldValue( int*& ps, int*& p ) { static int a = 13; int b = 13; ps = &a; p = &b; } // 4.アドレスは常に一定である void CheckAddress_Sub1() { static int sa; int a; cout << "&sa = " << &sa << endl; cout << "&a = " << &a << endl; } void CheckAddress_Sub2() { cout << "in Sub2" << endl; CheckAddress_Sub1(); } void CheckAddress() { CheckAddress_Sub1(); CheckAddress_Sub2(); CheckAddress_Sub1(); } int main() { // 1.一度しか初期化されない OnceInit(2); // 3 OnceInit(5); // 4 OnceInit(10); // 5 // 2. 初期値が与えられなければ0で初期化される InitZero(); // 3.関数を抜けても値がそのアドレス上に保持される int *ps, *p; HoldValue( ps, p ); cout << "*ps = " << *ps << endl; cout << "*p = " << *p << endl; // 4.アドレスは常に一定である CheckAddress(); return 0; }
実行結果:
3 4 5 num0 = 5 num1 = 0 num2 = 0 num3 = 0 num4 = 0 *ps = 13 *p = 32765 &sa = 0x55833c02715c &a = 0x7ffd11396004 in Sub2 &sa = 0x55833c02715c &a = 0x7ffd11395ff4 &sa = 0x55833c02715c &a = 0x7ffd11396004
リンケージ
複数ファイルを実行するときに必要になるリンケージという概念について。
変数・関数が他のファイルでも使えるとき、その変数・関数は「外部リンケージを持つ」と言う。他のファイルでは使えないとき、その変数・関数は「内部リンケージを持つ」と言う。
まず外部リンケージについて。グローバル変数・関数は普通に定義しただけで外部リンケージを持つ。1つのファイルXにそれらの実体を書き、他のファイルYから呼び出したいとする。ファイルYには、関数のプロトタイプ宣言やグローバル変数のextern宣言を書いておく必要がある。ここで、externを書かなければグローバル変数の二重定義になってしまうので注意。
次に内部リンケージについて。グローバル変数・関数をstatic宣言するとそれらは内部リンケージを持つ。
// linkage1.cpp #include <iostream> using namespace std; // linkage2.cpp 内のCall関数にはstatic記憶クラス指定子がついているので // 内部リンケージを持っている。よって、 // linkage1.cpp からは呼び出せない。 void Disp( int n ); static void Func1( int n ); extern void Call( int n ); // static記憶クラス指定子がついているので // linkage2.cppでは使えない static int sn = -7; int main() { extern int num; Disp( num ); // 37 num = 36; Disp( num ); // 36 Disp( sn ); // -7 // linkage1.cpp内のFunc1関数が呼ばれる Func1( num ); // 36 // linkage2.cpp内のFunc1関数が(Call関数を通して)呼ばれる Call( num ); // 360 return 0; }6; Disp( num ); // 36 Disp( sn ); // -7 // linkage1.cpp内のFunc1関数が呼ばれる Func1( num ); // 36 // linkage2.cpp内のFunc1関数が(Call関数を通して)呼ばれる Call( num ); // 360 return 0; } static void Func1( int n ) { cout << "extern1.cpp内" << endl; cout << n << endl; }
// linkage2.cpp #include <iostream> using namespace std; int num = 37; // 実体 void Disp( int n ) // 実体 { cout << n << endl; } static void Func1( int n ) // 実体 { cout << "linkage2.cpp内" << endl; cout << n*10 << endl; } void Call( int n ) // 実体 { Func1( n ); }
実行結果:
37 36 -7 linkage1.cpp内 36 linkage2.cpp内 360
複数ファイルをコンパイルするときは、「main関数は1つでなければならない」ことに注意。
インライン関数
関数を呼ぶ時は、関数のアドレスまで移動する。この移動にかかる時間をなくしたい。
例えば下のコードでは、20000回もhello関数に移動している。
// inline_hello.cpp #include <iostream> using namespace std; void hello() { cout << "Hello!" << endl; } int main() { for( i=0; i<20000; i++ ) hello(); return 0; }
そこでhello関数をインラインにする。
inline void hello() { cout << "Hello!" << endl; }
するとhello関数を呼び出している場所に、hello関数の中身が埋め込まれるようになる(これをインライン展開と呼ぶ)。for文の部分を見てみよう。
for( i=0; i<20000; i++ ) cout << "Hello!" << endl;
ただし、展開するとプログラムサイズが肥大する場合はインライン展開されない。
また、hello関数のアドレスを取得する文をプログラム中に書いていてもインライン展開されない。インライン展開したら普通の関数ではなくなってしまうからである。
インライン関数をインライン展開するかどうかの判断はコンパイラが行う。
二重定義防止
例えばこんなファイル達をコンパイルしようとする。
// A.h class CA { int a; };
// B.h #include "A.h" class CB : public CA { int b; };
// main.cpp #include "A.h" #include "B.h" int main {}
うちの環境はコンパイラがgccのver.7.5.0でOSがUbuntuなので、端末でコンパイルの命令をこんな風に打ってみる。
$ g++ -o main main.cpp A.h B.h
するとこんなエラーが出る。
In file included from B.h:2:0,from main.cpp:3:
A.h:2:7:
error:
redefinition of ‘class CA’
class
CA
^~
クラスCA
が2回も定義されていますよというエラー。
これを避けるにはインクルードガード(二重定義防止)を施す。
// A.h #ifndef __A_H__INCLUDED__ #define __A_H__INCLUDED__ class CA { int a; }; #endif
// B.h #ifndef __B_H__INCLUDED__ #define __B_H__INCLUDED__ #include "A.h" class CB : public CA { int b; }; #endif
#ifndef <名前>
は、<名前>
が定義されていなければ#endif
までの文を実行する、という意味。#ifndef <名前>
の直後に#define <名前>
と書いて空の定義をする(<名前>という名前だけが定義をされる)。
ヘッダファイルに上記の処理を施しておけば、初めて呼び出された時だけ<名前>
が定義され、2回目以降は#ifndef
文と#endif
文の間にあるコードは無視される。
ヘッダファイル展開後のmain.cpp
を見てみよう。
// main.cpp // A.h #ifndef __A_H__INCLUDED__ #define __A_H__INCLUDED__ class CA { int a; }; #endif // B.h #ifndef __B_H__INCLUDED__ #define __B_H__INCLUDED__ // A.h #ifndef __A_H__INCLUDED__ // ここにあった処理がスキップされる #endif class CB : public CA { int b; }; #endif int main {}
1回インクルードしたA.h
が2回めではインクルードされていない。
列挙型
// enumerator.cpp #include <iostream> using namespace std; enum ECmp { LESSTHAN = 0, EQUALTO = 1, GREATERTHAN = 2, }; ECmp Cmp( int n ) { return (n>100) ? GREATERTHAN : (n<100) ? LESSTHAN : EQUALTO; } bool Cmp() { int n; cout << "整数を入力せよ" << endl; cin >> n; // −1が入力されたら偽を返す if( n == -1 ) return false; cout << "あなたが入力した数字は100"; switch( Cmp(n) ) { case LESSTHAN: cout << "より小さい" << endl; break; case GREATERTHAN: cout << "より大きい" << endl; break; case EQUALTO: cout << "に等しい" << endl; break; } return true; } int main() { while( Cmp() ); }
上のコードでは、enumを使ってECmpという型を作った。ECmpの波カッコ内にあるLESSTHANなどの名前を「列挙子」と呼ぶ。ECmpの定義は以下のように書き換えても一緒。
enum ECmp
{
LESSTHAN, EQUALTO, GREATERTHAN,
};
列挙子の値を何も書かなければ、最初の列挙子から順に0,1,2……となる。また、例えば
enum X { A = -3, B, C = 10, D, DEFAULT = 11, };
とすればBは-2、Dは11になる。同じ数値を持つ列挙子があってもよいが、区別がつかなくなるので注意。
列挙型オブジェクトに値を代入する時は、列挙子の名前か、キャストしたint型の数を入れる。
int main() { ECmp ec; ec = LESSTHAN; ec = (ECmp)0; //ec = 0; // エラー! ec = (ECmp)10; // エラーにならないので注意 }
ECmpの列挙子の値は0,1,2だがそれ以外の整数も代入できてしまうので注意が必要。
C++に入門した
サイトプログラムを書こう!のページの「C++入門」を読んだのでその内容を自分用にメモしてみる。
継承
以下のコードは継承はこんな風にやるという例。この例ではNekoが親クラスでShisankaNekoが子クラス。
// neko.cpp #include <iostream> #include <string> using namespace std; class Neko { string name; public: Neko(string s) : name(s){} void naku() const{ cout<<"にゃあ。我が名は"<<name<<"である"<<endl; } }; // 資産の利子(年2%)だけで食べている猫 class ShisankaNeko : public Neko { int sisan; public: ShisankaNeko( string n, int s ) : Neko(n), sisan(s){} int get_salary() const{ return sisan * 2/100; } }; int main(){ cout<<"ドラ猫の名前を入力して下さい"<<endl; string y; cin>>y; Neko dora(y); dora.naku(); cout<<"資産家猫の資産を入力して下さい(半角数字)"<<endl; int x; cin>>x; cout<<"資産家猫の名前を入力して下さい"<<endl; cin>>y; ShisankaNeko hugou(y, x); hugou.naku(); cout<<hugou.get_salary()<<endl; }
実行結果:
ドラ猫の名前を入力して下さい ごん にゃあ。我が名はごんである 資産家猫の資産を入力して下さい(半角数字) 1000000 資産家猫の名前を入力して下さい かねも にゃあ。我が名はかねもである 20000
デストラクタ
インスタンスはそれが作成されたカッコ{}を抜けると破棄される。 また、子クラスのインスタンスを作成しようとすると、まず親クラスのインスタンスが作成されてから子クラスのインスタンスが作成される。
//dest_sample2.cpp #include <iostream> using namespace std; class Nanika { int datum; public: //コンストラクタ Nanika(int x) : datum(x){ cout << "Nanikaのインスタンス" << datum << "が生成されました。" << endl; } void func() const{ cout << "Nanikaのインスタンス" << datum << "のfuncが呼ばれました。" <<endl; } //datumの値を戻す関数をつけておく int get_datum() const { return datum; } //デストラクタ ~Nanika(){ cout << "Nanikaのインスタンス" << datum << "が消滅しました。" << endl; } }; //Nanikaの派生クラス class NanikaNoKo : public Nanika { public: //コンストラクタ NanikaNoKo(int x) : Nanika(x){ cout << "NanikaNoKoのインスタンス" << get_datum() << "が生成されました。" << endl; } //デストラクタ ~NanikaNoKo(){ cout << "NanikaNoKoのインスタンス" << get_datum() << "が消滅しました。" << endl; } void func() const{ cout << "NanikaNoKoのインスタンス" << get_datum() << "funcが呼ばれました。" << endl; } }; //NanikaNoKoの派生クラス class NanikaNoMago : public NanikaNoKo { public: //コンストラクタ NanikaNoMago(int x):NanikaNoKo(x){ cout << "NanikaNoMagoのインスタンス" <<get_datum() << "が生成されました。" << endl; } ~NanikaNoMago(){ cout << "NanikaNoMagoのインスタンス" << get_datum() << "が消滅しました。" << endl; } void func() const{ cout << "NanikaNoMagoのインスタンス" << get_datum() << "funcが呼ばれました。" << endl; } }; int main() { // クラスNanikaのオブジェクトOneはif文の{}内でのみ有効。{}を抜ける時にデストラクタが呼び出されて破棄される。 int x = 1; if( x==1 ){ Nanika One(1); One.func(); } // 子クラスのインスタンスは親→子の順に生成される。孫にあたるクラスでも同様。 // デストラクタは子→親の順に呼び出される Nanika Two(2); NanikaNoKo Three(3); NanikaNoMago Four(4); Two.func(); Three.func(); Four.func(); }
実行結果は
Nanikaのインスタンス1が生成されました。 Nanikaのインスタンス1のfuncが呼ばれました。 Nanikaのインスタンス1が消滅しました。 Nanikaのインスタンス2が生成されました。 Nanikaのインスタンス3が生成されました。 NanikaNoKoのインスタンス3が生成されました。 Nanikaのインスタンス4が生成されました。 NanikaNoKoのインスタンス4が生成されました。 NanikaNoMagoのインスタンス4が生成されました。 Nanikaのインスタンス2のfuncが呼ばれました。 NanikaNoKoのインスタンス3funcが呼ばれました。 NanikaNoMagoのインスタンス4funcが呼ばれました。 NanikaNoMagoのインスタンス4が消滅しました。 NanikaNoKoのインスタンス4が消滅しました。 Nanikaのインスタンス4が消滅しました。 NanikaNoKoのインスタンス3が消滅しました。 Nanikaのインスタンス3が消滅しました。 Nanikaのインスタンス2が消滅しました。
ポインタ
継承の節のコードneko.cppを利用する。クラスNekoに引数なしのコンストラクタを追加し、main関数の中身はすべて書き換えた。 ポインタの説明はmain関数の中で行う。
// neko_pointer.cpp #include <iostream> #include <string> using namespace std; class Neko { string name; public: Neko(string s) : name(s){} Neko(){} // 引数を取らないコンストラクタを追記した(後述) void naku() const{ cout<<"にゃあ。我が名は"<<name<<"である"<<endl; } }; // 資産の利子(年2%)だけで食べている猫 class ShisankaNeko : public Neko { int sisan; public: ShisankaNeko( string n, int s ) : Neko(n), sisan(s){} int get_salary() const{ return sisan * 2/100; } }; int main(){ // 通常のポインタの使い方 int x = 10; int *p; // int* p;でもいい p = &x; // xのアドレスがpに代入される cout<<"変数xのアドレスは"<<p<<endl; cout<<"変数xに格納されている値は"<<*p<<endl; // クラス型のポインタの使い方(上と同様) Neko *pcat; Neko dora("ボス"); pcat = &dora; pcat->naku(); // dora.naku();や(*pcat).naku();と一緒 // クラス型のポインタ〜newとdeleteを使う方法〜 Neko *p0; p0 = new Neko("タマ"); // メンバ変数nameの値が"タマ"のNeko型オブジェクトを生成し、そのアドレスをp0に代入 p0->naku(); delete p0; // さらに配列を扱う方法 // このためには引数をとらないコンストラクタが必要なので作ろう cout<<"猫の頭数を入力(半角数字)"<<endl; int tousu; cin>>tousu; p0 = new Neko[tousu]; // 引数なしコンストラクタNekoが呼び出される for(int i=0; i<tousu; i++) p0[i].naku(); // p0[i]はi番目のオブジェクトである(アドレスではない!) }
実行結果:
変数xのアドレスは0x7ffd172ad58c 変数xに格納されている値は10 にゃあ。我が名はボスである にゃあ。我が名はタマである 猫の頭数を入力(半角数字) 3 1番目の猫の名前を入力 くろ 2番目の猫の名前を入力 茶トラ 3番目の猫の名前を入力 ぶち にゃあ。我が名はくろである にゃあ。我が名は茶トラである にゃあ。我が名はぶちである
仮想関数
またneko.cppをベースに考えてみた。今回はクラスにメンバ関数を追加するなどいろいろ変更した。
// neko_kasou.cpp #include <iostream> #include <string> using namespace std; class Neko { string name; public: Neko(string s) : name(s){} // 仮想デストラクタ virtual ~Neko(){} // virtualを追加(仮想関数) void virtual naku(){ cout<<"にゃあ。我が名は"<<name<<"である"<<endl; } //関数get_nameを追加 string get_name(){ return name; } }; // 資産の利子(年2%)だけで食べている猫 class ShisankaNeko : public Neko { int sisan; public: ShisankaNeko( string n, int s ) : Neko(n), sisan(s){} int get_salary() const{ return sisan * 2/100; } //関数nakuを追加 void naku(); }; // 子クラスからも親クラスNekoのプライベートメンバ変数にはアクセスできないので、get_name関数で呼び出した void ShisankaNeko::naku(){ cout<<"にゃあ。私は資産家猫。我が名は"<<get_name()<<"である"<<endl; } int main(){ Neko *p1, *p2; // Neko* p1, p2;としてしまうとp2がポインタにならない p1 = new Neko("タマ"); p2 = new ShisankaNeko("ミケ",100); p1->naku(); p2->naku(); delete p1; delete p2; }
実行結果は
にゃあ。我が名はタマである にゃあ。私は資産家猫。我が名はミケである
クラスShisankaNeko内にnaku関数を定義した(親クラスと子クラスで同名の関数があっても良い)。クラスNeko内のnaku関数の定義部分に「virtual」と追記した。このような関数を仮想関数という。 ここでもしvirtualがなければ、実行結果は
にゃあ。我が名はタマである にゃあ。我が名はミケである
つまりvirtualがなければ親クラスのメンバ関数が呼び出されるのだ。 デストラクタも同様でvirtualをつけないとdelete p2;したときにp2の指しているNekoオブジェクトの部分だけが破棄されてShisankaNekoの一部が残ってしまう。なのでデストラクタも仮想関数にする必要がある。
演算子のオーバーロード
#include<iostream> using namespace std; class Thing { int data1, data2; public: Thing( int d1, int d2 ) : data1(d1), data2(d2){} int get_data1() const{ return data1; } int get_data2() const{ return data2; } }; // Thing型変数とint型変数間の演算子*を定義(+,-,/なども自分で定義できる) // データaやデータbを変更するつもりはないのでconstをつけよう // &をつけて参照を使った方が効率が良いのでつけた Thing operator*(const Thing& a, const int& b){ Thing temp( a.get_data1()*b, a.get_data2()*b ); return temp; } int main(){ Thing x(100, 200); int q = 5; Thing y = x * q; cout<<y.get_data1()<<endl; cout<<y.get_data2()<<endl; }
実行結果
500 1000
上でもし参照を使わなかったら、演算子を呼び出した時にxとqのコピーが改めて生成される。これがThing型オブジェクトではなく画像データなどであれば、時間がとても増える。&をつけて参照渡しするとxとqそのものを使って演算子の関数が処理を行う。 以下、参照のわかりやすい例。
#include<iostream> using namespace std; int func(int a){ a += 5; } int func_sansyo(int& a){ a += 5; } int main(){ // 参照なし版 int x = 3; func(x); cout<<"func(x)した後のxの値:"<<x<<endl; // 参照あり版 int y = 3; func_sansyo(y); cout<<"func_sansyo(y)した後のyの値:"<<y<<endl; }
実行結果
func(x)した後のxの値:3 func_sansyo(y)した後のyの値:8
コピーコンストラクタ/代入演算子
// neko_betu.cpp // 実はこのコードには問題がある #include <iostream> #include <string> using namespace std; class Neko { string name; public: Neko(string s) : name(s){} void naku() const{ cout<<"にゃあ。我が名は"<<name<<"である"<<endl; } // 名前を返す関数を追加 string get_name() const{ return name; } }; // クラスNekoとは別のクラスBetuを作成 class Betu { Neko* a; public: // ポインタの0はどこも指さないという意味 Betu() : a(0){} ~Betu(){ delete a; } void show() const; void input(); }; void Betu::show() const{ // aがどこも指していなければ終了 if( a==0 ) return; cout<<"名前:"<<a->get_name()<<endl; } void Betu::input(){ string s; // これまで持っていたNekoオブジェクトaを破棄 delete a; cout<<"名前を入力して下さい"<<endl; cin>>s; a = new Neko(s); } int main(){ Betu one; one.input(); one.show(); }
実行結果
名前を入力して下さい ごん 名前:ごん
上のコードではNeko型ポインタをメンバ変数に持つクラスBetuを定義した。 mainの中にはNeko型オブジェクトを破棄するdelete文がないが、Betuオブジェクトのデストラクタで破棄するようにしてあるのでよい。 これで一見なんの問題もないようにみえるけれど、実はBetuオブジェクトのコピー&代入に問題がある。
int main(){ Betu one; one.input(); Betu two = one; // コピー Betu three; three = one; // 代入 }
上のコードでtwoやthreeは、oneと同じNeko型ポインタをメンバに持っている。 ここで先にoneが破棄されたらoneの指すNeko型ポインタも一緒に破棄される。しかしtwoやthreeはそれに気づけない。 するとtwoとthreeは無いものを指していることになる(?)。 ここでわざとエラーを起こしてみる。
int main(){ Betu two; two.input(); int x=1; if( x==1 ){ Betu one; one.input(); two = one; // 代入 } two.show(); }
実行結果
名前を入力して下さい ごん 名前を入力して下さい たろう terminate called after throwing an instance of 'std::logic_error' what(): basic_string::_M_construct null not valid 中止 (コアダンプ)
オブジェクトoneはif文のカッコが終わると破棄されてしまうので、twoは指す場所がなくなる。 この問題を解決するために「Betuオブジェクトのコピーのやり方」と「Betuオブジェクトの代入のやり方」を定義してやる必要がある。 それがコピーコンストラクタと代入演算子だ。
// neko_betu2.cpp #include <iostream> #include <string> using namespace std; class Neko { string name; public: Neko(string s) : name(s){} void naku() const{ cout<<"にゃあ。我が名は"<<name<<"である"<<endl; } string get_name() const{ return name; } }; class Betu { Neko* a; public: Betu() : a(0){} ~Betu(){ delete a; } void show() const; void input(); //コピーコンストラクタ Betu(const Betu&); //宣言なので仮引数は省略した //代入演算子 Betu& operator=(const Betu&); //宣言なので仮引数は省略した //新しく定義 string get_name() const{ return a->get_name(); } }; //コピーコンストラクタ Betu::Betu(const Betu& x){ // xと同じ猫名nameを持つ新しいNekoオブジェクトを生成する a = new Neko(x.get_name()); } //代入演算子 Betu& Betu::operator=(const Betu& x){ /* もし自分自身のアドレスthis(さっきの例でいうtwoやthreeのアドレス)と 代入もとxのアドレス(さっきの例でいうoneのアドレス)が一致したら つまり間違えて自己代入(one = one;)してしまった場合は、何もしないで関数を終了する*/ if(this == &x) return *this; /* 自己代入でなければ今持っているNekoオブジェクトを破棄して、 xと同じ猫名nameを持つ新しいNekoオブジェクトを生成する */ delete a; a = new Neko(x.get_name()); return *this; } void Betu::show() const{ if( a==0 ) return; cout<<"名前:"<<a->get_name()<<endl; } void Betu::input(){ string s; delete a; cout<<"名前を入力して下さい"<<endl; cin>>s; a = new Neko(s); } int main(){ Betu one; one.input(); one.show(); }
そもそもBetuオブジェクトの代入&コピーを禁じたいときは、コピーコンストラクタ&代入演算子をBetuクラスのprivate関数にしてしまえばいい。 そうすれば代入やコピーをしたときコンパイラがエラーを出してくれる。
class Betu { Neko* a; // コピーコンストラクタと代入演算子のprivate化 Betu(const Betu&); Betu& operator=(const Betu&); public: Betu() : a(0){} ~Betu(){ delete a; } void show() const; void input(); string get_name() const{ return a->get_name(); } };
この場合どうせ使わないので関数の中身まで書く必要はなく、定義だけしておけばいい。
const
// neko_const.cpp #include <iostream> #include <string> using namespace std; class Neko { string name; public: Neko(string s) : name(s){} void naku() const{ cout<<"にゃあ。我が名は"<<name<<"である"<<endl; } void naku2(){ cout<<"にゃあ。我が名は"<<name<<"である"<<endl; } }; int main(){ Neko const nora("茶トラ"); nora.naku(); // 問題なし // nora.naku2(); // 関数naku2にはconstがついていないので、メンバ変数の値を変更するとみなされてエラー }
テンプレート
引数がint型でもfloat型でも使える関数が作りたい、などのときにテンプレートというものが活用できる。
//templ.cpp #include<iostream> using namespace std; template<class T, class U>class Thing { T data1; U data2; public: Thing( T d1, U d2 ) : data1(d1), data2(d2){} T get_data1() const{ return data1; } U get_data2() const{ return data2; } }; template<class S> S tasugo(S a){ return a + 5; } int main(){ Thing<int, float> v(10, 1.5); cout<<"["<<v.get_data1()<<","<<v.get_data2()<<"]"<<endl; cout<<"tasugo関数の結果:"<<endl; cout<<tasugo<int>(3)<<endl; cout<<tasugo<float>(1.5)<<endl; // 関数の引数の型がテンプレートの場合は普通、与えられた引数から型を勝手に判断してくれる cout<<"tasugo関数の結果:"<<endl; cout<<tasugo(3)<<endl; cout<<tasugo(1.5)<<endl; }
実行結果
[10,1.5] tasugo関数の結果: 8 6.5 tasugo関数の結果: 8 6.5
このS,T,Uなどの名前は好きに決めてよい。 このSなどにはプログラマが作ったクラスを入れても良いのがすごいところらしい?