3.1 constメンバー関数とmutable
3.1.1 メンバー変数を変更する/しない関数
#include <iostream>
class product
{
int id;
public:
int get_id() const;
void set_id(int new_id);
};
// メンバー変数を変更しないメンバー関数は呼べても問題ない。
// インスタンスがconstとなっていても呼び出すことができるメンバー関数
int product::get_id() const{
return id;
}
void product::set_id(int new_id){
id = new_id;
}
int main(){
product p;
p.set_id(42);
std::cout << p.get_id() << std::endl;
const product cp{};
// constantなインスタンスのメンバー変数を変更すると困る
cp.set_id(42);
std::cout << cp.get_id() << std::endl;
}
3.1.2 const/非constメンバー関数間のオーバーロード
インスタンスがconstかどうかに依存する。
#include <iostream>
class product
{
int id;
public:
int get_id();
int get_id() const;
};
int product::get_id()
{
std::cout << "非constメンバー関数のget_id()が呼ばれました。" << std::endl;
return id;
}
int product::get_id() const
{
std::cout << "constメンバー関数のget_id()が呼ばれました。" << std::endl;
return id;
}
int main()
{
product p;
p.get_id();
const product cp{};
cp.get_id();
}
3.1.3 constメンバー関数でも書き込みを行いたい場合
mutable指定
3.2 コンストラクターとデストラクタ
#include <iostream>
#include <string>
class person
{
std::string m_name;
int m_age;
public:
person();
void set_name(std::string name);
void set_age(int age);
std::string name() const;
int age() const;
};
person::person() : m_age(-1)
{
std::cout << "コンストラクター呼び出し" << std::endl;
}
void person::set_name(std::string name)
{
m_name = name;
}
void person::set_age(int age)
{
m_age = age;
}
std::string person::name() const{
return m_name;
}
int person::age() const{
return m_age;
}
int main(){
person bob; // これでコンストラクタが呼び出される。
std::cout << bob.age() << std::endl;
bob.set_name("bob");
bob.set_age(20);
std::cout << bob.name() << std::endl;
std::cout << bob.age() << std::endl;
}
RAII: Resource Acquisition Is Initialization
→動的に確保されたメモリ領域の解放やOSが提供する特殊なリソースの返却。
引数を受け取るコンストラクターを1つでも定義してしまうと、コンパイラーが自動で生成するデフォルトコンストラクターがなくなってしまう。
3.3.2 委譲コンストラクター
#include <iostream>
#include <string>
class person
{
std::string m_name;
int m_age;
person();
person(int age);
public:
person(std::string name, int age);
void set_name(std::string name);
void set_age(int age);
std::string name() const;
int age() const;
};
person::person(int age): m_age(age){
std::cout << "共通コンストラクタ呼び出し" << std::endl;
}
person::person(): person(-1){
std::cout << "引数なし共通コンストラクタ呼び出し" << std::endl;
}
person::person(std::string name, int age): person(age){
std::cout << "引数付きコンストラクタ呼び出し" << std::endl;
set_name(name);
}
void person::set_name(std::string name){
m_name = name;
}
void person::set_age(int age){
m_age = age;
}
std::string person::name() const {
return m_name;
}
int person::age() const {
return m_age;
}
int main(){
person alice("alice", 15);
std::cout << alice.name() << std::endl;
}
3.3.3 コピーコンストラクタ
コンパイラー生成コンストラクターの中には、クラスをコピーする際に使われるコピーコンストラクターが存在する。
でもそれを使うと、メモリ領域やリソースを扱うクラスの場合は、不都合が起きる。
ポインター変数の値のみコピーを行うと、オブジェクトの二重開放が起きる。
person::person(const person& other){
std::cout << "コピーコンストラクタ呼び出し" << std::endl;
set_name(other.name());
set_age(other.age());
}
通常コピーコンストラクターは、そのクラス自身をconst参照で受け取るコンストラクターとして定義される。
3.3.4 =を使った初期化
#include <iostream>
#include <string>
class A
{
int m_v;
public:
A(int);
int v() const;
};
A::A(int v) : m_v(v)
{
}
int A::v() const {
return m_v;
}
int main(){
A x = 42;
if (x.v() == 42){
std::cout << "A.vは42です" << std::endl;
}else{
std::cout << "A.vは42ではありません" << std::endl;
}
}
これはスタックに確保されるからデコンストラクタ呼び出される。
ちょっと関係ないけど、下みたいにすると、クラスBのインスタンスbの初期化が行われたあと、コンストラクタの中身の処理が行われるから、はじめにデフォルトコンストラクタが呼ばれた後に、引数付きコンストラクタが呼ばれて非効率。
#include <iostream>
#include <string>
class B{
public:
int b_;
B(int v):b_{v}{
std::cout << "B constructor" << std::endl;
}
B():b_{0}{
std::cout << "B default constructor" << std::endl;
}
};
class A
{
int m_v;
B b;
public:
A(int);
int v() const;
~A();
};
A::A(int v) : m_v(v)
{
b = B(21);
}
A::~A(){
std::cout << "deconstructor" << std::endl;
}
int A::v() const {
return m_v;
}
int main(){
A x = 42;
if (x.v() == 42){
std::cout << "A.vは42です" << std::endl;
}else{
std::cout << "A.vは42ではありません" << std::endl;
}
}
3.3.5 explicit指定子
コンストラクタにexplicitを指定することで、暗黙のコンストラクタ呼び出しを禁止できる
explicit A(int);
A::A(int v) : m_v(v) // 定義にはexplicitを書かない
{
}
3.3まとめ
#include <iostream>
#include <string>
class Book
{
std::string title_;
std::string write_;
int price_;
public:
Book(std::string title, std::string writer, int price);
Book(const Book &other);
std::string getTitle()
{
return title_;
}
std::string getWrite()
{
return write_;
}
int getPrice()
{
return price_;
}
};
Book::Book(std::string title, std::string writer, int price) : title_(title), write_(writer), price_(price)
{
std::cout << &title_ << std::endl;
}
Book::Book(const Book &other)
{
std::cout << &other.title_ << std::endl;
title_ = other.title_;
write_ = other.write_;
price_ = other.price_;
std::cout << &title_ << std::endl;
}
int main()
{
Book b = Book("aa", "bb", 42);
Book copy = Book(b);
std::cout << copy.getWrite() << std::endl;
std::cout << copy.getPrice() << std::endl;
}
→
0x16f9b71d8
0x16f9b71d8
0x16f9b7190(ちゃんとディープコピーできてる!)
3.4 デフォルトの初期値
3.4.1 メンバー変数の初期値
NSDMIの方法は以下の3通りの書き方がある。
メンバー初期化
#include <iostream>
class S{
public:
int answer = 42;
int answer2 = {40};
float pi{3.1333};
};
int main(){
S s;
std::cout << s.answer << std::endl;
std::cout << s.answer2<< std::endl;
std::cout << s.pi<< std::endl;
}
コンストラクタの初期化リストとかコンストラクタ内の方が優先される。
3.5 継承の概要
3.5.1 継承とは
#include <iostream>
class Base
{
public:
void foo();
};
void Base::foo()
{
std::cout << "foo()" << std::endl;
}
class Derived : public Base
{
public:
void bar();
};
void Derived::bar()
{
std::cout << "bar()" << std::endl;
}
int main()
{
Base base;
base.foo();
Derived derived;
derived.foo();
derived.bar();
}
foo()
foo()
bar()
3.5.2 仮想関数とオーバーライド
仮想関数: 派生クラスで変更可能なもの。
オーバーライド: 派生クラスで関数を変更すること
#include <iostream>
class Base
{
public:
virtual void foo();
};
void Base::foo()
{
std::cout << "foo()" << std::endl;
}
class Derived : public Base
{
public:
void bar();
void foo() override; // overrideしてることを示すために。
void foo(int i);
};
void Derived::bar()
{
std::cout << "bar()" << std::endl;
}
void Derived::foo(){
std::cout << "Derived::foo() override" << std::endl;
}
void Derived::foo(int i){
std::cout << "Derived::foo(int) override" << std::endl;
}
int main()
{
Base base;
base.foo();
Derived derived;
derived.foo();
derived.foo(2);
derived.bar();
}
foo()
Derived::foo() override
Derived::foo(int) override
bar()
3.5.3 名前の隠蔽
基底クラスが持っているメンバー関数名と同じ名前のメンバー関数を派生クラスに追加すると名前の隠蔽が起こる。
using宣言を使うことで、メンバー関数をオーバーロードとして追加できる。
#include <iostream>
class Base
{
public:
void foo();
};
void Base::foo()
{
std::cout << "Base::foo()" << std::endl;
}
class Derived : public Base
{
public:
using Base::foo;
void foo(int v);
};
void Derived::foo(int i){
std::cout << "Derived::foo(int) override" << std::endl;
}
int main()
{
Derived derived;
derived.foo();
derived.foo(2);
}
3.5.4 純粋仮想関数と抽象クラス
純粋仮想関数: 派生クラスが必ずオーバーライドをして処理を書くように強制させる。
#include <iostream>
class Shape
{
public:
virtual float area() const = 0;
virtual float perimeter() const = 0;
};
class Rectangle : public Shape
{
float height;
float width;
public:
explicit Rectangle(float height, float width);
float area() const override;
float perimeter() const override;
};
Rectangle::Rectangle(float height, float width) : height(height), width(width)
{
}
float Rectangle::area() const
{
return height * width;
}
float Rectangle::perimeter() const
{
return 2 * (height + width);
}
class Circle : public Shape
{
float r;
public:
explicit Circle(float r);
float area() const override;
float perimeter() const override;
};
Circle::Circle(float r) : r(r)
{
}
float Circle::area() const
{
return r * r * 3.14f;
}
float Circle::perimeter() const
{
return 2 * r * 3.14f;
}
int main()
{
Rectangle rec(10, 2);
std::cout << rec.area() << std::endl;
std::cout << rec.perimeter() << std::endl;
Circle c(4);
std::cout << c.area() << std::endl;
std::cout << c.perimeter() << std::endl;
}
純粋仮想関数が宣言されたクラスは抽象クラスと呼ばれていて、抽象クラスだけではインスタンス化することはできない。