c++ lecture-0
TRANSCRIPT
C++言語講習会第0回資料
書いた人:@sunaemon0(回路屋2年)
1 目標
• linukairoのソースコードを読むことが出来るようになる• 効率的で安全なコードを書くための基礎知識を身につける
– 例外安全性を保つ– 資源管理をおこなう– 未定義動作を避ける
2 C++ことはじめ
C++は、C++98, C++03, C++11と進化してきました。
CもK&R, C89, C99, C11と進化してきました。
初期においてはC++はCの上位互換だったのですが、今ではそうではありません。
ただ、libkairoで使われている構文なら、ほとんどがC++でもそのまま動きます。
今回の講習会ではC89の知識を前提とした上で、C++03およびC++11はそれよりどのように違うのかという事を説明していく予定です。
3 コンパイルの方法について
このpdfファイルに同梱されているソースコードは以下のようにコンパイルできます。
1 # g++ --std=c++11 example0.cpp -W -Wall
4 C89とC++03の違い1
• 一行コメントが追加された。 //コレです• 関数内のブロックで先頭以外でも宣言を行えるようになった。• stdint.hがある。• bool型がある。
以上はC99に入っているので使ったことのあるひとも多いでしょう。
list 1 example0.cpp1 #include <stdio.h>23 int main(int, char **) // you can omit unused variable4 {
5 printf("input values:");
67 int val[10]; // declears variable after function call
89 for (int i = 0; i < 10; i++) // declears variable in the init expression
10 scanf("%d", val + i);
1112 for (int i = 0; i < 10; i++)13 printf("%d ", val[i]);
1415 printf("\n");
16 return 0;17 }
5 C++03とC89との違い2
• 構造体を使いやすくするのに、Cでは typedeをよく使いましたが、typedefなしで同じことができるようになりました。• 引数を取らない関数について、int f(void)でなく、int f()と宣言することになりました。Cの int f()のように引数を具体的に指定しない宣言はできなくなりました。
6 iostream詳しいことは次回やりますが、C++ではprintf/scanfの代わりに次のように書きます。
list 2 iostream0.cpp1 #include <string>2 #include <iostream >34 int main(int, char**) {5 std::string s;
6 std::cin >> s; // read from standard input
7 std::cout << s << std::endl; // write to standard output
8 std::cerr << "no error" << std::endl; // write to standard error output
9 }
std::cout <<なんちゃらでprintf的なことができるという認識で実用上問題ありません。
std::stringというのは可変長の文字列を保持できる型です。
以降、とりあえず::について解説します。
7 名前空間
C++ではライブラリの間の名前の衝突を防止するため、namespaceというキーワードを使って名前空間を分離することができるようになっています。それぞれの名前空間にアクセスするには::演算子を使います。
list 3 namespace.cpp1 #include <iostream >23 namespace foo4 {
5 int f() {return 1;}6 }
78 namespace bar9 {
10 int f() {return 2;}11 }
1213 int main(int, char **)14 {
15 std::cout << foo::f() << std::endl; // 1
16 std::cout << bar::f() << std::endl; // 2
17 // std::cout << f() << std::endl;
18 // f was not declared in this scope
19 return 0;20 }
大域スコープにはこのようにアクセスします。
list 4 namespace1.cpp1 #include <iostream >23 int f() {return 1;}45 int main(int, char **)6 {
7 std::cout << ::f() << std::endl; // 1
8 std::cout << f() << std::endl; // 1
910 return 0;11 }
8 using
いちいち名前空間を指定するのが面倒な場合、usingディレクティブを使って名前空間を指定しなかった時に検索する名前空間を登録することができます。
この際、関数名が衝突して曖昧さが発生した場合は、その関数の使用時にコンパイルエラーが出ます。
list 5 using0.cpp1 #include <iostream >23 namespace foo4 {
5 int f() {return 1;}6 }
7 namespace bar8 {
9 int f() {return 2;}10 }
1112 using namespace foo;1314 int main(int, char **)15 {
16 std::cout << foo::f() << std::endl; // 1
17 std::cout << bar::f() << std::endl; // 2
18 std::cout << f() << std::endl; // 1
19 return 0;20 }
9 Koenig Lookup(飛ばして良い)
このコードはコンパイルエラーが出ます。
list 6 koenig0.cpp1 #include <iostream >23 struct test {int a;};4 namespace foo5 {
6 int f(test dummy) {return 1;}7 }
89 int main(int, char **)
10 {
11 test tmp;
12 std::cout << f(tmp) << std::endl;
13 // f was not declared in this scope
1415 return 0;16 }
これだとコンパイルできます。
list 7 koenig1.cpp1 #include <iostream >23 namespace foo4 {
5 struct test {int a;};6 int f(test dummy) {return 1;}7 }
89 int main(int, char **)
10 {
11 foo::test tmp;
12 std::cout << f(tmp) << std::endl;
13 // ok!
1415 return 0;16 }
どうして2つ目の例だとうまく行くかというと、C++のコンパイラはスコープ指定子がない関数の呼び出しが有った場合に、大域スコープと無名スコープ、usingディレクティブで導入されたスコープ、usingで導入されたシンボル以外にも、引数の型が属する名前空間も検索するからです。
このように引数の型に依存した検索を行うことを、ADL(Argument Dependent Lookup)または、Konig Lookupと言います。
10 標準ライブラリについて
c++03からはcおよびc++のライブラリを読み込むときに1 #include <stdio.h>2 #include <iostream.h>
でなく、1 #include <cstdio>2 #include <iostream >
とします。後者の方が新しい書き方です。
前者だと、それぞれのシンボルは大域名前空間に入っていますが、後者ではstd名前空間に入っています。
11 関数のオーバーロード
c++では引数の型が違う同名の関数を定義できます。
list 8 overload0.cpp1 #include <stdio.h>23 int f(int c) {return 1;}4 int f(char i) {return 2;}5 int f(int i, char d) {return 3;}67 int main(int, char **)8 {
9 char c = 0;10 int i = 0;1112 std::cout << f(i) << std::endl; // 1
13 std::cout << f(c) << std::endl; // 2
14 std::cout << f(c,c) << std::endl; // 3
1516 return 0;17 }
型がぴったり合う関数定義があれば、それが呼ばれます。型変換を行うことで呼び出せる関数があれば、それが呼ばれます。
12 extern “C”gccでコンパイルすると、.oファイルのシンボル名と、Cで書いた関数名が一致します。nm a.outとして調べてみるとわかり安いです。やったことがある人なら解ると思いますが、違う翻訳単位で同名の関数を定義するとリンクするときに怒られます。
C++では同名でも型の違う関数が定義できます。これでなんで ldに怒られないかというと単純にシンボル名に型の情報を含めているためです。
例えば、1 can_packet make_can_motor_packet(uint8_t, uint16_t, motor_mode);
2 // _ZN12liblinukairo21make_can_motor_packetEhtNS_10motor_modeE
3 std::string get_short_name(bool, uint8_t);4 // _ZN12liblinukairo14get_short_nameEbh
みたいな感じです。c++filtというコマンドを使うとこの暗号を解読できます。
しかし、cで書かれたライブラリとc++で書いたコードをリンクするときにこの機能が悪さをします。
1 int getchar();
みたいにしても変な名前のシンボル Z7getcharvを探しに行ってしまってリンクがうまく出来ません。代わりに
1 extern "C"2 {
3 int getchar();4 }
とすることでちゃんとgetcharというシンボルを呼び出すのでうまく行きます。
13 スタックとヒープ
ローカル変数や呼び出し番地などが格納される領域をスタック領域と言います。
Cで言えば、mallocで取って来れる領域のことをヒープ領域と言います。
static変数が配置される領域を静的領域といいます。
14 newとdelete
c++ではヒープ領域に変数を確保するのにnewを使います。開放するには、deleteを使います。
list 9 new0.cpp1 #include <cstdio>23 int main(int, char **)4 {
5 int *i = new int;67 std::scanf("%d", i);
8 std::printf("%d\n", *i);
910 delete i;1112 return 0;13 }
配列を確保する場合はnew[]を使います。また開放するにはdelete[]を使います。
list 10 new1.cpp1 #include <cstdio>23 int main(int, char **)4 {
5 char *s = new char[100];67 std::scanf("%s", s);
8 std::printf("%s\n", s);
910 delete[] s;1112 return 0;13 }
new/deleteはmalloc/freeに比べてキャストが必要がないという点で型安全になっています。
開放するときに使う演算子が違うことに注意して下さい!
15 左辺値参照とポインタ
C++では実はポインタはあまり使いません。
Cでポインタを使う場面は多分この4つです。
1. 参照渡しをする。2. mallocを使ってヒープ領域に動的に領域を確保する。3. 文字列を扱う。(2.の特殊例)4. (回路レイヤだけですが)定数からポインタにキャストしてレジスタをゴリゴリする。
今回やる左辺値参照というのはざっくり言うと参照渡しをするためのものです。
次回以降やりますが、文字列はstd::stringを使い、ヒープ領域に配列を確保するには普通std::vectorを使います。
C++でポインタが必要になるのは、thisポインタを使うとき、Cで書かれたライブラリをいじるとき、それとメモリを直接ゴニョゴニョするときくらいです。
実引数を関数に渡すときに、値をコピーして渡すのを値渡しと言います。
• 呼び出された側は、実引数を変更できません。• 大きなデータを値渡しすると、大きなコストがかかります。
実引数を関数に渡すときに、値をコピーせずそのまま渡すのを参照渡しと言います。
• 呼び出された側は、実引数を変更できます。• 大きなデータを参照渡ししても、コストは小さいデータと変わりません。
ポインタで参照渡しを行うと以下の問題があります。
• 値渡しの時と記法が違う (同時に利点でもあります。)• NULLを渡せてしまう
これらを解消したのが左辺値参照です。
左辺値参照は基本的にはポインタです。
宣言は1 int a = 0; //local variable2 int &r = a; //reference of a
このように行います。
左辺値参照はこのように、lvalue(ローカル変数などの具体的に名前のついた値)で初期化しないと作れません。
1 int &r;2 // ‘’r declared as reference but not initialized3 int &e = 1;4 // invalid initialization of non-const reference of type
5 // ‘int’& from an rvalue of type ‘’int6 const int &f = 1;7 // ok
歴史的経緯から、定数左辺値参照はなんと領域を確保するの
で、rvalue(リテラルなどの名前のついていない一時的な値)で初期化できます。
これはポインタとは全く違った振る舞いです。
参照渡ししてみましょう。
list 11 reference0.cpp1 #include <cstdio>23 void f(int &i)4 {
5 i=3;
6 }
78 int main(int argc, char **argv)9 {
10 int i=0;11 f(i);
12 std::printf("%d\n", i);
13 return 0;14 }
ポインタみたいに1 *i = 3;
とは書きません。参照はその元の変数と同じように振る舞います。
そのため、ポインタと違って free()やdeleteはできません。
16 オブジェクト指向
オブジェクト志向というのは、プログラムをオブジェクトがメッセージを投げ合うという形に表現するパラダイムです。
オブジェクトは内部にデータを保持すると同時に、メッセージを投げられた時にどうしようというプログラムも持ちます。
17 クラスと構造体とオブジェクト指向
C++にはclassというキーワードがあります。またC++にもstructというキーワードがあります。C++において両者はほとんど同じです。違いは、メンバがデフォルトでprivateになるかpublicかというだけです。publicとかprivateについては後でやります。
そこで、以降structのC的な用法を構造体、classのC++的な用法をクラスと呼びます。
クラスというのは、C++においては構造体にメンバ関数やカプセル化やコンストラクタ・デストラクタや継承や仮想関数などの概念を付け加えたものです。
オブジェクト指向でいうオブジェクトを、クラスのインスタ
ンスで表すのがC++流のオブジェクト指向です。
もう少しまともな例を挙げます。
例えば、libkairoのhalがオブジェクト指向の概念を実装しています。細かいところを省くと以下のようなコードになっています。
list 12 hal0.cpp1 #include <stdio.h>23 struct hal_adc {4 unsigned int value;5 };
67 void set_adc_value(hal_adc *adc, unsigned int value) {8 adc->value = value;
9 }
1011 unsigned int get_adc_value(const hal_adc *adc) {12 return adc->value;13 }
1415 int main(int, char **) {16 hal_adc adc;
17 set_adc_value(&adc, 3);
18 printf("%d\n", get_adc_value(&adc));
19 return 0;20 }
had adc型の変数がオブジェクトです。このオブジェクトに電圧をよこせというメッセージを送ることは、
1 unsigned int get_adc_value(const hal_adc *adc);
にhal adc型の変数を渡して呼び出すという事に相当します。
C++のクラスは、これの書き方を書き換えたものがベースになっています。
18 thisポインタとメンバ関数
これをメンバ変数と同じ感じで呼び出せるようにしようというのがC++のメンバ関数です。
hal encを簡略化したものを構造体とクラスで書いて見ました。
list 13 hal0.cpp1 #include <stdio.h>23 struct hal_adc {4 unsigned int value;5 };
67 void set_adc_value(hal_adc *adc, unsigned int value) {8 adc->value = value;
9 }
1011 unsigned int get_adc_value(const hal_adc *adc) {12 return adc->value;13 }
1415 int main(int, char **) {16 hal_adc adc;
17 set_adc_value(&adc, 3);
18 printf("%d\n", get_adc_value(&adc));
19 return 0;20 }
list 14 hal1.cpp1 #include <cstdio>23 class hal_adc {4 private:5 unsigned int value;6 public:7 unsigned int get_value() {8 return this->value;9 }
10 void set_value(unsigned int value) {11 this->value = value;12 }
13 };
1415 int main(int, char **)16 {
17 hal_adc adc;
18 adc.set_value(3);
19 std::printf("%d\n", adc.get_value());
20 return 0;21 }
これをみれば、thisポインタというのがなにか分かると思います。
hal enc型の変数とさっきから書いて来ましたが、正確には変数の中に入っている値こそが議論の対象です。
これをC++ではhal encのインスタンスと言います。
内部的にはやってることは構造体の方と同じです。
19 隠蔽と friend
しかしset adc valueやget adc valueって余計な関数呼び出しが発生するだけじゃね?って適当な人が直接valueを書き換えたとします。
これは後々halの中身を書き換えたり、不正な値が代入されないかチェックしたりということができなくなるのでよくありません。
しかし、Cのコードだと結構簡単にできてしまいます。
list 15 access0.cpp1 #include <stdio.h>23 struct hal_adc {4 unsigned int value;5 };
67 void set_adc_value(hal_adc *adc, unsigned int value) {8 adc->value = value;
9 }
1011 unsigned int get_adc_value(const hal_adc *adc) {12 return adc->value;13 }
1415 int main(int, char **)16 {
17 hal_adc adc;
18 adc.value = 3;
19 //set_adc_value(&adc, 3);
20 printf("%d\n", adc.value);
21 //printf("%d\n", get_adc_value(&adc));
22 }
libkairoだと構造体をhal adc prv.hというヘッダファイルで定義して、制御屋はそこで定義されてるものは直接使わないようにしようという事にしていますが、強制力はありません。
しかし、それをC++でやってみるとこれはコンパイルが通りません。
list 16 access1.cpp1 #include <cstdio>23 class hal_adc {4 private:5 unsigned int value;6 public:7 unsigned int get_value() {8 return this->value;9 }
10 void set_value(unsigned int value) {11 this->value = value;12 }
13 };
1415 int main(int, char **)16 {
17 hal_adc adc;
18 adc.value = 3;
19 // ‘unsigned int hal_adc::’value is private20 std::printf("%d\n", adc.value);
21 // ‘unsigned int hal_adc::’value is private22 return 0;23 }
こうして世界は平和になりました。
エラーメッセージを見ると解ると思いますが、仕事してるのは四行目のprivate:というアクセス指定子です。次のアクセス指定子までのメンバ変数をpublicにします。
これによってvalueは外側からは直接触れなくなります。これを隠蔽と言います。
逆にget valueはpublicというアクセス修飾子がついているので外側からアクセスできます。
先述の通りclassのデフォルトのアクセス修飾子はprivateなので本当は四行目は要りません。
また、structとclassの違いはデフォルトのアクセス修飾子の違いだけですから、これでも同じ事です。
list 17 access2.cpp1 #include <cstdio>23 struct hal_adc {4 unsigned int get_value() {5 return this->value;6 }
7 void set_value(unsigned int value) {8 this->value = value;9 }
10 private:11 unsigned int value;12 };
1314 int main(int, char **)15 {
16 hal_adc adc;
17 adc.value = 3;
18 // ‘unsigned int hal_adc::’value is private19 std::printf("%d\n", adc.value);
20 // ‘unsigned int hal_adc::’value is private21 return 0;22 }
でも特別にこの関数にだけはprivateも触らせてあげようという事もあります。そんな時には friendを使います。
list 18 access3.cpp1 #include <cstdio>23 struct hal_adc {4 unsigned int get_value() {5 return this->value;6 }
7 void set_value(unsigned int value) {8 this->value = value;9 }
10 private:11 unsigned int value;1213 friend int main(int argc, char **argv);14 };
1516 int main(int, char **) {17 hal_adc adc;
18 adc.value = 3;
19 // ok
20 std::printf("%d\n", adc.value);
21 // ok
22 }
20 演習問題
• Cで定義した関数をC++から呼び出してみよ。逆にC++で定義した関数をCから呼び出してみよ。• 静的なローカル変数への左辺値参照を関数から返して、呼び出し元でそれを変更してみよ。
21 Referencehttp://www.cplusplus.com/
http://en.cppreference.com/w/
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2012/n3337.pdf
22 LisenceThe text of this document is distributed under the CreativeCommons Attribution-ShareAlike 2.0 License.