コンテンツにスキップ

💡 要約:const型とは、型修飾子constを使って宣言された型のことで、const型の変数やオブジェクトの値は更新できません。

最終更新:2026-10-13

C++ const 詳解:定数と 2 種類の const ポインタ

作者について

WeChat公式アカウント:

C++ const 詳解:定数・const ポインタとポインタ const の違い | C++那些事

1.constの意味

const型とは、型修飾子**const**を使って宣言された型のことで、const型の変数やオブジェクトの値は更新できません。

2.constの役割

  • 定数を定義できる
const int a=100;
  • 型チェック

    • const定数と#defineマクロ定数の違い:

      const定数は型を持ち、コンパイラが安全性のチェックを行えます;#defineマクロ定義にはデータ型がなく、単なる文字列置換なので、安全性のチェックは行えません。ここの問題を指摘してくださった2人の方に感謝します。参照:issue

    • const定数はすべての型をサポートします。

    • その他の場合、それはただの const 修飾された変数であり、定数と混同しないでください。
  • 変更を防ぎ、保護の役割を果たし、プログラムの堅牢性を高める

void f(const int i){
    i++; // error!
}
  • スペースを節約し、不要なメモリ割り当てを避ける

    • 整数型/列挙型で定数式により初期化されたconst(すなわち定数式)は、使い方としてコンパイル時定数に近いものです:アドレスを取らず、値だけを使う場合は通常即値に最適化され、#defineと同様です;アドレスを取ったり、翻訳単位をまたいでodr-useしたりするときに初めてメモリに置かれ、プログラム全体で1つしか存在しません。
    • #define はリテラルに展開された後、即値として命令に組み込まれます。命令自体もメモリを占有するため、複数箇所で使えば複数箇所に現れます(issue #5 の議論を参照)。
    • 注意:すべてのconst変数が定数式というわけではありません——定数式で初期化された整数/列挙型のものだけがそうであり、それ以外のconst変数はただのconst修飾変数です(浮動小数点型、ユーザー定義型など)。コンパイル時定数を正しく表現するのは constexpr です。

3.constオブジェクトはデフォルトでファイル内ローカル

注意:非const変数はデフォルトでexternです。const変数を他のファイルからアクセスできるようにするには、そのファイル内で明示的にexternとして指定しなければなりません。

constで修飾されていない変数の、異なるファイル間でのアクセス

// file1.cpp
int ext;
// file2.cpp
#include<iostream>

extern int ext;
int main(){
    std::cout<<(ext+10)<<std::endl;
}

const定数の、異なるファイル間でのアクセス

// extern_file1.cpp
extern const int ext=12;
// extern_file2.cpp
#include<iostream>
extern const int ext;
int main(){
    std::cout<<ext<<std::endl;
}

小結:
constで修飾されていない変数にはexternの明示的な宣言が不要なことがわかります!一方、const定数はexternの明示的な宣言が必要であり、さらに初期化も必要です!定数は定義後に変更できないため、定義時に必ず初期化しなければなりません。

4.定数の定義

const int b = 10;
b = 0; // error: assignment of read-only variable ‘b’
const string s = "helloworld";
const int i,j=0 // error: uninitialized const ‘i’

上のコードには2つのエラーがあります: + b は定数であり、変更できません! + i は定数であり、初期化しなければなりません!(定数は定義後に変更できないため、定義時に必ず初期化する必要があります。)

5.ポインタとconst

ポインタに関連するconstには4種類あります:

const char * a; // 指向const对象的指针或者说指向常量的指针。
char const * a; // 同上
char * const a; // 指向类型对象的const指针。或者说常指针、const指针。
const char * const a; // 指向const对象的const指针。

小結:
const が * の左側にある場合、constはポインタが指す変数を修飾します。つまりポインタの指す先が定数です;
constが * の右側にある場合、const はポインタ自体を修飾します。つまりポインタ自体が定数です。

もう一つの読み方
英語を右から左へ読み、toを境目とします。toの前はポインタの特性の説明、toの後は指す対象の特性の説明です

const char * p; //p is a pointer to const char
char const * p; //同上
char * const p; //p is a const pointer to char
const char * const p; //p is a const pointer to const char
ポインタ自体にconstの特性が付くと、ポインタは指す先のアドレスを変更できなくなります
指す対象の特性がcharなら、内容はポインタを通して変更できます。例:*char='y';
指す対象の特性がconst charなら、内容はポインタを通して変更できません

具体的な使い方は以下の通りです:

(1)定数へのポインタ

const int *ptr;
*ptr = 10; // error

ptrはint型のconstオブジェクトへのポインタです。constが修飾しているのはint型、すなわちptrが指すオブジェクトの型であって、ptr自体ではありません。したがってptrに初期値は必要ありません。ただし、ptrを通して指すオブジェクトの値を変更することはできません。

また、void* ポインタでconstオブジェクトのアドレスを保持することもできません。constオブジェクトのアドレスを保持するには、const void* 型のポインタを使わなければなりません。

const int p = 10;
const void * vp = &p;
void *vp = &p; // error

もう一つの重要な点:非constオブジェクトのアドレスを、constオブジェクトへのポインタに代入することは許されます。

非constオブジェクトのアドレスをconstオブジェクトへのポインタに代入する:

const int *ptr;
int val = 3;
ptr = &val; // ok

ptrが指しているのが非constオブジェクトであっても、ptrポインタを通してvalの値を変更することはできません!

constオブジェクトへのポインタを使って基底のオブジェクトを変更することはできませんが、そのポインタが非constオブジェクトを指している場合、他の方法で指すオブジェクトを変更することは可能です。constポインタが指す値は変更できます——ただしconstオブジェクトポインタを通してはできないだけです!次のように変更します:

int *ptr1 = &val;
*ptr1=4;
cout<<*ptr<<endl;

小結:
1.定数へのポインタについては、ポインタを通してオブジェクトの値を変更することはできません。
2.void* ポインタでconstオブジェクトのアドレスを保持することはできず、const void* 型のポインタで保持しなければなりません。
3.非constオブジェクトのアドレスをconstオブジェクトへのポインタに代入することは許されます。ポインタが指すオブジェクトの値を変更するには、他の方法で変更しなければならず、現在のポインタを通して直接変更することはできません。

(2)定数ポインタ

constポインタは初期化しなければならず、constポインタが指す値は変更できますが、指す先は変更できません。

#include<iostream>
using namespace std;
int main(){
    int num=0, num1=1;
    int * const ptr=&num; // const指针必须初始化!且const指针的指向不能修改
    ptr = &num1; // error! const指针不能修改指向!
    cout<<*ptr<<endl;
}

このコードはコンパイルエラーになります:constポインタは指す先を変更できません。

#include<iostream>
using namespace std;
int main(){
    int num=0, num1=1;
    int * const ptr=&num; // const指针必须初始化!且const指针的指向不能修改
    *ptr = 1;
    cout<<*ptr<<endl;
}

このコードは何も起こらず、正常に1を出力します。

最後に、const定数のアドレスをptrに代入すると、ptrが指すのは変数であってconst定数ではないため、エラーが発生します:const int* -> int * のエラーです!

#include<iostream>
using namespace std;
int main(){
    const int num=0;
    int * const ptr=&num; // error! const int* -> int*
    cout<<*ptr<<endl;
}

上のコードを const int *ptr あるいは const int *const ptr に変えれば、どちらも正常に動作します!

(3)定数への定数ポインタ

前の2つの場合を理解すれば、次の場合は理解しやすいです:

const int p = 3;
const int * const ptr = &p; 

ptrはconstポインタであり、int型のconstオブジェクトを指しています。

6.関数でのconstの使用

constが関数の戻り値を修飾する

これはconstが普通の変数やポインタを修飾する場合と基本的に同じ意味です:

(1)const int

const int func1();

これ自体には意味がありません。戻り値はもともと他の変数に代入されるものだからです!

(2)const int*

const int* func2();

ポインタが指す内容は不変です。

(3)int *const

int *const func2();

ポインタ自体は不変です。

constが関数のパラメータを修飾する

(1)渡されたパラメータおよびポインタ自体は関数内で不変——意味がありません!

void func(const int var); // 传递过来的参数不可变
void func(int *const var); // 指针本身不可变

これはパラメータが関数本体内で変更できないことを示しますが、ここでは何の意味もありません。varはもともと仮引数であり、関数内で変更されることはありません。渡される仮引数がポインタの場合も同様です。

入力パラメータが「値渡し」を採る場合、関数は自動的にテンポラリ変数を生成してそのパラメータをコピーするため、その入力パラメータはもともと保護の必要がなく、constを付けるべきではありません。

(2)パラメータポインタが指す内容が定数であり不変

void StringCopy(char *dst, const char *src);

ここでsrcは入力パラメータ、dstは出力パラメータです。srcにconstを付けると、関数本体内の文がsrcの内容を変更しようとしたとき、コンパイラがエラーを指摘します。これがconstを付ける効果の一つです。

(3)パラメータが参照の場合。効率を高めると同時に変更を防ぐためです。

void func(const A &a)

非内部データ型のパラメータにとって、void func(A a) のように宣言された関数は効率が低くなる運命にあります。関数本体内でA型のテンポラリオブジェクトが生成されてパラメータaをコピーするからで、テンポラリオブジェクトの構築、コピー、破棄にはすべて時間がかかります。

効率を上げるには、関数宣言を void func(A &a) に変えればよいのです。「参照渡し」はパラメータの別名を借りるだけであり、テンポラリオブジェ クトを生成する必要がないからです。

しかし関数 void func(A &a) には欠点があります:

「参照渡し」ではパラメータaが変更される可能性があり、それは望まないことです。この問題の解決は簡単で、constを付ければよく、関数は最終的に void func(const A &a) になります。

同じ理屈で、効率向上のために void func(int x) を void func(const int &x) に書き換えるべきでしょうか?まったく必要ありません。内部データ型のパラメータには構築・破棄の過程がなく、コピーも非常に速く、「値渡し」と「参照渡し」の効率はほぼ同じだからです。

小結:
1.非内部データ型の入力パラメータについては、効率向上のために「値渡し」を「const参照渡し」に変えるべきです。例えば void func(A a) を void func(const A &a) に変えます。

2.内部データ型の入力パラメータについては、「値渡し」を「const参照渡し」に変えてはいけません。そうしないと効率向上の目的を達成できないばかりか、関数の分かりやすさも下がります。例えば void func(int x) は void func(const int &x) に変えるべきではありません。

以上で2つの面接の質問が解決されました:

  • 関数にポインタを渡す場合、そのポインタにconstを付ける必要があるか、constをポインタのどの位置に付けるかで何が違うのか;
  • 書いた関数のパラメータが複雑な型のインスタンスである場合、値パラメータと参照パラメータの違いは何か、いつ渡された参照パラメータにconstを付ける必要があるのか。

7.クラスでのconstの使用

クラスでは、データメンバーを変更しない関数はすべてconst型として宣言すべきです。constメンバー関数を書くときに誤ってデータメンバーを変更したり、他の非constメンバー関数を呼び出したりすると、コンパイラがエラーを指摘してくれます。これは間違いなくプログラムの堅牢性を高めます。

constキーワードで指定されたメンバー関数は、定数メンバー関数と呼ばれます。定数メンバー関数だけが定数や定数オブジェクトを操作する資格を持ち、constキーワードで指定されていないメンバー関数は定数オブジェクトの操作に使えません。

クラスのconstメンバー変数は、以下に示すように初期化リストで初期化しなければなりません:

class Apple{
private:
    int people[100];
public:
    Apple(int i); 
    const int apple_number;
};

Apple::Apple(int i):apple_number(i)
{

}

constオブジェクトはconstメンバー関数にしかアクセスできず、非constオブジェクトは任意のメンバー関数(constメンバー関数を含む)にアクセスできます。

例えば:

// apple.cpp
class Apple
{
private:
    int people[100];
public:
    Apple(int i); 
    const int apple_number;
    void take(int num) const;
    int add();
    int add(int num) const;
    int getCount() const;

};
// apple.cpp
Apple::Apple(int i) : apple_number(i)
{
}
int Apple::add(int num)
{
    take(num);
    return 0;
}
int Apple::add(int num) const
{
    take(num);
    return 0;
}
void Apple::take(int num) const
{
    std::cout << "take func " << num << std::endl;
}
int Apple::getCount() const
{
    take(1);
    //    add(); // error
    return apple_number;
}
int main()
{
    Apple a(2);
    cout << a.getCount() << endl;
    a.add(10);
    const Apple b(3);
    b.add(100);
    return 0;
}
// main.cpp

コンパイル:bazel run basic_content/const/class_const/first_example:main

このときエラーが報告されます:getCount() はconstメンバー関数であり、その中の add() のオーバーロード解決はconst版(int add(int num) const)にしか一致しません;このconst版をコメントアウトして初めて、非constの add() を呼び出したことによるエラーになります。すなわち:constオブジェクトはconstメンバー関数にしかアクセスできず、非constオブジェクトは任意のメンバー関数にアクセスできます(issue #338 を参照)。

呼び出しを次のように変えると:

const Apple b(3);
b.add(); // error

このとき、constオブジェクトはconstメンバー関数にしかアクセスできないことが証明できます。

上記のように初期化リストでconst定数を初期化する以外に、次の方法を使うこともできます:

第一に:定数の定義をstaticと組み合わせます。つまり:

static const int apple_number

第二に:外で初期化します:

const int Apple::apple_number=10;

もちろん、C++11でコンパイルするなら、定義の時点で直接初期化でき、次のように書けます:

static const int apple_number=10;
// 或者
const int apple_number=10;

この2つはどちらもC++11でサポートされています!

コンパイル時に -std=c++11 を付けるだけです!

ここでstaticという言葉が出てきたので、簡単に触れておきます:

C++では、static静的メンバー変数はクラスの内部では初期化できません。クラスの内部では宣言のみであり、定義はクラス定義体の外で行わなければならず、通常はクラスの実装ファイルで初期化します。

クラス内で宣言:

static int ap;

クラスの実装ファイルで使用:

int Apple::ap=666

この項目については、C++11では宣言と初期化を同時に行うことはできず、上記の使い方になります。

よくある質問

const ポインタとポインタ const の違いは?

const int* p は「定数へのポインタ」で指す先の値を変更できません。int* const p は「ポインタ自体が定数」で再代入できません。* の左の const は値を、右の const はポインタを保護します。

const オブジェクトは非 const メンバ関数を呼べる?

呼べません。const オブジェクトが呼べるのは const メンバ関数だけです。非 const オブジェクトは両方を呼べます。

定数は const と #define のどちらで定義すべき?

const を推奨します。型検査があり、スコープに従い、デバッグ可能です。#define は型情報のない前処理の文字置換です。

関連トピック

コメント