【C言語/C++】 配列は戻り値にできない【配列を適切に返す方法】

C言語やC++では、配列を関数の戻り値として返すことができません。

// error: Function cannot return array type 'int [3]'
// error: Brackets are not allowed here; to declare an array, place the brackets after the identifier
int [3] getDate() { return {2016, 2, 29}; }

// error: Function cannot return array type 'int [3]'
int getDate()[3] {
  int a[3] = {2017, 11, 28};
  return a; // error: Cannot initialize return object of type 'int' with an lvalue of type 'int [3]'
            // error: Incompatible pointer to integer conversion returning 'int [3]' from a function with result type 'int' [-Werror,-Wint-conversion]
}

本記事では、配列を返すための6つの代替案を紹介します。

固定長配列を返す場合は「配列のポインタ渡し」の利用が適切です。可変長配列や定数型の配列を返す場合は「ポインタとして返す」方法が利用できます。

配列のポインタ渡し

もっともメジャーな方法です。既定の配列を引数として受け取り、受け取った配列の値を書き換えます。

void getDateArray(int date[3]) {
  date[0] = 2016;
  date[1] = 2;
  date[2] = 29;
}

呼び出し側では、事前に固定長配列を宣言しておく必要があります。

int main() {
  int a[3];        // 配列を自前で用意する
  getDateArray(a); // ポインタ渡し
  printf("%d年%d月%d日", a[0], a[1], a[2]); // 2016年2月29日
}
配列はポインタと等価関係にあるため、呼び出し元の配列aの要素はきちんと書き換わります。

配列の長さが変動する場合

配列のサイズが動的に決まるようなケースでは、配列とそのサイズを一緒に引数として渡すような設計を用いることが一般的です。

/* 受け取った配列の要素を全てゼロ埋めする関数 */
char *zeroing(char *array, size_t size) {
  for (size_t i = 0; i < size; i++)
    array[i] = '\0'; // ゼロ埋め
  return array;
}
char s[3];
zeroing(s, 3); // 配列と配列のサイズを一緒に渡す
assert( 0 == (s[0] | s[1] | s[2]) ); // 配列内の値は全てゼロ(健康志向)

/* 同等の設計はmemset関数などの標準ライブラリ関数にも見られる */
// void *memset(void *string, int character, size_t length);
puts( memset(s, '9', 2) ); // "99"

仮引数の宣言方法については、char array[]と宣言してもchar *arrayと宣言しても、内部的には同じポインタ型として解釈されます。

上記のzeroing関数やmemset関数のように、受け取った引数を戻り値として返すような設計を用いれば、puts(zeroing(s, 3))という形で、変更後の配列をすぐさま別の関数に受け渡すようなテクニックも実現可能となります。

ポインタとして返す

配列が定数値の場合

定数値として宣言された配列であれば、直接ポインタとして返すことができます。

// 定数
const int referenceDate[] = {2001, 1, 1};
const int *getReferenceDate() {
  return referenceDate;
}

静的変数を返すこともできます。

const int *getReferenceDate() {
  // 静的変数
  static const int referenceDate[] = {2001, 1, 1};
  return referenceDate;
}

用途によっては、定数宣言されていないグローバル変数や静的オブジェクトの配列を返すこともできます。

char *ctoa(char c) {
  static char str[2] = {'\0', '\0'};
  str[0] = c;
  return str;
}
この手のコードはマルチスレッドに対応できないため、利用用途が限られます。

注意点

ローカル変数を戻り値として利用しないようにしてください。

int *getDate() {
  int date[3] = {2016, 2, 29};
  
  // 警告: Address of stack memory associated with local variable 'date' returned
  return date;
  
  // Clang(C言語コンパイラ)の場合は警告されないが危険
  return (int []){2016, 2, 29};
  // Clang++(C++コンパイラ)の場合は警告される
  // 警告(Clang++): Returning address of local temporary object
  // 警告(Clang++): Pointer is initialized by a temporary array, which will be destroyed at the end of the full-expression
}

date配列はgetDate関数内部でのみ有効です。受け取り側では無効な値になるため注意してください。

また複合リテラル(int []){2016, 2, 29}は自動変数dateと同等の記憶領域・記憶期間を持つため、date配列と同等の問題を引き起こします。上記のコードはコンパイラによっては警告がされないため注意が必要です。

参考: 複合リテラル【生存期間や注意点について】

メモリの動的確保を行う方法

配列の代わりに動的確保されたポインタを返す方法もあります。

int *getDatePointer() {
  int *date = malloc(sizeof(int) * 3);
  date[0] = 2016;
  date[1] = 2;
  date[2] = 29;
  return date;
}

受け取ったポインタは最後に解放する必要があります。

int main() {
  int *date = getDatePointer();
  printf("%d年%d月%d日", date[0], date[1], date[2]);
  free(date); // 解放
}
こちらの方法は利用用途が限られます。関数の結果が可変長配列/動的配列になるような場合に使うと良いでしょう。

構造体を利用する

ポインタを用いない方法としては、構造体の利用が考えられます。

typedef struct { int year, month, day; } Date;
Date getDateSet() { return (Date){2016, 2, 29}; }

int main() {
  Date date = getDateSet();
  printf("%d年%d月%d日", date.year, date.month, date.day);
}

わざわざ配列形式にする必要のないようなデータに関しては、構造体でやり取りしたほうが色々と便利です。

配列型メンバ変数を活用する

配列そのものを構造体でラップする方法もあります。

typedef struct { int array[3]; } DateArray;
DateArray getDateArray() { return (DateArray){{2016, 2, 29}}; }

int main() {
  DateArray date = getDateArray();
  printf("%d年%d月%d日", date.array[0], date.array[1], date.array[2]);
  
  int *a = date.array;
  printf("%d年%d月%d日", a[0], a[1], a[2]);
  
  // `b`の有効期限は一時オブジェクトの寿命に依存するため危険
  int *b = getDateArray().array; // warning: Pointer is initialized by a temporary array, which will be destroyed at the end of the full-expression
  printf("%d年%d月%d日", b[0], b[1], b[2]); // 未定義動作
}

std::arrayを利用する(C++)

C++の場合は、固定長配列クラスstd::arrayを利用することが一般的です。

#include <array> // std::array

std::array<int, 3> getDate() {
  return {2017, 6, 28};
}

auto main() -> int {
  std::array<int, 3> date = getDate();
  printf("%d年%d月%d日", date[0], date[1], date[2]);
}

可変長配列の場合は、std::vectorを利用します。

std::vector<int> date = {2017, 6};
date.push_back(28);
printf("%d年%d月%d日", date[0], date[1], date[2]);

配列の参照を返す

あまり使われない方法ですが、豆知識として配列の参照を返す方法も紹介しておきます。

const int (*getDateRef())[3] {
  static const int date[] = {2017, 1, 8};
  return &date;
}
const int *p = *getDateRef(); // (int *){2017, 1, 8}
printf("%d年%d月", **getDateRef(), p[1]); // 2017年1月
printf("%ld", sizeof(*getDateRef()));    // 12

このように、戻り値に含まれる配列のサイズ12を取得することができるという特徴があります。

ちなみにこの場合のgetDateRefは「配列を指したポインタを返す関数」です。戻り値の型は「配列へのポインタ(int (*pa)[3])」です。これは「ポインタの配列(int *ap[3])」とは真逆の性質を持ったものであり、感覚的には「ポインタのポインタ(int **dp)」(ダブルポインタ)に近いものだと思ってもらうとよいでしょう。

広告