C/C++ではオブジェクトに何らかの制限や意味を持たせるための修飾子があります。
const修飾子は値が不変であるという意味をオブジェクトに持たせるために使用されます。
下記にconst修飾子の使い方とその効果について記載します。
変数宣言時のconst
変数宣言時にconstを付けることでその変数には値が不変であるという意味がコンバイラに通知されます。そのため、次の行で値を変更しようと変数への代入を行っていますが、コンパイラはそれは出来ないとエラーを出し、コンパイルが出来ない状態になります。そのためconstを付けた変数には宣言と同時に初期化が必要になります。
int main(void) {
// 変数宣言時のconst
const int ival = 0;
ival = 1; // コンパイルエラー
return 0;
}
ポインタ変数の場合は少し気を付ける必要があります。ポインタ変数の操作にはメモリを指すアドレスとメモリを指すアドレスのデータの両方の存在があるため、constでどちらの操作を不変としたいのかを考える必要があります。constが*の前に付いている場合はメモリを指すアドレスのデータが不変、後についている場合はメモリを指すアドレスが不変となります。
まずconstが*の前に付いているメモリを指すアドレスのデータが不変であることを宣言しているパターンです。
int main(void) {
// 変数宣言時のconst
int ival1 = 1;
int ival2 = 2;
const int* p_ival = &ival1;
p_ival = &ival2; // メモリを指すアドレスの変更のためOK
*p_ival = 1; // メモリを指すアドレスのデータの変更のためコンパイルエラー
return 0;
}
次にconstが*の後に付いているアドレスが不変であることを宣言しているパターンです。
int main(void) {
// 変数宣言時のconst
int ival1 = 1;
int ival2 = 2;
int* const p_ival = &ival1;
p_ival = &ival2; // メモリを指すアドレスの変更のためコンパイルエラー
*p_ival = 2; // メモリを指すアドレスのデータの変更のためOK
return 0;
}
最後にconstが*の前後両方に付いているデータもアドレスも不変であることを宣言しているパターンです。
int main(void) {
// 変数宣言時のconst
int ival1 = 1;
int ival2 = 2;
const int* const p_ival = &ival1;
p_ival = &ival2; // メモリを指すアドレスの変更のためコンパイルエラー
*p_ival = 2; // メモリを指すアドレスのデータの変更のためコンパイルエラー
return 0;
}
ポインタ変数の場合はデータかアドレスの何を不変としたいのかを意識する必要があります。
ここで少し下記のコードについて少し考えてみます。
下記のコードはconstが*の前にあるのでメモリを指すアドレスのデータが不変であることを意図しています。ですが、メモリを指すアドレス自体は変更することは可能なので下記のようにp_ivalの指すアドレスをival2とすると結果的にp_ivalの指すアドレスのデータが変わっています。このconstはあくまでp_ivalに対する修飾子のためp_ivalを通じてのデータが不変(*p_ival = 1は出来ない)というだけで、p_ivalの指すアドレスのデータがp_ivalを経由する以外のやり方であれば変わることはもちろんあります。
int main(void) {
// 変数宣言時のconst
int ival1 = 1;
int ival2 = 2;
const int* p_ival = &ival1;
// アドレス変更のためOK
// ただし、p_ivalはival2のアドレスを指すことになります。
// そのため、結果的にp_ivalの指すアドレスのデータが1から2に変わっているように見えます。
p_ival = &ival2;
return 0;
}
関数の引数のconst
関数の引数の型にconstを付ける場合も同様にその変数が不変であることを示します。
void f(const int ival) {
ival = 10; // データ不変のためコンパイルエラー
return;
}
int main(void) {
int ival = 1;
f(ival);
return 0;
}
ポインタ変数が引数の場合も変数宣言時と同様の効果になるため変更のパターン自体はデータ不変のみ記載しますが、関数の引数とした呼び出し元の変数を変更したい場合はポインタや参照で渡す必要があります。
関数内で呼び出し元のconst付きのポインタ変数の指すアドレスを変更するコードの例を下記に記載します。
// ダブルポインタ
// ポインタ変数であるp_ivalのアドレスを受け取りp_ivalを操作する
void f1(const int** pp_ival) {
static int g_ival1 = 10;
// アドレス変更のためOK
*pp_ival = &g_ival1;
// データ不変のためコンパイルエラー
//**pp_ival = 100;
return;
}
// 参照
// ポインタ変数であるp_ivalの参照を受け取りp_ivalを操作する
void f2(const int*& p_ival) {
static int g_ival2 = 20;
// アドレス変更のためOK
p_ival = &g_ival2;
// データ不変のためコンパイルエラー
//*pp_ival = 200;
return;
}
// 値渡し
// ポインタ変数であるp_ivalのコピーを受け取るので呼び出し元のp_ivalには影響がない
void f3(const int* p_ival) {
static int g_ival3 = 30;
// アドレス変更のためOK
p_ival = &g_ival3;
// データ不変のためコンパイルエラー
//*pp_ival = 300;
return;
}
int main(void) {
int ival = 1;
const int* p_ival = &ival;
// p_ivalが指すアドレスが変更される
f1(&p_ival);
// p_ivalが指すアドレスが変更される
f2(p_ival);
// p_ivalが指すアドレスは変更されない
f3(p_ival);
return 0;
}
クラスのメンバ関数のconst
クラスのメンバ関数に付くconstについて記載します。このconstはクラスのメンバ変数を変更しない関数であるということを示します。仮に座標データを保持するPointというクラスを定義してみます。このクラスのメンバ関数であるgetx()の後ろに付いているconstはPointのメンバ変数が不変であることを示しています。そのためメンバ変数の値を変えるような処理を入れるとコンパイルエラーとなります。
class Point {
double x;
double y;
public:
Point() :x(1.0), y(2.0) {};
~Point() {};
double getx() const {
x = 3.0; //コンパイルエラー
return x;
}
};
int main(void) {
Point pnt;
double x = pnt.getx();
return 0;
}
まとめ
constは値が不変であることをコンパイラに通知しますので、それに違反するようなコーディングがあるとコンパイルエラーとして開発者に教えてくれます。また、そのスコープではそのスコープ全体を確認しなくてもそのオブジェクトが不変であることが分かるためコードの可読性を上げる効果もあります。
基本的には複数人の開発者が関わるプロジェクトが多いと思いますが、その際には自分含め他開発者に処理を正確かつ分かりやすいようにコメントを書くことが必要になってきます。constはコメントではないですが、コードを書いた人の意志としてこのオブジェクトの値が不変であるという意味をそのオブジェクト自体に持たせることができるため、不変が明確なのであれば基本的には使用すべきと考えています。
C/C++のconst修飾子の使い方とその効果についてまとめてみました。
本記事の内容が訪問頂いた貴方のお役に立てれば幸いです。
内容の正確性には可能な限り配慮しておりますが、誤りや見落としが含まれる場合があります。
また、学習の進行や考え方の変化により記事の内容は随時加筆・修正される場合がありますので、あらかじめご了承いただけますと幸いです。