メンバ関数の外部化とフレンド関数

クラスの分割

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


#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();
}

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

クラス外のメンバ関数は、関数名の前に「::」で区切りクラス名を記述します。
これは「std::cout」などの「::」(スコープ解決演算子)と同じで、特定のクラス(名前空間)のメンバ関数であることを明示する役割があります。

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

ヘッダファイルに分離

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


//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言語編のソースコードの分割を参考にしてください。

フレンド関数

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

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

フレンド関数の使い道はかなり限定的で、効果的な使い方を例示することが難しいので機能の紹介だけに留めます。


#include <iostream>
#include <string>

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

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

//フレンド関数の定義
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メンバはもちろん、privateやprotectedメンバにもアクセスできます。

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 *c = 0)
    {
        number = n;
        name = c;
    }
};

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();
}

TestClassAの定義時点ではTestClassBの定義がまだ行われていませんから、TestClassAの定義にTestClassBを含めることはできません。
(TestClassBが関係する処理が書けない)
そのため、前方宣言というものを最初に指定しておきます。
前方宣言は単純に「class TestClassB;」とするだけです。

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

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