c++よく忘れること

概要

  • 久しぶりにC++を書こうとすると(というよりも読もうとすると)忘れてしまっていてリハビリするのがコピーコンストラクタ,this, Template, 参照渡し,仮想関数, オーバライド,スマートポインタ。忘れがちな点を纏めておく。

符号なし整数と符号あり整数の計算

  • 符号なし(unsinged int)と符号あり(int)の足し算をすると,intが暗黙のキャストをされて意図しない値(多くの場合は負の数が巨大な正の数)に変換されてしまう。
unsigned a = 1;
int b = -1;
cout << a + b << endl;  // 0じゃない!!

enum class

  • C++11からenum classが追加されている。古いenumと何が違うかと言うと,列挙子に使った名前を変数として使用できること。
enum SIGNAL {BLUE, YELLOW, RED};
int BLUE;  // エラー

staticキーワード

  • キーワードstaticはローカル変数につくのか,グローバル変数,関数に付くのか,メンバ変数かメンバ関数か?で意味が大きく異なるので混乱する。C++のstaticは多義語なので,個人的には久しぶりに見ると混乱するキーワード1位(クラス変数が初期化できない,という罠もあるし)。

staticローカル変数

  • statiを付けて宣言したローカル変数は1度だけ初期化され,関数呼び出し終了後も状態を保存する変数となる。
int f(){
  static int x=0;
  return ++x;
};


int main(int argc, char* argv[])
{
  cout << f() << endl; // 1
  cout << f() << endl; // 2
  cout << f() << endl; // 3
  return (0);
}

グローバル変数/関数

  • staticを付けた変数,関数はファイルにローカルになる。よって,複数のファイルで同じ名前の関数名を使いたい時(そんなことは良くないが)に,各変数,各関数にstaticを付けておけば,ファイルローカルになるので,名前衝突は発生しない。
// file1.cpp
int x=0;

// file2.cpp
static x=1;  // staticが無いと変数名'x'が衝突してエラー。

メンバ変数/メンバ関数

  • staticを付けるとクラス変数(インスタンス変数ではない),クラス関数になる。
  • ここで注意なのが,クラス変数は宣言時に初期化が出来ない!!。(なんでなんだろう??)

関数仮引数の書き方

  • 値渡し,ポインタ渡しに加えて,参照渡し(&),右辺値参照渡し(&&)の4パタンがある。それにconstが付くので8パタンがある。

値渡し

  • 値のコピーが渡されるので,関数内で値を更新しても,呼び出し側の変数は更新されない。
  • constを付けると更新するコードのコンパイルが出来ない。
  • そもそも,更新後の値が必要ないから値渡ししている変数にconstを付ける意味ってあるのかな??,と思ったが更新しないものを(意図せずに)更新している,ということをコンパイル時に明確に気づくためには意味があるのかも知れない。
int f(int x) { return ++x; }  // xはコピーなので引数は影響を受けない。
int f(const int x) { return ++x; } // コンパイルエラー

ポインタ私

  • ポインタ変数pに’*’を適用するとポインタを剥がせる(アドレスpから値を取り出せる)。
  • ポインタ変数のconstは少し注意が必要。ポインタが指す先の値が変更不可なだけで,ポインタ自体の変更は可能。ポインタ自体も変更不可にしたい場合には,癖のある書き方(下の例を参照)をする。
int f(int* x) { return ++(*x); }
int f(const int* x) { return ++(*x); }  // コンパイルエラー
int f(const int* x) {
    int* y=++x; // ポインタ自体は変更可能
    return(0); // 
}
int f(const int* const x) {...}  // ポインタが指す先もポインタも変更不可にする場合

参照渡し

  • ほとんどポインタと変わらない(表記が異なっていて,定義時に初期値が必要な点がポインタとの違い)。
  • constを付けないとリテラルは渡せない。(というか参照は変数の参照というのが使い方なんだからリテラルを渡すことはあまり無いのかな??)
int f1(int& x) { return ++x; }  //引数も更新される
int f2(const int& x) {return ++x}  // コンパイルエラー
f1(10)  // constがついていないとリテラルを渡すことはできない
f2(10)  // constがついていれば渡せる

右辺値参照

  • もう少し頭の中の整理が必要だ。moveとか含めて最近(と言ってもC++11からあるのか・・・)。

コピーコンストラクタと代入演算子オーバーロード

  • C++では関数にオブジェクトを値渡しで渡す場合,宣言時の初期化,返り値とした場合にコピーコンストラクタが呼ばれる.
  • 値渡しじゃなく,参照渡し(リファレンス&,ポインタ*)の場合はコピーコンストラクタは呼ばれない(コピーしないんだから).
  • 変数宣言時の初期化のときの"="はコピーコンストラクタ呼ばれる(だって「初期化」だから)けれど,それ以外の代入のとき代入演算子=が呼ばれる.だから,自前クラスに=を使う場合はコピーコンストラクタの定義と代入演算子オーバーロードが必要.
  • 返り値にした場合もコピーコンストラクタが呼ばれるのは忘れやすいので注意.
  • わざわざあるコピーコンストラクタが存在する理由は,ポインタを持っているオブジェクトをコピーされると,そのコピーされたオブジェクトがスコープを抜ける時にデストラクタが呼ばれて,もとのオブジェクトのポインタを解放してしまうから.例えば,クラスの中でメモリ参照をしている(newしている)ような場合,値渡しで単純にコピーするとそのポインタがコピーされる.そして,そのオブジェクトが関数を抜ける時にデストラクタが呼ばれてしまうと,もともとの参照していたポインタも解放されてしまう.(関数呼び出し元で再度デストラクタが呼ばれると2重解放になってエラー発生して止まる)
  • 基本的には代入演算子はオーバロードして,コピーコンストラクタを書くようにする.ポインタをメンバ変数に持っている場合は必須.
#include <iostream>
#include <string>

using namespace std;

class TestClass {
public:
  TestClass(){ // Default constructor
    cout << "Default constructor is called." << endl;
  }

  TestClass(const string name) : name_(name){ // Custom constructor
    cout << "Custom constructor is called, I am name=." << this->name_ << endl;
  }

  TestClass(const TestClass & c){ // Copy constructor
    name_ = c.name_ + "_copy";
    cout << "Copy constructor is calld, I am name=." << this->name_ << endl;
  }

  string getName() {return name_;}
private:
  string name_;
};
void value_pass_func(const TestClass c) {
  return;
}

void ref_pass_func(const TestClass & c) {
  return;
}

void pointer_pass_func(const TestClass * c) {
  return;
}

int main(void) {
  TestClass c0;          // default constructor is called.
  TestClass c1 {"hoge"}; // custom constructor is called.
  TestClass c2 = c1;     // copy constructor is called.

  cout << "value_pass_func is called," << endl;
  value_pass_func(c2);
  cout << "ref_pass_func is called," << endl;
  ref_pass_func(c2);
  cout << "pointer_pass_func is called," << endl;
  pointer_pass_func(&c2);
  return 0;
}

this

  • メソッド呼び出しの際には暗黙で呼び出し元のオブジェクトのポインタが渡される.それがthis.pythonで言うところのselfなのかな.
  • メンバ関数を普通に使用する分には省略する.
  • 使いみちとしては,メンバ関数ではない関数(クラスの外にある関数)にオブジェクトを渡す時に使う.

Template

  • 型が違うだけで他は同じクラス,関数を一つにまとめる.具体的な型は呼び出し側で指定して,コンパイラコンパイル時に型を埋め込む.(当然だけど動的言語みたいに実行時に型を解決している訳じゃない.)
  • 同様のことは継承(多態性)でも出来る.違いはtemplateはコンパイル時に型を解決しているのに対して,継承は実行時にvtable(virtualな関数のテーブルをオブジェクトは持っている)を手繰って実行するべき関数を検索している.よって,継承のほうが実行コストが高い.templateは型付きの関数をコンパイル時に自動生成しているようなもの.但し,templateはコンパイル時に型を特定する必要があるが故に,次の通りヘッダに実装を書く必要があって汚くみえる.
  • テンプレートを使う場合はクラスの実装をヘッダファイルに記述する.これは分割コンパイルはあくまでもソース単位で実行されるため,テンプレートの実装”だけ”を書いたソースをコンパイルしても,それが使われていなので実態が生成されないため.よって,ヘッダにしておいて,そのテンプレート関数なりクラスを使うソース(は当然それをインクルードしていて,型を指定する記述が登場する)に渡すのだ. (補足として,Cではヘッダファイルのインクルードは極端に言えばただの置換で,インクルードしたファイルの内容がそこに書き込まれると思えば良い.ソースファイルのコンパルはその置換後のファイルに対して実行される.これがヘッダファイルだけのライブラリが使われる理由.つまり,外部ファイルとかなく,ヘッダだけで真に完結する.コンパイル時間が大きくなるけどプリコンパイルを使うと避けられる.)
  • テンプレートのスコープはtemplate文の次の宣言文の中だけ.
#include <iostream>
#include <string>

using namespace std;

// この関数が型Xを使うことを宣言
template <class X>
X add(X a, X b)
{
  return a+b;
}

int main()
{
  cout << add<int>(1, 2) << endl;
  cout << add<float>(1.0, 2.0) << endl;
  cout << add<string>("Oda", "Nobunaga") << endl;
  return 0;
}

オーバーライド

  • 親クラスのメンバ関数を子クラスが上書きすることができる.
  • virtualを付けなくてもできることに注意.じゃあvirtualの意味は?というのは下を参照.
  • 演算子の再定義や,引数の異なる関数を定義することはオーバロード(多重定義).

virtual

  • C++では子クラスは親クラスのメソッドと同名のメソッドを定義可能.
  • virtualと書くのは基底側だけでも良いけど,分かりやすいように継承側でも書く.
  • virtualを付与すると実行時に型を解決して,どの関数を実行するのかを検索するので,多少はオーバヘッドがある.
  • 親クラスでは中身が無い,つまり子クラス側で実装をお願いする関数を純粋仮想関数と呼び,純粋仮想関数を持つクラスを抽象クラスと呼ぶ.抽象クラスは実態を作れない.
class Hoge
{
public:
  virtual void func(void) = 0; // 純粋仮想関数
};
  • virtualを付ける場合と付けない場合の違いは,ポインタ経由でメソッドを呼び出す時に現れる.C++では親クラスのポインタに子クラスのアドレスを入れることができて,その時に(おそらく)意図と異なる動作になってしまう.つまり,親クラスのポインタに子クラスのアドレスを入れて,親と子で同名のメソッドをポインタ経由で呼び出した時にどうなるか.virtualが付いていれば子クラスのメソッドが呼ばれて,付いていなければ親クラスのメソッドが呼ばれる.
#include <iostream>

using namespace std;

class Hoge {
public:
  void non_virtual_func (void) {cout << "I am Hoge (non_virtual)" << endl;};
  virtual void virtual_func (void) {cout << "I am Hoge (virtual)" << endl;};  
};

class Foo : public Hoge {
public:
  void non_virtual_func (void) {cout << "I am Foo (non_virtual)" << endl;};
  virtual void virtual_func (void) {cout << "I am Foo (virtual)" << endl;};
};


int main()
{
  Hoge *h1 = new Hoge();
  h1->non_virtual_func(); // I am Hoge (non_virtual)
  h1->virtual_func();     // I am Hoge (virtual)

  Hoge *h2 = new Foo();
  h2->non_virtual_func(); // I am Hoge (non_virtual) !!
  h2->virtual_func();     // I am Foo (virtual)

  return 0;
}

関数オブジェクト(ファンクタ)

  • Cでは関数ポインタを使って関数に関数を渡せた.同じような目的で使えるのが関数をオブジェクト化した関数オブジェクト.
  • テンプレートと組合せて使われる例が多いけれど,それは(おそらく)STLVectorとかに渡す場合を想定しているから.ファンクタ自体はテンプレートと関係は無い.
  • 実装はoperator()をオーバーロードする.
#include <iostream>

using namespace std;

template <typename T>
class MyAdder {
public:
  T operator() (const T& a, const T& b) { // functor
    return a+b;
  }
};

int main(void) 
{
  MyAdder<double> double_adder;
  MyAdder<int> int_adder;
  cout << double_adder(2.0, 1.2) << endl;
  cout << int_adder(2, 1) << endl;
  cout << int_adder(2.3, 1) << endl; // Warning
}

スマートポインタ

  • new/deleteの面倒さと危険さを回避するために導入された.
  • auto_ptrは使うな,unique_ptrを使え.
  • unique_ptrはスコープからでると自動で削除される.shared_ptrは参照数をカウントしていて,参照が0になったら削除される.

    unique_ptr

  • C++14以降はmake_uniqueで見た目がすっきりする.
#include <memory>
std::unique_ptr<Class_name> p(new Class_name(arg*)); // c++14以前
std::unique_ptr<Class_name[]> p(new Class_name[N]); // N個の配列として渡す.
auto v = std::make_uniqe<Class_name>(arg*);
std::unique_ptr<Class_name[]> v_arr = std::make_uniqe<Class_name>(size); // 配列でかつ,引数渡したいときは?

vector

  • 初期化方法と,emplace_back()とpush_back()の違いを忘れてしまう.emplace_back()はクラスのオブジェクトをvectorに入れる場合に使う.コンストラクタの引数を渡したいというのと,コピーコンストラクタのコストを下げるため.
#include <iostream>
#include <vector>
#include <algorithm>
#include <functional>

using namespace std;


int main()
{
  //////////////////////////////////////////////////
  // Construction
  //  - initialize with size and init. value
  vector<int> v0(10, 0); // {0,0,0,0...0}

  //  - initialize with a vector
  vector<int> v1(v0);

  //  - initializer list (from c++11)
  vector<int> v2{0,1,2,3,4,5,6,7,8,9};

  //  - initialize with an array
  int arr[] = { 100, 600, 300, 500, 400, 200 };
  vector<int> v3(arr, arr + (sizeof(arr)/sizeof(int)));

  //  - initialize with iota
  vector<int> v4(10);
  iota(v4.begin(), v4.end(), 5); // {5,6,7,...14}
  
  //////////////////////////////////////////////////
  // Add element
  //  - If add element is an object, use emplace_back()
  //    which can pass the constructer args.
  v0.push_back(10); // v0={0,0,0...0,10}
  // vobj.emplace_back(arg1, arg2,...)

  //////////////////////////////////////////////////
  // Algorithm
  //sort(viter, vend);
  

  // ranged for
  for (auto d : v4) 
    cout << d << endl;

  return 0;
}