こんにちは、ナナです。
constは皆さんの作るプログラムをより安全にするための仕組みです。もちろん、使わなくてもプログラミングはできますが、大規模システムになるほど安全性を高めることが重要になるのです。
「安全にするための仕組み」とはいったい何なのか、それを示します。
本記事では次の悩みを解消する内容となっています。
では、const修飾子を使い方を学んでいきましょう。
const修飾子の使い方と役割
「const」とは修飾子と呼ばれるキーワードです。「constant:定数」の略称であり、定数を作り出すための機能です。
const修飾子を使用することで、変数の値を「書き換え禁止(読み取り専用)」にすることができます。
変数を読み取り専用にすることで、書き換えてはいけない変数に対して「間違えて書き換えてしまった…」といった、不慮の事故を防止することができます。
変数に鍵を掛けてロックする。それがconst修飾子の役割であり、安全化するための仕組みです。
static修飾子による保護方針との違い
「const」は変数を読み取り専用にすることで保護を行いますが、「static」は変数や関数の参照範囲を狭めることで保護します。保護方針は異なりますが、非常に近い兄弟のような関係です。
staticに関して知りたい方は『staticの利用価値【システムを安全にする仕組みを解説】』の記事を参考にしてください。
「const」と「static」キーワードは併用することも可能です。この2つは喧嘩せず、仲良しなんです。
const修飾子を変数に付与する方法
const修飾子は変数定義に対して付与します。
const変数の定義方法
変数にconst修飾子を付与するのは簡単です。変数定義に「const」を記述するだけです。
書き方
const データ型 変数名 = 定数値の初期値;
変数定義例
const long cNum = 100;
この時に注意しなければならないのが、必ず初期化することです。
定数には代入処理ができません。そのため初期化でしか値を与えることができないのです。
変数へのconst修飾子の付与
では、具体的にconstを付与したプログラム例を示しましょう。
#include <stdio.h>
int main(void)
{
// const変数の定義
const long cNum = 100;
// 変数の書き換えを行う
cNum = 50;
return 0;
}
constで定義した変数に対して、値の書き換えを行おうとするとビルドエラーが発生します。
error C2166: 左辺値は const オブジェクトに指定されています。
このようにconstは変数を読み取り専用にし、代入演算子の使用を禁止します。
変数への「初期化」と「代入」は違う!
「const」はその影響がある代表的なキーワードです。const変数は必ず初期化しましょう。
ポインタ変数へのconst付与の効果とは
constはポインタ変数に対してよく利用されます。
ポインタ変数に対する使い方は少し注意が必要です。なぜかというと、定義の方法が2種類あるからです。
ポインタ変数の基礎
ポインタ変数は次のように利用します。
char num1 = 20;
char * pnum = &num1;
ここで大事なことは、「ポインタ変数」とはその性質上、2つのメモリを管理していることです。
ポインタについて知りたい方は『ポインタ変数定義の正しい解釈とは【「*」の意味を解説】』の記事を参考にしてください。
それでは、この知識を踏まえた上で、ポインタ変数とconst修飾子の関係を掘り下げましょう。
ポインタ変数に対する2つのconst付与方法とは
ポインタ変数の定義に対してconstの付与方法は、次の2つの書き方があり意味が異なります。
付与パターン①
char num1 = 20;
const char * pnum = &num1;
付与パターン②
char num1 = 20;
char * const pnum = &num1;
constを付与している位置が異なるのがわかりますね。この位置によって定数化する対象が変化します。
この付与パターンはポインタの図で言うと、それぞれ別のメモリを書き込み禁止にします。
constパターン①の場合にはできない書き込み処理
constパターン①で、書き込みができないパターンのプログラムを紹介しましょう。
#include <stdio.h>
int main(void)
{
char num1 = 20;
const char * pnum = &num1;
*pnum = 50; // NG
pnum++; // OK
return 0;
}
ポインタ参照先への「50」の代入処理は、次のようにビルドエラーが発生します。
error C2166: 左辺値は const オブジェクトに指定されています。
constパターン②の場合にできない書き込み
続いてconstパターン②で、書き込みができないパターンのプログラムを紹介しましょう。
#include <stdio.h>
int main(void)
{
char num = 20;
char * const pnum = &num1;
*pnum = 50; // OK
pnum++; // NG
return 0;
}
ポインタ自身へのインクリメント処理は、次のようにビルドエラーが発生します。
error C2166: 左辺値は const オブジェクトに指定されています。
パターン①と②の併用パターン
実は、この2つのconstは併用することも可能です。
#include <stdio.h>
int main(void)
{
char num1 = 20;
const char * const pnum = &num1;
*pnum = 50; // NG
pnum++; // NG
return 0;
}
この場合は、どちらの代入行為も禁止されることになります。
この2つのパターンでよく利用されるのは、パターン①の方です。
ポインタの番地を固定化する必要性はあまりないので、私はパターン②をほとんど見たことはありません。
constの実践的な利用シーンを紹介
それでは、実際の開発でconstを使う実践的なシーンを紹介しましょう。
シーン①:テーブル定義を行おう
「const」はテーブルと呼ばれるデータを作り出す時によく利用されます。テーブルとは構造体と配列を使った固定データの塊のことです。
#include <stdio.h>
// フルーツ構造体定義
typedef struct
{
char name[32];
long price;
} S_FRUIT;
// 定数テーブルの定義
const S_FRUIT fruits[] =
{
{"りんご", 200 },
{"バナナ", 50 },
{"桃", 400 },
{"メロン", 1500},
{"みかん", 100 },
};
int main(void)
{
int i;
for (i= 0 ; i < sizeof(fruits)/sizeof(fruits[0]); i++)
{
printf("名前:%6s 価格:%4d円\n", fruits[i].name, fruits[i].price);
}
return 0;
}
実行した結果は次のように表示されます。
名前:りんご 価格: 200円
名前:バナナ 価格: 50円
名前: 桃 価格: 400円
名前:メロン 価格:1500円
名前:みかん 価格: 100円
このようなプログラム実行時に変更の必要がない情報を「テーブル」と呼ばれる形でまとめておくのです。必要な時にテーブルを参照し、情報を取り出して利用します。
シーン②:関数におけるポインタ引数の安全性を保障しよう
もうひとつの「const」を使うシーンが、関数のポインタ引数に対してです。特によくあるのが、構造体のポインタ渡しの時に、構造体データを保護するシーンです。
構造体は多くの構造体メンバで構成されることは珍しくありません。
この場合、関数の引数で構造体データを値渡しで行うと、スタックメモリを過度に消費する傾向があります。そのため、構造体はポインタで引き渡すことがよくあります。
#include <stdio.h>
// フルーツ情報構造体
typedef struct
{
char name[128];
long price;
} S_FRUIT;
// フルーツ価格表示
void printFruit(S_FRUIT * pFruit)
{
printf("名前:%6s 価格:%4d円\n", pFruit->name, pFruit->price);
// 100円に勝手に値下げ
pFruit->price = 100;
}
int main(void)
{
S_FRUIT fruit = { "パイナップル", 300 };
printFruit(&fruit);
return 0;
}
printFruit関数は、価格を表示するだけの機能のはずが、誤って価格を100円に値下げしてしまいました。このように、ポインタでデータを渡すというのは、データを書き換えられてしまう危険性があるのです。
このような時はポインタ引数にconstを付与することで「渡されたポインタのデータは書き換えませんから、安心して渡してください!」と意思表明することができます。
// フルーツ価格表示
void printFruit(const S_FRUIT * pFruit)
{
printf("名前:%6s 価格:%4d円\n", pFruit->name, pFruit->price);
// 100円に勝手に値下げ
pFruit->price = 100; // ビルドエラー
}
このように、ポインタにconstを付与することでpriceの書き換えはビルドエラーが発生します。
関数をサービスとして提供している側からの視点で「書き換えないので、安心してポインタを渡してください!」と伝えるためにconstを付与して定義します。
これは代表的なconstを利用するシーンです。
「const」と「define」と「enum」の定数定義の使い分け
C言語において定数を定義する方法は、次の3つです。
それぞれの使いどころをまとめておきましょう。
constの使いどころ
constは本記事でも解説している通り、変数に対する代入の禁止が主な目的です。
- テーブルによる固定データを生成したい
- 関数におけるポインタ引数に対する書き換えから保護したい
enumの使いどころ
enumは連続した値を、一括定義したい目的で利用します。
- 重複しない連番の定数を作りたい
enumによる定数定義は『enum 列挙型【連番の作り方と使いどころを教えます】』の記事を参考にしてください。
defineの使いどころ
enumを使うシーン以外で定数定義を行いたいシーンはdefineを利用します。
- 連番以外の定数値を管理したい
- 浮動小数点の定数値を管理したい
defineによる定数定義は『C言語 define マクロ【数値に名前を付ける意味とメリット】』の記事を参考にしてください。
3つの選択肢がありますが、どれを使うか迷うことはありません。それぞれが適切な利用シーンがありますので、合わせて使ってあげるとよいでしょう。