C言語規約シリーズです。今回は変数の定義と宣言についてです。
この記事では変数の定義と宣言について注意すべきことを記載します。
変数も関数と同様に、「定義」とは実際にメモリ上に配置されることで、「宣言」とはどこかに定義があることを示すだけのものです。
extern宣言しても、メモリ上には配置されません。また、「定義」は各変数に必ず1つしか書けませんが、宣言はいくつあっても(文法上は)かまいません。
変数を定義するときも関数と同様に、名前空間についての配慮も必要です。
変数の場合は「関数内からだけ見える」「同じ.cファイル内からだけ見える」「どこからでも見える」の3パタンです。
モジュール化の観点からも、また、名前の重複を避けるためにも、名前空間はできるだけ小さくなるように、つまりできるだけ他から見えないようにしてください。
そうしておけば、後の変更するときにも影響範囲が小さくなり、メンテナンスが楽になります。
_ で始まるオブジェクト(マクロ・関数・変数etc…)を定義しない#
C言語の規格上、_ で始まる名前のオブジェクトは使用不可のものが多いです。
_ で始まる名前のオブジェクトは処理系(標準ライブラリ、コンパイラ組込み関数など)が使用すると決められているものがとても多くあります。
細かい規格をおぼえるのは大変ですし、重複した場合に意図しない動作になるため、_ で始まる名前のオブジェクトは使用しないでください。
違反コード#
1
2
3
4
5
6
7
| #ifndef _HOGE_H_INCLUDED_ // ← アンダースコア"_"で始まるのでNG.
#define _HOGE_H_INCLUDED_
extern int _variable; // ← アンダースコア"_"で始まるのでNG.
int _func(void); // ← アンダースコア"_"で始まるのでNG.
#endif
|
適合コード#
1
2
3
4
5
6
7
| #ifndef HOGE_H_INCLUDED
#define HOGE_H_INCLUDED
extern int variable;
int func(void);
#endif
|
ポインタ変数を引数とする関数で、関数内でポインタ先を変更しない場合、constをつける#
引数のポインタ変数にconstをつけると、「この関数では引数で受け取ったポインタ変数の先に何も書き込みませんよ」という意思表示になります(書き込もうとしてもコンパイルエラーになる)。
ですので、値を変更されたら困るポインタ変数も、ユーザーが安心して渡せます。逆に言うと、constがなければ怖くてポインタを渡せません。
例えば、データの比較をする標準ライブラリ関数memcmpですが、比較するデータのアドレスはconstがついています。
よってmemcmpに渡したポインタ先が書き換えられることを心配する必要はありません。
int memcmp(const void *s1, const void *s2, size_t n);
違反コード#
1
2
3
4
5
6
7
8
9
| // コピー元アドレスsrcの参照先は、絶対に変更しないはずなので const をつけるべき.
int memory_copy(void *dst, void *src, size_t size)
{
int i;
for (i = 0; i < size; i++)
dst[i] = src[i];
return i;
}
|
適合コード#
1
2
3
4
5
6
7
8
9
10
11
12
| // コピー元アドレスsrcの参照先は、絶対に変更しないはず。
int memory_copy(void *dst, const void *src, size_t size)
{
int i;
for (i = 0; i < size; i++)
dst[i] = src[i];
//万が一下記のように、constポインタに書こうとしてもコンパイラがエラーを出してくれる。
// src[i] = dst[i];
return i;
}
|
構造体を引数として渡さない#
構造体を引数として渡すと、スタック上に構造体すべてのメンバがコピーされるので、スタックオーバーフローの原因になります。
また、構造体のメンバが少なくても、CPUレジスタでなくスタックを使って引数を渡すようになるため、関数呼び出しに大きな時間がかかります(コンパイラの最適化が効きづらい)。
構造体を引数として渡したい場合は、構造体へのポインタ変数を渡すようにしてください。
違反コード#
1
2
3
4
5
6
7
8
9
10
11
12
|
struct hoge{
int piyo;
};
// 構造体をそのまま渡しているので関数呼び出しに時間がかかる.
// (CPUレジスタを使わずスタックを使うため)
int func_NG(struct hoge arg)
{
return arg.piyo;
}
|
適合コード#
1
2
3
4
5
6
7
8
9
| struct hoge{
int piyo;
};
int func_OK(struct hoge* arg_p)
{
return arg_p->piyo;
}
|
単項演算子 “-“は符号無しの値には使用しない#
符号無しの値は負数を表現できないので、符号 - をつけても意味がありません。符号無しの値に-は使用しないでください。
違反コード#
1
2
3
4
5
| void func_NG(void)
{
unsigned int uiValue;
uiValue = -uiValue;
}
|
適合コード#
1
2
3
4
5
| void func_OK(void)
{
signed int iValue;
iValue = -iValue;
}
|
8進定数および8進拡張表記は使わない#
0で始まる定数は8進数と解釈されます。まぎらわしく、また通常、必要もないので使用しないでください。
違反コード#
1
2
3
| table[0] = 1234;
table[1] = 4567;
table[2] = 0089; // 89(10進)でなく73(10進)が格納される
|
適合コード#
1
2
3
| table[0] = 1234;
table[1] = 4567;
table[2] = 89; // 期待通り89(10進)が格納される
|
“??“で始まる3文字以上の文字の並びは使用しない#
?? で始まる文字は3文字表記(trigraph sequence)と認識されます。
3文字表記とは、キーボードによっては入力できない、ある種の文字の代わりとなる3文字列です。
具体的には下記のように変換がなされます。
日本の一般的なキーボードではまず必要の無い機能ですので、使用しないでください。
トリグラフ(3文字表記)一覧#
1
2
3
4
5
6
7
8
9
| ??= → #
??) → ]
??> → }
??( → [
??' → ^
??! → |
??/ → \
??< → {
??- → ~
|
違反コード#
1
2
3
4
5
6
7
8
9
10
11
12
| void func_NG(void)
{
int a = 10;
/* 下記のコードはコメント行末の「??/」が「\」に変換されるので、
* a++の行までコメントアウトされます。
*/
// a の値はなんだろう??/
a++; // ← ここまでコメント行と認識されてしまう.
printf("a : %d\n", a); // a : 10と表示される.
}
|
適合コード#
1
2
3
4
5
6
7
8
9
10
11
12
13
| void func_OK(void)
{
int a = 10;
/* 「??」の間にスペース追加。
*/
// a の値はなんだろう? ?/
a++; // 期待通りインクリメントされます.
printf("a : %d\n", a); // 期待通り a : 11と表示される.
}
|
(signedでもunsignedでもない)単なるchar型は文字データのみに使用し、数値データは格納しない#
C言語の規格上、文字型は
- char型
- signed char型
- unsigned char型
の3種類あります。ここで、単なるchar型が符号付なのか符号なしなのかは、処理系依存です。
例えばARMのコンパイラは、伝統的にデフォルトでcharは符号無しになっています。
(gcc,RVCT, GHSどれもcharは符号無し型でした)
オーバーフローなどのバグを防ぐため、単なるchar型は文字データのみとします。
数値を格納する場合はsigned char型もしくはunsigned char型を使用してください。
違反コード#
1
2
3
4
5
6
| void func_NG(void)
{
char char_val;
char_val = -4; // 処理系によっては負数は使えない
}
|
適合コード#
1
2
3
4
5
6
| void func_OK(void)
{
signed char schar_val;
schar_val = -4; // どの処理系でもOk.
}
|
signed char型、unsigned char型は数値データのみに使用し、文字データは格納しない#
char型は符号付か符号無か定義されていないので、数値はchar型には代入禁止とします。
また、統一性のためにsigned char型、unsigned char型は数値のみとし、文字データは代入しないこととします。
(signed, unsigned含む)char型は、int型にくらべて処理速度が遅く、しかもキャスト命令が必要になるのでファームサイズも大きくなります。
さらに、charはCPUが1サイクルでアクセスできないので、アトミックな制御ができず、マルチタスク環境下では発生頻度の極端にひくいバグにつながります。
配列や構造体以外では、基本的にint型を使用してください。
違反コード#
1
2
3
4
5
6
| void func_NG(void)
{
signed char char_val;
char_val = 'A'; // 動作はするが、文字データを使う場合はchar型とするべき.
}
|
適合コード#
1
2
3
4
5
6
| void func_OK(void)
{
char char_val;
char_val = 'A';
}
|
CPU以外が更新する可能性のあるデータにはvolatileをつける#
CPU以外が更新するデータ領域には、volatileをつけてください。
言い換えると、「CPUが知らない間に、データが変更される可能性のある領域にはvolatileをつける」です。
該当するのは例えば、レジスタ、DMA領域、(キャッシュコヒーレンシのない)他CPUとの共有メモリなどです(ハードウェアが更新する可能性があるデータです)。
違反コード#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| #define ASIC_REG (*(uint32_t *) (0xf0000000)) // ASICレジスタ.
void func_NG(void)
{
ASIC_REG = 1UL;
if(ASIC_REG == 0){
/*
* すぐ上でASIC_REGに1を代入しているので、
* コンパイラが「このif文にはくるはずないよね。」、と
* 解釈してここを削除する可能性あり
*/
}
}
|
適合コード#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| #define ASIC_REG (*(volatile uint32_t *) (0xf0000000)) // ASICレジスタ.
void func_OK(void)
{
ASIC_REG = 1UL;
if(ASIC_REG == 0){
/*
* volatileがついているので、
* 「ASIC_REGに1を代入しているけど、私(CPU)の知らない間にいつの間にか0になっているかも」
* とコンパイラが判断するので、このif文を削除することはない。
*/
}
}
|
複数のタスク、割込みハンドラがアクセスする可能性のあるグローバル変数・static変数にはvolatileつける#
自タスク以外(他タスク、割込みハンドラなど)が更新する可能性のあるデータ領域には、volatileをつけてください。
言い換えると、「自タスクが知らない間に、データが変更される可能性のある領域にはvolatileをつける」です。
複数のタスクからアクセスされるグローバル変数や、割込みハンドラからアクセスされるグローバル変数などが該当します。自動変数はスタックに詰まれるので、該当しません。
違反コード#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
static int temp;
void task_A(void)
{
temp = 1;
if (temp == 0) {
/*
* すぐ上でtempに1を代入しているので、
* コンパイラが「このif文にはくるはずないよね。」、と
* 解釈してここを削除する可能性あり
* 実際は下記の割込みハンドラで0にされることも考慮してほしい。
*/
}
}
void interrupt_handler(void)
{
temp = 0; // 変数クリア.
}
|
適合コード#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| static volatile int temp;
void task_A(void)
{
temp = 1;
if (temp == 0) {
/*
* volatileがついているので、
* 「tempに1を代入しているけど、task_A の知らない間にいつの間にか0になっているかも」
* とコンパイラが判断するので、このif文を削除することはない。
*/
}
}
void interrupt_handler(void)
{
temp = 0; // 変数クリア
}
|
const変数は宣言時に初期化する#
const変数は宣言時にしか値を設定できないので、必ず宣言時に値を設定してください。
違反コード#
1
2
3
4
5
| int func_NG(void)
{
static const int value; // 値が不定。そしてもう二度と変更できない.
return value
}
|
適合コード#
1
2
3
4
5
| int func_OK(void)
{
static const int value = 5;
return value
}
|
同一ファイル内で定義された複数の関数からしかアクセスされないグローバル変数はstaticをつける#
staticをつけないグローバル変数は、他の.cファイルからも参照できます。
もし、同じ名前のグローバル変数が複数あった場合、リンクエラーにならないことが多く、また、リンク順によってどの変数が参照されるかが変わります(ライブラリの場合はエラーにならない、オブジェクトファイルの場合はエラーになる)。
他への影響を少なくするために、他の.cファイルから参照しないグローバル変数にはstaticをつけてください。
違反コード#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| // == file0.c ==
// 本当はfile0.cからしかコールしないグローバル変数
int g_variable;
// == file1.c ==
// 他のファイルからも参照するグローバル変数
int g_variable;
// == file2.c ==
extern int g_variable;
void func_NGother(void)
{
// NG, file0.c, file1.cのどちらのg_variableが参照されるかわからない
printf("%d\n",g_variable);
}
|
適合コード#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| // == file0.c ==
// 本当はfile0.cからしかコールしないグローバル変数
static int g_variable;
// == file1.c ==
// 他のファイルからも参照するグローバル変数
int g_variable;
// == file2.c ==
extern int g_variable;
void func_OK(void)
{
// OK, file0.cのg_variableはstaticがついているのでfile2.cからは見えない。
// 必ずfile1.cのg_variableが参照される。
printf("%d\n",g_variable);
}
|
文字列リテラルへのポインタは const 修飾子をつける#
文字列リテラル(文字列定数)は、C言語の規格上その領域を変更してはいけませんし、また、コンパイラも通常は文字列リテラルをリードオンリー領域へ配置します(つまり
const と同じ領域に配置される)。
ですが文字列リテラルの型自体はただのchar型の配列ですので、その領域を変更するようなコードを書いてもコンパイルエラーにはなりません。
そこで文字列リテラルを保護するために、文字列リテラルを参照するポインタ型変数を使う場合にはchar型でなく const char 型をつかってください。
違反コード#
1
2
3
4
5
6
7
| char *s;
s = "hello world.";
// NG. 文字列リテラルを参照するポインタ変数なのにconst でない.
// なので下記のようなコードを書いてもコンパイルエラーにはならない.
s[0] = 'H'; // どのような挙動を引き起こすかは不定.
|
適合コード#
1
2
3
4
5
6
| const char *s;
s = "hello world";
// OK. 文字列リテラルを参照するポインタ変数にちゃんとconstがついている.
// なので下記のように文字列リテラルを変更するコードがあると、コンパイルエラーになる.
s[0] = 'H';
|
以上、変数の定義と宣言のコーディング規約でした!
Note
下記のC言語コーディング規約シリーズも併せてご参照ください