🖥️

【C++言語入門】 第16回 コピーコンストラクタ

に公開

https://youtu.be/E4uABkBYEpU

四国めたん
\textcolor{pink}{四国めたん: }教師役ですわ

ずんだもん
\textcolor{lime}{ずんだもん: }生徒役なのだ

\footnotesize \textcolor{pink}{四国めたん:} こんにちは。四国めたんです

\footnotesize \textcolor{lime}{ずんだもん:} ずんだもんなのだ。こんにちはなのだ

\footnotesize \textcolor{pink}{四国めたん:} 今回はクラスのインスタンスのコピーについてお話ししますわ

\footnotesize \textcolor{lime}{ずんだもん:} ただ単にコピーすればいいのではないのか?

\footnotesize \textcolor{pink}{四国めたん:} 通常の変数の場合には問題ないですわね

\footnotesize \textcolor{pink}{四国めたん:} でも、クラスのインスタンスの場合には、問題が出る場合があるのですわ

\footnotesize \textcolor{lime}{ずんだもん:} その辺りを詳しく教えて欲しいのだ

メンバ変数をそのままコピーします

\footnotesize \textcolor{pink}{四国めたん:} 今回はクラスのインスタンスのコピーについて考えてみますわ

\footnotesize \textcolor{lime}{ずんだもん:} ふむふむ

\footnotesize \textcolor{pink}{四国めたん:} まず、構造体のコピーについては、非常に単純ですわ

#include <iostream>

struct fruit {
  int weight;
  int price;
};

int main(int argc, char* argv[]) {
  fruit apple = {100, 200};
  fruit pomme = apple;
  apple.weight = 150;
  std::cout << "リンゴの重さは" << pomme.weight << "gです。" << std::endl;
  std::cout << "リンゴの値段は" << pomme.price << "円です。" << std::endl;
  return 0;
}

構造体のコピー

\footnotesize \textcolor{pink}{四国めたん:} fruit pomme = apple;の部分でコピーしていますわ

\footnotesize \textcolor{lime}{ずんだもん:} 基本的には構造体を生成して、メンバーを単純に代入しているだけなのだ

\footnotesize \textcolor{pink}{四国めたん:} コピー後に"apple.weight"に150を代入していますが、"pomme.weight"として100が表示されていますわ

\footnotesize \textcolor{pink}{四国めたん:} ですので、"apple"と"pomme"は異なるインスタンスであることがわかりますわ

\footnotesize \textcolor{lime}{ずんだもん:} たしかにその通りなのだ

\footnotesize \textcolor{pink}{四国めたん:} もし"pomme"がfruit& pomme = apple;のように 参照 であれば、"pomme.weight"も150に変わっているはずですわ

#include <iostream>

struct fruit {
  int weight;
  int price;
};

int main(int argc, char* argv[]) {
  fruit apple = {100, 200};
  fruit& pomme = apple;
  apple.weight = 150;
  std::cout << "リンゴの重さは" << pomme.weight << "gです。" << std::endl;
  std::cout << "リンゴの値段は" << pomme.price << "円です。" << std::endl;
  return 0;
}

構造体の参照

\footnotesize \textcolor{lime}{ずんだもん:} たしかに"pomme.weight"が150に変わっているのだ

\footnotesize \textcolor{pink}{四国めたん:} クラスについても基本は同じですわ

circle.h
#pragma once

#ifndef CIRCLE_H
#define CIRCLE_H

#include <iostream>

const double kPI = 3.14159265358979323846;

/// @brief 円
class Circle {
  double diameter_;  // 直径

 public:
  Circle() : Circle(0.0) {}
  Circle(double diameter) : diameter_(diameter) {
  }
  virtual ~Circle() {
  };

  double Diameter() { return diameter_; }
  void Diameter(double diameter) { diameter_ = diameter; }

  virtual double Area() {
    double radius = Diameter() / 2.0;
    double a = radius * radius * kPI;
    return a;
  }
};

#endif  // CIRCLE_H
hello_world.cpp
#include <iostream>
#include "circle.h"

int main(int argc, char* argv[]) {
  Circle c(10.0);
  Circle c0 = c;
  std::cout << "円の直径は" << c0.Diameter() << "cmです。" << std::endl;
  std::cout << "円の面積は" << c0.Area() << "cm^2です。" << std::endl;
  return 0;
}

クラスのコピー

\footnotesize \textcolor{lime}{ずんだもん:} クラスのインスタンスを生成してメンバ変数をコピーしているだけなのだ

\footnotesize \textcolor{pink}{四国めたん:} その通りですわ

\footnotesize \textcolor{pink}{四国めたん:} さて、ここで以前のようにCircleクラスのメンバ変数として文字列を持たせてみましょう

circle.h
#pragma once

#ifndef CIRCLE_H
#define CIRCLE_H

#include <iostream>

const double kPI = 3.14159265358979323846;
const int kMessageSize = 50;

/// @brief 円
class Circle {
  double diameter_;  // 直径
  char* pmessage_;

 public:
  Circle() : Circle(0.0) {}
  Circle(double diameter) : diameter_(diameter), pmessage_(nullptr) {
    pmessage_ = new char[kMessageSize];
  }
  virtual ~Circle() {
    delete[] pmessage_;
    pmessage_ = nullptr;
  };

  double Diameter() { return diameter_; }
  void Diameter(double diameter) { diameter_ = diameter; }

  virtual double Area() {
    double radius = Diameter() / 2.0;
    double a = radius * radius * kPI;
    return a;
  }

  virtual const char* Message() {
    double area = Area();
    sprintf_s(pmessage_, kMessageSize, "円の面積は%fです。", area);
    return pmessage_;
  }
};

#endif  // CIRCLE_H
hello_world.cpp
#include <iostream>
#include "circle.h"

int main(int argc, char* argv[]) {
  Circle c(10.0);
  Circle c0 = c;
  std::cout << "円の直径は" << c0.Diameter() << "cmです。" << std::endl;
  std::cout << c0.Message() << std::endl;
  return 0;
}

エラー

\footnotesize \textcolor{lime}{ずんだもん:} ブレークポイント指令が実行されました という警告とともにプログラムが中断したのだ

\footnotesize \textcolor{pink}{四国めたん:} はい、実は既に破棄されたメモリ領域を再度破棄しようとして、プログラムが中断しているのですわ

\footnotesize \textcolor{lime}{ずんだもん:} 既に破棄された?

\footnotesize \textcolor{pink}{四国めたん:} Circleクラスのインスタンスc0cは別のものですが、各メンバ変数はコピーされたものですわ

\footnotesize \textcolor{lime}{ずんだもん:} たしかに...

\footnotesize \textcolor{pink}{四国めたん:} ですので、"pmessage_"のポインタの値はコピーされて同じ領域を指していますわ

\footnotesize \textcolor{lime}{ずんだもん:} なるほどなのだ

\footnotesize \textcolor{pink}{四国めたん:} そのような状態で"c0"のデストラクタで"pmessage_"の領域を破棄するとどうなるでしょうか?

\footnotesize \textcolor{lime}{ずんだもん:} "c"のデストラクタで、既に破棄している領域を再度破棄することになるのだ

\footnotesize \textcolor{pink}{四国めたん:} その通りですわ

コピーコンストラクタで解消しましょう

\footnotesize \textcolor{lime}{ずんだもん:} メンバ変数のポインタにnewなどで動的にメモリを確保して割り当てた場合には、単純なコピーでは不具合が生じるのだ

\footnotesize \textcolor{pink}{四国めたん:} はい、これを解消するためにはクラスのインスタンスをコピーする際に、特別な処理をする必要がありますわ

\footnotesize \textcolor{lime}{ずんだもん:} たしかにその通りなのだ

\footnotesize \textcolor{pink}{四国めたん:} そのために用意されたのが コピーコンストラクタ ですわ

\footnotesize \textcolor{lime}{ずんだもん:} コピーコンストラクタ

\footnotesize \textcolor{pink}{四国めたん:} これはクラス インスタンス名 = 別のインスタンス;という形式でクラスのインスタンスを生成、初期化する際に呼ばれる、特別なコンストラクタですわ

\footnotesize \textcolor{lime}{ずんだもん:} ふむふむ

\footnotesize \textcolor{pink}{四国めたん:} 形式はこの通りですわ

クラス名(const クラス& 引数名) : 初期化子リスト {
  処理
}

\footnotesize \textcolor{lime}{ずんだもん:} 引数にクラスへの 参照 を指定している以外は普通のコンストラクタなのだ

\footnotesize \textcolor{pink}{四国めたん:} はい、この引数がコピー元のインスタンスとなりますわ

\footnotesize \textcolor{lime}{ずんだもん:} 処理 では何をおこなうのだ?

\footnotesize \textcolor{pink}{四国めたん:} 例えばメンバ変数に動的メモリを割り当てている場合には、 処理 で別のメモリ領域を割り当てますわ

\footnotesize \textcolor{lime}{ずんだもん:} メモリの内容はどうするのだ?

\footnotesize \textcolor{pink}{四国めたん:} メモリの内容についてはmemcpyなどを使用してコピーしておきますわ

\footnotesize \textcolor{lime}{ずんだもん:} なるほどなのだ

\footnotesize \textcolor{pink}{四国めたん:} とりあえずCircleクラスに コピーコンストラクタ を追加してみましょう

circle.h
#pragma once

#ifndef CIRCLE_H
#define CIRCLE_H

#include <iostream>

const double kPI = 3.14159265358979323846;
const int kMessageSize = 50;

/// @brief 円
class Circle {
  double diameter_;  // 直径
  char* pmessage_;

 public:
  Circle() : Circle(0.0) {}
  Circle(double diameter) : diameter_(diameter) {
    pmessage_ = new char[kMessageSize];
  }
  Circle(const Circle& c) : Circle(c.diameter_) {
    memcpy_s(pmessage_, kMessageSize, c.pmessage_, kMessageSize);
  }
  virtual ~Circle() {
    delete[] pmessage_;
    pmessage_ = nullptr;
  };

  double Diameter() { return diameter_; }
  void Diameter(double diameter) { diameter_ = diameter; }

  virtual double Area() {
    double radius = Diameter() / 2.0;
    double a = radius * radius * kPI;
    return a;
  }

  virtual const char* Message() {
    double area = Area();
    sprintf_s(pmessage_, kMessageSize, "円の面積は%fです。", area);
    return pmessage_;
  }
};

#endif  // CIRCLE_H

コピーコンストラクタ

\footnotesize \textcolor{lime}{ずんだもん:} Circle(const Circle& c)の部分が コピーコンストラクタ なのだ

\footnotesize \textcolor{pink}{四国めたん:} 初期化子リストとしてCircle(c.diameter_)を指定して通常のコンストラクタを呼び出していますわ

\footnotesize \textcolor{pink}{四国めたん:} その中で"pmessage_"に新たにメモリを配置していますわ

\footnotesize \textcolor{lime}{ずんだもん:} コピー元とは別のメモリ領域なのだ

\footnotesize \textcolor{pink}{四国めたん:} そして コピーコンストラクタ の処理部では、コピー元の"pmessage_"の内容を新しく割り当てられたメモリにmemcpy_sを使ってコピーしていますわ

\footnotesize \textcolor{lime}{ずんだもん:} なるほどなのだ

\footnotesize \textcolor{lime}{ずんだもん:} ところでインスタンスの生成でCircle c0 = c;の代わりにCircle c0(c);としてはダメなのか?

\footnotesize \textcolor{pink}{四国めたん:} もちろんOKですわ

\footnotesize \textcolor{pink}{四国めたん:} コピーコンストラクタ の定義を考えれば後者の方が判り易いですわね

まとめ

\footnotesize \textcolor{pink}{四国めたん:} お疲れさまでした。

\footnotesize \textcolor{lime}{ずんだもん:} おつかれさまなのだ

\footnotesize \textcolor{pink}{四国めたん:} 以上で コピーコンストラクタ を終わりにしますわ

\footnotesize \textcolor{pink}{四国めたん:} とりあえず、クラスのインスタンスを他のインスタンスからコピーして 生成 する場合には コピーコンストラクタ が呼ばれることを覚えておいてくださいね

\footnotesize \textcolor{lime}{ずんだもん:} わかったのだ

\footnotesize \textcolor{pink}{四国めたん:} また、 コピーコンストラクタ がない場合には、デフォルトの動作として、単純にメンバ変数がコピーされることも重要ですわ

\footnotesize \textcolor{lime}{ずんだもん:} りょうかいなのだ

Discussion