関数間でデータをやり取りする際、配列のような連続したデータを渡すことは基本的な操作です。しかし、C言語から続く伝統的な「Cスタイル配列」の受け渡しには、C++特有の「ポインタへの縮退」という重要なルールが存在します。
この記事では、Cスタイル配列の関数への渡し方と、それが持つ問題点、そして現代のC++で推奨される安全な代替手段について解説します。
Cスタイル配列の関数渡し:ポインタへの「縮退」
C++において、Cスタイル配列(例:int scores[10])を関数の引数として渡そうとすると、配列は「その配列の先頭要素へのポインタ」へと**縮退(decay)**します。
これは、関数が受け取るのは配列全体ではなく、配列の先頭アドレス(int*)のみであることを意味します。
// int arr[] という表記は、
// int* arr というポインタ表記と完全に等価です
void function_a(int arr[]) { /* ... */ }
void function_b(int* arr) { /* ... */ }
この仕様のため、関数は渡されたポインタだけでは「配列の要素がいくつあるか(サイズ)」を知ることができません。したがって、Cスタイル配列を関数に渡す際は、その要素数を別の引数として別途渡す必要があります。
配列の要素を変更する関数
ポインタ(アドレス)が渡されるため、関数内でそのポインタを介して行われた変更は、呼び出し元のオリジナルの配列に直接反映されます。
#include <iostream>
#include <cstddef> // size_t のために必要
/**
* @brief 全てのスコアにボーナス点を加算する (Cスタイル)
* @param scores (in/out) スコアが格納された配列 (へのポインタ)
* @param size (in) 配列の要素数
* @param bonus (in) 加算するボーナス点
*/
void applyBonus(int scores[], size_t size, int bonus) {
// scores[] は int* scores と同じ
// 呼び出し元の配列を直接変更している
for (size_t i = 0; i < size; ++i) {
scores[i] = scores[i] + bonus;
}
}
int main() {
constexpr size_t studentCount = 4;
int testScores[studentCount] = {75, 82, 66, 91};
std::cout << "--- 処理前 ---\n";
for (size_t i = 0; i < studentCount; ++i) {
std::cout << (i + 1) << "番: " << testScores[i] << "\n";
}
// 配列 'testScores' とそのサイズ 'studentCount' を渡す
applyBonus(testScores, studentCount, 5); // 5点のボーナス
std::cout << "\n--- 処理後 (5点加算) ---\n";
for (size_t i = 0; i < studentCount; ++i) {
// 呼び出し元の配列 testScores の値が変更されている
std::cout << (i + 1) << "番: " << testScores[i] << "\n";
}
return 0;
}
const による読み取り専用のCスタイル渡し
関数が配列の要素を参照するだけで、書き換えるべきでない場合、仮引数に const を付けることが強く推奨されます。
const int arr[](または const int* arr)と宣言することで、その関数内で配列の要素を変更しようとするとコンパイルエラーが発生し、意図しない変更(副作用)を未然に防ぐことができます。
#include <iostream>
#include <iomanip> // std::setprecision のために必要
#include <numeric> // std::accumulate のために必要
#include <cstddef>
/**
* @brief スコアの平均値を計算する (Cスタイル, 読み取り専用)
* @param scores (in) スコアが格納された配列 (constポインタ)
* @param size (in) 配列の要素数
* @return 平均値
*/
double calculateAverage(const int scores[], size_t size) {
// 'const' が付いているため、以下のような変更はコンパイルエラーになる
// scores[0] = 100; // エラー: read-only variable is not assignable
if (size == 0) {
return 0.0;
}
long long sum = 0;
// (参考) C++の <numeric> ヘッダのアルゴリズム
// sum = std::accumulate(scores, scores + size, 0LL);
// Cスタイルでの手動ループ
for (size_t i = 0; i < size; ++i) {
sum += scores[i];
}
// 整数除算を避けるため double にキャスト
return static_cast<double>(sum) / size;
}
int main() {
constexpr size_t studentCount = 4;
int testScores[studentCount] = {80, 85, 70, 95};
double avg = calculateAverage(testScores, studentCount);
std::cout << std::fixed << std::setprecision(2);
std::cout << "平均点: " << avg << "点\n"; // 82.50点
return 0;
}
Cスタイル渡しの問題点
この伝統的な方法は、C++プログラミングにおいて多くの問題を引き起こします。
- 情報の分離: 配列(ポインタ)とサイズ(
int)という、本来一体であるべき情報が2つの別々の引数に分離しています。 - エラーの温床:
applyBonus(testScores, 3)のように、呼び出し側が誤ったサイズ(4ではなく3など)を渡す可能性があり、コンパイラはそれを検出できません。 - 危険性: 誤ったサイズを渡すと、配列の範囲外のメモリを読み書きしてしまい(バッファオーバーラン)、プログラムのクラッシュや深刻なセキュリティ脆弱性の原因となります。
現代C++の解決策
現代のC++では、これらの問題を解決するため、Cスタイル配列の生のポインタ渡しを避け、コンテナクラスを使用することが強く推奨されます。
解決策1:std::vector(可変長配列)
std::vector は、サイズが実行時に変更可能な、最も一般的で柔軟なコンテナです。std::vector オブジェクトは自身のサイズを内部で保持しているため、サイズを別途渡す必要がありません。
- 変更する場合:
std::vector<int>&(参照)で渡します。 - 読み取り専用の場合:
const std::vector<int>&(const参照)で渡します。
#include <vector>
#include <iostream>
// --- vector を変更する関数 ---
// 引数が1つになり、サイズ情報は .size() で取得
void applyBonus(std::vector<int>& scores, int bonus) {
// C++11 範囲ベースforループで安全に走査
for (int& score : scores) {
score += bonus;
}
}
// --- vector を読み取る関数 ---
double calculateAverage(const std::vector<int>& scores) {
if (scores.empty()) { // .empty() で0件チェック
return 0.0;
}
long long sum = 0;
for (int score : scores) {
sum += score;
}
return static_cast<double>(sum) / scores.size(); // .size() で要素数を取得
}
int main() {
std::vector<int> testScores = {75, 82, 66, 91};
applyBonus(testScores, 5);
double avg = calculateAverage(testScores);
std::cout << "平均点 (ボーナス後): " << avg << "点\n"; // 86.00点
}
解決策2:std::array(固定長配列)
std::array は、コンパイル時にサイズが決定している固定長の配列を扱います。Cスタイル配列の安全性とパフォーマンスを両立させたものです。
#include <array>
#include <iostream>
// N はコンパイル時の定数
template <size_t N>
void applyBonus(std::array<int, N>& scores, int bonus) {
for (int& score : scores) {
score += bonus;
}
}
(std::array はサイズが型情報に含まれるため、std::array<int, 4> と std::array<int, 5> は異なる型として扱われます)
多次元配列の関数渡し(Cスタイル)
Cスタイルの多次元配列(int grid[3][4] など)を渡す場合、さらに複雑な制約が発生します。関数は、配列の先頭を除くすべての次元のサイズを固定で知る必要があります。
#include <iostream>
// 2次元目のサイズ (列数) は固定で '4' でなければならない
void printGrid(int grid[][4], int rows) {
for (int i = 0; i < rows; ++i) {
for (int j = 0; j < 4; ++j) {
std::cout << grid[i][j] << "\t";
}
std::cout << "\n";
}
}
int main() {
int myGrid[2][4] = {
{10, 20, 30, 40},
{50, 60, 70, 80}
};
// 1次元目のサイズ (行数) '2' は別途渡す
printGrid(myGrid, 2);
}
これは、grid[i+1] のアドレスを計算するために、コンパイラが1行あたりのサイズ(この場合は sizeof(int) * 4)を知る必要があるためです。この制約は非常に柔軟性に欠けます。
現代C++では、std::vector<std::vector<int>> を使うか、1次元の std::vector で2次元データを扱うラッパー(idx = y * width + x)を作成するのが一般的です。
まとめ(総括)
- Cスタイル配列を関数に渡すと、ポインタへの縮退が発生し、サイズ情報が失われます。
- そのため、
void func(int arr[], int size)のように、サイズを別途渡す必要があり、これはエラーの温床です。 - 関数内で配列を変更しない場合は、
const(const int arr[])を付けて安全性を高めるべきです。 - 現代のC++では、Cスタイル配列の受け渡しは推奨されません。
- 代わりに、自身のサイズを管理できる
std::vectorや、型でサイズを保証するstd::arrayを使用してください。 - 読み取り専用の場合は、
const std::vector<int>&のように const参照 で渡すのが、最も安全で効率的な標準的アプローチです。
