メンバ関数の機能

メンバ関数の外部定義

クラスにはメンバ変数やメンバ関数などを多数記述しますが、機能が多くなってくるとクラス定義が肥大化し、見にくくなってきます。
そのような場合にはクラスの関数定義をクラス外に分けて記述することができます。


#include <iostream>
#include <string>

class TestClass
{
    int number;
    std::string name;

public:
	//クラス内は全て関数の宣言(定義はもたない)

    explicit TestClass(int n, const char *s);

    int getNumber();
    void setNumber(int n);

    std::string getName();
    void setName(const char *s);

    void setData(int n, const char *s);
    void printData();
};

//ここからクラスのメンバ関数の定義
TestClass::TestClass(int n = 0, const char* s = "")
    :number(n), name(s) {}

int TestClass::getNumber() { return number; }
void TestClass::setNumber(int n) { number = n; }

std::string TestClass::getName() { return name; }
void TestClass::setName(const char *s) { name = s; }

void TestClass::setData(int n, const char *s)
{
    number = n;
    name = s;
}

void TestClass::printData()
{
    std::cout << "number: " << number << std::endl;
    std::cout << "name: " << name << std::endl;
}
//ここまでクラスのメンバ関数の定義

int main()
{
    TestClass tc(0, "John");

    tc.printData();

    std::cin.get();
}

クラスのブロック内の定義ではメンバ関数のプロトタイプ宣言だけを記述します。
具体的な動作の定義はクラスのブロック外で行います。

クラス外のメンバ関数定義では、クラス名::関数名(引数リスト){}という形式で行います。

これらはクラスのブロック外に書かれてますが、あくまでもクラスのメンバ関数であることに注意してください。
そのクラスを通さずに関数を呼び出すことはできません。

メンバ関数とインライン関数

メンバ関数をクラス内で定義すると、暗黙的にinlineキーワードが指定されたものとして扱われます。
定義をクラス外で行うと非inline関数となります。
もちろん必要ならinlineキーワードを指定することもできます。


class TestClass
{
    int n;

public:
	//inline
    TestClass(int n = 0)
        :n(n) {}

	//inline
    int getNumber() { return n; };
	//inline
    void setNumber(int n) { this-&gt;n = n; };

	//非inline
    void printData();
};

//非inline
void TestClass::printData()
{
    std::cout &lt;&lt; "n: " &lt;&lt; n &lt;&lt; std::endl;
}

inlineキーワードが指定されたものとして扱われるだけで、必ずインライン関数化されるわけではないのは通常の関数のときと同じです。

ヘッダファイルに分離

クラスのメンバ関数の分割は、以下のようにヘッダファイルとソースファイルとに分離して記述するのが一般的です。


//testclass.h

#pragma once

#include <string>

class TestClass
{
    int number;
    std::string name;

public:
    explicit TestClass(int n, const char *s);

    int getNumber();
    void setNumber(int n);

    std::string getName();
    void setName(const char *s);

    void setData(int n, const char *s);
    void printData();
};

//testclass.cpp

#include <iostream>
#include <string>
#include "testclass.h"

TestClass::TestClass(int n = 0, const char* s = "")
    :number(n), name(s) {}

int TestClass::getNumber() { return number; }
void TestClass::setNumber(int n) { number = n; }

std::string TestClass::getName() { return name; }
void TestClass::setName(const char *s) { name = s; }

void TestClass::setData(int n, const char *s)
{
    number = n;
    name = s;
}

void TestClass::printData()
{
    std::cout << "number: " << number << std::endl;
    std::cout << "name: " << name << std::endl;
}

//main.cpp

#include <iostream>
#include "testclass.h"

int main()
{
    TestClass tc(0, "John");

    tc.printData();

    std::cin.get();
}

処理はページ最初のサンプルコードと同じです。

なお、Visual Studioでのファイル分割方法はC言語編のソースコードの分割を参考にしてください。

constメンバ関数

クラスのメンバ関数には、関数そのものにconstというキーワードを付けることができます。


class TestClass
{
    int number;

public:
    explicit TestClass(int n)
        :number(n) {};

	//constメンバ関数
    int getNumber() const { return number; };

    int setNumber(int n) { number = n; }
};

メンバ関数getNumterは、メンバ変数をそのまま返すだけでメンバ変数の値を変更することはありません。
このような関数には関数の引数リストの後ろにconstを付けることでconstメンバ関数にすることができます。

constメンバ関数は、その関数内の処理でメンバ変数が変更されないことが保証されます。
constメンバ関数内でメンバ変数を書き換えようとするとコンパイルエラーになります。
(引数やローカル変数は書き換え可能です)

通常のメンバ関数はメンバ変数を書き換える可能性があります。
constメンバ関数は実行の前後でメンバ変数が変更されないことが保証されていなければならないので、constメンバ関数からは非constメンバ関数は呼び出せないように制限されます。


class TestClass
{
    int number;

public:
    explicit TestClass(int n)
        :number(n) {};

    //constメンバ関数
    int getNumber() const 
    { 
        //コンパイルエラー
        //setNumber(0); 

        //他のconstメンバ関数は呼び出せる
        printNumber();
        return number; 
    };

	//非constメンバ関数
    int setNumber(int n) { number = n; }

	//constメンバ関数
    void printNumber() const
    {
        std::cout << number << std::endl;
    }
};

なお、クラス型変数(インスタンス)をconst修飾している場合は値を書き換えることはできないので、非constメンバ関数を呼び出すことはできません。
そのため、メンバ変数を書き替えないメンバ関数には積極的にconstを付けておくことをお勧めします。


class TestClass
{
	int number = 10;

public:
	void func() 
	{
		number = 0;
	}

	void funcConst() const
	{
	}
};

int main()
{
	TestClass tc1;
	const TestClass tc2;

	tc1.func();
	tc1.funcConst();

	//tc2.func(); //非constメンバ関数は実行できない
	tc2.funcConst();
}

mutable

constメンバ関数からはメンバ変数を書き換えることはできませんが、mutableキーワードを使用して特別に書き換え可能なメンバ変数を作ることができます。


class TestClass
{
    int number1;
    mutable int number2; //constメンバ関数からも書き換え可能

public:
    explicit TestClass(int n)
        :number1(n) {};

    int getNumber() const 
    {
        number2 = number1;
        return number1;
    };
    int setNumber(int n) { number1 = n; }
};

ただし静的メンバ変数(static)はmutableにすることはできません。
静的メンバ変数についてはstaticの項で説明します。

フレンド関数

C++にはフレンド関数という機能があります。
一見するとメンバ関数の外部定義と似ていますが、別の機能です。

フレンド関数は、クラスのメンバ変数にアクセスできるメンバ関数ではない関数(つまり外部関数)を作ることができる機能です。

フレンド関数の使い道はかなり限定的で、効果的な使い方を例示することが難しいのでここでは機能の紹介だけに留めます。
(演算子のオーバーロードではフレンド関数を利用することがあります)


#include <iostream>
#include <string>

class TestClass
{
    //フレンド関数の指定
    friend void printData(const TestClass&);
    int number;
    std::string name;

public:
	TestClass(int n = 0, const char* s = "")
    	:number(n), name(s) {}
};

//フレンド関数の定義
void printData(const TestClass &c)
{
    std::cout << "number: " << c.number << std::endl;
    std::cout << "name: " << c.name << std::endl;
}

int main()
{
    TestClass tc(1, "John");
    printData(tc);

    std::cin.get();
}

フレンド関数はメンバ関数の前にfriendというキーワードを付加して定義します。
関数の定義はクラス内では行わず、クラス外で行います。

フレンド関数は、クラスのメンバ関数ではなくあくまでも通常の関数です。
メンバ関数の外部化の時のように、クラス名::関数名というような形式は使用しません。
フレンド関数は、クラスのメンバ関数ではない通常の関数に、メンバ変数へのアクセス権を与える機能です。
publicメンバはもちろん、privateprotectedメンバにもアクセスできます。

上記コードのTestClassはprivateなメンバ変数にアクセスする機能(ゲッター、セッター)を持っていませんが、フレンド関数printDataを通してprivateメンバにアクセスしています。

複数のクラスで共通のフレンド関数

フレンド関数は、複数のクラスで同じものを定義することもできます。


#include <iostream>
#include <string>

//前方宣言
class TestClassB;

class TestClassA
{
    //フレンド関数
    friend void printData(const TestClassA&, const TestClassB&);

    int number;
    std::string name;

public:
	TestClassA(int n = 0, const char* s = "")
    	:number(n), name(s) {}
};

class TestClassB
{
    //フレンド関数
    friend void printData(const TestClassA&, const TestClassB&);

    void print() const
    {
        std::cout << "TestClassB" << std::endl;
    }
};

//フレンド関数の実装
void printData(const TestClassA &a, const TestClassB &b)
{
    std::cout << "number: " << a.number << std::endl;
    std::cout << "name: " << a.name << std::endl;
    b.print();
}

int main()
{
    TestClassA tcA(1, "John");
    TestClassB tcB;
    printData(tcA, tcB);

    std::cin.get();
}

両方のクラスから指定されたフレンド関数printDataは、両方のクラスのprivateメンバにアクセスできます。
フレンド関数はメンバ変数だけでなく、メンバ関数を呼び出すこともできます。

フレンド関数は、本来は非公開であるはずのprivate領域のメンバにアクセスを許してしまうので、使うべきではないという主張もあります。
C++以外のオブジェクト指向をサポートする言語ではこのような機能はほとんどなく、フレンドのような機能がなくても問題なくプログラミングができるということです。
privateメンバにアクセスするならゲッターとセッターを定義した方が分かりやすく、またほとんどの場合でそれで足りるでしょう。

前方宣言

上記コードでは、TestClassAの定義の時点ではTestClassBの定義がまだ行われていませんから、そのままではTestClassAの定義にTestClassBを含めることはできません。
(TestClassBが存在することをコンパイラは知らないので、TestClassBに関係する処理が書けない)
これは前方宣言というものを使用することで解決できます。
前方宣言はclass TestClassB;とするだけで、これ以降の行では「TestClassBという名前のクラス」が存在することをコンパイラに知らせることができます。

ただし実装はまだ行っていないので、TestClassBのインスタンスを生成したり、具体的な機能を使用したりすることはできません。
また、前方宣言をしただけで実際にTestClassBクラスを定義しないとコンパイルエラーになります。