const修飾子を使うと値書き換え不可の定数が作れます。
ポインタと一緒に使うことも多いです。
constは変数の意図しない書き換えによるプログラムミスを防ぐためや、将来コードを読む人に「この変数は書き換え不可ですよ」と伝えるためによく使います。
constとは?意味と利用目的を解説
constとは変数を書き換え不可の定数とするための修飾子です。
constを付けた変数に値を代入しようとするとコンパイルエラーになります。
仕事におけるプログラム開発は複数人で行うことが多いです。
一人で開発する場合でも数年後に見直す頃にはどの変数が書き換え不可なのかすっかり忘れていることでしょう。
constを使えば同僚または未来の自分に向けて「この変数は書き換え不可だよ!」と伝えるメッセージになります。
また、書き換えようとしたときにエラーが出ることで意図しない変数書き換えも防いでくれるのです。
変数におけるconstの使い方
変数における使い方
const データ型 変数名=初期値;
「データ型 const 変数名=初期値;」と書いても同じ意味です。
変数値は後から変更できないので定義と同時に初期値を代入しておきます。
例1:
#include <stdio.h> const float PI=3.14; void main() { const int num=1; //PI=3; //書き換えようとするとコンパイルエラー //num=1; printf("PI=%f\n",PI); printf("num=%d\n",num); }
実行結果
PI=3.140000
num=1
コメントアウトしている行のようにconstを使って定義した変数を書き換えようとするとコンパイルエラーになります。
ポインタにおけるconstの位置と使い方
ポインタにconstを使う場合はconstの位置によって意味が変わってきます。
constの位置によってポインタに格納されたアドレスとポインタが示す位置に格納されている値のどちらが書き換え不可になるのかは以下にまとめました。
アドレスの書き換え | 値の書き換え | |
const データ型* | 〇 | × |
データ型* const | × | 〇 |
const データ型* const | × | × |
覚えにくい法則ですが、個人的には以下のように覚えています。
- ポインタはアドレスを示すので「データ型* const」はconstの直後に来るポインタ自体の値(アドレス)を固定。
- ポインタにアスタリスク(*)を付けるとそこに格納されている値を示すので「const データ型*」は*ポインタ名(値)を固定。
値のみ書き換え不可「const データ型*」
データ型の前にconstを書いてポインタを定義するとポインタが示す位置に格納されている値が書き換え不可となります。
例2:
#include <stdio.h> void main() { int a=100, b=999; const int* c_ptr=&a; printf("c_ptr=%d\n",*c_ptr); c_ptr=&b; //アドレス書き換えは可能 //*c_ptr=0; //値書き換えはコンパイルエラー printf("c_ptr=%d\n",*c_ptr); }
実行結果
c_ptr=100
c_ptr=999
アドレスのみ書き換え不可「データ型* const」
データ型の後ろにconstを書いてポインタを定義するとポインタが示すアドレスの書き換えができなくなります。
例3:
#include <stdio.h> void main() { int a=100, b=999; int* const c_ptr=&a; printf("c_ptr=%d\n",*c_ptr); //c_ptr=&b; //アドレス書き換えはコンパイルエラー *c_ptr=0; //値書き換えは可能 printf("c_ptr=%d\n",*c_ptr); }
実行結果
c_ptr=100
c_ptr=0
値もアドレスも書き換え不可「const データ型* const」
ポインタに格納されているアドレスとポインタが示す先に格納されている値の両方を書き換え不可にする場合はデータ型の前後にconstを書きます。
例4:
#include <stdio.h> void main() { int a=100, b=999; const int* const c_ptr=&a; printf("c_ptr=%d\n",*c_ptr); //c_ptr=&b; //アドレス書き換えはコンパイルエラー //*c_ptr=0; //値書き換えもコンパイルエラー printf("c_ptr=%d\n",*c_ptr); }
実行結果
c_ptr=100
c_ptr=100
char型配列におけるconstの使い方
char型配列
配列にconstを使う場合は以下のように記述します。
例5:
#include <stdio.h> void main() { const char array[]="Hello"; //array[0]='h'; //書き換えようとするとコンパイルエラー printf("array=%s\n",array); }
実行結果
array=Hello
constは多次元配列でも同じように使えます。
char型のポインタ配列
ポインタを使って配列を表す場合は先述したようにconstの位置によって値とアドレスどちらを書き換えるかを制御できます。
例えば値を書き換え不可にしたい場合は以下のように書きます。
例6:
#include <stdio.h> void main() { char array[]="Hello"; const char *c_ptr=array; //c_ptr[0]='h'; //書き換えようとするとコンパイルエラー printf("c_ptr=%s\n",c_ptr); }
実行結果
c_ptr=Hello
関数の引数におけるconstの使い方
ポインタ渡し
-
参考【C/C++】関数の値渡し・ポインタ渡し(アドレス渡し)・参照渡し
C言語、C++の関数には値渡しとポインタ渡し(アドレス渡し)と参照渡しの3種類があります。 ここではその違いを解説します。 関数への値渡し 関数へ変数を渡すとき、特にアドレスやポインタを使わなければ値 ...
続きを見る
ポインタを実引数(関数呼び出し時に渡す引数)として渡すと、関数内で引数を書き換えた結果が関数外にも引き継がれてしまいます。
以下の例では実引数c_ptrはconstを使って定義されていますが、plus関数内で定義された引数(仮引数)xはconstでないため、関数内で値が書き換えられます。
例7(NG例):
#include <stdio.h> int plus(int *x, int y) { *x=0; //xは書き換え可能 return *x+y; } void main() { int a=2, b=3; const int *c_ptr=&a; //定数として定義 printf("*c_ptr=%d\n",*c_ptr); printf("2+3=%d\n",plus(c_ptr,b)); printf("*c_ptr=%d\n",*c_ptr); }
実行結果
*c_ptr=2
2+3=3
*c_ptr=0
constを使って定義した*c_ptrの初期値は2だったのに関数呼び出し時後は0に値変更されてしまっています。
筆者の環境ではコンパイルするとワーニングが出ますが、エラーにはならず、プログラムが実行できてしまいまうんです。
これはまずいですよね。
関数内での値書き換えを防ぐには仮引数の定義時にconstを付ける必要があります。
下の例では例5の仮引数xの定義にconstを追加したので、*xを書き換えようとするとコンパイルエラーになります。
例8:
#include <stdio.h> int plus(const int *x, int y) { //*x=0; //xを書き換えようとするとコンパイルエラー return *x+y; } void main() { int a=2, b=3; const int *c_ptr=&a; //constが無くても同じ結果になる printf("*c_ptr=%d\n",*c_ptr); printf("2+3=%d\n",plus(c_ptr,b)); printf("*c_ptr=%d\n",*c_ptr); }
実行結果
*c_ptr=2
2+3=5
*c_ptr=2
上の例のようにポインタの値書き換えが無い関数ではポインタ引数にconstを付けて定義するのが一般的です。
そうすることで将来この関数を使う人に「値書き換えがない関数ですよ。」と伝えられます。
もしポインタの仮引数にconstが付いていなかったら値書き換えされないかいちいち確認する必要があり面倒です。
今は値書き換えがなくても将来的に値書き換えのコードが追加され、予期しないミスに繋がることも考えられます。
仮引数にポインタを使う場合は意図を持ってconstを付ける付けないを判断しましょう。
一般的に仮引数においては値書き換えを禁止する「const データ型*」を使います。仮引数のアドレスが変更されても関数外には影響がないため「データ型* const」は通常使いません。
参照渡し(C++限定)
参照渡しはC++限定の機能です。
参照渡しする仮引数にconstを使うと関数内で値が書き換えられるのを防げます。
例9:
#include <stdio.h> void rewrite(const int &x) { //x=1; //xを書き換えようとするとコンパイルエラー } int main() { int num=0; plus1(num); return 0; }
参照渡しにおいてもポインタ渡しと同様、関数内で値書き換えがない場合はconstを付けることで「値書き換えがない関数ですよ。」というメッセージを残しておきましょう。
値渡しにconstは不要
値渡しする仮引数にはconstを付ける意味がないです。
値渡しされた変数は関数を出ると破棄されるので、値が変更されようされなかろうが関数外に響かないからです。
関数の戻り値におけるconstの使い方
ポインタが戻り値の場合
戻り値の型の前にconstを付けると戻り値が書き換え不可になります。
通常は戻り値がポインタの時にポインタの値を書き換え不可にするために使います。
例10:
#include <stdio.h> const char *hello(){return "Hello";} void main() { const char *ptr=hello(); //constを付けないとワーニングが出る printf("%s\n",ptr); }
実行結果
Hello
上の例では戻り値を代入するポインタptrにconstを付けないとコンパイル時にワーニングメッセージが出ます。
ptrにconstを付けないとptrの値は書き換えできるのですが、ワーニングが出るのでconstを付けざるを得なくなり、結果としてptrを書き換え不可にできるのです。
これは以下の例のような異常終了を防止するのに有効です。
例11:
#include <stdio.h> void main() { char array[]="Hello"; char *ptr1=array; ptr1[0]='h'; //問題なし printf("%s\n",ptr1); char *ptr2="Hello"; ptr2[0]='h'; //異常終了するNGなコード printf("%s\n",ptr2); }
実行結果
hello
(異常終了)
ptr2ように文字列を直接代入したポインタを値書き換えしようとするとコンパイルは通りますが、プログラムが異常終了します。
異常終了する際はエラーメッセージが出ないため、エラー箇所の特定に苦労します。
そのような事態を防ぐために例10のような関数の戻り値にはconstを付けるのが有効です。
ポインタでない戻り値にconstは不要
戻り値がポインタでない時はconstを付ける意味がありません。
constが使えなくても不都合がないためです。
以下のように戻り値を代入する変数numにconstを付けなくてもエラーもワーニングも出ません。
例12:
#include <stdio.h> const int number(){return 1;} void main() { char num=number(); //エラーもワーニングも出ない num=0; printf("num=%d\n",num); }
実行結果
num=0
numを定数にしたければnumの定義時にconstを付けます。
キャストでconstが外れるので注意【外してはいけない】
-
参考【C/C++】キャストとは?変数とポインタにおける書き方・注意点
変数のデータ型を変更することを型変換と言い、型変換を明示的に行うことをキャストと言います。 型変換はある変数に異なるデータ型の値を代入する際などに用いられます。 キャストとは?意味を解説 キャストとは ...
続きを見る
const変数をconstでない変数に代入(型変換)するとconstが外れます。
しかもキャスト(明示的型変換)ではワーニングが出ないので注意です。
以下の例ではconstを使って定義したポインタc_ptrをconstでないポインタptr1とptr2に代入しています。
例13:
#include <stdio.h> void main() { char array[]="Hello"; const char *c_ptr=array; printf("c_ptr=%s\n",c_ptr); char *ptr1=c_ptr; //暗黙的型変換(ワーニングが出る) char *ptr2=(char*)c_ptr; //キャスト(ワーニングが出ない) ptr2[0]='h'; printf("c_ptr=%s\n",c_ptr); }
実行結果
c_ptr=Hello
c_ptr=hello
代入時に暗黙的型変換がされた場合は「constが外れたよ」という内容のワーニングが出るのでミスに気付けます。
しかしキャストした時はワーニングが出ません。
ptr2の値を書き換える際にも警告は出ないので、気づかないうちにconstで定義したはずのポインタを書き換えるというおかしなコードになってしまいます。
これはどんな副作用を引き起こすかわからないNGコードです。
ポインタをキャストする際はconstが外れないよう注意しましょう。
constとdefineの違い
constを付けて定義した変数とdefineを使って定義した変数はどちらも値を変更できないという特徴があります。
ではconstとdefineの違いは何なのでしょうか?
constまたはdefineを使って定義した変数には以下のような違いがあります。
const | define | |
配列の作成 | 〇 | × |
ポインタ変数の作成 | 〇 | × |
変数が作られるタイミング | コンパイル時 | プリプロセス時(コンパイルの前) |
変数の有効範囲 | 変数の種類による (グローバル変数、ローカル変数、static変数) |
プログラム全体 |
-
参考【C/C++】define(マクロ)による置き換え方法・関数との違いを解説
マクロには数字・文字列・式などを簡単な文字列に置き換えたり、関数の外に条件分岐を書けたりといった機能があります。 特に大きなプログラムを作る際にはよく使われます。 define(マクロ)とは何かわかり ...
続きを見る
例を挙げて詳しく見てみましょう。
以下の例ではdefineで定義した変数PIとconstを付けて定義した変数piに円周率3を代入し、半径rの円の面積areaを求めています。
例14:
#include <stdio.h> #define PI 3 void main() { const int pi=3; int r=1; int area1, area2, area3; area1=pi*r*r; area2=PI*r*r; area3=3*r*r; }
変数piとPIはそれぞれ以下の手順で処理されます。
pi(const変数) | PI(define変数) | |
プリプロセス | - | PIをすべて3に置き換えるというルールを設定 |
コンパイル | piに行う処理(メモリ確保など)の手順をプログラムに記入 | PIを3で置き換えたプログラムを作成 |
リンク | - | - |
プログラム実行 | piのメモリ領域確保、初期値代入、変数呼び出しなどを行う | - |
プリプロセス、コンパイル、リンクとはプログラムをビルドする際に自動で行われる処理です。プリプロセスではdefineやincludeなどの処理をし、コンパイルでコードをコンピュータが理解できるように翻訳した後、リンクで複数のソースファイルを繋げて1つのプログラムにします。defineのようにプリプロセス時に行う処理をプリプロセッサ命令と言います。
defineではコンパイル時に値が代入・固定されますが、constではプログラム実行時に変数が作られ、値も固定されます。
define変数はすべて定数で置き換えられるので、変数を保存するためのメモリ領域を必要とせず、メモリの節約になります。
しかし変数の有効範囲がプログラム全体に及ぶため、変数名が被りやすいなど管理がやや面倒です。
constを使って定義した変数(ローカル変数、グローバル変数、static変数)の作られ方は以下の記事で解説しています。
-
参考スタック領域・ヒープ領域・静的領域の仕組みと違いをわかりやすく解説
プログラムを実行するとメインメモリはどうやって使われるの? スタック領域・ヒープ領域・静的領域ってどんな仕組み?それぞれどう違うの? こんな疑問にお答えします。 おゆプログラマーにとってメインメモリの ...
続きを見る
どちらも一長一短ありますが、通常、定数を定義したいときはconstを使います。
constなら変数の有効範囲が制限できて管理しやすいコードが書けるためです。
defineはプリプロセッサ命令を使いたい場合などdefineでしか実現できないコードを書く際に使います。
C言語で行き詰まったら…
-
C言語・C++がわからない時に質問できるサイト・サービス5選
C言語を独学で勉強しているけど内容がイマイチわからない。もはや何がわからないのかもわからない。 C++のエラーが解決できない。ググってもわからない。わかる人に解説してほしい。 こんなお悩みにお答えしま ...
続きを見る