構造体のあるメンバが,その構造体の先頭から何バイト目に位置するかオフセットを知りたい場合に使用できる
便利なマクロを最近知りました。offsetof マクロがそうです。
#include
struct test_st {
unsigned char c;
unsigned int n;
};
size_t nOffset = offsetof( test_st, n );
なお,メンバがビットフィールドだとコンパイルエラーになります。
このマクロがどんな風に定義されているか stddef.h を覗いてみました。
#define offsetof(s,m) (size_t)&(((s *)0)->m)
0を構造体のポインタとしてキャストし,メンバのアドレスを取得すれば,なるほどオフセットになりますね〜。
定数を意味するconst修飾子の存在は知っていても,Windows系のプログラミングではあまり使用していないのではないでしょうか?
組み込み系のプログラミングをしたことがある方なら,データをROM上に配置したいときに使ったことがあるかもしれませんね。
このconst修飾子は,特にポインタでアドレスを扱うときに効果を発揮するのです!
extern int nNum;
int *pNumV = &nNum;
const int *pNumC = &nNum;
*pNumV = 100; /* ○ */
*pNumC = 100; /* × */
上記のようにあるポインタを扱う場合,pNumV の示す先を変更することができますが,pNumC の示す先を変更することはできません(コンパイルエラーになります)。
pNumC が const int つまり定数の整数へのポインタなので読み出すことはできても変更することはできないからです。
pNumC が変更ができないROMに配置されたデータへのポインタとして宣言されたと考えればわかりやすいでしょうか?
関数で構造体のポインタを渡すときに引渡し先で中身が変更されたくない場合,const修飾子をつけておけば
コンパイラがそれを防いでくれるので安心です。バグを予防する上でも大事なことです。
DLLなど動的に使用される関数の場合は無駄ですけど…。
ちなみに C++ での参照渡しの時も使えます。
const修飾子は,それを書き換えても良いのかダメなのかを明らかにするのでプログラムがわかりやすくなるのです。
汎用的に使用されるライブラリ関数や不用意なメモリ書き換えが即暴走に繋がる組み込み系でプログラミングするとき
意識的にconst修飾子を使ってみてはいかがでしょうか。
以下,頭が混乱しますが,const修飾子の宣言位置によって何が定数なのかを細かく定義できる例を示します。
const char *pText;
pText は const char を指すポインタ
(pText は変更できても *pText は変更できない)
char * const pText;
pText は char を指す const ポインタ
(pText は変更できないが *pText は変更できる)
const char * const pText;
pText は const char を指す const ポインタ
(pText も *pText も変更できない)
const char ** ppArray;
ppArray は const char を指す ポインタ の ポインタ
(ppArray と *ppArray は変更できても **ppArray は変更できない)
char * const *ppArray;
ppArray は char を指す ポインタ の const ポインタ
(ppArray と **ppArray は変更できても *ppArray は変更できない)
char ** const ppArray;
ppArray は char を指す const ポインタ の ポインタ
(*ppArray と **ppArray は変更できても ppArray は変更できない)
char * const * const ppArray;
ppArray は char を指す const ポインタ の const ポインタ
(**ppArray は変更できても ppArray と *ppArray は変更できない)
const char * const * const ppArray;
ppArray は const char を指す const ポインタ の const ポインタ
(ppArray も *ppArray も **ppArray も変更できない)
筆者もつい最近知ったのですが,浮動小数点をprintfするときの興味深い現象を確認しました。
例えば,
double d = 0.25;
printf( "%3.1lf", d );
とすると 0.2 という出力が行われます。
printf( )が四捨五入していないのではないかと思いきや,
double d = 0.55;
printf( "%3.1lf", d );
の場合は 0.6 という出力が行われます。
こうなるのは理由がありました。
ご存知の通りコンピュータ内部では2進数で演算していますが,
10進数の0.25 という小数は2進数にぴったりに変換できません(無限小数)。
この誤差の関係で,10進数に戻ったとき,0.25 は 0.249999999 となってしまい
これを小数第2位で四捨五入すると 0.2 となってしまうようです。
コンピュータ内部で 0.249999999 になってしまっているので,
一番の解決策は「浮動小数点を使わない」というなんだか矛盾する結論になってしまいます。
つまり,例えば1000倍の整数値で計算するということです(2007.05.08修正)。
これまでのソフト資産を活用するようなとき,C++からCの関数を呼びたい場合があります。
その場合は以下のようにすればOKです。
==== cファイル ここから ====
void C_Function( void )
{
/* This is C function */
}
==== cファイル ここまで ====
==== cppファイル ここから ====
extern "C" void C_Function( void )
void CPP_Function( void )
{
C_Function();
}
==== cppファイル ここまで ====
また,反対にCからC++の関数を実行することもできます!
==== cppファイル ここから ====
extern "C" void CPP_Function( void )
{
/* This is CPP function */
}
==== cppファイル ここまで ====
==== cファイル ここから ====
extern void CPP_Function( void );
void C_Function( void )
{
CPP_Function();
}
==== cファイル ここまで ====
基本クラスから派生したクラスで基本クラスにある関数と同じ型の同名関数を宣言したら
どうなるでしょうか?基本クラスでその関数を仮想関数宣言(virtual)しておかなければコンパイル
が通らないでしょうか?結果は以下参照。
正解: 仮想関数宣言をしなくてもコンパイルは通ります。
#include "iostream.h"
class cBase {
int x;
public:
cBase( ){ cout << "cBase Construction\n"; };
~cBase( ){ cout << "cBase Destruction\n"; };
void Test( ){ cout << "cBase Test\n"; };
};
class cChild : public cBase {
int y;
public:
cChild( ){ cout << "cChild Construction\n"; };
~cChild( ){ cout << "cChild Destruction\n"; };
void Test( ){ cout << "cChild Test\n"; };
};
void main( )
{
cChild ob;
ob.Test( );
cBase *p;
p = &ob;
p->Test( );
}
実行結果
cBase Construction
cChild Construction
cChild Test
cBase Test
cChild Destruction
cBase Destruction
ではcBaseクラスのTest( )を仮想関数宣言したら実行結果はどうなるでしょうか?
cBase Construction
cChild Construction
cChild Test
cChild Test
cChild Destruction
cBase Destruction
上記の結果から分かるように派生クラスオブジェクトを基本クラスのポインタで受けて実行した
場合に違いが現れます。仮想関数宣言していなければ基本クラスの関数が実行され、
仮想関数宣言しておくと派生クラスの関数が実行されます。
微妙な違いですがこれがオーバーライドの実態です。
なお引数の数や種類が異るなど型が違う同名の関数が基本クラスと派生クラスに存在する場合、
派生クラスオブジェクトを基本クラスのポインタで受けたものでは派生クラスの関数を
実行することができません。なぜならそれはオーバライドではなく異なる関数が派生クラスだけに
存在しているという意味になるからです。
これは基本クラスからは派生クラスで追加された変数や関数にはアクセスできない当たり前の
結果を現しています。そのような場合は最初から派生クラスのポインタで受けるべきですが、
派生クラスのポインタとしてキャストすることでアクセスすることも可能です。
ちなみに基本クラスオブジェクトを派生クラスのポインタで受けることはできません。
コンパイルエラーとなります。
C++ではclass宣言で定義したオブジェクトも、アドレスではなく実体として関数の引数として
使用することができます。ただしint変数などと同様に引き渡された関数先では単なるコピーに
すぎないので呼び出し元のオブジェクトには何ら影響を与えません。
面白いのは、コピーが作られる際にそのコピーオブジェクトのコンストラクタは実行されませんが、
呼び出した関数が終了する際にコピーのデストラクタが実行されるということです。
オブジェクトを初期化するコンストラクタは、コピー生成の場合には実行するとオブジェクトが
初期化されてしまいコピーではなくなってしまうので実行せず、オブジェクトが消滅する場合には
デストラクタの実行が必要であるという考えのようです。
ただし注意しなければならないのはコンストラクタでメモリを割り当て、メンバ変数にその
ポインタを保持し、デストラクタでそれを解放するというようなオブジェクトは、たとえコピーと
いえど呼び先の関数で勝手にデストラクタが実行されてしまうとメンバ変数にポインタを保存して
おいたメモリが解放されてしまうため不具合が発生してしまうことです。
このような特性を知った上でオブジェクトを関数の引数に用いるのは構いませんが、ミスを防ぐためにも
オブジェクトを引数にする場合は実体でなくそのアドレス(ポインタ)で渡した方が無難だと思います。
ポインタで渡す場合、当然ですが関数実行の前後でコンストラクタやデストラクタが実行されることは
ありません。
C++ではもう一つの方法があります。ポインタを使わず且つ引き渡し時のコンストラクタ・デストラクタを
実行させない方法があります。それは「参照」と呼ばれる方法で、実体によるコピーの引き渡し方法とポインタ
によるアドレス渡しの中間のような特性のものです。
下記のように関数宣言時に演算子&を使って「参照」を宣言します。
void func( int &i )
{
i = 100;
}
void main( )
{
int n;
func( n );
// n には100が入っている
}
こうすると関数func( )ではmain( )のnを直接操作するのと同じ効果があります。例ではint型の参照変数ですが、
もちろんオブジェクトでも可能です。
ポインタ渡しよりもコードがすっきりするのとアドレスを直接扱わないので比較的安全だとされており、
C++では結構使われているようです。
関数のオーバロードにしろ、参照にしろ、Cの時と比べてコンパイラ自身が賢くなっていることを
実感できますね。
newとdeleteについておさらいします。
演算子newとdeleteは配列(メモリ)の確保やオブジェクトの生成作成に使えます。
以下のようにしてint型変数の確保と解放ができます。
int *nNum = new int;
delete nNum;
// 通常 int nNum; で十分なのであまり意味はありません。
また以下のようにして初期値を与えることができます。
int *nNum = new int ( 1000 );
delete nNum;
// 通常 int nNum = 1000; で十分なのであまり意味はありません。
配列(メモリ)の確保は以下のようにしてできます。
char *pBuf = new char[256];
delete [] pBuf; // ※ []を必ず記述することに注意
// 上記の場合 char bBuf[256]; で十分なのであまり意味はありませんが、
// 解放する予定があったり、任意のサイズを確保する場合は下のように
// 行うことができます。
int len = 256;
char *pBuf = new char[len];
delete [] pBuf; // ※ []を必ず記述することに注意
// C++といえど、char bBuf[len]; は不可能です。
// また従来のようにmallocで行うこともできます。
int len = 256;
char *pBuf = (char *)malloc( len );
free( pBuf );
newで配列の確保をした場合は解放の記述方法に注意してください。
なお残念ながらnewで確保する配列には初期値を与えることはできません。
ここまではあまりnewのありがたみがありませんでした。せいぜいキャストしなくてすむくらいです。
しかしオブジェクトの動的な確保というか生成を行う場合はnewを使用するしかありません。
mallocでは不可能です。以下に実例を述べます。
以下のようなオブジェクトがあるとします。
class CTest {
int m;
public:
CTest( ){ m = 0; cout << "construct: default\n"; }
CTest( int n ){ m = n; cout << "construct: " << m << "\n"; }
~CTest( ){ cout << "destruct\n"; }
};
このオブジェクトを一個使おうと思った時は、もちろん
CTest test;
とします。この時デフォルト(引数なし)のコンストラクタ(とデストラクタも)当然実行されます。
ちなみに、
CTest test( 0 );
とやって引数ありのコンストラクタを実行させることもできます。
またオブジェクトの配列も以下のようにして生成できます。
// デフォルト(引数なし)のコンストラクタを実行させる場合
CTest atest[10];
// 引数が一つのコンストラクタを実行させる場合
CTest atest[10] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
// 多次元配列の場合
CTest atest[4][2] = {
0, 1,
2, 3,
4, 5,
6, 7
};
// 引数が二つのコンストラクタを実行させる場合
CTest atest[3] = { CTest( 0, 0 ), CTest( 1, 1 ), CTest( 2, 2 ) };
// さらにそれが多次元配列の場合
CTest atest[2][3] = {
CTest( 0, 0 ), CTest( 0, 1 ), CTest( 0, 2 ),
CTest( 1, 0 ), CTest( 1, 1 ), CTest( 1, 2 ),
};
引数が複数になるとクラス名を添える正式な書式が必要となります。
次に、動的にオブジェクトの生成と消滅を行う場合はnewとdeleteを使います。
// 一個だけ生成(デフォルトのコンストラクタ)と消滅
CTest *p = new CTest;
delete p;
// 一個だけ生成(引数ありのコンストラクタ)と消滅
CTest *p = new CTest( 10 );
delete p;
これはmallocを使って、
CTest *p = (CTest *)malloc( sizeof( CTest ) );
free( p );
のように実現できるかといえばできません。mallocを使った例ではCTestのサイズ(メンバに持つ
メモリの合計でコードサイズは含まれない)だけの領域を確保しただけで、オブジェクトを作成した
ことにはならないからです。newとdeleteではコンストラクタとデストラクタが自動で実行されます
が、mallocとfreeでは当然実行されません。かといって手動で p->CTest( ); とやろうとしても
コンパイルエラーとなってしまいます。コンストラクタが実行できないのはオブジェクトにとって
致命的です。したがってオブジェクトの動的な生成、消滅をさせる場合は、newとdeleteを使う以外
ありません。
複数のオブジェクトを動的に生成、消滅させるには以下のように行います。
int len = 5;
CTest *p = new CTest[len]; // もちろん定数5でもOKです。
delete [] p;
// ここでデストラクタがあればlen回実行される。添字は必要ないようだ。
newで[]を使ってオブジェクトの配列を生成した場合、deleteの時に[]を入れないと、配列分の
メモリは解放されますが、配列の一つ一つに対してデストラクタが実行されないので予期しない
不具合の原因になるので注意してください。
なおnewで生成するオブジェクトの配列は、デフォルトのコンストラクタしか実行されません。
CTest *p = new CTest[10]( 100 ); // ダメ
delete [] p;
CTest *p = new CTest[10] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; // ダメ
delete [] p;
©Yutaka Wada(ana53), AirparkLab ALL RIGHTS RESERVED.