cpp-reference

C++ 参照渡し

概要

C++における参照渡し(Pass by Reference)に関する知識とメモ

基本概念

値渡し(Pass by Value)との違い

値渡し(Pass by Value):

  • 関数に引数を渡す際、値のコピーが作成される
  • 関数内で引数を変更しても、呼び出し元の変数には影響しない
  • オブジェクトが大きい場合、コピーのオーバーヘッドが発生
void increment(int x) {
    x++;  // コピーを変更しているだけ
}

int main() {
    int a = 5;
    increment(a);
    // a は依然として 5
}

参照渡し(Pass by Reference):

  • 関数に引数を渡す際、変数への参照(エイリアス)を渡す
  • 関数内で引数を変更すると、呼び出し元の変数も変更される
  • コピーが発生しないため、効率的
void increment(int& x) {
    x++;  // 元の変数を直接変更
}

int main() {
    int a = 5;
    increment(a);
    // a は 6 になる
}

参照とは

参照(Reference)の特徴:

  • 既存の変数に対する別名(エイリアス)
  • 宣言時に必ず初期化が必要
  • 一度初期化すると、別の変数を参照することはできない
  • ポインタと異なり、nullにはできない
int a = 10;
int& ref = a;  // refはaの別名

ref = 20;      // aも20になる
std::cout << a;  // 出力: 20

参照渡しの使い方

基本的な構文

// 参照渡しの関数定義
void swap(int& a, int& b) {
    int temp = a;
    a = b;
    b = temp;
}

int main() {
    int x = 10, y = 20;
    swap(x, y);
    // x = 20, y = 10
}

const 参照(定数参照)

読み取り専用の参照渡し:

  • 関数内で値を変更しない場合に使用
  • コピーを避けつつ、意図しない変更を防ぐ
  • 大きなオブジェクトを効率的に渡せる
// const参照を使った効率的な関数
void printVector(const std::vector<int>& vec) {
    for (int i : vec) {
        std::cout << i << " ";
    }
    // vec.push_back(10);  // エラー: const参照なので変更不可
}

int main() {
    std::vector<int> numbers = {1, 2, 3, 4, 5};
    printVector(numbers);  // コピーなしで渡せる
}

const参照の重要性:

// 悪い例: 大きなオブジェクトを値渡し
void processData(std::string data) {  // コピーが発生
    std::cout << data;
}

// 良い例: const参照で渡す
void processData(const std::string& data) {  // コピーなし
    std::cout << data;
}

参照を返す関数

class Container {
private:
    int value;
public:
    Container(int v) : value(v) {}
    
    // 参照を返す(変更可能)
    int& getValue() {
        return value;
    }
    
    // const参照を返す(読み取り専用)
    const int& getValue() const {
        return value;
    }
};

int main() {
    Container c(10);
    c.getValue() = 20;  // 直接代入可能
    std::cout << c.getValue();  // 出力: 20
}

ポインタとの比較

参照 vs ポインタ

特徴参照ポインタ
初期化必須任意
null不可可能
再代入不可可能
アドレス演算不可可能
構文シンプル*-> が必要
// 参照を使った場合
void incrementRef(int& x) {
    x++;
}

// ポインタを使った場合
void incrementPtr(int* x) {
    if (x != nullptr) {
        (*x)++;
    }
}

int main() {
    int a = 5;
    
    incrementRef(a);     // シンプルな呼び出し
    incrementPtr(&a);    // アドレスを渡す必要がある
}

どちらを使うべきか

参照を使う場合:

  • 引数が必ず存在することが保証される
  • nullチェックが不要
  • シンプルな構文を好む場合

ポインタを使う場合:

  • 引数がnullの可能性がある
  • 動的にアドレスを変更する必要がある
  • 配列を扱う場合

呼び出し時に & をつけるケース(アドレス演算子)

関数定義では&を使っていないのに、呼び出し時に&をつけるケースがあります。 これはポインタを引数として受け取る関数を呼び出す場合です。

重要な区別:

  • 参照の&(関数定義側): 型の一部として使用(例: int&
  • アドレス演算子の&(呼び出し側): 変数のメモリアドレスを取得
// ポインタを受け取る関数(定義時に&は使っていない)
void increment(int* ptr) {
    if (ptr != nullptr) {
        (*ptr)++;
    }
}

int main() {
    int value = 10;
    
    // 呼び出し時に&をつけてアドレスを渡す
    increment(&value);  // &はアドレス演算子
    
    // valueは11になる
    std::cout << value;  // 出力: 11
}

参照渡しとの比較:

// 参照渡し(定義時に&を使う)
void incrementByRef(int& x) {
    x++;
}

// ポインタ渡し(定義時に*を使う)
void incrementByPtr(int* x) {
    (*x)++;
}

int main() {
    int a = 5;
    int b = 5;
    
    incrementByRef(a);   // 呼び出し時に&は不要
    incrementByPtr(&b);  // 呼び出し時に&が必要(アドレスを渡す)
    
    // どちらも結果は同じ(a = 6, b = 6)
}

なぜ呼び出し時に&が必要なのか:

関数定義呼び出し側理由
void func(int& x)func(a)参照は自動的にバインドされる
void func(int* x)func(&a)ポインタには明示的にアドレスを渡す必要がある

実用的な例:

// C言語スタイルのAPI(ポインタを使用)
bool parseInteger(const char* str, int* result) {
    // 文字列を整数に変換し、成功したらtrueを返す
    *result = std::atoi(str);
    return true;
}

// scanf系の関数
int value;
scanf("%d", &value);  // &で変数のアドレスを渡す

int main() {
    int number;
    if (parseInteger("42", &number)) {  // &でアドレスを渡す
        std::cout << number;  // 出力: 42
    }
}

呼び出し側から見た違い(可読性の観点):

int x = 10;

// 参照渡し: 呼び出し側では変更されるかどうかが分かりにくい
modifyByRef(x);   // xが変更される可能性があるが、見た目では分からない

// ポインタ渡し: &があることで「変更される可能性」が明示的
modifyByPtr(&x);  // &があるので、xが変更されるかもしれないと分かる

この可読性の違いから、一部のコーディング規約では「出力パラメータにはポインタを使う」というルールを採用しています(例:Google C++ Style Guide)。

よくある使用パターン

パターン1: 複数の値を返す

// 参照を使って複数の値を返す
void divideAndRemainder(int dividend, int divisor, 
                        int& quotient, int& remainder) {
    quotient = dividend / divisor;
    remainder = dividend % divisor;
}

int main() {
    int q, r;
    divideAndRemainder(17, 5, q, r);
    // q = 3, r = 2
}

パターン2: 大きなオブジェクトの効率的な受け渡し

struct LargeData {
    std::vector<double> data;
    std::string metadata;
    // ... 多くのメンバー
};

// 値渡し(非効率)
void processData_bad(LargeData data) {
    // 全データがコピーされる
}

// const参照渡し(効率的)
void processData_good(const LargeData& data) {
    // コピーなし、かつ変更も防止
}

パターン3: メソッドチェーン

class Builder {
private:
    std::string result;
public:
    // 自身への参照を返すことでチェーンが可能
    Builder& append(const std::string& str) {
        result += str;
        return *this;
    }
    
    Builder& appendLine(const std::string& str) {
        result += str + "\n";
        return *this;
    }
    
    std::string build() const {
        return result;
    }
};

int main() {
    std::string result = Builder()
        .append("Hello, ")
        .append("World!")
        .appendLine("")
        .appendLine("This is a test.")
        .build();
}

パターン4: 範囲ベースforループでの参照

std::vector<int> numbers = {1, 2, 3, 4, 5};

// 値のコピー(読み取りのみ、変更しても元に影響なし)
for (int n : numbers) {
    n *= 2;  // 元の配列は変更されない
}

// 参照(直接変更可能)
for (int& n : numbers) {
    n *= 2;  // 元の配列が変更される
}

// const参照(読み取り専用、効率的)
for (const int& n : numbers) {
    std::cout << n << " ";
}

注意点とベストプラクティス

ダングリング参照を避ける

// 危険: ローカル変数への参照を返す
int& badFunction() {
    int localVar = 10;
    return localVar;  // 警告: ローカル変数への参照を返している
}  // localVarはスコープを抜けると破棄される

// 安全: クラスメンバーへの参照を返す
class Safe {
    int member;
public:
    int& getMember() { return member; }  // OK: memberはオブジェクトと共存
};

ベストプラクティスまとめ

  1. 読み取り専用ならconst参照を使う

    void print(const std::string& str);  // 推奨
  2. 小さな型は値渡しでOK

    void process(int value);     // int, char, boolなどは値渡しで十分
    void process(double value);  // doubleも値渡しでOK
  3. 変更が必要なら参照渡し

    void modify(std::vector<int>& vec);  // 変更が必要な場合
  4. nullの可能性がある場合はポインタを使う

    void process(Widget* widget);  // nullチェックが必要な場合
  5. 参照を返す場合は寿命に注意

    // メンバー変数やstatic変数への参照は安全
    // ローカル変数への参照は危険

C++11以降の機能

右辺値参照(Rvalue Reference)

// 右辺値参照(ムーブセマンティクス用)
void process(std::string&& str) {
    // strは一時オブジェクト(右辺値)からムーブ可能
    std::string local = std::move(str);
}

int main() {
    std::string temp = "Hello";
    process(std::move(temp));  // tempをムーブ
    // tempは「有効だが内容は未規定な状態」になる(内容が空かどうかなどに依存しないこと)
}

ユニバーサル参照(Forwarding Reference)

template<typename T>
void wrapper(T&& arg) {
    // 完全転送(Perfect Forwarding)
    someFunction(std::forward<T>(arg));
}

参考リンク