目次
一、はじめに
二、C++におけるstructの変化
三、クラスの定義
四、アクセス修飾子
五、カプセル化
六、クラスのインスタンス化
七、クラスオブジェクトのモデル
7.1 クラスオブジェクトのサイズの計算方法
7.2 クラスオブジェクトの保存方法
八、thisポインタ
8.1 thisポインタの用途
8.2 thisポインタの特性
一、はじめに
C言語は手続き型言語であり、問題解決のプロセスに焦点を当て、関数呼び出しを通じて段階的に問題を解決します。
C++はC言語の基盤にオブジェクト指向プログラミングを追加し、ある事柄を異なるオブジェクトに分割し、オブジェクト間の相互作用に焦点を当てます。
C++では、万物がオブジェクトであり、それぞれに**属性**と**振る舞い**があると考えられています。例えば:
人間はオブジェクトとして、属性には名前、年齢、身長...、振る舞いには歩く、走る、跳ぶ、話す...があります。
車もオブジェクトとして、属性にはタイヤ、ステアリング、ライト...、振る舞いには人を乗せる、音楽を流す...があります。
同じ性質を持つオブジェクトはクラスとして抽象化できます。異なる人間は人間クラスに属し、車は車クラスに属します。
クラスはC++の核心的な特性であり、ユーザー定義型と呼ばれ、データと関数をカプセル化したものです。
クラス内のデータはメンバ変数、関数はメンバ関数と呼ばれます。
二、C++におけるstructの変化
C言語では、structで定義された構造体内では変数のみを定義できます。しかしC++では、構造体内では変数だけでなく関数も定義できます。
なぜなら、C++ではstructもクラスを定義するために使用できるからです。
例えば、スタックを実装する場合、C言語では各種インターフェース関数を構造体の外で定義する必要がありますが、C++で実装すると構造体内でも関数を定義できることがわかります。例:
struct MyStack
{
void Initialize(int capacity = 4)
{
data = (int*)malloc(sizeof(int) * capacity);
if (nullptr == data)
{
perror("malloc failed");
return;
}
maxSize = capacity;
currentSize = 0;
}
void Push(int value)
{
//...
data[currentSize] = value;
++currentSize;
}
//...
int* data;
int maxSize;
int currentSize;
};
int main()
{
MyStack s;
s.Initialize();
s.Push(1);
s.Push(2);
s.Push(3);
return 0;
}
また、C言語では構造体の型名はstructを含みますが、C++では型名はstructを含みません。
三、クラスの定義
C++ではstructでクラスを定義できますが、通常はclassを使用することを好みます。
class ClassIdentifier
{
// クラス本体:メンバ関数とメンバ変数で構成される
};
classはクラスを定義する**キーワード**で、ClassIdentifierはクラス名、中括弧内がクラスの本体です。クラスの後のセミコロンは省略できません。
クラス本体の内容はクラスのメンバであり、その変数は**クラスの属性**またはメンバ変数、関数は**クラスのメソッド**またはメンバ関数と呼ばれます。
クラスには2つの定義方式があります:
(1)メンバ関数の宣言と定義をすべてクラス本体に配置する方法(上記のスタックの定義を参照)
(2)メンバ関数の宣言と定義を分離し、ヘッダファイルでクラスを定義し、関数の宣言をクラス本体に配置し、ソースファイルでインターフェース関数を定義する方法:
注意点として、クラスを定義すると、クラススコープが作成されます。2番目の方法でクラスを定義する場合、メンバ関数名の前にクラス名とスコープ解決演算子を付ける必要があります。
また、メンバ変数を命名する際は、前にアンダースコア_を付けることを推奨します。これにより、仮引数との区別が容易になります。
例:
class Calendar
{
void Setup(int year)
{
// このyearはメンバ変数か、関数の仮引数か?
year = year;
}
int year;
};
前にアンダースコアを付けて区別すれば、このような尴尬な状況を避けることができます。もちろん、他の方法で区別することも選択できます。
しかし、上記の2番目のクラス定義方法で関数を使用すると、以下のような状況が発生します。
これはC++のアクセス修飾子に関連しています。
四、アクセス修飾子
C++には3つのアクセス修飾子があります:**public**(公開)、**protected**(保護)、**private**(非公開)
その中で、C++はC言語との互換性を保つ必要があるため、structのメンバはデフォルトでpublicであり、classのメンバはデフォルトでprivateであるため、上記のような状況が発生します。
アクセス修飾子に関する説明:
- publicで修飾されたメンバはクラス外から直接アクセスできますが、protectedとprivateで修飾されたメンバはアクセスできません
- アクセス修飾子のスコープは、その修飾子の位置から次の修飾子が現れるまで続き、後ろにアクセス修飾子がない場合は中括弧が現れるまで終了します
メンバ関数をpublicで修飾すると、クラス外からアクセスできるようになります。
五、カプセル化
C++のオブジェクト指向の3大特性:カプセル化、継承、多態性
クラスとオブジェクトの学習段階では、主にクラスのカプセル化特性を理解します。
カプセル化:**データ**とデータを操作する**メソッド**を結合し、オブジェクトの属性と実装の詳細を隠蔽し、外部にインターフェースを公開してオブジェクトと対話することです。
例えば、コンピュータはユーザーとの対話に各種ボタンとポートを提供しますが、実際の作業は内部の各種ハードウェアが担っています。ユーザーにとっては、内部の構造がどうなっているかを知る必要はなく、どう使うかを知るだけで十分です。
C++では、クラスを使用してデータとメソッドを結合し、アクセス権を調整してオブジェクトの内部の詳細を隠蔽し、メンバ関数の権限を開放して使用可能にすることは、カプセル化の一種です。
六、クラスのインスタンス化
クラスを使用してオブジェクトを作成するプロセスを、クラスのインスタンス化と呼びます。
クラスは設計図のようなもので、クラスにどのようなメンバがあるかを定義しますが、実際のメモリ領域を割り当ててデータを保存しているわけではありません。設計図に基づいて家を建てる、つまり具体的なオブジェクトをインスタンス化するのです。
以前に定義したStackクラスには空間はなく、Stackクラスからインスタンス化されたオブジェクトのみが具体的なメモリ領域を占有します。
七、クラスオブジェクトのモデル
7.1 クラスオブジェクトのサイズの計算方法
質問:Stackクラスからインスタンス化されたオブジェクトのサイズはどれくらいでしょうか?
答え:12です。これから推測できるように、クラスのサイズも**メモリアライメント**ルールに従っており、メンバ変数のみを計算し、メンバ関数は計算しません。
7.2 クラスオブジェクトの保存方法
なぜクラスオブジェクトのサイズを計算する際にメンバ関数を含まないのでしょうか?
同一クラスの各オブジェクトのメンバ変数の値は異なるため、独立して保存する必要があります。
しかし、各オブジェクトが呼び出すメンバ関数は同じです。メンバ関数をオブジェクト内に保存すると、オブジェクトのメモリ領域を占有することになり、クラスから複数のオブジェクトをインスタンス化する場合、各オブジェクトがコードを1つずつ保存してしまい、メモリ領域の無駄になります。
そのため、メンバ関数は共通のコード領域に保存します。
では、以下の2つのクラスのサイズはどれくらいでしょうか?
class SampleClass1
{
public:
void Method()
{
;
}
};
class SampleClass2
{
};
答え:1です。
クラスにメンバ関数のみがある場合と、何もないクラスでも、オブジェクトがインスタンス化されたことを示すために1バイトのメモリ領域が割り当てられます。
この1バイトの空間には有効なデータは保存されません。
八、thisポインタ
今では、C++ではメンバ変数とメンバ関数は分離して保存されていることがわかっています。では、メンバ関数を呼び出すとき、関数はどのようにしてどのオブジェクトが自分を呼び出した区別するのでしょうか?
C++では、thisポインタがこの問題を解決します。thisポインタは関数を呼び出したオブジェクトを指し、**暗黙的**に関数に渡されます。
我々はパラメータリストにthisポインタを明示的に追加することはできません。コンパイラが自動的に処理します。
8.1 thisポインタの用途
(1)仮引数とメンバ変数が同名の場合、thisポインタで区別できます
(2)クラスの非静的メンバ関数内でオブジェクト自体を返す場合
8.2 thisポインタの特性
(1)thisポインタの実体はポインタ定数です:const Type* const pointer、そのためメンバ関数内でthisポインタに代入することはできません
(2)thisポインタは非静的メンバ関数内でのみ使用でき、グローバル関数や静的メンバ関数では使用できません
(3)thisポインタの実体はメンバ関数の仮引数であり、オブジェクトがメンバ関数を呼び出すとき、コンパイラはオブジェクトのアドレスを実引数としてthis仮引数に渡すため、thisポインタは**オブジェクト内に保存されていません!**
(4)thisポインタはコンパイラによって保存場所が異なり、通常はスタックに保存されます。Visual Studioではecxレジスタに保存されます。
thisポインタはnullになることがありますか?
質問:以下の場合、プログラムはエラー、クラッシュ、または正常に動作するでしょうか?
class MyClass
{
public:
void Func()
{
cout << "正常に実行" << endl;
}
};
int main()
{
MyClass* ptr = nullptr;
ptr->Func();
return 0;
}
答え:正常に実行します。
では、以下の場合はどうでしょうか?
class MyClass
{
public:
void Init()
{
cout << "年: " << _year << endl;
cout << "月: " << _month << endl;
cout << "日: " << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
MyClass* ptr = nullptr;
ptr->Init();
return 0;
}
答え:クラッシュします。
なぜ最初のケースは正常に実行でき、2番目のケースはクラッシュするのでしょうか?
矢印が出現しても必ずしもデリファレンスされるわけではないため、Funcはオブジェクト内にないため、nullポインタptrに対するデリファレンスは行わず、正常に実行できます。一方、Initではデリファレンスが行われ、nullポインタにアクセスするため、クラッシュします。
デリファレンスするかどうかは、右側にアクセスするものがオブジェクト内にあるかどうかによって決まり、記号によって判断するわけではありません。