C言語は会社に入ってから覚えました. かれこれ30年以上ずっとC言語でプログラムを開発していました. Linux Kernel やらドライバはまだまだCは現役の言語です.
ミドルウェアの開発でC++を使うようになったのはここ数年.
なかなか正面切ってC++使いとは言い切れません…
五十の手習いでちょっとずつC++を覚えて、その言語のご利益を利用させていただいてる、という話を書きます.
Table of Contents
まずは言い訳から(笑)
最初に言い訳しておきます(笑)
いま開発しているミドルウェアはC++で書いてますけど、元となるソフトがCで書かれており、それを拡張していますので、クラスはごく一部を除いて使っていません.
Cでベタに書いているところをクラスを使って大幅に書きなおせば見通しやメンテナンスが向上するのは、ある程度C++が分かってきた(…といってもまだまだ勉強途中です)今では理解できます.
いま読んでる本、 「事業をエンジニアリングする技術者たち」でいうところの「技術的負債」ですね.
大幅に書きなおす「ビッグ・リライト」したい衝動にかられますが、そのような時間も工数もありません.
C++でコンパクトにできるところを地道な改善を進めていく作戦をとってます.
そんな地道な作業のひとつ、構造体の初期化について書きます.
構造体の初期化を取り上げた理由は以下の通りです.
- 構造体メンバの初期化忘れがバグの原因となることが多い
- 現象が発生しない場合もあり原因が分かり辛い.
- 既存のコードのロジックを極力変えずにメンテナンス性を上げたい.
C++の構造体
C++の構造体はクラスと同じです. 各メンバがデフォルトで pubulic になっているのが構造体、各メンバがデフォルトで private となっているのがクラス、という点が相違点です.
構造体でもメンバ関数を書くことも可能です.
struct hoge
{
int a;
int b;
unsigned long c;
int foo ()
{
return (a++);
}
};
そして、構造体の変数を宣言した際、クラス同様にコンストラクタが実行されます.
その点がCの構造体と大きく異なるところです.
memset() での初期化はやめてしまえ
構造体の変数を宣言し、0で初期化する場合によくやってしまうのが
このような memset() を使った初期化です.
#include <string.h>
struct hoge
{
int a;
int b;
unsigned long c;
};
int main()
{
struct hoge hogehoge;
memset (&hogehoge, 0, sizeof (struct hoge));
return 0;
}
C++で書いてるのなら今すぐやめましょう、なぜなら
- C++はそのオブジェクトサイズを知っている、
なぜmemset()の第三引数にわざわざサイズを指定する? - サイズの指定を間違える可能性もある.
特に別のところからコピペした時に sizeof() に渡すデータ型名を
そのまま修正しないで使ってしまってビルドが通ることもある. - データサイズが小さい場合は0初期化が不完全、
逆にサイズが大きいと他の変数データ破壊、
あるいは怖い々SIGSEGVが発生. これは目も当てられない. - この点の不具合はソースコードの目視で分からないこと多し. (経験者が語る…)
- ソースコードを静的解析にかけたとき memset()は指摘されがち.
- そもそも memset() で初期化するのはカッコ悪い…
C++の構造体初期化
先にも書きましたがC++で構造体の変数(この場合はオブジェクトと言った方が正確か)の宣言時の初期化はコンストラクタに働いてもらいましょう.
このように簡単に書けます. h0 が初期化なし、h1 が初期化を行った場合です.
struct hoge
{
int a;
int b;
unsigned long c;
};
int main()
{
struct hoge ho0;
struct hoge ho1 {};
return 0;
}
これで構造体の変数 ho0 が初期化されず、ho1 が初期化されます.
一点だけ注意
C++では構造体とクラスが同位と書きました. 構造体でも自前でコンストラクタを書くことが可能です.
#include <iostream>
struct hoge
{
int a;
int b;
unsigned long c;
hoge ()
{
std::cout << "constructor for hoge\n";
}
};
int main()
{
struct hoge ho0;
struct hoge ho1 {};
return 0;
}
自前でコンストラクタを書いた場合、自分の責任で各メンバを初期化するコードを書く必要があります.
さもなければ以下の様に期待した0での初期化がされない事態が発生します.
上記がgdb の結果です, 構造体変数 ho0, ho1 とも初期化されていません.
初期値つき構造体型定義
C++11 からメンバに初期値つきの構造体型定義ができるようになりました.
このように型定義で初期値を設定することが可能です.
struct hoge
{
int a = 1;
int b = 2;
unsigned long c = 3;
};
int main()
{
struct hoge hoi;
return 0;
}
構造体オブジェクトを宣言した場合、以下のような結果となります.
構造体変数 hoi は初期値つきで生成されています.
動的に構造体変数を生成する場合は?
ヒープ領域からメモリを確保して構造体変数を生成する場合に初期化を行う場合は、malloc ではなく new を用います.
生成する変数を明示的に初期化する場合、
new データ型 ()
という書式をとります.
余談ですが, malloc で確保した領域は 0 クリアされされません.
The malloc() function allocates size bytes and returns a pointer to the allocated memory. The memory is not initialized.
man malloc より
new を使った場合、メモリ確保に失敗した場合に例外をキャッチする必要がありますが、例外処理が既存のC言語で書いたコードとの整合性が取れない場合があります.
そこで (std::nothrow) を使って、new に失敗した場合, ポインタに nullptr を返します.
そうなると既存のCで書いたコードのロジックを変更せず整合性が取れます.
#include <stdio.h>
#include <new>
struct hoge
{
int a;
int b;
unsigned long c;
};
int main()
{
struct hoge * hp;
hp = new (std::nothrow) struct hoge ();
if (hp == nullptr)
{
fprintf (stderr,"memory allocation fail.\n");
}
return 0;
}
上記のコードを実行した結果です. ポインタは0で初期化された構造体変数を指してます.
初期値つき構造体を動的に確保する場合は以下のようなコードとなります.
#include <stdio.h>
#include <new>
struct hoge
{
int a = 1;
int b = 2;
unsigned long c = 3;
};
int main()
{
struct hoge * hp = nullptr;
hp = new (std::nothrow) struct hoge;
if (hp == nullptr)
{
fprintf (stderr,"memory allocation fail.\n");
}
return 0;
}
上記コードの結果はこのようになります.
変数生成時に明示的に初期化指示しなくても初期値が設定されるところが興味深いです.
以上のことから、これならロジックを変えずに初期化抜けを防止できて、いままで無駄に memset() やらで初期化していた箇所もキレイにできます.
まぁ、全体のコード量から見るとホンの少しの改善ですが、これの積み重ねが効いてくる、
…と信じています.
追伸
全体のロジックを変えずにコード量を減らしてスッキリする活動、意外にもC++の新しい機能、すなわちC++11やC++14などで追加された機能を使うと良いことをこれらの本で知りました.
とりあえず分からないところは飛ばして、使えそうな機能は使ってみる、というスタンスで読んでます.
また、基本的なC++で分からない部分はこの本で勉強中です.
この本、分かりやすいです. C言語の知識が無い人にもお勧めします.
“CとC++の狭間…C++使いになりきれないつぶやき その1構造体初期化編” への2件の返信
コメントは受け付けていません。