メンバ変数をprivate(またはprotect)とpublicのどちらで指定していますか

[戻る]
はまち 2002/04/04(木) 02:17:16
C++を勉強し始めて半年ぐらいたつものです。少しずつ、少しずつ勉強してきてなんとなく
ですが、「オブジェクト」の考え方がわかってきたのですが、ひとつ、どうしてもわからいない
ことがあります。それは、メンバ変数のカプセル化のことです。「カプセル化をすれば、クラス
を使う側から直接参照できなくなることで、オブジェクトを壊れにくいものにすることができ
る」と本には書いてあり、僕自身もそれに納得して、今まですべてのメンバ変数をprivate:で
指定していました。他のクラスから使いたいときは、そのprivateのメンバ変数を間接的に仲介
する
メンバ関数を作って使っていました。ですが、あるとき、スプリッタ-ウインドウを作ろうと
したとき、そのプログラム例では、スプリッタ-ウィンドウをpublicで指定していました。
きっと、参照することがよくあるからかなと、そのときは思ったのですが、実際、どうやって
publicとprivate(またはprotect)を使い分けていいのか、不思議に思いました。privateなど
だと、一つメンバ変数を宣言するごとに、間接的なメンバ関数も宣言しなくてはいけないと
思うので、数が増えてくると大変になってくると思います。ですが、オブジェクトを壊れにくい
ものにするために、privateにするというのもわかります。僕自身、まだ、プログラム経験が
浅く、クラスを継承しても1回ぐらいしか継承しないようなプログラムしか作ったことがないの
で、privateやprotectのよさがわからないだけなのかとも感じます。みなさんは、どのように
使い分けているのでしょうか。

みなさん、お忙しいとは思いますが、どんなことでもいいです。なにかわかったことや気づいた
点があれば教えてください。よろしくお願いします。

環境はVC++ 6.0 Win98SEです。

TAD [E-Mail] 2002/04/04(木) 04:49:58
簡単ではありますが、回答させていただきます。

初めの頃は、私も悩んだりしていたのですが、最近はあまり気にしなくなりました。
別に、実行ファイルに影響を与えるようなことではないので、はまちさんがいうように、他のク
ラスからアクセスされる可能性のある場合は public にしています。
ドキュメントクラスのメンバは、ビューから変更されるものは、ほとんど public です。ドキュ
メントクラス内部でだけ使用されるものは protected です。

ちなみに、スプリッターウィンドウは protected で使っています。スプリッターを操作するの
は、ほとんどがフレームウィンドウだからです。もし、外部から変更されることがあった場合で
も、フレームウィンドウクラスに、Set系の関数を追加するつもりでいます。(スプリッターに限
らず、子ウィンドウはほとんど protected 指定)

クリリン 2002/04/04(木) 11:13:30
初心者なりの解答ですが、解答させていただきます。

ある程度、私も開発をするようになって、解ったことがあります。
そこで、私のやり方を述べます。
基本的に、カプセル化したいメンバ変数の場合はprivateにおき、その変数の値を操作する場合
は、基本クラスであればprotectedに、通常のクラスであればpublicに、そのメンバ変数を取
得、削除、更新を行うメンバ関数を手間でも作成して、そのメンバ関数を使用して値の操作を行
っています。

参考までに、ADO操作の基本クラスの一部を表記します。
……上記省略
class CAdoOperation
{    
private:
  // --- メンバ変数
  _ConnectionPtr  m_pConnect;   // Connectionオブジェクト
  _CommandPtr     m_pCommand;   // Commandオブジェクト
  _RecordsetPtr   m_pRecordset; // Recordsetオブジェクト
protected:
  // --- メンバ関数
  BOOL    f_ConnectOpen();                      // DBのコネクトを開く
  void    f_ConnectClose();                     // DBのコネクトを閉じる
  BOOL    f_rOpen(_bstr_t &GetSqlScript);       // DBのレコードセットを開く
  BOOL    f_rDel(CString GetTableName, CString GetFieldName, CString 
GetCode); // レコード削除
  void    f_rClose();                           // DBのレコードセットを閉じる
  BOOL    f_rEOF();                             // EOF処理
  void    f_rMoveFirst();                       // 先頭のレコードへ
  void    f_rMoveNext();                        // 次のレコードへ
  int     f_rCount();                           // レコード数を取得
  BOOL    f_BeginTran();                        // DB前処理
  void    f_EndTran(BOOL &TranFlag);            // DB後処理
  CString f_GetFieldData(CString GetFieldName); // レコードデータ取得
……下記省略

長くなってすいませんm(__)m
また、おかしい場合は指摘してください。初心者なもので・・・

くたくた 2002/04/04(木) 11:13:37
>どうやってpublicとprivate(またはprotect)を使い分けていいのか、不思議に思いました。
私の場合、分からない時は暫定的に privateに配置 + アクセサ定義 にします。
で、後ではっきりした時に適切なアクセス宣言に修正するって感じです。

>一つメンバ変数を宣言するごとに、間接的なメンバ関数も宣言しなくてはいけないと
>思うので、数が増えてくると大変になってくると思います。
一見、アクセサの実装は量が多くて大変そうに思えます。
が、ヘッダで自動生成コードを用意すれば、以降は各アクセサ定義毎に1行で出来ます。
説明が長くなる為今回は書きません。万が一必要なら要求して下さい。

>privateやprotectのよさがわからないだけなのかとも感じます
私にとっての良さは、「追跡を簡単にしてくれる」です。
"このメンバは外からは参照できない"という言語保証がある為、
"注意を払うべきスコープを極力狭く"して追跡できます。

それ以外は一般に言われてる程の魅力を感じません。「コード量増加とそれに対する見返り」
を天秤にかけると 損である事が多いからです。

クリリン 2002/04/04(木) 11:27:15
追記:関数の一部がないとわかりずらいですよね…f_rOpenのメンバ関数を記述します。

BOOL CAdoOperation::f_rOpen(_bstr_t &GetSqlScript)
{
  try {
    // SQLの設定
    m_pCommand->ActiveConnection = m_pConnect;
    m_pCommand->CommandText = GetSqlScript; // 取得したSQL文をセットする
    m_pRecordset->PutRefSource(m_pCommand);
    // レコードセットを取得
    _variant_t vNull;  // VARIANT型のNULLとして使う
    vNull.vt = VT_ERROR;
    vNull.scode = DISP_E_PARAMNOTFOUND;
    m_pRecordset->Open(vNull, vNull, adOpenDynamic, 
                       adLockReadOnly, adCmdUnknown);
  }
  catch(_com_error &e){
    // データ操作失敗
    AfxMessageBox(e.Description());
    return FALSE;
  }
  return TRUE;
}

ADOの参考関連を見ると、データの操作を行うクラスにADO操作を直接記述しているのが多
いのですが、私としては、ADOの操作を基本クラスにする事でカプセル化を実現でき、またA
DO操作を意識しないコーディングが出来たと思います。

なぜ、長くなってまで発言をするかというと、VCをやり始めた頃の気持ちが痛いほどわかるか
らです。ですから、みなさん、怒らないでください・・・

不良PG 2002/04/04(木) 11:57:27
クリリンさんへ

>なぜ、長くなってまで発言をするかというと、VCをやり始めた頃の気持ちが痛いほどわかる
>からです。ですから、みなさん、怒らないでください・・・

こういう理由の発言でしたら、
ADOというものを例にあげるのでは無く、
一般的なオブジェクトモデルがわかりやすいものを例にあげるほうが理解しやすいと思います
たとえば車クラスとか複素数クラスとか・・等等

やり始めた人がADOを理解しているとは、とても思えません
ADOの動作をイメージできなければ、例をあげても意味がありません。

クリリンさんの気持ちも解りますが、ほどほどに・・(^^;

クリリン 2002/04/04(木) 12:51:55
すいませんm(__)m

蟹飯 2002/04/04(木) 13:58:49
はじめまして
私もひとこと

カプセル化とはメンバーをpublicにするとかprivateにするとかの問題ではないと思います。
要は、クラスの実装を不可視にすること、クラスの実装方法が変わっても外部に影響を与え
ないようにすることです。そのためには、クラスのデータの処理はクラスにまかせ切ること
です。

例えば、身長と体重をメンバーとする個人クラスがあるとします。
Class CPerson
{
public:
    int m_nHeight;  // 身長
    int m_nWeight;  // 体重
}
別のクラスで太りすぎかどうかを判定したいので体重と身長をpublicにする必要があります。

これをカプセル化されたクラスにするには、太りすぎを判定するpublic関数を追加して
Class CPerson
{
private:
    int m_nHeight;  // 身長
    int m_nWeight;  // 体重
public:
    BOOL IsFat();   // 太りすぎ判定関数
}
とすればいいのです。要はクラスのことはクラスにまかせるですね。

身長を知りたいときの関数
    int GetHeight() {return m_nHeight;}
も単にプライベートメンバーの値を返すという意味以上に、メンバーの実装方法が変わっても
関数の利用者には影響を与えないということの方が大事です。将来、身長がshortや構造体の
メンバとして実装されるようになっても影響を与えないということです。これがカプセル化だ
と思います。

はまち 2002/04/04(木) 23:58:58
TADさん、クリリンさん、くたくたさん、不良GPさん、蟹飯さん、みなさん、お返事ありがとう
ございました。みなさん、それぞれいろいろな使い方があるようですね。とても、参考になり
ます。基本的には、みなさん、はじめはメンバ変数をprivate:で指定して、間接的な関数を
public:かprotected:で指定するということでしょうか。クリリンさんのは、特にこれですよ
ね。僕も今まではこの方法でした。
 くたくたさんのも、この方法かと思ったのですが、くたくたさんの言うアクセサ定義と
言うのは、「アクセスする定義」という意味でしょうか。「アクセサってなんの略だろう、
アクセサリとかの意味じゃなさそうだし」とか思ったのですが、きっと本文から考えると、
やっぱり、アクセスの方でしょうか。

>一見、アクセサの実装は量が多くて大変そうに思えます。
>が、ヘッダで自動生成コードを用意すれば、以降は各アクセサ定義毎に1行で出来ます。
>説明が長くなる為今回は書きません。万が一必要なら要求して下さい。

この本文が非常に気になります。僕は今のところ、アクセス関数を作るとき、たとえば、
蟹飯さんのCPersonクラスが次のようになってたとします。(蟹飯さん使わせてもらいます!)

ヘッダーファイル
Class CPerson
{
public:
    int GetHeight();//m_nHeightを取得
    void SetHeight(int v);//m_nHeightを設定
private:
    int m_nHeight;  // 身長
}


ソースファイル
int CPerson::GetHeight() 
{
    return m_nHeight;
}

void CPerson::SetHeight(int v) 
{
    m_nHeight = v;
}

この状態で、メンバ変数m_nWeightを増やしたいとき、
(1)ヘッダーファイルでm_nWeightを宣言
(2)m_nHeightと同じint型だからそのままアクセス関数も活用できるので、コピー・ペースト
して、m_nHeightをm_nWeightに変えたり、GetHeight()をGetWeight()と関数の名前を変えた
する。
(3)同様に、ヘッダーファイルの方もコピー・ペースト+名前の変更をする。

という具合です。くたくたさんのはこれとは違うでしょうか。くたくたさんの方法もお聞きした
いですが、くたくたさんも書くのが大変だと思います。お暇なときにできれば教えていただける
とうれしいです。
 カプセルかについては蟹飯さんのいうとおりですね。とても、よくわかりました。特に

>も単にプライベートメンバーの値を返すという意味以上に、メンバーの実装方法が変わっても
>関数の利用者には影響を与えないということの方が大事です。将来、身長がshortや構造体の
>メンバとして実装されるようになっても影響を与えないということです。これがカプセル化だ
>と思います。

の部分はなるほどと思いました。確かに、のちのち身長が

CNagasa
{
private:
    int m_atama;//頭の上から肩まで
    int m_karada;//肩から腰まで
    int m_asi;//腰からつま先まで
}

のように細かくなってしまったとしても、アクセス関数は
CNagasa m_ngasa;//メンバ変数

int GetHeight() 
{
    return (m_ngasa.m_atama + m_ngasa.m_karada + m_nagasa.m_asi);
}

となるだけで、使う側のクラスは変更しなくてすみますもんね。それを考えると、やはり、
僕自身、まだ、こういったメンバオブジェクトを使ったような事がなかったので、カプセル化
のよさがわからなかったのかなとも思います。データを整理するくらいで使うような小さな
クラスなら、TADさんの言うように、publicにするのがいいのかもしれませんね。今まで、
何のために、毎回アクセス関数を作っていたのだろう、と悲しくなってしまいますが。

クリリンさんのいう、ADOやSQLというのは聞いたことがあったので、どういうことだろうと
理解しようとしてみたのですが、不良PGさんのいうように、すこし僕にはむずかしく、なかなか
全体像を把握するというところまではいけませんでした。ただ、他の人がどういう風に、
アクセス関数の部分を作っているのだろうというところを理解する上では、とても参考になり
ました。僕としてはなにか書いていただけるだけでヒントになってうれしいのでこれからも
よろしくお願いします。

クリリン 2002/04/05(金) 10:33:40
クリリンです。
お望みで、別のスレッドで質問していただけば、
ADOのソースと使用方法を記述します。
また、構造体の動的配列の使用方法(Vector版)も記述しますよ。

ちなみに、ヘッダーファイルだけでこういう使用方法もあります。
Class CPerson
{
public:
    // 取得
    int  GetHeight(){ return m_nHeight; }    // 身長取得
    int  GetWeight(){ return m_nWeight; }    // 体重取得
    // 設定(※ 引数を参照にしています)
    void SetHeight(int &v){ m_nHeight = v; } // 身長設定
    void SetWeight(int &v){ m_nWeight = v; } // 体重取得
private:
    int m_nHeight; // 身長
    int m_nWeight; // 体重
};

こうする事で、ただメンバ変数を操作をするだけのもので有れば、ソースファイルに記述しなく
ても済みますし、すっきりしたコーディング、かつ解り易いですよね!ただ、「仕様書工房」み
たいなソフトを使用している場合等は各関数毎にコメントを記述しないといけないので、返って
解り辛くなりますが…

はまち 2002/04/05(金) 14:33:35
こんにちは、クリリンさん。

>お望みで、別のスレッドで質問していただけば、
>ADOのソースと使用方法を記述します。
>また、構造体の動的配列の使用方法(Vector版)も記述しますよ。

ありがとうございます。クリリンさん。構造体の動的配列の使用方法っていうのはむずがし
そうですが、面白そうですね。newなんかの動的なメモリ割り当てと近いような話なので
しょうか。この辺は、まだまだ僕も苦手で目下、勉強中です。その前に、クリリンさんの
ヘッダーの使用方法はすごいですね!こういう風に書いてもいいものなんですね。しりません
でした。今思えば、蟹飯さんの書き方もこのような感じでしたし、もしかしたら、くたくたさん
のいう、アクセサ定義を1行で行う方法というのもこのことだったのではないかと思います。
これなら、今まで大変だったアクセス関数の定義が一段と楽になりそうです!

dairygoods 2002/04/05(金) 15:02:59
> void SetHeight(int &v){ m_nHeight = v; } // 身長設定

本題と関係ないですが、int&だと SetHeight(170) のように定数を
指定できなくなってしまいます。

void SetHeight(const int &v){ m_nHeight = v; } // 身長設定

ならOKです。

くたくた 2002/04/05(金) 17:56:17
>アクセサってなんの略だろう、
アクセス関数を気取った表現にした奴です。文字数が少ない為よく使います。

>アクセサリとかの意味じゃなさそうだし
いいとこ突いてきますね。

>この本文が非常に気になります。
単にタイピング量を減らす工夫に過ぎません。早速、コードを書きます。
------------------------- ヘッダ ここから --------------------------------
// 取得
#define ACCESSOR_G( NameV, NameT ) \
    inline NameT Get_##NameV( void ) const { return NameV; }

// 設定
#define ACCESSOR_S( NameV, NameT ) \
    inline void Set_##NameV( const NameT& r ){ NameV = r; }

// 両方
#define ACCESSOR( NameV, NameT ) \
    ACCESSOR_G( NameV, NameT )   \
    ACCESSOR_S( NameV, NameT )

// ※ NameV:メンバ変数名    NameT:メンバ変数の型名
------------- ヘッダ ここまで --- 実装 ここから ---------------------------
class a
{
    int  iA, iB;
public:
    ACCESSOR( iA, int )
    ACCESSOR( iB, int )
};
------------------------- 実装 ここまで ----------------------------------
これで a::Get_iA() a::Set_iA() a::Get_iB() a::Set_iB()が 実装されます。
クラスが何百個、何千個と増えた時、これだけで相当量のコード削減になります。
上記例では単純値型のアクセサ実装例でしたが、
複雑値型(クラスは配列等)でも適したACCESSOR()を用意すれば応用はききます。

こんな感じです。

はまち 2002/04/07(日) 16:26:28
くたくたさん、お返事、ありがとうございました。こちらからお聞きしておいて、返事が
遅くなりすみませんでした。僕自身、プリプロセッサ関係のことが苦手で、本を読みながら
解釈していたので遅くなってしまいました。
 はじめは、どういうことなのだろうかと、なかなか意味がわからなかったのですが、実際、
コードを記述しながら解釈してみると、とてもビックリしました。こんな書き方もあるんです
ね。確かに、これなら、メンバ変数を定義した後、ACCESSOR()を一行定義するだけできます。
僕のあるクラスで、30個ほどのメンバ変数と、そのアクセス関数(約50個)をもつクラス
があったのですが、今まで、何のためにこんなことをしていたのかと思ってしまいます。

できれば、もう一つお聞きしたいことがあります。今、くたくたさんのを応用させて、ポインタ
を取得させるものを、みようみまねで作ったのですが、

//ポインタ取得
#define ACCESSOR_GP( NameV, NameT ) \
    inline NameT##* GetP_##NameV( void ) const { return &##NameV; }

// 3つ
#define ACCESSOR( NameV, NameT ) \
    ACCESSOR_G( NameV, NameT )   \
    ACCESSOR_S( NameV, NameT )   \
    ACCESSOR_GP( NameV, NameT )

うまく行きそうでなぜかうまく行きません。何か根本的な間違いがあるのでしょうか。
くたくたさんもお忙しいかもしれません。他の方でもしわかる方がいるようでしたら、
よろしくお願いします。

くたくた 2002/04/07(日) 18:09:28
>   inline NameT##* GetP_##NameV( void ) const { return &##NameV; }
constを取り去って下さい。これで期待通りの結果になります。
原因は御自分で考えてみて下さい。
次に、Name##* と &##NameV の##は不要です。これも考えてみて下さい。

ヒントを貰ったら残りは絶対に自力で解決して下さい。初心者か否かは無関係です。
「どうしても駄目なら諦める」これも大切な経験です。(怒って言ってるのではないですよ)

はまち 2002/04/07(日) 23:45:28
[[解決]]
申し訳ありません、くたくたさん。できるだけ自力で解こうと気をつけてはいたんですが。
これからはもう少し気をつけていこうと思います。すみませんでした。

古いスレッドには追加発言できません。