C++における演算子は、プログラム内での計算、比較、論理判断、さらにはメモリ操作など、あらゆる処理の基盤となる非常に重要な要素です。

C++はC言語の演算子を継承しつつ、オブジェクト指向やメタプログラミングに対応するための強力な演算子や、C++20で導入された三方向比較演算子など、多種多様な機能を備えています。

本記事では、C++で利用可能なすべての演算子の種類、演算の優先順位と結合則、そしてC++の大きな特徴の一つである演算子オーバーロードについて、初心者から上級者まで役立つよう詳細に解説します。

演算子の正しい理解は、バグの少ない効率的なコードを記述するための第一歩となります。

C++の演算子:基本カテゴリと一覧

C++の演算子は、その役割に応じていくつかのカテゴリに分類されます。

まずは、日常的なプログラミングで頻繁に使用される基本的な演算子から確認していきましょう。

算術演算子

算術演算子は、数値の計算を行うための演算子です。

これらは数学的な四則演算をカバーしており、整数や浮動小数点数に対して使用されます。

演算子名称説明
+加算a + baとbの和を求める
-減算a - baからbを引いた差を求める
*乗算a * baとbの積を求める
/除算a / baをbで割った商を求める
%剰余a % baをbで割った余りを求める

除算において、整数同士の計算を行った場合は結果も整数となり、小数点以下は切り捨てられる点に注意が必要です。

例えば、5 / 2 の結果は 2.5 ではなく 2 になります。

正確な値を得るには、少なくとも一方の数値を 5.0 / 2 のように浮動小数点数として扱う必要があります。

代入演算子

代入演算子は、変数に値を格納するために使用されます。

単純な代入だけでなく、算術演算と代入を同時に行う複合代入演算子も存在します。

演算子名称等価な表現
=代入a = baにbの値を代入する
+=加算代入a += ba = a + b
-=減算代入a -= ba = a - b
*=乗算代入a *= ba = a * b
/=除算代入a /= ba = a / b
%=剰余代入a %= ba = a % b

複合代入演算子を使用すると、コードが簡潔になるだけでなく、特定の条件下ではコンパイラによる最適化が効きやすくなる場合があります。

比較演算子(関係演算子)

比較演算子は、2つの値を比較し、その結果を真(true)または偽(false)の bool 型で返します。

演算子名称説明
==等価a == baとbが等しければ真
!=不等a != baとbが等しくなければ真
<小なりa < baがbより小さければ真
>大なりa > baがbより大きければ真
<=以下a <= baがb以下であれば真
>=以上a >= baがb以上であれば真

特に === の書き間違いは、コンパイルエラーにならずに意図しない動作(バグ)を引き起こす典型的な原因であるため、注意深く記述する必要があります。

論理演算子

論理演算子は、複数の条件式を組み合わせるために使用されます。

演算子名称説明
&&論理積(AND)a && baとbの両方が真なら真
||論理和(OR)a || baまたはbのどちらかが真なら真
!論理否定(NOT)!aaが偽なら真、真なら偽

C++の論理演算子には短絡評価(ショートサーキット評価)という特性があります。

&& の場合、左辺が偽であれば右辺は評価されません。

|| の場合、左辺が真であれば右辺は評価されません。

これを利用して、ポインタのヌルチェックとメンバアクセスを一つの if 文で行うといったテクニックが頻用されます。

ビット演算子

ビット演算子は、数値をビット単位で直接操作するために使用されます。

ハードウェア制御やフラグ管理、最適化処理などで多用されます。

演算子名称説明
&ビット論理積a & b各ビットのANDをとる
|ビット論理和a | b各ビットのORをとる
^ビット排他的論理和a ^ b各ビットのXORをとる
~ビット反転~a各ビットを反転させる
<<左シフトa << naを左にnビットシフトする
>>右シフトa >> naを右にnビットシフトする

インクリメント・デクリメント演算子の詳細

変数の値を1増やしたり減らしたりするインクリメント(++)およびデクリメント(--)演算子には、記述する位置によって挙動が異なるという特徴があります。

前置と後置の違い

前置(++a)と後置(a++)は、どちらも最終的に変数の値を1増やしますが、式としての返り値が異なります。

  • 前置インクリメント:値を増やした後、増やした後の値を返します。
  • 後置インクリメント:値を増やす前の値を返し、その後に実際の値を増やします。
C++
#include <iostream>

int main() {
    int a = 5;
    int b = 5;

    // 前置インクリメント: 加算してから代入
    int res1 = ++a; 
    // 後置インクリメント: 代入してから加算
    int res2 = b++; 

    std::cout << "Prefix res: " << res1 << ", a: " << a << std::endl;
    std::cout << "Postfix res: " << res2 << ", b: " << b << std::endl;

    return 0;
}
実行結果
Prefix res: 6, a: 6
Postfix res: 5, b: 6

C++において、特にイテレータなどを操作する場合は、後置インクリメントは「古い値のコピー」を一時的に作成する必要があるため、パフォーマンスの観点から前置インクリメントの使用が推奨されます。

C++20 新機能:三方向比較演算子(宇宙船演算子)

現代的なC++において、比較の概念を大きく変えたのがC++20で導入された三方向比較演算子 <=> です。

その形状から「宇宙船演算子」とも呼ばれます。

この演算子は、2つの値を比較し、以下の3つの状態(またはそれ以上)を一つの戻り値で表します。

  1. 左辺が右辺より小さい
  2. 左辺と右辺が等しい
  3. 左辺が右辺より大きい

戻り値は整数ではなく、std::strong_orderingstd::partial_ordering といった型になります。

これらを 0 と比較することで、従来の比較演算子と同様の結果を得ることができます。

C++
#include <iostream>
#include <compare>

int main() {
    int x = 10;
    int y = 20;

    auto result = x <=> y;

    if (result < 0) {
        std::cout << "x is less than y" << std::endl;
    } else if (result == 0) {
        std::cout << "x is equal to y" << std::endl;
    } else {
        std::cout << "x is greater than y" << std::endl;
    }

    return 0;
}
実行結果
x is less than y

この演算子の最大のメリットは、ユーザー定義のクラスにおいて operator<=> を一つ定義し、default 指定するだけで、==, !=, <, <=, >, >=全6種類の比較演算子が自動的に生成される点にあります。

演算子の優先順位と結合則

複雑な式を書く際、どの演算子が先に評価されるかを知ることは不可欠です。

これを「優先順位」と呼びます。

また、同じ優先順位の演算子が並んだ場合に、左から右へ評価するか、右から左へ評価するかを「結合則」と呼びます。

演算子の優先順位表(主要抜粋)

以下の表は、優先順位が高い順(先に実行される順)に並んでいます。

順位演算子結合則
1:: (スコープ解決)左から右
2(), [], ., ->, 後置++, --左から右
3前置++, --, !, ~, 単項+, -, *(参照剥がし), &(アドレス), sizeof, new, delete右から左
4.*, ->*左から右
5*, /, %左から右
6+, -左から右
7<<, >>左から右
8<=>左から右
9<, <=, >, >=左から右
10==, !=左から右
11& (ビット論理積)左から右
12^ (ビットXOR)左から右
13| (ビット論理和)左から右
14&&左から右
15||左から右
16?: (三項演算子)右から左
17代入演算子 (=, += など)右から左
18, (カンマ演算子)左から右

優先順位に関する注意点

初心者が陥りやすいミスとして、比較演算子とビット演算子の混同があります。

例えば、a & b == c という式は、== の方が & よりも優先順位が高いため、a & (b == c) として評価されてしまいます。

意図した順序で計算を行わせるためには、括弧 ( ) を活用して優先順位を明示することが最も安全で確実な方法です。

演算子オーバーロードの基礎

C++の強力な機能の一つに、演算子オーバーロードがあります。

これは、ユーザーが定義したクラスや構造体に対して、既存の演算子の挙動を定義できる機能です。

例えば、ベクトルを扱う Vector2D クラスを作成した際、v1 + v2 と書くだけでベクトル同士の足し算ができるようになります。

演算子オーバーロードの構文

演算子をオーバーロードするには、operator キーワードに続けて、定義したい演算子を記述した関数を作成します。

C++
#include <iostream>

class Vector2D {
public:
    double x, y;

    Vector2D(double x = 0, double y = 0) : x(x), y(y) {}

    // + 演算子のオーバーロード
    Vector2D operator+(const Vector2D& other) const {
        return Vector2D(x + other.x, y + other.y);
    }

    void display() const {
        std::cout << "(" << x << ", " << y << ")" << std::endl;
    }
};

int main() {
    Vector2D v1(1.0, 2.0);
    Vector2D v2(3.0, 4.0);

    // 自然な形で加算ができる
    Vector2D v3 = v1 + v2;

    v3.display();

    return 0;
}
実行結果
(4, 6)

メンバ関数か非メンバ関数か

演算子オーバーロードには、「クラスのメンバ関数として定義する方法」と「クラス外の関数(しばしば friend 関数)として定義する方法」の2種類があります。

  1. メンバ関数
    左辺のオブジェクトがそのクラスのインスタンスである必要があります。
    例:obj1 + obj2
  2. 非メンバ関数
    左辺がクラスインスタンスでない場合(例:double + Vector2D)に必要です。また、左右のオペランドに対して対称的な処理を行いたい場合にも適しています。

オーバーロードできない演算子

すべての演算子がオーバーロードできるわけではありません。

以下の演算子はオーバーロードが禁止されています。

  • スコープ解決演算子 (::)
  • メンバアクセス演算子 (.)
  • メンバポインタアクセス演算子 (.*)
  • 三項演算子 (?:)
  • sizeof 演算子
  • typeid 演算子

また、新しい演算子(例:** など)を自作することはできません

既存の演算子のシンボルのみが対象です。

メモリ操作・ポインタ関連の演算子

C++はメモリを直接扱うことができる言語であるため、ポインタに関連する演算子も重要な役割を果たします。

アドレス演算子と間接参照演算子

演算子名称説明
&アドレス演算子変数が配置されているメモリ上のアドレスを取得する
*間接参照演算子ポインタが指し示しているアドレスの中身(値)を参照する

メンバアクセス演算子

演算子名称説明
.ドット演算子オブジェクトのメンバにアクセスする
->アロー演算子ポインタを介してオブジェクトのメンバにアクセスする

アロー演算子 p->m は、内部的には (*p).m と等価です。

動的メモリ管理演算子

C++では、実行時にメモリを確保・解放するために newdelete を使用します。

C++
int* p = new int(10); // int型のメモリを確保し10で初期化
// ... 処理 ...
delete p;             // メモリを解放

配列の場合は new[]delete[] を対応させて使用する必要があります。

これらを間違えるとメモリリークや未定義動作の原因となります。

現代のC++では、これらを直接使う代わりに std::unique_ptrstd::vector を使うことが推奨されています。

キャスト演算子

型変換を行うためのキャスト演算子も、C++ではより安全な方法が提供されています。

C言語スタイルのキャスト (type)value も使用可能ですが、意図せぬ変換を防ぐためにC++固有のキャスト演算子を使用するのが一般的です。

  1. static_cast
    コンパイル時に行われる安全な型変換(例:intからdouble)。
  2. dynamic_cast
    クラスの継承関係におけるダウンキャスト。実行時に型チェックが行われます。
  3. const_castconst 属性を取り除くためのキャスト。
  4. reinterpret_cast
    ビットレベルでの強制的な型解釈(ポインタから整数など)。

これらは演算子としての側面を持ち、プログラムの意図を明確にするために非常に有効です。

その他の特殊な演算子

C++には、他にも特定の目的で使用される演算子があります。

sizeof 演算子

型または変数のサイズをバイト単位で返します。

コンパイル時に決定される定数です。

C++
std::cout << "Size of int: " << sizeof(int) << " bytes" << std::endl;

三項演算子(条件演算子)

条件式 ? 真の場合の値 : 偽の場合の値 という形式で記述します。

if-else 文を簡潔に記述できる唯一の三項演算子です。

C++
int a = 10, b = 20;
int max = (a > b) ? a : b;

カンマ演算子

複数の式を一行に記述し、左から右へ評価します。

最終的な式の値は、一番右にある式の値となります。

主に for 文の初期化式や更新式で複数の変数を扱う際に使用されます。

演算子使用時のベストプラクティス

演算子を使いこなす上で、以下のポイントを意識すると、より堅牢なプログラムが作成できます。

  1. 可読性を優先する
    過度に複雑な式を一行に詰め込むのではなく、適宜変数に分割したり、括弧を使用して意図を明確にしましょう。
  2. 演算子オーバーロードの乱用を避ける
    演算子オーバーロードは便利ですが、本来の意味(+ なら加算)から逸脱した定義をすると、コードの可読性が著しく低下します。誰が見ても納得できる直感的な挙動を心がけましょう。
  3. 副作用に注意するa[i++] = i; のような式は、コンパイラによって評価順序が異なる可能性があり、未定義動作や意図しない結果を招くことがあります。一つの式で同じ変数に対して複数回の変更を加えるのは避けましょう。
  4. C++20以降は spaceship 演算子を検討する
    自作クラスで比較演算を実装する場合、個別に6つの演算子を書くよりも<=> を一つ定義する方が、保守性が高くミスも少なくなります。

まとめ

C++の演算子は、単純な計算から複雑なオブジェクト間の操作、メモリ制御まで、非常に多岐にわたる機能を担っています。

本記事では、基本的な算術・代入・比較演算子から、C++の強力な機能である演算子オーバーロード、さらには最新のC++20で導入された三方向比較演算子までを網羅的に解説しました。

演算子の優先順位や結合則を正しく理解し、適切に括弧を用いることで、論理的なミスを大幅に減らすことができます。

また、演算子オーバーロードを適切に活用することで、ユーザー定義型を組み込み型と同じように自然に扱えるようになり、表現力の高いコードを記述することが可能になります。

この記事をリファレンスとして活用し、C++の持つ演算子の真の力を引き出してください。