私は元々1985年頃からC言語(8bitのBDS-C)を使うようになり、その後米国駐在時代にC++(Borland Turbo C++ for DOS)を知るようになりました。何れの言語もアセンブラーに近く、ハードウェアの動きを知らないと正しくプログラミングできないような低級言語でしたので、「プログラミングミス == コンピューターの暴走」というのが当時の常識でした。
所がOS(Windows)の高度化、堅牢化により、「コンピューターの暴走」は先ずなくなり、エラー表示(+実行スレッドの終了)だけで済んでしまいますし、MicrosoftのCLI(Common Language Infrastructure)のおかげで、C的言語でもプログラマー自身がメモリー管理を行わず、CLIのランタイム(CLR)が自動的にGC(Garbage Collection)を行ってくれます。
しかし、
その結果プログラムされたコードをコンピューターがどのように処理(特にメモリー管理)しているかがプログラマーには直観(直感ではありません)できず、自分で確認することが必要となります。
例えば、
C#で、単純に
【C#】
int i = 10; または (宣言後)i = 10;
string str = "Hello, world!"; または (宣言後)str = "Hello, world!";
という整数型や文字列型オブジェクト(インスタンス)に「変数宣言、初期設定または代入」を行う式を見ていると、C++の同様の式、
【C++】
int i = 10; または (宣言後)i = 10;
char* str = "Hello, world!"; または (宣言後)str = "Hello, world!";
(char str[14] = "Hello, world!"; または (宣言後)strcopy(str, "Hello, world!"; //文字列13文字 + ヌル終端)
を思い浮かべてしまいますが、実は全く違います。C++では宣言時にメモリーにint型変数の値の長さ分のメモリーを(newやmalloc等でヒープ領域に確保しないのであれば)スタック領域に確保し、そこに(システムによって異なるが32ビットの)'000AH'(十進法の10)を、char*は他に確保した文字列(char[文字数+1]配列)のメモリーアドレスを参照するポインター変数(長さはアドレス長)でしかないのですが、C#では"int"はInt32という構造体の、stringもStringクラスの別名(alias)です。(注)従って"="という等号記号を用いていますが、実は初期化や値取得のメソッド等を実行しています。次は(初期化をどうやっているのかはよくわかりませんでしたが)大体↑のC#のプログラムと同じようなものです。
注:構造体とクラスの重要な相違は↓で触れます。
Int32 i = new Int32();
i = 10;
//ご参考:これとかこれ。
//String str = new String("Hello, world!"); //「string' から 'char*' に変換できません」エラー
String str = new String("Hello, world!".ToCharArray()); //.ToCharArrayはStringクラスのメソッド
str = "Hello, world!";
//ご参考:これ
なお、以降は構造体は除けておき、クラスに話を絞ります。(注)
注:構造体とクラスの対比を以下に載せておきます。
| 構造体 | クラス | |
|---|---|---|
| オブジェクトの型 | 値型 | 参照型 |
| クラスの継承 | 不可 | 可能 |
| フィールド初期化子の使用 | 不可 | 可能 |
| デフォルトコンストラクタ(引数なし)の定義 | 不可 | 可能 |
| デストラクタの定義 | 不可 | 可能 |
で、私が興味を持ったのは
C#はC++と似たようなコードでも、実際にはメモリーを直接アクセスするようなことはせず、
【値保有-newでメモリー領域を確保する】
クラス名 インスタンス名 = new クラスコンストラクター; //コンストラクターに引数可、オーバーロード可
【値参照-データメモリー領域は確保しない】
クラス名 インスタンス名 = (他の)クラスインスタンス; //他のクラスインスタンスを参照するだけ
という点です。C++にも似たようなところがありますが、
【値保有-newでメモリー領域を確保する】
クラス名 インスタンス名 = new クラスコンストラクター; //コンストラクターに引数可、オーバーロード可
(C++ではnewしてヒープメモリーを確保したら、必ずdeleteでメモリーを開放しないとメモリーリークになる。)
【値参照-データのメモリー領域は確保しない】
クラス名* インスタンス名 = (他の)クラスインスタンス; //他のクラスインスタンスを参照するだけ
(C++では参照はポインター(型名+'*')を使い、他でメモリー領域を確保したインスタンスのアドレスが入る。)
「C++で参照はポインター(*)であることがコード上、一目瞭然ですが、C#では一見違いが分からない」ことです。
このことから、参照型のインスタンスの参照先をGCに出してしまうと「オブジェクト参照がオブジェクトインスタンスに設定されていません。」エラーとなるので注意が必要です。又、値保有型のインスタンスに'='等号記号で代入を行っても、必ずしも代入元のメンバーの値が代入先にコピーされるわけではないので注意しないとなりません。
C++とC#の「参照」の主な違い(ざっくり比較)
| 観点 | C++ | C# |
|---|---|---|
| オブジェクトの標準的な扱い |
値型(スタックに置かれる) |
参照型(ヒープに置かれる) |
= の意味(クラスの場合) |
コピー(明示的にコピーコンストラクタ) |
参照のコピー(ポインタのようなもの) |
| メモリ管理 |
手動(new+delete) |
自動(GC) |
構造体(struct) |
通常でもメンバー関数を持てる(やや柔軟) |
値型、基本的に軽量でコピー主体 |
| デフォルトのコピー動作 |
シャローコピー (必要に応じて明示的に定義) |
シャローコピー ( classでは参照、structではコピー) |
と、まぁ、こんなことを偉そうに書いているのは、
「現在CのプログラムをC#に移植しようとして、ポインター引数をrefで書いたり、メソッド内でデータを保有しているのか、参照しているのか、に注意を払っている」ので、皆さんと注意を共有したいと考えただけです。
悪しからず。
ps. 上を書いたのは結構前なので、C#のクラスと構造体の違いを補足すると「(名前の付いた)変数が参照(変数のデータはアドレス)するのは、クラスのインスタンスではヒープ領域のメモリー、構造体はスタックに積まれたメモリーだそうです。(従ってCやC++と同じく、プログラムブロックを抜けると消滅する揮発性があります。)
