C言語からC++言語への移行
本節では、C言語とC++言語の非互換項目のうち、特に間違え易い項目について解説する。本稿では、以下の5項目について述べる。
- const 型修飾子
- void型へのポインタ
- 列挙型変数
- プロトタイプ宣言
- 構造体、共用体型のタグのスコープ
[const 型修飾子]
大きな非互換項目の一つにconst型修飾子の取り扱いの違いがある。C言語では記憶域クラス指定子を持たず、const型修飾されたファイル有効範囲の識別子は、外部結合の識別子として取り扱われる。一方、C++言語では内部結合の識別子として取り扱われる。つまりC言語ではexternが補完されるのに対し、C++言語ではstaticが補完されると考えればわかりやすい。多くのプログラマは、C言語でのプログラミングにおいて、extern型修飾子は、変数の外部参照宣言を行なう時にのみ使用し、変数の定義においては使用していない。この問題は、const型修飾子を使用している多くのプログラムで発生すると予想される。
例: (プログラム上の記述)
const int a=10;
(C言語で認識される型)
extern const int a=10;
(C++言語で認識される型)
static const int a=10;
このため、外部結合が必要なconst型修飾された変数には、すべてextern記憶域クラス指定子を使用する必要がある。
[void型へのポインタ]
void型へのポインタの取り扱いにも大きな違いがある。C++言語では、void型へのポインタをvoid型以外へのポインタ型の変数に代入するときには、明示的に代入先の変数の型にキャストしなければならない。他方、C言語の標準ライブラリの多くは、void型へのポインタを返す。これらの値をvoid型以外へのポインタ型の変数に代入する時には明示的なキャストが必要となる。
[例] int* p;
p = malloc(40);
malloc は void へのポインタを返すため、intへのポインタ型のキャストが必要。
このような記述はC言語でごく一般的に行なわれるが、これをC++処理系が見つけると、必ずエラーもしくは警告メッセージを生成する。従って、面倒ではあるが、確実にそして容易にプログラムを修正することができる。void型へのポインタを返すC言語の標準ライブラリには以下のものがある。
calloc, malloc, realloc, bsearch, memcpy, memmove, memchr,memset
[列挙型変数]
列挙型変数の取り扱いにも違いがある。列挙型変数に対し、その列挙型以外の型の値を代入することはできない。このため、列挙型変数に対し単項演算子++,--をつけることはできない。また、複合代入を行なうこともできない。
[例] enum RBG { red, green, blue } rgb;
rgb++;
rgb++ は rgb = rgb + 1 と同じであり、rgb+1の演算結果(int型)の列挙型への代入となるためエラーとなる。
マクロに対するデバッグ情報を生成できない処理系が多いため、プログラマは、デファインによる定数の定義の代わりにenumによる定数の定義を利用するケースがある。メンバのみの利用にとどめれば問題はないが、同時に変数を使用すると、問題になるだろう。いずれにせよ、この問題はvoid型へのポインタと同様、コンパイラのエラーメッセージが解決してくれるだろう。
[プロトタイプ宣言]
プロトタイプ宣言では、エラ−チェックと引数省略時の扱いに違いがある。C++言語では、関数呼び出しを行なう前に、必ずプロトタイプ宣言を行なう必要がある。また、関数の型チェックは、コンパイル単位を越えて行うことがでいる。C言語処理系では、複数のオブジェクトをリンクする時に、シンボル名が一致しているかどうかのチェックしか行なえなかったが、C++言語では型チェックまで行う。更に、C++言語では、プロトタイプ宣言の際に引数を書かなければ、引数が無い('void'型である)とみなす。
[例] extern void func();
....
sub();
func();
関数sub
の呼び出しは、プロトタイプ宣言がないためエラーとなる。関数funcはプロトタイプ宣言に引数が書かれていないため、void型の引数であると解釈される。
関数を呼び出す際には、必ずプロトタイプ宣言を行わなければならない。また、引数を記述しないと引数は存在しないと解釈される。
[構造体、共用体型のタグのスコープ]
構造体、共用体型の取り扱いにも、違いがある。構造体/共用体型の型定義の中で定義された構造体/共用体型タグの有効範囲は、C言語では外部だが、C++言語ではその構造体/共用体型の型定義の中のみとなる。
[例] struct st1 {
int member1;
struct st2 {
int member21;
} member2;
int member3;
};
st2 は、st1 の中のみで有効であり、指定無しにst1
の外で使用することができない。
ここで挙げた例以外にもいくつかの違いがあるが、いずれもコンパイラのエラーメッセージや記述方法の手がかりから、比較的容易に解決できるものがほとんどである。また、これらの点に留意すれば、C言語で記述されたプログラムをそのままC++言語処理系で処理することも可能である。C言語でのプログラミングの観点からは、むしろ、より高度な型チェックなどの結果、プログラムの品質は向上する。今後はC++言語処理系を使うC言語プログラマーという形態が増えてもおかしくはない。
性能を考慮したコーディング
C++言語で組込みプログラムを開発するときの問題点として、C言語にくらべてプログラム容量が増加したり実行性能が低下するということがある。 確かに、C++言語のうち、C言語と同じ言語仕様の範囲でプログラムを記述していればこういった問題は発生しない。しかしそれでは
C++ 言語を使用する意義がなくなってしまう。 Embedded C++では、C++プログラム実行のオーバヘッドを削減するために言語機能を制約している。しかし、オブジェクト志向プログラミングに必須であるクラス機能を使用するだけでも、使い方によっては、変数のダイナミックな割り付けや初期化が発生し、メモリサイズ、実効性能の低下につながる。
Embedded
C++ガイドライン性能編では、こういったダイナミックなプログラムの挙動によってプログラム性能が低下する原因と、その対策を解説した。以下にその項目を列挙し、代表的な項目について解説する。今後は処理系の実現などに合わせて充実を図るべき項目であると委員会でも認識している。
- オブジェクトの初期化形式の選択方法
- inline 指定子の利用方法
- 引数と一時オブジェクトの関係
- グローバルオブジェクトの初期化の順序
- クラスの配列のnew/delete
- ループ中のクラス型変数定義
[オブジェクトの初期化形式の選択方法]
オブジェクトを初期化する記述方法には様々な形式があり、初期化の記述方法によっては、不必要な一時オブジェクトが生成され、コードサイズが増える場合がある。
例
T x(i) // (1)
T x= i; // (2)
T x = T(i) // (3)
T x; // (4)
x=i; //
上の4種類の初期化をそれぞれ評価すると、以下のようになる。
(1) 一時オブジェクトを用いずにオブジェクトを初期化する。
呼び出しコード
x.T(i); //コンストラクタ
(2) 一時オブジェクト用いてオブジェクトを生成する場合がある。(一時オブジェクトを生成しない処理系もある)
呼び出しコード
temp.T(i); //コンストラクタ
x.operator=(temp); //代入演算子
(3) 一時オブジェクトを用いてオブジェクトを生成する場合がある。(一時オブジェクトを生成しない処理系もある)
呼び出しコード
temp.T(i); //コンストラクタ
x.operator=(temp); //代入演算子
(4)一時オブジェクトを用いてオブジェクトを生成すると(1)に比べコードサイズが増えてしまう。
呼び出しコード
x.T(); //デフォルトコンストラクタ
x.operator=(i); //代入演算子
ガイドライン
コンストラクタの宣言されたオブジェクトを初期化する場合は、(1)の形式で初期化すること。
なお、性能に関するガイドラインの策定にあたっては、Thmas Plum, Dan
Sacks共著"C++ Programming Guidelines", Plum
Hall,1991を参考にさせて戴くとともに、一部筆者のご好意で引用させていただいた。
|